Add initial project files and configurations for bus_running_record app

This commit is contained in:
ImBenji
2026-03-25 17:19:53 +00:00
parent f5da563c29
commit e41e14e252
151 changed files with 9829 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
import "dart:convert";
import "dart:typed_data";
import "package:archive/archive.dart";
import "package:excel/excel.dart";
import "package:flutter/services.dart";
import "../models/trip.dart";
import "../models/brr_metadata.dart";
import "brr_exporter.dart";
class ArrivaBRRExporter implements BRRExporter {
// Numbers xlsx export leaves numFmtId="0" in custom formats which the
// excel package rejects. Strip it out before decoding.
List<int> _patchTemplateBytes(List<int> bytes) {
final archive = ZipDecoder().decodeBytes(bytes);
final output = Archive();
for (final file in archive) {
if (file.name == "xl/styles.xml" && file.isFile) {
var xml = utf8.decode(file.content as List<int>);
// strip the whole numFmts block — Numbers export puts built-in IDs
// in there which the excel package rejects
xml = xml.replaceAll(RegExp(r'<numFmts[^>]*>.*?</numFmts>', dotAll: true), "");
// reset all numFmtId refs in xf elements to 0 (General)
// so nothing tries to look up the stripped formats
xml = xml.replaceAll(RegExp(r'numFmtId="\d+"'), 'numFmtId="0"');
final patched = utf8.encode(xml);
output.addFile(ArchiveFile(file.name, patched.length, patched));
} else {
output.addFile(file);
}
}
return ZipEncoder().encode(output)!;
}
static const int _dataStartRow = 8; // row 9 (0-indexed)
static const int _templateDataRows = 15; // rows 923
@override
Future<Uint8List> export(List<Trip> trips, BRRMetadata metadata) async {
final templateBytes = await rootBundle.load("assets/arriva_brr.xlsx");
final patched = _patchTemplateBytes(templateBytes.buffer.asUint8List());
final excel = Excel.decodeBytes(patched);
final sheetName = excel.sheets.keys.first;
final sheet = excel[sheetName];
if (trips.length > _templateDataRows) {
_shiftRowsDown(sheet, trips.length - _templateDataRows);
}
_populateData(sheet, trips);
final bytes = excel.encode();
if (bytes == null) throw Exception("Failed to encode Excel");
return Uint8List.fromList(bytes);
}
// Shifts all rows from (_dataStartRow + _templateDataRows) onwards down by extraRows
void _shiftRowsDown(Sheet sheet, int extraRows) {
final firstRowToShift = _dataStartRow + _templateDataRows; // row 24 (index 23)
// figure out how many rows exist beyond the data block
final maxRow = sheet.rows.length;
// copy from the bottom up to avoid overwriting
for (var r = maxRow - 1; r >= firstRowToShift; r--) {
final destRow = r + extraRows;
if (r >= sheet.rows.length) continue;
final srcRow = sheet.rows[r];
for (var c = 0; c < srcRow.length; c++) {
final cell = srcRow[c];
if (cell == null) continue;
sheet.cell(CellIndex.indexByColumnRow(columnIndex: c, rowIndex: destRow)).value =
cell.value;
sheet.cell(CellIndex.indexByColumnRow(columnIndex: c, rowIndex: destRow)).cellStyle =
cell.cellStyle;
}
}
// clear the original rows that were shifted
for (var r = firstRowToShift; r < firstRowToShift + extraRows; r++) {
if (r >= sheet.rows.length) break;
for (var c = 0; c < 18; c++) {
sheet.cell(CellIndex.indexByColumnRow(columnIndex: c, rowIndex: r)).value = null;
}
}
}
void _populateData(Sheet sheet, List<Trip> trips) {
for (var i = 0; i < trips.length; i++) {
final trip = trips[i];
final row = _dataStartRow + i;
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: row)).value =
TextCellValue(trip.scheduledTime);
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: row)).value =
TextCellValue(trip.tripNumber);
if (trip.actualDepartureTime != null) {
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: row)).value =
TextCellValue(trip.actualDepartureTime!);
}
if (trip.actualFleetNumber != null) {
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: row)).value =
TextCellValue(trip.actualFleetNumber!);
}
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 4, rowIndex: row)).value =
TextCellValue(trip.dutyNumber);
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: row)).value =
TextCellValue(trip.runningNumber);
final didOperate = trip.actualDepartureTime != null && trip.actualFleetNumber != null;
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 6, rowIndex: row)).value =
TextCellValue(didOperate ? "Y" : "N");
}
}
}
+7
View File
@@ -0,0 +1,7 @@
import "dart:typed_data";
import "../models/trip.dart";
import "../models/brr_metadata.dart";
abstract class BRRExporter {
Future<Uint8List> export(List<Trip> trips, BRRMetadata metadata);
}
@@ -0,0 +1,76 @@
import "dart:typed_data";
import "package:excel/excel.dart";
import "../models/trip.dart";
import "../models/brr_metadata.dart";
import "brr_exporter.dart";
class StagecoachBRRExporter implements BRRExporter {
@override
Future<Uint8List> export(List<Trip> trips, BRRMetadata metadata) async {
final excel = Excel.createExcel();
final sheetName = "BRR";
excel.rename(excel.getDefaultSheet()!, sheetName);
final sheet = excel[sheetName];
final headers = [
"Dep. Time",
"(+/-) No.",
"Ser.",
"Bus Wk No",
"Fleet No.",
"Crew Duty",
"No.of Pax",
"Notes",
];
final bold = CellStyle(bold: true);
for (var c = 0; c < headers.length; c++) {
final cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: c, rowIndex: 0));
cell.value = TextCellValue(headers[c]);
cell.cellStyle = bold;
}
for (var i = 0; i < trips.length; i++) {
final trip = trips[i];
final row = i + 1;
// Dep Time (HHMM no colon)
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 0, rowIndex: row)).value =
TextCellValue(trip.scheduledTime.replaceAll(":", ""));
// (+/-) No. - user fills in
if (trip.actualDepartureTime != null) {
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 1, rowIndex: row)).value =
TextCellValue(trip.actualDepartureTime!);
}
// Ser. — outbound shows route name, inbound shows "PARK"
// For now derive from station order: last outbound station as label
final ser = trip.direction == "outbound"
? (metadata.route != "Unknown" ? metadata.route : "OUT")
: "PARK";
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 2, rowIndex: row)).value =
TextCellValue(ser);
// Bus Wk No
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 3, rowIndex: row)).value =
TextCellValue(trip.dutyNumber);
// Fleet No. — actual fleet number entered by user
if (trip.actualFleetNumber != null) {
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 4, rowIndex: row)).value =
TextCellValue(trip.actualFleetNumber!);
}
// Crew Duty
sheet.cell(CellIndex.indexByColumnRow(columnIndex: 5, rowIndex: row)).value =
TextCellValue(trip.tripNumber);
}
final bytes = excel.encode();
if (bytes == null) throw Exception("Failed to encode excel");
return Uint8List.fromList(bytes);
}
}