production
This commit is contained in:
@@ -1,31 +1,159 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:miler/Models/summary/riderweeklykms.dart';
|
||||
import 'package:miler/data/miler_api.dart';
|
||||
import 'package:miler/views/helpers/constants/apiconstants.dart';
|
||||
|
||||
class RiderWeeklyKmController {
|
||||
final String baseUrl = ApiConstants.summaryriderkmLive;
|
||||
|
||||
/// `GET /miler/earnings?period=weekly` → `data:{ total_kms, breakdown }`.
|
||||
///
|
||||
/// [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 provider above passes it and the call sites read
|
||||
/// better for naming whose distance they mean.
|
||||
Future<Map<String, dynamic>> getRiderWeeklyKms(int userId) async {
|
||||
final res = await MilerApi.earnings(period: 'weekly');
|
||||
if (!res.ok) {
|
||||
throw Exception('Failed to fetch (code: ${res.status})');
|
||||
}
|
||||
final breakdown = res.map['breakdown'];
|
||||
final details = (breakdown is List ? breakdown : const [])
|
||||
.whereType<Map>()
|
||||
.map((e) => RiderWeeklyKms.fromJson(Map<String, dynamic>.from(e)))
|
||||
.toList();
|
||||
return {
|
||||
'details': details,
|
||||
'total_kms': (res.map['total_kms'] ?? 0).toDouble(),
|
||||
};
|
||||
}
|
||||
}
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user