Files
doormile_milderapp/lib/data/rider_tier.dart
2026-08-11 13:16:33 +05:30

173 lines
6.3 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<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 => 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<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;
}
}