160 lines
6.7 KiB
Dart
160 lines
6.7 KiB
Dart
import 'package:miler/Models/summary/riderweeklykms.dart';
|
|
import 'package:miler/data/miler_api.dart';
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// THE WEEK'S KILOMETRES
|
|
///
|
|
/// ── Why the chart was empty ──
|
|
///
|
|
/// This read `data.breakdown` off `GET /miler/earnings?period=weekly` and
|
|
/// mapped it into the seven bars. That field is not on the contract: the
|
|
/// earnings response carries `completed_stops`, `cancelled_stops`,
|
|
/// `total_stops`, `total_kms`, `total_earnings` and `total_bonus` — six
|
|
/// **totals for the period asked for**, and no per-day series at all.
|
|
///
|
|
/// So `breakdown` was always null, the list was always empty, and the chart
|
|
/// drew seven bars of nothing while the rider had ridden all week. It failed
|
|
/// silently because an absent key and an empty week look identical downstream.
|
|
///
|
|
/// ── What it does instead ──
|
|
///
|
|
/// The endpoint answers for *a* period, and it takes a `date`. A week is seven
|
|
/// days, so the week is seven daily calls — asked concurrently, so the page
|
|
/// waits for the slowest one rather than the sum of seven.
|
|
///
|
|
/// That is more requests than one, and it is the honest cost of a contract that
|
|
/// has no series on it. It is bounded (seven, on a screen opened occasionally
|
|
/// rather than polled), and it is built from the same figure the totals row
|
|
/// prints, so the bars and the total cannot disagree.
|
|
///
|
|
/// **The fast path stays.** If `breakdown` ever ships, it is used and the seven
|
|
/// calls are skipped — that check is two lines and it is what makes this
|
|
/// removable later without touching the page.
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
class RiderWeeklyKmController {
|
|
/// `Mon` … `Sun`. Written out rather than taken from `intl` because these
|
|
/// strings are matched against the server's own day names elsewhere, and a
|
|
/// locale-aware formatter would introduce a mismatch that comparison cannot
|
|
/// survive.
|
|
static const List<String> _days = [
|
|
'Mon',
|
|
'Tue',
|
|
'Wed',
|
|
'Thu',
|
|
'Fri',
|
|
'Sat',
|
|
'Sun',
|
|
];
|
|
|
|
static String _iso(DateTime d) =>
|
|
'${d.year}-${d.month.toString().padLeft(2, '0')}-'
|
|
'${d.day.toString().padLeft(2, '0')}';
|
|
|
|
static double _num(dynamic v) {
|
|
if (v is num) return v.toDouble();
|
|
return double.tryParse('${v ?? ''}') ?? 0;
|
|
}
|
|
|
|
/// Reads a weekly `breakdown` into the chart's series, or returns null when
|
|
/// there is nothing usable in it.
|
|
///
|
|
/// ── Null is the whole contract here ──
|
|
///
|
|
/// Anything short of a series the chart can draw has to fall through to the
|
|
/// seven daily calls, because the alternative is what this page shipped for
|
|
/// months: an empty list drawn as seven empty bars, indistinguishable from a
|
|
/// week with no riding in it. Absent, null, not a list, empty, entries that
|
|
/// are not maps, entries with no day or no parseable distance — all of them
|
|
/// are "no series", and none of them is a chart.
|
|
///
|
|
/// ── The day label is normalised ──
|
|
///
|
|
/// The chart labels its bars with the first three characters of `day`, which
|
|
/// works for `Mon` and produces `202` for `2026-08-19`. The backend's example
|
|
/// uses the ISO form, so an ISO date is converted to the weekday it names and
|
|
/// anything else is passed through — a server that sends `Monday`, `Mon` or
|
|
/// `mon` already works, and one that sends something unrecognisable is
|
|
/// unusable rather than silently mislabelled.
|
|
static List<RiderWeeklyKms>? _readBreakdown(dynamic raw) {
|
|
if (raw is! List || raw.isEmpty) return null;
|
|
|
|
final rows = <RiderWeeklyKms>[];
|
|
for (final entry in raw) {
|
|
if (entry is! Map) return null;
|
|
final day = _dayLabel(entry['day']);
|
|
if (day.isEmpty) return null;
|
|
final kms = entry['kms'];
|
|
if (kms != null && kms is! num && double.tryParse('$kms') == null) {
|
|
return null;
|
|
}
|
|
rows.add(RiderWeeklyKms(day: day, kms: _num(kms)));
|
|
}
|
|
return rows.isEmpty ? null : rows;
|
|
}
|
|
|
|
/// `2026-08-19` → `Tue`. Any other non-empty string is returned as it came.
|
|
static String _dayLabel(dynamic raw) {
|
|
final s = raw?.toString().trim() ?? '';
|
|
if (s.isEmpty) return '';
|
|
final parsed = DateTime.tryParse(s);
|
|
if (parsed != null) return _days[parsed.weekday - 1];
|
|
return s;
|
|
}
|
|
|
|
/// The last seven days, oldest first, plus the week's total.
|
|
///
|
|
/// [userId] is unused: the token identifies the rider, and asking for someone
|
|
/// else's kilometres is not a thing the endpoint offers. Kept on the
|
|
/// signature because the call sites read better for naming whose distance
|
|
/// they mean.
|
|
Future<Map<String, dynamic>> getRiderWeeklyKms(int userId) async {
|
|
final weekly = await MilerApi.earnings(period: 'weekly');
|
|
if (!weekly.ok) {
|
|
throw Exception('Failed to fetch (code: ${weekly.status})');
|
|
}
|
|
|
|
final weekTotal = _num(weekly.map['total_kms']);
|
|
|
|
// ── The fast path ──
|
|
//
|
|
// If the server returns a per-day series, take it and spend no further
|
|
// requests. Nothing below runs.
|
|
final fast = _readBreakdown(weekly.map['breakdown']);
|
|
if (fast != null) {
|
|
return {'details': fast, 'total_kms': weekTotal};
|
|
}
|
|
|
|
// Seven days ending today, asked at once.
|
|
final today = DateTime.now();
|
|
final dates = <DateTime>[
|
|
for (var back = 6; back >= 0; back--)
|
|
DateTime(today.year, today.month, today.day - back),
|
|
];
|
|
|
|
final results = await Future.wait(
|
|
dates.map((d) => MilerApi.earnings(period: 'daily', date: _iso(d))),
|
|
);
|
|
|
|
final details = <RiderWeeklyKms>[];
|
|
var summed = 0.0;
|
|
for (final (i, res) in results.indexed) {
|
|
// A day that failed is a day with no figure, not a zero worth charting
|
|
// against the others — but the bar still has to exist or the week is six
|
|
// days long and the labels slide. Zero, and the total below is what
|
|
// corrects for it.
|
|
final km = res.ok ? _num(res.map['total_kms']) : 0.0;
|
|
summed += km;
|
|
details.add(RiderWeeklyKms(day: _days[dates[i].weekday - 1], kms: km));
|
|
}
|
|
|
|
return {
|
|
'details': details,
|
|
// The weekly total is the server's own where it has one — the seven daily
|
|
// figures are a reconstruction, and a reconstruction should not overrule
|
|
// the number the backend computed. It stands in only when the weekly call
|
|
// reported nothing.
|
|
'total_kms': weekTotal > 0 ? weekTotal : summed,
|
|
};
|
|
}
|
|
}
|