diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 0000000..1d78244 --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1 @@ +docs/superpowers/specs/*.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 039a8fc..455356e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: rev: v0.13.0 hooks: - id: markdownlint - exclude: "^.github|(^docs/_sidebar\\.md$)" + exclude: "^.github|_sidebar\\.md$|superpowers/specs/.*\\.md$" - repo: https://github.com/shellcheck-py/shellcheck-py rev: v0.11.0.1 diff --git a/docs/superpowers/specs/2026-07-06-myice-flutter-app-design.md b/docs/superpowers/specs/2026-07-06-myice-flutter-app-design.md new file mode 100644 index 0000000..39c5ab0 --- /dev/null +++ b/docs/superpowers/specs/2026-07-06-myice-flutter-app-design.md @@ -0,0 +1,350 @@ + +# 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`.