feat(admin): validation AM/famille, À valider au survol, nettoyage logs
- À valider : ligne avec survol type AdminUserCard, icône Ouvrir centrée et plus grande (iconSize 34). - Wizard AM : titres Identité et coordonnées / Dossier professionnel / Présentation ; photo à gauche, grille droite [2,2,2,2] (NIR, naissance, agrément, capacité) ; AppUser champs naissance ; dates dd/MM/yyyy via formatIsoDateFr ; ValidationDetailSection titre optionnel. - Wizard famille : titre étape 4 « Présentation ». - Suppression des debugPrint liés médias / images / dossier (api_config, auth_network_image, dossier_unifie, user_service, validation_family). Made-with: Cursor
This commit is contained in:
parent
f442c85cb6
commit
0befd4eb2a
@ -1,4 +1,3 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
|
||||
/// Réponse unifiée GET /dossiers/:numeroDossier. Ticket #119, #107.
|
||||
@ -213,14 +212,6 @@ class EnfantDossier {
|
||||
factory EnfantDossier.fromJson(Map<String, dynamic> json) {
|
||||
final rawPhoto = json['photo_url'] ?? json['photoUrl'];
|
||||
final resolvedPhoto = _optionalPhotoUrl(rawPhoto);
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[PetitsPas/dossier] EnfantDossier.fromJson id=${json['id']} '
|
||||
'prénom=${json['first_name'] ?? json['prenom']} | '
|
||||
'photo_url brute=$rawPhoto (${rawPhoto?.runtimeType}) → '
|
||||
'résolu=${resolvedPhoto ?? "∅"}',
|
||||
);
|
||||
}
|
||||
return EnfantDossier(
|
||||
id: json['id']?.toString() ?? '',
|
||||
firstName: (json['first_name'] ?? json['prenom'])?.toString(),
|
||||
|
||||
@ -16,6 +16,9 @@ class AppUser {
|
||||
final String? relaisId;
|
||||
final String? relaisNom;
|
||||
final String? numeroDossier;
|
||||
final String? dateNaissance;
|
||||
final String? lieuNaissanceVille;
|
||||
final String? lieuNaissancePays;
|
||||
|
||||
AppUser({
|
||||
required this.id,
|
||||
@ -35,6 +38,9 @@ class AppUser {
|
||||
this.relaisId,
|
||||
this.relaisNom,
|
||||
this.numeroDossier,
|
||||
this.dateNaissance,
|
||||
this.lieuNaissanceVille,
|
||||
this.lieuNaissancePays,
|
||||
});
|
||||
|
||||
static String _str(dynamic v) {
|
||||
@ -78,9 +84,24 @@ class AppUser {
|
||||
?.toString(),
|
||||
relaisNom: relaisMap['nom']?.toString(),
|
||||
numeroDossier: json['numero_dossier'] is String ? json['numero_dossier'] as String : null,
|
||||
dateNaissance: _optionalDateString(json['date_naissance']),
|
||||
lieuNaissanceVille: json['lieu_naissance_ville'] is String
|
||||
? json['lieu_naissance_ville'] as String
|
||||
: null,
|
||||
lieuNaissancePays: json['lieu_naissance_pays'] is String
|
||||
? json['lieu_naissance_pays'] as String
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
static String? _optionalDateString(dynamic v) {
|
||||
if (v == null) return null;
|
||||
if (v is String) return v.isEmpty ? null : v;
|
||||
if (v is DateTime) return v.toIso8601String().split('T').first;
|
||||
final s = v.toString();
|
||||
return s.isEmpty ? null : s;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
@ -100,6 +121,9 @@ class AppUser {
|
||||
'relais_id': relaisId,
|
||||
'relais_nom': relaisNom,
|
||||
'numero_dossier': numeroDossier,
|
||||
'date_naissance': dateNaissance,
|
||||
'lieu_naissance_ville': lieuNaissanceVille,
|
||||
'lieu_naissance_pays': lieuNaissancePays,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:p_tits_pas/config/env.dart';
|
||||
|
||||
class ApiConfig {
|
||||
@ -21,26 +20,14 @@ class ApiConfig {
|
||||
/// On préfixe avec [baseUrl] (…/api/v1), pas seulement l’hôte : Traefik n’expose souvent que `/api`.
|
||||
static String absoluteMediaUrl(String? pathOrUrl) {
|
||||
if (pathOrUrl == null || pathOrUrl.trim().isEmpty) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[PetitsPas/media] absoluteMediaUrl: entrée vide (null ou "")');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
final u = pathOrUrl.trim();
|
||||
if (u.startsWith('http://') || u.startsWith('https://')) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[PetitsPas/media] absoluteMediaUrl: déjà absolu → $u');
|
||||
}
|
||||
return u;
|
||||
}
|
||||
final base = baseUrl.replaceAll(RegExp(r'/+$'), '');
|
||||
final out = u.startsWith('/') ? '$base$u' : '$base/$u';
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[PetitsPas/media] absoluteMediaUrl: base=$base | chemin brut="$u" → "$out"',
|
||||
);
|
||||
}
|
||||
return out;
|
||||
return u.startsWith('/') ? '$base$u' : '$base/$u';
|
||||
}
|
||||
|
||||
// Auth endpoints
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
import 'package:p_tits_pas/models/parent_model.dart';
|
||||
@ -94,9 +93,6 @@ class UserService {
|
||||
static Future<DossierUnifie> getDossier(String numeroDossier) async {
|
||||
final encoded = Uri.encodeComponent(numeroDossier);
|
||||
final uri = Uri.parse('${ApiConfig.baseUrl}${ApiConfig.dossiers}/$encoded');
|
||||
if (kDebugMode) {
|
||||
debugPrint('[PetitsPas/dossier] GET $uri');
|
||||
}
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: await _headers(),
|
||||
@ -119,19 +115,6 @@ class UserService {
|
||||
throw FormatException('Réponse invalide');
|
||||
}
|
||||
final dossier = DossierUnifie.fromJson(Map<String, dynamic>.from(decoded));
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[PetitsPas/dossier] réponse OK type=${dossier.type} | '
|
||||
'ApiConfig.baseUrl=${ApiConfig.baseUrl} | apiOrigin=${ApiConfig.apiOrigin}',
|
||||
);
|
||||
if (dossier.isFamily) {
|
||||
final f = dossier.asFamily;
|
||||
debugPrint(
|
||||
'[PetitsPas/dossier] famille ${f.numeroDossier} | '
|
||||
'${f.enfants.length} enfant(s)',
|
||||
);
|
||||
}
|
||||
}
|
||||
return dossier;
|
||||
} catch (e) {
|
||||
if (e is FormatException) rethrow;
|
||||
|
||||
11
frontend/lib/utils/date_display_utils.dart
Normal file
11
frontend/lib/utils/date_display_utils.dart
Normal file
@ -0,0 +1,11 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// Affiche une date ISO / parseable en `dd/MM/yyyy`, avec repli sur la chaîne ou [ifEmpty].
|
||||
String formatIsoDateFr(String? s, {String ifEmpty = '–'}) {
|
||||
if (s == null || s.trim().isEmpty) return ifEmpty;
|
||||
try {
|
||||
return DateFormat('dd/MM/yyyy').format(DateTime.parse(s.trim()));
|
||||
} catch (_) {
|
||||
return s.trim();
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,8 @@ import 'admin_detail_modal.dart';
|
||||
/// [rowLayout] : même disposition que la création de compte, ex. [2, 2, 1, 2] = ligne de 2, ligne de 2, plein largeur, ligne de 2.
|
||||
/// [rowFlex] : flex par index de ligne (optionnel). Ex. {3: [2, 5]} = 4e ligne : code postal étroit (2), ville large (5).
|
||||
class ValidationDetailSection extends StatelessWidget {
|
||||
final String title;
|
||||
/// Si null ou vide, pas de bandeau titre (gain de place vertical, ex. wizard AM).
|
||||
final String? title;
|
||||
final List<AdminDetailField> fields;
|
||||
|
||||
/// Nombre de champs par ligne (1 = plein largeur, 2 = deux côte à côte). Ex. [2, 2, 1, 2] pour identité.
|
||||
@ -16,7 +17,7 @@ class ValidationDetailSection extends StatelessWidget {
|
||||
|
||||
const ValidationDetailSection({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.title,
|
||||
required this.fields,
|
||||
this.rowLayout,
|
||||
this.rowFlex,
|
||||
@ -60,19 +61,22 @@ class ValidationDetailSection extends StatelessWidget {
|
||||
));
|
||||
}
|
||||
}
|
||||
final showTitle = title != null && title!.trim().isNotEmpty;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
if (showTitle) ...[
|
||||
Text(
|
||||
title!.trim(),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
...rows,
|
||||
],
|
||||
);
|
||||
|
||||
@ -155,70 +155,6 @@ class _PendingValidationWidgetState extends State<PendingValidationWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Ligne commune : icône | titre (+ sous-titre) | bouton Ouvrir.
|
||||
/// [titleWidget] remplace [title] si les deux sont fournis : priorité à [titleWidget].
|
||||
Widget _buildPendingRow({
|
||||
required IconData icon,
|
||||
String? title,
|
||||
Widget? titleWidget,
|
||||
String? subtitle,
|
||||
TextStyle? subtitleStyle,
|
||||
required VoidCallback onOpen,
|
||||
}) {
|
||||
assert(title != null || titleWidget != null);
|
||||
final titleChild = titleWidget ??
|
||||
Text(
|
||||
title!,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 14,
|
||||
),
|
||||
);
|
||||
final subStyle = subtitleStyle ??
|
||||
TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
);
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, color: Colors.grey.shade600, size: 28),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
titleChild,
|
||||
if (subtitle != null && subtitle.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle,
|
||||
style: subStyle,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onOpen,
|
||||
icon: const Icon(Icons.open_in_new, size: 18),
|
||||
label: const Text('Ouvrir'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Sous-titre AM : `email - date • tél. • CP ville` (plan affichage lignes À valider).
|
||||
String _amSubtitleLine(AppUser user) {
|
||||
@ -244,9 +180,9 @@ class _PendingValidationWidgetState extends State<PendingValidationWidget> {
|
||||
final numDossier = user.numeroDossier ?? '–';
|
||||
final nameBold =
|
||||
user.fullName.isNotEmpty ? user.fullName : (user.email.isNotEmpty ? user.email : '–');
|
||||
return _buildPendingRow(
|
||||
return _PendingValidationRow(
|
||||
icon: Icons.person_outline,
|
||||
titleWidget: Text.rich(
|
||||
title: Text.rich(
|
||||
TextSpan(
|
||||
style: const TextStyle(fontSize: 14, color: Colors.black87),
|
||||
children: [
|
||||
@ -320,9 +256,9 @@ class _PendingValidationWidgetState extends State<PendingValidationWidget> {
|
||||
Widget _buildFamilyCard(PendingFamily family) {
|
||||
final numDossier = family.numeroDossier ?? '–';
|
||||
final nameBold = family.libelle.isNotEmpty ? family.libelle : 'Famille';
|
||||
return _buildPendingRow(
|
||||
return _PendingValidationRow(
|
||||
icon: Icons.family_restroom_outlined,
|
||||
titleWidget: Text.rich(
|
||||
title: Text.rich(
|
||||
TextSpan(
|
||||
style: const TextStyle(fontSize: 14, color: Colors.black87),
|
||||
children: [
|
||||
@ -352,3 +288,111 @@ class _PendingValidationWidgetState extends State<PendingValidationWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ligne « À valider » : survol comme [AdminUserCard], icône « Ouvrir » visible au hover uniquement.
|
||||
class _PendingValidationRow extends StatefulWidget {
|
||||
final IconData icon;
|
||||
final Widget title;
|
||||
final String? subtitle;
|
||||
final TextStyle? subtitleStyle;
|
||||
final VoidCallback onOpen;
|
||||
|
||||
const _PendingValidationRow({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.subtitleStyle,
|
||||
required this.onOpen,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_PendingValidationRow> createState() => _PendingValidationRowState();
|
||||
}
|
||||
|
||||
class _PendingValidationRowState extends State<_PendingValidationRow> {
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final subStyle = widget.subtitleStyle ??
|
||||
TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
);
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
hoverColor: const Color(0x149CC5C0),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(widget.icon, color: Colors.grey.shade600, size: 28),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
widget.title,
|
||||
if (widget.subtitle != null &&
|
||||
widget.subtitle!.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.subtitle!,
|
||||
style: subStyle,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 52,
|
||||
child: Center(
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 120),
|
||||
opacity: _isHovered ? 1 : 0,
|
||||
child: IgnorePointer(
|
||||
ignoring: !_isHovered,
|
||||
child: IconButtonTheme(
|
||||
data: IconButtonThemeData(
|
||||
style: IconButton.styleFrom(
|
||||
padding: const EdgeInsets.all(0),
|
||||
minimumSize: const Size(48, 48),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: widget.onOpen,
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
iconSize: 34,
|
||||
tooltip: 'Ouvrir',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
||||
import 'package:p_tits_pas/utils/date_display_utils.dart';
|
||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||
import 'package:p_tits_pas/utils/nir_utils.dart';
|
||||
import 'package:p_tits_pas/models/user.dart';
|
||||
@ -74,29 +75,40 @@ class _ValidationAmWizardState extends State<ValidationAmWizard> {
|
||||
AdminDetailField(label: 'Ville', value: _v(u.ville)),
|
||||
];
|
||||
|
||||
/// Informations professionnelles : N° Agrément|Date agrément, NIR, Capacité|Places, Ville.
|
||||
List<AdminDetailField> _proFields(DossierAM d) => [
|
||||
AdminDetailField(label: 'N° Agrément', value: _v(d.numeroAgrement)),
|
||||
AdminDetailField(
|
||||
label: 'Date d’agrément',
|
||||
value: d.dateAgrement != null && d.dateAgrement!.trim().isNotEmpty
|
||||
? d.dateAgrement!.trim()
|
||||
: '–',
|
||||
),
|
||||
AdminDetailField(label: 'NIR', value: _formatNirForDisplay(d.nir)),
|
||||
AdminDetailField(
|
||||
label: 'Capacité max (enfants)',
|
||||
value: d.nbMaxEnfants != null ? d.nbMaxEnfants.toString() : '–',
|
||||
),
|
||||
AdminDetailField(
|
||||
label: 'Places disponibles',
|
||||
value: d.placesDisponibles != null
|
||||
? d.placesDisponibles.toString()
|
||||
: '–',
|
||||
),
|
||||
AdminDetailField(
|
||||
label: 'Ville de résidence', value: _v(d.villeResidence)),
|
||||
];
|
||||
/// Panneau photo + grille droite : NIR|naissance, ville|pays, agrément|date, capa|places.
|
||||
List<AdminDetailField> _photoProFields(DossierAM d) {
|
||||
final u = d.user;
|
||||
return [
|
||||
AdminDetailField(label: 'NIR', value: _formatNirForDisplay(d.nir)),
|
||||
AdminDetailField(
|
||||
label: 'Date de naissance',
|
||||
value: formatIsoDateFr(u.dateNaissance),
|
||||
),
|
||||
AdminDetailField(
|
||||
label: 'Ville de naissance',
|
||||
value: _v(u.lieuNaissanceVille),
|
||||
),
|
||||
AdminDetailField(
|
||||
label: 'Pays de naissance',
|
||||
value: _v(u.lieuNaissancePays),
|
||||
),
|
||||
AdminDetailField(label: 'N° Agrément', value: _v(d.numeroAgrement)),
|
||||
AdminDetailField(
|
||||
label: 'Date d’agrément',
|
||||
value: formatIsoDateFr(d.dateAgrement),
|
||||
),
|
||||
AdminDetailField(
|
||||
label: 'Capacité max (enfants)',
|
||||
value: d.nbMaxEnfants != null ? d.nbMaxEnfants.toString() : '–',
|
||||
),
|
||||
AdminDetailField(
|
||||
label: 'Places disponibles',
|
||||
value: d.placesDisponibles != null
|
||||
? d.placesDisponibles.toString()
|
||||
: '–',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
static const List<int> _personalRowLayout = [2, 2, 1, 2];
|
||||
static const Map<int, List<int>> _personalRowFlex = {
|
||||
@ -119,15 +131,6 @@ class _ValidationAmWizardState extends State<ValidationAmWizard> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'Photo de profil',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
@ -264,7 +267,7 @@ class _ValidationAmWizardState extends State<ValidationAmWizard> {
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
||||
child: ValidationDetailSection(
|
||||
title: 'Informations personnelles',
|
||||
title: 'Identité et coordonnées',
|
||||
fields: _personalFields(u),
|
||||
rowLayout: _personalRowLayout,
|
||||
rowFlex: _personalRowFlex,
|
||||
@ -280,8 +283,7 @@ class _ValidationAmWizardState extends State<ValidationAmWizard> {
|
||||
builder: (context, c) {
|
||||
final maxRowW = c.maxWidth;
|
||||
final maxRowH = c.maxHeight;
|
||||
// Titre « Photo de profil » + espacement (~52 px) : hauteur dispo pour le cadre photo.
|
||||
const photoHeaderH = 52.0;
|
||||
const photoHeaderH = 0.0;
|
||||
final bodyH = (maxRowH - photoHeaderH).clamp(0.0, double.infinity);
|
||||
final idealPhotoW =
|
||||
bodyH * _idPhotoAspectRatio + 16; // marge approx. cadre clair
|
||||
@ -307,14 +309,9 @@ class _ValidationAmWizardState extends State<ValidationAmWizard> {
|
||||
constraints: BoxConstraints(
|
||||
minWidth: constraints.maxWidth),
|
||||
child: ValidationDetailSection(
|
||||
title: 'Informations professionnelles',
|
||||
fields: _proFields(d),
|
||||
rowLayout: const [
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
1
|
||||
], // N° Agrément|Date agrément, NIR, Capacité|Places, Ville
|
||||
title: 'Dossier professionnel',
|
||||
fields: _photoProFields(d),
|
||||
rowLayout: const [2, 2, 2, 2],
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -333,12 +330,13 @@ class _ValidationAmWizardState extends State<ValidationAmWizard> {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
const Text(
|
||||
'Présentation',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
||||
import 'package:p_tits_pas/utils/date_display_utils.dart';
|
||||
import 'package:p_tits_pas/utils/phone_utils.dart';
|
||||
import 'package:p_tits_pas/services/user_service.dart';
|
||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||
@ -106,15 +105,8 @@ class _ValidationFamilyWizardState extends State<ValidationFamilyWizard> {
|
||||
(s != null && s.trim().isNotEmpty) ? s.trim() : 'Non défini';
|
||||
|
||||
/// Date de naissance en jour/mois/année (dd/MM/yyyy).
|
||||
static String _formatBirthDate(String? s) {
|
||||
if (s == null || s.trim().isEmpty) return 'Non défini';
|
||||
try {
|
||||
final d = DateTime.parse(s.trim());
|
||||
return DateFormat('dd/MM/yyyy').format(d);
|
||||
} catch (_) {
|
||||
return s.trim();
|
||||
}
|
||||
}
|
||||
static String _formatBirthDate(String? s) =>
|
||||
formatIsoDateFr(s, ifEmpty: 'Non défini');
|
||||
|
||||
/// Même ordre et disposition que le formulaire de création (Nom/Prénom, Tél/Email, Adresse, CP/Ville).
|
||||
List<AdminDetailField> _parentFields(ParentDossier p) => [
|
||||
@ -344,12 +336,6 @@ 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),
|
||||
@ -569,7 +555,7 @@ class _ValidationFamilyWizardState extends State<ValidationFamilyWizard> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'Présentation / Motivation',
|
||||
'Présentation',
|
||||
style: TextStyle(
|
||||
fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87),
|
||||
),
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/services/api/tokenService.dart';
|
||||
|
||||
@ -43,12 +42,6 @@ class _AuthNetworkImageState extends State<AuthNetworkImage> {
|
||||
final isPublic = AuthNetworkImage.isPublicUploadUrl(widget.url);
|
||||
_headersFuture =
|
||||
isPublic ? Future<Map<String, String>?>.value(null) : _loadHeaders();
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[PetitsPas/image] préparation chargement url=${widget.url} | '
|
||||
'uploadPublic=$isPublic | avecBearer=${!isPublic}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, String>?> _loadHeaders() async {
|
||||
@ -59,11 +52,6 @@ class _AuthNetworkImageState extends State<AuthNetworkImage> {
|
||||
|
||||
ImageErrorWidgetBuilder _wrapErrorBuilder() {
|
||||
return (BuildContext context, Object error, StackTrace? stackTrace) {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
'[PetitsPas/image] ❌ échec chargement url=${widget.url} | erreur=$error',
|
||||
);
|
||||
}
|
||||
final inner = widget.errorBuilder;
|
||||
if (inner != null) {
|
||||
return inner(context, error, stackTrace);
|
||||
@ -112,9 +100,6 @@ class _AuthNetworkImageState extends State<AuthNetworkImage> {
|
||||
);
|
||||
}
|
||||
final headers = snapshot.data;
|
||||
if (kDebugMode && headers != null) {
|
||||
debugPrint('[PetitsPas/image] requête avec en-tête Authorization (Bearer présent)');
|
||||
}
|
||||
return Image.network(
|
||||
widget.url,
|
||||
width: widget.width,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user