# MyIce Flutter App - Design Specification **Date:** 2026-07-06 **Context:** Application mobile Flutter (iOS/Android) reproduisant le fonctionnement de la webapp myice existante. **Backend:** L'application se connecte a l'API FastAPI existante (myice/webapi.py) via HTTP. ## 1. Architecture globale Approche : **Repository + Provider (B)** ``` lib/ ├── main.dart # Entry point, MaterialApp, MultiProvider ├── config/ │ └── api_config.dart # URL base de l'API, timeouts ├── models/ │ ├── account.dart # {name, label} │ ├── event.dart # Game/Practice du schedule │ ├── event_detail.dart # Details + convocation │ ├── user_info.dart # {email} │ └── convocation.dart # Player, Staff, Convocation ├── services/ │ ├── api_service.dart # Dio + intercepteur Bearer │ └── auth_service.dart # Login/logout OIDC, deep link ├── providers/ │ ├── auth_provider.dart # Etat auth + user info │ ├── schedule_provider.dart # Liste events + filtres │ └── event_provider.dart # Details d'un event (optionnel) ├── repositories/ │ └── myice_repository.dart # Appels API : accounts, schedule, game ├── screens/ │ ├── login_screen.dart │ ├── schedule_screen.dart │ └── event_detail_screen.dart └── widgets/ ├── event_card.dart ├── filter_dropdown.dart └── loading_indicator.dart ``` **Pattern :** Repository encapsule les appels API -> Providers gerent l'etat reactif -> Screens ecoutent les Providers. ## 2. Dependances Flutter ```yaml dependencies: flutter: sdk: flutter dio: ^5.4.0 provider: ^6.1.0 shared_preferences: ^2.2.0 flutter_web_auth: ^0.6.0 intl: ^0.19.0 # Pour le parsing/formatage de dates si besoin ``` ## 3. Modeles de donnees (JSON <-> Dart) ### Account ```dart class Account { final String name; final String label; Account({required this.name, required this.label}); factory Account.fromJson(Map json) => Account( name: json['name'] as String, label: json['label'] as String, ); } ``` ### Event Provenant de `/schedule`. ```dart class Event { final String idEvent; final String agegroup; final String name; final String title; final String opponent; final String place; final String start; final String end; final String? color; final String eventType; // mappe depuis le champ JSON 'event' Event({ required this.idEvent, required this.agegroup, required this.name, required this.title, required this.opponent, required this.place, required this.start, required this.end, this.color, required this.eventType, }); factory Event.fromJson(Map json) => Event( idEvent: json['id_event']?.toString() ?? '', agegroup: json['agegroup'] as String, name: json['name'] as String? ?? '', title: json['title'] as String, opponent: json['opponent'] as String? ?? '', place: json['place'] as String? ?? '', start: json['start'] as String, end: json['end'] as String, color: json['color'] as String?, eventType: json['event'] as String? ?? '', ); } ``` ### Player ```dart class Player { final String? position; final String? number; final String fname; final String lname; final String dob; Player({this.position, this.number, required this.fname, required this.lname, required this.dob}); factory Player.fromJson(Map json) => Player( position: json['position'] as String?, number: json['number']?.toString(), fname: json['fname'] as String, lname: json['lname'] as String, dob: json['dob'] as String, ); } ``` ### Staff ```dart class Staff { final String role; final String fname; final String lname; Staff({required this.role, required this.fname, required this.lname}); factory Staff.fromJson(Map json) => Staff( role: json['role'] as String, fname: json['fname'] as String, lname: json['lname'] as String, ); } ``` ### Convocation ```dart class Convocation { final List available; final List staff; Convocation({required this.available, required this.staff}); factory Convocation.fromJson(Map json) => Convocation( available: (json['available'] as List? ?? []) .map((e) => Player.fromJson(e as Map)) .toList(), staff: (json['staff'] as List? ?? []) .map((e) => Staff.fromJson(e as Map)) .toList(), ); } ``` ### EventDetail Provenant de `/game/{id}`. ```dart class EventDetail { final String title; final String type; final String place; final String timeStart; final String timeEnd; final Convocation convocation; EventDetail({ required this.title, required this.type, required this.place, required this.timeStart, required this.timeEnd, required this.convocation, }); factory EventDetail.fromJson(Map json) => EventDetail( title: json['title'] as String, type: json['type'] as String, place: json['place'] as String? ?? '', timeStart: json['time_start'] as String, timeEnd: json['time_end'] as String, convocation: Convocation.fromJson(json['convocation'] as Map), ); } ``` ### UserInfo ```dart class UserInfo { final String email; UserInfo({required this.email}); factory UserInfo.fromJson(Map json) => UserInfo( email: json['email'] as String? ?? '', ); } ``` ## 4. Services ### ApiService Utilise `dio`. Un intercepteur ajoute automatiquement le header `Authorization: Bearer `. Concept: - `setBearerToken(token)` ajoute un `InterceptorsWrapper` a Dio. - `clearBearerToken()` retire les intercepteurs existants. - Methodes: `getAccounts()`, `getSchedule(account)`, `getGameDetail(gameId, account)`, `getUserInfo()`. - Retournent des objets modeles via `fromJson`. ### AuthService Utilise `flutter_web_auth` pour l'OIDC + `shared_preferences` pour stocker le token. Concept: - `storedToken`, `storedAccount` depuis `SharedPreferences`. - `login()`: appelle `FlutterWebAuth.authenticate(url: baseUrl/login, callbackUrlScheme: 'myice')`. Parse le fragment `#access_token=xxx` de l'URL retournee. Stocke le token. - `setAccount(account)`: persiste le compte selectionne. - `logout()`: supprime token, account, userInfo des prefs. **Configuration deep link requise:** - Android: intent-filter dans `AndroidManifest.xml` pour scheme `myice://`. - iOS: `CFBundleURLTypes` dans `Info.plist` avec scheme `myice`. ## 5. Providers (Provider package) ### AuthProvider Gere l'etat global d'authentification. **Etat:** `isAuthenticated`, `userInfo`. **Methodes:** - `init()`: Verifie token stocke, appelle `/userinfo`. Si 401, considere deconnecte. - `login()`: Lance AuthService.login(), met a jour le token, appelle `/userinfo`, notifie listeners. - `logout()`: Efface le token, notifie listeners. ### ScheduleProvider Gere les events, les comptes, et les filtres. **Etat:** `accounts`, `events`, `selectedAccount`, `selectedAgegroup`, `selectedSubgroup`, `isLoading`, `error`. **Methodes:** - `loadAccounts()`: Appelle `/accounts`, met a jour `accounts`. Selectionne `storedAccount` si present, sinon le premier. - `loadSchedule()`: Met `isLoading = true`, appelle `/schedule?account=selectedAccount`, filtre events ou `eventType == "Jeu"`, extrait les `agegroup` uniques pour les filtres. - `setAccount(String)`: Met a jour `selectedAccount`, sauvegarde dans SharedPreferences, appelle `loadSchedule()`. - `setAgegroup(String)`: Met a jour, reset `selectedSubgroup`, notifie. - `setSubgroup(String)`: Met a jour, notifie. - `get filteredEvents`: Getter filtre `events` par `agegroup` et `subgroup`. **Logique de filtre sous-groupe:** Identique a la webapp. La fonction `extractSubgroup(name)` retire la partie apres `" - "` dans le `name` de l'event pour obtenir le sous-groupe (si applicable). ### EventDetailProvider (Optionnel) Gere le chargement des details d'un event. Peut etre integre dans `ScheduleProvider` si prefere pour garder un seul provider. **Etat:** `eventDetail`, `isLoading`, `error`. **Methodes:** `loadEventDetail(gameId)` appelle `/game/{gameId}`. ## 6. Ecrans ### LoginScreen - **AppBar:** Aucune (centre). - **Body:** Column centree. - Titre: "MyIce - Games" (headlineLarge, couleur indigo). - Bouton: "Se connecter avec Infomaniak" (style degrade indigo -> violet). - Texte d'aide: "Connectez-vous pour acceder aux evenements...". - **Comportement:** - Si `authProvider.isAuthenticated`, navigue automatiquement vers `ScheduleScreen`. - Affiche `CircularProgressIndicator` pendant le login. - SnackBar en cas d'erreur. ### ScheduleScreen - **AppBar:** Titre "MyIce", action logout + email utilisateur. - **Body:** - **Filtres:** Card avec 3 `DropdownButtonFormField` (Compte, Age, Sous-groupe). Le bouton "Charger" est optionnel (changement de compte recharge auto). - **Liste:** `Expanded` + `ListView.builder`. Affiche `EventCard` pour chaque event filtre. - Etats: Loading (`CircularProgressIndicator`), vide (texte info), erreur (SnackBar). ### EventDetailScreen - **AppBar:** Titre = `event.title`, bouton retour. - **Body:** `SingleChildScrollView` + `Padding(16)`. - Card recap: Type, Lieu, Heure. - Section Joueurs: - Si vide: Card amber "Aucun joueur convoque". - Sinon: Card avec total + repartition positions. Liste triee par position puis numero: `[Position] #Num - Prenom Nom (DOB)`. - Section Personnel: - Si vide: Card bleu "Aucun personnel convoque". - Sinon: Liste `role: Prenom Nom`. - Si aucun joueur ET aucun personnel: Card amber principal "Aucun joueur ni personnel convoque" uniquement. ### Widgets reutilisables - **EventCard:** Card avec bordure laterale coloree. Titre (`agegroup - name`), sous-titre, details (adversaire, lieu, heure). onTap -> `EventDetailScreen`. - **FilterDropdown:** `DropdownButtonFormField` generique stylise. ## 7. Navigation & Routing Utilisation de `Navigator` classique (pas de package tierce pour 3 ecrans). - `main.dart`: `MaterialApp` avec `home` determine par `Consumer` (`ScheduleScreen` ou `LoginScreen`). - Navigation details: `Navigator.push(MaterialPageRoute(builder: (_) => EventDetailScreen(...)))`. ## 8. Gestion des erreurs - Erreur reseau / API: SnackBar rouge avec message. - 401 (Token expire): Deconnexion automatique + redirection `LoginScreen`. - JSON malforme: La webapp gere cote backend. Cote Flutter, `jsonDecode` echoue -> message "Donnees invalides". - Aucun event/joueur: Interface vide avec message informatif (pas d'erreur). ## 9. Configuration requise ### Android Ajouter un intent-filter dans `android/app/src/main/AndroidManifest.xml` pour le deep link `myice://callback`. ### iOS Ajouter `CFBundleURLTypes` dans `ios/Runner/Info.plist` avec scheme `myice`. ### Configuration API `lib/config/api_config.dart`: ```dart class ApiConfig { static const String baseUrl = 'https://your-api-domain.com'; // A configurer } ``` ## 10. Plan d'implementation (High-level) 1. Scaffolding du projet Flutter (`flutter create`). 2. Ajout des dependances (`pubspec.yaml`). 3. Configuration des deep links (`AndroidManifest` + `Info.plist`). 4. Implementation des modeles de donnees (`fromJson`). 5. Implementation des services (`ApiService`, `AuthService`). 6. Implementation des providers (`AuthProvider`, `ScheduleProvider`). 7. Implementation des ecrans (`LoginScreen`, `ScheduleScreen`, `EventDetailScreen`). 8. Implementation des widgets (`EventCard`, etc.). 9. Wiring dans `main.dart` avec `MultiProvider`. 10. Tests manuels de l'auth OIDC et de la navigation. --- **Prochaine etape apres validation du spec:** Creer le plan d'implementation detaille avec le skill `writing-plans`.