fix(#131): polish fiche AM — champs pro, places, vigilance et photo.
Fiche pro entièrement éditable, places déclarées en lecture seule avec alerte rouge, icône vigilance sur la liste AM, et cadrage photo aligné sur le wizard validation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
479a32b4bf
commit
4985726bc6
@ -575,6 +575,9 @@ class UserService {
|
|||||||
'ville',
|
'ville',
|
||||||
'code_postal',
|
'code_postal',
|
||||||
'statut',
|
'statut',
|
||||||
|
'date_naissance',
|
||||||
|
'lieu_naissance_ville',
|
||||||
|
'lieu_naissance_pays',
|
||||||
]) {
|
]) {
|
||||||
if (body.containsKey(k)) userFields[k] = body[k];
|
if (body.containsKey(k)) userFields[k] = body[k];
|
||||||
}
|
}
|
||||||
|
|||||||
45
frontend/lib/utils/am_vigilance.dart
Normal file
45
frontend/lib/utils/am_vigilance.dart
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
||||||
|
|
||||||
|
/// Places disponibles attendues : capacité max − enfants rattachés.
|
||||||
|
int? amExpectedPlacesAvailable({
|
||||||
|
required int? maxChildren,
|
||||||
|
required int childrenCount,
|
||||||
|
}) {
|
||||||
|
if (maxChildren == null) return null;
|
||||||
|
return (maxChildren - childrenCount).clamp(0, maxChildren);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True si la valeur déclarée par l'AM ne correspond pas au calcul métier.
|
||||||
|
bool amHasPlacesInconsistency({
|
||||||
|
required int? maxChildren,
|
||||||
|
required int? placesAvailable,
|
||||||
|
required int childrenCount,
|
||||||
|
}) {
|
||||||
|
final expected = amExpectedPlacesAvailable(
|
||||||
|
maxChildren: maxChildren,
|
||||||
|
childrenCount: childrenCount,
|
||||||
|
);
|
||||||
|
if (expected == null) return false;
|
||||||
|
if (placesAvailable == null) return childrenCount > (maxChildren ?? 0);
|
||||||
|
return placesAvailable != expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? amPlacesVigilanceMessage(AssistanteMaternelleModel am) {
|
||||||
|
if (!amHasPlacesInconsistency(
|
||||||
|
maxChildren: am.maxChildren,
|
||||||
|
placesAvailable: am.placesAvailable,
|
||||||
|
childrenCount: am.children.length,
|
||||||
|
)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final stored = am.placesAvailable;
|
||||||
|
final expected = amExpectedPlacesAvailable(
|
||||||
|
maxChildren: am.maxChildren,
|
||||||
|
childrenCount: am.children.length,
|
||||||
|
);
|
||||||
|
final storedLabel = stored?.toString() ?? 'non renseigné';
|
||||||
|
final expectedLabel = expected?.toString() ?? '–';
|
||||||
|
return 'Point de vigilance : l\'AM déclare $storedLabel place(s) disponible(s), '
|
||||||
|
'alors que capacité ${am.maxChildren ?? '–'} − '
|
||||||
|
'${am.children.length} enfant(s) rattaché(s) = $expectedLabel.';
|
||||||
|
}
|
||||||
@ -10,7 +10,25 @@ String formatIsoDateFr(String? s, {String ifEmpty = '–'}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parties d'âge calendaire entre [birth] et [reference] (date-only).
|
/// Convertit `dd/MM/yyyy` (ou ISO) en `yyyy-MM-dd` pour l'API.
|
||||||
|
String? parseFrDateToIso(String text) {
|
||||||
|
final t = text.trim();
|
||||||
|
if (t.isEmpty) return null;
|
||||||
|
try {
|
||||||
|
return DateFormat('dd/MM/yyyy')
|
||||||
|
.parseStrict(t)
|
||||||
|
.toIso8601String()
|
||||||
|
.split('T')
|
||||||
|
.first;
|
||||||
|
} catch (_) {
|
||||||
|
try {
|
||||||
|
return DateTime.parse(t).toIso8601String().split('T').first;
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
({int years, int months, int days}) computeChildAgeParts(
|
({int years, int months, int days}) computeChildAgeParts(
|
||||||
DateTime birth,
|
DateTime birth,
|
||||||
DateTime reference,
|
DateTime reference,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
||||||
|
import 'package:p_tits_pas/utils/am_vigilance.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_am_edit_modal.dart';
|
import 'package:p_tits_pas/widgets/admin/common/admin_am_edit_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
||||||
@ -76,10 +77,12 @@ class _AssistanteMaternelleManagementWidgetState
|
|||||||
itemCount: filteredAssistantes.length,
|
itemCount: filteredAssistantes.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final assistante = filteredAssistantes[index];
|
final assistante = filteredAssistantes[index];
|
||||||
|
final vigilance = amPlacesVigilanceMessage(assistante);
|
||||||
return AdminUserCard(
|
return AdminUserCard(
|
||||||
title: assistante.user.fullName,
|
title: assistante.user.fullName,
|
||||||
avatarUrl: assistante.user.photoUrl,
|
avatarUrl: assistante.user.photoUrl,
|
||||||
fallbackIcon: Icons.face,
|
fallbackIcon: Icons.face,
|
||||||
|
vigilanceTooltip: vigilance,
|
||||||
subtitleLines: [
|
subtitleLines: [
|
||||||
assistante.user.email,
|
assistante.user.email,
|
||||||
'Zone : ${assistante.residenceCity ?? 'N/A'} | Capacité : ${assistante.maxChildren ?? 0}',
|
'Zone : ${assistante.residenceCity ?? 'N/A'} | Capacité : ${assistante.maxChildren ?? 0}',
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:p_tits_pas/models/assistante_maternelle_model.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/enfant_admin_model.dart';
|
||||||
import 'package:p_tits_pas/models/parent_child_summary.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/date_display_utils.dart';
|
||||||
import 'package:p_tits_pas/utils/nir_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/utils/phone_utils.dart';
|
||||||
@ -41,8 +42,12 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
late final TextEditingController _villeCtrl;
|
late final TextEditingController _villeCtrl;
|
||||||
late final TextEditingController _cpCtrl;
|
late final TextEditingController _cpCtrl;
|
||||||
late final TextEditingController _agrementCtrl;
|
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 final TextEditingController _capaciteCtrl;
|
||||||
late final TextEditingController _placesCtrl;
|
|
||||||
|
|
||||||
late String _statut;
|
late String _statut;
|
||||||
late bool _disponible;
|
late bool _disponible;
|
||||||
@ -87,12 +92,22 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
_villeCtrl = TextEditingController(text: u.ville ?? '');
|
_villeCtrl = TextEditingController(text: u.ville ?? '');
|
||||||
_cpCtrl = TextEditingController(text: u.codePostal ?? '');
|
_cpCtrl = TextEditingController(text: u.codePostal ?? '');
|
||||||
_agrementCtrl = TextEditingController(text: am.approvalNumber ?? '');
|
_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(
|
_capaciteCtrl = TextEditingController(
|
||||||
text: am.maxChildren?.toString() ?? '',
|
text: am.maxChildren?.toString() ?? '',
|
||||||
);
|
);
|
||||||
_placesCtrl = TextEditingController(
|
|
||||||
text: am.placesAvailable?.toString() ?? '',
|
|
||||||
);
|
|
||||||
_statut = u.statut ?? 'en_attente';
|
_statut = u.statut ?? 'en_attente';
|
||||||
_disponible = am.available ?? true;
|
_disponible = am.available ?? true;
|
||||||
_children = List.of(am.children);
|
_children = List.of(am.children);
|
||||||
@ -107,11 +122,16 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
_villeCtrl,
|
_villeCtrl,
|
||||||
_cpCtrl,
|
_cpCtrl,
|
||||||
_agrementCtrl,
|
_agrementCtrl,
|
||||||
|
_nirCtrl,
|
||||||
|
_dateNaissanceCtrl,
|
||||||
|
_lieuNaissanceVilleCtrl,
|
||||||
|
_lieuNaissancePaysCtrl,
|
||||||
|
_dateAgrementCtrl,
|
||||||
_capaciteCtrl,
|
_capaciteCtrl,
|
||||||
_placesCtrl,
|
|
||||||
]) {
|
]) {
|
||||||
c.addListener(_markDirty);
|
c.addListener(_markDirty);
|
||||||
}
|
}
|
||||||
|
_capaciteCtrl.addListener(_onCapacityChanged);
|
||||||
_nomCtrl.addListener(_onNameFieldChanged);
|
_nomCtrl.addListener(_onNameFieldChanged);
|
||||||
_prenomCtrl.addListener(_onNameFieldChanged);
|
_prenomCtrl.addListener(_onNameFieldChanged);
|
||||||
_tabCtrl.addListener(_onTabChanged);
|
_tabCtrl.addListener(_onTabChanged);
|
||||||
@ -124,6 +144,8 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
|
|
||||||
void _onNameFieldChanged() => setState(() {});
|
void _onNameFieldChanged() => setState(() {});
|
||||||
|
|
||||||
|
void _onCapacityChanged() => setState(() {});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_tabCtrl.dispose();
|
_tabCtrl.dispose();
|
||||||
@ -136,8 +158,12 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
_villeCtrl,
|
_villeCtrl,
|
||||||
_cpCtrl,
|
_cpCtrl,
|
||||||
_agrementCtrl,
|
_agrementCtrl,
|
||||||
|
_nirCtrl,
|
||||||
|
_dateNaissanceCtrl,
|
||||||
|
_lieuNaissanceVilleCtrl,
|
||||||
|
_lieuNaissancePaysCtrl,
|
||||||
|
_dateAgrementCtrl,
|
||||||
_capaciteCtrl,
|
_capaciteCtrl,
|
||||||
_placesCtrl,
|
|
||||||
]) {
|
]) {
|
||||||
c.dispose();
|
c.dispose();
|
||||||
}
|
}
|
||||||
@ -175,11 +201,43 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
|
|
||||||
String _formatNirDisplay() {
|
String _formatNirDisplay() {
|
||||||
final raw = widget.assistante.nir?.trim() ?? '';
|
final raw = widget.assistante.nir?.trim() ?? '';
|
||||||
if (raw.isEmpty) return '–';
|
if (raw.isEmpty) return '';
|
||||||
final digits = nirToRaw(raw).toUpperCase();
|
final digits = nirToRaw(raw).toUpperCase();
|
||||||
return digits.length == 15 ? formatNir(digits) : raw;
|
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: widget.assistante.placesAvailable,
|
||||||
|
childrenCount: _children.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
String _placesDisplayValue() {
|
||||||
|
final stored = widget.assistante.placesAvailable;
|
||||||
|
if (stored != null) return stored.toString();
|
||||||
|
return '–';
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _placesInconsistencyMessage() {
|
||||||
|
if (!_placesInconsistent()) return null;
|
||||||
|
final stored = widget.assistante.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) {
|
int? _parseIntField(TextEditingController c) {
|
||||||
final t = c.text.trim();
|
final t = c.text.trim();
|
||||||
if (t.isEmpty) return null;
|
if (t.isEmpty) return null;
|
||||||
@ -223,10 +281,14 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
'code_postal': _cpCtrl.text.trim(),
|
'code_postal': _cpCtrl.text.trim(),
|
||||||
'statut': _statut,
|
'statut': _statut,
|
||||||
'approval_number': _agrementCtrl.text.trim(),
|
'approval_number': _agrementCtrl.text.trim(),
|
||||||
if (_parseIntField(_capaciteCtrl) != null)
|
'nir': nirToRaw(_nirCtrl.text),
|
||||||
'max_children': _parseIntField(_capaciteCtrl),
|
if (_frDateToIso(_dateNaissanceCtrl.text) != null)
|
||||||
if (_parseIntField(_placesCtrl) != null)
|
'date_naissance': _frDateToIso(_dateNaissanceCtrl.text),
|
||||||
'places_available': _parseIntField(_placesCtrl),
|
'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(),
|
||||||
'available': _disponible,
|
'available': _disponible,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -388,8 +450,6 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _v(String? s) =>
|
|
||||||
(s != null && s.trim().isNotEmpty) ? s.trim() : '–';
|
|
||||||
|
|
||||||
Widget _identityTab() {
|
Widget _identityTab() {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
@ -407,7 +467,6 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _proFieldsGrid() {
|
Widget _proFieldsGrid() {
|
||||||
final u = widget.assistante.user;
|
|
||||||
return ValidationFormGrid(
|
return ValidationFormGrid(
|
||||||
title: 'Dossier professionnel',
|
title: 'Dossier professionnel',
|
||||||
rowLayout: _photoProRowLayout,
|
rowLayout: _photoProRowLayout,
|
||||||
@ -415,29 +474,34 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
fields: [
|
fields: [
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'NIR',
|
label: 'NIR',
|
||||||
field: ValidationReadOnlyField(
|
field: ValidationEditableField(
|
||||||
value: _formatNirDisplay(),
|
controller: _nirCtrl,
|
||||||
compact: true,
|
compact: true,
|
||||||
|
hintText: '15 chiffres',
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.allow(RegExp(r'[\d\s]')),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Date de naissance',
|
label: 'Date de naissance',
|
||||||
field: ValidationReadOnlyField(
|
field: ValidationEditableField(
|
||||||
value: formatIsoDateFr(u.dateNaissance),
|
controller: _dateNaissanceCtrl,
|
||||||
compact: true,
|
compact: true,
|
||||||
|
hintText: 'jj/mm/aaaa',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Ville de naissance',
|
label: 'Ville de naissance',
|
||||||
field: ValidationReadOnlyField(
|
field: ValidationEditableField(
|
||||||
value: _v(u.lieuNaissanceVille),
|
controller: _lieuNaissanceVilleCtrl,
|
||||||
compact: true,
|
compact: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Pays de naissance',
|
label: 'Pays de naissance',
|
||||||
field: ValidationReadOnlyField(
|
field: ValidationEditableField(
|
||||||
value: _v(u.lieuNaissancePays),
|
controller: _lieuNaissancePaysCtrl,
|
||||||
compact: true,
|
compact: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -450,9 +514,10 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Date d\'agrément',
|
label: 'Date d\'agrément',
|
||||||
field: ValidationReadOnlyField(
|
field: ValidationEditableField(
|
||||||
value: formatIsoDateFr(widget.assistante.agreementDate),
|
controller: _dateAgrementCtrl,
|
||||||
compact: true,
|
compact: true,
|
||||||
|
hintText: 'jj/mm/aaaa',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -513,76 +578,78 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _childrenCapacityFields() {
|
Widget _childrenCapacityFields() {
|
||||||
return ValidationEditableSection(
|
final inconsistent = _placesInconsistent();
|
||||||
compact: true,
|
return Column(
|
||||||
rowLayout: const [2],
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
fields: [
|
children: [
|
||||||
ValidationLabeledField(
|
ValidationEditableSection(
|
||||||
label: 'Capacité max (enfants)',
|
compact: true,
|
||||||
field: ValidationEditableField(
|
rowLayout: const [2],
|
||||||
controller: _capaciteCtrl,
|
fields: [
|
||||||
keyboardType: TextInputType.number,
|
ValidationLabeledField(
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
label: 'Capacité max (enfants)',
|
||||||
compact: true,
|
field: ValidationEditableField(
|
||||||
),
|
controller: _capaciteCtrl,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
compact: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ValidationLabeledField(
|
||||||
|
label: 'Places disponibles',
|
||||||
|
field: ValidationReadOnlyField(
|
||||||
|
value: _placesDisplayValue(),
|
||||||
|
compact: true,
|
||||||
|
error: inconsistent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
if (inconsistent) ...[
|
||||||
label: 'Places disponibles',
|
const SizedBox(height: 6),
|
||||||
field: ValidationEditableField(
|
Text(
|
||||||
controller: _placesCtrl,
|
_placesInconsistencyMessage()!,
|
||||||
keyboardType: TextInputType.number,
|
style: TextStyle(
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
fontSize: 12,
|
||||||
compact: true,
|
color: Colors.red.shade700,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _childrenTab() {
|
Widget _childrenTab() {
|
||||||
return LayoutBuilder(
|
return Column(
|
||||||
builder: (context, constraints) {
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
const headerBlock = 88.0;
|
children: [
|
||||||
const capacityBlock = 56.0;
|
_childrenCapacityFields(),
|
||||||
const gaps = 20.0;
|
const SizedBox(height: 10),
|
||||||
final listHeight = (constraints.maxHeight -
|
Text(
|
||||||
headerBlock -
|
'Enfants accueillis : ${_children.length}',
|
||||||
capacityBlock -
|
style: const TextStyle(
|
||||||
gaps)
|
fontSize: 15,
|
||||||
.clamp(72.0, AdminChildrenAffiliationPanel.defaultViewportHeight);
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black87,
|
||||||
return Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
),
|
||||||
children: [
|
const SizedBox(height: 2),
|
||||||
_childrenCapacityFields(),
|
Text(
|
||||||
const SizedBox(height: 10),
|
'Enfants rattachés à cette assistante (accueil / garde).',
|
||||||
Text(
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade700),
|
||||||
'Enfants accueillis : ${_children.length}',
|
),
|
||||||
style: const TextStyle(
|
const SizedBox(height: 8),
|
||||||
fontSize: 15,
|
Expanded(
|
||||||
fontWeight: FontWeight.w600,
|
child: AdminChildrenAffiliationPanel(
|
||||||
color: Colors.black87,
|
children: _children,
|
||||||
),
|
scrollController: _childrenScrollCtrl,
|
||||||
),
|
onOpen: _openChild,
|
||||||
const SizedBox(height: 2),
|
onDetach: _detachChild,
|
||||||
Text(
|
emptyMessage: 'Aucun enfant rattaché à cette assistante',
|
||||||
'Enfants rattachés à cette assistante (accueil / garde).',
|
),
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade700),
|
),
|
||||||
),
|
],
|
||||||
const SizedBox(height: 8),
|
|
||||||
Expanded(
|
|
||||||
child: AdminChildrenAffiliationPanel(
|
|
||||||
height: listHeight,
|
|
||||||
children: _children,
|
|
||||||
scrollController: _childrenScrollCtrl,
|
|
||||||
onOpen: _openChild,
|
|
||||||
onDetach: _detachChild,
|
|
||||||
emptyMessage: 'Aucun enfant rattaché à cette assistante',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,17 +36,19 @@ class AdminAmPhotoFrame extends StatelessWidget {
|
|||||||
ph = pw / ar;
|
ph = pw / ar;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
// Cadre gris = taille photo + padding uniforme ; centré dans la colonne
|
||||||
decoration: BoxDecoration(
|
// (évite le vide blanc en bas quand le conteneur parent est plus haut).
|
||||||
color: Colors.grey.shade100,
|
return Align(
|
||||||
borderRadius: BorderRadius.circular(8),
|
alignment: Alignment.topCenter,
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
child: Container(
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
clipBehavior: Clip.antiAlias,
|
color: Colors.grey.shade100,
|
||||||
child: Padding(
|
borderRadius: BorderRadius.circular(8),
|
||||||
padding: const EdgeInsets.all(uniformFrame),
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
child: Align(
|
),
|
||||||
alignment: Alignment.topCenter,
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(uniformFrame),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
@ -84,6 +86,7 @@ class AdminAmPhotoFrame extends StatelessWidget {
|
|||||||
width: pw,
|
width: pw,
|
||||||
height: ph,
|
height: ph,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
loadingBuilder: (_, child, progress) {
|
loadingBuilder: (_, child, progress) {
|
||||||
if (progress == null) return child;
|
if (progress == null) return child;
|
||||||
return ColoredBox(
|
return ColoredBox(
|
||||||
|
|||||||
@ -26,7 +26,20 @@ class AdminChildrenAffiliationPanel extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final panelHeight = height ?? defaultViewportHeight;
|
if (height != null) {
|
||||||
|
return _panel(height!);
|
||||||
|
}
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final h = constraints.maxHeight.isFinite && constraints.maxHeight > 0
|
||||||
|
? constraints.maxHeight
|
||||||
|
: defaultViewportHeight;
|
||||||
|
return _panel(h);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _panel(double panelHeight) {
|
||||||
return Container(
|
return Container(
|
||||||
height: panelHeight,
|
height: panelHeight,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
@ -12,6 +12,7 @@ class AdminUserCard extends StatefulWidget {
|
|||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
final Color? titleColor;
|
final Color? titleColor;
|
||||||
final Color? infoColor;
|
final Color? infoColor;
|
||||||
|
final String? vigilanceTooltip;
|
||||||
|
|
||||||
const AdminUserCard({
|
const AdminUserCard({
|
||||||
super.key,
|
super.key,
|
||||||
@ -24,6 +25,7 @@ class AdminUserCard extends StatefulWidget {
|
|||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
this.titleColor,
|
this.titleColor,
|
||||||
this.infoColor,
|
this.infoColor,
|
||||||
|
this.vigilanceTooltip,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -65,6 +67,17 @@ class _AdminUserCardState extends State<AdminUserCard> {
|
|||||||
children: [
|
children: [
|
||||||
_buildAvatar(avatarUrl),
|
_buildAvatar(avatarUrl),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
|
if (widget.vigilanceTooltip != null) ...[
|
||||||
|
Tooltip(
|
||||||
|
message: widget.vigilanceTooltip!,
|
||||||
|
child: Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.orange.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@ -210,6 +210,28 @@ class ValidationEditableField extends StatelessWidget {
|
|||||||
|
|
||||||
static const double _compactFieldHeight = 34;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!compact || maxLines > 1) {
|
if (!compact || maxLines > 1) {
|
||||||
@ -230,17 +252,19 @@ class ValidationEditableField extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: _compactFieldHeight,
|
height: _compactFieldHeight,
|
||||||
child: TextField(
|
child: DecoratedBox(
|
||||||
controller: controller,
|
decoration: _compactDecoration(),
|
||||||
keyboardType: keyboardType,
|
child: TextField(
|
||||||
inputFormatters: inputFormatters,
|
controller: controller,
|
||||||
maxLines: 1,
|
keyboardType: keyboardType,
|
||||||
style: const TextStyle(color: Colors.black87, fontSize: 13, height: 1.15),
|
inputFormatters: inputFormatters,
|
||||||
decoration: ValidationFieldDecoration.input(
|
maxLines: 1,
|
||||||
hint: hintText,
|
style: const TextStyle(
|
||||||
compact: true,
|
color: Colors.black87,
|
||||||
).copyWith(
|
fontSize: 13,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 9),
|
height: 1.15,
|
||||||
|
),
|
||||||
|
decoration: _compactInputDecoration(hint: hintText),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -278,12 +302,14 @@ class ValidationReadOnlyField extends StatelessWidget {
|
|||||||
final String value;
|
final String value;
|
||||||
final int? maxLines;
|
final int? maxLines;
|
||||||
final bool compact;
|
final bool compact;
|
||||||
|
final bool error;
|
||||||
|
|
||||||
const ValidationReadOnlyField({
|
const ValidationReadOnlyField({
|
||||||
super.key,
|
super.key,
|
||||||
required this.value,
|
required this.value,
|
||||||
this.maxLines = 1,
|
this.maxLines = 1,
|
||||||
this.compact = false,
|
this.compact = false,
|
||||||
|
this.error = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
static const double _compactFieldHeight = 34;
|
static const double _compactFieldHeight = 34;
|
||||||
@ -298,13 +324,20 @@ class ValidationReadOnlyField extends StatelessWidget {
|
|||||||
horizontal: compact ? 10 : 12,
|
horizontal: compact ? 10 : 12,
|
||||||
vertical: compact ? 7 : 10,
|
vertical: compact ? 7 : 10,
|
||||||
),
|
),
|
||||||
decoration: ValidationFieldDecoration.container(),
|
decoration: error
|
||||||
|
? BoxDecoration(
|
||||||
|
color: Colors.red.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: Border.all(color: Colors.red.shade400),
|
||||||
|
)
|
||||||
|
: ValidationFieldDecoration.container(),
|
||||||
child: Text(
|
child: Text(
|
||||||
value,
|
value,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black87,
|
color: error ? Colors.red.shade800 : Colors.black87,
|
||||||
fontSize: compact ? 13 : 14,
|
fontSize: compact ? 13 : 14,
|
||||||
height: compact ? 1.2 : null,
|
height: compact ? 1.2 : null,
|
||||||
|
fontWeight: error ? FontWeight.w600 : null,
|
||||||
),
|
),
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
|||||||
@ -13,6 +13,7 @@ class AuthNetworkImage extends StatefulWidget {
|
|||||||
this.width,
|
this.width,
|
||||||
this.height,
|
this.height,
|
||||||
this.fit = BoxFit.cover,
|
this.fit = BoxFit.cover,
|
||||||
|
this.alignment = Alignment.center,
|
||||||
this.loadingBuilder,
|
this.loadingBuilder,
|
||||||
this.errorBuilder,
|
this.errorBuilder,
|
||||||
});
|
});
|
||||||
@ -21,6 +22,7 @@ class AuthNetworkImage extends StatefulWidget {
|
|||||||
final double? width;
|
final double? width;
|
||||||
final double? height;
|
final double? height;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
|
final AlignmentGeometry alignment;
|
||||||
final ImageLoadingBuilder? loadingBuilder;
|
final ImageLoadingBuilder? loadingBuilder;
|
||||||
final ImageErrorWidgetBuilder? errorBuilder;
|
final ImageErrorWidgetBuilder? errorBuilder;
|
||||||
|
|
||||||
@ -75,6 +77,7 @@ class _AuthNetworkImageState extends State<AuthNetworkImage> {
|
|||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
fit: widget.fit,
|
fit: widget.fit,
|
||||||
|
alignment: widget.alignment,
|
||||||
loadingBuilder: widget.loadingBuilder,
|
loadingBuilder: widget.loadingBuilder,
|
||||||
errorBuilder: err,
|
errorBuilder: err,
|
||||||
);
|
);
|
||||||
@ -105,6 +108,7 @@ class _AuthNetworkImageState extends State<AuthNetworkImage> {
|
|||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
fit: widget.fit,
|
fit: widget.fit,
|
||||||
|
alignment: widget.alignment,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
loadingBuilder: widget.loadingBuilder,
|
loadingBuilder: widget.loadingBuilder,
|
||||||
errorBuilder: err,
|
errorBuilder: err,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user