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 _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? _readBreakdown(dynamic raw) { if (raw is! List || raw.isEmpty) return null; final rows = []; 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> 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 = [ 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 = []; 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, }; } }