feat(#170): sélecteur couple enfant-parent(s) pour AM

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-24 12:19:26 +02:00
co-authored by Cursor
parent 34a516d509
commit 4261667198
2 changed files with 126 additions and 18 deletions
+114 -10
View File
@@ -1,11 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/couple_garde.dart';
import 'package:p_tits_pas/models/user.dart'; import 'package:p_tits_pas/models/user.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/couple_garde_service.dart';
import 'package:p_tits_pas/widgets/quotidien/couple_selector_bandeau.dart';
import 'package:p_tits_pas/widgets/quotidien/quotidien_shell.dart'; import 'package:p_tits_pas/widgets/quotidien/quotidien_shell.dart';
import 'package:p_tits_pas/widgets/quotidien/quotidien_theme.dart'; import 'package:p_tits_pas/widgets/quotidien/quotidien_theme.dart';
import 'package:p_tits_pas/screens/home/parent_screen/agenda_absences_stub.dart';
/// Dashboard assistante maternelle – coquille 3 colonnes quotidien (#169). /// Dashboard assistante maternelle – coquille 3 colonnes quotidien (#169).
/// Colonne gauche : sélecteur de couple enfant–parent(s) (à venir #170). /// Colonne gauche : sélecteur de couple enfant–parent(s) (#170).
/// Métier cartes / blog / messagerie : tickets C/D/E. /// Métier cartes / blog / messagerie : tickets C/D/E.
class AmDashboardScreen extends StatefulWidget { class AmDashboardScreen extends StatefulWidget {
const AmDashboardScreen({super.key}); const AmDashboardScreen({super.key});
@@ -18,10 +22,16 @@ class _AmDashboardScreenState extends State<AmDashboardScreen> {
QuotidienNavSection _section = QuotidienNavSection.liaison; QuotidienNavSection _section = QuotidienNavSection.liaison;
AppUser? _user; AppUser? _user;
List<CoupleGarde> _couples = const [];
String? _selectedCoupleId;
bool _couplesLoading = true;
String? _couplesError;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadUser(); _loadUser();
_loadCouples();
} }
Future<void> _loadUser() async { Future<void> _loadUser() async {
@@ -29,6 +39,53 @@ class _AmDashboardScreenState extends State<AmDashboardScreen> {
if (mounted) setState(() => _user = user); if (mounted) setState(() => _user = user);
} }
Future<void> _loadCouples() async {
setState(() {
_couplesLoading = true;
_couplesError = null;
});
try {
// TODO: Pour l'instant on utilise le service générique de test (CoupleGardeService.getCouplesGarde).
// Dans le futur, l'API pour l'AM (enfants_accueillis) fournira la vraie liste.
final res = await CoupleGardeService.getCouplesGarde();
if (!mounted) return;
setState(() {
_couples = res.couples;
_selectedCoupleId = res.coupleCourant?.id;
_couplesLoading = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_couplesError = e.toString().replaceFirst('Exception: ', '');
_couplesLoading = false;
});
}
}
Future<void> _selectCouple(CoupleGarde couple) async {
if (couple.id == _selectedCoupleId) return;
setState(() => _selectedCoupleId = couple.id);
try {
final res = await CoupleGardeService.definirCoupleCourant(couple.id);
if (!mounted) return;
setState(() {
_couples = res.couples;
_selectedCoupleId = res.coupleCourant?.id ?? couple.id;
});
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"Impossible de changer d'enfant : "
'${e.toString().replaceFirst('Exception: ', '')}',
),
),
);
}
}
String get _displayName { String get _displayName {
final n = _user?.fullName.trim() ?? ''; final n = _user?.fullName.trim() ?? '';
if (n.isNotEmpty) return n; if (n.isNotEmpty) return n;
@@ -54,11 +111,13 @@ class _AmDashboardScreenState extends State<AmDashboardScreen> {
onSettingsTap: () => _soon('Paramètres'), onSettingsTap: () => _soon('Paramètres'),
// L'AM n'a pas de bouton "Recherche AM" dans le bandeau // L'AM n'a pas de bouton "Recherche AM" dans le bandeau
onSearchAmTap: null, onSearchAmTap: null,
leftColumn: const QuotidienColumnPlaceholder( leftColumn: _LeftColumn(
title: 'Cartes', couples: _couples,
subtitle: selectedCoupleId: _selectedCoupleId,
'Couple enfant–parent(s) et flux de cartes\n(à brancher — tickets #170 / #174).', loading: _couplesLoading,
icon: Icons.style_outlined, error: _couplesError,
onRetry: _loadCouples,
onCoupleSelected: _selectCouple,
), ),
centerColumn: const QuotidienColumnPlaceholder( centerColumn: const QuotidienColumnPlaceholder(
title: 'Blog', title: 'Blog',
@@ -72,10 +131,7 @@ class _AmDashboardScreenState extends State<AmDashboardScreen> {
'Mess. Parents · Mess. RPE\n(à brancher — ticket #185).', 'Mess. Parents · Mess. RPE\n(à brancher — ticket #185).',
icon: Icons.chat_bubble_outline, icon: Icons.chat_bubble_outline,
), ),
agendaBody: const QuotidienStubPage( agendaBody: AgendaAbsencesStub(placementId: _selectedCoupleId),
title: 'Agenda',
message: 'Agenda — contenu à venir (stub #187).',
),
contratBody: const QuotidienStubPage( contratBody: const QuotidienStubPage(
title: 'Contrat', title: 'Contrat',
message: 'Contrat — contenu à venir (stub #187).', message: 'Contrat — contenu à venir (stub #187).',
@@ -84,3 +140,51 @@ class _AmDashboardScreenState extends State<AmDashboardScreen> {
} }
} }
class _LeftColumn extends StatelessWidget {
final List<CoupleGarde> couples;
final String? selectedCoupleId;
final bool loading;
final String? error;
final VoidCallback onRetry;
final ValueChanged<CoupleGarde> onCoupleSelected;
const _LeftColumn({
required this.couples,
required this.selectedCoupleId,
required this.loading,
required this.error,
required this.onRetry,
required this.onCoupleSelected,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
CoupleSelectorBandeau(
mode: CoupleBandeauMode.assistanteMaternelle,
couples: couples,
selectedCoupleId: selectedCoupleId,
loading: loading,
errorMessage: error,
onRetry: onRetry,
onCoupleSelected: onCoupleSelected,
),
const SizedBox(height: 14),
const Expanded(
child: QuotidienColumnPlaceholder(
title: 'Cartes',
subtitle:
'Absences, congés AM, sorties à valider\n(à brancher — ticket #174).',
icon: Icons.style_outlined,
),
),
],
),
);
}
}
@@ -135,6 +135,16 @@ class _CoupleCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Parent mode: enfant | AM
// AM mode: enfant | parent(s)
final isParentMode = mode == CoupleBandeauMode.parent;
final fallbackRoleIcon = isParentMode ? Icons.volunteer_activism : Icons.person_outline;
final fallbackRoleName = isParentMode ? 'Nounou' : 'Parent';
// TODO: In AM mode, we should ideally display "parent1+parent2 empilés" or centered.
// For now, CoupleGarde only gives us `am` (which in AM mode might just represent one parent, or the system needs to feed both parents here).
// Assuming backend will populate `am` field with the parent data when called by AM.
return Container( return Container(
height: 90, height: 90,
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -175,14 +185,8 @@ class _CoupleCard extends StatelessWidget {
photoUrl: couple.am.photoUrl, photoUrl: couple.am.photoUrl,
name: (couple.am.prenom != null && couple.am.prenom!.isNotEmpty) name: (couple.am.prenom != null && couple.am.prenom!.isNotEmpty)
? couple.am.prenom! ? couple.am.prenom!
: couple.am.displayName( : couple.am.displayName(fallback: fallbackRoleName),
fallback: mode == CoupleBandeauMode.parent fallbackIcon: fallbackRoleIcon,
? 'Nounou'
: 'Parent',
),
fallbackIcon: mode == CoupleBandeauMode.parent
? Icons.volunteer_activism
: Icons.person_outline,
), ),
), ),
), ),