feat(#131): grille enfants AM, statuts garde et polish champs admin.
Aligne le front sur les statuts enfant back (garde/sans_garde), refond l’onglet Enfants accueillis en grille 2×2 avec correction des places, et unifie les champs validation éditables/lecture seule. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
53721ffbb3
commit
66c7f22280
@ -1,4 +1,5 @@
|
|||||||
import 'package:p_tits_pas/models/user.dart';
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
|
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
||||||
|
|
||||||
/// Réponse unifiée GET /dossiers/:numeroDossier. Ticket #119, #107.
|
/// Réponse unifiée GET /dossiers/:numeroDossier. Ticket #119, #107.
|
||||||
class DossierUnifie {
|
class DossierUnifie {
|
||||||
@ -220,7 +221,7 @@ class EnfantDossier {
|
|||||||
lastName: (json['last_name'] ?? json['nom'])?.toString(),
|
lastName: (json['last_name'] ?? json['nom'])?.toString(),
|
||||||
birthDate: json['birth_date']?.toString(),
|
birthDate: json['birth_date']?.toString(),
|
||||||
gender: (json['gender'] ?? json['genre'])?.toString(),
|
gender: (json['gender'] ?? json['genre'])?.toString(),
|
||||||
status: json['status']?.toString(),
|
status: normalizeEnfantStatus(json['status']?.toString()),
|
||||||
dueDate: json['due_date']?.toString(),
|
dueDate: json['due_date']?.toString(),
|
||||||
photoUrl: resolvedPhoto,
|
photoUrl: resolvedPhoto,
|
||||||
consentPhoto:
|
consentPhoto:
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:p_tits_pas/models/user.dart';
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
|
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
||||||
|
|
||||||
/// Enfant tel que renvoyé par `GET /enfants` (dashboard admin).
|
/// Enfant tel que renvoyé par `GET /enfants` (dashboard admin).
|
||||||
class EnfantAdminModel {
|
class EnfantAdminModel {
|
||||||
@ -54,7 +55,7 @@ class EnfantAdminModel {
|
|||||||
gender: json['gender'] as String?,
|
gender: json['gender'] as String?,
|
||||||
birthDate: _dateString(json['birth_date']),
|
birthDate: _dateString(json['birth_date']),
|
||||||
dueDate: _dateString(json['due_date']),
|
dueDate: _dateString(json['due_date']),
|
||||||
status: (json['status'] ?? '').toString(),
|
status: normalizeEnfantStatus(json['status']?.toString()),
|
||||||
photoUrl: json['photo_url'] as String?,
|
photoUrl: json['photo_url'] as String?,
|
||||||
consentPhoto: json['consent_photo'] == true,
|
consentPhoto: json['consent_photo'] == true,
|
||||||
isMultiple: json['is_multiple'] == true,
|
isMultiple: json['is_multiple'] == true,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
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/utils/enfant_status_utils.dart';
|
||||||
|
|
||||||
/// Résumé enfant affiché dans la fiche parent (dashboard admin).
|
/// Résumé enfant affiché dans la fiche parent (dashboard admin).
|
||||||
class ParentChildSummary {
|
class ParentChildSummary {
|
||||||
@ -33,7 +34,9 @@ class ParentChildSummary {
|
|||||||
id: (json['id'] ?? '').toString(),
|
id: (json['id'] ?? '').toString(),
|
||||||
firstName: (json['first_name'] ?? json['prenom'])?.toString(),
|
firstName: (json['first_name'] ?? json['prenom'])?.toString(),
|
||||||
lastName: (json['last_name'] ?? json['nom'])?.toString(),
|
lastName: (json['last_name'] ?? json['nom'])?.toString(),
|
||||||
status: (json['status'] ?? json['statut'] ?? '').toString(),
|
status: normalizeEnfantStatus(
|
||||||
|
(json['status'] ?? json['statut'])?.toString(),
|
||||||
|
),
|
||||||
photoUrl: json['photo_url']?.toString(),
|
photoUrl: json['photo_url']?.toString(),
|
||||||
birthDate: _dateString(json['birth_date']),
|
birthDate: _dateString(json['birth_date']),
|
||||||
dueDate: _dateString(json['due_date']),
|
dueDate: _dateString(json['due_date']),
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
||||||
|
|
||||||
/// Affiche une date ISO / parseable en `dd/MM/yyyy`, avec repli sur la chaîne ou [ifEmpty].
|
/// Affiche une date ISO / parseable en `dd/MM/yyyy`, avec repli sur la chaîne ou [ifEmpty].
|
||||||
String formatIsoDateFr(String? s, {String ifEmpty = '–'}) {
|
String formatIsoDateFr(String? s, {String ifEmpty = '–'}) {
|
||||||
@ -63,7 +64,7 @@ String formatChildAgeLabel({
|
|||||||
String? dueDate,
|
String? dueDate,
|
||||||
String? status,
|
String? status,
|
||||||
}) {
|
}) {
|
||||||
if (status == 'a_naitre') {
|
if (status == 'a_naitre' || normalizeEnfantStatus(status) == 'a_naitre') {
|
||||||
final due = formatIsoDateFr(dueDate, ifEmpty: '');
|
final due = formatIsoDateFr(dueDate, ifEmpty: '');
|
||||||
if (due.isNotEmpty) return 'Naissance prévue : $due';
|
if (due.isNotEmpty) return 'Naissance prévue : $due';
|
||||||
return 'À naître';
|
return 'À naître';
|
||||||
|
|||||||
51
frontend/lib/utils/enfant_status_utils.dart
Normal file
51
frontend/lib/utils/enfant_status_utils.dart
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/// Statuts enfant alignés sur `statut_enfant_type` (BDD / back #131).
|
||||||
|
const enfantStatusValues = [
|
||||||
|
'a_naitre',
|
||||||
|
'sans_garde',
|
||||||
|
'garde',
|
||||||
|
'scolarise',
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Normalise une valeur API (legacy `actif` → `sans_garde`).
|
||||||
|
String normalizeEnfantStatus(String? raw) {
|
||||||
|
final s = (raw ?? '').trim().toLowerCase();
|
||||||
|
if (s.isEmpty) return 'sans_garde';
|
||||||
|
if (s == 'actif') return 'sans_garde';
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _scolariseAccordeAuGenre(String? gender) {
|
||||||
|
final g = (gender ?? '').trim().toUpperCase();
|
||||||
|
if (g == 'F') return 'Scolarisée';
|
||||||
|
return 'Scolarisé';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Libellé affiché pour un statut enfant.
|
||||||
|
String enfantStatusLabel(String? status, {String? gender}) {
|
||||||
|
switch (normalizeEnfantStatus(status)) {
|
||||||
|
case 'a_naitre':
|
||||||
|
return 'À naître';
|
||||||
|
case 'sans_garde':
|
||||||
|
return 'Sans garde';
|
||||||
|
case 'garde':
|
||||||
|
return 'En garde';
|
||||||
|
case 'scolarise':
|
||||||
|
return _scolariseAccordeAuGenre(gender);
|
||||||
|
default:
|
||||||
|
return status?.trim().isNotEmpty == true ? status!.trim() : '–';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Libellé court pour colonnes validation (null = pas de bandeau).
|
||||||
|
String? enfantColumnStatusLabel({String? status, String? gender}) {
|
||||||
|
final s = normalizeEnfantStatus(status);
|
||||||
|
switch (s) {
|
||||||
|
case 'a_naitre':
|
||||||
|
case 'sans_garde':
|
||||||
|
case 'garde':
|
||||||
|
case 'scolarise':
|
||||||
|
return enfantStatusLabel(s, gender: gender);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,301 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:p_tits_pas/models/parent_child_summary.dart';
|
||||||
|
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||||
|
import 'package:p_tits_pas/utils/date_display_utils.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||||
|
|
||||||
|
/// Grille 2×2 des places d'accueil AM (max 4, limitée à [capacity]).
|
||||||
|
class AdminAmChildrenCapacityGrid extends StatelessWidget {
|
||||||
|
static const int _gridSlots = 4;
|
||||||
|
static const double _slotHeight = 44;
|
||||||
|
static const double _gridPadding = 10;
|
||||||
|
static const double _gridGap = 8;
|
||||||
|
static const double _borderWidth = 1;
|
||||||
|
static const double fixedHeight = _borderWidth * 2 +
|
||||||
|
_gridPadding * 2 +
|
||||||
|
_slotHeight * 2 +
|
||||||
|
_gridGap;
|
||||||
|
|
||||||
|
final List<ParentChildSummary> children;
|
||||||
|
final int capacity;
|
||||||
|
final void Function(ParentChildSummary child) onOpen;
|
||||||
|
final void Function(ParentChildSummary child) onDetach;
|
||||||
|
|
||||||
|
const AdminAmChildrenCapacityGrid({
|
||||||
|
super.key,
|
||||||
|
required this.children,
|
||||||
|
required this.capacity,
|
||||||
|
required this.onOpen,
|
||||||
|
required this.onDetach,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final maxSlots = capacity.clamp(0, _gridSlots);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
height: fixedHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.grey.shade300, width: _borderWidth),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(_gridPadding),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildRow(0, 1, maxSlots),
|
||||||
|
const SizedBox(height: _gridGap),
|
||||||
|
_buildRow(2, 3, maxSlots),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRow(int leftIndex, int rightIndex, int maxSlots) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildSlot(leftIndex, maxSlots)),
|
||||||
|
const SizedBox(width: _gridGap),
|
||||||
|
Expanded(child: _buildSlot(rightIndex, maxSlots)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSlot(int index, int maxSlots) {
|
||||||
|
return SizedBox(
|
||||||
|
height: _slotHeight,
|
||||||
|
child: _buildSlotContent(index, maxSlots),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSlotContent(int index, int maxSlots) {
|
||||||
|
if (index < children.length) {
|
||||||
|
return _OccupiedSlot(
|
||||||
|
child: children[index],
|
||||||
|
overCapacity: index >= maxSlots,
|
||||||
|
onOpen: () => onOpen(children[index]),
|
||||||
|
onDetach: () => onDetach(children[index]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (index < maxSlots) {
|
||||||
|
return const _EmptySlot();
|
||||||
|
}
|
||||||
|
return const _UnavailableSlot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SlotShell extends StatelessWidget {
|
||||||
|
final Color backgroundColor;
|
||||||
|
final Color borderColor;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _SlotShell({
|
||||||
|
required this.backgroundColor,
|
||||||
|
required this.borderColor,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox.expand(
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: borderColor),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UnavailableSlot extends StatelessWidget {
|
||||||
|
const _UnavailableSlot();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _SlotShell(
|
||||||
|
backgroundColor: Colors.grey.shade100,
|
||||||
|
borderColor: Colors.grey.shade200,
|
||||||
|
child: Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.block_outlined,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EmptySlot extends StatelessWidget {
|
||||||
|
const _EmptySlot();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _SlotShell(
|
||||||
|
backgroundColor: Colors.grey.shade50,
|
||||||
|
borderColor: Colors.grey.shade300,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'Place libre',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OccupiedSlot extends StatefulWidget {
|
||||||
|
final ParentChildSummary child;
|
||||||
|
final bool overCapacity;
|
||||||
|
final VoidCallback onOpen;
|
||||||
|
final VoidCallback onDetach;
|
||||||
|
|
||||||
|
const _OccupiedSlot({
|
||||||
|
required this.child,
|
||||||
|
required this.overCapacity,
|
||||||
|
required this.onOpen,
|
||||||
|
required this.onDetach,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_OccupiedSlot> createState() => _OccupiedSlotState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OccupiedSlotState extends State<_OccupiedSlot> {
|
||||||
|
bool _hovered = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final age = formatChildAgeLabel(
|
||||||
|
birthDate: widget.child.birthDate,
|
||||||
|
dueDate: widget.child.dueDate,
|
||||||
|
status: widget.child.status,
|
||||||
|
);
|
||||||
|
final avatarUrl = ApiConfig.absoluteMediaUrl(widget.child.photoUrl ?? '');
|
||||||
|
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (_) => setState(() => _hovered = true),
|
||||||
|
onExit: (_) => setState(() => _hovered = false),
|
||||||
|
child: _SlotShell(
|
||||||
|
backgroundColor: widget.overCapacity
|
||||||
|
? Colors.red.shade50
|
||||||
|
: const Color(0xFFF8F5FC),
|
||||||
|
borderColor: widget.overCapacity
|
||||||
|
? Colors.red.shade300
|
||||||
|
: const Color(0xFFD8CCE8),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildAvatar(avatarUrl),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
fit: FlexFit.loose,
|
||||||
|
child: Text(
|
||||||
|
widget.child.fullName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (age.isNotEmpty) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
age,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 120),
|
||||||
|
opacity: _hovered ? 1 : 0,
|
||||||
|
child: IgnorePointer(
|
||||||
|
ignoring: !_hovered,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Voir / modifier',
|
||||||
|
icon: const Icon(Icons.visibility_outlined, size: 20),
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 32,
|
||||||
|
minHeight: 32,
|
||||||
|
),
|
||||||
|
onPressed: widget.onOpen,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Détacher',
|
||||||
|
icon: Icon(
|
||||||
|
Icons.link_off,
|
||||||
|
size: 20,
|
||||||
|
color: Colors.orange.shade800,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 32,
|
||||||
|
minHeight: 32,
|
||||||
|
),
|
||||||
|
onPressed: widget.onDetach,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAvatar(String url) {
|
||||||
|
const size = 28.0;
|
||||||
|
const bg = Color(0xFFEDE5FA);
|
||||||
|
const iconColor = Color(0xFF6B3FA0);
|
||||||
|
|
||||||
|
if (url.isEmpty) {
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
backgroundColor: bg,
|
||||||
|
child: const Icon(Icons.child_care_outlined, size: 15, color: iconColor),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClipOval(
|
||||||
|
child: SizedBox(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
child: AuthNetworkImage(
|
||||||
|
url: url,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (_, __, ___) => ColoredBox(
|
||||||
|
color: bg,
|
||||||
|
child: const Icon(Icons.child_care_outlined, size: 15, color: iconColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,9 +8,9 @@ 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';
|
||||||
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_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_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_child_detail_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_children_affiliation_panel.dart';
|
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_status_capsule.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/admin/common/validation_detail_section.dart';
|
||||||
import 'package:p_tits_pas/widgets/common/identity_block.dart';
|
import 'package:p_tits_pas/widgets/common/identity_block.dart';
|
||||||
@ -51,8 +51,8 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
|
|
||||||
late String _statut;
|
late String _statut;
|
||||||
late bool _disponible;
|
late bool _disponible;
|
||||||
|
late int? _placesAvailable;
|
||||||
late List<ParentChildSummary> _children;
|
late List<ParentChildSummary> _children;
|
||||||
late final ScrollController _childrenScrollCtrl;
|
|
||||||
|
|
||||||
bool _saving = false;
|
bool _saving = false;
|
||||||
bool _dirty = false;
|
bool _dirty = false;
|
||||||
@ -64,12 +64,24 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
static const double _proTabHeight = 300;
|
static const double _proTabHeight = 300;
|
||||||
static const List<int> _photoProRowLayout = [2, 2, 2];
|
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) {
|
double _tabViewHeight(int index) {
|
||||||
switch (index) {
|
switch (index) {
|
||||||
case 1:
|
case 1:
|
||||||
return _proTabHeight;
|
return _proTabHeight;
|
||||||
case 2:
|
case 2:
|
||||||
return 280;
|
return _childrenTabHeight();
|
||||||
default:
|
default:
|
||||||
return 292;
|
return 292;
|
||||||
}
|
}
|
||||||
@ -110,8 +122,8 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
);
|
);
|
||||||
_statut = u.statut ?? 'en_attente';
|
_statut = u.statut ?? 'en_attente';
|
||||||
_disponible = am.available ?? true;
|
_disponible = am.available ?? true;
|
||||||
|
_placesAvailable = am.placesAvailable;
|
||||||
_children = List.of(am.children);
|
_children = List.of(am.children);
|
||||||
_childrenScrollCtrl = ScrollController();
|
|
||||||
|
|
||||||
for (final c in [
|
for (final c in [
|
||||||
_nomCtrl,
|
_nomCtrl,
|
||||||
@ -167,7 +179,6 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
]) {
|
]) {
|
||||||
c.dispose();
|
c.dispose();
|
||||||
}
|
}
|
||||||
_childrenScrollCtrl.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,19 +228,27 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
|
|
||||||
bool _placesInconsistent() => amHasPlacesInconsistency(
|
bool _placesInconsistent() => amHasPlacesInconsistency(
|
||||||
maxChildren: _capaciteMax(),
|
maxChildren: _capaciteMax(),
|
||||||
placesAvailable: widget.assistante.placesAvailable,
|
placesAvailable: _placesAvailable,
|
||||||
childrenCount: _children.length,
|
childrenCount: _children.length,
|
||||||
);
|
);
|
||||||
|
|
||||||
String _placesDisplayValue() {
|
String _placesDisplayValue() {
|
||||||
final stored = widget.assistante.placesAvailable;
|
if (_placesAvailable != null) return _placesAvailable.toString();
|
||||||
if (stored != null) return stored.toString();
|
|
||||||
return '–';
|
return '–';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _applyPlacesCorrection() {
|
||||||
|
final expected = _computedPlacesAvailable();
|
||||||
|
if (expected == null) return;
|
||||||
|
setState(() {
|
||||||
|
_placesAvailable = expected;
|
||||||
|
_dirty = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
String? _placesInconsistencyMessage() {
|
String? _placesInconsistencyMessage() {
|
||||||
if (!_placesInconsistent()) return null;
|
if (!_placesInconsistent()) return null;
|
||||||
final stored = widget.assistante.placesAvailable;
|
final stored = _placesAvailable;
|
||||||
final expected = _computedPlacesAvailable();
|
final expected = _computedPlacesAvailable();
|
||||||
final storedLabel = stored?.toString() ?? 'non renseigné';
|
final storedLabel = stored?.toString() ?? 'non renseigné';
|
||||||
final expectedLabel = expected?.toString() ?? '–';
|
final expectedLabel = expected?.toString() ?? '–';
|
||||||
@ -289,6 +308,8 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
if (_frDateToIso(_dateAgrementCtrl.text) != null)
|
if (_frDateToIso(_dateAgrementCtrl.text) != null)
|
||||||
'agreement_date': _frDateToIso(_dateAgrementCtrl.text),
|
'agreement_date': _frDateToIso(_dateAgrementCtrl.text),
|
||||||
if (_capaciteMax() != null) 'max_children': _capaciteMax(),
|
if (_capaciteMax() != null) 'max_children': _capaciteMax(),
|
||||||
|
if (_placesAvailable != widget.assistante.placesAvailable)
|
||||||
|
'places_available': _placesAvailable,
|
||||||
'available': _disponible,
|
'available': _disponible,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -363,9 +384,7 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => Navigator.pop(ctx, true),
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ValidationModalTheme.primaryElevatedStyle,
|
||||||
backgroundColor: Colors.red.shade700,
|
|
||||||
),
|
|
||||||
child: const Text('Détacher'),
|
child: const Text('Détacher'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -470,13 +489,11 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
return ValidationFormGrid(
|
return ValidationFormGrid(
|
||||||
title: 'Dossier professionnel',
|
title: 'Dossier professionnel',
|
||||||
rowLayout: _photoProRowLayout,
|
rowLayout: _photoProRowLayout,
|
||||||
compact: true,
|
|
||||||
fields: [
|
fields: [
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'NIR',
|
label: 'NIR',
|
||||||
field: ValidationEditableField(
|
field: ValidationEditableField(
|
||||||
controller: _nirCtrl,
|
controller: _nirCtrl,
|
||||||
compact: true,
|
|
||||||
hintText: '15 chiffres',
|
hintText: '15 chiffres',
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.allow(RegExp(r'[\d\s]')),
|
FilteringTextInputFormatter.allow(RegExp(r'[\d\s]')),
|
||||||
@ -487,7 +504,6 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
label: 'Date de naissance',
|
label: 'Date de naissance',
|
||||||
field: ValidationEditableField(
|
field: ValidationEditableField(
|
||||||
controller: _dateNaissanceCtrl,
|
controller: _dateNaissanceCtrl,
|
||||||
compact: true,
|
|
||||||
hintText: 'jj/mm/aaaa',
|
hintText: 'jj/mm/aaaa',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -495,28 +511,24 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
label: 'Ville de naissance',
|
label: 'Ville de naissance',
|
||||||
field: ValidationEditableField(
|
field: ValidationEditableField(
|
||||||
controller: _lieuNaissanceVilleCtrl,
|
controller: _lieuNaissanceVilleCtrl,
|
||||||
compact: true,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Pays de naissance',
|
label: 'Pays de naissance',
|
||||||
field: ValidationEditableField(
|
field: ValidationEditableField(
|
||||||
controller: _lieuNaissancePaysCtrl,
|
controller: _lieuNaissancePaysCtrl,
|
||||||
compact: true,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'N° Agrément',
|
label: 'N° Agrément',
|
||||||
field: ValidationEditableField(
|
field: ValidationEditableField(
|
||||||
controller: _agrementCtrl,
|
controller: _agrementCtrl,
|
||||||
compact: true,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Date d\'agrément',
|
label: 'Date d\'agrément',
|
||||||
field: ValidationEditableField(
|
field: ValidationEditableField(
|
||||||
controller: _dateAgrementCtrl,
|
controller: _dateAgrementCtrl,
|
||||||
compact: true,
|
|
||||||
hintText: 'jj/mm/aaaa',
|
hintText: 'jj/mm/aaaa',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -583,7 +595,6 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
ValidationEditableSection(
|
ValidationEditableSection(
|
||||||
compact: true,
|
|
||||||
rowLayout: const [2],
|
rowLayout: const [2],
|
||||||
fields: [
|
fields: [
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
@ -592,14 +603,12 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
controller: _capaciteCtrl,
|
controller: _capaciteCtrl,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
compact: true,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ValidationLabeledField(
|
ValidationLabeledField(
|
||||||
label: 'Places disponibles',
|
label: 'Places disponibles',
|
||||||
field: ValidationReadOnlyField(
|
field: ValidationReadOnlyField(
|
||||||
value: _placesDisplayValue(),
|
value: _placesDisplayValue(),
|
||||||
compact: true,
|
|
||||||
error: inconsistent,
|
error: inconsistent,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -607,13 +616,37 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
),
|
),
|
||||||
if (inconsistent) ...[
|
if (inconsistent) ...[
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Wrap(
|
||||||
_placesInconsistencyMessage()!,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
style: TextStyle(
|
spacing: 6,
|
||||||
fontSize: 12,
|
runSpacing: 4,
|
||||||
color: Colors.red.shade700,
|
children: [
|
||||||
height: 1.3,
|
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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@ -621,6 +654,7 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _childrenTab() {
|
Widget _childrenTab() {
|
||||||
|
final capacity = (_capaciteMax() ?? 4).clamp(1, 4);
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
@ -634,20 +668,12 @@ class _AdminAmEditModalState extends State<AdminAmEditModal>
|
|||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
|
||||||
Text(
|
|
||||||
'Enfants rattachés à cette assistante (accueil / garde).',
|
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade700),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Expanded(
|
AdminAmChildrenCapacityGrid(
|
||||||
child: AdminChildrenAffiliationPanel(
|
children: _children,
|
||||||
children: _children,
|
capacity: capacity,
|
||||||
scrollController: _childrenScrollCtrl,
|
onOpen: _openChild,
|
||||||
onOpen: _openChild,
|
onDetach: _detachChild,
|
||||||
onDetach: _detachChild,
|
|
||||||
emptyMessage: 'Aucun enfant rattaché à cette assistante',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
|
import 'package:p_tits_pas/utils/enfant_status_utils.dart';
|
||||||
|
|
||||||
/// Fiche enfant consultation / édition (ticket #138).
|
/// Fiche enfant consultation / édition (ticket #138).
|
||||||
class AdminChildDetailModal extends StatefulWidget {
|
class AdminChildDetailModal extends StatefulWidget {
|
||||||
@ -29,7 +30,6 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
bool _dirty = false;
|
bool _dirty = false;
|
||||||
bool _saving = false;
|
bool _saving = false;
|
||||||
|
|
||||||
static const _statuses = ['a_naitre', 'actif', 'scolarise'];
|
|
||||||
static const _genders = ['H', 'F', 'Autre'];
|
static const _genders = ['H', 'F', 'Autre'];
|
||||||
|
|
||||||
static const _labelStyle = TextStyle(
|
static const _labelStyle = TextStyle(
|
||||||
@ -46,7 +46,10 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
_nomCtrl = TextEditingController(text: e.lastName ?? '');
|
_nomCtrl = TextEditingController(text: e.lastName ?? '');
|
||||||
_birthCtrl = TextEditingController(text: e.birthDate ?? '');
|
_birthCtrl = TextEditingController(text: e.birthDate ?? '');
|
||||||
_dueCtrl = TextEditingController(text: e.dueDate ?? '');
|
_dueCtrl = TextEditingController(text: e.dueDate ?? '');
|
||||||
_status = _statuses.contains(e.status) ? e.status : 'actif';
|
_status = normalizeEnfantStatus(e.status);
|
||||||
|
if (!enfantStatusValues.contains(_status)) {
|
||||||
|
_status = 'sans_garde';
|
||||||
|
}
|
||||||
_gender = _normalizeGender(e.gender);
|
_gender = _normalizeGender(e.gender);
|
||||||
_consentPhoto = e.consentPhoto;
|
_consentPhoto = e.consentPhoto;
|
||||||
_isMultiple = e.isMultiple;
|
_isMultiple = e.isMultiple;
|
||||||
@ -213,8 +216,13 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
child: _labeledDropdown(
|
child: _labeledDropdown(
|
||||||
'Statut',
|
'Statut',
|
||||||
_status,
|
_status,
|
||||||
_statuses
|
enfantStatusValues
|
||||||
.map((s) => MapEntry(s, _statusLabel(s)))
|
.map(
|
||||||
|
(s) => MapEntry(
|
||||||
|
s,
|
||||||
|
enfantStatusLabel(s, gender: _gender),
|
||||||
|
),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
(v) => setState(() {
|
(v) => setState(() {
|
||||||
_status = v;
|
_status = v;
|
||||||
@ -388,19 +396,6 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _statusLabel(String status) {
|
|
||||||
switch (status) {
|
|
||||||
case 'a_naitre':
|
|
||||||
return 'À naître';
|
|
||||||
case 'actif':
|
|
||||||
return 'Actif';
|
|
||||||
case 'scolarise':
|
|
||||||
return 'Scolarisé';
|
|
||||||
default:
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _genderLabel(String gender) {
|
String _genderLabel(String gender) {
|
||||||
switch (gender) {
|
switch (gender) {
|
||||||
case 'H':
|
case 'H':
|
||||||
|
|||||||
@ -2,36 +2,26 @@ import 'package:flutter/material.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/date_display_utils.dart';
|
import 'package:p_tits_pas/utils/date_display_utils.dart';
|
||||||
|
import 'package:p_tits_pas/utils/enfant_status_utils.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';
|
||||||
|
|
||||||
String enfantStatusLabel(String status) {
|
|
||||||
switch (status) {
|
|
||||||
case 'a_naitre':
|
|
||||||
return 'À naître';
|
|
||||||
case 'actif':
|
|
||||||
return 'Actif';
|
|
||||||
case 'scolarise':
|
|
||||||
return 'Scolarisé';
|
|
||||||
default:
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> enfantAdminSubtitleLines({
|
List<String> enfantAdminSubtitleLines({
|
||||||
required String status,
|
required String status,
|
||||||
String? birthDate,
|
String? birthDate,
|
||||||
String? dueDate,
|
String? dueDate,
|
||||||
|
String? gender,
|
||||||
List<String> extra = const [],
|
List<String> extra = const [],
|
||||||
}) {
|
}) {
|
||||||
final lines = <String>[];
|
final lines = <String>[];
|
||||||
final age = formatChildAgeLabel(
|
final age = formatChildAgeLabel(
|
||||||
birthDate: birthDate,
|
birthDate: birthDate,
|
||||||
dueDate: dueDate,
|
dueDate: dueDate,
|
||||||
status: status,
|
status: normalizeEnfantStatus(status),
|
||||||
);
|
);
|
||||||
if (age.isNotEmpty) lines.add(age);
|
if (age.isNotEmpty) lines.add(age);
|
||||||
if (status.isNotEmpty) {
|
final normalized = normalizeEnfantStatus(status);
|
||||||
lines.add('Statut : ${enfantStatusLabel(status)}');
|
if (normalized.isNotEmpty) {
|
||||||
|
lines.add('Statut : ${enfantStatusLabel(normalized, gender: gender)}');
|
||||||
}
|
}
|
||||||
lines.addAll(extra);
|
lines.addAll(extra);
|
||||||
return lines;
|
return lines;
|
||||||
@ -67,6 +57,7 @@ class AdminEnfantUserCard extends StatelessWidget {
|
|||||||
status: enfant.status,
|
status: enfant.status,
|
||||||
birthDate: enfant.birthDate,
|
birthDate: enfant.birthDate,
|
||||||
dueDate: enfant.dueDate,
|
dueDate: enfant.dueDate,
|
||||||
|
gender: enfant.gender,
|
||||||
extra: [
|
extra: [
|
||||||
if (parents.isNotEmpty) 'Responsables : $parents',
|
if (parents.isNotEmpty) 'Responsables : $parents',
|
||||||
...extraSubtitleLines,
|
...extraSubtitleLines,
|
||||||
|
|||||||
@ -230,9 +230,7 @@ class _AdminParentEditModalState extends State<AdminParentEditModal> {
|
|||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => Navigator.pop(ctx, true),
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ValidationModalTheme.primaryElevatedStyle,
|
||||||
backgroundColor: Colors.red.shade700,
|
|
||||||
),
|
|
||||||
child: const Text('Détacher'),
|
child: const Text('Détacher'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -148,11 +148,30 @@ class ValidationFieldDecoration {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static BoxDecoration container() {
|
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(
|
return BoxDecoration(
|
||||||
color: Colors.grey.shade50,
|
color: error ? Colors.red.shade50 : Colors.grey.shade50,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
border: Border.all(
|
||||||
|
color: error ? Colors.red.shade400 : Colors.grey.shade300,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,20 +253,24 @@ class ValidationEditableField extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!compact || maxLines > 1) {
|
if (maxLines > 1) {
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
inputFormatters: inputFormatters,
|
inputFormatters: inputFormatters,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
style: TextStyle(
|
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
||||||
color: Colors.black87,
|
decoration: ValidationFieldDecoration.input(hint: hintText),
|
||||||
fontSize: compact ? 13 : 14,
|
);
|
||||||
),
|
}
|
||||||
decoration: ValidationFieldDecoration.input(
|
if (!compact) {
|
||||||
hint: hintText,
|
return TextField(
|
||||||
compact: compact,
|
controller: controller,
|
||||||
),
|
keyboardType: keyboardType,
|
||||||
|
inputFormatters: inputFormatters,
|
||||||
|
maxLines: 1,
|
||||||
|
style: const TextStyle(color: Colors.black87, fontSize: 14),
|
||||||
|
decoration: ValidationFieldDecoration.input(hint: hintText),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
@ -259,10 +282,11 @@ class ValidationEditableField extends StatelessWidget {
|
|||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
inputFormatters: inputFormatters,
|
inputFormatters: inputFormatters,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
textAlignVertical: TextAlignVertical.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.black87,
|
color: Colors.black87,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
height: 1.15,
|
height: 1.0,
|
||||||
),
|
),
|
||||||
decoration: _compactInputDecoration(hint: hintText),
|
decoration: _compactInputDecoration(hint: hintText),
|
||||||
),
|
),
|
||||||
@ -297,8 +321,8 @@ class ValidationEditableSection extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Champ texte en lecture seule, style formulaire (fond gris léger, bordure).
|
/// Champ texte en lecture seule, même coque [TextField] que [ValidationEditableField].
|
||||||
class ValidationReadOnlyField extends StatelessWidget {
|
class ValidationReadOnlyField extends StatefulWidget {
|
||||||
final String value;
|
final String value;
|
||||||
final int? maxLines;
|
final int? maxLines;
|
||||||
final bool compact;
|
final bool compact;
|
||||||
@ -312,34 +336,69 @@ class ValidationReadOnlyField extends StatelessWidget {
|
|||||||
this.error = false,
|
this.error = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ValidationReadOnlyField> createState() => _ValidationReadOnlyFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ValidationReadOnlyFieldState extends State<ValidationReadOnlyField> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
static const double _compactFieldHeight = 34;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: compact && maxLines == 1 ? _compactFieldHeight : null,
|
height: widget.compact && widget.maxLines == 1 ? _compactFieldHeight : null,
|
||||||
alignment: compact ? Alignment.centerLeft : null,
|
alignment: widget.compact ? Alignment.centerLeft : null,
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
horizontal: compact ? 10 : 12,
|
horizontal: widget.compact ? 10 : 12,
|
||||||
vertical: compact ? 7 : 10,
|
vertical: widget.compact ? 7 : 10,
|
||||||
),
|
),
|
||||||
decoration: error
|
decoration: ValidationFieldDecoration.container(error: widget.error),
|
||||||
? BoxDecoration(
|
|
||||||
color: Colors.red.shade50,
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
border: Border.all(color: Colors.red.shade400),
|
|
||||||
)
|
|
||||||
: ValidationFieldDecoration.container(),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
value,
|
widget.value,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: error ? Colors.red.shade800 : Colors.black87,
|
color: widget.error ? Colors.red.shade800 : Colors.black87,
|
||||||
fontSize: compact ? 13 : 14,
|
fontSize: widget.compact ? 13 : 14,
|
||||||
height: compact ? 1.2 : null,
|
height: widget.compact ? 1.0 : null,
|
||||||
fontWeight: error ? FontWeight.w600 : null,
|
fontWeight: widget.error ? FontWeight.w600 : null,
|
||||||
),
|
),
|
||||||
maxLines: maxLines,
|
maxLines: widget.maxLines,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:p_tits_pas/utils/enfant_status_utils.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/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart';
|
import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart';
|
||||||
@ -67,8 +68,9 @@ class _EnfantManagementWidgetState extends State<EnfantManagementWidget> {
|
|||||||
final query = widget.searchQuery.toLowerCase();
|
final query = widget.searchQuery.toLowerCase();
|
||||||
final filtered = _enfants.where((e) {
|
final filtered = _enfants.where((e) {
|
||||||
final matchesName = e.fullName.toLowerCase().contains(query);
|
final matchesName = e.fullName.toLowerCase().contains(query);
|
||||||
final matchesStatus =
|
final matchesStatus = widget.statusFilter == null ||
|
||||||
widget.statusFilter == null || e.status == widget.statusFilter;
|
normalizeEnfantStatus(e.status) ==
|
||||||
|
normalizeEnfantStatus(widget.statusFilter);
|
||||||
return matchesName && matchesStatus;
|
return matchesName && matchesStatus;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
|||||||
@ -205,10 +205,17 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
DropdownMenuItem<String?>(
|
DropdownMenuItem<String?>(
|
||||||
value: 'actif',
|
value: 'sans_garde',
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(left: 10),
|
padding: EdgeInsets.only(left: 10),
|
||||||
child: Text('Actif', style: TextStyle(fontSize: 12)),
|
child: Text('Sans garde', style: TextStyle(fontSize: 12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem<String?>(
|
||||||
|
value: 'garde',
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(left: 10),
|
||||||
|
child: Text('En garde', style: TextStyle(fontSize: 12)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
DropdownMenuItem<String?>(
|
DropdownMenuItem<String?>(
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:p_tits_pas/models/dossier_unifie.dart';
|
import 'package:p_tits_pas/models/dossier_unifie.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/enfant_status_utils.dart';
|
||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
|
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
|
||||||
@ -397,20 +398,9 @@ class _ValidationFamilyWizardState extends State<ValidationFamilyWizard> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// « Scolarisé » / « Scolarisée » selon le genre enfant (`F` / sinon masculin par défaut).
|
/// Statut dans la colonne 2/3 (scolarisé·e, à naître, sans garde, en garde).
|
||||||
static String _scolariseAccordeAuGenre(String? gender) {
|
|
||||||
final g = (gender ?? '').trim().toUpperCase();
|
|
||||||
if (g == 'F') return 'Scolarisée';
|
|
||||||
return 'Scolarisé';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Statut dans la colonne 2/3 uniquement (pas de [ValidationReadOnlyField]) : scolarisé·e ou « À naître ».
|
|
||||||
/// `actif` : pas de ligne statut.
|
|
||||||
String? _enfantColumnStatusLabel(EnfantDossier e) {
|
String? _enfantColumnStatusLabel(EnfantDossier e) {
|
||||||
final s = (e.status ?? '').trim().toLowerCase();
|
return enfantColumnStatusLabel(status: e.status, gender: e.gender);
|
||||||
if (s == 'a_naitre') return 'À naître';
|
|
||||||
if (s == 'scolarise') return _scolariseAccordeAuGenre(e.gender);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nom ; date de naissance et genre sur une ligne (prénom au-dessus, pleine largeur).
|
/// Nom ; date de naissance et genre sur une ligne (prénom au-dessus, pleine largeur).
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user