diff --git a/docs/23_LISTE-TICKETS.md b/docs/23_LISTE-TICKETS.md index 68dfa4c..3c473bf 100644 --- a/docs/23_LISTE-TICKETS.md +++ b/docs/23_LISTE-TICKETS.md @@ -1,12 +1,37 @@ # 🎫 Liste Complète des Tickets - Projet P'titsPas -**Version** : 1.1 +**Version** : 1.2 **Date** : 27 Janvier 2026 **Auteur** : Équipe PtitsPas **Estimation totale** : ~184h --- +## 🔗 Liste des tickets Gitea + +Correspondance entre les numéros d’issues Gitea et les tickets de ce document. + +| Gitea # | Titre court | Priorité | Statut | Section doc | +|--------|--------------|----------|--------|-------------| +| 1 | BDD - Champs manquants CDC | P0 | Ouvert | § Ticket #1 | +| 2 | BDD - Table présentation dossier parent | P0 | Ouvert | § Ticket #2 | +| 3 | BDD - Tokens création MDP | P0 | ✅ Fermé | § Ticket #3 | +| 4 | BDD - Champ genre enfants | P0 | ✅ Fermé | § Ticket #4 | +| 5 | BDD - Supprimer champs obsolètes | P0 | Ouvert | § Ticket #5 | +| 6 | BDD - Table configuration système | P0 | Ouvert | § Ticket #6 | +| 68 | BDD - Documents légaux & acceptations | P0 | ✅ Fermé | § Ticket #7 | +| 73 | Frontend - Inscription Parent Étape 1 | P3 | ✅ Fermé (PR) | § Ticket #36 | +| 78 | Frontend - Infrastructure formulaires multi-modes | P3 | ✅ Fermé | § Ticket #78 | +| 79 | Frontend - Renommer Nanny en AM | P3 | ✅ Fermé | § Ticket #79 | +| 81 | Frontend - Corrections refactoring widgets | P3 | ✅ Fermé | § Ticket #81 | +| 83 | Frontend - RegisterChoiceScreen mobile | P3 | ✅ Fermé | § Ticket #83 | + +*Les autres tickets (sans numéro Gitea dans ce tableau) sont décrits dans les sections par priorité ci‑dessous ; les numéros de section (#1 à #83) sont les références internes du document.* + +**Point API (tickets frontend)** – 27/01/2026 : 20 issues avec le label `frontend` dans Gitea (12 ouvertes, 8 fermées). Numéros concernés : 35–42, 43–51, 54, 82, 83. Les #73, #78, #79, #81 sont fermés mais sans label dans l’API. Détail : `docs/POINT_TICKETS_FRONT_API.txt`. + +--- + ## 📊 Vue d'ensemble ### Répartition par priorité @@ -1218,6 +1243,6 @@ Rédiger les documents légaux génériques (CGU et Politique de confidentialit --- **Dernière mise à jour** : 27 Janvier 2026 -**Version** : 1.1 +**Version** : 1.2 **Statut** : ✅ À jour diff --git a/frontend/assets/images/river_logo_mobile.png b/frontend/assets/images/river_logo_mobile.png new file mode 100644 index 0000000..8a7366a Binary files /dev/null and b/frontend/assets/images/river_logo_mobile.png differ diff --git a/frontend/lib/screens/auth/login_screen.dart b/frontend/lib/screens/auth/login_screen.dart index cd883e4..6d6cfd2 100644 --- a/frontend/lib/screens/auth/login_screen.dart +++ b/frontend/lib/screens/auth/login_screen.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; import 'package:url_launcher/url_launcher.dart'; import 'package:go_router/go_router.dart'; import 'package:p_tits_pas/services/bug_report_service.dart'; @@ -17,7 +16,7 @@ class LoginScreen extends StatefulWidget { State createState() => _LoginPageState(); } -class _LoginPageState extends State { +class _LoginPageState extends State with WidgetsBindingObserver { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); @@ -25,13 +24,28 @@ class _LoginPageState extends State { bool _isLoading = false; String? _errorMessage; + static const double _mobileBreakpoint = 900.0; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + } + @override void dispose() { + WidgetsBinding.instance.removeObserver(this); _emailController.dispose(); _passwordController.dispose(); super.dispose(); } + @override + void didChangeMetrics() { + super.didChangeMetrics(); + if (mounted) setState(() {}); + } + String? _validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Veuillez entrer votre email'; @@ -122,16 +136,20 @@ class _LoginPageState extends State { @override Widget build(BuildContext context) { + final isMobile = MediaQuery.of(context).size.width < _mobileBreakpoint; + if (isMobile) { + return Scaffold( + backgroundColor: Colors.transparent, + body: _buildMobileLayout(context), + ); + } return Scaffold( backgroundColor: Colors.transparent, body: LayoutBuilder( builder: (context, constraints) { - // Version desktop (web) - if (kIsWeb) { - final w = constraints.maxWidth; - final h = constraints.maxHeight; - - return FutureBuilder( + final w = constraints.maxWidth; + final h = constraints.maxHeight; + return FutureBuilder( future: _getImageDimensions(), builder: (context, snapshot) { if (!snapshot.hasData) { @@ -289,18 +307,19 @@ class _LoginPageState extends State { ), ), ), - // Pied de page + // Pied de page (Wrap pour éviter overflow sur petite largeur) Positioned( left: 0, right: 0, bottom: 0, child: Container( padding: const EdgeInsets.symmetric(vertical: 8.0), - decoration: BoxDecoration( + decoration: const BoxDecoration( color: Colors.transparent, ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, + child: Wrap( + alignment: WrapAlignment.center, + runSpacing: 8, children: [ _FooterLink( text: 'Contact support', @@ -349,17 +368,207 @@ class _LoginPageState extends State { ); }, ); - } - - // Version mobile (à implémenter) - return const Center( - child: Text('Version mobile à implémenter'), - ); }, ), ); } + /// Dimensions de river_logo_mobile.png (à mettre à jour si l'asset change). + static const int _riverLogoMobileWidth = 600; + static const int _riverLogoMobileHeight = 1080; + /// Fraction de la hauteur de l'image où se termine visuellement le slogan (0 = haut, 1 = bas). + static const double _sloganEndFraction = 0.42; + static const double _gapBelowSlogan = 12.0; + + Widget _buildMobileLayout(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final h = constraints.maxHeight; + final w = constraints.maxWidth; + final imageAspectRatio = _riverLogoMobileHeight / _riverLogoMobileWidth; + final formTop = w * imageAspectRatio * _sloganEndFraction + _gapBelowSlogan; + return Stack( + clipBehavior: Clip.none, + children: [ + Positioned.fill( + child: Image.asset( + 'assets/images/paper2.png', + fit: BoxFit.cover, + repeat: ImageRepeat.repeat, + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + height: h * 1.2, + child: OverflowBox( + alignment: Alignment.topCenter, + minWidth: w, + maxWidth: w, + minHeight: 0, + maxHeight: h * 2.5, + child: Image.asset( + 'assets/images/river_logo_mobile.png', + width: w, + fit: BoxFit.fitWidth, + ), + ), + ), + Positioned( + top: formTop, + left: 0, + right: 0, + bottom: 0, + child: SafeArea( + top: false, + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + CustomAppTextField( + controller: _emailController, + labelText: 'Email', + showLabel: false, + hintText: 'Votre adresse email', + validator: _validateEmail, + style: CustomAppTextFieldStyle.lavande, + fieldHeight: 48, + fieldWidth: double.infinity, + ), + const SizedBox(height: 12), + CustomAppTextField( + controller: _passwordController, + labelText: 'Mot de passe', + showLabel: false, + hintText: 'Votre mot de passe', + obscureText: true, + validator: _validatePassword, + style: CustomAppTextFieldStyle.jaune, + fieldHeight: 48, + fieldWidth: double.infinity, + ), + if (_errorMessage != null) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.red.shade300), + ), + child: Row( + children: [ + Icon(Icons.error_outline, color: Colors.red.shade700, size: 20), + const SizedBox(width: 10), + Expanded( + child: Text( + _errorMessage!, + style: GoogleFonts.merienda(fontSize: 12, color: Colors.red.shade700), + ), + ), + ], + ), + ), + ], + const SizedBox(height: 12), + _isLoading + ? const CircularProgressIndicator() + : ImageButton( + bg: 'assets/images/bg_green.png', + width: double.infinity, + height: 44, + text: 'Se connecter', + textColor: const Color(0xFF2D6A4F), + onPressed: _handleLogin, + ), + const SizedBox(height: 12), + TextButton( + onPressed: () { /* TODO */ }, + child: Text( + 'Mot de passe oublié ?', + style: GoogleFonts.merienda( + fontSize: 14, + color: const Color(0xFF2D6A4F), + decoration: TextDecoration.underline, + ), + ), + ), + TextButton( + onPressed: () => context.go('/register-choice'), + child: Text( + 'Créer un compte', + style: GoogleFonts.merienda( + fontSize: 16, + color: const Color(0xFF2D6A4F), + decoration: TextDecoration.underline, + ), + ), + ), + ], + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 12, top: 8), + child: Wrap( + alignment: WrapAlignment.center, + runSpacing: 6, + spacing: 4, + children: [ + _FooterLink( + text: 'Contact support', + fontSize: 11, + onTap: () async { + final uri = Uri(scheme: 'mailto', path: 'support@supernounou.local'); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Impossible d\'ouvrir le client mail', style: GoogleFonts.merienda())), + ); + } + }, + ), + _FooterLink( + text: 'Signaler un bug', + fontSize: 11, + onTap: () => _showBugReportDialog(context), + ), + _FooterLink( + text: 'Mentions légales', + fontSize: 11, + onTap: () => context.go('/legal'), + ), + _FooterLink( + text: 'Politique de confidentialité', + fontSize: 11, + onTap: () => context.go('/privacy'), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ); + }, + ); + } + void _showBugReportDialog(BuildContext context) { final TextEditingController controller = TextEditingController(); @@ -471,10 +680,12 @@ class ImageDimensions { class _FooterLink extends StatelessWidget { final String text; final VoidCallback onTap; + final double fontSize; const _FooterLink({ required this.text, required this.onTap, + this.fontSize = 14.0, }); @override @@ -482,11 +693,11 @@ class _FooterLink extends StatelessWidget { return InkWell( onTap: onTap, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: EdgeInsets.symmetric(horizontal: fontSize > 12 ? 8.0 : 4.0), child: Text( text, style: GoogleFonts.merienda( - fontSize: 14, + fontSize: fontSize, color: Colors.black87, decoration: TextDecoration.underline, ), diff --git a/frontend/lib/widgets/custom_app_text_field.dart b/frontend/lib/widgets/custom_app_text_field.dart index 2492cab..f593a15 100644 --- a/frontend/lib/widgets/custom_app_text_field.dart +++ b/frontend/lib/widgets/custom_app_text_field.dart @@ -25,11 +25,13 @@ class CustomAppTextField extends StatefulWidget { final IconData? suffixIcon; final double labelFontSize; final double inputFontSize; + final bool showLabel; const CustomAppTextField({ super.key, required this.controller, required this.labelText, + this.showLabel = true, this.hintText = '', this.fieldWidth = 300.0, this.fieldHeight = 53.0, @@ -73,15 +75,17 @@ class _CustomAppTextFieldState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text( - widget.labelText, - style: GoogleFonts.merienda( - fontSize: widget.labelFontSize, - color: Colors.black87, - fontWeight: FontWeight.w500, + if (widget.showLabel) ...[ + Text( + widget.labelText, + style: GoogleFonts.merienda( + fontSize: widget.labelFontSize, + color: Colors.black87, + fontWeight: FontWeight.w500, + ), ), - ), - const SizedBox(height: 6), + const SizedBox(height: 6), + ], SizedBox( width: widget.fieldWidth, height: dynamicFieldHeight,