feat(frontend): Refonte infrastructure formulaires multi-modes
- Support des modes Desktop/Mobile et Édition/Lecture seule - Refactoring des widgets de formulaire (PersonalInfo, ProfessionalInfo, Presentation, ChildCard) - Mise à jour des écrans de récapitulatif (ParentStep5, AmStep4) - Ajout de navigation (Précédent/Soumettre) sur mobile Closes #78 Co-authored-by: Cursor <cursoragent@cursor.com>
@ -20,7 +20,7 @@
|
|||||||
| **P4** | 4 tickets | ~24h | Tests & Documentation |
|
| **P4** | 4 tickets | ~24h | Tests & Documentation |
|
||||||
| **CRITIQUES** | 6 tickets | ~13h | Upload, Logs, Infra, CDC |
|
| **CRITIQUES** | 6 tickets | ~13h | Upload, Logs, Infra, CDC |
|
||||||
| **JURIDIQUE** | 1 ticket | ~8h | Rédaction CGU/Privacy |
|
| **JURIDIQUE** | 1 ticket | ~8h | Rédaction CGU/Privacy |
|
||||||
| **TOTAL** | **61 tickets** | **~173h** | |
|
| **TOTAL** | **62 tickets** | **~181h** | |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -892,6 +892,27 @@ Créer l'écran de consultation des logs système (optionnel pour v1.1).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Ticket #78 : [Frontend] Refonte Infrastructure Formulaires Multi-modes ✅
|
||||||
|
**Estimation** : 8h
|
||||||
|
**Labels** : `frontend`, `p3`, `refactoring`, `ux`
|
||||||
|
**Statut** : ✅ TERMINÉ (Fermé le 2026-01-27)
|
||||||
|
|
||||||
|
**Description** :
|
||||||
|
Créer une infrastructure générique pour gérer les formulaires en modes multiples (Editable / Readonly / Mobile / Desktop) afin d'harmoniser l'UI et faciliter la maintenance.
|
||||||
|
|
||||||
|
**Tâches** :
|
||||||
|
- [x] Créer `DisplayConfig` et `DisplayMode` (editable, readonly)
|
||||||
|
- [x] Créer `FormFieldWrapper` pour affichage uniforme readonly
|
||||||
|
- [x] Migrer `PersonalInfoFormScreen` (Parents/AM)
|
||||||
|
- [x] Migrer `ChildCardWidget` (Enfants)
|
||||||
|
- [x] Migrer `ProfessionalInfoFormScreen` (AM)
|
||||||
|
- [x] Migrer `PresentationFormScreen` (Motivation/CGU)
|
||||||
|
- [x] Implémenter layout "Vintage" (2:1) pour Desktop Readonly
|
||||||
|
- [x] Implémenter layout adaptatif pour Mobile Readonly
|
||||||
|
- [x] Harmoniser styles (champs beiges, polices)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🔵 PRIORITÉ 4 : Tests & Documentation
|
## 🔵 PRIORITÉ 4 : Tests & Documentation
|
||||||
|
|
||||||
### Ticket #52 : [Tests] Tests unitaires Backend
|
### Ticket #52 : [Tests] Tests unitaires Backend
|
||||||
@ -1116,7 +1137,7 @@ Rédiger les documents légaux génériques (CGU et Politique de confidentialit
|
|||||||
- **P0 (Bloquant BDD)** : 7 tickets (~5h)
|
- **P0 (Bloquant BDD)** : 7 tickets (~5h)
|
||||||
- **P1 (Bloquant Config)** : 7 tickets (~22h)
|
- **P1 (Bloquant Config)** : 7 tickets (~22h)
|
||||||
- **P2 (Backend)** : 18 tickets (~50h)
|
- **P2 (Backend)** : 18 tickets (~50h)
|
||||||
- **P3 (Frontend)** : 17 tickets (~52h) ← +1 ticket logs admin
|
- **P3 (Frontend)** : 18 tickets (~60h) ← +1 ticket logs admin, +1 ticket refonte formulaires
|
||||||
- **P4 (Tests/Doc)** : 4 tickets (~24h)
|
- **P4 (Tests/Doc)** : 4 tickets (~24h)
|
||||||
- **Critiques** : 6 tickets (~13h) ← -2 email, +1 logs, +1 CDC
|
- **Critiques** : 6 tickets (~13h) ← -2 email, +1 logs, +1 CDC
|
||||||
- **Juridique** : 1 ticket (~8h)
|
- **Juridique** : 1 ticket (~8h)
|
||||||
@ -1124,7 +1145,7 @@ Rédiger les documents légaux génériques (CGU et Politique de confidentialit
|
|||||||
### Par domaine
|
### Par domaine
|
||||||
- **BDD** : 7 tickets
|
- **BDD** : 7 tickets
|
||||||
- **Backend** : 23 tickets ← +1 logs
|
- **Backend** : 23 tickets ← +1 logs
|
||||||
- **Frontend** : 17 tickets ← +1 logs admin
|
- **Frontend** : 18 tickets ← +1 logs admin, +1 refonte formulaires
|
||||||
- **Tests** : 3 tickets
|
- **Tests** : 3 tickets
|
||||||
- **Documentation** : 5 tickets ← +1 amendement CDC
|
- **Documentation** : 5 tickets ← +1 amendement CDC
|
||||||
- **Infra** : 2 tickets
|
- **Infra** : 2 tickets
|
||||||
@ -1134,7 +1155,7 @@ Rédiger les documents légaux génériques (CGU et Politique de confidentialit
|
|||||||
- ❌ **Supprimé** : Tickets "Renvoyer email validation" (backend + frontend) - Pas prioritaire
|
- ❌ **Supprimé** : Tickets "Renvoyer email validation" (backend + frontend) - Pas prioritaire
|
||||||
- ✅ **Ajouté** : Ticket #55 "Service Logging Winston" - Monitoring essentiel
|
- ✅ **Ajouté** : Ticket #55 "Service Logging Winston" - Monitoring essentiel
|
||||||
- ✅ **Ajouté** : Ticket #56 "Écran Logs Admin" - Optionnel Phase 1.1
|
- ✅ **Ajouté** : Ticket #56 "Écran Logs Admin" - Optionnel Phase 1.1
|
||||||
- ✅ **Ajouté** : Ticket #60 "Amendement CDC v1.4 - Suppression SMS" - Simplification
|
- ✅ **Ajouté** : Ticket #78 "Refonte Infrastructure Formulaires" - Harmonisation UI/UX
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 510 KiB After Width: | Height: | Size: 510 KiB |
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
@ -4,7 +4,6 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
// Models
|
// Models
|
||||||
import '../models/user_registration_data.dart';
|
import '../models/user_registration_data.dart';
|
||||||
import '../models/nanny_registration_data.dart';
|
|
||||||
import '../models/am_registration_data.dart';
|
import '../models/am_registration_data.dart';
|
||||||
|
|
||||||
// Screens
|
// Screens
|
||||||
@ -15,11 +14,6 @@ import '../screens/auth/parent_register_step2_screen.dart';
|
|||||||
import '../screens/auth/parent_register_step3_screen.dart';
|
import '../screens/auth/parent_register_step3_screen.dart';
|
||||||
import '../screens/auth/parent_register_step4_screen.dart';
|
import '../screens/auth/parent_register_step4_screen.dart';
|
||||||
import '../screens/auth/parent_register_step5_screen.dart';
|
import '../screens/auth/parent_register_step5_screen.dart';
|
||||||
import '../screens/auth/nanny_register_step1_screen.dart';
|
|
||||||
import '../screens/auth/nanny_register_step2_screen.dart';
|
|
||||||
import '../screens/auth/nanny_register_step3_screen.dart';
|
|
||||||
import '../screens/auth/nanny_register_step4_screen.dart';
|
|
||||||
import '../screens/auth/nanny_register_confirmation_screen.dart';
|
|
||||||
import '../screens/auth/am_register_step1_screen.dart';
|
import '../screens/auth/am_register_step1_screen.dart';
|
||||||
import '../screens/auth/am_register_step2_screen.dart';
|
import '../screens/auth/am_register_step2_screen.dart';
|
||||||
import '../screens/auth/am_register_step3_screen.dart';
|
import '../screens/auth/am_register_step3_screen.dart';
|
||||||
@ -33,7 +27,6 @@ import '../screens/unknown_screen.dart';
|
|||||||
// For ShellRoute, creating them here and passing via .value is common.
|
// For ShellRoute, creating them here and passing via .value is common.
|
||||||
|
|
||||||
final userRegistrationDataNotifier = UserRegistrationData();
|
final userRegistrationDataNotifier = UserRegistrationData();
|
||||||
final nannyRegistrationDataNotifier = NannyRegistrationData();
|
|
||||||
final amRegistrationDataNotifier = AmRegistrationData();
|
final amRegistrationDataNotifier = AmRegistrationData();
|
||||||
|
|
||||||
class AppRouter {
|
class AppRouter {
|
||||||
@ -84,44 +77,6 @@ class AppRouter {
|
|||||||
path: '/parent-register-step5',
|
path: '/parent-register-step5',
|
||||||
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep5Screen(),
|
builder: (BuildContext context, GoRouterState state) => const ParentRegisterStep5Screen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
path: '/parent-register-confirmation',
|
|
||||||
builder: (BuildContext context, GoRouterState state) => const NannyRegisterConfirmationScreen(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
// --- Nanny Registration Flow ---
|
|
||||||
ShellRoute(
|
|
||||||
builder: (context, state, child) {
|
|
||||||
return ChangeNotifierProvider<NannyRegistrationData>.value(
|
|
||||||
value: nannyRegistrationDataNotifier,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
routes: <RouteBase>[
|
|
||||||
GoRoute(
|
|
||||||
path: '/nanny-register-step1',
|
|
||||||
builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep1Screen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/nanny-register-step2',
|
|
||||||
builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep2Screen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/nanny-register-step3',
|
|
||||||
builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep3Screen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/nanny-register-step4',
|
|
||||||
builder: (BuildContext context, GoRouterState state) => const NannyRegisterStep4Screen(),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: '/nanny-register-confirmation',
|
|
||||||
builder: (BuildContext context, GoRouterState state) {
|
|
||||||
return const NannyRegisterConfirmationScreen();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
106
frontend/lib/config/display_config.dart
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Mode d'affichage d'un formulaire
|
||||||
|
enum DisplayMode {
|
||||||
|
/// Mode éditable (formulaire d'inscription)
|
||||||
|
editable,
|
||||||
|
|
||||||
|
/// Mode lecture seule (récapitulatif)
|
||||||
|
readonly,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration d'affichage pour les widgets de formulaire
|
||||||
|
class DisplayConfig {
|
||||||
|
/// Mode d'affichage (editable/readonly)
|
||||||
|
final DisplayMode mode;
|
||||||
|
|
||||||
|
/// Type de layout détecté (mobile/desktop)
|
||||||
|
final LayoutType layoutType;
|
||||||
|
|
||||||
|
const DisplayConfig({
|
||||||
|
required this.mode,
|
||||||
|
required this.layoutType,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Crée une config à partir du contexte
|
||||||
|
factory DisplayConfig.fromContext(
|
||||||
|
BuildContext context, {
|
||||||
|
DisplayMode mode = DisplayMode.editable,
|
||||||
|
}) {
|
||||||
|
return DisplayConfig(
|
||||||
|
mode: mode,
|
||||||
|
layoutType: LayoutHelper.getLayoutType(context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Est en mode éditable
|
||||||
|
bool get isEditable => mode == DisplayMode.editable;
|
||||||
|
|
||||||
|
/// Est en mode lecture seule
|
||||||
|
bool get isReadonly => mode == DisplayMode.readonly;
|
||||||
|
|
||||||
|
/// Est en layout mobile
|
||||||
|
bool get isMobile => layoutType == LayoutType.mobile;
|
||||||
|
|
||||||
|
/// Est en layout desktop
|
||||||
|
bool get isDesktop => layoutType == LayoutType.desktop;
|
||||||
|
|
||||||
|
/// Layout vertical (mobile)
|
||||||
|
bool get isVerticalLayout => isMobile;
|
||||||
|
|
||||||
|
/// Layout horizontal (desktop)
|
||||||
|
bool get isHorizontalLayout => isDesktop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type de layout
|
||||||
|
enum LayoutType {
|
||||||
|
/// Mobile (< 600px) - toujours vertical
|
||||||
|
mobile,
|
||||||
|
|
||||||
|
/// Desktop/Tablette (≥ 600px) - horizontal
|
||||||
|
desktop,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utilitaires pour la détection de layout
|
||||||
|
class LayoutHelper {
|
||||||
|
/// Seuil de largeur pour mobile/desktop (en pixels)
|
||||||
|
static const double mobileBreakpoint = 600.0;
|
||||||
|
|
||||||
|
/// Détermine le type de layout selon la largeur d'écran
|
||||||
|
static LayoutType getLayoutType(BuildContext context) {
|
||||||
|
final width = MediaQuery.of(context).size.width;
|
||||||
|
return width < mobileBreakpoint
|
||||||
|
? LayoutType.mobile
|
||||||
|
: LayoutType.desktop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifie si on est sur mobile
|
||||||
|
static bool isMobile(BuildContext context) {
|
||||||
|
return getLayoutType(context) == LayoutType.mobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifie si on est sur desktop
|
||||||
|
static bool isDesktop(BuildContext context) {
|
||||||
|
return getLayoutType(context) == LayoutType.desktop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retourne un espacement adapté au layout
|
||||||
|
static double getSpacing(BuildContext context, {
|
||||||
|
double mobileSpacing = 12.0,
|
||||||
|
double desktopSpacing = 20.0,
|
||||||
|
}) {
|
||||||
|
return isMobile(context) ? mobileSpacing : desktopSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retourne une largeur max adaptée au layout
|
||||||
|
static double getMaxWidth(BuildContext context, {
|
||||||
|
double? mobileMaxWidth,
|
||||||
|
double? desktopMaxWidth,
|
||||||
|
}) {
|
||||||
|
if (isMobile(context)) {
|
||||||
|
return mobileMaxWidth ?? double.infinity;
|
||||||
|
} else {
|
||||||
|
return desktopMaxWidth ?? 1200.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,41 +1,18 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import '../../models/am_registration_data.dart';
|
|
||||||
import '../../widgets/image_button.dart';
|
|
||||||
import '../../models/card_assets.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
// Méthode helper pour afficher un champ de type "lecture seule" stylisé
|
import '../../models/am_registration_data.dart';
|
||||||
Widget _buildDisplayFieldValue(BuildContext context, String label, String value, {bool multiLine = false, double fieldHeight = 50.0, double labelFontSize = 18.0}) {
|
import '../../models/card_assets.dart';
|
||||||
const FontWeight labelFontWeight = FontWeight.w600;
|
import '../../config/display_config.dart';
|
||||||
|
import '../../widgets/hover_relief_widget.dart';
|
||||||
return Column(
|
import '../../widgets/image_button.dart';
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
import '../../widgets/custom_navigation_button.dart';
|
||||||
children: [
|
import '../../widgets/personal_info_form_screen.dart';
|
||||||
Text(label, style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: labelFontWeight)),
|
import '../../widgets/professional_info_form_screen.dart';
|
||||||
const SizedBox(height: 4),
|
import '../../widgets/presentation_form_screen.dart';
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
height: multiLine ? null : fieldHeight,
|
|
||||||
constraints: multiLine ? const BoxConstraints(minHeight: 50.0) : null,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: AssetImage('assets/images/input_field_bg.png'),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
value.isNotEmpty ? value : '-',
|
|
||||||
style: GoogleFonts.merienda(fontSize: labelFontSize),
|
|
||||||
maxLines: multiLine ? null : 1,
|
|
||||||
overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class AmRegisterStep4Screen extends StatefulWidget {
|
class AmRegisterStep4Screen extends StatefulWidget {
|
||||||
const AmRegisterStep4Screen({super.key});
|
const AmRegisterStep4Screen({super.key});
|
||||||
@ -49,6 +26,7 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final registrationData = Provider.of<AmRegistrationData>(context);
|
final registrationData = Provider.of<AmRegistrationData>(context);
|
||||||
final screenSize = MediaQuery.of(context).size;
|
final screenSize = MediaQuery.of(context).size;
|
||||||
|
final config = DisplayConfig.fromContext(context, mode: DisplayMode.readonly);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Stack(
|
body: Stack(
|
||||||
@ -60,7 +38,9 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
|
|||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 40.0),
|
padding: const EdgeInsets.symmetric(vertical: 40.0),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0),
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: config.isMobile ? 0 : screenSize.width / 4.0
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
@ -70,50 +50,167 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
|
|||||||
Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center),
|
Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
_buildPersonalInfoCard(context, registrationData),
|
// Carte 1: Informations personnelles
|
||||||
const SizedBox(height: 20),
|
_buildPersonalInfo(context, registrationData),
|
||||||
_buildProfessionalInfoCard(context, registrationData),
|
const SizedBox(height: 30),
|
||||||
const SizedBox(height: 20),
|
|
||||||
_buildPresentationCard(context, registrationData),
|
// Carte 2: Informations professionnelles
|
||||||
|
_buildProfessionalInfo(context, registrationData),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// Carte 3: Présentation
|
||||||
|
_buildPresentation(context, registrationData),
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
ImageButton(
|
// Boutons Mobile (Retour + Soumettre) ou Bouton Soumettre Desktop
|
||||||
bg: 'assets/images/btn_green.png',
|
if (config.isMobile)
|
||||||
text: 'Soumettre ma demande',
|
Padding(
|
||||||
textColor: const Color(0xFF2D6A4F),
|
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
|
||||||
width: 350,
|
child: Row(
|
||||||
height: 50,
|
children: [
|
||||||
fontSize: 18,
|
Expanded(
|
||||||
onPressed: () {
|
child: HoverReliefWidget(
|
||||||
print("Données AM finales: ${registrationData.firstName} ${registrationData.lastName}");
|
child: CustomNavigationButton(
|
||||||
_showConfirmationModal(context);
|
text: 'Précédent',
|
||||||
},
|
style: NavigationButtonStyle.purple,
|
||||||
),
|
onPressed: () {
|
||||||
|
if (context.canPop()) {
|
||||||
|
context.pop();
|
||||||
|
} else {
|
||||||
|
context.go('/am-register-step3');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: HoverReliefWidget(
|
||||||
|
child: CustomNavigationButton(
|
||||||
|
text: 'Soumettre',
|
||||||
|
style: NavigationButtonStyle.green,
|
||||||
|
onPressed: () {
|
||||||
|
print("Données AM finales: ${registrationData.firstName} ${registrationData.lastName}");
|
||||||
|
_showConfirmationModal(context);
|
||||||
|
},
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ImageButton(
|
||||||
|
bg: 'assets/images/bg_green.png',
|
||||||
|
text: 'Soumettre ma demande',
|
||||||
|
textColor: const Color(0xFF2D6A4F),
|
||||||
|
width: 350,
|
||||||
|
height: 50,
|
||||||
|
fontSize: 18,
|
||||||
|
onPressed: () {
|
||||||
|
print("Données AM finales: ${registrationData.firstName} ${registrationData.lastName}");
|
||||||
|
_showConfirmationModal(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
// Chevrons desktop uniquement
|
||||||
top: screenSize.height / 2 - 20,
|
if (!config.isMobile)
|
||||||
left: 40,
|
Positioned(
|
||||||
child: IconButton(
|
top: screenSize.height / 2 - 20,
|
||||||
icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)),
|
left: 40,
|
||||||
onPressed: () {
|
child: IconButton(
|
||||||
if (context.canPop()) {
|
icon: Transform(
|
||||||
context.pop();
|
alignment: Alignment.center,
|
||||||
} else {
|
transform: Matrix4.rotationY(math.pi),
|
||||||
context.go('/am-register-step3');
|
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||||
}
|
),
|
||||||
},
|
onPressed: () {
|
||||||
tooltip: 'Retour',
|
if (context.canPop()) {
|
||||||
|
context.pop();
|
||||||
|
} else {
|
||||||
|
context.go('/am-register-step3');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: 'Retour',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildPersonalInfo(BuildContext context, AmRegistrationData data) {
|
||||||
|
return PersonalInfoFormScreen(
|
||||||
|
mode: DisplayMode.readonly,
|
||||||
|
embedContentOnly: true,
|
||||||
|
stepText: '',
|
||||||
|
title: 'Informations personnelles',
|
||||||
|
cardColor: CardColorHorizontal.blue,
|
||||||
|
initialData: PersonalInfoData(
|
||||||
|
firstName: data.firstName,
|
||||||
|
lastName: data.lastName,
|
||||||
|
phone: data.phone,
|
||||||
|
email: data.email,
|
||||||
|
address: data.streetAddress,
|
||||||
|
postalCode: data.postalCode,
|
||||||
|
city: data.city,
|
||||||
|
),
|
||||||
|
onSubmit: (d, {hasSecondPerson, sameAddress}) {}, // No-op en readonly
|
||||||
|
previousRoute: '',
|
||||||
|
onEdit: () => context.go('/am-register-step1'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProfessionalInfo(BuildContext context, AmRegistrationData data) {
|
||||||
|
return ProfessionalInfoFormScreen(
|
||||||
|
mode: DisplayMode.readonly,
|
||||||
|
embedContentOnly: true,
|
||||||
|
stepText: '',
|
||||||
|
title: 'Informations professionnelles',
|
||||||
|
cardColor: CardColorHorizontal.green,
|
||||||
|
initialData: ProfessionalInfoData(
|
||||||
|
// TODO: Gérer photoPath vs photoFile correctement
|
||||||
|
photoPath: null, // Pas d'accès facile au fichier ici, on verra
|
||||||
|
dateOfBirth: data.dateOfBirth,
|
||||||
|
birthCity: data.birthCity,
|
||||||
|
birthCountry: data.birthCountry,
|
||||||
|
nir: data.nir,
|
||||||
|
agrementNumber: data.agrementNumber,
|
||||||
|
capacity: data.capacity,
|
||||||
|
photoConsent: data.photoConsent,
|
||||||
|
),
|
||||||
|
onSubmit: (d) {},
|
||||||
|
previousRoute: '',
|
||||||
|
onEdit: () => context.go('/am-register-step2'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPresentation(BuildContext context, AmRegistrationData data) {
|
||||||
|
return PresentationFormScreen(
|
||||||
|
mode: DisplayMode.readonly,
|
||||||
|
embedContentOnly: true,
|
||||||
|
stepText: '',
|
||||||
|
title: 'Présentation & CGU',
|
||||||
|
cardColor: CardColorHorizontal.peach,
|
||||||
|
textFieldHint: '',
|
||||||
|
initialText: data.presentationText,
|
||||||
|
initialCguAccepted: data.cguAccepted,
|
||||||
|
previousRoute: '',
|
||||||
|
onSubmit: (t, c) {},
|
||||||
|
onEdit: () => context.go('/am-register-step3'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _showConfirmationModal(BuildContext context) {
|
void _showConfirmationModal(BuildContext context) {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
@ -141,199 +238,4 @@ class _AmRegisterStep4ScreenState extends State<AmRegisterStep4Screen> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carte Informations personnelles
|
|
||||||
Widget _buildPersonalInfoCard(BuildContext context, AmRegistrationData data) {
|
|
||||||
const double verticalSpacing = 28.0;
|
|
||||||
const double labelFontSize = 22.0;
|
|
||||||
|
|
||||||
List<Widget> details = [
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)),
|
|
||||||
const SizedBox(width: 20),
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: verticalSpacing),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)),
|
|
||||||
const SizedBox(width: 20),
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: verticalSpacing),
|
|
||||||
_buildDisplayFieldValue(context, "Adresse:", "${data.streetAddress}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize),
|
|
||||||
const SizedBox(height: verticalSpacing),
|
|
||||||
_buildDisplayFieldValue(context, "Consentement photo:", data.photoConsent ? "Oui" : "Non", labelFontSize: labelFontSize),
|
|
||||||
];
|
|
||||||
|
|
||||||
return _SummaryCard(
|
|
||||||
backgroundImagePath: CardColorHorizontal.blue.path,
|
|
||||||
title: 'Informations personnelles',
|
|
||||||
content: details,
|
|
||||||
onEdit: () => context.go('/am-register-step1'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carte Informations professionnelles
|
|
||||||
Widget _buildProfessionalInfoCard(BuildContext context, AmRegistrationData data) {
|
|
||||||
const double verticalSpacing = 28.0;
|
|
||||||
const double labelFontSize = 22.0;
|
|
||||||
|
|
||||||
String formattedDate = '-';
|
|
||||||
if (data.dateOfBirth != null) {
|
|
||||||
formattedDate = '${data.dateOfBirth!.day.toString().padLeft(2, '0')}/${data.dateOfBirth!.month.toString().padLeft(2, '0')}/${data.dateOfBirth!.year}';
|
|
||||||
}
|
|
||||||
String birthPlace = '${data.birthCity}, ${data.birthCountry}'.trim();
|
|
||||||
if (birthPlace == ',') birthPlace = '-';
|
|
||||||
|
|
||||||
List<Widget> details = [
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "Date de naissance:", formattedDate, labelFontSize: labelFontSize)),
|
|
||||||
const SizedBox(width: 20),
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "Lieu de naissance:", birthPlace, labelFontSize: labelFontSize, multiLine: true)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: verticalSpacing),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "N° Sécurité Sociale:", data.nir, labelFontSize: labelFontSize)),
|
|
||||||
const SizedBox(width: 20),
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "N° Agrément:", data.agrementNumber, labelFontSize: labelFontSize)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: verticalSpacing),
|
|
||||||
_buildDisplayFieldValue(context, "Capacité d'accueil:", data.capacity?.toString() ?? '-', labelFontSize: labelFontSize),
|
|
||||||
];
|
|
||||||
|
|
||||||
return _SummaryCard(
|
|
||||||
backgroundImagePath: CardColorHorizontal.green.path,
|
|
||||||
title: 'Informations professionnelles',
|
|
||||||
content: details,
|
|
||||||
onEdit: () => context.go('/am-register-step2'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carte Présentation & CGU
|
|
||||||
Widget _buildPresentationCard(BuildContext context, AmRegistrationData data) {
|
|
||||||
const double labelFontSize = 22.0;
|
|
||||||
|
|
||||||
List<Widget> details = [
|
|
||||||
Text(
|
|
||||||
'Votre présentation (facultatif) :',
|
|
||||||
style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
constraints: const BoxConstraints(minHeight: 80.0),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: AssetImage('assets/images/input_field_bg.png'),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
data.presentationText.isNotEmpty ? data.presentationText : 'Aucune présentation rédigée.',
|
|
||||||
style: GoogleFonts.merienda(
|
|
||||||
fontSize: 18,
|
|
||||||
fontStyle: data.presentationText.isNotEmpty ? FontStyle.normal : FontStyle.italic,
|
|
||||||
color: data.presentationText.isNotEmpty ? Colors.black87 : Colors.black54,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
data.cguAccepted ? Icons.check_circle : Icons.cancel,
|
|
||||||
color: data.cguAccepted ? Colors.green : Colors.red,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
data.cguAccepted ? 'CGU acceptées' : 'CGU non acceptées',
|
|
||||||
style: GoogleFonts.merienda(fontSize: 18),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
return _SummaryCard(
|
|
||||||
backgroundImagePath: CardColorHorizontal.peach.path,
|
|
||||||
title: 'Présentation & CGU',
|
|
||||||
content: details,
|
|
||||||
onEdit: () => context.go('/am-register-step3'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Widget générique _SummaryCard
|
|
||||||
class _SummaryCard extends StatelessWidget {
|
|
||||||
final String backgroundImagePath;
|
|
||||||
final String title;
|
|
||||||
final List<Widget> content;
|
|
||||||
final VoidCallback onEdit;
|
|
||||||
|
|
||||||
const _SummaryCard({
|
|
||||||
super.key,
|
|
||||||
required this.backgroundImagePath,
|
|
||||||
required this.title,
|
|
||||||
required this.content,
|
|
||||||
required this.onEdit,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AspectRatio(
|
|
||||||
aspectRatio: 2.0,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: AssetImage(backgroundImagePath),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(15),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
...content,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
|
|
||||||
onPressed: onEdit,
|
|
||||||
tooltip: 'Modifier',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:p_tits_pas/services/bug_report_service.dart';
|
import 'package:p_tits_pas/services/bug_report_service.dart';
|
||||||
import '../../widgets/image_button.dart';
|
import '../../widgets/image_button.dart';
|
||||||
import '../../widgets/custom_app_text_field.dart';
|
import '../../widgets/custom_app_text_field.dart';
|
||||||
@ -241,7 +242,7 @@ class _LoginPageState extends State<LoginScreen> {
|
|||||||
child: _isLoading
|
child: _isLoading
|
||||||
? const CircularProgressIndicator()
|
? const CircularProgressIndicator()
|
||||||
: ImageButton(
|
: ImageButton(
|
||||||
bg: 'assets/images/btn_green.png',
|
bg: 'assets/images/bg_green.png',
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 40,
|
height: 40,
|
||||||
text: 'Se connecter',
|
text: 'Se connecter',
|
||||||
|
|||||||
@ -5,9 +5,11 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'dart:io' show File;
|
import 'dart:io' show File;
|
||||||
import '../../widgets/hover_relief_widget.dart';
|
import '../../widgets/hover_relief_widget.dart';
|
||||||
import '../../widgets/child_card_widget.dart';
|
import '../../widgets/child_card_widget.dart';
|
||||||
|
import '../../widgets/custom_navigation_button.dart';
|
||||||
import '../../models/user_registration_data.dart';
|
import '../../models/user_registration_data.dart';
|
||||||
import '../../utils/data_generator.dart';
|
import '../../utils/data_generator.dart';
|
||||||
import '../../models/card_assets.dart';
|
import '../../models/card_assets.dart';
|
||||||
|
import '../../config/display_config.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
@ -204,14 +206,157 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final registrationData = Provider.of<UserRegistrationData>(context /*, listen: true par défaut */);
|
final registrationData = Provider.of<UserRegistrationData>(context /*, listen: true par défaut */);
|
||||||
final screenSize = MediaQuery.of(context).size;
|
final screenSize = MediaQuery.of(context).size;
|
||||||
|
final config = DisplayConfig.fromContext(context, mode: DisplayMode.editable);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
|
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
|
||||||
),
|
),
|
||||||
Center(
|
config.isMobile
|
||||||
|
? _buildMobileLayout(context, config, screenSize, registrationData)
|
||||||
|
: _buildDesktopLayout(context, config, screenSize, registrationData),
|
||||||
|
// Chevrons desktop uniquement
|
||||||
|
if (!config.isMobile) ...[
|
||||||
|
Positioned(
|
||||||
|
top: screenSize.height / 2 - 20,
|
||||||
|
left: 40,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
|
||||||
|
onPressed: () {
|
||||||
|
if (context.canPop()) {
|
||||||
|
context.pop();
|
||||||
|
} else {
|
||||||
|
context.go('/parent-register-step2');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: 'Retour',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: screenSize.height / 2 - 20,
|
||||||
|
right: 40,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||||
|
onPressed: () {
|
||||||
|
context.go('/parent-register-step4');
|
||||||
|
},
|
||||||
|
tooltip: 'Suivant',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout MOBILE : Cartes empilées verticalement
|
||||||
|
Widget _buildMobileLayout(BuildContext context, DisplayConfig config, Size screenSize, UserRegistrationData registrationData) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// Header fixe
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 20.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Étape 3/5',
|
||||||
|
style: GoogleFonts.merienda(fontSize: 13, color: Colors.black54),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
'Informations Enfants',
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Liste scrollable des cartes + boutons
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Générer les cartes enfants
|
||||||
|
for (int index = 0; index < registrationData.children.length; index++) ...[
|
||||||
|
ChildCardWidget(
|
||||||
|
key: ValueKey(registrationData.children[index].hashCode),
|
||||||
|
childData: registrationData.children[index],
|
||||||
|
childIndex: index,
|
||||||
|
onPickImage: () => _pickImage(index, registrationData),
|
||||||
|
onDateSelect: () => _selectDate(context, index, registrationData),
|
||||||
|
onFirstNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData(
|
||||||
|
firstName: value, lastName: registrationData.children[index].lastName, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent,
|
||||||
|
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor
|
||||||
|
))),
|
||||||
|
onLastNameChanged: (value) => setState(() => registrationData.updateChild(index, ChildData(
|
||||||
|
firstName: registrationData.children[index].firstName, lastName: value, dob: registrationData.children[index].dob, photoConsent: registrationData.children[index].photoConsent,
|
||||||
|
multipleBirth: registrationData.children[index].multipleBirth, isUnbornChild: registrationData.children[index].isUnbornChild, imageFile: registrationData.children[index].imageFile, cardColor: registrationData.children[index].cardColor
|
||||||
|
))),
|
||||||
|
onTogglePhotoConsent: (newValue) {
|
||||||
|
final oldChild = registrationData.children[index];
|
||||||
|
registrationData.updateChild(index, ChildData(
|
||||||
|
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: newValue,
|
||||||
|
multipleBirth: oldChild.multipleBirth, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onToggleMultipleBirth: (newValue) {
|
||||||
|
final oldChild = registrationData.children[index];
|
||||||
|
registrationData.updateChild(index, ChildData(
|
||||||
|
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: oldChild.dob, photoConsent: oldChild.photoConsent,
|
||||||
|
multipleBirth: newValue, isUnbornChild: oldChild.isUnbornChild, imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onToggleIsUnborn: (newValue) {
|
||||||
|
final oldChild = registrationData.children[index];
|
||||||
|
registrationData.updateChild(index, ChildData(
|
||||||
|
firstName: oldChild.firstName, lastName: oldChild.lastName, dob: DataGenerator.dob(isUnborn: newValue),
|
||||||
|
photoConsent: oldChild.photoConsent, multipleBirth: oldChild.multipleBirth, isUnbornChild: newValue,
|
||||||
|
imageFile: oldChild.imageFile, cardColor: oldChild.cardColor
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onRemove: () => _removeChild(index, registrationData),
|
||||||
|
canBeRemoved: registrationData.children.length > 1,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Bouton "+" carré à la fin de la liste
|
||||||
|
Center(
|
||||||
|
child: HoverReliefWidget(
|
||||||
|
onPressed: () => _addChild(registrationData),
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
child: Container(
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
child: Image.asset('assets/images/plus.png', fit: BoxFit.contain),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
// Boutons navigation en bas du scroll
|
||||||
|
_buildMobileButtons(context, config, screenSize),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout DESKTOP : Scroll horizontal avec fondu
|
||||||
|
Widget _buildDesktopLayout(BuildContext context, DisplayConfig config, Size screenSize, UserRegistrationData registrationData) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text('Étape 3/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
|
Text('Étape 3/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
|
||||||
@ -288,12 +433,12 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Bouton Ajouter
|
// Bouton Ajouter Desktop (Gros bouton)
|
||||||
return Center(
|
return Center(
|
||||||
child: HoverReliefWidget(
|
child: HoverReliefWidget(
|
||||||
onPressed: () => _addChild(registrationData),
|
onPressed: () => _addChild(registrationData),
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
child: Image.asset('assets/images/plus.png', height: 80, width: 80),
|
child: Image.asset('assets/images/plus.png', height: 100, width: 100, fit: BoxFit.contain),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -306,13 +451,18 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
// Chevrons de navigation
|
}
|
||||||
Positioned(
|
|
||||||
top: screenSize.height / 2 - 20,
|
/// Boutons navigation mobile
|
||||||
left: 40,
|
Widget _buildMobileButtons(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||||
child: IconButton(
|
return Row(
|
||||||
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: HoverReliefWidget(
|
||||||
|
child: CustomNavigationButton(
|
||||||
|
text: 'Précédent',
|
||||||
|
style: NavigationButtonStyle.purple,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (context.canPop()) {
|
if (context.canPop()) {
|
||||||
context.pop();
|
context.pop();
|
||||||
@ -320,22 +470,28 @@ class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
|||||||
context.go('/parent-register-step2');
|
context.go('/parent-register-step2');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tooltip: 'Retour',
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
),
|
||||||
top: screenSize.height / 2 - 20,
|
const SizedBox(width: 16),
|
||||||
right: 40,
|
Expanded(
|
||||||
child: IconButton(
|
child: HoverReliefWidget(
|
||||||
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
child: CustomNavigationButton(
|
||||||
|
text: 'Suivant',
|
||||||
|
style: NavigationButtonStyle.green,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.go('/parent-register-step4');
|
context.go('/parent-register-step4');
|
||||||
},
|
},
|
||||||
tooltip: 'Suivant',
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,45 +1,18 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import '../../models/user_registration_data.dart'; // Utilisation du vrai modèle
|
|
||||||
import '../../widgets/image_button.dart'; // Import du ImageButton
|
|
||||||
import '../../models/card_assets.dart'; // Import des enums de cartes
|
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
// Nouvelle méthode helper pour afficher un champ de type "lecture seule" stylisé
|
import '../../models/user_registration_data.dart';
|
||||||
Widget _buildDisplayFieldValue(BuildContext context, String label, String value, {bool multiLine = false, double fieldHeight = 50.0, double labelFontSize = 18.0}) {
|
import '../../models/card_assets.dart';
|
||||||
const FontWeight labelFontWeight = FontWeight.w600;
|
import '../../config/display_config.dart';
|
||||||
|
import '../../widgets/hover_relief_widget.dart';
|
||||||
return Column(
|
import '../../widgets/image_button.dart';
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
import '../../widgets/custom_navigation_button.dart';
|
||||||
children: [
|
import '../../widgets/personal_info_form_screen.dart';
|
||||||
Text(label, style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: labelFontWeight)),
|
import '../../widgets/child_card_widget.dart';
|
||||||
const SizedBox(height: 4),
|
import '../../widgets/presentation_form_screen.dart';
|
||||||
Container(
|
|
||||||
width: double.infinity, // Prendra la largeur allouée par son parent (Expanded)
|
|
||||||
height: multiLine ? null : fieldHeight, // Hauteur flexible pour multiligne, sinon fixe
|
|
||||||
constraints: multiLine ? const BoxConstraints(minHeight: 50.0) : null, // Hauteur min pour multiligne
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), // Ajuster au besoin
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: const DecorationImage(
|
|
||||||
image: AssetImage('assets/images/input_field_bg.png'), // Image de fond du champ
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
// Si votre image input_field_bg.png a des coins arrondis intrinsèques, ce borderRadius n'est pas nécessaire
|
|
||||||
// ou doit correspondre. Sinon, pour une image rectangulaire, vous pouvez l'ajouter.
|
|
||||||
// borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
value.isNotEmpty ? value : '-',
|
|
||||||
style: GoogleFonts.merienda(fontSize: labelFontSize),
|
|
||||||
maxLines: multiLine ? null : 1, // Permet plusieurs lignes si multiLine est true
|
|
||||||
overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class ParentRegisterStep5Screen extends StatefulWidget {
|
class ParentRegisterStep5Screen extends StatefulWidget {
|
||||||
const ParentRegisterStep5Screen({super.key});
|
const ParentRegisterStep5Screen({super.key});
|
||||||
@ -53,9 +26,7 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final registrationData = Provider.of<UserRegistrationData>(context);
|
final registrationData = Provider.of<UserRegistrationData>(context);
|
||||||
final screenSize = MediaQuery.of(context).size;
|
final screenSize = MediaQuery.of(context).size;
|
||||||
final cardWidth = screenSize.width / 2.0; // Largeur de la carte (50% de l'écran)
|
final config = DisplayConfig.fromContext(context, mode: DisplayMode.readonly);
|
||||||
final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0)
|
|
||||||
final cardHeight = cardWidth / imageAspectRatio;
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Stack(
|
body: Stack(
|
||||||
@ -65,9 +36,11 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
|
|||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 40.0), // Padding horizontal supprimé ici
|
padding: const EdgeInsets.symmetric(vertical: 40.0),
|
||||||
child: Padding( // Ajout du Padding horizontal externe
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0),
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: config.isMobile ? 0 : screenSize.width / 4.0
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
@ -77,52 +50,202 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
|
|||||||
Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center),
|
Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
_buildParent1Card(context, registrationData.parent1),
|
// Carte Parent 1
|
||||||
|
_buildParent1(context, registrationData),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Carte Parent 2 (si présent)
|
||||||
if (registrationData.parent2 != null) ...[
|
if (registrationData.parent2 != null) ...[
|
||||||
_buildParent2Card(context, registrationData.parent2!),
|
_buildParent2(context, registrationData),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
..._buildChildrenCards(context, registrationData.children),
|
|
||||||
_buildMotivationCard(context, registrationData.motivationText),
|
// Cartes Enfants
|
||||||
const SizedBox(height: 40),
|
...registrationData.children.asMap().entries.map((entry) =>
|
||||||
ImageButton(
|
Column(
|
||||||
bg: 'assets/images/btn_green.png',
|
children: [
|
||||||
text: 'Soumettre ma demande',
|
_buildChildCard(context, entry.value, entry.key),
|
||||||
textColor: const Color(0xFF2D6A4F),
|
const SizedBox(height: 20),
|
||||||
width: 350,
|
],
|
||||||
height: 50,
|
)
|
||||||
fontSize: 18,
|
|
||||||
onPressed: () {
|
|
||||||
print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}");
|
|
||||||
_showConfirmationModal(context);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Carte Motivation
|
||||||
|
_buildMotivation(context, registrationData),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
|
// Boutons Mobile (Retour + Soumettre) ou Bouton Soumettre Desktop
|
||||||
|
if (config.isMobile)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: HoverReliefWidget(
|
||||||
|
child: CustomNavigationButton(
|
||||||
|
text: 'Précédent',
|
||||||
|
style: NavigationButtonStyle.purple,
|
||||||
|
onPressed: () {
|
||||||
|
if (context.canPop()) {
|
||||||
|
context.pop();
|
||||||
|
} else {
|
||||||
|
context.go('/parent-register-step4');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: HoverReliefWidget(
|
||||||
|
child: CustomNavigationButton(
|
||||||
|
text: 'Soumettre',
|
||||||
|
style: NavigationButtonStyle.green,
|
||||||
|
onPressed: () {
|
||||||
|
print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}");
|
||||||
|
_showConfirmationModal(context);
|
||||||
|
},
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ImageButton(
|
||||||
|
bg: 'assets/images/bg_green.png',
|
||||||
|
text: 'Soumettre ma demande',
|
||||||
|
textColor: const Color(0xFF2D6A4F),
|
||||||
|
width: 350,
|
||||||
|
height: 50,
|
||||||
|
fontSize: 18,
|
||||||
|
onPressed: () {
|
||||||
|
print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}");
|
||||||
|
_showConfirmationModal(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
// Chevrons desktop uniquement
|
||||||
top: screenSize.height / 2 - 20,
|
if (!config.isMobile)
|
||||||
left: 40,
|
Positioned(
|
||||||
child: IconButton(
|
top: screenSize.height / 2 - 20,
|
||||||
icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)),
|
left: 40,
|
||||||
onPressed: () {
|
child: IconButton(
|
||||||
if (context.canPop()) {
|
icon: Transform(
|
||||||
context.pop();
|
alignment: Alignment.center,
|
||||||
} else {
|
transform: Matrix4.rotationY(math.pi),
|
||||||
context.go('/parent-register-step4');
|
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||||
}
|
),
|
||||||
},
|
onPressed: () {
|
||||||
tooltip: 'Retour',
|
if (context.canPop()) {
|
||||||
|
context.pop();
|
||||||
|
} else {
|
||||||
|
context.go('/parent-register-step4');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: 'Retour',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildParent1(BuildContext context, UserRegistrationData data) {
|
||||||
|
return PersonalInfoFormScreen(
|
||||||
|
mode: DisplayMode.readonly,
|
||||||
|
embedContentOnly: true,
|
||||||
|
stepText: '',
|
||||||
|
title: 'Informations du Parent Principal',
|
||||||
|
cardColor: CardColorHorizontal.peach,
|
||||||
|
initialData: PersonalInfoData(
|
||||||
|
firstName: data.parent1.firstName,
|
||||||
|
lastName: data.parent1.lastName,
|
||||||
|
phone: data.parent1.phone,
|
||||||
|
email: data.parent1.email,
|
||||||
|
address: data.parent1.address,
|
||||||
|
postalCode: data.parent1.postalCode,
|
||||||
|
city: data.parent1.city,
|
||||||
|
),
|
||||||
|
onSubmit: (d, {hasSecondPerson, sameAddress}) {},
|
||||||
|
previousRoute: '',
|
||||||
|
onEdit: () => context.go('/parent-register-step1'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildParent2(BuildContext context, UserRegistrationData data) {
|
||||||
|
if (data.parent2 == null) return const SizedBox();
|
||||||
|
return PersonalInfoFormScreen(
|
||||||
|
mode: DisplayMode.readonly,
|
||||||
|
embedContentOnly: true,
|
||||||
|
stepText: '',
|
||||||
|
title: 'Informations du Deuxième Parent',
|
||||||
|
cardColor: CardColorHorizontal.blue,
|
||||||
|
initialData: PersonalInfoData(
|
||||||
|
firstName: data.parent2!.firstName,
|
||||||
|
lastName: data.parent2!.lastName,
|
||||||
|
phone: data.parent2!.phone,
|
||||||
|
email: data.parent2!.email,
|
||||||
|
address: data.parent2!.address,
|
||||||
|
postalCode: data.parent2!.postalCode,
|
||||||
|
city: data.parent2!.city,
|
||||||
|
),
|
||||||
|
onSubmit: (d, {hasSecondPerson, sameAddress}) {},
|
||||||
|
previousRoute: '',
|
||||||
|
onEdit: () => context.go('/parent-register-step2'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildChildCard(BuildContext context, ChildData child, int index) {
|
||||||
|
// Note: Le titre est maintenant intégré dans la carte ChildCardWidget en mode readonly
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
ChildCardWidget(
|
||||||
|
key: ValueKey('child_readonly_$index'),
|
||||||
|
childData: child,
|
||||||
|
childIndex: index,
|
||||||
|
mode: DisplayMode.readonly,
|
||||||
|
onPickImage: () {},
|
||||||
|
onDateSelect: () {},
|
||||||
|
onFirstNameChanged: (v) {},
|
||||||
|
onLastNameChanged: (v) {},
|
||||||
|
onTogglePhotoConsent: (v) {},
|
||||||
|
onToggleMultipleBirth: (v) {},
|
||||||
|
onToggleIsUnborn: (v) {},
|
||||||
|
onRemove: () {},
|
||||||
|
canBeRemoved: false,
|
||||||
|
onEdit: () => context.go('/parent-register-step3', extra: {'childIndex': index}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMotivation(BuildContext context, UserRegistrationData data) {
|
||||||
|
return PresentationFormScreen(
|
||||||
|
mode: DisplayMode.readonly,
|
||||||
|
embedContentOnly: true,
|
||||||
|
stepText: '',
|
||||||
|
title: 'Votre Motivation',
|
||||||
|
cardColor: CardColorHorizontal.green, // Changé de pink à green
|
||||||
|
textFieldHint: '',
|
||||||
|
initialText: data.motivationText,
|
||||||
|
initialCguAccepted: true, // Toujours true ici car déjà passé
|
||||||
|
previousRoute: '',
|
||||||
|
onSubmit: (t, c) {},
|
||||||
|
onEdit: () => context.go('/parent-register-step4'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _showConfirmationModal(BuildContext context) {
|
void _showConfirmationModal(BuildContext context) {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
@ -141,8 +264,7 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
|
child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(dialogContext).pop(); // Ferme la modale
|
Navigator.of(dialogContext).pop();
|
||||||
// Utiliser go_router pour la navigation
|
|
||||||
context.go('/login');
|
context.go('/login');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -151,294 +273,5 @@ class _ParentRegisterStep5ScreenState extends State<ParentRegisterStep5Screen> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Méthode pour construire la carte Parent 1
|
|
||||||
Widget _buildParent1Card(BuildContext context, ParentData data) {
|
|
||||||
const double verticalSpacing = 28.0; // Espacement vertical augmenté
|
|
||||||
const double labelFontSize = 22.0; // Taille de label augmentée
|
|
||||||
|
|
||||||
List<Widget> details = [
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)),
|
|
||||||
const SizedBox(width: 20),
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: verticalSpacing),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)),
|
|
||||||
const SizedBox(width: 20),
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: verticalSpacing),
|
|
||||||
_buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize),
|
|
||||||
];
|
|
||||||
return _SummaryCard(
|
|
||||||
backgroundImagePath: CardColorHorizontal.peach.path,
|
|
||||||
title: 'Parent Principal',
|
|
||||||
content: details,
|
|
||||||
onEdit: () => context.go('/parent-register-step1'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode pour construire la carte Parent 2
|
|
||||||
Widget _buildParent2Card(BuildContext context, ParentData data) {
|
|
||||||
const double verticalSpacing = 28.0;
|
|
||||||
const double labelFontSize = 22.0;
|
|
||||||
List<Widget> details = [
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)),
|
|
||||||
const SizedBox(width: 20),
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: verticalSpacing),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)),
|
|
||||||
const SizedBox(width: 20),
|
|
||||||
Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: verticalSpacing),
|
|
||||||
_buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize),
|
|
||||||
];
|
|
||||||
return _SummaryCard(
|
|
||||||
backgroundImagePath: CardColorHorizontal.blue.path,
|
|
||||||
title: 'Deuxième Parent',
|
|
||||||
content: details,
|
|
||||||
onEdit: () => context.go('/parent-register-step2'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode pour construire les cartes Enfants
|
|
||||||
List<Widget> _buildChildrenCards(BuildContext context, List<ChildData> children) {
|
|
||||||
return children.asMap().entries.map((entry) {
|
|
||||||
int index = entry.key;
|
|
||||||
ChildData child = entry.value;
|
|
||||||
|
|
||||||
CardColorHorizontal cardColorHorizontal = CardColorHorizontal.values.firstWhere(
|
|
||||||
(e) => e.name == child.cardColor.name,
|
|
||||||
orElse: () => CardColorHorizontal.lavender,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 20.0),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: 2.0,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: AssetImage(cardColorHorizontal.path),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(15),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// Titre centré dans la carte
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'Enfant ${index + 1}' + (child.isUnbornChild ? ' (à naître)' : ''),
|
|
||||||
style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
|
|
||||||
onPressed: () {
|
|
||||||
context.go('/parent-register-step3', extra: {'childIndex': index});
|
|
||||||
},
|
|
||||||
tooltip: 'Modifier',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 18),
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
// IMAGE SANS CADRE BLANC, PREND LA HAUTEUR
|
|
||||||
Expanded(
|
|
||||||
flex: 1,
|
|
||||||
child: Center(
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(18),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1,
|
|
||||||
child: (child.imageFile != null)
|
|
||||||
? (kIsWeb
|
|
||||||
? Image.network(child.imageFile!.path, fit: BoxFit.cover)
|
|
||||||
: Image.file(child.imageFile!, fit: BoxFit.cover))
|
|
||||||
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 32),
|
|
||||||
// INFOS À DROITE (2/3)
|
|
||||||
Expanded(
|
|
||||||
flex: 2,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
_buildDisplayFieldValue(context, 'Prénom :', child.firstName, labelFontSize: 22.0),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildDisplayFieldValue(context, 'Nom :', child.lastName, labelFontSize: 22.0),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildDisplayFieldValue(context, child.isUnbornChild ? 'Date de naissance :' : 'Date de naissance :', child.dob, labelFontSize: 22.0),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 18),
|
|
||||||
// Ligne des consentements
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Checkbox(
|
|
||||||
value: child.photoConsent,
|
|
||||||
onChanged: null,
|
|
||||||
),
|
|
||||||
Text('Consentement photo', style: GoogleFonts.merienda(fontSize: 16)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(width: 32),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Checkbox(
|
|
||||||
value: child.multipleBirth,
|
|
||||||
onChanged: null,
|
|
||||||
),
|
|
||||||
Text('Naissance multiple', style: GoogleFonts.merienda(fontSize: 16)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Méthode pour construire la carte Motivation
|
|
||||||
Widget _buildMotivationCard(BuildContext context, String motivation) {
|
|
||||||
List<Widget> details = [
|
|
||||||
Text(motivation.isNotEmpty ? motivation : 'Aucune motivation renseignée.',
|
|
||||||
style: GoogleFonts.merienda(fontSize: 18),
|
|
||||||
maxLines: 4,
|
|
||||||
overflow: TextOverflow.ellipsis)
|
|
||||||
];
|
|
||||||
return _SummaryCard(
|
|
||||||
backgroundImagePath: CardColorHorizontal.pink.path,
|
|
||||||
title: 'Votre Motivation',
|
|
||||||
content: details,
|
|
||||||
onEdit: () => context.go('/parent-register-step4'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper pour afficher une ligne de détail (police et agencement amélioré)
|
|
||||||
Widget _buildDetailRow(String label, String value) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"$label: ",
|
|
||||||
style: GoogleFonts.merienda(fontSize: 18, fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
value.isNotEmpty ? value : '-',
|
|
||||||
style: GoogleFonts.merienda(fontSize: 18),
|
|
||||||
softWrap: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widget générique _SummaryCard (ajusté)
|
|
||||||
class _SummaryCard extends StatelessWidget {
|
|
||||||
final String backgroundImagePath;
|
|
||||||
final String title;
|
|
||||||
final List<Widget> content;
|
|
||||||
final VoidCallback onEdit;
|
|
||||||
|
|
||||||
const _SummaryCard({
|
|
||||||
super.key,
|
|
||||||
required this.backgroundImagePath,
|
|
||||||
required this.title,
|
|
||||||
required this.content,
|
|
||||||
required this.onEdit,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AspectRatio(
|
|
||||||
aspectRatio: 2.0, // Le ratio largeur/hauteur de nos images de fond
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: AssetImage(backgroundImagePath),
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(15),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min, // Pour que la colonne prenne la hauteur du contenu
|
|
||||||
children: [
|
|
||||||
Align( // Centrer le titre
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), // Police légèrement augmentée
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12), // Espacement ajusté après le titre
|
|
||||||
...content,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.edit, color: Colors.black54, size: 28), // Icône un peu plus grande
|
|
||||||
onPressed: onEdit,
|
|
||||||
tooltip: 'Modifier',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
220
frontend/lib/widgets/README_FORM_WIDGETS.md
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
# Infrastructure générique pour les formulaires
|
||||||
|
|
||||||
|
## 📋 Vue d'ensemble
|
||||||
|
|
||||||
|
Cette infrastructure permet de créer des formulaires qui s'adaptent automatiquement :
|
||||||
|
- **Mode éditable** (inscription) vs **lecture seule** (récapitulatif)
|
||||||
|
- **Layout mobile** (vertical, < 600px) vs **desktop** (horizontal, ≥ 600px)
|
||||||
|
- **Mobile reste toujours vertical**, même en rotation paysage
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
### 1. `display_config.dart` - Configuration centrale
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Mode d'affichage
|
||||||
|
enum DisplayMode {
|
||||||
|
editable, // Formulaire éditable
|
||||||
|
readonly, // Récapitulatif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type de layout
|
||||||
|
enum LayoutType {
|
||||||
|
mobile, // < 600px, toujours vertical
|
||||||
|
desktop, // ≥ 600px, horizontal
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration complète
|
||||||
|
DisplayConfig config = DisplayConfig.fromContext(
|
||||||
|
context,
|
||||||
|
mode: DisplayMode.editable,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. `form_field_wrapper.dart` - Champs génériques
|
||||||
|
|
||||||
|
#### FormFieldWrapper
|
||||||
|
Widget pour afficher un champ unique qui s'adapte automatiquement.
|
||||||
|
|
||||||
|
**Mode éditable :**
|
||||||
|
```dart
|
||||||
|
FormFieldWrapper(
|
||||||
|
config: config,
|
||||||
|
label: 'Prénom',
|
||||||
|
value: '',
|
||||||
|
controller: firstNameController,
|
||||||
|
onChanged: (value) => {},
|
||||||
|
hint: 'Entrez votre prénom',
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mode readonly :**
|
||||||
|
```dart
|
||||||
|
FormFieldWrapper(
|
||||||
|
config: config,
|
||||||
|
label: 'Prénom',
|
||||||
|
value: 'Jean',
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### FormFieldRow
|
||||||
|
Widget pour afficher plusieurs champs sur une ligne (desktop) ou en colonne (mobile).
|
||||||
|
|
||||||
|
```dart
|
||||||
|
FormFieldRow(
|
||||||
|
config: config,
|
||||||
|
fields: [
|
||||||
|
FormFieldWrapper(...),
|
||||||
|
FormFieldWrapper(...),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. `base_form_screen.dart` - Structure de page générique
|
||||||
|
|
||||||
|
Encapsule toute la structure d'une page de formulaire :
|
||||||
|
- En-tête (étape + titre)
|
||||||
|
- Carte avec fond adapté (horizontal/vertical)
|
||||||
|
- Boutons de navigation
|
||||||
|
- Gestion automatique du layout
|
||||||
|
|
||||||
|
```dart
|
||||||
|
BaseFormScreen(
|
||||||
|
config: DisplayConfig.fromContext(
|
||||||
|
context,
|
||||||
|
mode: DisplayMode.editable,
|
||||||
|
),
|
||||||
|
stepText: 'Étape 1/4',
|
||||||
|
title: 'Informations personnelles',
|
||||||
|
cardColor: CardColorHorizontal.blue,
|
||||||
|
previousRoute: '/previous',
|
||||||
|
onSubmit: () => _handleSubmit(),
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
FormFieldRow(
|
||||||
|
config: config,
|
||||||
|
fields: [
|
||||||
|
FormFieldWrapper(...),
|
||||||
|
FormFieldWrapper(...),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 Comportement responsive
|
||||||
|
|
||||||
|
### Breakpoint : 600px
|
||||||
|
|
||||||
|
| Largeur écran | LayoutType | Orientation carte | Disposition champs |
|
||||||
|
|--------------|------------|-------------------|-------------------|
|
||||||
|
| < 600px | mobile | Verticale | Colonne |
|
||||||
|
| ≥ 600px | desktop | Horizontale | Ligne |
|
||||||
|
|
||||||
|
### Règle importante
|
||||||
|
**Sur mobile, le layout reste TOUJOURS vertical**, même si l'utilisateur tourne son téléphone en mode paysage.
|
||||||
|
|
||||||
|
## 🎨 Utilisation dans un widget de formulaire
|
||||||
|
|
||||||
|
### Exemple : PersonalInfoFormScreen
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class PersonalInfoFormScreen extends StatefulWidget {
|
||||||
|
final DisplayMode mode;
|
||||||
|
final PersonalInfoData? initialData;
|
||||||
|
final Function(PersonalInfoData) onSubmit;
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PersonalInfoFormScreenState extends State<PersonalInfoFormScreen> {
|
||||||
|
late TextEditingController _firstNameController;
|
||||||
|
late TextEditingController _lastNameController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final config = DisplayConfig.fromContext(
|
||||||
|
context,
|
||||||
|
mode: widget.mode,
|
||||||
|
);
|
||||||
|
|
||||||
|
return BaseFormScreen(
|
||||||
|
config: config,
|
||||||
|
stepText: 'Étape 1/4',
|
||||||
|
title: 'Informations personnelles',
|
||||||
|
cardColor: CardColorHorizontal.blue,
|
||||||
|
previousRoute: '/previous',
|
||||||
|
onSubmit: _handleSubmit,
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
FormFieldRow(
|
||||||
|
config: config,
|
||||||
|
fields: [
|
||||||
|
FormFieldWrapper(
|
||||||
|
config: config,
|
||||||
|
label: 'Prénom',
|
||||||
|
value: _firstNameController.text,
|
||||||
|
controller: config.isEditable ? _firstNameController : null,
|
||||||
|
onChanged: config.isEditable ? (v) => setState(() {}) : null,
|
||||||
|
),
|
||||||
|
FormFieldWrapper(
|
||||||
|
config: config,
|
||||||
|
label: 'Nom',
|
||||||
|
value: _lastNameController.text,
|
||||||
|
controller: config.isEditable ? _lastNameController : null,
|
||||||
|
onChanged: config.isEditable ? (v) => setState(() {}) : null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSubmit() {
|
||||||
|
final data = PersonalInfoData(
|
||||||
|
firstName: _firstNameController.text,
|
||||||
|
lastName: _lastNameController.text,
|
||||||
|
);
|
||||||
|
widget.onSubmit(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Avantages
|
||||||
|
|
||||||
|
1. **Code unique** : Un seul widget pour éditable + readonly + mobile + desktop
|
||||||
|
2. **Cohérence** : Tous les formulaires se comportent de la même façon
|
||||||
|
3. **Maintenance** : Modification centralisée de l'UI
|
||||||
|
4. **Performance** : Pas de rebuild inutile, layout déterminé au build
|
||||||
|
5. **Simplicité** : API claire et prévisible
|
||||||
|
|
||||||
|
## 🔧 Utilitaires disponibles
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Détecter le type de layout
|
||||||
|
bool isMobile = LayoutHelper.isMobile(context);
|
||||||
|
bool isDesktop = LayoutHelper.isDesktop(context);
|
||||||
|
|
||||||
|
// Espacement adaptatif
|
||||||
|
double spacing = LayoutHelper.getSpacing(
|
||||||
|
context,
|
||||||
|
mobileSpacing: 12.0,
|
||||||
|
desktopSpacing: 20.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Largeur max adaptative
|
||||||
|
double maxWidth = LayoutHelper.getMaxWidth(context);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Migration des widgets existants
|
||||||
|
|
||||||
|
Pour migrer un widget existant vers cette infrastructure :
|
||||||
|
|
||||||
|
1. Ajouter paramètre `DisplayMode mode`
|
||||||
|
2. Créer `DisplayConfig.fromContext(context, mode: widget.mode)`
|
||||||
|
3. Remplacer la structure Scaffold par `BaseFormScreen`
|
||||||
|
4. Remplacer les champs par `FormFieldWrapper`
|
||||||
|
5. Grouper les champs avec `FormFieldRow`
|
||||||
|
6. Tester en mode editable + readonly + mobile + desktop
|
||||||
@ -7,6 +7,7 @@ class AppCustomCheckbox extends StatelessWidget {
|
|||||||
final ValueChanged<bool> onChanged;
|
final ValueChanged<bool> onChanged;
|
||||||
final double checkboxSize;
|
final double checkboxSize;
|
||||||
final double checkmarkSizeFactor;
|
final double checkmarkSizeFactor;
|
||||||
|
final double fontSize;
|
||||||
|
|
||||||
const AppCustomCheckbox({
|
const AppCustomCheckbox({
|
||||||
super.key,
|
super.key,
|
||||||
@ -15,6 +16,7 @@ class AppCustomCheckbox extends StatelessWidget {
|
|||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
this.checkboxSize = 20.0,
|
this.checkboxSize = 20.0,
|
||||||
this.checkmarkSizeFactor = 1.4,
|
this.checkmarkSizeFactor = 1.4,
|
||||||
|
this.fontSize = 16.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -51,7 +53,7 @@ class AppCustomCheckbox extends StatelessWidget {
|
|||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
style: GoogleFonts.merienda(fontSize: 16),
|
style: GoogleFonts.merienda(fontSize: fontSize),
|
||||||
overflow: TextOverflow.ellipsis, // Gérer le texte long
|
overflow: TextOverflow.ellipsis, // Gérer le texte long
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -265,7 +265,7 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
|
|||||||
child: _isLoading
|
child: _isLoading
|
||||||
? const CircularProgressIndicator()
|
? const CircularProgressIndicator()
|
||||||
: ImageButton(
|
: ImageButton(
|
||||||
bg: 'assets/images/btn_green.png',
|
bg: 'assets/images/bg_green.png',
|
||||||
width: 250,
|
width: 250,
|
||||||
height: 40,
|
height: 40,
|
||||||
text: 'Changer le mot de passe',
|
text: 'Changer le mot de passe',
|
||||||
|
|||||||
225
frontend/lib/widgets/base_form_screen.dart
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import '../config/display_config.dart';
|
||||||
|
import '../models/card_assets.dart';
|
||||||
|
import 'hover_relief_widget.dart';
|
||||||
|
import 'image_button.dart';
|
||||||
|
|
||||||
|
/// Widget de base générique pour tous les écrans de formulaire
|
||||||
|
/// Gère automatiquement le layout, les boutons de navigation, etc.
|
||||||
|
class BaseFormScreen extends StatelessWidget {
|
||||||
|
/// Configuration d'affichage
|
||||||
|
final DisplayConfig config;
|
||||||
|
|
||||||
|
/// Texte de l'étape (ex: "Étape 1/4")
|
||||||
|
final String stepText;
|
||||||
|
|
||||||
|
/// Titre du formulaire
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// Couleur de la carte (horizontal pour desktop)
|
||||||
|
final CardColorHorizontal cardColor;
|
||||||
|
|
||||||
|
/// Contenu du formulaire
|
||||||
|
final Widget content;
|
||||||
|
|
||||||
|
/// Texte du bouton de soumission (par défaut "Suivant")
|
||||||
|
final String? submitButtonText;
|
||||||
|
|
||||||
|
/// Callback de soumission
|
||||||
|
final VoidCallback onSubmit;
|
||||||
|
|
||||||
|
/// Route précédente (pour le bouton retour)
|
||||||
|
final String previousRoute;
|
||||||
|
|
||||||
|
/// Widget supplémentaire au-dessus du contenu (ex: toggle)
|
||||||
|
final Widget? headerWidget;
|
||||||
|
|
||||||
|
/// Widget supplémentaire en dessous du contenu (ex: checkbox CGU)
|
||||||
|
final Widget? footerWidget;
|
||||||
|
|
||||||
|
/// Padding personnalisé pour le contenu
|
||||||
|
final EdgeInsets? contentPadding;
|
||||||
|
|
||||||
|
const BaseFormScreen({
|
||||||
|
super.key,
|
||||||
|
required this.config,
|
||||||
|
required this.stepText,
|
||||||
|
required this.title,
|
||||||
|
required this.cardColor,
|
||||||
|
required this.content,
|
||||||
|
required this.onSubmit,
|
||||||
|
required this.previousRoute,
|
||||||
|
this.submitButtonText,
|
||||||
|
this.headerWidget,
|
||||||
|
this.footerWidget,
|
||||||
|
this.contentPadding,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFFFF8E1),
|
||||||
|
body: SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.all(
|
||||||
|
LayoutHelper.getSpacing(context,
|
||||||
|
mobileSpacing: 16.0,
|
||||||
|
desktopSpacing: 32.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: LayoutHelper.getMaxWidth(context),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Texte de l'étape
|
||||||
|
Text(
|
||||||
|
stepText,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: config.isMobile ? 14 : 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: const Color(0xFF6D4C41),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Titre
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: config.isMobile ? 24 : 32,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: const Color(0xFF4A4A4A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Header widget (si fourni)
|
||||||
|
if (headerWidget != null) ...[
|
||||||
|
headerWidget!,
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Carte principale
|
||||||
|
_buildCard(context),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Footer widget (si fourni)
|
||||||
|
if (footerWidget != null) ...[
|
||||||
|
footerWidget!,
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Boutons de navigation
|
||||||
|
_buildNavigationButtons(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit la carte principale
|
||||||
|
Widget _buildCard(BuildContext context) {
|
||||||
|
final effectivePadding = contentPadding ??
|
||||||
|
EdgeInsets.all(
|
||||||
|
LayoutHelper.getSpacing(context,
|
||||||
|
mobileSpacing: 16.0,
|
||||||
|
desktopSpacing: 32.0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (config.isMobile) {
|
||||||
|
// Carte verticale sur mobile
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(_getVerticalCardAsset()),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: effectivePadding,
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Carte horizontale sur desktop
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(cardColor.path),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: effectivePadding,
|
||||||
|
child: content,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retourne l'asset de carte vertical correspondant à la couleur
|
||||||
|
String _getVerticalCardAsset() {
|
||||||
|
// Mapping couleur horizontale -> verticale
|
||||||
|
switch (cardColor) {
|
||||||
|
case CardColorHorizontal.blue:
|
||||||
|
return CardColorVertical.blue.path;
|
||||||
|
case CardColorHorizontal.green:
|
||||||
|
return CardColorVertical.green.path;
|
||||||
|
case CardColorHorizontal.lavender:
|
||||||
|
return CardColorVertical.lavender.path;
|
||||||
|
case CardColorHorizontal.lime:
|
||||||
|
return CardColorVertical.lime.path;
|
||||||
|
case CardColorHorizontal.peach:
|
||||||
|
return CardColorVertical.peach.path;
|
||||||
|
case CardColorHorizontal.pink:
|
||||||
|
return CardColorVertical.pink.path;
|
||||||
|
case CardColorHorizontal.red:
|
||||||
|
return CardColorVertical.red.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit les boutons de navigation
|
||||||
|
Widget _buildNavigationButtons(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
// Bouton Précédent
|
||||||
|
HoverReliefWidget(
|
||||||
|
child: ImageButton(
|
||||||
|
bg: 'assets/images/bg_green.png',
|
||||||
|
text: 'Précédent',
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: () => Navigator.pushNamed(context, previousRoute),
|
||||||
|
width: config.isMobile ? 120 : 150,
|
||||||
|
height: config.isMobile ? 40 : 50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Bouton Suivant/Soumettre
|
||||||
|
HoverReliefWidget(
|
||||||
|
child: ImageButton(
|
||||||
|
bg: 'assets/images/bg_green.png',
|
||||||
|
text: submitButtonText ?? 'Suivant',
|
||||||
|
textColor: Colors.white,
|
||||||
|
onPressed: config.isReadonly ? onSubmit : () {
|
||||||
|
// En mode éditable, valider avant de soumettre
|
||||||
|
onSubmit();
|
||||||
|
},
|
||||||
|
width: config.isMobile ? 120 : 150,
|
||||||
|
height: config.isMobile ? 40 : 50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,8 +5,10 @@ import 'package:flutter/foundation.dart' show kIsWeb;
|
|||||||
import '../models/user_registration_data.dart';
|
import '../models/user_registration_data.dart';
|
||||||
import '../models/card_assets.dart';
|
import '../models/card_assets.dart';
|
||||||
import 'custom_app_text_field.dart';
|
import 'custom_app_text_field.dart';
|
||||||
|
import 'form_field_wrapper.dart';
|
||||||
import 'app_custom_checkbox.dart';
|
import 'app_custom_checkbox.dart';
|
||||||
import 'hover_relief_widget.dart';
|
import 'hover_relief_widget.dart';
|
||||||
|
import '../config/display_config.dart';
|
||||||
|
|
||||||
/// Widget pour afficher et éditer une carte enfant
|
/// Widget pour afficher et éditer une carte enfant
|
||||||
/// Utilisé dans le workflow d'inscription des parents
|
/// Utilisé dans le workflow d'inscription des parents
|
||||||
@ -22,6 +24,8 @@ class ChildCardWidget extends StatefulWidget {
|
|||||||
final ValueChanged<bool> onToggleIsUnborn;
|
final ValueChanged<bool> onToggleIsUnborn;
|
||||||
final VoidCallback onRemove;
|
final VoidCallback onRemove;
|
||||||
final bool canBeRemoved;
|
final bool canBeRemoved;
|
||||||
|
final DisplayMode mode;
|
||||||
|
final VoidCallback? onEdit;
|
||||||
|
|
||||||
const ChildCardWidget({
|
const ChildCardWidget({
|
||||||
required Key key,
|
required Key key,
|
||||||
@ -36,6 +40,8 @@ class ChildCardWidget extends StatefulWidget {
|
|||||||
required this.onToggleIsUnborn,
|
required this.onToggleIsUnborn,
|
||||||
required this.onRemove,
|
required this.onRemove,
|
||||||
required this.canBeRemoved,
|
required this.canBeRemoved,
|
||||||
|
this.mode = DisplayMode.editable,
|
||||||
|
this.onEdit,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -87,101 +93,137 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final config = DisplayConfig.fromContext(context, mode: widget.mode);
|
||||||
|
final screenSize = MediaQuery.of(context).size;
|
||||||
|
final scaleFactor = config.isMobile ? 0.9 : 1.1; // Réduire légèrement sur mobile
|
||||||
|
|
||||||
|
// Si mode Readonly Desktop : Layout spécial "Vintage" horizontal
|
||||||
|
if (config.isReadonly && !config.isMobile) {
|
||||||
|
return _buildReadonlyDesktopCard(context, config, screenSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si mode Readonly Mobile : Layout spécial "Vintage" vertical (1:2)
|
||||||
|
if (config.isReadonly && config.isMobile) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
|
||||||
|
child: _buildReadonlyMobileCard(context, config),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final File? currentChildImage = widget.childData.imageFile;
|
final File? currentChildImage = widget.childData.imageFile;
|
||||||
// Utiliser la couleur de la carte de childData pour l'ombre si besoin, ou directement pour le fond
|
// ... (reste du code existant pour mobile/editable)
|
||||||
final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender
|
final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender
|
||||||
? Colors.purple.shade200
|
? Colors.purple.shade200
|
||||||
: (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200); // Placeholder pour autres couleurs
|
: (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200);
|
||||||
final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90);
|
final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90);
|
||||||
final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130);
|
final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 345.0 * 1.1, // 379.5
|
width: config.isMobile ? double.infinity : screenSize.width * 0.6,
|
||||||
height: 570.0 * 1.2, // 684.0
|
// On retire la hauteur fixe pour laisser le contenu définir la taille, comme les autres cartes
|
||||||
padding: const EdgeInsets.all(22.0 * 1.1), // 24.2
|
// height: config.isMobile ? null : 600.0 * scaleFactor,
|
||||||
|
padding: EdgeInsets.all(22.0 * scaleFactor),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.cover),
|
image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.fill),
|
||||||
borderRadius: BorderRadius.circular(20 * 1.1), // 22
|
borderRadius: BorderRadius.circular(20 * scaleFactor),
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
// ... (contenu existant)
|
||||||
HoverReliefWidget(
|
HoverReliefWidget(
|
||||||
onPressed: widget.onPickImage,
|
onPressed: config.isReadonly ? null : widget.onPickImage,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
initialShadowColor: initialPhotoShadow,
|
initialShadowColor: initialPhotoShadow,
|
||||||
hoverShadowColor: hoverPhotoShadow,
|
hoverShadowColor: hoverPhotoShadow,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 200.0,
|
height: 200.0 * (config.isMobile ? 0.8 : 1.0),
|
||||||
width: 200.0,
|
width: 200.0 * (config.isMobile ? 0.8 : 1.0),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(5.0 * 1.1), // 5.5
|
padding: EdgeInsets.all(5.0 * scaleFactor),
|
||||||
child: currentChildImage != null
|
child: currentChildImage != null
|
||||||
? ClipRRect(borderRadius: BorderRadius.circular(10 * 1.1), child: kIsWeb ? Image.network(currentChildImage.path, fit: BoxFit.cover) : Image.file(currentChildImage, fit: BoxFit.cover))
|
? ClipRRect(borderRadius: BorderRadius.circular(10 * scaleFactor), child: kIsWeb ? Image.network(currentChildImage.path, fit: BoxFit.cover) : Image.file(currentChildImage, fit: BoxFit.cover))
|
||||||
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
|
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12.0 * 1.1), // Augmenté pour plus d'espace après la photo
|
SizedBox(height: 10.0 * scaleFactor),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 16 * 1.1, fontWeight: FontWeight.w600)),
|
Text(
|
||||||
Switch(value: widget.childData.isUnbornChild, onChanged: widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor),
|
'Enfant à naître ?',
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: config.isMobile ? 14 : 16 * scaleFactor,
|
||||||
|
fontWeight: FontWeight.w600
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Transform.scale(
|
||||||
|
scale: config.isMobile ? 0.8 : 1.0,
|
||||||
|
child: Switch(
|
||||||
|
value: widget.childData.isUnbornChild,
|
||||||
|
onChanged: config.isReadonly ? null : widget.onToggleIsUnborn,
|
||||||
|
activeColor: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 9.0 * 1.1), // 9.9
|
SizedBox(height: 8.0 * scaleFactor),
|
||||||
CustomAppTextField(
|
_buildField(
|
||||||
|
config: config,
|
||||||
|
scaleFactor: scaleFactor,
|
||||||
|
label: 'Prénom',
|
||||||
controller: _firstNameController,
|
controller: _firstNameController,
|
||||||
labelText: 'Prénom',
|
hint: 'Facultatif si à naître',
|
||||||
hintText: 'Facultatif si à naître',
|
|
||||||
isRequired: !widget.childData.isUnbornChild,
|
isRequired: !widget.childData.isUnbornChild,
|
||||||
fieldHeight: 55.0 * 1.1, // 60.5
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6.0 * 1.1), // 6.6
|
SizedBox(height: 5.0 * scaleFactor),
|
||||||
CustomAppTextField(
|
_buildField(
|
||||||
|
config: config,
|
||||||
|
scaleFactor: scaleFactor,
|
||||||
|
label: 'Nom',
|
||||||
controller: _lastNameController,
|
controller: _lastNameController,
|
||||||
labelText: 'Nom',
|
hint: 'Nom de l\'enfant',
|
||||||
hintText: 'Nom de l\'enfant',
|
|
||||||
enabled: true,
|
|
||||||
fieldHeight: 55.0 * 1.1, // 60.5
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 9.0 * 1.1), // 9.9
|
SizedBox(height: 8.0 * scaleFactor),
|
||||||
CustomAppTextField(
|
_buildField(
|
||||||
|
config: config,
|
||||||
|
scaleFactor: scaleFactor,
|
||||||
|
label: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
|
||||||
controller: _dobController,
|
controller: _dobController,
|
||||||
labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
|
hint: 'JJ/MM/AAAA',
|
||||||
hintText: 'JJ/MM/AAAA',
|
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
onTap: widget.onDateSelect,
|
onTap: config.isReadonly ? null : widget.onDateSelect,
|
||||||
suffixIcon: Icons.calendar_today,
|
suffixIcon: Icons.calendar_today,
|
||||||
fieldHeight: 55.0 * 1.1, // 60.5
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 11.0 * 1.1), // 12.1
|
SizedBox(height: 10.0 * scaleFactor),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
AppCustomCheckbox(
|
AppCustomCheckbox(
|
||||||
label: 'Consentement photo',
|
label: 'Consentement photo',
|
||||||
value: widget.childData.photoConsent,
|
value: widget.childData.photoConsent,
|
||||||
onChanged: widget.onTogglePhotoConsent,
|
onChanged: config.isReadonly ? (v) {} : widget.onTogglePhotoConsent,
|
||||||
checkboxSize: 22.0 * 1.1, // 24.2
|
checkboxSize: config.isMobile ? 20.0 : 22.0 * scaleFactor,
|
||||||
|
fontSize: config.isMobile ? 13.0 : 16.0,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6.0 * 1.1), // 6.6
|
SizedBox(height: 5.0 * scaleFactor),
|
||||||
AppCustomCheckbox(
|
AppCustomCheckbox(
|
||||||
label: 'Naissance multiple',
|
label: 'Naissance multiple',
|
||||||
value: widget.childData.multipleBirth,
|
value: widget.childData.multipleBirth,
|
||||||
onChanged: widget.onToggleMultipleBirth,
|
onChanged: config.isReadonly ? (v) {} : widget.onToggleMultipleBirth,
|
||||||
checkboxSize: 22.0 * 1.1, // 24.2
|
checkboxSize: config.isMobile ? 20.0 : 22.0 * scaleFactor,
|
||||||
|
fontSize: config.isMobile ? 13.0 : 16.0,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (widget.canBeRemoved)
|
if (widget.canBeRemoved && !config.isReadonly)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: -5, right: -5,
|
top: -5, right: -5,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
@ -189,14 +231,361 @@ class _ChildCardWidgetState extends State<ChildCardWidget> {
|
|||||||
customBorder: const CircleBorder(),
|
customBorder: const CircleBorder(),
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/images/red_cross2.png',
|
'assets/images/red_cross2.png',
|
||||||
width: 36,
|
width: config.isMobile ? 30 : 36,
|
||||||
height: 36,
|
height: config.isMobile ? 30 : 36,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
if (config.isReadonly && widget.onEdit != null)
|
||||||
|
Positioned(
|
||||||
|
top: -5, right: -5,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.edit, color: Colors.black54),
|
||||||
|
onPressed: widget.onEdit,
|
||||||
|
tooltip: 'Modifier',
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Layout SPÉCIAL Readonly Desktop (Ancien Design Horizontal)
|
||||||
|
Widget _buildReadonlyDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||||
|
// Convertir la couleur verticale (pour mobile) en couleur horizontale (pour desktop/récap)
|
||||||
|
// On mappe les couleurs verticales vers horizontales
|
||||||
|
String horizontalCardAsset = CardColorHorizontal.lavender.path; // Par défaut
|
||||||
|
|
||||||
|
// Mapping manuel simple
|
||||||
|
if (widget.childData.cardColor.path.contains('lavender')) horizontalCardAsset = CardColorHorizontal.lavender.path;
|
||||||
|
else if (widget.childData.cardColor.path.contains('blue')) horizontalCardAsset = CardColorHorizontal.blue.path;
|
||||||
|
else if (widget.childData.cardColor.path.contains('green')) horizontalCardAsset = CardColorHorizontal.green.path;
|
||||||
|
else if (widget.childData.cardColor.path.contains('lime')) horizontalCardAsset = CardColorHorizontal.lime.path;
|
||||||
|
else if (widget.childData.cardColor.path.contains('peach')) horizontalCardAsset = CardColorHorizontal.peach.path;
|
||||||
|
else if (widget.childData.cardColor.path.contains('pink')) horizontalCardAsset = CardColorHorizontal.pink.path;
|
||||||
|
else if (widget.childData.cardColor.path.contains('red')) horizontalCardAsset = CardColorHorizontal.red.path;
|
||||||
|
|
||||||
|
final File? currentChildImage = widget.childData.imageFile;
|
||||||
|
final cardWidth = screenSize.width / 2.0;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: cardWidth,
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 2.0,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(horizontalCardAsset),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Titre + Edit Button
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Enfant ${widget.childIndex + 1}' + (widget.childData.isUnbornChild ? ' (à naître)' : ''),
|
||||||
|
style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.onEdit != null)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
|
||||||
|
onPressed: widget.onEdit,
|
||||||
|
tooltip: 'Modifier',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
|
||||||
|
// Contenu principal : Photo + Champs
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// PHOTO (1/3)
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Center(
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 5),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
child: currentChildImage != null
|
||||||
|
? (kIsWeb
|
||||||
|
? Image.network(currentChildImage.path, fit: BoxFit.cover)
|
||||||
|
: Image.file(currentChildImage, fit: BoxFit.cover))
|
||||||
|
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 32),
|
||||||
|
|
||||||
|
// CHAMPS (2/3)
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildReadonlyField('Prénom :', _firstNameController.text),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildReadonlyField('Nom :', _lastNameController.text),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildReadonlyField(
|
||||||
|
widget.childData.isUnbornChild ? 'Date prévisionnelle :' : 'Date de naissance :',
|
||||||
|
_dobController.text
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
|
||||||
|
// Consentements
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
AppCustomCheckbox(
|
||||||
|
label: 'Consentement photo',
|
||||||
|
value: widget.childData.photoConsent,
|
||||||
|
onChanged: (v) {}, // Readonly
|
||||||
|
checkboxSize: 22.0,
|
||||||
|
fontSize: 16.0,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 32),
|
||||||
|
AppCustomCheckbox(
|
||||||
|
label: 'Naissance multiple',
|
||||||
|
value: widget.childData.multipleBirth,
|
||||||
|
onChanged: (v) {}, // Readonly
|
||||||
|
checkboxSize: 22.0,
|
||||||
|
fontSize: 16.0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Carte en mode readonly MOBILE avec hauteur adaptative
|
||||||
|
Widget _buildReadonlyMobileCard(BuildContext context, DisplayConfig config) {
|
||||||
|
final File? currentChildImage = widget.childData.imageFile;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
// Pas de height fixe
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 24.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(widget.childData.cardColor.path), // Image verticale
|
||||||
|
fit: BoxFit.fill, // Fill pour s'adapter
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min, // S'adapte au contenu
|
||||||
|
children: [
|
||||||
|
// Titre + Edit Button
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Enfant ${widget.childIndex + 1}' + (widget.childData.isUnbornChild ? ' (à naître)' : ''),
|
||||||
|
style: GoogleFonts.merienda(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.onEdit != null)
|
||||||
|
const SizedBox(width: 28),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Contenu aligné en haut
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 20.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Photo
|
||||||
|
SizedBox(
|
||||||
|
height: 150,
|
||||||
|
width: 150,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 5),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
child: currentChildImage != null
|
||||||
|
? (kIsWeb
|
||||||
|
? Image.network(currentChildImage.path, fit: BoxFit.cover)
|
||||||
|
: Image.file(currentChildImage, fit: BoxFit.cover))
|
||||||
|
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Champs
|
||||||
|
_buildReadonlyField('Prénom :', _firstNameController.text),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildReadonlyField('Nom :', _lastNameController.text),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildReadonlyField(
|
||||||
|
widget.childData.isUnbornChild ? 'Date prévisionnelle :' : 'Date de naissance :',
|
||||||
|
_dobController.text
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Consentements
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
AppCustomCheckbox(
|
||||||
|
label: 'Consentement photo',
|
||||||
|
value: widget.childData.photoConsent,
|
||||||
|
onChanged: (v) {},
|
||||||
|
checkboxSize: 20.0,
|
||||||
|
fontSize: 14.0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
AppCustomCheckbox(
|
||||||
|
label: 'Naissance multiple',
|
||||||
|
value: widget.childData.multipleBirth,
|
||||||
|
onChanged: (v) {},
|
||||||
|
checkboxSize: 20.0,
|
||||||
|
fontSize: 14.0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
if (widget.onEdit != null)
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
child: IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
icon: const Icon(Icons.edit, color: Colors.black54, size: 24),
|
||||||
|
onPressed: widget.onEdit,
|
||||||
|
tooltip: 'Modifier',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper pour champ Readonly style "Beige"
|
||||||
|
Widget _buildReadonlyField(String label, String value) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: GoogleFonts.merienda(fontSize: 22.0, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50.0,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: const DecorationImage(
|
||||||
|
image: AssetImage('assets/images/bg_beige.png'),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
value.isNotEmpty ? value : '-',
|
||||||
|
style: GoogleFonts.merienda(fontSize: 18.0),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildField({
|
||||||
|
required DisplayConfig config,
|
||||||
|
required double scaleFactor,
|
||||||
|
required String label,
|
||||||
|
required TextEditingController controller,
|
||||||
|
String? hint,
|
||||||
|
bool isRequired = false,
|
||||||
|
bool readOnly = false,
|
||||||
|
VoidCallback? onTap,
|
||||||
|
IconData? suffixIcon,
|
||||||
|
}) {
|
||||||
|
if (config.isReadonly) {
|
||||||
|
return FormFieldWrapper(
|
||||||
|
config: config,
|
||||||
|
label: label,
|
||||||
|
value: controller.text,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return CustomAppTextField(
|
||||||
|
controller: controller,
|
||||||
|
labelText: label,
|
||||||
|
hintText: hint ?? label,
|
||||||
|
isRequired: isRequired,
|
||||||
|
fieldHeight: config.isMobile ? 40.0 : 50.0 * scaleFactor, // Hauteur réduite
|
||||||
|
labelFontSize: config.isMobile ? 12.0 : 18.0, // Police réduite
|
||||||
|
inputFontSize: config.isMobile ? 13.0 : 16.0, // Police réduite
|
||||||
|
readOnly: readOnly,
|
||||||
|
onTap: onTap,
|
||||||
|
suffixIcon: suffixIcon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,12 +54,12 @@ class _CustomAppTextFieldState extends State<CustomAppTextField> {
|
|||||||
String getBackgroundImagePath() {
|
String getBackgroundImagePath() {
|
||||||
switch (widget.style) {
|
switch (widget.style) {
|
||||||
case CustomAppTextFieldStyle.lavande:
|
case CustomAppTextFieldStyle.lavande:
|
||||||
return 'assets/images/input_field_lavande.png';
|
return 'assets/images/bg_lavender.png';
|
||||||
case CustomAppTextFieldStyle.jaune:
|
case CustomAppTextFieldStyle.jaune:
|
||||||
return 'assets/images/input_field_jaune.png';
|
return 'assets/images/bg_yellow.png';
|
||||||
case CustomAppTextFieldStyle.beige:
|
case CustomAppTextFieldStyle.beige:
|
||||||
default:
|
default:
|
||||||
return 'assets/images/input_field_bg.png';
|
return 'assets/images/bg_beige.png';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
87
frontend/lib/widgets/custom_navigation_button.dart
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
|
||||||
|
/// Style de bouton de navigation
|
||||||
|
enum NavigationButtonStyle {
|
||||||
|
green, // Bouton vert avec texte vert foncé
|
||||||
|
purple, // Bouton violet avec texte violet foncé
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget de bouton de navigation personnalisé
|
||||||
|
/// Utilise les assets existants pour le fond
|
||||||
|
class CustomNavigationButton extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final NavigationButtonStyle style;
|
||||||
|
final double? width;
|
||||||
|
final double height;
|
||||||
|
final double fontSize;
|
||||||
|
|
||||||
|
const CustomNavigationButton({
|
||||||
|
super.key,
|
||||||
|
required this.text,
|
||||||
|
required this.onPressed,
|
||||||
|
this.style = NavigationButtonStyle.green,
|
||||||
|
this.width,
|
||||||
|
this.height = 50,
|
||||||
|
this.fontSize = 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final backgroundImage = _getBackgroundImage();
|
||||||
|
final textColor = _getTextColor();
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
// Fond avec image
|
||||||
|
Positioned.fill(
|
||||||
|
child: Image.asset(
|
||||||
|
backgroundImage,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Bouton cliquable
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onPressed,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
color: textColor,
|
||||||
|
fontSize: fontSize,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getBackgroundImage() {
|
||||||
|
switch (style) {
|
||||||
|
case NavigationButtonStyle.green:
|
||||||
|
return 'assets/images/bg_green.png';
|
||||||
|
case NavigationButtonStyle.purple:
|
||||||
|
return 'assets/images/bg_lavender.png';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getTextColor() {
|
||||||
|
switch (style) {
|
||||||
|
case NavigationButtonStyle.green:
|
||||||
|
return const Color(0xFF2E7D32); // Vert foncé
|
||||||
|
case NavigationButtonStyle.purple:
|
||||||
|
return const Color(0xFF5E35B1); // Violet foncé
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
205
frontend/lib/widgets/form_field_wrapper.dart
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import '../config/display_config.dart';
|
||||||
|
import 'custom_app_text_field.dart';
|
||||||
|
|
||||||
|
/// Widget générique pour afficher un champ de formulaire
|
||||||
|
/// S'adapte automatiquement selon le DisplayConfig (editable/readonly, mobile/desktop)
|
||||||
|
class FormFieldWrapper extends StatelessWidget {
|
||||||
|
/// Configuration d'affichage
|
||||||
|
final DisplayConfig config;
|
||||||
|
|
||||||
|
/// Label du champ
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
/// Valeur actuelle
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
/// Controller pour le mode éditable
|
||||||
|
final TextEditingController? controller;
|
||||||
|
|
||||||
|
/// Callback de changement (mode éditable)
|
||||||
|
final ValueChanged<String>? onChanged;
|
||||||
|
|
||||||
|
/// Hint du champ (mode éditable)
|
||||||
|
final String? hint;
|
||||||
|
|
||||||
|
/// Nombre de lignes (pour textarea)
|
||||||
|
final int? maxLines;
|
||||||
|
|
||||||
|
/// Type de clavier
|
||||||
|
final TextInputType? keyboardType;
|
||||||
|
|
||||||
|
/// Widget personnalisé à afficher (override le champ standard)
|
||||||
|
final Widget? customWidget;
|
||||||
|
|
||||||
|
const FormFieldWrapper({
|
||||||
|
super.key,
|
||||||
|
required this.config,
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
this.controller,
|
||||||
|
this.onChanged,
|
||||||
|
this.hint,
|
||||||
|
this.maxLines,
|
||||||
|
this.keyboardType,
|
||||||
|
this.customWidget,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (config.isReadonly) {
|
||||||
|
return _buildReadonlyField(context);
|
||||||
|
} else {
|
||||||
|
return _buildEditableField(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit un champ en mode lecture seule
|
||||||
|
Widget _buildReadonlyField(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: LayoutHelper.getSpacing(context,
|
||||||
|
mobileSpacing: 8.0,
|
||||||
|
desktopSpacing: 12.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Label
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: config.isMobile ? 14 : 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: const Color(0xFF4A4A4A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
|
// Valeur avec fond beige
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: const DecorationImage(
|
||||||
|
image: AssetImage('assets/images/bg_beige.png'),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
value.isEmpty ? '-' : value,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: config.isMobile ? 14 : 16,
|
||||||
|
color: const Color(0xFF2C2C2C),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit un champ en mode éditable
|
||||||
|
Widget _buildEditableField(BuildContext context) {
|
||||||
|
// Si un widget personnalisé est fourni, l'utiliser
|
||||||
|
if (customWidget != null) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: LayoutHelper.getSpacing(context,
|
||||||
|
mobileSpacing: 8.0,
|
||||||
|
desktopSpacing: 12.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: customWidget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sinon, utiliser le champ standard
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: LayoutHelper.getSpacing(context,
|
||||||
|
mobileSpacing: 8.0,
|
||||||
|
desktopSpacing: 12.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Label
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: config.isMobile ? 14 : 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: const Color(0xFF4A4A4A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Champ de saisie
|
||||||
|
CustomAppTextField(
|
||||||
|
controller: controller!,
|
||||||
|
labelText: label,
|
||||||
|
hintText: hint ?? label,
|
||||||
|
keyboardType: keyboardType ?? TextInputType.text,
|
||||||
|
fieldWidth: double.infinity,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget générique pour afficher une ligne de champs
|
||||||
|
/// S'adapte automatiquement: horizontal sur desktop, vertical sur mobile
|
||||||
|
class FormFieldRow extends StatelessWidget {
|
||||||
|
/// Configuration d'affichage
|
||||||
|
final DisplayConfig config;
|
||||||
|
|
||||||
|
/// Liste des champs à afficher
|
||||||
|
final List<Widget> fields;
|
||||||
|
|
||||||
|
/// Espacement entre les champs
|
||||||
|
final double? spacing;
|
||||||
|
|
||||||
|
const FormFieldRow({
|
||||||
|
super.key,
|
||||||
|
required this.config,
|
||||||
|
required this.fields,
|
||||||
|
this.spacing,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final effectiveSpacing = spacing ??
|
||||||
|
LayoutHelper.getSpacing(context,
|
||||||
|
mobileSpacing: 12.0,
|
||||||
|
desktopSpacing: 20.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (config.isMobile) {
|
||||||
|
// Layout vertical sur mobile
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: fields,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Layout horizontal sur desktop
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
for (int i = 0; i < fields.length; i++) ...[
|
||||||
|
Expanded(child: fields[i]),
|
||||||
|
if (i < fields.length - 1) SizedBox(width: effectiveSpacing),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,9 +5,15 @@ import 'dart:math' as math;
|
|||||||
|
|
||||||
import 'custom_decorated_text_field.dart';
|
import 'custom_decorated_text_field.dart';
|
||||||
import 'app_custom_checkbox.dart';
|
import 'app_custom_checkbox.dart';
|
||||||
|
import 'custom_navigation_button.dart';
|
||||||
|
import 'hover_relief_widget.dart';
|
||||||
import '../models/card_assets.dart';
|
import '../models/card_assets.dart';
|
||||||
|
import '../config/display_config.dart';
|
||||||
|
|
||||||
|
/// Widget générique pour le formulaire de présentation avec texte libre + CGU
|
||||||
|
/// Supporte mode éditable et readonly, responsive mobile/desktop
|
||||||
class PresentationFormScreen extends StatefulWidget {
|
class PresentationFormScreen extends StatefulWidget {
|
||||||
|
final DisplayMode mode;
|
||||||
final String stepText; // Ex: "Étape 3/4" ou "Étape 4/5"
|
final String stepText; // Ex: "Étape 3/4" ou "Étape 4/5"
|
||||||
final String title; // Ex: "Présentation et Conditions" ou "Motivation de votre demande"
|
final String title; // Ex: "Présentation et Conditions" ou "Motivation de votre demande"
|
||||||
final CardColorHorizontal cardColor;
|
final CardColorHorizontal cardColor;
|
||||||
@ -17,8 +23,12 @@ class PresentationFormScreen extends StatefulWidget {
|
|||||||
final String previousRoute;
|
final String previousRoute;
|
||||||
final Function(String text, bool cguAccepted) onSubmit;
|
final Function(String text, bool cguAccepted) onSubmit;
|
||||||
|
|
||||||
|
final bool embedContentOnly;
|
||||||
|
final VoidCallback? onEdit;
|
||||||
|
|
||||||
const PresentationFormScreen({
|
const PresentationFormScreen({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.mode = DisplayMode.editable,
|
||||||
required this.stepText,
|
required this.stepText,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.cardColor,
|
required this.cardColor,
|
||||||
@ -27,6 +37,8 @@ class PresentationFormScreen extends StatefulWidget {
|
|||||||
required this.initialCguAccepted,
|
required this.initialCguAccepted,
|
||||||
required this.previousRoute,
|
required this.previousRoute,
|
||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
|
this.embedContentOnly = false,
|
||||||
|
this.onEdit,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -66,9 +78,11 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final screenSize = MediaQuery.of(context).size;
|
final screenSize = MediaQuery.of(context).size;
|
||||||
final cardWidth = screenSize.width * 0.6;
|
final config = DisplayConfig.fromContext(context, mode: widget.mode);
|
||||||
final double imageAspectRatio = 2.0;
|
|
||||||
final cardHeight = cardWidth / imageAspectRatio;
|
if (widget.embedContentOnly) {
|
||||||
|
return _buildCard(context, config, screenSize);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Stack(
|
body: Stack(
|
||||||
@ -76,94 +90,497 @@ class _PresentationFormScreenState extends State<PresentationFormScreen> {
|
|||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
|
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
|
||||||
),
|
),
|
||||||
Center(
|
config.isMobile
|
||||||
child: SingleChildScrollView(
|
? _buildMobileLayout(context, config, screenSize)
|
||||||
padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0),
|
: _buildDesktopLayout(context, config, screenSize),
|
||||||
child: Column(
|
// Chevrons desktop uniquement
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
if (!config.isMobile) ...[
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
// Chevron Gauche (Retour)
|
||||||
|
Positioned(
|
||||||
|
top: screenSize.height / 2 - 20,
|
||||||
|
left: 40,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Transform(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
transform: Matrix4.rotationY(math.pi),
|
||||||
|
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (context.canPop()) {
|
||||||
|
context.pop();
|
||||||
|
} else {
|
||||||
|
context.go(widget.previousRoute);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: 'Retour',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Chevron Droit (Suivant)
|
||||||
|
Positioned(
|
||||||
|
top: screenSize.height / 2 - 20,
|
||||||
|
right: 40,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||||
|
onPressed: _cguAccepted ? _handleSubmit : null,
|
||||||
|
tooltip: 'Suivant',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout MOBILE : Plein écran sans scroll global
|
||||||
|
Widget _buildMobileLayout(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// Header fixe
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 20.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.stepText,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
widget.title,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Carte qui prend tout l'espace restant
|
||||||
|
Expanded(
|
||||||
|
child: _buildCard(context, config, screenSize),
|
||||||
|
),
|
||||||
|
// Boutons en bas
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildMobileButtons(context, config, screenSize),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout DESKTOP : Avec scroll
|
||||||
|
Widget _buildDesktopLayout(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||||
|
return Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.stepText,
|
||||||
|
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
widget.title,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
_buildCard(context, config, screenSize),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper pour la carte (Mobile ou Desktop)
|
||||||
|
Widget _buildCard(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||||
|
// Si mode Readonly Desktop : Layout spécial "Vintage" horizontal (2:1)
|
||||||
|
if (config.isReadonly && !config.isMobile && widget.embedContentOnly) {
|
||||||
|
return _buildReadonlyDesktopCard(context, config, screenSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si mode Readonly Mobile : Layout spécial "Vintage" vertical (1:2)
|
||||||
|
if (config.isReadonly && config.isMobile && widget.embedContentOnly) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
|
||||||
|
child: _buildMobileReadonlyCard(context, config, screenSize),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Widget cardContent = config.isMobile
|
||||||
|
? _buildMobileCard(context, config, screenSize)
|
||||||
|
: _buildDesktopCard(context, config, screenSize);
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
if (widget.embedContentOnly)
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.title,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: config.isMobile ? 18 : 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
cardContent,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
cardContent,
|
||||||
|
|
||||||
|
if (config.isReadonly && widget.onEdit != null)
|
||||||
|
Positioned(
|
||||||
|
top: widget.embedContentOnly ? 50 : 10,
|
||||||
|
right: 10,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.edit, color: Colors.black54),
|
||||||
|
onPressed: widget.onEdit,
|
||||||
|
tooltip: 'Modifier',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Carte en mode readonly MOBILE avec hauteur adaptative
|
||||||
|
Widget _buildMobileReadonlyCard(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
// Pas de height fixe, s'adapte au contenu
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 24.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(_getVerticalCardAsset()),
|
||||||
|
fit: BoxFit.fill, // Fill pour que l'image s'étire selon la hauteur du contenu
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min, // S'adapte au contenu
|
||||||
|
children: [
|
||||||
|
// Titre + Edit Button
|
||||||
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Expanded(
|
||||||
widget.stepText,
|
child: Text(
|
||||||
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
widget.title,
|
||||||
),
|
style: GoogleFonts.merienda(
|
||||||
const SizedBox(height: 20),
|
fontSize: 18,
|
||||||
Text(
|
fontWeight: FontWeight.bold,
|
||||||
widget.title,
|
color: Colors.black87
|
||||||
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 30),
|
|
||||||
Container(
|
|
||||||
width: cardWidth,
|
|
||||||
height: cardHeight,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
image: DecorationImage(
|
|
||||||
image: AssetImage(widget.cardColor.path),
|
|
||||||
fit: BoxFit.fill,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(40.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: CustomDecoratedTextField(
|
|
||||||
controller: _textController,
|
|
||||||
hintText: widget.textFieldHint,
|
|
||||||
fieldHeight: cardHeight * 0.6,
|
|
||||||
maxLines: 10,
|
|
||||||
expandDynamically: true,
|
|
||||||
fontSize: 18.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
AppCustomCheckbox(
|
|
||||||
label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité',
|
|
||||||
value: _cguAccepted,
|
|
||||||
onChanged: (value) => setState(() => _cguAccepted = value ?? false),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (widget.onEdit != null)
|
||||||
|
const SizedBox(width: 28),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
// Contenu aligné en haut (Texte scrollable + Checkbox)
|
||||||
// Chevron Gauche (Retour)
|
Padding(
|
||||||
Positioned(
|
padding: const EdgeInsets.only(top: 20.0),
|
||||||
top: screenSize.height / 2 - 20,
|
child: Column(
|
||||||
left: 40,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
child: IconButton(
|
children: [
|
||||||
icon: Transform(
|
// Champ texte scrollable
|
||||||
alignment: Alignment.center,
|
// On utilise ConstrainedBox pour limiter la hauteur max
|
||||||
transform: Matrix4.rotationY(math.pi),
|
ConstrainedBox(
|
||||||
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
constraints: const BoxConstraints(maxHeight: 300), // Max height pour éviter une carte infinie
|
||||||
|
child: CustomDecoratedTextField(
|
||||||
|
controller: _textController,
|
||||||
|
hintText: widget.textFieldHint,
|
||||||
|
fieldHeight: null, // Flexible
|
||||||
|
maxLines: 100,
|
||||||
|
expandDynamically: true, // Scrollable
|
||||||
|
fontSize: 14.0,
|
||||||
|
readOnly: config.isReadonly,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Checkbox
|
||||||
|
Transform.scale(
|
||||||
|
scale: 0.85,
|
||||||
|
child: AppCustomCheckbox(
|
||||||
|
label: 'J\'accepte les CGU et la\nPolitique de confidentialité',
|
||||||
|
value: _cguAccepted,
|
||||||
|
onChanged: (v) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
if (widget.onEdit != null)
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
child: IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
icon: const Icon(Icons.edit, color: Colors.black54, size: 24),
|
||||||
|
onPressed: widget.onEdit,
|
||||||
|
tooltip: 'Modifier',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Carte en mode readonly desktop avec AspectRatio 2:1 (format de l'ancien récapitulatif)
|
||||||
|
Widget _buildReadonlyDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||||
|
// Largeur de la carte : 50% de l'écran
|
||||||
|
final cardWidth = screenSize.width / 2.0;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: cardWidth,
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 2.0,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(widget.cardColor.path),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Titre + Edit Button
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.title,
|
||||||
|
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.onEdit != null)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
|
||||||
|
onPressed: widget.onEdit,
|
||||||
|
tooltip: 'Modifier',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
|
||||||
|
// Texte de motivation
|
||||||
|
Expanded(
|
||||||
|
child: CustomDecoratedTextField(
|
||||||
|
controller: _textController,
|
||||||
|
hintText: '',
|
||||||
|
fieldHeight: double.infinity, // Remplit l'espace disponible
|
||||||
|
maxLines: 10,
|
||||||
|
expandDynamically: false, // Fixe pour le readonly
|
||||||
|
fontSize: 18.0,
|
||||||
|
readOnly: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// CGU
|
||||||
|
AppCustomCheckbox(
|
||||||
|
label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité',
|
||||||
|
value: _cguAccepted,
|
||||||
|
onChanged: (v) {}, // Readonly
|
||||||
|
checkboxSize: 22.0,
|
||||||
|
fontSize: 16.0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Carte DESKTOP : Format horizontal 2:1
|
||||||
|
Widget _buildDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||||
|
final cardWidth = screenSize.width * 0.6;
|
||||||
|
final double imageAspectRatio = 2.0;
|
||||||
|
final cardHeight = cardWidth / imageAspectRatio;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: cardWidth,
|
||||||
|
height: cardHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(widget.cardColor.path),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(40.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: CustomDecoratedTextField(
|
||||||
|
controller: _textController,
|
||||||
|
hintText: widget.textFieldHint,
|
||||||
|
fieldHeight: cardHeight * 0.6,
|
||||||
|
maxLines: 10,
|
||||||
|
expandDynamically: true,
|
||||||
|
fontSize: 18.0,
|
||||||
|
readOnly: config.isReadonly,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
AppCustomCheckbox(
|
||||||
|
label: 'J\'accepte les Conditions Générales\nd\'Utilisation et la Politique de confidentialité',
|
||||||
|
value: _cguAccepted,
|
||||||
|
onChanged: config.isReadonly ? (v) {} : (value) => setState(() => _cguAccepted = value ?? false),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Carte MOBILE : Prend tout l'espace disponible
|
||||||
|
Widget _buildMobileCard(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||||
|
// Le contenu du champ texte
|
||||||
|
Widget textFieldContent = LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
// En mode embed (récap), constraints.maxHeight peut être infini, donc on fixe une hauteur par défaut
|
||||||
|
// En mode standalone, on utilise la hauteur disponible
|
||||||
|
double height = constraints.maxHeight;
|
||||||
|
if (height.isInfinite) height = 200.0;
|
||||||
|
|
||||||
|
return CustomDecoratedTextField(
|
||||||
|
controller: _textController,
|
||||||
|
hintText: widget.textFieldHint,
|
||||||
|
fieldHeight: height,
|
||||||
|
maxLines: 100,
|
||||||
|
expandDynamically: false,
|
||||||
|
fontSize: 14.0,
|
||||||
|
readOnly: config.isReadonly,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(_getVerticalCardAsset()),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Champ de texte
|
||||||
|
if (widget.embedContentOnly)
|
||||||
|
// En mode récapitulatif, on donne une hauteur fixe pour éviter l'erreur d'Expanded
|
||||||
|
SizedBox(height: 200, child: textFieldContent)
|
||||||
|
else
|
||||||
|
// En mode écran complet, on prend tout l'espace restant
|
||||||
|
Expanded(child: textFieldContent),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Checkbox en bas
|
||||||
|
Transform.scale(
|
||||||
|
scale: 0.85,
|
||||||
|
child: AppCustomCheckbox(
|
||||||
|
label: 'J\'accepte les CGU et la\nPolitique de confidentialité',
|
||||||
|
value: _cguAccepted,
|
||||||
|
onChanged: config.isReadonly ? (v) {} : (value) => setState(() => _cguAccepted = value ?? false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boutons mobile
|
||||||
|
Widget _buildMobileButtons(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: screenSize.width * 0.05,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: HoverReliefWidget(
|
||||||
|
child: CustomNavigationButton(
|
||||||
|
text: 'Précédent',
|
||||||
|
style: NavigationButtonStyle.purple,
|
||||||
|
onPressed: () {
|
||||||
|
if (context.canPop()) {
|
||||||
|
context.pop();
|
||||||
|
} else {
|
||||||
|
context.go(widget.previousRoute);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
|
||||||
if (context.canPop()) {
|
|
||||||
context.pop();
|
|
||||||
} else {
|
|
||||||
context.go(widget.previousRoute);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: 'Retour',
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Chevron Droit (Suivant)
|
const SizedBox(width: 16),
|
||||||
Positioned(
|
Expanded(
|
||||||
top: screenSize.height / 2 - 20,
|
child: HoverReliefWidget(
|
||||||
right: 40,
|
child: CustomNavigationButton(
|
||||||
child: IconButton(
|
text: 'Suivant',
|
||||||
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
style: NavigationButtonStyle.green,
|
||||||
onPressed: _cguAccepted ? _handleSubmit : null,
|
onPressed: _handleSubmit,
|
||||||
tooltip: 'Suivant',
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retourne l'asset de carte vertical correspondant à la couleur
|
||||||
|
String _getVerticalCardAsset() {
|
||||||
|
switch (widget.cardColor) {
|
||||||
|
case CardColorHorizontal.blue:
|
||||||
|
return CardColorVertical.blue.path;
|
||||||
|
case CardColorHorizontal.green:
|
||||||
|
return CardColorVertical.green.path;
|
||||||
|
case CardColorHorizontal.lavender:
|
||||||
|
return CardColorVertical.lavender.path;
|
||||||
|
case CardColorHorizontal.lime:
|
||||||
|
return CardColorVertical.lime.path;
|
||||||
|
case CardColorHorizontal.peach:
|
||||||
|
return CardColorVertical.peach.path;
|
||||||
|
case CardColorHorizontal.pink:
|
||||||
|
return CardColorVertical.pink.path;
|
||||||
|
case CardColorHorizontal.red:
|
||||||
|
return CardColorVertical.red.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,9 +5,12 @@ import 'package:intl/intl.dart';
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import '../models/card_assets.dart';
|
import '../models/card_assets.dart';
|
||||||
|
import '../config/display_config.dart';
|
||||||
import 'custom_app_text_field.dart';
|
import 'custom_app_text_field.dart';
|
||||||
|
import 'form_field_wrapper.dart';
|
||||||
import 'app_custom_checkbox.dart';
|
import 'app_custom_checkbox.dart';
|
||||||
import 'hover_relief_widget.dart';
|
import 'hover_relief_widget.dart';
|
||||||
|
import 'custom_navigation_button.dart';
|
||||||
|
|
||||||
/// Données pour le formulaire d'informations professionnelles
|
/// Données pour le formulaire d'informations professionnelles
|
||||||
class ProfessionalInfoData {
|
class ProfessionalInfoData {
|
||||||
@ -36,7 +39,9 @@ class ProfessionalInfoData {
|
|||||||
|
|
||||||
/// Widget générique pour le formulaire d'informations professionnelles
|
/// Widget générique pour le formulaire d'informations professionnelles
|
||||||
/// Utilisé pour l'inscription des Assistantes Maternelles
|
/// Utilisé pour l'inscription des Assistantes Maternelles
|
||||||
|
/// Supporte mode éditable et readonly, responsive mobile/desktop
|
||||||
class ProfessionalInfoFormScreen extends StatefulWidget {
|
class ProfessionalInfoFormScreen extends StatefulWidget {
|
||||||
|
final DisplayMode mode;
|
||||||
final String stepText;
|
final String stepText;
|
||||||
final String title;
|
final String title;
|
||||||
final CardColorHorizontal cardColor;
|
final CardColorHorizontal cardColor;
|
||||||
@ -44,9 +49,12 @@ class ProfessionalInfoFormScreen extends StatefulWidget {
|
|||||||
final String previousRoute;
|
final String previousRoute;
|
||||||
final Function(ProfessionalInfoData) onSubmit;
|
final Function(ProfessionalInfoData) onSubmit;
|
||||||
final Future<void> Function()? onPickPhoto;
|
final Future<void> Function()? onPickPhoto;
|
||||||
|
final bool embedContentOnly;
|
||||||
|
final VoidCallback? onEdit;
|
||||||
|
|
||||||
const ProfessionalInfoFormScreen({
|
const ProfessionalInfoFormScreen({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.mode = DisplayMode.editable,
|
||||||
required this.stepText,
|
required this.stepText,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.cardColor,
|
required this.cardColor,
|
||||||
@ -54,6 +62,8 @@ class ProfessionalInfoFormScreen extends StatefulWidget {
|
|||||||
required this.previousRoute,
|
required this.previousRoute,
|
||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
this.onPickPhoto,
|
this.onPickPhoto,
|
||||||
|
this.embedContentOnly = false,
|
||||||
|
this.onEdit,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -163,15 +173,10 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final screenSize = MediaQuery.of(context).size;
|
final screenSize = MediaQuery.of(context).size;
|
||||||
final Color baseCardColorForShadow = Colors.green.shade300;
|
final config = DisplayConfig.fromContext(context, mode: widget.mode);
|
||||||
final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90);
|
|
||||||
final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130);
|
|
||||||
|
|
||||||
ImageProvider? currentImageProvider;
|
if (widget.embedContentOnly) {
|
||||||
if (_photoFile != null) {
|
return _buildCard(context, config, screenSize);
|
||||||
currentImageProvider = FileImage(_photoFile!);
|
|
||||||
} else if (_photoPathFramework != null && _photoPathFramework!.startsWith('assets/')) {
|
|
||||||
currentImageProvider = AssetImage(_photoPathFramework!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -186,201 +191,697 @@ class _ProfessionalInfoFormScreenState extends State<ProfessionalInfoFormScreen>
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(widget.stepText, style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
|
Text(
|
||||||
const SizedBox(height: 10),
|
widget.stepText,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: config.isMobile ? 13 : 16,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: config.isMobile ? 6 : 10),
|
||||||
Text(
|
Text(
|
||||||
widget.title,
|
widget.title,
|
||||||
style: GoogleFonts.merienda(
|
style: GoogleFonts.merienda(
|
||||||
fontSize: 24,
|
fontSize: config.isMobile ? 18 : 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 30),
|
SizedBox(height: config.isMobile ? 16 : 30),
|
||||||
Container(
|
_buildCard(context, config, screenSize),
|
||||||
width: screenSize.width * 0.6,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 50),
|
// Boutons mobile sous la carte
|
||||||
constraints: const BoxConstraints(minHeight: 650),
|
if (config.isMobile) ...[
|
||||||
decoration: BoxDecoration(
|
const SizedBox(height: 20),
|
||||||
image: DecorationImage(image: AssetImage(widget.cardColor.path), fit: BoxFit.fill),
|
_buildMobileButtons(context, config, screenSize),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Chevrons desktop uniquement
|
||||||
|
if (!config.isMobile) ...[
|
||||||
|
// Chevron Gauche (Retour)
|
||||||
|
Positioned(
|
||||||
|
top: screenSize.height / 2 - 20,
|
||||||
|
left: 40,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Transform(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
transform: Matrix4.rotationY(math.pi),
|
||||||
|
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (context.canPop()) {
|
||||||
|
context.pop();
|
||||||
|
} else {
|
||||||
|
context.go(widget.previousRoute);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: 'Précédent',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Chevron Droit (Suivant)
|
||||||
|
Positioned(
|
||||||
|
top: screenSize.height / 2 - 20,
|
||||||
|
right: 40,
|
||||||
|
child: IconButton(
|
||||||
|
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||||
|
onPressed: _submitForm,
|
||||||
|
tooltip: 'Suivant',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCard(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||||
|
// Si mode Readonly Desktop : Layout spécial "Vintage" horizontal
|
||||||
|
if (config.isReadonly && !config.isMobile && widget.embedContentOnly) {
|
||||||
|
return _buildReadonlyDesktopCard(context, config, screenSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si mode Readonly Mobile : Layout spécial "Vintage" vertical
|
||||||
|
if (config.isReadonly && config.isMobile && widget.embedContentOnly) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05),
|
||||||
|
child: _buildMobileReadonlyCard(context, config, screenSize),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: config.isMobile ? screenSize.width * 0.9 : screenSize.width * 0.6,
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: config.isMobile ? 20 : (config.isReadonly ? 30 : 50),
|
||||||
|
horizontal: config.isMobile ? 24 : 50,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(
|
||||||
|
config.isMobile
|
||||||
|
? _getVerticalCardAsset()
|
||||||
|
: widget.cardColor.path
|
||||||
|
),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (widget.embedContentOnly) ...[
|
||||||
|
Text(
|
||||||
|
widget.title,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: config.isMobile ? 18 : 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
),
|
),
|
||||||
child: Form(
|
textAlign: TextAlign.center,
|
||||||
key: _formKey,
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
config.isMobile
|
||||||
|
? _buildMobileFields(context, config)
|
||||||
|
: _buildDesktopFields(context, config),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (config.isReadonly && widget.onEdit != null)
|
||||||
|
Positioned(
|
||||||
|
top: 10,
|
||||||
|
right: 10,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.edit, color: Colors.black54),
|
||||||
|
onPressed: widget.onEdit,
|
||||||
|
tooltip: 'Modifier',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Carte en mode readonly MOBILE avec hauteur adaptative
|
||||||
|
Widget _buildMobileReadonlyCard(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
// Pas de height fixe
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 24.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(_getVerticalCardAsset()),
|
||||||
|
fit: BoxFit.fill, // Fill pour s'adapter
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Titre + Edit Button
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.title,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.onEdit != null)
|
||||||
|
const SizedBox(width: 28),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Contenu aligné en haut
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 20.0),
|
||||||
|
child: _buildMobileFields(context, config),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
if (widget.onEdit != null)
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
child: IconButton(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
icon: const Icon(Icons.edit, color: Colors.black54, size: 24),
|
||||||
|
onPressed: widget.onEdit,
|
||||||
|
tooltip: 'Modifier',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Carte en mode readonly desktop avec AspectRatio 2:1
|
||||||
|
Widget _buildReadonlyDesktopCard(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||||
|
final cardWidth = screenSize.width / 2.0;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: cardWidth,
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 2.0,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: AssetImage(widget.cardColor.path),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Titre + Edit Button
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
widget.title,
|
||||||
|
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.onEdit != null)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
|
||||||
|
onPressed: widget.onEdit,
|
||||||
|
tooltip: 'Modifier',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
|
||||||
|
// Contenu
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// PHOTO (1/3)
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 5),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
child: _photoFile != null
|
||||||
|
? Image.file(_photoFile!, fit: BoxFit.cover)
|
||||||
|
: (_photoPathFramework != null && _photoPathFramework!.startsWith('assets/')
|
||||||
|
? Image.asset(_photoPathFramework!, fit: BoxFit.contain)
|
||||||
|
: Image.asset('assets/images/photo.png', fit: BoxFit.contain)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
AppCustomCheckbox(
|
||||||
|
label: 'J\'accepte l\'utilisation\nde ma photo.',
|
||||||
|
value: _photoConsent,
|
||||||
|
onChanged: (v) {}, // Readonly
|
||||||
|
checkboxSize: 22.0,
|
||||||
|
fontSize: 14.0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 32),
|
||||||
|
|
||||||
|
// CHAMPS (2/3) - Layout optimisé compact
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Ligne 1 : Ville + Pays
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
// Colonne Gauche: Photo et Checkbox
|
Expanded(child: _buildReadonlyField('Ville de naissance', _birthCityController.text)),
|
||||||
SizedBox(
|
const SizedBox(width: 16),
|
||||||
width: 300,
|
Expanded(child: _buildReadonlyField('Pays de naissance', _birthCountryController.text)),
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
HoverReliefWidget(
|
|
||||||
onPressed: _pickPhoto,
|
|
||||||
borderRadius: BorderRadius.circular(10.0),
|
|
||||||
initialShadowColor: initialPhotoShadow,
|
|
||||||
hoverShadowColor: hoverPhotoShadow,
|
|
||||||
child: SizedBox(
|
|
||||||
height: 270,
|
|
||||||
width: 270,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
image: currentImageProvider != null
|
|
||||||
? DecorationImage(image: currentImageProvider, fit: BoxFit.cover)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
child: currentImageProvider == null
|
|
||||||
? Image.asset('assets/images/photo.png', fit: BoxFit.contain)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
AppCustomCheckbox(
|
|
||||||
label: 'J\'accepte l\'utilisation\nde ma photo.',
|
|
||||||
value: _photoConsent,
|
|
||||||
onChanged: (val) => setState(() => _photoConsent = val ?? false),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 30),
|
|
||||||
// Colonne Droite: Champs de naissance
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
CustomAppTextField(
|
|
||||||
controller: _birthCityController,
|
|
||||||
labelText: 'Ville de naissance',
|
|
||||||
hintText: 'Votre ville de naissance',
|
|
||||||
fieldWidth: double.infinity,
|
|
||||||
labelFontSize: 22.0,
|
|
||||||
inputFontSize: 20.0,
|
|
||||||
validator: (v) => v!.isEmpty ? 'Ville requise' : null,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
CustomAppTextField(
|
|
||||||
controller: _birthCountryController,
|
|
||||||
labelText: 'Pays de naissance',
|
|
||||||
hintText: 'Votre pays de naissance',
|
|
||||||
fieldWidth: double.infinity,
|
|
||||||
labelFontSize: 22.0,
|
|
||||||
inputFontSize: 20.0,
|
|
||||||
validator: (v) => v!.isEmpty ? 'Pays requis' : null,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
CustomAppTextField(
|
|
||||||
controller: _dateOfBirthController,
|
|
||||||
labelText: 'Date de naissance',
|
|
||||||
hintText: 'JJ/MM/AAAA',
|
|
||||||
readOnly: true,
|
|
||||||
onTap: () => _selectDate(context),
|
|
||||||
suffixIcon: Icons.calendar_today,
|
|
||||||
fieldWidth: double.infinity,
|
|
||||||
labelFontSize: 22.0,
|
|
||||||
inputFontSize: 20.0,
|
|
||||||
validator: (v) => _selectedDate == null ? 'Date requise' : null,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 12),
|
||||||
CustomAppTextField(
|
|
||||||
controller: _nirController,
|
// Ligne 2 : Date + NIR (NIR prend plus de place si possible ou 50/50)
|
||||||
labelText: 'N° Sécurité Sociale (NIR)',
|
|
||||||
hintText: 'Votre NIR à 13 chiffres',
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
fieldWidth: double.infinity,
|
|
||||||
labelFontSize: 22.0,
|
|
||||||
inputFontSize: 20.0,
|
|
||||||
validator: (v) {
|
|
||||||
if (v == null || v.isEmpty) return 'NIR requis';
|
|
||||||
if (v.length != 13) return 'Le NIR doit contenir 13 chiffres';
|
|
||||||
if (!RegExp(r'^[1-3]').hasMatch(v[0])) return 'Le NIR doit commencer par 1, 2 ou 3';
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(flex: 2, child: _buildReadonlyField('Date de naissance', _dateOfBirthController.text)),
|
||||||
child: CustomAppTextField(
|
const SizedBox(width: 16),
|
||||||
controller: _agrementController,
|
Expanded(flex: 3, child: _buildReadonlyField('NIR', _nirController.text)),
|
||||||
labelText: 'N° d\'agrément',
|
],
|
||||||
hintText: 'Votre numéro d\'agrément',
|
),
|
||||||
fieldWidth: double.infinity,
|
const SizedBox(height: 12),
|
||||||
labelFontSize: 22.0,
|
|
||||||
inputFontSize: 20.0,
|
// Ligne 3 : Agrément + Capacité
|
||||||
validator: (v) => v!.isEmpty ? 'Agrément requis' : null,
|
Row(
|
||||||
),
|
children: [
|
||||||
),
|
Expanded(flex: 3, child: _buildReadonlyField('N° Agrément', _agrementController.text)),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(flex: 2, child: _buildReadonlyField('Capacité', _capacityController.text)),
|
||||||
child: CustomAppTextField(
|
|
||||||
controller: _capacityController,
|
|
||||||
labelText: 'Capacité d\'accueil',
|
|
||||||
hintText: 'Ex: 3',
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
fieldWidth: double.infinity,
|
|
||||||
labelFontSize: 22.0,
|
|
||||||
inputFontSize: 20.0,
|
|
||||||
validator: (v) {
|
|
||||||
if (v == null || v.isEmpty) return 'Capacité requise';
|
|
||||||
final n = int.tryParse(v);
|
|
||||||
if (n == null || n <= 0) return 'Nombre invalide';
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper pour champ Readonly style "Beige"
|
||||||
|
Widget _buildReadonlyField(String label, String value) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: GoogleFonts.merienda(fontSize: 18.0, fontWeight: FontWeight.w600),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 45.0, // Hauteur réduite pour compacter
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: const DecorationImage(
|
||||||
|
image: AssetImage('assets/images/bg_beige.png'),
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
value.isNotEmpty ? value : '-',
|
||||||
|
style: GoogleFonts.merienda(fontSize: 16.0),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout DESKTOP : Photo à gauche, champs à droite
|
||||||
|
Widget _buildDesktopFields(BuildContext context, DisplayConfig config) {
|
||||||
|
final double verticalSpacing = config.isReadonly ? 16.0 : 32.0;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Photo + Checkbox à gauche
|
||||||
|
SizedBox(
|
||||||
|
width: 300,
|
||||||
|
child: _buildPhotoSection(context, config),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 30),
|
||||||
|
// Champs à droite
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildField(
|
||||||
|
config: config,
|
||||||
|
label: 'Ville de naissance',
|
||||||
|
controller: _birthCityController,
|
||||||
|
hint: 'Votre ville de naissance',
|
||||||
|
validator: (v) => v!.isEmpty ? 'Ville requise' : null,
|
||||||
|
),
|
||||||
|
SizedBox(height: verticalSpacing),
|
||||||
|
_buildField(
|
||||||
|
config: config,
|
||||||
|
label: 'Pays de naissance',
|
||||||
|
controller: _birthCountryController,
|
||||||
|
hint: 'Votre pays de naissance',
|
||||||
|
validator: (v) => v!.isEmpty ? 'Pays requis' : null,
|
||||||
|
),
|
||||||
|
SizedBox(height: verticalSpacing),
|
||||||
|
_buildField(
|
||||||
|
config: config,
|
||||||
|
label: 'Date de naissance',
|
||||||
|
controller: _dateOfBirthController,
|
||||||
|
hint: 'JJ/MM/AAAA',
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () => _selectDate(context),
|
||||||
|
suffixIcon: Icons.calendar_today,
|
||||||
|
validator: (v) => _selectedDate == null ? 'Date requise' : null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
// Chevron Gauche (Retour)
|
),
|
||||||
Positioned(
|
SizedBox(height: verticalSpacing),
|
||||||
top: screenSize.height / 2 - 20,
|
_buildField(
|
||||||
left: 40,
|
config: config,
|
||||||
child: IconButton(
|
label: 'N° Sécurité Sociale (NIR)',
|
||||||
icon: Transform(
|
controller: _nirController,
|
||||||
alignment: Alignment.center,
|
hint: 'Votre NIR à 13 chiffres',
|
||||||
transform: Matrix4.rotationY(math.pi),
|
keyboardType: TextInputType.number,
|
||||||
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
validator: (v) {
|
||||||
|
if (v == null || v.isEmpty) return 'NIR requis';
|
||||||
|
if (v.length != 13) return 'Le NIR doit contenir 13 chiffres';
|
||||||
|
if (!RegExp(r'^[1-3]').hasMatch(v[0])) return 'Le NIR doit commencer par 1, 2 ou 3';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: verticalSpacing),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildField(
|
||||||
|
config: config,
|
||||||
|
label: 'N° d\'agrément',
|
||||||
|
controller: _agrementController,
|
||||||
|
hint: 'Votre numéro d\'agrément',
|
||||||
|
validator: (v) => v!.isEmpty ? 'Agrément requis' : null,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
),
|
||||||
if (context.canPop()) {
|
const SizedBox(width: 20),
|
||||||
context.pop();
|
Expanded(
|
||||||
} else {
|
child: _buildField(
|
||||||
context.go(widget.previousRoute);
|
config: config,
|
||||||
}
|
label: 'Capacité d\'accueil',
|
||||||
},
|
controller: _capacityController,
|
||||||
tooltip: 'Précédent',
|
hint: 'Ex: 3',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (v) {
|
||||||
|
if (v == null || v.isEmpty) return 'Capacité requise';
|
||||||
|
final n = int.tryParse(v);
|
||||||
|
if (n == null || n <= 0) return 'Nombre invalide';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layout MOBILE : Tout empilé verticalement
|
||||||
|
Widget _buildMobileFields(BuildContext context, DisplayConfig config) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Photo + Checkbox en premier
|
||||||
|
_buildPhotoSection(context, config),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
_buildField(
|
||||||
|
config: config,
|
||||||
|
label: 'Ville de naissance',
|
||||||
|
controller: _birthCityController,
|
||||||
|
hint: 'Votre ville de naissance',
|
||||||
|
validator: (v) => v!.isEmpty ? 'Ville requise' : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
_buildField(
|
||||||
|
config: config,
|
||||||
|
label: 'Pays de naissance',
|
||||||
|
controller: _birthCountryController,
|
||||||
|
hint: 'Votre pays de naissance',
|
||||||
|
validator: (v) => v!.isEmpty ? 'Pays requis' : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
_buildField(
|
||||||
|
config: config,
|
||||||
|
label: 'Date de naissance',
|
||||||
|
controller: _dateOfBirthController,
|
||||||
|
hint: 'JJ/MM/AAAA',
|
||||||
|
readOnly: true,
|
||||||
|
onTap: () => _selectDate(context),
|
||||||
|
suffixIcon: Icons.calendar_today,
|
||||||
|
validator: (v) => _selectedDate == null ? 'Date requise' : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
_buildField(
|
||||||
|
config: config,
|
||||||
|
label: 'N° Sécurité Sociale (NIR)',
|
||||||
|
controller: _nirController,
|
||||||
|
hint: 'Votre NIR à 13 chiffres',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (v) {
|
||||||
|
if (v == null || v.isEmpty) return 'NIR requis';
|
||||||
|
if (v.length != 13) return 'Le NIR doit contenir 13 chiffres';
|
||||||
|
if (!RegExp(r'^[1-3]').hasMatch(v[0])) return 'Le NIR doit commencer par 1, 2 ou 3';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
_buildField(
|
||||||
|
config: config,
|
||||||
|
label: 'N° d\'agrément',
|
||||||
|
controller: _agrementController,
|
||||||
|
hint: 'Votre numéro d\'agrément',
|
||||||
|
validator: (v) => v!.isEmpty ? 'Agrément requis' : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
_buildField(
|
||||||
|
config: config,
|
||||||
|
label: 'Capacité d\'accueil',
|
||||||
|
controller: _capacityController,
|
||||||
|
hint: 'Ex: 3',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (v) {
|
||||||
|
if (v == null || v.isEmpty) return 'Capacité requise';
|
||||||
|
final n = int.tryParse(v);
|
||||||
|
if (n == null || n <= 0) return 'Nombre invalide';
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Section photo + checkbox
|
||||||
|
Widget _buildPhotoSection(BuildContext context, DisplayConfig config) {
|
||||||
|
final Color baseCardColorForShadow = Colors.green.shade300;
|
||||||
|
final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90);
|
||||||
|
final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130);
|
||||||
|
|
||||||
|
ImageProvider? currentImageProvider;
|
||||||
|
if (_photoFile != null) {
|
||||||
|
currentImageProvider = FileImage(_photoFile!);
|
||||||
|
} else if (_photoPathFramework != null && _photoPathFramework!.startsWith('assets/')) {
|
||||||
|
currentImageProvider = AssetImage(_photoPathFramework!);
|
||||||
|
}
|
||||||
|
|
||||||
|
final photoSize = config.isMobile ? 200.0 : 270.0;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
HoverReliefWidget(
|
||||||
|
onPressed: _pickPhoto,
|
||||||
|
borderRadius: BorderRadius.circular(10.0),
|
||||||
|
initialShadowColor: initialPhotoShadow,
|
||||||
|
hoverShadowColor: hoverPhotoShadow,
|
||||||
|
child: SizedBox(
|
||||||
|
height: photoSize,
|
||||||
|
width: photoSize,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
image: currentImageProvider != null
|
||||||
|
? DecorationImage(image: currentImageProvider, fit: BoxFit.cover)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: currentImageProvider == null
|
||||||
|
? Image.asset('assets/images/photo.png', fit: BoxFit.contain)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Chevron Droit (Suivant)
|
),
|
||||||
Positioned(
|
const SizedBox(height: 10),
|
||||||
top: screenSize.height / 2 - 20,
|
AppCustomCheckbox(
|
||||||
right: 40,
|
label: 'J\'accepte l\'utilisation\nde ma photo.',
|
||||||
child: IconButton(
|
value: _photoConsent,
|
||||||
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
onChanged: (val) => setState(() => _photoConsent = val ?? false),
|
||||||
onPressed: _submitForm,
|
),
|
||||||
tooltip: 'Suivant',
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit un champ individuel
|
||||||
|
Widget _buildField({
|
||||||
|
required DisplayConfig config,
|
||||||
|
required String label,
|
||||||
|
required TextEditingController controller,
|
||||||
|
String? hint,
|
||||||
|
TextInputType? keyboardType,
|
||||||
|
bool readOnly = false,
|
||||||
|
VoidCallback? onTap,
|
||||||
|
IconData? suffixIcon,
|
||||||
|
String? Function(String?)? validator,
|
||||||
|
}) {
|
||||||
|
if (config.isReadonly) {
|
||||||
|
return FormFieldWrapper(
|
||||||
|
config: config,
|
||||||
|
label: label,
|
||||||
|
value: controller.text,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return CustomAppTextField(
|
||||||
|
controller: controller,
|
||||||
|
labelText: label,
|
||||||
|
hintText: hint ?? label,
|
||||||
|
fieldWidth: double.infinity,
|
||||||
|
fieldHeight: config.isMobile ? 45.0 : 53.0,
|
||||||
|
labelFontSize: config.isMobile ? 15.0 : 22.0,
|
||||||
|
inputFontSize: config.isMobile ? 14.0 : 20.0,
|
||||||
|
keyboardType: keyboardType ?? TextInputType.text,
|
||||||
|
readOnly: readOnly,
|
||||||
|
onTap: onTap,
|
||||||
|
suffixIcon: suffixIcon,
|
||||||
|
validator: validator,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boutons mobile
|
||||||
|
Widget _buildMobileButtons(BuildContext context, DisplayConfig config, Size screenSize) {
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: screenSize.width * 0.05,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: HoverReliefWidget(
|
||||||
|
child: CustomNavigationButton(
|
||||||
|
text: 'Précédent',
|
||||||
|
style: NavigationButtonStyle.purple,
|
||||||
|
onPressed: () {
|
||||||
|
if (context.canPop()) {
|
||||||
|
context.pop();
|
||||||
|
} else {
|
||||||
|
context.go(widget.previousRoute);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: HoverReliefWidget(
|
||||||
|
child: CustomNavigationButton(
|
||||||
|
text: 'Suivant',
|
||||||
|
style: NavigationButtonStyle.green,
|
||||||
|
onPressed: _submitForm,
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retourne l'asset de carte vertical correspondant à la couleur
|
||||||
|
String _getVerticalCardAsset() {
|
||||||
|
switch (widget.cardColor) {
|
||||||
|
case CardColorHorizontal.blue:
|
||||||
|
return CardColorVertical.blue.path;
|
||||||
|
case CardColorHorizontal.green:
|
||||||
|
return CardColorVertical.green.path;
|
||||||
|
case CardColorHorizontal.lavender:
|
||||||
|
return CardColorVertical.lavender.path;
|
||||||
|
case CardColorHorizontal.lime:
|
||||||
|
return CardColorVertical.lime.path;
|
||||||
|
case CardColorHorizontal.peach:
|
||||||
|
return CardColorVertical.peach.path;
|
||||||
|
case CardColorHorizontal.pink:
|
||||||
|
return CardColorVertical.pink.path;
|
||||||
|
case CardColorHorizontal.red:
|
||||||
|
return CardColorVertical.red.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ class SummaryScreen extends StatelessWidget {
|
|||||||
actions: [
|
actions: [
|
||||||
Center(
|
Center(
|
||||||
child: ImageButton(
|
child: ImageButton(
|
||||||
bg: 'assets/images/btn_green.png',
|
bg: 'assets/images/bg_green.png',
|
||||||
text: 'OK',
|
text: 'OK',
|
||||||
textColor: const Color(0xFF2D6A4F),
|
textColor: const Color(0xFF2D6A4F),
|
||||||
width: 150,
|
width: 150,
|
||||||
@ -89,7 +89,7 @@ class SummaryScreen extends StatelessWidget {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
ImageButton(
|
ImageButton(
|
||||||
bg: 'assets/images/btn_green.png',
|
bg: 'assets/images/bg_green.png',
|
||||||
text: submitButtonText,
|
text: submitButtonText,
|
||||||
textColor: const Color(0xFF2D6A4F),
|
textColor: const Color(0xFF2D6A4F),
|
||||||
width: 350,
|
width: 350,
|
||||||
@ -230,11 +230,12 @@ Widget buildDisplayFieldValue(
|
|||||||
height: multiLine ? null : fieldHeight,
|
height: multiLine ? null : fieldHeight,
|
||||||
constraints: multiLine ? const BoxConstraints(minHeight: 50.0) : null,
|
constraints: multiLine ? const BoxConstraints(minHeight: 50.0) : null,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
image: DecorationImage(
|
image: const DecorationImage(
|
||||||
image: AssetImage('assets/images/input_field_bg.png'),
|
image: AssetImage('assets/images/bg_beige.png'),
|
||||||
fit: BoxFit.fill,
|
fit: BoxFit.fill,
|
||||||
),
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
value.isNotEmpty ? value : '-',
|
value.isNotEmpty ? value : '-',
|
||||||
|
|||||||