[#101] [Frontend] Inscription parent — API, soumission et validation
Squash merge de develop vers master. Livrables principaux (ticket #101 et mise au point associée) : - Branchement du formulaire d'inscription parent sur POST /api/v1/auth/register/parent - Payload DTO (parents, enfants, photos base64, CGU) et services Auth - Parcours gestionnaire : cartes dossiers, wizard validation famille, images authentifiées - Scripts d'inscription test (Martin, Durand/Rousseau, Lecomte) ; .gitignore .cursor/ Inclut également les ajustements develop fusionnés dans ce lot (inscription AM, champs relais, etc.). Closes #101 Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:p_tits_pas/services/configuration_service.dart';
|
||||
import 'package:p_tits_pas/utils/email_utils.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/relais_management_panel.dart';
|
||||
|
||||
/// Panneau Paramètres admin : Email (SMTP), Personnalisation, Avancé.
|
||||
@@ -182,6 +183,9 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
hintText: 'admin@example.com',
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
inputFormatters: const [EmailMaxLengthFormatter()],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@@ -191,7 +195,17 @@ class _ParametresPanelState extends State<ParametresPanel> {
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final t = c.text.trim();
|
||||
if (t.isNotEmpty) Navigator.pop(ctx, t);
|
||||
if (t.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final err = validateEmail(t, allowEmpty: true);
|
||||
if (err != null) {
|
||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||
SnackBar(content: Text(err)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
Navigator.pop(ctx, t);
|
||||
},
|
||||
child: const Text('Envoyer'),
|
||||
),
|
||||
|
||||
@@ -482,6 +482,9 @@ class _RelaisFormDialogState extends State<_RelaisFormDialog> {
|
||||
if (!_isValidPostalCode(_postalCodeCtrl.text.trim())) {
|
||||
return false;
|
||||
}
|
||||
if (validateFrenchNationalPhone(_ligneFixeCtrl.text, allowEmpty: true) != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (final day in _days) {
|
||||
final ferme = _closedByDay[day] ?? false;
|
||||
@@ -721,11 +724,7 @@ class _RelaisFormDialogState extends State<_RelaisFormDialog> {
|
||||
TextField(
|
||||
controller: _ligneFixeCtrl,
|
||||
keyboardType: TextInputType.phone,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
FrenchPhoneNumberFormatter(),
|
||||
],
|
||||
inputFormatters: frenchPhoneInputFormatters,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Ligne fixe',
|
||||
hintText: '01 23 45 67 89',
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:p_tits_pas/services/user_service.dart';
|
||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/admin_detail_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
|
||||
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||
import 'validation_modal_theme.dart';
|
||||
import 'validation_refus_form.dart';
|
||||
import 'validation_valider_confirm_dialog.dart';
|
||||
@@ -110,15 +111,8 @@ class _ValidationAmWizardState extends State<ValidationAmWizard> {
|
||||
static const double _proColumnMinWidth = 260;
|
||||
static const double _photoColumnMinWidth = 160;
|
||||
|
||||
/// URL complète pour la photo : si relatif, on préfixe par l’origine de l’API.
|
||||
static String _fullPhotoUrl(String? url) {
|
||||
if (url == null || url.trim().isEmpty) return '';
|
||||
final u = url.trim();
|
||||
if (u.startsWith('http://') || u.startsWith('https://')) return u;
|
||||
final base = ApiConfig.baseUrl;
|
||||
final origin = base.replaceAll(RegExp(r'/api/v1.*'), '');
|
||||
return u.startsWith('/') ? '$origin$u' : '$origin/$u';
|
||||
}
|
||||
/// URL complète pour la photo : si relatif, préfixe [ApiConfig.baseUrl] (ex. `/api/v1/uploads/...`).
|
||||
static String _fullPhotoUrl(String? url) => ApiConfig.absoluteMediaUrl(url);
|
||||
|
||||
Widget _buildPhotoSection(AppUser u) {
|
||||
final photoUrl = _fullPhotoUrl(u.photoUrl);
|
||||
@@ -187,8 +181,8 @@ class _ValidationAmWizardState extends State<ValidationAmWizard> {
|
||||
],
|
||||
),
|
||||
)
|
||||
: Image.network(
|
||||
photoUrl,
|
||||
: AuthNetworkImage(
|
||||
url: photoUrl,
|
||||
fit: BoxFit.cover,
|
||||
width: pw,
|
||||
height: ph,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@@ -8,6 +9,7 @@ import 'package:p_tits_pas/services/user_service.dart';
|
||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/admin_detail_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
|
||||
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||
import 'validation_modal_theme.dart';
|
||||
import 'validation_refus_form.dart';
|
||||
import 'validation_valider_confirm_dialog.dart';
|
||||
@@ -134,14 +136,7 @@ class _ValidationFamilyWizardState extends State<ValidationFamilyWizard> {
|
||||
3: [2, 5]
|
||||
}; // Code postal étroit, Ville large
|
||||
|
||||
static String _fullPhotoUrl(String? url) {
|
||||
if (url == null || url.trim().isEmpty) return '';
|
||||
final u = url.trim();
|
||||
if (u.startsWith('http://') || u.startsWith('https://')) return u;
|
||||
final base = ApiConfig.baseUrl;
|
||||
final origin = base.replaceAll(RegExp(r'/api/v1.*'), '');
|
||||
return u.startsWith('/') ? '$origin$u' : '$origin/$u';
|
||||
}
|
||||
static String _fullPhotoUrl(String? url) => ApiConfig.absoluteMediaUrl(url);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -349,6 +344,12 @@ class _ValidationFamilyWizardState extends State<ValidationFamilyWizard> {
|
||||
/// Carte enfant : prénom pleine largeur, puis ligne photo 1/3 + colonne 2/3 (champs + statut hors TF si besoin).
|
||||
Widget _buildEnfantCard(EnfantDossier e) {
|
||||
final photoUrl = _fullPhotoUrl(e.photoUrl);
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[PetitsPas/validation-famille] carte enfant id=${e.id} prénom=${e.firstName} | '
|
||||
'photoUrl modèle=${e.photoUrl ?? "∅"} | url affichée=${photoUrl.isEmpty ? "∅" : photoUrl}',
|
||||
);
|
||||
}
|
||||
final columnStatusLabel = _enfantColumnStatusLabel(e);
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(_enfantCardRadius),
|
||||
@@ -507,12 +508,12 @@ class _ValidationFamilyWizardState extends State<ValidationFamilyWizard> {
|
||||
}
|
||||
|
||||
Widget _buildEnfantPhotoSlot(String photoUrl, double width, double height) {
|
||||
const photoRadius = 8.0;
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(photoRadius),
|
||||
border: Border.all(color: Colors.black.withValues(alpha: 0.08)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -530,9 +531,9 @@ class _ValidationFamilyWizardState extends State<ValidationFamilyWizard> {
|
||||
child: Icon(Icons.person_outline, size: 32, color: Colors.grey.shade400),
|
||||
),
|
||||
)
|
||||
: Image.network(
|
||||
photoUrl,
|
||||
fit: BoxFit.contain,
|
||||
: AuthNetworkImage(
|
||||
url: photoUrl,
|
||||
fit: BoxFit.cover,
|
||||
width: width,
|
||||
height: height,
|
||||
errorBuilder: (_, __, ___) => ColoredBox(
|
||||
|
||||
Reference in New Issue
Block a user