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.
140 lines
4.2 KiB
Dart
140 lines
4.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../core/providers.dart';
|
|
import '../../widgets/ui.dart';
|
|
import 'shop_picker.dart';
|
|
import '../../core/theme.dart';
|
|
|
|
const _maxW = 640.0;
|
|
|
|
class UserFormScreen extends ConsumerStatefulWidget {
|
|
const UserFormScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<UserFormScreen> createState() => _UserFormScreenState();
|
|
}
|
|
|
|
class _UserFormScreenState extends ConsumerState<UserFormScreen> {
|
|
final _name = TextEditingController();
|
|
final _phone = TextEditingController();
|
|
final _pass = TextEditingController();
|
|
final _shops = <int>{};
|
|
bool _busy = false;
|
|
String? _error;
|
|
|
|
@override
|
|
void dispose() {
|
|
_name.dispose();
|
|
_phone.dispose();
|
|
_pass.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
bool get _ready => _name.text.trim().length >= 3 && _pass.text.length >= 8;
|
|
|
|
Future<void> _create() async {
|
|
setState(() {
|
|
_busy = true;
|
|
_error = null;
|
|
});
|
|
try {
|
|
await mutate(
|
|
ref,
|
|
() => ref
|
|
.read(apiProvider)
|
|
.createUser(
|
|
username: _name.text,
|
|
phone: _phone.text.trim().isEmpty ? null : _phone.text,
|
|
password: _pass.text,
|
|
shopIds: _shops.toList(),
|
|
),
|
|
);
|
|
if (!mounted) return;
|
|
context.pop();
|
|
toast(context, 'User created');
|
|
} catch (e) {
|
|
if (mounted) setState(() => _error = errText(e));
|
|
} finally {
|
|
if (mounted) setState(() => _busy = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final shops = ref.watch(activeShopsProvider);
|
|
return Scaffold(
|
|
body: SafeArea(
|
|
child: Column(
|
|
children: [
|
|
const ScreenHeader(title: 'Add user', back: true, maxWidth: _maxW),
|
|
Expanded(
|
|
child: PageList(
|
|
maxWidth: _maxW,
|
|
top: S.s8,
|
|
children: [
|
|
SrsField(
|
|
controller: _name,
|
|
label: 'Username',
|
|
hint: 'e.g. kzk.staff',
|
|
helper:
|
|
'Letters, numbers, dots, dashes and underscores. Used to sign in.',
|
|
onChanged: (_) => setState(() => _error = null),
|
|
),
|
|
const SizedBox(height: S.s18),
|
|
SrsField(
|
|
controller: _phone,
|
|
label: 'Phone (optional)',
|
|
hint: '90000 12345',
|
|
prefix: '+91 ',
|
|
keyboard: TextInputType.phone,
|
|
formatters: [
|
|
FilteringTextInputFormatter.allow(RegExp(r'[0-9 ]')),
|
|
LengthLimitingTextInputFormatter(12),
|
|
],
|
|
onChanged: (_) => setState(() => _error = null),
|
|
),
|
|
const SizedBox(height: S.s18),
|
|
SrsField(
|
|
controller: _pass,
|
|
label: 'Password',
|
|
hint: 'Enter password',
|
|
password: true,
|
|
helper: 'At least 8 characters.',
|
|
onChanged: (_) => setState(() => _error = null),
|
|
),
|
|
const SectionTitle('Shop access'),
|
|
asyncView(
|
|
shops,
|
|
(list) => ShopPicker(
|
|
shops: list,
|
|
selected: _shops,
|
|
onToggle: (id) => setState(
|
|
() => _shops.contains(id)
|
|
? _shops.remove(id)
|
|
: _shops.add(id),
|
|
),
|
|
),
|
|
),
|
|
if (_error != null) ...[
|
|
const SizedBox(height: S.s8),
|
|
ErrorLine(_error!),
|
|
],
|
|
const SizedBox(height: S.s22),
|
|
BigButton(
|
|
'Create user',
|
|
loading: _busy,
|
|
onPressed: _ready ? _create : null,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|