A whole bunch of changes

This commit is contained in:
Courtney Arnold 2026-08-15 17:52:25 -05:00
parent 901ec678a3
commit 956231048e
122 changed files with 5442 additions and 914 deletions

View file

@ -1,19 +1,55 @@
import '../models/fuel_entry.dart';
import '../models/vehicle.dart';
/// One vehicle's totals within a [FuelReport]'s date range.
/// One vehicle's totals — and, for the line-items/receipt-images report
/// sections, its individual entries — within a [FuelReport]'s date range.
/// [entries] is sorted oldest-first, the natural order for a ledger of
/// line items building up to a total.
class VehicleReportRow {
final Vehicle vehicle;
final int entryCount;
final List<FuelEntry> entries;
final double totalGallons;
final double totalCost;
VehicleReportRow({
required this.vehicle,
required this.entryCount,
required this.entries,
required this.totalGallons,
required this.totalCost,
});
int get entryCount => entries.length;
}
/// One entry paired with the vehicle it belongs to — [FuelEntry] only knows
/// [FuelEntry.vehicleId], not the [Vehicle] itself, so anything that needs
/// to show both together (like [FuelReport.allEntries], for a combined
/// cross-vehicle list) needs this pairing.
class ReportEntry {
final FuelEntry entry;
final Vehicle vehicle;
ReportEntry({required this.entry, required this.vehicle});
}
/// Every qualifying entry in a single calendar month, across every vehicle
/// in the report — the "Gallons by month" breakdown is this, one per
/// month present in [FuelReport.monthlyTotals].
class MonthlyTotal {
/// The first of the month, e.g. `DateTime(2026, 8)` for August 2026.
final DateTime month;
final int fillCount;
final double totalGallons;
final double totalCost;
MonthlyTotal({
required this.month,
required this.fillCount,
required this.totalGallons,
required this.totalCost,
});
double get avgPricePerGallon => totalGallons == 0 ? 0 : totalCost / totalGallons;
}
/// Per-vehicle fuel totals for entries with a receipt, dated within
@ -27,6 +63,47 @@ class FuelReport {
double get totalGallons => rows.fold(0.0, (sum, r) => sum + r.totalGallons);
double get totalCost => rows.fold(0.0, (sum, r) => sum + r.totalCost);
int get fillCount => rows.fold(0, (sum, r) => sum + r.entryCount);
double get avgPricePerGallon => totalGallons == 0 ? 0 : totalCost / totalGallons;
/// Every entry across every vehicle, oldest first — the natural order for
/// a combined "purchase detail" ledger that isn't grouped by vehicle.
List<ReportEntry> get allEntries {
final list = [
for (final row in rows)
for (final entry in row.entries) ReportEntry(entry: entry, vehicle: row.vehicle),
];
list.sort((a, b) => a.entry.date.compareTo(b.entry.date));
return list;
}
/// [allEntries] grouped by calendar month, in chronological order — the
/// "Purchase detail" section's grouping, and the source data for
/// [monthlyTotals].
Map<DateTime, List<ReportEntry>> get entriesByMonth {
final map = <DateTime, List<ReportEntry>>{};
for (final reportEntry in allEntries) {
final month = DateTime(reportEntry.entry.date.year, reportEntry.entry.date.month);
(map[month] ??= []).add(reportEntry);
}
return map;
}
/// One [MonthlyTotal] per calendar month with qualifying entries, oldest
/// first — the "Gallons by month" chart/table's data.
List<MonthlyTotal> get monthlyTotals {
final byMonth = entriesByMonth;
final months = byMonth.keys.toList()..sort();
return [
for (final month in months)
MonthlyTotal(
month: month,
fillCount: byMonth[month]!.length,
totalGallons: byMonth[month]!.fold(0.0, (sum, re) => sum + re.entry.gallons),
totalCost: byMonth[month]!.fold(0.0, (sum, re) => sum + re.entry.totalCost),
),
];
}
}
/// Builds a per-vehicle fuel summary for [startDate]–[endDate] (inclusive,
@ -68,11 +145,12 @@ FuelReport buildFuelReport({
// No matching (active) vehicle — e.g. deleted since — nothing sane to
// attribute these entries to.
if (vehicle == null) continue;
final sortedEntries = entry.value.toList()..sort((a, b) => a.date.compareTo(b.date));
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),
entries: sortedEntries,
totalGallons: sortedEntries.fold(0.0, (sum, e) => sum + e.gallons),
totalCost: sortedEntries.fold(0.0, (sum, e) => sum + e.totalCost),
));
}
rows.sort((a, b) => a.vehicle.displayLabel.compareTo(b.vehicle.displayLabel));