petitspas/frontend/lib/widgets/admin/common/admin_am_edit_modal.dart
Julien Martin d03a8e6c8b fix(#131): activer Sauvegarder sur statut, enfants et champs AM.
Compare l'état de la fiche à un snapshot initial (liste enfants, statut, places, formulaire) et corrige la gélule statut pour les valeurs hors liste.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 23:11:24 +02:00

894 lines
29 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
import 'package:p_tits_pas/models/enfant_admin_model.dart';
import 'package:p_tits_pas/models/parent_child_summary.dart';
import 'package:p_tits_pas/utils/am_vigilance.dart';
import 'package:p_tits_pas/utils/date_display_utils.dart';
import 'package:p_tits_pas/utils/nir_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/widgets/admin/common/admin_am_children_capacity_grid.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_am_photo_frame.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart';
import 'package:p_tits_pas/widgets/admin/common/admin_status_capsule.dart';
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
import 'package:p_tits_pas/widgets/common/identity_block.dart';
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
/// Fiche AM éditable (ticket #131) — identité | fiche pro (photo) | enfants.
class AdminAmEditModal extends StatefulWidget {
final AssistanteMaternelleModel assistante;
final VoidCallback? onSaved;
const AdminAmEditModal({
super.key,
required this.assistante,
this.onSaved,
});
@override
State<AdminAmEditModal> createState() => _AdminAmEditModalState();
}
class _AdminAmEditModalState extends State<AdminAmEditModal>
with SingleTickerProviderStateMixin {
late final TabController _tabCtrl;
late final TextEditingController _nomCtrl;
late final TextEditingController _prenomCtrl;
late final TextEditingController _emailCtrl;
late final TextEditingController _telCtrl;
late final TextEditingController _adresseCtrl;
late final TextEditingController _villeCtrl;
late final TextEditingController _cpCtrl;
late final TextEditingController _agrementCtrl;
late final TextEditingController _nirCtrl;
late final TextEditingController _dateNaissanceCtrl;
late final TextEditingController _lieuNaissanceVilleCtrl;
late final TextEditingController _lieuNaissancePaysCtrl;
late final TextEditingController _dateAgrementCtrl;
late final TextEditingController _capaciteCtrl;
late String _statut;
late bool _disponible;
late int? _placesAvailable;
late List<ParentChildSummary> _children;
bool _saving = false;
bool _dirty = false;
late String _baseStatut;
late bool _baseDisponible;
late int? _basePlacesAvailable;
late List<String> _baseChildIds;
late String _baseNom;
late String _basePrenom;
late String _baseEmail;
late String _baseTelephone;
late String _baseAdresse;
late String _baseVille;
late String _baseCodePostal;
late String _baseAgrement;
late String _baseNir;
late String? _baseDateNaissanceIso;
late String _baseLieuNaissanceVille;
late String _baseLieuNaissancePays;
late String? _baseAgreementDateIso;
late int? _baseCapacite;
static const double _modalWidth = 930;
static const double _photoProGap = 24;
static const double _proColumnMinWidth = 260;
static const double _photoColumnMinWidth = 160;
static const double _proTabHeight = 300;
static const List<int> _photoProRowLayout = [2, 2, 2];
/// Hauteur onglet enfants : champs + titre + grille 2×2 (+ alerte places si besoin).
double _childrenTabHeight() {
const capacityFields = 72.0;
const titleSection = 40.0;
const inconsistencyExtra = 46.0;
var h = capacityFields +
titleSection +
AdminAmChildrenCapacityGrid.fixedHeight;
if (_placesInconsistent()) h += inconsistencyExtra;
return h + 4;
}
double _tabViewHeight(int index) {
switch (index) {
case 1:
return _proTabHeight;
case 2:
return _childrenTabHeight();
default:
return 292;
}
}
@override
void initState() {
super.initState();
_tabCtrl = TabController(length: 3, vsync: this);
final u = widget.assistante.user;
final am = widget.assistante;
_nomCtrl = TextEditingController(text: u.nom ?? '');
_prenomCtrl = TextEditingController(text: u.prenom ?? '');
_emailCtrl = TextEditingController(text: u.email);
_telCtrl = TextEditingController(
text: formatPhoneForDisplay(u.telephone ?? ''),
);
_adresseCtrl = TextEditingController(text: u.adresse ?? '');
_villeCtrl = TextEditingController(text: u.ville ?? '');
_cpCtrl = TextEditingController(text: u.codePostal ?? '');
_agrementCtrl = TextEditingController(text: am.approvalNumber ?? '');
_nirCtrl = TextEditingController(text: _formatNirDisplay());
_dateNaissanceCtrl = TextEditingController(
text: formatIsoDateFr(u.dateNaissance, ifEmpty: ''),
);
_lieuNaissanceVilleCtrl = TextEditingController(
text: u.lieuNaissanceVille ?? '',
);
_lieuNaissancePaysCtrl = TextEditingController(
text: u.lieuNaissancePays ?? '',
);
_dateAgrementCtrl = TextEditingController(
text: formatIsoDateFr(am.agreementDate, ifEmpty: ''),
);
_capaciteCtrl = TextEditingController(
text: am.maxChildren?.toString() ?? '',
);
_statut = u.statut ?? 'en_attente';
_disponible = am.available ?? true;
_placesAvailable = am.placesAvailable;
_children = List.of(am.children);
for (final c in [
_nomCtrl,
_prenomCtrl,
_emailCtrl,
_telCtrl,
_adresseCtrl,
_villeCtrl,
_cpCtrl,
_agrementCtrl,
_nirCtrl,
_dateNaissanceCtrl,
_lieuNaissanceVilleCtrl,
_lieuNaissancePaysCtrl,
_dateAgrementCtrl,
_capaciteCtrl,
]) {
c.addListener(_markDirty);
}
_capaciteCtrl.addListener(_onCapacityChanged);
_nomCtrl.addListener(_onNameFieldChanged);
_prenomCtrl.addListener(_onNameFieldChanged);
_tabCtrl.addListener(_onTabChanged);
_captureBaseline();
_reloadChildren();
}
void _captureBaseline() {
_baseStatut = _statut;
_baseDisponible = _disponible;
_basePlacesAvailable = _placesAvailable;
_baseChildIds = _children.map((c) => c.id).toList()..sort();
_baseNom = _nomCtrl.text.trim();
_basePrenom = _prenomCtrl.text.trim();
_baseEmail = _emailCtrl.text.trim();
_baseTelephone = normalizePhone(_telCtrl.text);
_baseAdresse = _adresseCtrl.text.trim();
_baseVille = _villeCtrl.text.trim();
_baseCodePostal = _cpCtrl.text.trim();
_baseAgrement = _agrementCtrl.text.trim();
_baseNir = nirToRaw(_nirCtrl.text);
_baseDateNaissanceIso = _frDateToIso(_dateNaissanceCtrl.text);
_baseLieuNaissanceVille = _lieuNaissanceVilleCtrl.text.trim();
_baseLieuNaissancePays = _lieuNaissancePaysCtrl.text.trim();
_baseAgreementDateIso = _frDateToIso(_dateAgrementCtrl.text);
_baseCapacite = _capaciteMax();
}
bool _hasUnsavedChanges() {
if (_statut != _baseStatut) return true;
if (_disponible != _baseDisponible) return true;
if (_placesAvailable != _basePlacesAvailable) return true;
final childIds = _children.map((c) => c.id).toList()..sort();
if (!listEquals(childIds, _baseChildIds)) return true;
if (_nomCtrl.text.trim() != _baseNom) return true;
if (_prenomCtrl.text.trim() != _basePrenom) return true;
if (_emailCtrl.text.trim() != _baseEmail) return true;
if (normalizePhone(_telCtrl.text) != _baseTelephone) return true;
if (_adresseCtrl.text.trim() != _baseAdresse) return true;
if (_villeCtrl.text.trim() != _baseVille) return true;
if (_cpCtrl.text.trim() != _baseCodePostal) return true;
if (_agrementCtrl.text.trim() != _baseAgrement) return true;
if (nirToRaw(_nirCtrl.text) != _baseNir) return true;
if (_frDateToIso(_dateNaissanceCtrl.text) != _baseDateNaissanceIso) {
return true;
}
if (_lieuNaissanceVilleCtrl.text.trim() != _baseLieuNaissanceVille) {
return true;
}
if (_lieuNaissancePaysCtrl.text.trim() != _baseLieuNaissancePays) {
return true;
}
if (_frDateToIso(_dateAgrementCtrl.text) != _baseAgreementDateIso) {
return true;
}
if (_capaciteMax() != _baseCapacite) return true;
return false;
}
void _syncDirtyState() {
final next = _hasUnsavedChanges();
if (next != _dirty) setState(() => _dirty = next);
}
void _onTabChanged() {
if (!_tabCtrl.indexIsChanging) setState(() {});
}
void _onNameFieldChanged() => setState(() {});
void _onCapacityChanged() {
setState(() {});
_syncDirtyState();
}
@override
void dispose() {
_tabCtrl.dispose();
for (final c in [
_nomCtrl,
_prenomCtrl,
_emailCtrl,
_telCtrl,
_adresseCtrl,
_villeCtrl,
_cpCtrl,
_agrementCtrl,
_nirCtrl,
_dateNaissanceCtrl,
_lieuNaissanceVilleCtrl,
_lieuNaissancePaysCtrl,
_dateAgrementCtrl,
_capaciteCtrl,
]) {
c.dispose();
}
super.dispose();
}
void _markDirty() => _syncDirtyState();
String _headerTitle() {
final fn = _prenomCtrl.text.trim();
final ln = _nomCtrl.text.trim();
if (fn.isEmpty && ln.isEmpty) {
final fallback = widget.assistante.user.fullName.trim();
return fallback.isNotEmpty ? fallback : 'Assistante maternelle';
}
return '$fn $ln'.trim();
}
String? _headerSubtitle() {
final parts = <String>[];
final zone = (widget.assistante.residenceCity ?? '').trim();
if (zone.isNotEmpty) parts.add('Zone : $zone');
final agrement = _agrementCtrl.text.trim();
if (agrement.isNotEmpty) parts.add('Agrément : $agrement');
final dossier = widget.assistante.user.numeroDossier?.trim();
if (dossier != null && dossier.isNotEmpty) {
parts.add('Dossier $dossier');
}
if (parts.isEmpty) return null;
return parts.join(' · ');
}
String _formatNirDisplay() {
final raw = widget.assistante.nir?.trim() ?? '';
if (raw.isEmpty) return '';
final digits = nirToRaw(raw).toUpperCase();
return digits.length == 15 ? formatNir(digits) : raw;
}
String? _frDateToIso(String text) => parseFrDateToIso(text);
int? _capaciteMax() => _parseIntField(_capaciteCtrl);
int? _computedPlacesAvailable() => amExpectedPlacesAvailable(
maxChildren: _capaciteMax(),
childrenCount: _children.length,
);
bool _placesInconsistent() => amHasPlacesInconsistency(
maxChildren: _capaciteMax(),
placesAvailable: _placesAvailable,
childrenCount: _children.length,
);
String _placesDisplayValue() {
if (_placesAvailable != null) return _placesAvailable.toString();
return '';
}
void _applyPlacesCorrection() {
final expected = _computedPlacesAvailable();
if (expected == null) return;
setState(() => _placesAvailable = expected);
_syncDirtyState();
}
String? _placesInconsistencyMessage() {
if (!_placesInconsistent()) return null;
final stored = _placesAvailable;
final expected = _computedPlacesAvailable();
final storedLabel = stored?.toString() ?? 'non renseigné';
final expectedLabel = expected?.toString() ?? '';
return 'Incohérence : l\'AM déclare $storedLabel place(s) disponible(s), '
'le calcul (capacité ${_capaciteMax() ?? ''} ${_children.length} '
'enfant(s) rattaché(s)) donne $expectedLabel.';
}
int? _parseIntField(TextEditingController c) {
final t = c.text.trim();
if (t.isEmpty) return null;
return int.tryParse(t);
}
Future<void> _save() async {
if (!_dirty) return;
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Confirmer'),
content: const Text(
'Enregistrer les modifications de la fiche assistante maternelle ?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Sauvegarder'),
),
],
),
);
if (confirmed != true || !mounted) return;
setState(() => _saving = true);
try {
await UserService.updateAmFiche(
amUserId: widget.assistante.user.id,
body: {
'nom': _nomCtrl.text.trim(),
'prenom': _prenomCtrl.text.trim(),
'email': _emailCtrl.text.trim(),
'telephone': normalizePhone(_telCtrl.text),
'adresse': _adresseCtrl.text.trim(),
'ville': _villeCtrl.text.trim(),
'code_postal': _cpCtrl.text.trim(),
'statut': _statut,
'approval_number': _agrementCtrl.text.trim(),
'nir': nirToRaw(_nirCtrl.text),
if (_frDateToIso(_dateNaissanceCtrl.text) != null)
'date_naissance': _frDateToIso(_dateNaissanceCtrl.text),
'lieu_naissance_ville': _lieuNaissanceVilleCtrl.text.trim(),
'lieu_naissance_pays': _lieuNaissancePaysCtrl.text.trim(),
if (_frDateToIso(_dateAgrementCtrl.text) != null)
'agreement_date': _frDateToIso(_dateAgrementCtrl.text),
if (_capaciteMax() != null) 'max_children': _capaciteMax(),
if (_placesAvailable != widget.assistante.placesAvailable)
'places_available': _placesAvailable,
'available': _disponible,
},
);
if (!mounted) return;
setState(() {
_dirty = false;
_saving = false;
});
_captureBaseline();
widget.onSaved?.call();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Fiche assistante maternelle enregistrée')),
);
} catch (e) {
if (!mounted) return;
setState(() => _saving = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
);
}
}
Future<void> _reloadChildren() async {
try {
final refreshed =
await UserService.getAssistanteMaternelle(widget.assistante.user.id);
final all = await UserService.getEnfants();
final byId = {for (final e in all) e.id: e};
final kids = refreshed.children.map((c) {
final full = byId[c.id];
if (full != null) return ParentChildSummary.fromEnfant(full);
return c;
}).toList();
if (!mounted) return;
setState(() => _children = kids);
_syncDirtyState();
} catch (_) {}
}
Future<void> _openChild(ParentChildSummary child) async {
try {
final enfant = await UserService.getEnfant(child.id);
if (!mounted) return;
await showDialog<void>(
context: context,
builder: (ctx) => AdminChildDetailModal(
enfant: enfant,
onSaved: _reloadChildren,
),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
);
}
}
Future<void> _detachChild(ParentChildSummary child) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Détacher l\'enfant'),
content: Text(
'Retirer ${child.fullName} de la fiche de cette assistante ?\n'
'(L\'enfant ne sera pas supprimé.)',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
style: ValidationModalTheme.primaryElevatedStyle,
child: const Text('Détacher'),
),
],
),
);
if (confirmed != true || !mounted) return;
try {
await UserService.detachEnfantFromAm(
amUserId: widget.assistante.user.id,
enfantId: child.id,
);
if (!mounted) return;
await _reloadChildren();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Enfant détaché')),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
);
}
}
Future<void> _attachChild() async {
List<EnfantAdminModel> all;
try {
all = await UserService.getEnfants();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
);
return;
}
final linkedIds = _children.map((c) => c.id).toSet();
final candidates = all.where((e) => !linkedIds.contains(e.id)).toList();
if (candidates.isEmpty) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Aucun enfant disponible à rattacher')),
);
return;
}
if (!mounted) return;
final selected = await showDialog<EnfantAdminModel>(
context: context,
builder: (ctx) => SimpleDialog(
title: const Text('Rattacher un enfant'),
children: candidates
.map(
(e) => SimpleDialogOption(
onPressed: () => Navigator.pop(ctx, e),
child: Text(e.fullName),
),
)
.toList(),
),
);
if (selected == null || !mounted) return;
try {
await UserService.attachEnfantToAm(
amUserId: widget.assistante.user.id,
enfantId: selected.id,
);
if (!mounted) return;
await _reloadChildren();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Enfant rattaché')),
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
);
}
}
Widget _identityTab() {
return SingleChildScrollView(
child: IdentityBlock.editable(
title: 'Identité et coordonnées',
nomController: _nomCtrl,
prenomController: _prenomCtrl,
telephoneController: _telCtrl,
emailController: _emailCtrl,
adresseController: _adresseCtrl,
codePostalController: _cpCtrl,
villeController: _villeCtrl,
),
);
}
Widget _proFieldsGrid() {
return ValidationFormGrid(
title: 'Dossier professionnel',
rowLayout: _photoProRowLayout,
fields: [
ValidationLabeledField(
label: 'NIR',
field: ValidationEditableField(
controller: _nirCtrl,
hintText: '15 chiffres',
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[\d\s]')),
],
),
),
ValidationLabeledField(
label: 'Date de naissance',
field: ValidationEditableField(
controller: _dateNaissanceCtrl,
hintText: 'jj/mm/aaaa',
),
),
ValidationLabeledField(
label: 'Ville de naissance',
field: ValidationEditableField(
controller: _lieuNaissanceVilleCtrl,
),
),
ValidationLabeledField(
label: 'Pays de naissance',
field: ValidationEditableField(
controller: _lieuNaissancePaysCtrl,
),
),
ValidationLabeledField(
label: 'N° Agrément',
field: ValidationEditableField(
controller: _agrementCtrl,
),
),
ValidationLabeledField(
label: 'Date d\'agrément',
field: ValidationEditableField(
controller: _dateAgrementCtrl,
hintText: 'jj/mm/aaaa',
),
),
],
);
}
Widget _proTab() {
return LayoutBuilder(
builder: (context, c) {
final maxRowW = c.maxWidth;
final maxRowH = c.maxHeight;
final bodyH = maxRowH;
final idealPhotoW = bodyH * AdminAmPhotoFrame.idPhotoAspectRatio + 16;
final maxPhotoW = (maxRowW - _photoProGap - _proColumnMinWidth)
.clamp(0.0, double.infinity);
var photoW = idealPhotoW.clamp(_photoColumnMinWidth, 220.0);
if (photoW > maxPhotoW) photoW = maxPhotoW;
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
width: photoW,
child: AdminAmPhotoFrame(
photoUrl: widget.assistante.user.photoUrl,
),
),
const SizedBox(width: _photoProGap),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_proFieldsGrid(),
SwitchListTile(
contentPadding: EdgeInsets.zero,
dense: true,
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
title: const Text(
'Disponible pour accueillir',
style: TextStyle(fontSize: 13),
),
value: _disponible,
onChanged: (v) {
setState(() => _disponible = v);
_syncDirtyState();
},
),
],
),
),
),
],
);
},
);
}
Widget _childrenCapacityFields() {
final inconsistent = _placesInconsistent();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ValidationEditableSection(
rowLayout: const [2],
fields: [
ValidationLabeledField(
label: 'Capacité max (enfants)',
field: ValidationEditableField(
controller: _capaciteCtrl,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
),
),
ValidationLabeledField(
label: 'Places disponibles',
field: ValidationReadOnlyField(
value: _placesDisplayValue(),
error: inconsistent,
),
),
],
),
if (inconsistent) ...[
const SizedBox(height: 6),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 6,
runSpacing: 4,
children: [
Text(
_placesInconsistencyMessage()!,
style: TextStyle(
fontSize: 12,
color: Colors.red.shade700,
height: 1.3,
),
),
OutlinedButton(
onPressed: _saving ? null : _applyPlacesCorrection,
style: OutlinedButton.styleFrom(
foregroundColor: Colors.red.shade800,
disabledForegroundColor: Colors.red.shade300,
side: BorderSide(color: Colors.red.shade400),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
minimumSize: const Size(0, 28),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
textStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
child: const Text('Mettre à jour'),
),
],
),
],
],
);
}
Widget _childrenTab() {
final capacity = (_capaciteMax() ?? 4).clamp(1, 4);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_childrenCapacityFields(),
const SizedBox(height: 10),
Text(
'Enfants accueillis : ${_children.length}',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 8),
AdminAmChildrenCapacityGrid(
children: _children,
capacity: capacity,
onOpen: _openChild,
onDetach: _detachChild,
),
],
);
}
Widget _buildFooter() {
final isChildrenTab = _tabCtrl.index == 2;
return Row(
children: [
TextButton(
onPressed: _saving ? null : () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
const Spacer(),
if (isChildrenTab)
TextButton.icon(
onPressed: _attachChild,
icon: const Icon(Icons.link, size: 18),
label: const Text('Rattacher un enfant'),
),
if (isChildrenTab) const SizedBox(width: 12),
ElevatedButton(
style: ValidationModalTheme.primaryElevatedStyle,
onPressed: !_dirty || _saving ? null : _save,
child: _saving
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(_dirty ? 'Sauvegarder' : 'Aucune modification'),
),
],
);
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: _modalWidth),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(18, 16, 4, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_headerTitle(),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
if (_headerSubtitle() != null) ...[
const SizedBox(height: 4),
Text(
_headerSubtitle()!,
style: const TextStyle(
fontSize: 13,
color: Colors.black54,
),
),
],
],
),
),
const SizedBox(width: 12),
SizedBox(
width: 160,
child: AdminStatusCapsule(
statut: _statut,
onChanged: (v) {
setState(() => _statut = v);
_syncDirtyState();
},
),
),
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 40,
minHeight: 40,
),
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
tooltip: 'Fermer',
),
],
),
),
TabBar(
controller: _tabCtrl,
onTap: (_) => setState(() {}),
labelColor: ValidationModalTheme.primaryActionBackground,
unselectedLabelColor: Colors.black54,
indicatorColor: ValidationModalTheme.primaryActionBackground,
tabs: const [
Tab(text: 'Identité'),
Tab(text: 'Fiche professionnelle'),
Tab(text: 'Enfants accueillis'),
],
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
child: SizedBox(
height: _tabViewHeight(_tabCtrl.index),
child: TabBarView(
controller: _tabCtrl,
children: [
_identityTab(),
_proTab(),
_childrenTab(),
],
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 12),
child: _buildFooter(),
),
],
),
),
);
}
}