# Rail Replacement BRR App - MVP Specification ## Overview A Flutter mobile app for rail replacement controllers to generate and fill Bus Running Reports (BRRs) from schedule documents. Eliminates manual data entry by parsing schedule files and providing a streamlined interface for completing required fields. **Note:** MVP is being developed and tested using Arriva schedule format, but the app should be designed to support multiple Train Operating Companies (TOCs) in future iterations. ## Core Problem Currently, controllers must manually transcribe trip data from schedule documents into BRR spreadsheets - a time-consuming, error-prone process that can involve 40-50+ trips per shift. ## MVP Features ### 1. Schedule Upload & Parsing **Functionality:** - Upload schedule documents (.docx) - Parse schedule to extract: - Trip numbers - Duty numbers - Running numbers - Bus fleet numbers (format varies by TOC) - Scheduled departure times - Route information **Technical Requirements:** - File picker for local document selection - Docx parsing library (e.g., `docx_to_text` or native parser) - Regex pattern matching for schedule format - Parser should be modular to support multiple TOC formats in future - Error handling for invalid/unparseable files **MVP Implementation:** - Initial parser built for Arriva schedule format - Architecture allows for additional parsers to be added later **User Flow:** 1. User taps "Upload Schedule" 2. Select .docx file from device 3. App parses and displays trip count 4. User confirms or re-uploads if incorrect ## Schedule Parsing Implementation ### Understanding Arriva Schedule Format #### Document Structure Arriva schedules are .docx files with a specific text-based layout: 1. **Header Section** - Route information and metadata 2. **Column Headers** - Station/timing point names 3. **Trip Lines** - Individual service data 4. **Duty Scheduling Section** - Bus allocation information #### Example Trip Line (Outbound from Uxbridge) ``` 1531_1532 1540 1542 1545 1549 1552 1557 1604 1613 1618 1624 1624 EC518 518 518 112 1635 115 ``` Breaking this down: - `1531_1532` - Departure window (1531 = actual departure from Uxbridge) - `1540 1542 1545...` - Timing points along route - `1618 1624 1624` - Arrival times at destination - `EC518` - Bus fleet number - `518` - Duty number - `518` - Running number - `112` - Trip number (current) - `1635` - Next scheduled time - `115` - Next trip number #### Example Trip Line (Finishing Trip) ``` 1521_1522 1530 1532 1535 1539 1542 1547 1554 1603 1608 1614______FINISH AT POINT EC509 509 F 110 ``` Key differences: - `______FINISH AT POINT` - Indicates service terminates - `F` flag instead of running number - `110` - Trip number (no next trip) ### Parsing Strategy #### Step 1: Extract Raw Text from DOCX ```dart import 'package:docx_to_text/docx_to_text.dart'; Future extractTextFromDocx(String filePath) async { try { final bytes = await File(filePath).readAsBytes(); final text = docxToText(bytes); return text; } catch (e) { throw ScheduleParseException('Failed to read document: $e'); } } ``` #### Step 2: Identify Schedule Section The schedule contains multiple sections. We need the section with trip data, which: - Contains lines starting with time patterns (HHMM_HHMM) - Has bus fleet numbers (EC###) - Contains trip numbers ```dart List extractTripLines(String fullText) { final lines = fullText.split('\n'); final tripLines = []; for (final line in lines) { // Look for lines that start with time pattern and contain EC bus numbers if (RegExp(r'^\d{4}_\d{4}').hasMatch(line) && line.contains('EC')) { tripLines.add(line.trim()); } } return tripLines; } ``` #### Step 3: Parse Individual Trip Lines **Pattern 1: Regular Trips** ```dart RegExp regularTripPattern = RegExp( r'^(\d{4})_(\d{4})\s+.*\s+(EC\d+)\s+(\d+)\s+([NRF]?)(\d+)\s+(\d+)\s+(\d{4})\s+(\d+)' ); Trip? parseRegularTrip(String line) { final match = regularTripPattern.firstMatch(line); if (match == null) return null; return Trip( scheduledTime: formatTime(match.group(1)!), // 1531 -> "15:31" tripNumber: match.group(7)!, // 112 dutyNumber: match.group(4)!, // 518 runningNumber: match.group(6)!, // 518 busFleetNumber: match.group(3)!, // EC518 tripType: match.group(5) ?? '', // N/R/F or empty ); } ``` **Pattern 2: Finishing Trips** ```dart RegExp finishingTripPattern = RegExp( r'^(\d{4})_(\d{4})\s+.*FINISH.*\s+(EC\d+)\s+(\d+)\s+F\s+(\d+)' ); Trip? parseFinishingTrip(String line) { final match = finishingTripPattern.firstMatch(line); if (match == null) return null; return Trip( scheduledTime: formatTime(match.group(1)!), // 1521 -> "15:21" tripNumber: match.group(5)!, // 110 dutyNumber: match.group(4)!, // 509 runningNumber: match.group(4)!, // Same as duty for finishing trips busFleetNumber: match.group(3)!, // EC509 tripType: 'F', // Finishing trip isFinishing: true, ); } ``` #### Step 4: Time Formatting ```dart String formatTime(String rawTime) { // Convert 1531 -> "15:31" if (rawTime.length != 4) throw FormatException('Invalid time: $rawTime'); final hours = rawTime.substring(0, 2); final minutes = rawTime.substring(2, 4); return '$hours:$minutes'; } ``` #### Step 5: Filter by Time Range ```dart List filterByTimeRange(List trips, String startTime) { return trips.where((trip) => trip.scheduledTime.compareTo(startTime) >= 0).toList(); } ``` ### Complete Parser Implementation ```dart class ArrivaScheduleParser implements ScheduleParser { static final _regularPattern = RegExp( r'^(\d{4})_(\d{4})\s+.*\s+(EC\d+)\s+(\d+)\s+([NRF]?)(\d+)\s+(\d+)\s+(\d{4})\s+(\d+)' ); static final _finishingPattern = RegExp( r'^(\d{4})_(\d{4})\s+.*FINISH.*\s+(EC\d+)\s+(\d+)\s+F\s+(\d+)' ); @override bool canParse(String content) { // Check if content looks like Arriva format return content.contains('EC5') && content.contains('FINISH AT POINT') && RegExp(r'\d{4}_\d{4}').hasMatch(content); } @override Future> parse(String filePath) async { // Step 1: Extract text final text = await extractTextFromDocx(filePath); // Step 2: Find trip lines final tripLines = extractTripLines(text); if (tripLines.isEmpty) { throw ScheduleParseException('No trip data found in schedule'); } // Step 3: Parse each line final trips = []; for (final line in tripLines) { Trip? trip; // Try regular pattern first trip = _parseRegularTrip(line); // If not regular, try finishing pattern trip ??= _parseFinishingTrip(line); if (trip != null) { trips.add(trip); } } // Step 4: Sort by scheduled time trips.sort((a, b) => a.scheduledTime.compareTo(b.scheduledTime)); return trips; } Trip? _parseRegularTrip(String line) { final match = _regularPattern.firstMatch(line); if (match == null) return null; return Trip( scheduledTime: _formatTime(match.group(1)!), tripNumber: match.group(7)!, dutyNumber: match.group(4)!, runningNumber: match.group(6)!, busFleetNumber: match.group(3)!, tripType: match.group(5) ?? '', isFinishing: false, ); } Trip? _parseFinishingTrip(String line) { final match = _finishingPattern.firstMatch(line); if (match == null) return null; return Trip( scheduledTime: _formatTime(match.group(1)!), tripNumber: match.group(5)!, dutyNumber: match.group(4)!, runningNumber: match.group(4)!, busFleetNumber: match.group(3)!, tripType: 'F', isFinishing: true, ); } String _formatTime(String rawTime) { if (rawTime.length != 4) { throw FormatException('Invalid time format: $rawTime'); } return '${rawTime.substring(0, 2)}:${rawTime.substring(2, 4)}'; } List extractTripLines(String text) { final lines = text.split('\n'); return lines .where((line) => RegExp(r'^\d{4}_\d{4}').hasMatch(line) && line.contains('EC')) .map((line) => line.trim()) .toList(); } } ``` ### Error Handling ```dart class ScheduleParseException implements Exception { final String message; ScheduleParseException(this.message); @override String toString() => 'ScheduleParseException: $message'; } // Usage in parser try { final trips = await parser.parse(filePath); } on ScheduleParseException catch (e) { // Show user-friendly error showError('Could not parse schedule: ${e.message}'); } catch (e) { // Unexpected error showError('An unexpected error occurred'); logError(e); } ``` ### Validation ```dart class TripValidator { static List validate(Trip trip) { final errors = []; // Validate time format if (!RegExp(r'^\d{2}:\d{2}$').hasMatch(trip.scheduledTime)) { errors.add('Invalid time format: ${trip.scheduledTime}'); } // Validate trip number if (trip.tripNumber.isEmpty) { errors.add('Missing trip number'); } // Validate fleet number format if (!RegExp(r'^EC\d{3,4}$').hasMatch(trip.busFleetNumber)) { errors.add('Invalid fleet number: ${trip.busFleetNumber}'); } // Validate duty/running numbers if (trip.dutyNumber.isEmpty || trip.runningNumber.isEmpty) { errors.add('Missing duty or running number'); } return errors; } static bool isValid(Trip trip) { return validate(trip).isEmpty; } } ``` ### Testing the Parser ```dart void main() async { final parser = ArrivaScheduleParser(); // Test with sample schedule final trips = await parser.parse('/path/to/schedule.docx'); print('Parsed ${trips.length} trips'); // Validate all trips for (final trip in trips) { final errors = TripValidator.validate(trip); if (errors.isNotEmpty) { print('Trip ${trip.tripNumber} has errors: $errors'); } } // Filter to 15:00 onwards final filteredTrips = trips.where((t) => t.scheduledTime.compareTo('15:00') >= 0).toList(); print('${filteredTrips.length} trips from 15:00 onwards'); // Print first few trips for (final trip in filteredTrips.take(5)) { print('${trip.scheduledTime} - Trip ${trip.tripNumber} - ${trip.busFleetNumber}'); } } ``` ### Key Parsing Insights 1. **Departure Time**: First time in the `HHMM_HHMM` pattern is the actual departure 2. **Fleet Numbers**: Always prefixed with `EC` for Arriva (varies by TOC) 3. **Trip Types**: - Empty string = regular trip - `N` = Next trip has note/change - `R` = Relief (driver change) - `F` = Finishing trip 4. **Finishing Trips**: Look for `FINISH AT POINT` text and `F` flag 5. **Time Filtering**: Use string comparison (works because HH:MM format sorts correctly) ### Common Parsing Pitfalls 1. **Inconsistent whitespace**: Use `\s+` instead of single space in regex 2. **Missing data**: Not all trips have all fields - handle nulls 3. **Time wraparound**: Services past midnight (00:00-05:59) should sort after 23:59 4. **Multiple sections**: Schedule may have outbound AND inbound - filter correctly 5. **Dead running**: Some services show but aren't in passenger service - may need filtering ### 2. Trip List View **Functionality:** - Display all parsed trips in scrollable list - Show pre-filled data: - Scheduled departure time - Trip number - Bus fleet number - Duty number - Running number - Highlight unfilled required fields (actual departure time, actual fleet) - Filter by time range (e.g., "15:00 onwards") - Sort by scheduled time (default) **UI Components:** - List/ListView with trip cards - Each card shows: - Scheduled time (read-only, bold) - Trip number badge - Editable fields for actual departure time and fleet - Status indicator (complete/incomplete) ### 3. Data Entry Interface **Functionality:** - Quick entry for: - **Actual departure time** (time picker or manual entry) - **Actual fleet number** (text input with validation) - Validation: - Fleet number format (e.g., EC### or ####) - Time format (HH:MM) - Flag if actual time significantly differs from scheduled - Keyboard optimization for numeric entry - Quick "same as scheduled" button for times - Quick "same as planned" button for fleet **UX Considerations:** - Focus on speed - controller may be entering data between radio calls - Large tap targets - Auto-advance to next trip after entry - Save progress automatically (local storage) ### 4. BRR Export **Functionality:** - Generate Excel (.xlsx) file in BRR template format - Include all trip data: - Scheduled departure times (pre-filled) - Trip numbers (pre-filled) - Actual departure times (user-entered) - Fleet numbers (user-entered) - Duty and running numbers (pre-filled) - Preserve BRR template formatting - Export to device storage - Share via standard system share sheet **Technical Requirements:** - Excel generation library (e.g., `excel` package) - Template structure matching standard BRR format - File system access for saving - Share functionality **MVP Implementation:** - Initial template based on Arriva BRR format - Template system designed to support multiple TOC formats in future **User Flow:** 1. User taps "Export BRR" 2. App validates all required fields filled 3. Generate .xlsx file 4. Save to device 5. Show success message with option to share ## BRR Export Implementation ### Understanding BRR Excel Structure The BRR template has a specific structure: - **Header rows (1-6)**: Title, metadata, column headers - **Data rows (7+)**: One row per trip - **Columns**: - A: Scheduled Departure Time - B: Trip No. - C: Actual Departure Time (user input) - D: Fleet No. (user input) - E: Duty Number - F: Running No. - G-M: Additional fields (Lost Mileage, Standby, etc.) ### Excel Generation Strategy #### Step 1: Setup Excel Package ```dart import 'package:excel/excel.dart'; import 'dart:io'; class BRRExporter { final Excel _excel; late Sheet _sheet; BRRExporter() : _excel = Excel.createExcel(); Future generateBRR(List trips, BRRMetadata metadata) async { _sheet = _excel['Sheet1']; // Build the BRR _createHeader(metadata); _createColumnHeaders(); _populateTrips(trips); _formatCells(); // Save to file return await _saveToFile(metadata); } } ``` #### Step 2: Create Header Section ```dart void _createHeader(BRRMetadata metadata) { // Row 1: Title _sheet.cell(CellIndex.indexByString('A1')).value = 'Break'; _sheet.cell(CellIndex.indexByString('M1')).value = 'Sheet no:'; // Row 2: Location _sheet.cell(CellIndex.indexByString('A2')).value = 'Location'; _sheet.cell(CellIndex.indexByString('M2')).value = metadata.date; // Row 3: Title _sheet.cell(CellIndex.indexByString('A3')).value = 'Rail Replacement BRR'; // Row 4: Start/Finish times _sheet.cell(CellIndex.indexByString('A4')).value = 'Start Time'; _sheet.cell(CellIndex.indexByString('D4')).value = 'Finish Time'; _sheet.cell(CellIndex.indexByString('F4')).value = 'Every published departure time needs to be recorded and reason given if not operated from each location'; } void _createColumnHeaders() { // Row 5: Main column headers final headers = [ 'Scheduled Departure Time', 'Trip No.', 'Actual Departure Time', 'Fleet No.', 'Duty Number', 'Running No.', 'Did Trip Operate (Y/N)', 'Lost Milage (Tick Appropriate)', '', // Staff '', // Mech '', // Traffic '', // Non Deduct '', // Other 'Standby used (Y/N)', 'Return trip if curtailed', 'Curtailment Details', '', // From '', // To 'Reason or comments' ]; for (var i = 0; i < headers.length; i++) { _sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 4)) .value = headers[i]; } // Row 6: Sub-headers for Lost Mileage _sheet.cell(CellIndex.indexByString('H6')).value = 'Staff'; _sheet.cell(CellIndex.indexByString('I6')).value = 'Mech'; _sheet.cell(CellIndex.indexByString('J6')).value = 'Traffic'; _sheet.cell(CellIndex.indexByString('K6')).value = 'Non Deduct'; _sheet.cell(CellIndex.indexByString('L6')).value = 'Other'; } ``` #### Step 3: Populate Trip Data ```dart void _populateTrips(List trips) { int startRow = 6; // Data starts at row 7 (index 6) for (var i = 0; i < trips.length; i++) { final trip = trips[i]; final rowIndex = startRow + i; // Column A: Scheduled Departure Time _sheet.cell(CellIndex.indexByColumnRow( columnIndex: 0, rowIndex: rowIndex )).value = trip.scheduledTime; // Column B: Trip No. _sheet.cell(CellIndex.indexByColumnRow( columnIndex: 1, rowIndex: rowIndex )).value = trip.tripNumber; // Column C: Actual Departure Time (user input) if (trip.actualDepartureTime != null) { _sheet.cell(CellIndex.indexByColumnRow( columnIndex: 2, rowIndex: rowIndex )).value = trip.actualDepartureTime; } // Column D: Fleet No. (user input) if (trip.actualFleetNumber != null) { _sheet.cell(CellIndex.indexByColumnRow( columnIndex: 3, rowIndex: rowIndex )).value = trip.actualFleetNumber; } // Column E: Duty Number _sheet.cell(CellIndex.indexByColumnRow( columnIndex: 4, rowIndex: rowIndex )).value = trip.dutyNumber; // Column F: Running No. _sheet.cell(CellIndex.indexByColumnRow( columnIndex: 5, rowIndex: rowIndex )).value = trip.runningNumber; } } ``` #### Step 4: Format Cells ```dart void _formatCells() { // Define styles final headerStyle = CellStyle( bold: true, fontSize: 12, horizontalAlign: HorizontalAlign.Center, backgroundColorHex: '#D9D9D9', ); final dataStyle = CellStyle( fontSize: 11, horizontalAlign: HorizontalAlign.Left, ); // Apply header styles (rows 1-5) for (var row = 0; row < 6; row++) { for (var col = 0; col < 19; col++) { final cell = _sheet.cell(CellIndex.indexByColumnRow( columnIndex: col, rowIndex: row )); cell.cellStyle = headerStyle; } } // Apply data styles (row 7 onwards) final maxRow = _sheet.maxRows; for (var row = 6; row < maxRow; row++) { for (var col = 0; col < 19; col++) { final cell = _sheet.cell(CellIndex.indexByColumnRow( columnIndex: col, rowIndex: row )); cell.cellStyle = dataStyle; } } // Set column widths _sheet.setColWidth(0, 25.0); // Scheduled Departure Time _sheet.setColWidth(1, 10.0); // Trip No. _sheet.setColWidth(2, 25.0); // Actual Departure Time _sheet.setColWidth(3, 12.0); // Fleet No. _sheet.setColWidth(4, 15.0); // Duty Number _sheet.setColWidth(5, 15.0); // Running No. } ``` #### Step 5: Save to File ```dart Future _saveToFile(BRRMetadata metadata) async { // Get output directory final directory = await getApplicationDocumentsDirectory(); final fileName = 'BRR_${metadata.route}_${metadata.date}.xlsx'; final filePath = '${directory.path}/$fileName'; // Encode to bytes final bytes = _excel.encode(); if (bytes == null) { throw Exception('Failed to encode Excel file'); } // Write to file final file = File(filePath); await file.writeAsBytes(bytes); return file; } ``` ### Complete Exporter Implementation ```dart class ArrivaBRRExporter implements BRRExporter { @override Future export(List trips, BRRMetadata metadata) async { final excel = Excel.createExcel(); final sheet = excel['Sheet1']; // Build BRR structure _buildHeader(sheet, metadata); _buildColumnHeaders(sheet); _populateData(sheet, trips); _applyFormatting(sheet, trips.length); // Save and return file return await _saveFile(excel, metadata); } void _buildHeader(Sheet sheet, BRRMetadata metadata) { // Row 1 sheet.cell(CellIndex.indexByString('A1')).value = 'Break'; sheet.cell(CellIndex.indexByString('M1')).value = 'Sheet no:'; // Row 2 sheet.cell(CellIndex.indexByString('A2')).value = 'Location'; sheet.cell(CellIndex.indexByString('B2')).value = metadata.location; // Row 3 sheet.cell(CellIndex.indexByString('A3')).value = 'Rail Replacement BRR'; // Row 4 sheet.cell(CellIndex.indexByString('A4')).value = 'Start Time'; sheet.cell(CellIndex.indexByString('D4')).value = 'Finish Time'; // Row 5: Main headers final headers = [ 'Scheduled Departure Time', 'Trip No.', 'Actual Departure Time', 'Fleet No.', 'Duty Number', 'Running No.', 'Did Trip Operate (Y/N)', 'Lost Milage', '', '', '', '', 'Standby used (Y/N)', 'Return trip if curtailed', 'Curtailment Details', '', '', 'Reason or comments' ]; for (var i = 0; i < headers.length; i++) { sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 4)) .value = headers[i]; } // Row 6: Sub-headers final subHeaders = ['', '', '', '', '', '', '', 'Staff', 'Mech', 'Traffic', 'Non Deduct', 'Other', '', '', '', 'From', 'To', '']; for (var i = 0; i < subHeaders.length; i++) { if (subHeaders[i].isNotEmpty) { sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 5)) .value = subHeaders[i]; } } } void _buildColumnHeaders(Sheet sheet) { // Already done in _buildHeader } void _populateData(Sheet sheet, List trips) { for (var i = 0; i < trips.length; i++) { final trip = trips[i]; final row = 6 + i; // Data starts at row 7 (index 6) sheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: row)) .value = trip.scheduledTime; sheet.cell(CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: row)) .value = trip.tripNumber; if (trip.actualDepartureTime != null) { sheet.cell(CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: row)) .value = trip.actualDepartureTime; } if (trip.actualFleetNumber != null) { sheet.cell(CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: row)) .value = trip.actualFleetNumber; } sheet.cell(CellIndex.indexByColumnRow(columnIndex: 4, rowIndex: row)) .value = trip.dutyNumber; sheet.cell(CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: row)) .value = trip.runningNumber; } } void _applyFormatting(Sheet sheet, int tripCount) { // Header style final headerStyle = CellStyle( bold: true, fontSize: 12, horizontalAlign: HorizontalAlign.Center, backgroundColorHex: '#D9D9D9', ); // Apply to header rows (0-5) for (var row = 0; row < 6; row++) { for (var col = 0; col < 18; col++) { sheet.cell(CellIndex.indexByColumnRow( columnIndex: col, rowIndex: row )).cellStyle = headerStyle; } } // Data style final dataStyle = CellStyle( fontSize: 11, horizontalAlign: HorizontalAlign.Left, ); // Apply to data rows for (var row = 6; row < 6 + tripCount; row++) { for (var col = 0; col < 18; col++) { sheet.cell(CellIndex.indexByColumnRow( columnIndex: col, rowIndex: row )).cellStyle = dataStyle; } } // Column widths sheet.setColWidth(0, 25); // Scheduled time sheet.setColWidth(1, 10); // Trip no sheet.setColWidth(2, 25); // Actual time sheet.setColWidth(3, 12); // Fleet sheet.setColWidth(4, 15); // Duty sheet.setColWidth(5, 15); // Running } Future _saveFile(Excel excel, BRRMetadata metadata) async { final directory = await getApplicationDocumentsDirectory(); final timestamp = DateTime.now().millisecondsSinceEpoch; final fileName = 'BRR_${metadata.route}_${metadata.date}_$timestamp.xlsx'; final filePath = '${directory.path}/$fileName'; final bytes = excel.encode(); if (bytes == null) throw Exception('Failed to encode Excel'); final file = File(filePath); await file.writeAsBytes(bytes); return file; } } ``` ### Metadata Class ```dart class BRRMetadata { final String route; // "Metropolitan_Line_Uxbridge" final String date; // "21_09_2025" final String location; // "Uxbridge Station" final String controller; // "Benjamin Watt" final String? sheetNumber; BRRMetadata({ required this.route, required this.date, required this.location, required this.controller, this.sheetNumber, }); factory BRRMetadata.fromSchedule(String scheduleText) { // Extract metadata from schedule header final routeMatch = RegExp(r'ROUTE\s+(\w+)').firstMatch(scheduleText); final dateMatch = RegExp(r'(\d{1,2})\s+(\d{1,2})\s+(\d{2})').firstMatch(scheduleText); return BRRMetadata( route: routeMatch?.group(1) ?? 'Unknown', date: dateMatch != null ? '${dateMatch.group(1)}_${dateMatch.group(2)}_20${dateMatch.group(3)}' : DateTime.now().toString().split(' ')[0], location: 'Unknown', controller: 'Unknown', ); } } ``` ### Export with Validation ```dart class BRRExportService { final BRRExporter _exporter = ArrivaBRRExporter(); Future exportBRR( List trips, BRRMetadata metadata, ) async { // Validate trips final validationErrors = []; for (final trip in trips) { if (trip.actualDepartureTime == null) { validationErrors.add('Trip ${trip.tripNumber}: Missing actual departure time'); } if (trip.actualFleetNumber == null) { validationErrors.add('Trip ${trip.tripNumber}: Missing fleet number'); } } if (validationErrors.isNotEmpty) { return ExportResult.error(validationErrors); } try { final file = await _exporter.export(trips, metadata); return ExportResult.success(file); } catch (e) { return ExportResult.error(['Export failed: $e']); } } } class ExportResult { final File? file; final List errors; bool get isSuccess => errors.isEmpty; ExportResult.success(this.file) : errors = []; ExportResult.error(this.errors) : file = null; } ``` ### Sharing the File ```dart import 'package:share_plus/share_plus.dart'; Future shareFile(File file) async { await Share.shareXFiles( [XFile(file.path)], subject: 'BRR ${DateTime.now().toString().split(' ')[0]}', text: 'Bus Running Report', ); } ``` ### Usage Example ```dart // In your export button handler Future _handleExport() async { // Show loading setState(() => _isExporting = true); try { final metadata = BRRMetadata( route: 'Metropolitan_Line_Uxbridge', date: '21_09_2025', location: 'Uxbridge Station', controller: 'Benjamin Watt', ); final exportService = BRRExportService(); final result = await exportService.exportBRR(_trips, metadata); if (result.isSuccess) { // Show success message ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('BRR exported successfully')), ); // Offer to share final share = await showDialog( context: context, builder: (context) => AlertDialog( title: Text('Export Complete'), content: Text('Would you like to share the BRR?'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: Text('Not Now'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: Text('Share'), ), ], ), ); if (share == true && result.file != null) { await shareFile(result.file!); } } else { // Show errors showDialog( context: context, builder: (context) => AlertDialog( title: Text('Cannot Export'), content: Text(result.errors.join('\n')), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('OK'), ), ], ), ); } } finally { setState(() => _isExporting = false); } } ``` ### Key Export Insights 1. **Template Preservation**: Maintain exact header structure for compatibility 2. **Cell Formatting**: Use consistent styles (bold headers, aligned data) 3. **Column Widths**: Set appropriate widths for readability 4. **Validation**: Check all required fields before export 5. **File Naming**: Use descriptive names with route, date, timestamp 6. **Error Handling**: Graceful failure with user-friendly messages 7. **Sharing**: Offer immediate sharing after successful export ## Data Model ### Trip Object ```dart class Trip { String scheduledTime; // "15:31" String tripNumber; // "112" String dutyNumber; // "518" String runningNumber; // "518" String busFleetNumber; // "EC518" String? actualDepartureTime; // "15:33" (nullable - user input) String? actualFleetNumber; // "33523" (nullable - user input) bool isComplete; // Computed: both actuals filled } ``` ### App State ```dart class BRRState { List trips; String? scheduleFileName; DateTime? uploadedAt; String? routeInfo; // e.g., "Metropolitan Line - Uxbridge" String? serviceDate; // "21/09/2025" } ``` ## Technical Stack ### UI Framework - **UI Library:** `shadcn_flutter: ^0.0.47` (NO Material or Cupertino components) - **Icons:** Lucide Icons - **Routing:** `go_router` with routes defined in page classes ### Routing Pattern ```dart class HomePage extends StatefulWidget { static GoRoute route = GoRoute( path: "/", builder: (context, state) => HomePage(), ); @override State createState() => _HomePageState(); } ``` ### Required Packages - `shadcn_flutter: ^0.0.47` - UI components - `lucide_icons` - Icons - `go_router` - Navigation - `file_picker` - Document upload - `docx_to_text` or custom parser - Schedule parsing - `excel` - BRR generation - `share_plus` - File sharing - `path_provider` - File system access - `shared_preferences` - Local data persistence ### Platform Support - **Primary:** Android (most controllers use Android devices) - **Secondary:** iOS (if time permits) - **Future:** Web version for desktop use ## User Interface ### UI Design Reference The app should follow the visual pattern of First's Coordination Portal V2.0: - **List-based layout** with cards for each trip - **Information density** - show key data at a glance - **Expandable details** - tap to show/edit full trip information - **Clean typography** - labels in bold blue, data in black - **Header bar** showing station/location, user, and date - **Tab/filter system** for viewing different subsets (All/Arrival/Departure/Standby) ### Key UI Elements from Reference ``` Time: 16:31 Type: D Coach: 438 Vehicle Type: Double Deck Bus Customer: South Western Railway Destination: Station Forecourt off Station Parade Operator: Imperial Coaches Ltd VRM: YX25OJN ``` Each trip card should show: - **Primary info** (large): Time, Type indicator, Coach/Fleet number - **Secondary info** (smaller): Vehicle type, destination, operator - **Expandable section** for data entry and additional details ### Screens 1. **Home/Upload Screen** - Upload button (prominent) - Recent BRRs list (if any saved) - App info/help button 2. **Trip List Screen** - **Header bar:** - Route info (e.g., "Metropolitan Line - Uxbridge") - User name - Service date with prev/next navigation - **Tab bar:** - All trips - Departure (outbound from start point) - Arrival (return trips) - Standby (if applicable) - **Trip cards:** - Collapsed view: Time, Type, Coach number - Tap to expand: Full details + editable fields - **Bottom action bar:** - Completion progress indicator - "Export BRR" button 3. **Trip Detail/Entry (Expanded Card)** - All scheduled data (read-only, gray background) - Editable fields (white background): - Actual departure time - Actual fleet number - Quick action buttons: - "Same as scheduled" (time) - "As planned" (fleet) - Status indicator (complete/incomplete) ### Design Principles - **High contrast** - usable in bright daylight - **Large text** - readable at a glance - **Minimal taps** - optimize for speed - **Offline-first** - works without internet - **Auto-save** - never lose entered data - **Information hierarchy** - most important data largest/boldest - **Touch-friendly** - 44pt minimum tap targets - **Collapsible content** - reduce scrolling, expand only what's needed ## Out of Scope for MVP ### Future Features - **Multi-TOC support** - Parser and template support for First, Stagecoach, National Express, etc. - Multi-route support in single session - Photo upload for schedule documents (OCR) - Sync across devices - Historical BRR archive - Analytics/reports (trips per duty, delays, etc.) - Did Trip Operate (Y/N) tracking - Lost mileage reasons - Curtailment details - Integration with radio/messaging systems - Offline schedule library - Duty roster integration - Real-time collaboration (multiple controllers on same BRR) ### Technical Debt Acceptable for MVP - Basic error handling (can improve later) - Simple validation (can add more rules later) - Single schedule format parser (Arriva only) - No user authentication - No cloud sync - No data encryption (local storage only) ## Success Metrics ### MVP Success Criteria 1. Parse Arriva schedule accurately (>95% accuracy) 2. Generate valid BRR Excel file 3. Reduce data entry time by >50% vs manual 4. Zero crashes during normal operation 5. Works offline ### User Acceptance - Controller can complete a 40-trip BRR in <10 minutes (vs 30+ minutes manual) - No need to reference paper schedule while entering data - Generated BRR passes management review without corrections ## Development Phases ### Phase 1: Core Parsing (Week 1) - Schedule upload - Docx parsing - Trip data extraction - Basic validation ### Phase 2: Data Entry UI (Week 2) - Trip list view - Input forms - Auto-save - Validation feedback ### Phase 3: Export & Polish (Week 3) - Excel generation - Template formatting - File sharing - Bug fixes - UX refinements ## Testing Requirements ### Critical Test Cases 1. Upload valid schedule → correct trip count 2. Parse trips → all fields populated correctly 3. Enter actual times/fleet → data saved 4. Export BRR → valid Excel file 5. Re-open app → previous data persisted 6. Handle missing fields → appropriate error messages 7. Handle malformed schedule → graceful failure ### Test Data - **Primary test data:** Arriva Metropolitan Line schedule (21/09/2025) - Minimum 3 different Arriva schedule formats for validation - Edge cases: early morning times (00:00-06:00), finishing trips (F flag) ### Future Testing (Post-MVP) - Test with First, Stagecoach, and other TOC schedule formats - Cross-TOC compatibility validation ## Deployment ### MVP Release - **Platform:** Android APK - **Distribution:** Direct download (no app store initially) - **Version:** 0.1.0-alpha - **Target Users:** Benji + 2-3 other controllers for feedback ### Post-MVP - Google Play Store release - iOS App Store (if demand) - Feature iterations based on user feedback ## Notes - **MVP uses Arriva schedule format for initial development and testing** - **Architecture should support multiple TOC formats in future releases** - Parser should be modular/pluggable for easy addition of new formats - BRR template matches existing Excel format used across industry - App must be usable with gloves (winter operations) - Consider dark mode (night shifts) ## Architecture Considerations for Multi-TOC Support ### Parser Design ```dart abstract class ScheduleParser { List parse(String docxContent); bool canParse(String docxContent); // Detect format } class ArrivaScheduleParser extends ScheduleParser { ... } // Future: FirstScheduleParser, StagecoachScheduleParser, etc. ``` ### Template Design ```dart abstract class BRRTemplate { File generate(List trips, BRRMetadata metadata); } class ArrivaBRRTemplate extends BRRTemplate { ... } // Future: FirstBRRTemplate, StagecoachBRRTemplate, etc. ```