445 lines
13 KiB
Dart
445 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: [
|
|
if (isWide(context)) ...[
|
|
RefreshButton(onRefresh: refresh),
|
|
const SizedBox(width: S.s10),
|
|
],
|
|
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)} · 3:00 PM', 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(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: S.s10,
|
|
vertical: S.s6,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: C.goldSoft,
|
|
borderRadius: BorderRadius.circular(R.pill),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.schedule_rounded, size: 14, color: C.gold),
|
|
const SizedBox(width: S.s6),
|
|
Text(
|
|
'AWAITING RESULTS',
|
|
style: ts(10, w: w7, c: C.gold, ls: 1),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: S.s14),
|
|
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. Prize details will appear here once available.',
|
|
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),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|