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>
266 lines
7.4 KiB
Dart
266 lines
7.4 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;
|
||
|
||
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);
|
||
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: const EdgeInsets.only(bottom: 12),
|
||
child: rowFields.first,
|
||
));
|
||
} else {
|
||
rows.add(Padding(
|
||
padding: const EdgeInsets.only(bottom: 12),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
for (int i = 0; i < rowFields.length; i++) ...[
|
||
if (i > 0) const SizedBox(width: 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: const TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.black87,
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
],
|
||
...rows,
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 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 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;
|
||
|
||
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;
|
||
|
||
const ValidationReadOnlyField({
|
||
super.key,
|
||
required this.value,
|
||
this.maxLines = 1,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||
decoration: ValidationFieldDecoration.container(),
|
||
child: Text(
|
||
value,
|
||
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
||
maxLines: maxLines,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
);
|
||
}
|
||
}
|