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.
93 lines
2.9 KiB
Dart
93 lines
2.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../core/models.dart';
|
|
import '../../core/theme.dart';
|
|
import '../../widgets/ui.dart';
|
|
|
|
class ShopPicker extends StatelessWidget {
|
|
const ShopPicker({
|
|
super.key,
|
|
required this.shops,
|
|
required this.selected,
|
|
required this.onToggle,
|
|
});
|
|
final List<Shop> shops;
|
|
final Set<int> selected;
|
|
final ValueChanged<int> onToggle;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (shops.isEmpty) {
|
|
return Text(
|
|
'No active shops available. Add a shop first.',
|
|
style: ts(13, c: C.muted),
|
|
);
|
|
}
|
|
return Column(
|
|
children: [
|
|
for (final s in shops)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: S.s8),
|
|
child: Pressable(
|
|
onTap: () => onToggle(s.id),
|
|
radius: R.input,
|
|
builder: (_, hover) => AnimatedContainer(
|
|
duration: const Duration(milliseconds: 140),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: S.s14,
|
|
vertical: S.s12,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: selected.contains(s.id)
|
|
? C.chip
|
|
: (hover ? C.hover : Colors.white),
|
|
borderRadius: BorderRadius.circular(R.input),
|
|
border: Border.all(
|
|
color: selected.contains(s.id) ? C.navy : C.border,
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 24,
|
|
height: 24,
|
|
decoration: BoxDecoration(
|
|
color: selected.contains(s.id) ? C.navy : Colors.white,
|
|
borderRadius: BorderRadius.circular(R.sm),
|
|
border: Border.all(
|
|
color: selected.contains(s.id) ? C.navy : C.border2,
|
|
width: 1.5,
|
|
),
|
|
),
|
|
child: selected.contains(s.id)
|
|
? const Icon(
|
|
Icons.check_rounded,
|
|
size: 16,
|
|
color: Colors.white,
|
|
)
|
|
: null,
|
|
),
|
|
const SizedBox(width: S.s12),
|
|
Text(
|
|
s.code,
|
|
style: ts(13, w: w7, c: C.navy),
|
|
),
|
|
const SizedBox(width: S.s10),
|
|
Expanded(
|
|
child: Text(
|
|
s.name,
|
|
style: ts(14, w: w5),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|