452 lines
13 KiB
Dart
452 lines
13 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
|
||
import '../../core/format.dart';
|
||
import '../../core/models.dart';
|
||
import '../../core/providers.dart';
|
||
import '../../core/theme.dart';
|
||
import '../../widgets/account_sheet.dart';
|
||
import 'calendar_picker.dart';
|
||
import '../../widgets/ui.dart';
|
||
|
||
class ResultsScreen extends ConsumerWidget {
|
||
const ResultsScreen({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final date = ref.watch(selectedDateProvider);
|
||
final res = ref.watch(resultsProvider(date));
|
||
Future<void> refresh() => refreshData(context, ref, resultsProvider(date));
|
||
return Column(
|
||
children: [
|
||
ScreenHeader(
|
||
title: 'Results',
|
||
trailing: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Builder(
|
||
builder: (anchor) => RoundIcon(
|
||
Icons.calendar_month_rounded,
|
||
label: 'Calendar',
|
||
onTap: () => showCalendarPicker(anchor),
|
||
),
|
||
),
|
||
// Wider windows show the account in the sidebar.
|
||
if (!isWide(context)) ...[
|
||
const SizedBox(width: S.s10),
|
||
RoundIcon(
|
||
Icons.person_outline_rounded,
|
||
label: 'Account',
|
||
onTap: () => showAccountSheet(context, ref),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
Expanded(
|
||
child: RefreshableView(
|
||
value: res,
|
||
onRefresh: refresh,
|
||
skeleton: _Loading(date: date),
|
||
builder: (r) => _Body(results: r, date: date),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _DrawRow extends ConsumerWidget {
|
||
const _DrawRow({required this.date, required this.info});
|
||
final DateTime date;
|
||
final Widget info;
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
final n = ref.read(selectedDateProvider.notifier);
|
||
return Row(
|
||
children: [
|
||
Expanded(child: info),
|
||
RoundIcon(
|
||
Icons.chevron_left_rounded,
|
||
label: 'Previous draw',
|
||
size: 40,
|
||
onTap: () => n.set(addDays(date, -1)),
|
||
),
|
||
const SizedBox(width: S.s8),
|
||
RoundIcon(
|
||
Icons.chevron_right_rounded,
|
||
label: 'Next draw',
|
||
size: 40,
|
||
onTap: () => n.set(addDays(date, 1)),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Skeleton of the Results page.
|
||
class _Loading extends StatelessWidget {
|
||
const _Loading({required this.date});
|
||
final DateTime date;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
const onNavy = C.onNavy8;
|
||
Widget line(double h, Bone bone) => SizedBox(
|
||
height: h,
|
||
child: Align(alignment: Alignment.centerLeft, child: bone),
|
||
);
|
||
Widget stat() => SrsCard(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
line(16, const Bone(width: 72, height: 10)),
|
||
const SizedBox(height: S.s8),
|
||
line(30, const Bone(width: 96, height: 22)),
|
||
],
|
||
),
|
||
);
|
||
return PageList(
|
||
children: [
|
||
_DrawRow(
|
||
date: date,
|
||
info: Skeleton(
|
||
announce: false,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
line(22, const Bone(width: 120, height: 14)),
|
||
const SizedBox(height: S.s2),
|
||
line(18, const Bone(width: 180, height: 10)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: S.s16),
|
||
Skeleton(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
_HeroRow(
|
||
hero: NavyCard(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
line(
|
||
15,
|
||
const Bone(width: 90, height: 10, color: onNavy),
|
||
),
|
||
const SizedBox(height: S.s10),
|
||
line(
|
||
58,
|
||
const Bone(
|
||
width: 200,
|
||
height: 38,
|
||
radius: R.md,
|
||
color: onNavy,
|
||
),
|
||
),
|
||
const SizedBox(height: S.s10),
|
||
line(
|
||
18,
|
||
const Bone(width: 170, height: 10, color: onNavy),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
stats: [stat(), stat()],
|
||
),
|
||
const Padding(
|
||
padding: EdgeInsets.fromLTRB(S.s2, S.s22, S.s2, S.s10),
|
||
child: SizedBox(
|
||
height: 21,
|
||
child: Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Bone(width: 64, height: 14),
|
||
),
|
||
),
|
||
),
|
||
const TileGridBone(count: 4),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _Body extends StatelessWidget {
|
||
const _Body({required this.results, required this.date});
|
||
final DrawResults results;
|
||
final DateTime date;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final r = results;
|
||
final d = r.draw;
|
||
return PageList(
|
||
children: [
|
||
_DrawRow(
|
||
date: date,
|
||
info: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(d.label, style: ts(16, w: w7)),
|
||
const SizedBox(height: S.s2),
|
||
Text(dateLong(d.date), style: ts(13, c: C.muted)),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: S.s16),
|
||
if (d.published) ..._published(context, r) else ..._notOut(context, r),
|
||
],
|
||
);
|
||
}
|
||
|
||
List<Widget> _published(BuildContext context, DrawResults r) {
|
||
final top = r.topShop;
|
||
return [
|
||
_HeroRow(
|
||
hero: NavyCard(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'TOTAL PRIZE',
|
||
style: ts(11, w: w6, c: C.softOnNavy, ls: 1.2),
|
||
),
|
||
const SizedBox(height: S.s10),
|
||
Text(
|
||
rupees(r.totalPrize),
|
||
style: ts(
|
||
40,
|
||
w: w7,
|
||
c: r.totalPrize > 0 ? C.gold : C.softOnNavy,
|
||
ls: -1,
|
||
),
|
||
),
|
||
const SizedBox(height: S.s10),
|
||
Text(
|
||
r.totalPrize > 0
|
||
? '${plural(r.winningTickets, 'winning ticket')} · ${plural(r.winningShops, 'shop')}'
|
||
: 'No winning tickets in this draw',
|
||
style: ts(13, w: w5, c: C.softOnNavy),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
stats: [
|
||
StatCard(label: 'TOTAL TICKETS', value: inr(r.totalTickets)),
|
||
StatCard(
|
||
label: 'TOP SHOP',
|
||
value: top?.shop.code ?? '—',
|
||
valueColor: top == null ? C.muted : C.ink,
|
||
),
|
||
],
|
||
),
|
||
SectionTitle(
|
||
'Shops',
|
||
trailing: r.shops.isEmpty ? null : plural(r.shops.length, 'shop'),
|
||
),
|
||
if (r.shops.isEmpty)
|
||
const SrsCard(
|
||
child: EmptyState(
|
||
icon: Icons.receipt_long_outlined,
|
||
title: 'No orders for this draw',
|
||
body: 'Orders placed for your shops on this date will appear here.',
|
||
),
|
||
)
|
||
else
|
||
CardGrid(children: [for (final s in r.shops) _ShopRow(s: s)]),
|
||
];
|
||
}
|
||
|
||
List<Widget> _notOut(BuildContext context, DrawResults r) {
|
||
return [
|
||
_HeroRow(
|
||
hero: NavyCard(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.fromLTRB(S.s4, S.s4, S.s12, S.s4),
|
||
decoration: BoxDecoration(
|
||
color: C.goldSoft,
|
||
borderRadius: BorderRadius.circular(R.pill),
|
||
border: Border.all(color: C.gold.withAlpha(90)),
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Container(
|
||
width: 20,
|
||
height: 20,
|
||
decoration: const BoxDecoration(
|
||
color: C.gold,
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: const Icon(
|
||
Icons.hourglass_top_rounded,
|
||
size: 12,
|
||
color: C.navy,
|
||
),
|
||
),
|
||
const SizedBox(width: S.s8),
|
||
Text(
|
||
'AWAITING RESULTS',
|
||
style: ts(11, w: w7, c: C.gold, ls: 1.1),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: S.s12),
|
||
Text(
|
||
'Results not yet published',
|
||
style: ts(26, w: w7, c: Colors.white, ls: -0.4),
|
||
),
|
||
const SizedBox(height: S.s8),
|
||
Text(
|
||
'Official results are usually published after 4 PM.',
|
||
style: ts(13, c: C.softOnNavy, h: 1.5),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
stats: [
|
||
StatCard(label: 'ORDERS', value: inr(r.orders)),
|
||
StatCard(label: 'TICKETS', value: inr(r.totalTickets)),
|
||
],
|
||
),
|
||
SectionTitle(
|
||
'Shops',
|
||
trailing: r.shops.isEmpty ? null : plural(r.shops.length, 'shop'),
|
||
),
|
||
if (r.shops.isEmpty)
|
||
const SrsCard(
|
||
child: EmptyState(
|
||
icon: Icons.inbox_outlined,
|
||
title: 'No orders',
|
||
body: 'No orders have been placed for this draw.',
|
||
),
|
||
)
|
||
else
|
||
CardGrid(
|
||
children: [for (final s in r.shops) _ShopRow(s: s, pending: true)],
|
||
),
|
||
];
|
||
}
|
||
}
|
||
|
||
/// The navy hero with its two stat cards: stacked under it on phones,
|
||
/// beside it on wide windows.
|
||
class _HeroRow extends StatelessWidget {
|
||
const _HeroRow({required this.hero, required this.stats});
|
||
final Widget hero;
|
||
final List<Widget> stats;
|
||
|
||
@override
|
||
Widget build(BuildContext context) => LayoutBuilder(
|
||
builder: (context, c) {
|
||
if (c.maxWidth < 760) {
|
||
return Column(
|
||
children: [
|
||
hero,
|
||
const SizedBox(height: S.s12),
|
||
Row(
|
||
children: [
|
||
Expanded(child: stats[0]),
|
||
const SizedBox(width: S.s12),
|
||
Expanded(child: stats[1]),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
}
|
||
return IntrinsicHeight(
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Expanded(flex: 2, child: hero),
|
||
const SizedBox(width: S.s12),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: [
|
||
Expanded(child: stats[0]),
|
||
const SizedBox(height: S.s12),
|
||
Expanded(child: stats[1]),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
class _ShopRow extends StatelessWidget {
|
||
const _ShopRow({required this.s, this.pending = false});
|
||
final ShopResult s;
|
||
final bool pending;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final win = s.prize > 0;
|
||
return SrsCard(
|
||
onTap: () => context.push('/shops/${s.shop.id}'),
|
||
padding: const EdgeInsets.all(S.s14),
|
||
child: Row(
|
||
children: [
|
||
CodeTile(s.shop.code, dark: win),
|
||
const SizedBox(width: S.s14),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
s.shop.name,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: ts(15, w: w6),
|
||
),
|
||
const SizedBox(height: S.s4),
|
||
Text(
|
||
pending
|
||
? plural(s.tickets, 'ticket')
|
||
: win
|
||
? plural(s.winTickets, 'winning ticket')
|
||
: 'No winning tickets',
|
||
style: ts(12, c: C.muted),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
if (!pending)
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.end,
|
||
children: [
|
||
Text(
|
||
rupees(s.prize),
|
||
style: ts(16, w: w7, c: win ? C.green : C.muted),
|
||
),
|
||
if (win) ...[
|
||
const SizedBox(height: S.s2),
|
||
Text(
|
||
plural(s.winTickets, 'ticket'),
|
||
style: ts(11, c: C.muted),
|
||
),
|
||
],
|
||
],
|
||
)
|
||
else
|
||
const Icon(Icons.chevron_right_rounded, color: C.placeholder),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|