srs/lib/features/results/results_screen.dart
Aswin Biju 125f4d3e86
All checks were successful
deploy / deploy (push) Successful in 5m18s
feat: loading skeletons, pull-to-refresh and reload bar on list screens
2026-11-05 08:04:55 +05:30

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),
],
),
);
}
}