srs/lib/features/shops/shop_detail_screen.dart
Aswin Biju 45fcdcebfb Initial commit
Add SRS app: results, shops, orders and users, wired to srs-api
Flutter web/PWA for the lottery agency, built on Riverpod and go_router,
talking to srs-api over HTTP (HttpApi, token kept in SharedPreferences).

- Results, Shops, Orders and Users tabs (Users for admins only), with
  phone, tablet and desktop layouts and shared widgets and theme tokens
- Users: list with search and Active/Inactive filters; user detail with
  shops, sign-in details, and an Access card
- WhatsApp results switch on each user (sendResults on
  PATCH /api/users/:id). Shows the WhatsApp logo (Simple Icons, CC0,
  drawn with flutter_svg), green only when results will really go out.
  Can't be turned on without a phone number ("Add a phone number first"
  opens the phone sheet), and is paused while the user is inactive. The
  users list marks people who get results with the logo.
- Each Access switch has a one-line description; the explanation
  paragraphs are gone
- Remove demo mode: the app always uses the real AP
  test/support and is used only by tests.
2026-11-04 23:21:50 +05:30

295 lines
9.1 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: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/ui.dart';
import 'shop_sheet.dart';
class ShopDetailScreen extends ConsumerWidget {
const ShopDetailScreen({super.key, required this.shopId});
final int shopId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final date = ref.watch(selectedDateProvider);
final detail = ref.watch(shopDetailProvider((shopId, date)));
final isAdmin = ref.watch(sessionProvider)?.isAdmin ?? false;
return Scaffold(
body: SafeArea(
child: asyncView(
detail,
(d) => Column(
children: [
ScreenHeader(
title: d.shop.name,
back: true,
maxWidth: _maxW,
trailing: isAdmin
? RoundIcon(
Icons.edit_outlined,
label: 'Edit shop',
onTap: () => showShopSheet(
context,
shop: d.shop,
onDeactivated: () {
if (context.mounted) context.go('/shops');
},
),
)
: null,
),
Expanded(
child: _Body(d: d, date: date),
),
],
),
retry: () => ref.invalidate(shopDetailProvider((shopId, date))),
),
),
);
}
}
const _maxW = 960.0;
class _Body extends StatelessWidget {
const _Body({required this.d, required this.date});
final ShopDetail d;
final DateTime date;
@override
Widget build(BuildContext context) {
final out = d.draw.published;
final wins = d.wins;
return PageList(
maxWidth: _maxW,
top: 0,
children: [
Row(
children: [
Pill(d.shop.code, bg: C.chip, fg: C.navy),
const SizedBox(width: S.s10),
Expanded(
child: Text(
'${d.draw.label} · ${dateShort(date)}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: ts(13, c: C.muted),
),
),
],
),
const SizedBox(height: S.s16),
if (out)
NavyCard(
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'WINNING ENDINGS',
style: ts(10, w: w6, c: C.softOnNavy, ls: 1),
),
const SizedBox(height: S.s8),
Text(
'${wins.length}',
style: ts(34, w: w7, c: Colors.white, ls: -0.5),
),
const SizedBox(height: S.s2),
Text(
'of ${inr(d.endings.length)} endings',
style: ts(12, c: C.softOnNavy),
),
],
),
),
Container(width: 1, height: 64, color: C.onNavy20),
const SizedBox(width: S.s22),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'PRIZE AMOUNT',
style: ts(10, w: w6, c: C.softOnNavy, ls: 1),
),
const SizedBox(height: S.s8),
Text(
rupees(d.prize),
style: ts(
30,
w: w7,
c: d.prize > 0 ? C.gold : C.softOnNavy,
ls: -0.5,
),
),
const SizedBox(height: S.s2),
Text(
plural(d.winTickets, 'winning ticket'),
style: ts(12, c: C.softOnNavy),
),
],
),
),
],
),
)
else
NavyCard(
child: Row(
children: [
const Icon(Icons.schedule_rounded, color: C.gold, size: 28),
const SizedBox(width: S.s14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Results not yet published',
style: ts(18, w: w7, c: Colors.white),
),
const SizedBox(height: S.s4),
Text(
'Prize details will appear here once results are published.',
style: ts(12, c: C.softOnNavy),
),
],
),
),
],
),
),
const SizedBox(height: S.s12),
Row(
children: [
Expanded(
child: StatCard(label: 'TICKETS', value: inr(d.tickets)),
),
const SizedBox(width: S.s12),
Expanded(
child: StatCard(
label: 'PRIZE WON',
value: out ? rupees(d.prize) : '—',
valueColor: out && d.prize > 0 ? C.green : C.muted,
),
),
],
),
if (out) ...[
SectionTitle(
'Winning endings',
trailing: wins.isEmpty ? null : '${wins.length}',
),
if (wins.isEmpty)
const SrsCard(
child: EmptyState(
icon: Icons.sentiment_neutral_rounded,
title: 'No winning endings',
body:
'None of this shop’s endings matched a prize in this draw.',
),
)
else
CardGrid(
minItemWidth: 320,
maxColumns: 2,
children: [for (final w in wins) _WinRow(w: w)],
),
],
const SizedBox(height: S.s14),
SrsCard(
onTap: d.tickets == 0
? null
: () => context.push('/shops/${d.shop.id}/tickets'),
child: Row(
children: [
Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: C.chip,
borderRadius: BorderRadius.circular(R.md),
),
child: const Icon(
Icons.confirmation_number_outlined,
color: C.navy,
size: 22,
),
),
const SizedBox(width: S.s14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Tickets · ${inr(d.tickets)}', style: ts(15, w: w6)),
const SizedBox(height: S.s2),
Text(
d.tickets == 0
? 'No orders for this draw'
: '${plural(d.endings.length, 'ending')} · ${plural(d.orders.length, 'order')}',
style: ts(12, c: C.muted),
),
],
),
),
if (d.tickets > 0)
const Icon(Icons.chevron_right_rounded, color: C.placeholder),
],
),
),
],
);
}
}
class _WinRow extends StatelessWidget {
const _WinRow({required this.w});
final EndingQty w;
@override
Widget build(BuildContext context) => SrsCard(
padding: const EdgeInsets.all(S.s14),
child: Row(
children: [
Container(
width: 62,
height: 46,
alignment: Alignment.center,
decoration: BoxDecoration(
color: C.greenTint,
borderRadius: BorderRadius.circular(R.input),
),
child: Text(
w.ending,
style: ts(17, w: w7, c: C.green, ls: 0.5),
),
),
const SizedBox(width: S.s14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('× ${plural(w.qty, 'ticket')}', style: ts(14, w: w6)),
const SizedBox(height: S.s4),
Text(
'${w.tierText} · ${rupees(w.perTicket)} each',
style: ts(12, c: C.muted),
),
],
),
),
Text(
rupees(w.prize),
style: ts(16, w: w7, c: C.green),
),
],
),
);
}