fix(#138): carte AM dédiée, titre placement et consent_photo au payload.
Améliore la zone AM/scolarisation (carte dédiée + titre), envoie consent_photo à l'inscription parent, et parse le consentement enfant de façon plus robuste (heuristique photo tant que le back ne persiste pas). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e65ae4cebe
commit
16001f695b
@ -48,17 +48,28 @@ class EnfantAdminModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final photoUrl = json['photo_url'] as String? ?? json['photoUrl'] as String?;
|
||||||
|
final hasPhoto = (photoUrl ?? '').trim().isNotEmpty;
|
||||||
|
// L'inscription parent exigeait le consentement pour envoyer la photo, mais
|
||||||
|
// le back a longtemps forcé consent_photo=false (voir reprise_mapper).
|
||||||
|
final consentFromApi = _parseBool(json['consent_photo']) ||
|
||||||
|
_parseBool(json['consentement_photo']) ||
|
||||||
|
_parseBool(json['consentPhoto']);
|
||||||
|
|
||||||
return EnfantAdminModel(
|
return EnfantAdminModel(
|
||||||
id: (json['id'] ?? '').toString(),
|
id: (json['id'] ?? '').toString(),
|
||||||
firstName: json['first_name'] as String?,
|
firstName: json['first_name'] as String? ?? json['prenom'] as String?,
|
||||||
lastName: json['last_name'] as String?,
|
lastName: json['last_name'] as String? ?? json['nom'] as String?,
|
||||||
gender: json['gender'] as String?,
|
gender: json['gender'] as String? ?? json['genre'] as String?,
|
||||||
birthDate: _dateString(json['birth_date']),
|
birthDate: _dateString(json['birth_date'] ?? json['date_naissance']),
|
||||||
dueDate: _dateString(json['due_date']),
|
dueDate: _dateString(json['due_date'] ?? json['date_prevue_naissance']),
|
||||||
status: normalizeEnfantStatus(json['status']?.toString()),
|
status: normalizeEnfantStatus(
|
||||||
photoUrl: json['photo_url'] as String?,
|
(json['status'] ?? json['statut'])?.toString(),
|
||||||
consentPhoto: json['consent_photo'] == true,
|
),
|
||||||
isMultiple: json['is_multiple'] == true,
|
photoUrl: photoUrl,
|
||||||
|
consentPhoto: consentFromApi || hasPhoto,
|
||||||
|
isMultiple: _parseBool(json['is_multiple']) ||
|
||||||
|
_parseBool(json['est_multiple']),
|
||||||
parentLinks: links,
|
parentLinks: links,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -76,6 +87,15 @@ class EnfantAdminModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool _parseBool(dynamic value) {
|
||||||
|
if (value == true || value == 1) return true;
|
||||||
|
if (value is String) {
|
||||||
|
final s = value.trim().toLowerCase();
|
||||||
|
return s == 'true' || s == '1' || s == 't' || s == 'yes';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static String? _dateString(dynamic v) {
|
static String? _dateString(dynamic v) {
|
||||||
if (v == null) return null;
|
if (v == null) return null;
|
||||||
if (v is String) return v.split('T').first;
|
if (v is String) return v.split('T').first;
|
||||||
|
|||||||
@ -198,6 +198,9 @@ class ParentRegistrationPayload {
|
|||||||
map['photo_base64'] = photo.$1;
|
map['photo_base64'] = photo.$1;
|
||||||
map['photo_filename'] = photo.$2;
|
map['photo_filename'] = photo.$2;
|
||||||
}
|
}
|
||||||
|
// Consentement photo enfant (le back l'ignore encore à l'inscription —
|
||||||
|
// à brancher côté API ; en attendant le front admin infère via photo).
|
||||||
|
map['consent_photo'] = c.photoConsent;
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
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/models/enfant_admin_model.dart';
|
import 'package:p_tits_pas/models/enfant_admin_model.dart';
|
||||||
|
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||||
import 'package:p_tits_pas/services/auth_service.dart';
|
import 'package:p_tits_pas/services/auth_service.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/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/utils/enfant_status_utils.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_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_user_card.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/admin/validation_modal_theme.dart';
|
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||||
|
|
||||||
/// Fiche enfant consultation / édition (ticket #138) — format paysage comme fiche AM.
|
/// Fiche enfant consultation / édition (ticket #138) — format paysage comme fiche AM.
|
||||||
class AdminChildDetailModal extends StatefulWidget {
|
class AdminChildDetailModal extends StatefulWidget {
|
||||||
@ -610,9 +611,15 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_fieldsGrid(),
|
_fieldsGrid(),
|
||||||
const Spacer(),
|
Expanded(
|
||||||
const SizedBox(height: 16),
|
child: Padding(
|
||||||
_placementSection(),
|
padding: const EdgeInsets.only(top: 16),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: _placementBlock(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -625,6 +632,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
Widget _scolariseCard() {
|
Widget _scolariseCard() {
|
||||||
return Container(
|
return Container(
|
||||||
height: _placementHeight,
|
height: _placementHeight,
|
||||||
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
@ -692,36 +700,54 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
child: Container(
|
child: Container(
|
||||||
height: _placementHeight,
|
height: _placementHeight,
|
||||||
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey.shade50,
|
color: Colors.grey.shade50,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.face_outlined, size: 28, color: Colors.grey.shade400),
|
Container(
|
||||||
const SizedBox(width: 12),
|
width: 56,
|
||||||
Column(
|
height: 56,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
decoration: BoxDecoration(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
color: Colors.white,
|
||||||
children: [
|
borderRadius: BorderRadius.circular(12),
|
||||||
Text(
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
'Aucune assistante maternelle',
|
),
|
||||||
style: TextStyle(
|
child: Icon(
|
||||||
fontSize: 14,
|
Icons.face_outlined,
|
||||||
fontWeight: FontWeight.w600,
|
size: 30,
|
||||||
color: Colors.grey.shade700,
|
color: Colors.grey.shade400,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
const SizedBox(width: 16),
|
||||||
'Cliquer pour choisir une assistante',
|
Expanded(
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
child: Column(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
],
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Aucune assistante maternelle',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Cliquer pour choisir une assistante',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
|
||||||
Icon(Icons.add_circle_outline, color: Colors.grey.shade500),
|
Icon(Icons.add_circle_outline, color: Colors.grey.shade500),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -732,42 +758,142 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
|
|
||||||
Widget _linkedAmCard() {
|
Widget _linkedAmCard() {
|
||||||
final am = _linkedAm!;
|
final am = _linkedAm!;
|
||||||
return SizedBox(
|
final zone = (am.residenceCity ?? '').trim();
|
||||||
height: _placementHeight,
|
final agrement = (am.approvalNumber ?? '').trim();
|
||||||
child: AdminUserCard(
|
final subtitle = [
|
||||||
margin: EdgeInsets.zero,
|
if (zone.isNotEmpty) 'Zone : $zone',
|
||||||
title: am.user.fullName,
|
if (agrement.isNotEmpty) 'Agrément : $agrement',
|
||||||
avatarUrl: am.user.photoUrl,
|
].join(' · ');
|
||||||
fallbackIcon: Icons.face,
|
|
||||||
onCardTap: _busy ? null : _openLinkedAm,
|
return Material(
|
||||||
subtitleLines: [
|
color: Colors.transparent,
|
||||||
if (am.residenceCity != null && am.residenceCity!.trim().isNotEmpty)
|
child: InkWell(
|
||||||
'Zone : ${am.residenceCity!.trim()}',
|
onTap: _busy ? null : _openLinkedAm,
|
||||||
if (am.approvalNumber != null && am.approvalNumber!.trim().isNotEmpty)
|
borderRadius: BorderRadius.circular(10),
|
||||||
'Agrément : ${am.approvalNumber!.trim()}',
|
child: Container(
|
||||||
],
|
height: _placementHeight,
|
||||||
actions: [
|
width: double.infinity,
|
||||||
IconButton(
|
decoration: BoxDecoration(
|
||||||
icon: const Icon(Icons.open_in_new),
|
color: const Color(0xFFF7F3FC),
|
||||||
tooltip: 'Ouvrir la fiche AM',
|
borderRadius: BorderRadius.circular(10),
|
||||||
onPressed: _busy ? null : _openLinkedAm,
|
border: Border.all(color: const Color(0xFFD4C4EF)),
|
||||||
),
|
),
|
||||||
IconButton(
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
icon: Icon(Icons.link_off, color: Colors.orange.shade800),
|
child: Row(
|
||||||
tooltip: 'Détacher',
|
children: [
|
||||||
onPressed: _busy ? null : _detachAm,
|
_amAvatar(am.user.photoUrl),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
am.user.fullName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Color(0xFF4A2F7A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (subtitle.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.open_in_new, size: 20),
|
||||||
|
tooltip: 'Ouvrir la fiche AM',
|
||||||
|
onPressed: _busy ? null : _openLinkedAm,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.link_off, size: 20, color: Colors.orange.shade800),
|
||||||
|
tooltip: 'Détacher',
|
||||||
|
onPressed: _busy ? null : _detachAm,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _amAvatar(String? photoUrl) {
|
||||||
|
const size = 56.0;
|
||||||
|
const bg = Color(0xFFEDE5FA);
|
||||||
|
const iconColor = Color(0xFF6B3FA0);
|
||||||
|
final url = ApiConfig.absoluteMediaUrl(photoUrl);
|
||||||
|
|
||||||
|
if (url.isEmpty) {
|
||||||
|
return Container(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bg,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.face, size: 30, color: iconColor),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: AuthNetworkImage(
|
||||||
|
url: url,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (_, __, ___) => Container(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
color: bg,
|
||||||
|
child: const Icon(Icons.face, size: 30, color: iconColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get _placementTitle =>
|
||||||
|
_isScolarise ? 'Scolarisation' : 'Assistante maternelle';
|
||||||
|
|
||||||
|
Widget _placementBlock() {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_placementTitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_placementSection(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _placementSection() {
|
Widget _placementSection() {
|
||||||
if (!_showsAmPlacement) return _scolariseCard();
|
if (!_showsAmPlacement) return _scolariseCard();
|
||||||
|
|
||||||
if (_loadingAm || _placementBusy) {
|
if (_loadingAm || _placementBusy) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: _placementHeight,
|
height: _placementHeight,
|
||||||
|
width: double.infinity,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 24,
|
width: 24,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user