srs/lib/features/users/user_form_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

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,
),
],
),
),
],
),
),
);
}
}