MO-Fuel-Tax-Back/lib/services/fuel_report.dart

81 lines
3.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}