import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../app/theme.dart'; import '../../providers/auth_provider.dart'; import '../../shared/api_client.dart'; import '../../shared/models/match.dart'; import '../../shared/widgets/match_live_wordmark.dart'; import '../../shared/widgets/primary_cta.dart'; import '../../shared/widgets/screen_insets.dart'; import 'resume_session_sheet.dart'; import 'no_team_onboarding.dart'; import 'schedule_match_sheet.dart'; import 'team_providers.dart'; final matchesProvider = FutureProvider>((ref) async { final auth = ref.watch(authProvider); if (!auth.isAuthenticated) return []; final teams = await ref.read(teamsProvider.future); if (teams.isEmpty) return []; final client = ref.read(apiClientProvider); final list = await client.fetchMatches(teams.first.id); list.sort((a, b) { final sa = a.scheduledAt; final sb = b.scheduledAt; if (sa == null && sb == null) return 0; if (sa == null) return 1; if (sb == null) return -1; return sa.compareTo(sb); }); return list; }); class MatchesScreen extends ConsumerWidget { const MatchesScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final auth = ref.watch(authProvider); final teamsAsync = ref.watch(teamsProvider); final matchesAsync = ref.watch(matchesProvider); final theme = Theme.of(context); final dateFormat = DateFormat('EEE d MMM · HH:mm', 'it_IT'); final bottomInset = ScreenInsets.of(context).bottom; return Scaffold( appBar: AppBar( title: const MatchLiveWordmark(compact: true), actions: [ IconButton( icon: const Icon(Icons.group_outlined), tooltip: 'Gestisci staff', onPressed: () async { final team = await ref.read(primaryTeamProvider.future); final url = team?.staffManageUrl ?? team?.billingUrl; if (url != null && await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); } }, ), IconButton( icon: const Icon(Icons.video_library_outlined), tooltip: 'Archivio', onPressed: () => context.push('/archive'), ), IconButton( icon: const Icon(Icons.logout), tooltip: 'Esci', onPressed: () { ref.read(authProvider.notifier).logout(); context.go('/login'); }, ), ], ), body: teamsAsync.when( loading: () => const Center( child: CircularProgressIndicator(color: AppTheme.primaryRed), ), error: (e, _) => Center(child: Text('Errore squadre: $e')), data: (teams) { if (teams.isEmpty) { return NoTeamOnboarding(theme: theme); } return matchesAsync.when( loading: () => const Center( child: CircularProgressIndicator(color: AppTheme.primaryRed), ), error: (e, _) => Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Errore nel caricamento', style: theme.textTheme.titleMedium), const SizedBox(height: 8), Text( e.toString(), style: theme.textTheme.bodySmall, textAlign: TextAlign.center, ), const SizedBox(height: 12), TextButton( onPressed: () { ref.invalidate(teamsProvider); ref.invalidate(matchesProvider); }, child: const Text('RIPROVA'), ), ], ), ), ), data: (matches) { MatchModel? activeMatch; for (final m in matches) { if (m.hasActiveSession) { activeMatch = m; break; } } return RefreshIndicator( color: AppTheme.primaryRed, onRefresh: () async { ref.invalidate(matchesProvider); await ref.read(matchesProvider.future); }, child: CustomScrollView( slivers: [ SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Ciao, ${auth.user?.name ?? ''}', style: theme.textTheme.headlineSmall, ), const SizedBox(height: 4), Text( 'Le tue partite', style: theme.textTheme.bodyMedium, ), if (activeMatch != null) ...[ const SizedBox(height: 12), Material( color: AppTheme.primaryRed.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), child: InkWell( onTap: () => showResumeSessionSheet( context, ref, match: activeMatch!, ), borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(14), child: Row( children: [ const Icon( Icons.videocam, color: AppTheme.primaryRed, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Riprendi diretta in corso', style: theme.textTheme.titleSmall ?.copyWith( color: AppTheme.primaryRed, fontWeight: FontWeight.w700, ), ), Text( '${activeMatch.teamName} vs ${activeMatch.opponentName}', style: theme.textTheme.bodySmall, ), ], ), ), const Icon( Icons.chevron_right, color: AppTheme.primaryRed, ), ], ), ), ), ), ], ], ), ), ), if (matches.isEmpty) SliverFillRemaining( hasScrollBody: false, child: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Text( 'Nessuna partita in calendario.\nTocca «Programma partita» per aggiungere data e ora.', style: theme.textTheme.bodyMedium, textAlign: TextAlign.center, ), ), ), ) else SliverList( delegate: SliverChildBuilderDelegate( (context, index) { final match = matches[index]; return _MatchCard( match: match, dateFormat: dateFormat, onTap: () { if (match.hasActiveSession) { showResumeSessionSheet(context, ref, match: match); } else { context.push('/setup/${match.id}/1'); } }, onDelete: match.canResumeCamera ? null : () => _deleteMatch(context, ref, match), ); }, childCount: matches.length, ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(20, 20, 20, 8), child: PrimaryCta( label: 'PROGRAMMA PARTITA', icon: Icons.event_available, onPressed: () => _scheduleMatch(context, ref), ), ), ), SliverToBoxAdapter( child: Padding( padding: EdgeInsets.fromLTRB(20, 0, 20, 20 + bottomInset), child: TextButton.icon( onPressed: () => _createMatchQuick(context, ref), icon: const Icon(Icons.settings_outlined, size: 18), label: const Text('Crea e configura subito (senza orario)'), ), ), ), ], ), ); }, ); }, ), ); } Future _deleteMatch( BuildContext context, WidgetRef ref, MatchModel match, ) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: AppTheme.surface, title: const Text('Elimina partita'), content: Text( 'Eliminare «${match.teamName} vs ${match.opponentName}»?\n\n' 'L\'operazione non si può annullare.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Annulla'), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), style: FilledButton.styleFrom(backgroundColor: AppTheme.primaryRed), child: const Text('Elimina'), ), ], ), ); if (confirmed != true || !context.mounted) return; try { await ref.read(apiClientProvider).deleteMatch(match.id); ref.invalidate(matchesProvider); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Partita eliminata')), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Impossibile eliminare: $e')), ); } } } Future _scheduleMatch(BuildContext context, WidgetRef ref) async { final teams = await ref.read(teamsProvider.future); if (teams.isEmpty) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Nessuna squadra disponibile')), ); } return; } final data = await showScheduleMatchSheet(context); if (data == null || !context.mounted) return; try { final client = ref.read(apiClientProvider); final match = await client.createMatch(teams.first.id, { 'opponent_name': data.opponentName, 'scheduled_at': data.scheduledAt.toUtc().toIso8601String(), if (data.location != null) 'location': data.location, 'sport': 'volleyball', 'sets_to_win': 3, }); ref.invalidate(matchesProvider); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Partita programmata — visibile sul sito')), ); final configure = await showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: AppTheme.surface, title: const Text('Configurare ora?'), content: const Text( 'Puoi preparare telefono e regia subito, oppure tornare quando sei in palestra.', ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Più tardi'), ), FilledButton( onPressed: () => Navigator.pop(ctx, true), style: FilledButton.styleFrom(backgroundColor: AppTheme.primaryRed), child: const Text('Configura'), ), ], ), ); if (configure == true && context.mounted) { context.push('/setup/${match.id}/1'); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Errore: $e')), ); } } } Future _createMatchQuick(BuildContext context, WidgetRef ref) async { final teams = await ref.read(teamsProvider.future); if (teams.isEmpty) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Nessuna squadra disponibile')), ); } return; } final client = ref.read(apiClientProvider); final match = await client.createMatch(teams.first.id, { 'opponent_name': 'Avversario', 'sport': 'volleyball', 'sets_to_win': 3, }); if (context.mounted) context.push('/setup/${match.id}/1'); } } String _matchStatusLabel( MatchModel match, { required bool canResume, required bool hasSession, }) { if (canResume) return 'RIPRENDI'; if (hasSession) return 'IN CORSO'; final at = match.scheduledAt; if (at != null && at.isAfter(DateTime.now())) return 'PROGRAMMATA'; return 'AVVIA'; } class _MatchCard extends StatelessWidget { const _MatchCard({ required this.match, required this.dateFormat, required this.onTap, this.onDelete, }); final MatchModel match; final DateFormat dateFormat; final VoidCallback onTap; final VoidCallback? onDelete; @override Widget build(BuildContext context) { final theme = Theme.of(context); final hasSession = match.hasActiveSession; final canResume = match.canResumeCamera; return Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6), child: Material( color: AppTheme.surface, borderRadius: BorderRadius.circular(12), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${match.teamName} vs ${match.opponentName}', style: theme.textTheme.titleMedium, ), const SizedBox(height: 4), if (match.scheduledAt != null) Text( dateFormat.format(match.scheduledAt!.toLocal()), style: theme.textTheme.bodyMedium, ), if (match.location != null && match.location!.isNotEmpty) Text( match.location!, style: theme.textTheme.bodyMedium, ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: hasSession ? AppTheme.primaryRed.withValues(alpha: 0.2) : AppTheme.surfaceElevated, borderRadius: BorderRadius.circular(8), ), child: Text( _matchStatusLabel(match, canResume: canResume, hasSession: hasSession), style: theme.textTheme.labelLarge?.copyWith( color: hasSession ? AppTheme.primaryRed : AppTheme.textSecondary, fontSize: 11, ), ), ), if (onDelete != null) ...[ const SizedBox(width: 4), IconButton( icon: const Icon(Icons.delete_outline, color: AppTheme.textSecondary), tooltip: 'Elimina partita', onPressed: onDelete, visualDensity: VisualDensity.compact, constraints: const BoxConstraints(minWidth: 36, minHeight: 36), ), ], ], ), ), ), ), ); } }