petitspas/frontend/lib/widgets/admin/common/admin_am_edit_modal.dart
Julien Martin 18c1d1eba7 fix(#148): désactiver « Rattacher un enfant » si capacité AM pleine.
Le lien du footer est grisé avec tooltip quand enfants ≥ capacité max ; il se réactive après détachement.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 16:12:53 +02:00

892 lines
28 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/material.dart';
import 'package:flutter/services.dart';
import 'package:p_tits_pas/models/assistante_maternelle_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_select_enfant_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;
late Set<String> _baselineChildIds;
/// Enfants ajoutés localement qui étaient déjà chez une autre AM
/// (enfantId → amUserId d'origine). Au save : détacher puis rattacher.
final Map<String, String> _transferFromAmIds = {};
bool _saving = false;
bool _dirty = false;
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);
_baselineChildIds = _children.map((c) => c.id).toSet();
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);
_fetchChildrenFromServer();
}
bool _childrenChanged() {
final current = _children.map((c) => c.id).toSet();
return current.length != _baselineChildIds.length ||
!current.containsAll(_baselineChildIds);
}
void _syncPlacesAfterChildrenChange() {
final expected = _computedPlacesAvailable();
if (expected != null) _placesAvailable = expected;
}
void _onTabChanged() {
if (!_tabCtrl.indexIsChanging) setState(() {});
}
void _onNameFieldChanged() => setState(() {});
void _onCapacityChanged() => setState(() {});
@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() {
if (!_dirty) setState(() => _dirty = true);
}
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);
/// True si plus aucune place d'accueil (enfants ≥ capacité max).
bool get _capacityFull {
final max = _capaciteMax();
if (max == null) return false;
return _children.length >= max;
}
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;
_dirty = true;
});
}
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 {
if (_childrenChanged()) _syncPlacesAfterChildrenChange();
final currentIds = _children.map((c) => c.id).toSet();
for (final id in _baselineChildIds.difference(currentIds)) {
await UserService.detachEnfantFromAm(
amUserId: widget.assistante.user.id,
enfantId: id,
);
}
for (final id in currentIds.difference(_baselineChildIds)) {
// Transfert : détacher l'ancienne AM sans recalculer ses places
// (le ! de vigilance apparaît côté liste AM).
final previousAmId = _transferFromAmIds[id] ??
(await UserService.findAmForEnfant(id))?.user.id;
if (previousAmId != null &&
previousAmId != widget.assistante.user.id) {
await UserService.detachEnfantFromAm(
amUserId: previousAmId,
enfantId: id,
);
}
await UserService.attachEnfantToAm(
amUserId: widget.assistante.user.id,
enfantId: id,
);
}
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 != null) 'places_available': _placesAvailable,
'available': _disponible,
},
);
if (!mounted) return;
setState(() {
_dirty = false;
_saving = false;
_baselineChildIds = currentIds;
_transferFromAmIds.clear();
});
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> _fetchChildrenFromServer() 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;
_baselineChildIds = kids.map((c) => c.id).toSet();
_transferFromAmIds.clear();
});
} catch (_) {}
}
Future<void> _refreshChildrenDetails() async {
try {
final all = await UserService.getEnfants();
final byId = {for (final e in all) e.id: e};
if (!mounted) return;
setState(() {
_children = _children.map((c) {
final full = byId[c.id];
if (full != null) return ParentChildSummary.fromEnfant(full);
return c;
}).toList();
});
} 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: _refreshChildrenDetails,
),
);
} 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;
setState(() {
_children = _children.where((c) => c.id != child.id).toList();
_transferFromAmIds.remove(child.id);
_syncPlacesAfterChildrenChange();
_dirty = true;
});
}
Future<void> _attachChild() async {
if (_capacityFull || !mounted) return;
final selected = await AdminSelectEnfantModal.show(
context,
excludeIds: _children.map((c) => c.id).toSet(),
title: 'Rattacher un enfant',
showSansGardeFilter: true,
);
if (selected == null || !mounted) return;
AssistanteMaternelleModel? previousAm;
try {
previousAm = await UserService.findAmForEnfant(selected.id);
} catch (_) {
previousAm = null;
}
if (!mounted) return;
final previousAmId = previousAm?.user.id;
final isTransfer = previousAmId != null &&
previousAmId != widget.assistante.user.id;
if (isTransfer) {
final amName = previousAm!.user.fullName.trim().isNotEmpty
? previousAm.user.fullName.trim()
: 'une autre assistante maternelle';
final gardeLabel =
(selected.gender ?? '').trim().toUpperCase() == 'F'
? 'déjà gardée'
: 'déjà gardé';
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Changer d\'affectation'),
content: Text(
'${selected.fullName} est $gardeLabel par $amName.\n\n'
'Confirmer le transfert vers cette assistante ? '
'L\'enfant sera détaché de $amName.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
style: ValidationModalTheme.primaryElevatedStyle,
child: const Text('Confirmer'),
),
],
),
);
if (confirmed != true || !mounted) return;
}
setState(() {
_children = [
..._children,
ParentChildSummary.fromEnfant(selected),
];
if (isTransfer) {
_transferFromAmIds[selected.id] = previousAmId!;
}
_syncPlacesAfterChildrenChange();
_dirty = true;
});
}
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;
_dirty = true;
}),
),
],
),
),
),
],
);
},
);
}
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;
final canAttachChild = !_saving && !_capacityFull;
return Row(
children: [
TextButton(
onPressed: _saving ? null : () => Navigator.of(context).pop(),
child: const Text('Fermer'),
),
const Spacer(),
if (isChildrenTab)
Tooltip(
message: _capacityFull
? 'Capacité maximale atteinte'
: 'Rattacher un enfant',
child: TextButton.icon(
onPressed: canAttachChild ? _attachChild : null,
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;
_dirty = true;
}),
),
),
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(),
),
],
),
),
);
}
}