81 lines
3.1 KiB
Dart
81 lines
3.1 KiB
Dart
import '../models/fuel_entry.dart';
|
||
import '../models/vehicle.dart';
|
||
|
||
/// One vehicle's totals within a [FuelReport]'s date range.
|
||
class VehicleReportRow {
|
||
final Vehicle vehicle;
|
||
final int entryCount;
|
||
final double totalGallons;
|
||
final double totalCost;
|
||
|
||
VehicleReportRow({
|
||
required this.vehicle,
|
||
required this.entryCount,
|
||
required this.totalGallons,
|
||
required this.totalCost,
|
||
});
|
||
}
|
||
|
||
/// Per-vehicle fuel totals for entries with a receipt, dated within
|
||
/// [startDate]–[endDate] inclusive. See [buildFuelReport].
|
||
class FuelReport {
|
||
final DateTime startDate;
|
||
final DateTime endDate;
|
||
final List<VehicleReportRow> rows;
|
||
|
||
FuelReport({required this.startDate, required this.endDate, required this.rows});
|
||
|
||
double get totalGallons => rows.fold(0.0, (sum, r) => sum + r.totalGallons);
|
||
double get totalCost => rows.fold(0.0, (sum, r) => sum + r.totalCost);
|
||
}
|
||
|
||
/// Builds a per-vehicle fuel summary for [startDate]–[endDate] (inclusive,
|
||
/// judged by each entry's purchase date, in local time), restricted to
|
||
/// entries that actually have a receipt attached — a report meant to
|
||
/// substantiate a fuel tax refund claim is only as good as the
|
||
/// documentation behind each entry, so an entry logged without a photo
|
||
/// isn't something the report should be claiming on your behalf. "Has a
|
||
/// receipt" means a local copy or a cloud copy — [FuelEntry.receiptImagePath]
|
||
/// or [FuelEntry.receiptDriveFileId] — not specifically an already-synced
|
||
/// one, since sync status is a technical detail unrelated to whether the
|
||
/// purchase is documented.
|
||
///
|
||
/// Vehicles with no qualifying entries in range are omitted rather than
|
||
/// shown with an all-zero row. Rows are sorted by [Vehicle.displayLabel].
|
||
FuelReport buildFuelReport({
|
||
required List<Vehicle> vehicles,
|
||
required List<FuelEntry> fuelEntries,
|
||
required DateTime startDate,
|
||
required DateTime endDate,
|
||
}) {
|
||
final rangeStart = DateTime(startDate.year, startDate.month, startDate.day);
|
||
final rangeEndExclusive =
|
||
DateTime(endDate.year, endDate.month, endDate.day).add(const Duration(days: 1));
|
||
|
||
final vehiclesById = {for (final v in vehicles) v.id: v};
|
||
final entriesByVehicle = <String, List<FuelEntry>>{};
|
||
|
||
for (final entry in fuelEntries) {
|
||
final hasReceipt = entry.receiptImagePath != null || entry.receiptDriveFileId != null;
|
||
if (!hasReceipt) continue;
|
||
if (entry.date.isBefore(rangeStart) || !entry.date.isBefore(rangeEndExclusive)) continue;
|
||
(entriesByVehicle[entry.vehicleId] ??= []).add(entry);
|
||
}
|
||
|
||
final rows = <VehicleReportRow>[];
|
||
for (final entry in entriesByVehicle.entries) {
|
||
final vehicle = vehiclesById[entry.key];
|
||
// No matching (active) vehicle — e.g. deleted since — nothing sane to
|
||
// attribute these entries to.
|
||
if (vehicle == null) continue;
|
||
rows.add(VehicleReportRow(
|
||
vehicle: vehicle,
|
||
entryCount: entry.value.length,
|
||
totalGallons: entry.value.fold(0.0, (sum, e) => sum + e.gallons),
|
||
totalCost: entry.value.fold(0.0, (sum, e) => sum + e.totalCost),
|
||
));
|
||
}
|
||
rows.sort((a, b) => a.vehicle.displayLabel.compareTo(b.vehicle.displayLabel));
|
||
|
||
return FuelReport(startDate: rangeStart, endDate: endDate, rows: rows);
|
||
}
|