feat(#140): fiches admin famille — IdentityBlock, parsing enfants et avatars web

Extrait IdentityBlock réutilisable, aligne les modales parent/enfant sur le style validation, corrige le parsing parentChildren et les URLs /uploads en Flutter web.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-21 17:42:00 +02:00
co-authored by Cursor
parent 1dddc67933
commit 8494341b56
11 changed files with 1075 additions and 458 deletions
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'admin_detail_modal.dart';
/// Bloc type formulaire (titre de section + champs read-only) pour les modales de validation.
@@ -23,6 +24,39 @@ class ValidationDetailSection extends StatelessWidget {
this.rowFlex,
});
@override
Widget build(BuildContext context) {
return ValidationFormGrid(
title: title,
rowLayout: rowLayout,
rowFlex: rowFlex,
fields: fields
.map(
(f) => ValidationLabeledField(
label: f.label,
field: ValidationReadOnlyField(value: f.value),
),
)
.toList(),
);
}
}
/// Grille label/champ réutilisable (validation, fiches admin).
class ValidationFormGrid extends StatelessWidget {
final String? title;
final List<ValidationLabeledField> fields;
final List<int>? rowLayout;
final Map<int, List<int>>? rowFlex;
const ValidationFormGrid({
super.key,
this.title,
required this.fields,
this.rowLayout,
this.rowFlex,
});
@override
Widget build(BuildContext context) {
final layout = rowLayout ?? List.filled(fields.length, 1);
@@ -39,7 +73,7 @@ class ValidationDetailSection extends StatelessWidget {
if (count == 1) {
rows.add(Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _buildFieldCell(rowFields.first),
child: rowFields.first,
));
} else {
rows.add(Padding(
@@ -53,7 +87,7 @@ class ValidationDetailSection extends StatelessWidget {
flex: (flexForRow != null && i < flexForRow.length)
? flexForRow[i]
: 1,
child: _buildFieldCell(rowFields[i]),
child: rowFields[i],
),
],
],
@@ -81,14 +115,62 @@ class ValidationDetailSection extends StatelessWidget {
],
);
}
}
Widget _buildFieldCell(AdminDetailField field) {
/// Décoration commune lecture seule / édition (modales validation, fiches admin).
class ValidationFieldDecoration {
ValidationFieldDecoration._();
static InputDecoration input({String? hint}) {
return InputDecoration(
isDense: true,
filled: true,
fillColor: Colors.grey.shade50,
hintText: hint,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide(color: Colors.grey.shade500),
),
);
}
static BoxDecoration container() {
return BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.grey.shade300),
);
}
}
/// Libellé au-dessus dun champ (même typo que [ValidationDetailSection]).
class ValidationLabeledField extends StatelessWidget {
final String label;
final Widget field;
const ValidationLabeledField({
super.key,
required this.label,
required this.field,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
field.label,
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
@@ -96,13 +178,66 @@ class ValidationDetailSection extends StatelessWidget {
),
),
const SizedBox(height: 4),
ValidationReadOnlyField(value: field.value),
field,
],
);
}
}
/// Champ texte en lecture seule, style formulaire (fond gris léger, bordure). Réutilisable en éditable plus tard.
/// Champ texte éditable, même rendu que [ValidationReadOnlyField].
class ValidationEditableField extends StatelessWidget {
final TextEditingController controller;
final TextInputType keyboardType;
final List<TextInputFormatter>? inputFormatters;
final String? hintText;
final int maxLines;
const ValidationEditableField({
super.key,
required this.controller,
this.keyboardType = TextInputType.text,
this.inputFormatters,
this.hintText,
this.maxLines = 1,
});
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
maxLines: maxLines,
style: const TextStyle(color: Colors.black87, fontSize: 14),
decoration: ValidationFieldDecoration.input(hint: hintText),
);
}
}
/// Grille label/champ éditable (délègue à [ValidationFormGrid]).
class ValidationEditableSection extends StatelessWidget {
final List<ValidationLabeledField> fields;
final List<int>? rowLayout;
final Map<int, List<int>>? rowFlex;
const ValidationEditableSection({
super.key,
required this.fields,
this.rowLayout,
this.rowFlex,
});
@override
Widget build(BuildContext context) {
return ValidationFormGrid(
rowLayout: rowLayout,
rowFlex: rowFlex,
fields: fields,
);
}
}
/// Champ texte en lecture seule, style formulaire (fond gris léger, bordure).
class ValidationReadOnlyField extends StatelessWidget {
final String value;
final int? maxLines;
@@ -118,11 +253,7 @@ class ValidationReadOnlyField extends StatelessWidget {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.grey.shade300),
),
decoration: ValidationFieldDecoration.container(),
child: Text(
value,
style: const TextStyle(color: Colors.black87, fontSize: 14),