feat(#170): sélecteur couple enfant-parent(s) pour AM
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user