import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/format.dart'; import '../../core/models.dart'; import '../../core/providers.dart'; import '../../core/theme.dart'; import '../../widgets/ui.dart'; import 'user_bits.dart'; enum _Filter { all, active, off } class UsersScreen extends ConsumerStatefulWidget { const UsersScreen({super.key}); @override ConsumerState createState() => _UsersScreenState(); } class _UsersScreenState extends ConsumerState { _Filter _filter = _Filter.all; String _q = ''; bool _keep(AppUser u) => switch (_filter) { _Filter.all => true, _Filter.active => u.active, _Filter.off => !u.active, }; @override Widget build(BuildContext context) { final users = ref.watch(usersProvider); final shops = ref.watch(activeShopsProvider).asData?.value ?? const []; final byId = {for (final s in shops) s.id: s}; final list = users.asData?.value; final off = list?.where((u) => !u.active).length ?? 0; return Column( children: [ ScreenHeader( title: 'Users', subtitle: list == null ? null : off == 0 ? plural(list.length, 'user') : '${plural(list.length, 'user')}, $off inactive', trailing: SmallButton( 'Add user', onTap: () => context.push('/users/new'), ), ), Expanded( child: asyncView(users, (all) { final counts = { _Filter.all: all.length, _Filter.active: all.where((u) => u.active).length, _Filter.off: all.where((u) => !u.active).length, }; final shown = all .where(_keep) .where( (u) => _q.isEmpty || u.username.toLowerCase().contains(_q) || (u.phone ?? '').contains(_q), ) .toList() // Inactive accounts last. ..sort((a, b) { final r = (a.active ? 0 : 1).compareTo(b.active ? 0 : 1); return r != 0 ? r : a.username.compareTo(b.username); }); return PageList( top: S.s4, children: [ _Toolbar( filter: _filter, counts: counts, onFilter: (f) => setState(() => _filter = f), onSearch: (v) => setState(() => _q = v.trim().toLowerCase()), ), const SizedBox(height: S.s14), if (shown.isEmpty) SrsCard( child: EmptyState( icon: Icons.person_search_rounded, title: _q.isNotEmpty ? 'No one matches “$_q”' : 'No one here', body: _filter == _Filter.off ? 'Every user can sign in.' : all.isEmpty ? 'Add a user to let them sign in and see their shops.' : 'Try another filter, or add a user.', ), ) else _Roster(users: shown, shops: byId), ], ); }, retry: () => ref.invalidate(usersProvider)), ), ], ); } } class _Toolbar extends StatelessWidget { const _Toolbar({ required this.filter, required this.counts, required this.onFilter, required this.onSearch, }); final _Filter filter; final Map<_Filter, int> counts; final ValueChanged<_Filter> onFilter; final ValueChanged onSearch; static const _labels = { _Filter.all: 'All', _Filter.active: 'Active', _Filter.off: 'Inactive', }; @override Widget build(BuildContext context) { final chips = SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ for (final f in _Filter.values) if (f == _Filter.all || counts[f]! > 0) Padding( padding: const EdgeInsets.only(right: S.s8), child: _FilterChip( label: _labels[f]!, count: counts[f]!, on: f == filter, onTap: () => onFilter(f), ), ), ], ), ); final search = SearchField( hint: 'Search by name or phone', onChanged: onSearch, ); return LayoutBuilder( builder: (context, c) => c.maxWidth < 760 ? Column( children: [ search, const SizedBox(height: S.s12), chips, ], ) : Row( children: [ Expanded(child: chips), const SizedBox(width: S.s16), SizedBox(width: 320, child: search), ], ), ); } } class _FilterChip extends StatelessWidget { const _FilterChip({ required this.label, required this.count, required this.on, required this.onTap, }); final String label; final int count; final bool on; final VoidCallback onTap; @override Widget build(BuildContext context) => Semantics( selected: on, child: Pressable( onTap: onTap, radius: R.pill, builder: (_, hover) => AnimatedContainer( duration: const Duration(milliseconds: 140), height: 38, padding: const EdgeInsets.fromLTRB(S.s14, 0, S.s8, 0), decoration: BoxDecoration( color: on ? C.navy : (hover ? C.hover : Colors.white), borderRadius: BorderRadius.circular(R.pill), border: Border.all( color: on ? C.navy : (hover ? C.border2 : C.border), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( label, style: ts(13, w: w6, c: on ? Colors.white : C.ink), ), const SizedBox(width: S.s6), Container( constraints: const BoxConstraints(minWidth: 22), height: 22, padding: const EdgeInsets.symmetric(horizontal: S.s6), alignment: Alignment.center, decoration: BoxDecoration( color: on ? C.onNavy20 : C.chip, borderRadius: BorderRadius.circular(R.pill), ), child: Text( '$count', style: ts(12, w: w7, c: on ? Colors.white : C.navy), ), ), ], ), ), ), ); } /// One card holding every row: a table with column headings on wide windows, /// a compact list on phones. class _Roster extends StatelessWidget { const _Roster({required this.users, required this.shops}); final List users; final Map shops; @override Widget build(BuildContext context) => LayoutBuilder( builder: (context, c) { final wide = c.maxWidth >= 720; return SrsCard( padding: EdgeInsets.zero, child: Column( children: [ if (wide) const _HeadingRow(), for (var i = 0; i < users.length; i++) ...[ if (i > 0 || wide) const Divider(height: 1, color: C.border), _UserRow( u: users[i], shops: shops, wide: wide, first: i == 0 && !wide, last: i == users.length - 1, ), ], ], ), ); }, ); } const _phoneCol = 150.0; const _statusCol = 120.0; class _HeadingRow extends StatelessWidget { const _HeadingRow(); @override Widget build(BuildContext context) { final h = ts(12, w: w6, c: C.muted); return Padding( padding: const EdgeInsets.fromLTRB(S.s20, S.s14, S.s20, S.s12), child: Row( children: [ Expanded(flex: 3, child: Text('Name', style: h)), SizedBox( width: _phoneCol, child: Text('Phone', style: h), ), Expanded(flex: 3, child: Text('Shops', style: h)), SizedBox( width: _statusCol, child: Text('Status', style: h), ), const SizedBox(width: 28), ], ), ); } } class _UserRow extends StatelessWidget { const _UserRow({ required this.u, required this.shops, required this.wide, required this.first, required this.last, }); final AppUser u; final Map shops; final bool wide, first, last; @override Widget build(BuildContext context) { final mine = [ for (final id in u.shopIds) if (shops[id] != null) shops[id]!, ]; final shopsCell = ShopCodes(shops: mine); final name = Row( children: [ UserAvatar(user: u, size: 42), const SizedBox(width: S.s12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Flexible( child: Text( u.username, maxLines: 1, overflow: TextOverflow.ellipsis, style: ts(15, w: w6, c: u.active ? C.ink : C.muted), ), ), if (u.getsResults) ...[ const SizedBox(width: S.s6), const Tooltip( message: 'Gets results on WhatsApp', child: WhatsAppLogo(size: 14), ), ], ], ), if (!wide) ...[ const SizedBox(height: S.s4), Text( u.phone == null ? 'No phone' : fmtPhone(u.phone), maxLines: 1, overflow: TextOverflow.ellipsis, style: ts(12, c: C.muted), ), const SizedBox(height: S.s8), shopsCell, ], ], ), ), ], ); const trailing = Icon(Icons.chevron_right_rounded, color: C.placeholder); final radius = BorderRadius.vertical( top: Radius.circular(first ? R.card : 0), bottom: Radius.circular(last ? R.card : 0), ); return Pressable( onTap: () => context.push('/users/${u.id}'), radius: 0, builder: (_, hover) => AnimatedContainer( duration: const Duration(milliseconds: 120), decoration: BoxDecoration( color: hover ? C.hover : Colors.transparent, borderRadius: radius, ), padding: EdgeInsets.symmetric( horizontal: wide ? S.s20 : S.s14, vertical: S.s12, ), child: wide ? Row( children: [ Expanded(flex: 3, child: name), SizedBox( width: _phoneCol, child: Text( u.phone == null ? '—' : fmtPhone(u.phone), style: ts(13, c: u.phone == null ? C.placeholder : C.ink), ), ), Expanded(flex: 3, child: shopsCell), SizedBox( width: _statusCol, child: StatusLabel(active: u.active), ), SizedBox(width: 28, child: trailing), ], ) : Row( children: [ Expanded(child: name), const SizedBox(width: S.s8), trailing, ], ), ), ); } }