initial import

This commit is contained in:
2026-07-06 18:01:38 +02:00
commit 8ce435e298
122 changed files with 5392 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/event_detail_provider.dart';
import '../widgets/loading_indicator.dart';
class EventDetailScreen extends StatefulWidget {
final String gameId;
final String account;
final String eventTitle;
const EventDetailScreen({
super.key,
required this.gameId,
required this.account,
required this.eventTitle,
});
@override
State<EventDetailScreen> createState() => _EventDetailScreenState();
}
class _EventDetailScreenState extends State<EventDetailScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<EventDetailProvider>().loadEventDetail(
widget.gameId,
widget.account,
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.eventTitle),
),
body: Consumer<EventDetailProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const LoadingIndicator();
}
if (provider.error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
provider.error!,
style: const TextStyle(color: Colors.red),
),
ElevatedButton(
onPressed: () => provider.loadEventDetail(
widget.gameId,
widget.account,
),
child: const Text('Reessayer'),
),
],
),
);
}
final detail = provider.eventDetail;
if (detail == null) {
return const Center(child: Text('Aucune donnee disponible'));
}
final hasPlayers = detail.convocation.available.isNotEmpty;
final hasStaff = detail.convocation.staff.isNotEmpty;
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Recap card
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Type: ${detail.type}'),
Text('Lieu: ${detail.place}'),
Text('Heure: ${detail.timeStart} - ${detail.timeEnd}'),
],
),
),
),
const SizedBox(height: 16),
// No convocation at all
if (!hasPlayers && !hasStaff) ...[
const Card(
color: Colors.amber,
child: Padding(
padding: EdgeInsets.all(16),
child: Text(
'Aucun joueur ni personnel convoque',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
],
// Players section
if (hasPlayers) ...[
Text(
'Joueurs (${detail.convocation.available.length})',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
...detail.convocation.available.map((player) {
final position = player.position ?? 'N/A';
final number = player.number ?? 'N/A';
return Card(
child: ListTile(
title: Text('[$position] #$number - ${player.fname} ${player.lname}'),
subtitle: Text('DOB: ${player.dob}'),
),
);
}),
const SizedBox(height: 16),
],
// Staff section
if (hasStaff) ...[
Text(
'Personnel',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
...detail.convocation.staff.map((staff) {
return Card(
child: ListTile(
title: Text('${staff.role}: ${staff.fname} ${staff.lname}'),
),
);
}),
],
],
),
);
},
),
);
}
}
+84
View File
@@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
import 'schedule_screen.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
bool _isLoading = false;
void _login() async {
setState(() => _isLoading = true);
try {
await context.read<AuthProvider>().login();
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const ScheduleScreen()),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Erreur de connexion'),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'MyIce - Games',
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
color: Colors.indigo,
),
),
const SizedBox(height: 32),
if (_isLoading)
const CircularProgressIndicator()
else
ElevatedButton(
onPressed: _login,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
),
child: const Text('Se connecter avec Infomaniak'),
),
const SizedBox(height: 16),
const Text(
'Connectez-vous pour acceder aux evenements...',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
],
),
),
),
);
}
}
+240
View File
@@ -0,0 +1,240 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
import '../providers/schedule_provider.dart';
import '../widgets/event_card.dart';
import '../widgets/filter_dropdown.dart';
import '../widgets/loading_indicator.dart';
import 'event_detail_screen.dart';
import 'login_screen.dart';
class ScheduleScreen extends StatefulWidget {
const ScheduleScreen({super.key});
@override
State<ScheduleScreen> createState() => _ScheduleScreenState();
}
class _ScheduleScreenState extends State<ScheduleScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final scheduleProvider = context.read<ScheduleProvider>();
scheduleProvider.loadAccounts().then((_) {
scheduleProvider.loadCachedSchedule();
});
});
}
void _logout() async {
await context.read<AuthProvider>().logout();
await context.read<ScheduleProvider>().clearCache();
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const LoginScreen()),
);
}
}
Future<void> _refresh() async {
await context.read<ScheduleProvider>().refreshSchedule();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('MyIce'),
actions: [
Consumer<AuthProvider>(
builder: (context, auth, child) {
if (auth.userInfo != null) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(auth.userInfo!.email),
),
);
}
return const SizedBox.shrink();
},
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _refresh,
),
IconButton(
icon: const Icon(Icons.logout),
onPressed: _logout,
),
],
),
body: Consumer<ScheduleProvider>(
builder: (context, schedule, child) {
if (schedule.isLoading && schedule.events.isEmpty) {
return const LoadingIndicator();
}
if (schedule.error != null && schedule.events.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
schedule.error!,
style: const TextStyle(color: Colors.red),
),
ElevatedButton(
onPressed: schedule.refreshSchedule,
child: const Text('Reessayer'),
),
],
),
);
}
return Stack(
children: [
Column(
children: [
Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
FilterDropdown(
label: 'Compte',
value: schedule.selectedAccount,
items: schedule.accounts.map((a) => a.name).toList(),
onChanged: (value) {
if (value != null) {
schedule.setAccount(value);
}
},
),
const SizedBox(height: 8),
FilterDropdown(
label: 'Age',
value: schedule.selectedAgegroup,
items: schedule.agegroups,
onChanged: (value) {
schedule.setAgegroup(value);
},
),
const SizedBox(height: 8),
FilterDropdown(
label: 'Sous-groupe',
value: schedule.selectedSubgroup,
items: schedule.subgroups,
onChanged: (value) {
schedule.setSubgroup(value);
},
),
],
),
),
),
Expanded(
child: schedule.filteredEvents.isEmpty
? Center(
child: schedule.events.isEmpty
? const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Aucun evenement disponible',
),
SizedBox(height: 8),
Text(
'Appuyez sur le bouton de rafraichissement pour charger les evenements',
style: TextStyle(
color: Colors.grey,
fontSize: 12,
),
),
],
)
: const Text('Aucun evenement disponible'),
)
: ListView.builder(
itemCount: schedule.filteredEvents.length,
itemBuilder: (context, index) {
final event = schedule.filteredEvents[index];
return EventCard(
event: event,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => EventDetailScreen(
gameId: event.idEvent,
account: schedule.selectedAccount!,
eventTitle: event.title,
),
),
);
},
);
},
),
),
],
),
if (schedule.isRefreshing)
Positioned.fill(
child: AnimatedOpacity(
duration: const Duration(milliseconds: 300),
opacity: schedule.isRefreshing ? 1.0 : 0.0,
child: AbsorbPointer(
absorbing: true,
child: Container(
color: Colors.black.withValues(alpha: 0.4),
child: Center(
child: Container(
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 15,
spreadRadius: 2,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
width: 48,
height: 48,
child: CircularProgressIndicator(
strokeWidth: 4,
),
),
const SizedBox(height: 16),
Text(
'Chargement...',
style: TextStyle(
color: Colors.grey[800],
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
],
),
),
),
),
),
),
),
],
);
},
),
);
}
}