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

168 lines
4.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../core/format.dart';
import '../../core/models.dart';
import '../../core/theme.dart';
class UserAvatar extends StatelessWidget {
const UserAvatar({super.key, required this.user, this.size = 42});
final AppUser user;
final double size;
@override
Widget build(BuildContext context) {
final admin = user.isAdmin;
final dot = (size * 0.3).clamp(10.0, 18.0);
return SizedBox(
width: size + 2,
height: size + 2,
child: Stack(
children: [
Opacity(
opacity: user.active ? 1 : 0.55,
child: Container(
width: size,
height: size,
alignment: Alignment.center,
decoration: BoxDecoration(
color: admin ? C.navy : C.chip,
borderRadius: BorderRadius.circular(size * 0.3),
),
child: Text(
initials(user.username),
style: ts(size * 0.36, w: w7, c: admin ? C.gold : C.navy),
),
),
),
Positioned(
right: 0,
bottom: 0,
child: Container(
width: dot,
height: dot,
decoration: BoxDecoration(
color: user.active ? C.green : C.placeholder,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
),
),
],
),
);
}
}
class StatusLabel extends StatelessWidget {
const StatusLabel({super.key, required this.active});
final bool active;
@override
Widget build(BuildContext context) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: active ? C.green : C.placeholder,
shape: BoxShape.circle,
),
),
const SizedBox(width: S.s8),
Text(
active ? 'Active' : 'Inactive',
style: ts(13, w: w5, c: active ? C.ink : C.muted),
),
],
);
}
class ShopCodes extends StatelessWidget {
const ShopCodes({super.key, required this.shops});
final List<Shop> shops;
@override
Widget build(BuildContext context) {
if (shops.isEmpty) {
return Text('No shops', style: ts(13, c: C.placeholder));
}
const tile = 40.0, gap = S.s6, more = 36.0;
return LayoutBuilder(
builder: (context, c) {
var fit = ((c.maxWidth + gap) / (tile + gap)).floor();
if (fit < shops.length) {
fit = ((c.maxWidth - more) / (tile + gap)).floor().clamp(
0,
shops.length,
);
}
final shown = shops.take(fit).toList();
final rest = shops.length - shown.length;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
for (final s in shown)
Padding(
padding: const EdgeInsets.only(right: gap),
child: Tooltip(
message: s.name,
child: Container(
width: tile,
height: 26,
alignment: Alignment.center,
decoration: BoxDecoration(
color: C.chip,
borderRadius: BorderRadius.circular(R.sm),
),
child: Text(
s.code.length > 4 ? s.code.substring(0, 4) : s.code,
style: ts(11, w: w7, c: C.navy, ls: 0.3),
),
),
),
),
if (rest > 0)
Tooltip(
message: shops.skip(shown.length).map((s) => s.name).join(', '),
child: Container(
height: 26,
constraints: const BoxConstraints(minWidth: 32),
padding: const EdgeInsets.symmetric(horizontal: S.s6),
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border.all(color: C.border2),
borderRadius: BorderRadius.circular(R.sm),
),
child: Text(
'+$rest',
style: ts(12, w: w7, c: C.navy),
),
),
),
],
);
},
);
}
}
class WhatsAppLogo extends StatelessWidget {
const WhatsAppLogo({super.key, this.size = 20, this.color});
final double size;
final Color? color;
static const brand = Color(0xFF25D366);
@override
Widget build(BuildContext context) => ExcludeSemantics(
child: SvgPicture.asset(
'assets/icons/whatsapp.svg',
width: size,
height: size,
colorFilter: ColorFilter.mode(color ?? brand, BlendMode.srcIn),
),
);
}