Aligne le front sur les statuts enfant back (garde/sans_garde), refond l’onglet Enfants accueillis en grille 2×2 avec correction des places, et unifie les champs validation éditables/lecture seule. Co-authored-by: Cursor <cursoragent@cursor.com>
407 lines
12 KiB
Dart
407 lines
12 KiB
Dart
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.
|
||
/// [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 {
|
||
/// 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é.
|
||
final List<int>? rowLayout;
|
||
|
||
/// Flex par ligne (index de ligne -> [flex1, flex2, ...]). Ex. {3: [2, 5]} pour Code postal | Ville.
|
||
final Map<int, List<int>>? rowFlex;
|
||
|
||
const ValidationDetailSection({
|
||
super.key,
|
||
this.title,
|
||
required this.fields,
|
||
this.rowLayout,
|
||
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;
|
||
final bool compact;
|
||
|
||
const ValidationFormGrid({
|
||
super.key,
|
||
this.title,
|
||
required this.fields,
|
||
this.rowLayout,
|
||
this.rowFlex,
|
||
this.compact = false,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final layout = rowLayout ?? List.filled(fields.length, 1);
|
||
int index = 0;
|
||
int rowIndex = 0;
|
||
final rows = <Widget>[];
|
||
for (final count in layout) {
|
||
if (index >= fields.length) break;
|
||
final rowFields = fields.skip(index).take(count).toList();
|
||
index += count;
|
||
if (rowFields.isEmpty) continue;
|
||
final flexForRow = rowFlex?[rowIndex];
|
||
rowIndex++;
|
||
if (count == 1) {
|
||
rows.add(Padding(
|
||
padding: EdgeInsets.only(bottom: compact ? 8 : 12),
|
||
child: rowFields.first,
|
||
));
|
||
} else {
|
||
rows.add(Padding(
|
||
padding: EdgeInsets.only(bottom: compact ? 8 : 12),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
for (int i = 0; i < rowFields.length; i++) ...[
|
||
if (i > 0) SizedBox(width: compact ? 12 : 16),
|
||
Expanded(
|
||
flex: (flexForRow != null && i < flexForRow.length)
|
||
? flexForRow[i]
|
||
: 1,
|
||
child: rowFields[i],
|
||
),
|
||
],
|
||
],
|
||
),
|
||
));
|
||
}
|
||
}
|
||
final showTitle = title != null && title!.trim().isNotEmpty;
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
if (showTitle) ...[
|
||
Text(
|
||
title!.trim(),
|
||
style: TextStyle(
|
||
fontSize: compact ? 15 : 16,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.black87,
|
||
),
|
||
),
|
||
SizedBox(height: compact ? 8 : 12),
|
||
],
|
||
...rows,
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Décoration commune lecture seule / édition (modales validation, fiches admin).
|
||
class ValidationFieldDecoration {
|
||
ValidationFieldDecoration._();
|
||
|
||
static InputDecoration input({String? hint, bool compact = false}) {
|
||
return InputDecoration(
|
||
isDense: true,
|
||
filled: true,
|
||
fillColor: Colors.grey.shade50,
|
||
hintText: hint,
|
||
contentPadding: EdgeInsets.symmetric(
|
||
horizontal: compact ? 10 : 12,
|
||
vertical: compact ? 7 : 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 InputDecoration readOnly({bool error = false, bool compact = false}) {
|
||
final borderColor = error ? Colors.red.shade400 : Colors.grey.shade300;
|
||
final fillColor = error ? Colors.red.shade50 : Colors.grey.shade50;
|
||
return input(compact: compact).copyWith(
|
||
filled: true,
|
||
fillColor: fillColor,
|
||
enabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
borderSide: BorderSide(color: borderColor),
|
||
),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
borderSide: BorderSide(color: borderColor),
|
||
),
|
||
);
|
||
}
|
||
|
||
static BoxDecoration container({bool error = false}) {
|
||
return BoxDecoration(
|
||
color: error ? Colors.red.shade50 : Colors.grey.shade50,
|
||
borderRadius: BorderRadius.circular(6),
|
||
border: Border.all(
|
||
color: error ? Colors.red.shade400 : Colors.grey.shade300,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Libellé au-dessus d’un 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(
|
||
label,
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: Colors.grey.shade700,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
field,
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 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;
|
||
final bool compact;
|
||
|
||
const ValidationEditableField({
|
||
super.key,
|
||
required this.controller,
|
||
this.keyboardType = TextInputType.text,
|
||
this.inputFormatters,
|
||
this.hintText,
|
||
this.maxLines = 1,
|
||
this.compact = false,
|
||
});
|
||
|
||
static const double _compactFieldHeight = 34;
|
||
|
||
static BoxDecoration _compactDecoration({bool error = false}) {
|
||
return BoxDecoration(
|
||
color: error ? Colors.red.shade50 : Colors.grey.shade50,
|
||
borderRadius: BorderRadius.circular(6),
|
||
border: Border.all(
|
||
color: error ? Colors.red.shade400 : Colors.grey.shade300,
|
||
),
|
||
);
|
||
}
|
||
|
||
static InputDecoration _compactInputDecoration({String? hint}) {
|
||
return InputDecoration(
|
||
isDense: true,
|
||
filled: false,
|
||
hintText: hint,
|
||
border: InputBorder.none,
|
||
enabledBorder: InputBorder.none,
|
||
focusedBorder: InputBorder.none,
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 9),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (maxLines > 1) {
|
||
return TextField(
|
||
controller: controller,
|
||
keyboardType: keyboardType,
|
||
inputFormatters: inputFormatters,
|
||
maxLines: maxLines,
|
||
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
||
decoration: ValidationFieldDecoration.input(hint: hintText),
|
||
);
|
||
}
|
||
if (!compact) {
|
||
return TextField(
|
||
controller: controller,
|
||
keyboardType: keyboardType,
|
||
inputFormatters: inputFormatters,
|
||
maxLines: 1,
|
||
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
||
decoration: ValidationFieldDecoration.input(hint: hintText),
|
||
);
|
||
}
|
||
return SizedBox(
|
||
height: _compactFieldHeight,
|
||
child: DecoratedBox(
|
||
decoration: _compactDecoration(),
|
||
child: TextField(
|
||
controller: controller,
|
||
keyboardType: keyboardType,
|
||
inputFormatters: inputFormatters,
|
||
maxLines: 1,
|
||
textAlignVertical: TextAlignVertical.center,
|
||
style: const TextStyle(
|
||
color: Colors.black87,
|
||
fontSize: 13,
|
||
height: 1.0,
|
||
),
|
||
decoration: _compactInputDecoration(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;
|
||
final bool compact;
|
||
|
||
const ValidationEditableSection({
|
||
super.key,
|
||
required this.fields,
|
||
this.rowLayout,
|
||
this.rowFlex,
|
||
this.compact = false,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return ValidationFormGrid(
|
||
rowLayout: rowLayout,
|
||
rowFlex: rowFlex,
|
||
compact: compact,
|
||
fields: fields,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Champ texte en lecture seule, même coque [TextField] que [ValidationEditableField].
|
||
class ValidationReadOnlyField extends StatefulWidget {
|
||
final String value;
|
||
final int? maxLines;
|
||
final bool compact;
|
||
final bool error;
|
||
|
||
const ValidationReadOnlyField({
|
||
super.key,
|
||
required this.value,
|
||
this.maxLines = 1,
|
||
this.compact = false,
|
||
this.error = false,
|
||
});
|
||
|
||
@override
|
||
State<ValidationReadOnlyField> createState() => _ValidationReadOnlyFieldState();
|
||
}
|
||
|
||
class _ValidationReadOnlyFieldState extends State<ValidationReadOnlyField> {
|
||
late final TextEditingController _controller;
|
||
|
||
static const double _compactFieldHeight = 34;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_controller = TextEditingController(text: widget.value);
|
||
}
|
||
|
||
@override
|
||
void didUpdateWidget(ValidationReadOnlyField oldWidget) {
|
||
super.didUpdateWidget(oldWidget);
|
||
if (oldWidget.value != widget.value) {
|
||
_controller.text = widget.value;
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_controller.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (!widget.compact && widget.maxLines == 1) {
|
||
return TextField(
|
||
controller: _controller,
|
||
readOnly: true,
|
||
enableInteractiveSelection: false,
|
||
style: TextStyle(
|
||
color: widget.error ? Colors.red.shade800 : Colors.black87,
|
||
fontSize: 14,
|
||
fontWeight: widget.error ? FontWeight.w600 : null,
|
||
),
|
||
decoration: ValidationFieldDecoration.readOnly(error: widget.error),
|
||
);
|
||
}
|
||
|
||
return Container(
|
||
width: double.infinity,
|
||
height: widget.compact && widget.maxLines == 1 ? _compactFieldHeight : null,
|
||
alignment: widget.compact ? Alignment.centerLeft : null,
|
||
padding: EdgeInsets.symmetric(
|
||
horizontal: widget.compact ? 10 : 12,
|
||
vertical: widget.compact ? 7 : 10,
|
||
),
|
||
decoration: ValidationFieldDecoration.container(error: widget.error),
|
||
child: Text(
|
||
widget.value,
|
||
style: TextStyle(
|
||
color: widget.error ? Colors.red.shade800 : Colors.black87,
|
||
fontSize: widget.compact ? 13 : 14,
|
||
height: widget.compact ? 1.0 : null,
|
||
fontWeight: widget.error ? FontWeight.w600 : null,
|
||
),
|
||
maxLines: widget.maxLines,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
);
|
||
}
|
||
}
|