Design system - MilerSurface ladder (canvas → working → raised → floating) with MilerPanel as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1. - Visible vocabulary applied across Home, Deliveries, Activity, Account and the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x), canvas wells for anything that opens, small filled tags for shelf labels, demoted placeholders. Recorded in DESIGN_SYSTEM.md §6. - One icon family: 222 Material glyphs migrated to Lucide; none left outside lib/xpress. - Colour semantics corrected: amber only for what is genuinely owed, brand red reserved for the live stop, disabled primaries go neutral rather than pale. Data and lifecycle - lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart makes admin sequence the single ordering authority; service_day.dart, and stop_area.dart rewritten against live Coimbatore addresses (digit-token stripping, city stoplist, street suffixes, stammer collapse). - countLabel states the load once, in bags. Testing - 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity, sheets and verify, with test/failures/ now gitignored (diff debris). - New pins: home_gutter_test, stop_area_test, plus updated structural bounds. Note: this commit also carries pre-existing working-tree deletions that were present before this work (API_SPEC.md, README.md, demo test fixtures). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
174 lines
6.4 KiB
Dart
174 lines
6.4 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||
|
||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||
|
||
/// ─────────────────────────────────────────────────────────────────────────
|
||
/// RIDER STATUS — the tier ladder the Rewards page is built around
|
||
///
|
||
/// Modelled on Uber Pro, which is the version of this a rider is most likely to
|
||
/// have already used: four named statuses, points earned per completed job over
|
||
/// a fixed reward period, and quality standards that gate the higher tiers
|
||
/// regardless of volume. Every loyalty-UX source says the same three things
|
||
/// belong on the screen before anything else — where you stand, what you have,
|
||
/// and exactly how far the next step is — so those are the three this model has
|
||
/// to be able to answer.
|
||
///
|
||
/// ── What is real here and what the business still owns ──
|
||
///
|
||
/// The *points* are real: the backend already awards them per stop closed
|
||
/// inside its ETA, scaled by the distance ridden, and `RewardsController` reads
|
||
/// that total. The *standards* are real: on-time and on-route are measured per
|
||
/// stop (see `StopCompliance`).
|
||
///
|
||
/// The **thresholds below are placeholders**, and so is the idea that a tier
|
||
/// unlocks anything. Where the bar sits, and what a rider gets for clearing it,
|
||
/// are commercial decisions belonging to the hub — not to this file. They live
|
||
/// in one place, named, so a real number drops straight in. Nothing in the UI
|
||
/// invents a perk: the page states status, progress and standards, and says
|
||
/// plainly that the hub sets what each status is worth.
|
||
/// ─────────────────────────────────────────────────────────────────────────
|
||
enum RiderTier { blue, silver, gold, platinum }
|
||
|
||
/// Points needed to reach each tier within one reward period.
|
||
///
|
||
/// PLACEHOLDER — replace with the hub's real ladder. Kept as a single map so
|
||
/// the whole ladder can be swapped without touching the page.
|
||
const Map<RiderTier, int> kTierThresholds = {
|
||
RiderTier.blue: 0,
|
||
RiderTier.silver: 150,
|
||
RiderTier.gold: 400,
|
||
RiderTier.platinum: 800,
|
||
};
|
||
|
||
/// The share of measured stops that must clear a check for the status to hold.
|
||
///
|
||
/// PLACEHOLDER, same as above. Uber gates its tiers on acceptance and
|
||
/// cancellation rates; the equivalents here are the two the hub actually
|
||
/// measures — did he make the ETA, did he ride the route.
|
||
const int kStandardTarget = 80;
|
||
|
||
extension RiderTierUi on RiderTier {
|
||
String get label => switch (this) {
|
||
RiderTier.blue => 'Blue',
|
||
RiderTier.silver => 'Silver',
|
||
RiderTier.gold => 'Gold',
|
||
RiderTier.platinum => 'Platinum',
|
||
};
|
||
|
||
/// The tier above this one, or null at the top.
|
||
RiderTier? get next => switch (this) {
|
||
RiderTier.blue => RiderTier.silver,
|
||
RiderTier.silver => RiderTier.gold,
|
||
RiderTier.gold => RiderTier.platinum,
|
||
RiderTier.platinum => null,
|
||
};
|
||
|
||
int get threshold => kTierThresholds[this] ?? 0;
|
||
|
||
IconData get icon => switch (this) {
|
||
RiderTier.blue => LucideIcons.bike,
|
||
RiderTier.silver => LucideIcons.medal,
|
||
RiderTier.gold => LucideIcons.award,
|
||
RiderTier.platinum => LucideIcons.gem,
|
||
};
|
||
|
||
/// Colour of the badge, not of the page. The card behind it stays brand
|
||
/// maroon at every tier — a status screen that changes colour scheme as the
|
||
/// rider moves up stops looking like the same product.
|
||
Color get accent => switch (this) {
|
||
RiderTier.blue => const Color(0xFF7FB3D5),
|
||
RiderTier.silver => const Color(0xFFC0C6CC),
|
||
RiderTier.gold => ColorConstants.rewardAmber,
|
||
RiderTier.platinum => const Color(0xFFB9F2FF),
|
||
};
|
||
}
|
||
|
||
/// Where a rider stands this period, and how far the next step is.
|
||
class TierStanding {
|
||
final int points;
|
||
final RiderTier tier;
|
||
|
||
const TierStanding({required this.points, required this.tier});
|
||
|
||
static TierStanding of(int points) {
|
||
var reached = RiderTier.blue;
|
||
for (final tier in RiderTier.values) {
|
||
if (points >= tier.threshold) reached = tier;
|
||
}
|
||
return TierStanding(points: points, tier: reached);
|
||
}
|
||
|
||
RiderTier? get nextTier => tier.next;
|
||
|
||
/// Points still needed for the next status. Null at the top.
|
||
int? get pointsToNext {
|
||
final next = nextTier;
|
||
if (next == null) return null;
|
||
final gap = next.threshold - points;
|
||
return gap <= 0 ? 0 : gap;
|
||
}
|
||
|
||
/// 0–1 across the current band, for the progress bar.
|
||
///
|
||
/// Measured from *this* tier's threshold rather than from zero: a rider one
|
||
/// point into Gold should see an empty bar to Platinum, not a bar that is
|
||
/// already half full because he has a big lifetime number.
|
||
double get progressToNext {
|
||
final next = nextTier;
|
||
if (next == null) return 1;
|
||
final floor = tier.threshold;
|
||
final span = next.threshold - floor;
|
||
if (span <= 0) return 1;
|
||
return ((points - floor) / span).clamp(0.0, 1.0);
|
||
}
|
||
}
|
||
|
||
/// The reward period: the calendar month.
|
||
///
|
||
/// Uber runs three-month periods. A month is the right length here because a
|
||
/// miler's shift patterns and the hub's rosters are monthly, and because a
|
||
/// three-month bar is one a rider cannot move in a day — the whole point of
|
||
/// showing progress is that today's work visibly changes it.
|
||
class RewardPeriod {
|
||
final DateTime start;
|
||
final DateTime end;
|
||
|
||
const RewardPeriod({required this.start, required this.end});
|
||
|
||
static RewardPeriod current([DateTime? now]) {
|
||
final t = now ?? DateTime.now();
|
||
return RewardPeriod(
|
||
start: DateTime(t.year, t.month, 1),
|
||
// Day zero of next month is the last day of this one.
|
||
end: DateTime(t.year, t.month + 1, 0),
|
||
);
|
||
}
|
||
|
||
static const List<String> _months = [
|
||
'January',
|
||
'February',
|
||
'March',
|
||
'April',
|
||
'May',
|
||
'June',
|
||
'July',
|
||
'August',
|
||
'September',
|
||
'October',
|
||
'November',
|
||
'December',
|
||
];
|
||
|
||
String get label => '${_months[start.month - 1]} ${start.year}';
|
||
|
||
/// Whole days left, today included. Never negative.
|
||
int daysLeft([DateTime? now]) {
|
||
final t = now ?? DateTime.now();
|
||
final today = DateTime(t.year, t.month, t.day);
|
||
final last = DateTime(end.year, end.month, end.day);
|
||
final diff = last.difference(today).inDays + 1;
|
||
return diff < 0 ? 0 : diff;
|
||
}
|
||
}
|