production
This commit is contained in:
@@ -8,6 +8,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:miler/Models/summary/riderweeklykms.dart';
|
||||
import 'package:miler/views/helpers/constants/Font_constant.dart';
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:miler/views/helpers/constants/narrative.dart';
|
||||
import 'package:miler/views/helpers/widgets/miler_app_bar.dart';
|
||||
import 'package:miler/controllers/summary_controller.dart';
|
||||
import 'package:miler/controllers/rewards_controller.dart';
|
||||
@@ -92,59 +93,89 @@ class _SummaryState extends State<Summary> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: ColorConstants.surface,
|
||||
// ── The same ground and the same curve as the four tabs ──
|
||||
//
|
||||
// It was `surface`, the warm off-white, on a page with no sheet at all —
|
||||
// so the one screen a rider opens to look at his money was the one screen
|
||||
// that did not belong to the app around it. The brand behind the sheet's
|
||||
// two top corners, the canvas under the cards, the gutter and the radii
|
||||
// are all the shared ones now.
|
||||
backgroundColor: ColorConstants.primary,
|
||||
// Earnings is no longer a tab — it opens from Account — so it needs a way
|
||||
// back that the four tabs never did.
|
||||
appBar: const MilerAppBar(title: 'Earnings', showBack: true),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
color: ColorConstants.primary,
|
||||
child: CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(
|
||||
parent: BouncingScrollPhysics(),
|
||||
),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 16.h),
|
||||
_buildPeriodTabs(),
|
||||
SizedBox(height: 16.h),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: _buildPickupsHero(),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: _buildStatsGrid(),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: TotalDistanceCard(
|
||||
userId: _userId,
|
||||
refreshTick: _refreshTick,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 32.h),
|
||||
],
|
||||
),
|
||||
// `pageColor`: this Scaffold is brand-red, so the curve names the sheet
|
||||
// under it instead — see [milerBrandSurface].
|
||||
appBar: const MilerAppBar(
|
||||
title: 'Earnings',
|
||||
showBack: true,
|
||||
pageColor: ColorConstants.pureSurface,
|
||||
),
|
||||
body: MilerSheet(
|
||||
// White, like Home, Activity and the record page. It was
|
||||
// [Narrative.canvas] — a grey ground for panels that carry a hairline
|
||||
// and a lift of their own, so the grey was a second separator under
|
||||
// things that already had one. Four screens, one colour.
|
||||
color: ColorConstants.pureSurface,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _refreshData,
|
||||
color: ColorConstants.primary,
|
||||
child: CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(
|
||||
parent: BouncingScrollPhysics(),
|
||||
),
|
||||
],
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 14.h),
|
||||
_buildPeriodTabs(),
|
||||
SizedBox(height: Narrative.gapBlock.h),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: Narrative.gutter.w,
|
||||
),
|
||||
child: _buildPickupsHero(),
|
||||
),
|
||||
SizedBox(height: Narrative.gapBlock.h),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: Narrative.gutter.w,
|
||||
),
|
||||
child: _buildStatsGrid(),
|
||||
),
|
||||
SizedBox(height: Narrative.gapBlock.h),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: Narrative.gutter.w,
|
||||
),
|
||||
child: TotalDistanceCard(
|
||||
userId: _userId,
|
||||
refreshTick: _refreshTick,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 32.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPeriodTabs() {
|
||||
// The app's segmented control, the same object as Activity's filter and
|
||||
// Home's trip tabs: a tonal track, one white pill, the selected label in
|
||||
// the brand. It was a bordered white bar with a *filled maroon* segment —
|
||||
// a third idiom for the one interaction, and the only place in the app
|
||||
// where a selected tab was a solid brand slab.
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
height: 44.h,
|
||||
margin: EdgeInsets.symmetric(horizontal: Narrative.gutter.w),
|
||||
height: 46.h,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.pureSurface,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
border: Border.all(color: ColorConstants.outlineVariant),
|
||||
color: Narrative.insetDeep,
|
||||
borderRadius: BorderRadius.circular(Narrative.radiusPill),
|
||||
),
|
||||
child: Row(
|
||||
children: List.generate(_periods.length, (index) {
|
||||
@@ -158,23 +189,23 @@ class _SummaryState extends State<Summary> {
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
child: AnimatedContainer(
|
||||
duration: DesignConstants.motionState,
|
||||
curve: Curves.easeOutCubic,
|
||||
margin: EdgeInsets.all(4.r),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? ColorConstants.primary
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusLg),
|
||||
color: isSelected ? Narrative.card : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(Narrative.radiusPill),
|
||||
boxShadow: isSelected ? Narrative.cardShadow : null,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
_periods[index],
|
||||
style: TextStyle(
|
||||
fontSize: 13.sp,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: ColorConstants.onSurfaceVariant,
|
||||
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
|
||||
letterSpacing: -0.1,
|
||||
color: isSelected ? Narrative.burgundy : Narrative.inkMuted,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
@@ -203,11 +234,14 @@ class _SummaryState extends State<Summary> {
|
||||
// not an effect. The two faint circles behind it survive: they are what
|
||||
// stops a solid maroon slab reading as a error state.
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.primary,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
color: Narrative.burgundy,
|
||||
borderRadius: BorderRadius.circular(Narrative.radiusCard),
|
||||
// The one celebratory surface in the app, and the only place the
|
||||
// brand's own glow is spent.
|
||||
boxShadow: Narrative.burgundyGlow,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
borderRadius: BorderRadius.circular(Narrative.radiusCard),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
@@ -317,14 +351,30 @@ class _SummaryState extends State<Summary> {
|
||||
icon: LucideIcons.circleMinus,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: _buildHeroMetric(
|
||||
'$points',
|
||||
'Reward pts',
|
||||
icon: LucideIcons.star,
|
||||
// ── The reward tile is withheld, not zeroed ──
|
||||
//
|
||||
// `bonuspoints` is never written — confirmed by the
|
||||
// backend on 24 Aug, along with the fact that
|
||||
// `ridercharges` holds the *client's* order price rather
|
||||
// than any rider payout, and that a real rate-card is a
|
||||
// separate build. A tile reading **0 Reward pts** on
|
||||
// every screen of every shift does not say "not built
|
||||
// yet"; it says "you have earned nothing", which is a
|
||||
// worse thing to tell a rider than saying nothing.
|
||||
//
|
||||
// It comes back the moment the figure is real: the tile
|
||||
// is drawn whenever the source is non-zero, so no code
|
||||
// has to change when the backend starts writing it.
|
||||
if (points > 0) ...[
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: _buildHeroMetric(
|
||||
'$points',
|
||||
'Reward pts',
|
||||
icon: LucideIcons.star,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -437,8 +487,15 @@ class _SummaryState extends State<Summary> {
|
||||
// competes with the hero above it for the reader's first look.
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorConstants.cardSurface,
|
||||
// A hairline and a soft lift on white, rather than a tonal fill.
|
||||
// The fill was carrying the separation while the page had a grey
|
||||
// ground to sit on; on white it was a fourth surface tone doing
|
||||
// what an edge does more quietly. Same recipe as every other card
|
||||
// in the app — see [Narrative.cardBox].
|
||||
color: Narrative.card,
|
||||
borderRadius: BorderRadius.circular(DesignConstants.radiusXl),
|
||||
border: Border.all(color: Narrative.cardBorder),
|
||||
boxShadow: Narrative.cardShadow,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 16.h),
|
||||
child: IntrinsicHeight(
|
||||
@@ -575,6 +632,15 @@ class _TotalDistanceCardState extends State<TotalDistanceCard> {
|
||||
|
||||
String _shortDay(String day) => day.length <= 3 ? day : day.substring(0, 3);
|
||||
|
||||
/// `Mon` … `Sun`, for matching a chart row against the calendar.
|
||||
///
|
||||
/// Written out rather than taken from `intl`: the series' own day names are
|
||||
/// whatever the backend sends, and the only thing being compared is the first
|
||||
/// three letters — so a locale-aware formatter would introduce a mismatch
|
||||
/// this comparison cannot have.
|
||||
String _weekdayName(DateTime d) =>
|
||||
const ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][d.weekday - 1];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -713,16 +779,74 @@ class _TotalDistanceCardState extends State<TotalDistanceCard> {
|
||||
|
||||
Widget _buildChart(List<RiderWeeklyKms> details, double totalKms) {
|
||||
final values = details.map((e) => e.kms).toList();
|
||||
final maxVal = values.reduce((a, b) => a > b ? a : b);
|
||||
final safeMax = maxVal <= 0 ? 1.0 : maxVal;
|
||||
final maxVal = values.isEmpty
|
||||
? 0.0
|
||||
: values.reduce((a, b) => a > b ? a : b);
|
||||
|
||||
// Best day + averages for the summary row.
|
||||
int bestIdx = 0;
|
||||
for (int i = 1; i < values.length; i++) {
|
||||
if (values[i] > values[bestIdx]) bestIdx = i;
|
||||
// ── A week with nothing in it says so ──
|
||||
//
|
||||
// Seven bars of zero under a chart frame is not an empty state, it is a
|
||||
// chart that looks broken — and it looked exactly like the bug this page
|
||||
// had for months, where the series was never being read at all. An axis
|
||||
// with nothing on it should be replaced, not drawn.
|
||||
if (maxVal <= 0) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 34.h),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
LucideIcons.chartNoAxesColumn,
|
||||
size: 28.sp,
|
||||
color: ColorConstants.secondaryText,
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
Text(
|
||||
'No distance recorded this week',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ColorConstants.slateText,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
'Kilometres appear here once a stop is completed.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12.5.sp,
|
||||
height: 1.4,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: ColorConstants.secondaryText,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final safeMax = maxVal;
|
||||
|
||||
final avgPerDay = details.isEmpty ? 0.0 : totalKms / details.length;
|
||||
|
||||
// ── Today, from the same series the chart is drawn from ──
|
||||
//
|
||||
// Matched on the day name rather than taken from a second endpoint: the
|
||||
// bars and this figure have to agree, and the only way to guarantee that is
|
||||
// for them to be the same number. A week whose rows carry no day names —
|
||||
// which happens, and is why `Best day` used to render blank — simply has no
|
||||
// today to point at, and the row prints the em dash.
|
||||
final today = _shortDay(_weekdayName(DateTime.now())).toLowerCase();
|
||||
RiderWeeklyKms? todayRow;
|
||||
for (final d in details) {
|
||||
if (_shortDay(d.day).toLowerCase() == today) {
|
||||
todayRow = d;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
@@ -843,17 +967,28 @@ class _TotalDistanceCardState extends State<TotalDistanceCard> {
|
||||
//
|
||||
// These were amber / brand-red / blue — three hues for three ledger
|
||||
// facts, on the one page family where colour is reserved for the hero
|
||||
// and the chart's own bars. And "Best Day" rendered blank when the
|
||||
// week's data carries no day names, leaving a caption with nothing
|
||||
// above it; an absent fact prints the em dash like everywhere else.
|
||||
// and the chart's own bars.
|
||||
//
|
||||
// ── `Best day` gave way to `Today` ──
|
||||
//
|
||||
// Best day named a weekday — `Wed` — which is a fact about the past
|
||||
// that a rider can do nothing with, and it rendered blank whenever the
|
||||
// week's rows carried no day names. What he opens this page to find out
|
||||
// is how far he has ridden *today*, and until now the app said that
|
||||
// nowhere: the distance was per-stop on Activity and per-week here,
|
||||
// with nothing in between.
|
||||
//
|
||||
// It also puts the same figure on two screens — this and the Activity
|
||||
// hero — which is deliberate: they are read at different moments, and
|
||||
// both take it from a measurement rather than from each other.
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildChartStat(
|
||||
'Best day',
|
||||
_shortDay(details[bestIdx].day).trim().isEmpty
|
||||
'Today',
|
||||
todayRow == null
|
||||
? '\u2014'
|
||||
: _shortDay(details[bestIdx].day),
|
||||
: '${todayRow.kms.toStringAsFixed(1)} km',
|
||||
ColorConstants.slateText,
|
||||
),
|
||||
_buildChartStat(
|
||||
|
||||
Reference in New Issue
Block a user