srs/lib/features/shops/tickets_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

174 lines
4.9 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 '../orders/order_card.dart';
class TicketsScreen extends ConsumerStatefulWidget {
const TicketsScreen({super.key, required this.shopId});
final int shopId;
@override
ConsumerState<TicketsScreen> createState() => _TicketsScreenState();
}
class _TicketsScreenState extends ConsumerState<TicketsScreen> {
bool _byEnding = true;
String _q = '';
@override
Widget build(BuildContext context) {
final date = ref.watch(selectedDateProvider);
final detail = ref.watch(shopDetailProvider((widget.shopId, date)));
return Scaffold(
body: SafeArea(
child: asyncView(
detail,
(d) => Column(
children: [
ScreenHeader(
title: '${d.shop.name} tickets',
subtitle: '${plural(d.tickets, 'ticket')} · ${dateShort(date)}',
back: true,
),
PagePad(
top: S.s4,
bottom: S.s10,
child: Container(
constraints: const BoxConstraints(maxWidth: 420),
padding: const EdgeInsets.all(S.s4),
decoration: BoxDecoration(
color: C.chip,
borderRadius: BorderRadius.circular(R.input),
),
child: Row(
children: [
_seg(
'By ending',
_byEnding,
() => setState(() => _byEnding = true),
),
_seg(
'By order',
!_byEnding,
() => setState(() => _byEnding = false),
),
],
),
),
),
if (_byEnding)
PagePad(
bottom: S.s8,
child: SearchField(
hint: 'Search endings (e.g. 0987)',
onChanged: (v) => setState(() => _q = v.trim()),
),
),
Expanded(child: _byEnding ? _endings(d) : _orders(d)),
],
),
retry: () =>
ref.invalidate(shopDetailProvider((widget.shopId, date))),
),
),
);
}
Widget _seg(String t, bool on, VoidCallback tap) => Expanded(
child: Pressable(
onTap: tap,
radius: R.md,
builder: (_, hover) => AnimatedContainer(
duration: const Duration(milliseconds: 160),
height: 38,
alignment: Alignment.center,
decoration: BoxDecoration(
color: on ? Colors.white : (hover ? C.hover : Colors.transparent),
borderRadius: BorderRadius.circular(R.md),
),
child: Text(
t,
style: ts(13, w: on ? w7 : w5, c: on ? C.navy : C.muted),
),
),
),
);
Widget _endings(ShopDetail d) {
final list = d.endings
.where((e) => _q.isEmpty || e.ending.contains(_q))
.toList();
if (list.isEmpty) {
return ListView(
children: const [
EmptyState(
icon: Icons.search_off_rounded,
title: 'No matching endings',
),
],
);
}
return PageList(
children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: [for (final e in list) _chip(e)],
),
],
);
}
Widget _chip(EndingQty e) {
final win = e.wins;
return Container(
padding: const EdgeInsets.symmetric(horizontal: S.s12, vertical: S.s8),
decoration: BoxDecoration(
color: win ? C.greenTint : Colors.white,
borderRadius: BorderRadius.circular(R.md),
border: Border.all(color: win ? C.green : C.border),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
e.ending,
style: ts(14, w: w7, c: win ? C.green : C.ink, ls: 0.4),
),
const SizedBox(width: S.s6),
Text(
'×${inr(e.qty)}',
style: ts(12, w: w6, c: win ? C.green : C.muted),
),
],
),
);
}
Widget _orders(ShopDetail d) {
if (d.orders.isEmpty) {
return ListView(
children: const [
EmptyState(icon: Icons.receipt_long_outlined, title: 'No orders'),
],
);
}
return PageList(
children: [
CardGrid(
children: [
for (final o in d.orders)
OrderCard(order: o, onTap: () => context.push('/orders/${o.id}')),
],
),
],
);
}
}