import 'package:flutter/material.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 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 => Icons.pedal_bike_rounded, RiderTier.silver => Icons.military_tech_rounded, RiderTier.gold => Icons.workspace_premium_rounded, RiderTier.platinum => Icons.diamond_rounded, }; /// 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 _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; } }