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.
108 lines
3.4 KiB
Dart
108 lines
3.4 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import 'core/providers.dart';
|
|
import 'core/theme.dart';
|
|
import 'features/auth/login_screen.dart';
|
|
import 'features/orders/order_detail_screen.dart';
|
|
import 'features/orders/orders_screen.dart';
|
|
import 'features/results/results_screen.dart';
|
|
import 'features/shops/shop_detail_screen.dart';
|
|
import 'features/shops/shops_screen.dart';
|
|
import 'features/shops/tickets_screen.dart';
|
|
import 'features/users/user_detail_screen.dart';
|
|
import 'features/users/user_form_screen.dart';
|
|
import 'features/users/users_screen.dart';
|
|
import 'widgets/main_shell.dart';
|
|
|
|
final routerProvider = Provider<GoRouter>((ref) {
|
|
final refresh = ValueNotifier<int>(0);
|
|
ref.listen(sessionProvider, (_, _) => refresh.value++);
|
|
ref.onDispose(refresh.dispose);
|
|
|
|
Page<void> tab(GoRouterState s, Widget w) =>
|
|
NoTransitionPage<void>(key: s.pageKey, child: w);
|
|
|
|
return GoRouter(
|
|
initialLocation: '/results',
|
|
refreshListenable: refresh,
|
|
redirect: (context, state) {
|
|
final user = ref.read(sessionProvider);
|
|
final loc = state.matchedLocation;
|
|
if (user == null) return loc == '/login' ? null : '/login';
|
|
if (loc == '/login') return '/results';
|
|
if (loc.startsWith('/users') && !user.isAdmin) return '/results';
|
|
return null;
|
|
},
|
|
routes: [
|
|
GoRoute(path: '/login', builder: (_, _) => const LoginScreen()),
|
|
// Every signed-in screen lives in the shell, so the sidebar stays in
|
|
// place on wide windows while detail screens open beside it.
|
|
ShellRoute(
|
|
builder: (_, s, child) =>
|
|
MainShell(location: s.matchedLocation, child: child),
|
|
routes: [
|
|
GoRoute(
|
|
path: '/results',
|
|
pageBuilder: (_, s) => tab(s, const ResultsScreen()),
|
|
),
|
|
GoRoute(
|
|
path: '/shops',
|
|
pageBuilder: (_, s) => tab(s, const ShopsScreen()),
|
|
routes: [
|
|
GoRoute(
|
|
path: ':id',
|
|
builder: (_, s) => ShopDetailScreen(shopId: _id(s)),
|
|
routes: [
|
|
GoRoute(
|
|
path: 'tickets',
|
|
builder: (_, s) => TicketsScreen(shopId: _id(s)),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
GoRoute(
|
|
path: '/orders',
|
|
pageBuilder: (_, s) => tab(s, const OrdersScreen()),
|
|
routes: [
|
|
GoRoute(
|
|
path: ':id',
|
|
builder: (_, s) => OrderDetailScreen(orderId: _id(s)),
|
|
),
|
|
],
|
|
),
|
|
GoRoute(
|
|
path: '/users',
|
|
pageBuilder: (_, s) => tab(s, const UsersScreen()),
|
|
routes: [
|
|
GoRoute(path: 'new', builder: (_, _) => const UserFormScreen()),
|
|
GoRoute(
|
|
path: ':id',
|
|
builder: (_, s) => UserDetailScreen(userId: _id(s)),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
});
|
|
|
|
int _id(GoRouterState s) => int.parse(s.pathParameters['id']!);
|
|
|
|
class SrsApp extends ConsumerWidget {
|
|
const SrsApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return MaterialApp.router(
|
|
title: 'SRS',
|
|
debugShowCheckedModeBanner: false,
|
|
theme: buildTheme(),
|
|
routerConfig: ref.watch(routerProvider),
|
|
);
|
|
}
|
|
}
|