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 fields; /// Nombre de champs par ligne (1 = plein largeur, 2 = deux côte à côte). Ex. [2, 2, 1, 2] pour identité. final List? rowLayout; /// Flex par ligne (index de ligne -> [flex1, flex2, ...]). Ex. {3: [2, 5]} pour Code postal | Ville. final Map>? 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 fields; final List? rowLayout; final Map>? 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 = []; 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? 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 fields; final List? rowLayout; final Map>? 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 createState() => _ValidationReadOnlyFieldState(); } class _ValidationReadOnlyFieldState extends State { 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, ), ); } }