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 'package:intl/intl.dart'; import '../../auth/application/auth_notifier.dart'; import '../../teams/application/teams_notifier.dart'; import '../../teams/domain/join_request.dart'; import '../../teams/domain/player.dart'; import '../../teams/domain/team.dart'; import '../../teams/infrastructure/teams_repository.dart'; import '../application/profile_notifier.dart'; import '../infrastructure/profile_repository.dart'; /// Dashboard for managers — their team's roster, stats inputs, and pending /// join requests live here. /// /// The route redirect guard in `app_router.dart` ensures only managers reach /// this screen, so we don't re-check inside. class ManagerDashboardScreen extends ConsumerWidget { const ManagerDashboardScreen({super.key}); static const double _maxContentWidth = 760; @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(authNotifierProvider).valueOrNull; final profileAsync = ref.watch(currentProfileProvider); return Scaffold( appBar: AppBar( title: const Text('MANAGER DASHBOARD'), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => context.go('/events'), ), ), body: profileAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center(child: Text('Could not load: $e')), data: (profile) { if (profile == null || user == null) { return const Center(child: Text('Not signed in.')); } if (!profile.hasTeam) { return _NoTeamYet(onCreate: () => context.go('/teams/new')); } final team = ref.watch(teamByIdProvider(profile.teamId!)); if (team == null) { // We have the id but the team stream may be filtered (pending // teams are excluded from the public feed). Fall back to a // direct fetch. return _ManagerForPendingTeam(teamId: profile.teamId!); } return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: _maxContentWidth), child: _DashboardBody(team: team), ), ); }, ), ); } } class _NoTeamYet extends StatelessWidget { const _NoTeamYet({required this.onCreate}); final VoidCallback onCreate; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.add_business_outlined, size: 64, color: theme.colorScheme.primary, ), const SizedBox(height: 16), Text( 'No team yet', style: theme.textTheme.titleMedium, ), const SizedBox(height: 8), Text( 'Create your team to start managing rosters, stats, and join ' 'requests. Admins review new teams before they appear publicly.', textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 24), FilledButton.icon( onPressed: onCreate, icon: const Icon(Icons.add), label: const Text('CREATE A TEAM'), ), ], ), ), ); } } /// Loads the team document directly when the manager's team is pending and /// therefore excluded from the public teams stream. class _ManagerForPendingTeam extends ConsumerStatefulWidget { const _ManagerForPendingTeam({required this.teamId}); final String teamId; @override ConsumerState<_ManagerForPendingTeam> createState() => _ManagerForPendingTeamState(); } class _ManagerForPendingTeamState extends ConsumerState<_ManagerForPendingTeam> { Team? _team; bool _loading = true; Object? _error; @override void initState() { super.initState(); _load(); } Future _load() async { try { final team = await ref.read(teamsRepositoryProvider).getTeam( widget.teamId, ); if (!mounted) return; setState(() { _team = team; _loading = false; }); } catch (e) { if (!mounted) return; setState(() { _error = e; _loading = false; }); } } @override Widget build(BuildContext context) { if (_loading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return Center(child: Text('Could not load team: $_error')); } if (_team == null) { return const Center(child: Text('Team not found.')); } return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 760), child: _DashboardBody(team: _team!), ), ); } } class _DashboardBody extends ConsumerWidget { const _DashboardBody({required this.team}); final Team team; @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final scheme = theme.colorScheme; return ListView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), children: [ _TeamHeaderCard(team: team), if (team.isPending) ...[ const SizedBox(height: 16), Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.amber.withValues(alpha: 0.14), borderRadius: BorderRadius.circular(12), border: Border.all( color: Colors.amber.withValues(alpha: 0.4), ), ), child: Row( children: [ Icon(Icons.hourglass_bottom, color: Colors.amber.shade300), const SizedBox(width: 10), Expanded( child: Text( 'Awaiting admin approval. The team will appear publicly ' 'once approved.', style: theme.textTheme.bodyMedium, ), ), ], ), ), ], if (team.isRejected) ...[ const SizedBox(height: 16), Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: scheme.error.withValues(alpha: 0.14), borderRadius: BorderRadius.circular(12), border: Border.all( color: scheme.error.withValues(alpha: 0.4), ), ), child: Row( children: [ Icon(Icons.block, color: scheme.error), const SizedBox(width: 10), Expanded( child: Text( 'This team was rejected by an admin. Contact the league ' 'for next steps.', style: theme.textTheme.bodyMedium, ), ), ], ), ), ], if (team.isApproved) ...[ const SizedBox(height: 24), _SectionHeader(title: 'ROSTER (${team.players.length})'), const SizedBox(height: 8), if (team.players.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Text( 'No players yet — approved join requests will appear here as ' 'roster entries.', style: theme.textTheme.bodySmall?.copyWith( color: scheme.onSurfaceVariant, ), ), ) else ...team.players.map((p) => _RosterRow(team: team, player: p)), const SizedBox(height: 24), _SectionHeader(title: 'JOIN REQUESTS'), const SizedBox(height: 8), _JoinRequestsList(team: team), ], ], ); } } class _TeamHeaderCard extends StatelessWidget { const _TeamHeaderCard({required this.team}); final Team team; @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final initial = team.name.isEmpty ? '?' : team.name.characters.first; return Card( child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Container( width: 56, height: 56, alignment: Alignment.center, decoration: BoxDecoration( color: scheme.primaryContainer, shape: BoxShape.circle, ), child: Text( initial.toUpperCase(), style: TextStyle( color: scheme.onPrimaryContainer, fontWeight: FontWeight.w800, fontSize: 26, ), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( team.name, style: theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w800, ), ), const SizedBox(height: 4), Text( 'Record ${team.record} - ${team.players.length} players', style: theme.textTheme.bodySmall?.copyWith( color: scheme.onSurfaceVariant, ), ), ], ), ), _TeamStatusChip(status: team.status), ], ), ), ); } } class _TeamStatusChip extends StatelessWidget { const _TeamStatusChip({required this.status}); final String status; @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final (Color bg, Color fg, String label) = switch (status) { TeamStatus.pending => ( Colors.amber.withValues(alpha: 0.18), Colors.amber.shade300, 'PENDING', ), TeamStatus.rejected => ( scheme.error.withValues(alpha: 0.18), scheme.error, 'REJECTED', ), _ => ( Colors.green.withValues(alpha: 0.18), Colors.green.shade300, 'APPROVED', ), }; return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: bg, borderRadius: BorderRadius.circular(12), ), child: Text( label, style: theme.textTheme.labelSmall?.copyWith( color: fg, fontWeight: FontWeight.w800, letterSpacing: 1.0, ), ), ); } } class _SectionHeader extends StatelessWidget { const _SectionHeader({required this.title}); final String title; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Row( children: [ Text( title, style: theme.textTheme.labelLarge?.copyWith( letterSpacing: 1.4, fontWeight: FontWeight.w800, ), ), const SizedBox(width: 12), Expanded(child: Divider(color: theme.colorScheme.outlineVariant)), ], ); } } class _RosterRow extends ConsumerWidget { const _RosterRow({required this.team, required this.player}); final Team team; final Player player; Future _editStats(BuildContext context, WidgetRef ref) async { final result = await showDialog<_StatEdit>( context: context, builder: (_) => _EditStatsDialog(player: player), ); if (result == null) return; if (!context.mounted) return; final updatedPlayers = team.players .map( (p) => p.id == player.id ? p.copyWith(goalsScored: result.goals, assists: result.assists) : p, ) .toList(growable: false); try { await ref.read(teamsRepositoryProvider).updateTeam( team.copyWith(players: updatedPlayers), ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Updated stats for ${player.name}')), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Update failed: $e')), ); } } } @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final scheme = theme.colorScheme; return Card( margin: const EdgeInsets.symmetric(vertical: 4), child: ListTile( leading: CircleAvatar( backgroundColor: scheme.primaryContainer, child: Text( player.name.isEmpty ? '?' : player.name.characters.first, style: TextStyle( color: scheme.onPrimaryContainer, fontWeight: FontWeight.w700, ), ), ), title: Text( player.name, style: theme.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w600, ), ), subtitle: Text( 'Goals ${player.goalsScored} - Assists ${player.assists}' '${player.position == null ? '' : ' - ${player.position}'}', style: theme.textTheme.bodySmall?.copyWith( color: scheme.onSurfaceVariant, ), ), trailing: const Icon(Icons.edit_outlined), onTap: () => _editStats(context, ref), ), ); } } class _StatEdit { const _StatEdit({required this.goals, required this.assists}); final int goals; final int assists; } class _EditStatsDialog extends StatefulWidget { const _EditStatsDialog({required this.player}); final Player player; @override State<_EditStatsDialog> createState() => _EditStatsDialogState(); } class _EditStatsDialogState extends State<_EditStatsDialog> { late int _goals = widget.player.goalsScored; late int _assists = widget.player.assists; @override Widget build(BuildContext context) { return AlertDialog( title: Text('Edit ${widget.player.name}'), content: Column( mainAxisSize: MainAxisSize.min, children: [ _StatStepper( label: 'Goals', value: _goals, onChanged: (v) => setState(() => _goals = v), ), const SizedBox(height: 12), _StatStepper( label: 'Assists', value: _assists, onChanged: (v) => setState(() => _assists = v), ), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('CANCEL'), ), FilledButton( onPressed: () => Navigator.of(context).pop( _StatEdit(goals: _goals, assists: _assists), ), child: const Text('SAVE'), ), ], ); } } class _StatStepper extends StatelessWidget { const _StatStepper({ required this.label, required this.value, required this.onChanged, }); final String label; final int value; final ValueChanged onChanged; @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; return Row( children: [ SizedBox( width: 80, child: Text( label, style: theme.textTheme.labelLarge?.copyWith( letterSpacing: 1.0, fontWeight: FontWeight.w700, ), ), ), IconButton( icon: const Icon(Icons.remove_circle_outline), color: scheme.error, onPressed: value <= 0 ? null : () => onChanged(value - 1), ), Container( width: 56, alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 6), decoration: BoxDecoration( color: scheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: Text( '$value', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w800, ), ), ), IconButton( icon: const Icon(Icons.add_circle_outline), color: scheme.primary, onPressed: () => onChanged(value + 1), ), const Spacer(), SizedBox( width: 56, child: TextFormField( initialValue: '$value', textAlign: TextAlign.center, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, ], decoration: const InputDecoration(isDense: true), onChanged: (raw) { final v = int.tryParse(raw); if (v != null && v >= 0) onChanged(v); }, ), ), ], ); } } class _JoinRequestsList extends ConsumerWidget { const _JoinRequestsList({required this.team}); final Team team; @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final scheme = theme.colorScheme; final async = ref.watch(joinRequestsForTeamProvider(team.id)); return async.when( loading: () => const Padding(padding: EdgeInsets.all(16), child: LinearProgressIndicator()), error: (e, _) => Text('Could not load requests: $e'), data: (requests) { final pending = requests .where((r) => r.status == JoinRequestStatus.pending) .toList(); if (pending.isEmpty) { return Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Text( 'No pending requests.', style: theme.textTheme.bodySmall?.copyWith( color: scheme.onSurfaceVariant, ), ), ); } return Column( children: pending .map((r) => _RequestRow(team: team, request: r)) .toList(), ); }, ); } } class _RequestRow extends ConsumerStatefulWidget { const _RequestRow({required this.team, required this.request}); final Team team; final JoinRequest request; @override ConsumerState<_RequestRow> createState() => _RequestRowState(); } class _RequestRowState extends ConsumerState<_RequestRow> { bool _busy = false; Future _act({required bool approve}) async { setState(() => _busy = true); final repo = ref.read(teamsRepositoryProvider); final profileRepo = ref.read(profileRepositoryProvider); try { if (approve) { // Mark the request approved. await repo.updateJoinRequestStatus( widget.request.id, JoinRequestStatus.approved.name, ); // Stamp the team on the player's profile. await profileRepo.updateTeamId( widget.request.playerId, widget.team.id, ); // Add the player to the team roster (if not already there). final alreadyOnRoster = widget.team.players.any( (p) => p.id == widget.request.playerId, ); if (!alreadyOnRoster) { final updated = [ ...widget.team.players, Player( id: widget.request.playerId, name: widget.request.playerName, ), ]; await repo.updateTeam(widget.team.copyWith(players: updated)); } if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${widget.request.playerName} approved')), ); } else { await repo.updateJoinRequestStatus( widget.request.id, JoinRequestStatus.rejected.name, ); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${widget.request.playerName} rejected')), ); } } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Action failed: $e')), ); } finally { if (mounted) setState(() => _busy = false); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final date = DateFormat.yMMMd().format(widget.request.requestedAt); return Card( margin: const EdgeInsets.symmetric(vertical: 4), child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ CircleAvatar( backgroundColor: scheme.primaryContainer, child: Text( widget.request.playerName.isEmpty ? '?' : widget.request.playerName.characters.first, style: TextStyle( color: scheme.onPrimaryContainer, fontWeight: FontWeight.w700, ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.request.playerName, style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w700, ), ), Text( widget.request.playerEmail, style: theme.textTheme.bodySmall?.copyWith( color: scheme.onSurfaceVariant, ), ), ], ), ), Text( date, style: theme.textTheme.bodySmall?.copyWith( color: scheme.onSurfaceVariant, ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: _busy ? null : () => _act(approve: false), icon: const Icon(Icons.close, size: 18), label: const Text('REJECT'), ), ), const SizedBox(width: 8), Expanded( child: FilledButton.icon( onPressed: _busy ? null : () => _act(approve: true), icon: _busy ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.check, size: 18), label: const Text('APPROVE'), ), ), ], ), ], ), ), ); } }