Files
doormile_milderapp/lib/data/rider_tier.dart
Thiru-tenext d7348e253f Miler rider app: surface system, visible design language, backend lifecycle
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>
2026-08-22 05:40:35 +05:30

174 lines
6.4 KiB
Dart
Raw 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: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;
}
}