Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91f2c3e1ef | ||
|
|
4007d7010e | ||
|
|
4261667198 | ||
|
|
15e2d8d850 | ||
|
|
34a516d509 |
@@ -39,13 +39,15 @@ class CoupleMembre {
|
|||||||
class CoupleGarde {
|
class CoupleGarde {
|
||||||
final String id;
|
final String id;
|
||||||
final CoupleMembre enfant;
|
final CoupleMembre enfant;
|
||||||
final CoupleMembre am;
|
final CoupleMembre? am;
|
||||||
|
final List<CoupleMembre> parents;
|
||||||
final bool courant;
|
final bool courant;
|
||||||
|
|
||||||
const CoupleGarde({
|
const CoupleGarde({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.enfant,
|
required this.enfant,
|
||||||
required this.am,
|
this.am,
|
||||||
|
this.parents = const [],
|
||||||
this.courant = false,
|
this.courant = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -55,9 +57,15 @@ class CoupleGarde {
|
|||||||
enfant: CoupleMembre.fromJson(
|
enfant: CoupleMembre.fromJson(
|
||||||
Map<String, dynamic>.from(json['enfant'] ?? const {}),
|
Map<String, dynamic>.from(json['enfant'] ?? const {}),
|
||||||
),
|
),
|
||||||
am: CoupleMembre.fromJson(
|
am: json['am'] != null
|
||||||
Map<String, dynamic>.from(json['am'] ?? const {}),
|
? CoupleMembre.fromJson(Map<String, dynamic>.from(json['am']))
|
||||||
),
|
: null,
|
||||||
|
parents: json['parents'] != null
|
||||||
|
? (json['parents'] as List)
|
||||||
|
.whereType<Map>()
|
||||||
|
.map((e) => CoupleMembre.fromJson(Map<String, dynamic>.from(e)))
|
||||||
|
.toList()
|
||||||
|
: const [],
|
||||||
courant: json['courant'] == true,
|
courant: json['courant'] == true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -67,6 +75,7 @@ class CoupleGarde {
|
|||||||
id: id,
|
id: id,
|
||||||
enfant: enfant,
|
enfant: enfant,
|
||||||
am: am,
|
am: am,
|
||||||
|
parents: parents,
|
||||||
courant: courant ?? this.courant,
|
courant: courant ?? this.courant,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
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/widgets/app_footer.dart';
|
import 'package:p_tits_pas/services/couple_garde_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/dashboard/dashboard_bandeau.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_theme.dart';
|
||||||
|
import 'package:p_tits_pas/screens/home/parent_screen/agenda_absences_stub.dart';
|
||||||
|
|
||||||
/// Dashboard assistante maternelle – page blanche avec bandeau générique.
|
/// Dashboard assistante maternelle – coquille 3 colonnes quotidien (#169).
|
||||||
/// Contenu détaillé à venir.
|
/// Colonne gauche : sélecteur de couple enfant–parent(s) (#170).
|
||||||
|
/// 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});
|
||||||
|
|
||||||
@@ -14,13 +19,19 @@ class AmDashboardScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AmDashboardScreenState extends State<AmDashboardScreen> {
|
class _AmDashboardScreenState extends State<AmDashboardScreen> {
|
||||||
int selectedTabIndex = 0;
|
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 {
|
||||||
@@ -28,51 +39,150 @@ 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 {
|
||||||
|
final res = await CoupleGardeService.getAmCouplesGarde();
|
||||||
|
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.definirAmCoupleCourant(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 {
|
||||||
|
final n = _user?.fullName.trim() ?? '';
|
||||||
|
if (n.isNotEmpty) return n;
|
||||||
|
final email = _user?.email.trim() ?? '';
|
||||||
|
if (email.isNotEmpty) return email.split('@').first;
|
||||||
|
return 'Assistante maternelle';
|
||||||
|
}
|
||||||
|
|
||||||
|
void _soon(String label) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('$label — à venir')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return QuotidienShell(
|
||||||
appBar: PreferredSize(
|
selectedSection: _section,
|
||||||
preferredSize: const Size.fromHeight(60.0),
|
onSectionSelected: (s) => setState(() => _section = s),
|
||||||
child: DashboardBandeau(
|
userDisplayName: _displayName,
|
||||||
tabItems: const [
|
userEmail: _user?.email,
|
||||||
DashboardTabItem(label: 'Mon tableau de bord'),
|
onProfileTap: () => _soon('Profil'),
|
||||||
DashboardTabItem(label: 'Paramètres'),
|
onSettingsTap: () => _soon('Paramètres'),
|
||||||
],
|
// L'AM n'a pas de bouton "Recherche AM" dans le bandeau
|
||||||
selectedTabIndex: selectedTabIndex,
|
onSearchAmTap: null,
|
||||||
onTabSelected: (index) => setState(() => selectedTabIndex = index),
|
leftColumn: _LeftColumn(
|
||||||
userDisplayName: _user?.fullName.isNotEmpty == true
|
couples: _couples,
|
||||||
? _user!.fullName
|
selectedCoupleId: _selectedCoupleId,
|
||||||
: 'Assistante maternelle',
|
loading: _couplesLoading,
|
||||||
userEmail: _user?.email,
|
error: _couplesError,
|
||||||
userRole: _user?.role,
|
onRetry: _loadCouples,
|
||||||
onProfileTap: () {
|
onCoupleSelected: _selectCouple,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('Modification du profil – à venir')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSettingsTap: () {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Paramètres – à venir')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onLogout: () {},
|
|
||||||
showLogoutConfirmation: true,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
body: Column(
|
centerColumn: const QuotidienColumnPlaceholder(
|
||||||
|
title: 'Blog',
|
||||||
|
subtitle:
|
||||||
|
'Fil du quotidien (affichage par défaut)\n(à brancher — ticket #179).',
|
||||||
|
icon: Icons.auto_stories_outlined,
|
||||||
|
),
|
||||||
|
rightColumn: const QuotidienColumnPlaceholder(
|
||||||
|
title: 'Messagerie',
|
||||||
|
subtitle:
|
||||||
|
'Mess. Parents · Mess. RPE\n(à brancher — ticket #185).',
|
||||||
|
icon: Icons.chat_bubble_outline,
|
||||||
|
),
|
||||||
|
agendaBody: AgendaAbsencesStub(placementId: _selectedCoupleId),
|
||||||
|
contratBody: const QuotidienStubPage(
|
||||||
|
title: 'Contrat',
|
||||||
|
message: 'Contrat — contenu à venir (stub #187).',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: [
|
children: [
|
||||||
Expanded(
|
CoupleSelectorBandeau(
|
||||||
child: Center(
|
mode: CoupleBandeauMode.assistanteMaternelle,
|
||||||
child: Text(
|
couples: couples,
|
||||||
'Dashboard AM – à venir',
|
selectedCoupleId: selectedCoupleId,
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const AppFooter(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,11 @@ class ApiConfig {
|
|||||||
static const String parentsCoupleGardeCourant =
|
static const String parentsCoupleGardeCourant =
|
||||||
'/parents/me/couples-garde/courant';
|
'/parents/me/couples-garde/courant';
|
||||||
static const String assistantesMaternelles = '/assistantes-maternelles';
|
static const String assistantesMaternelles = '/assistantes-maternelles';
|
||||||
/// Création dossier AM actif par le staff (#156) — body type register AM.
|
/// Couples de garde de l'AM connectée (#170 / #171).
|
||||||
|
static const String assistantesMaternellesCouplesGarde =
|
||||||
|
'/assistantes-maternelles/me/couples-garde';
|
||||||
|
static const String assistantesMaternellesCoupleGardeCourant =
|
||||||
|
'/assistantes-maternelles/me/couples-garde/courant';
|
||||||
static const String assistantesMaternellesDossier =
|
static const String assistantesMaternellesDossier =
|
||||||
'/assistantes-maternelles/dossier';
|
'/assistantes-maternelles/dossier';
|
||||||
static const String enfants = '/enfants';
|
static const String enfants = '/enfants';
|
||||||
|
|||||||
@@ -73,4 +73,45 @@ class CoupleGardeService {
|
|||||||
Map<String, dynamic>.from(decoded as Map),
|
Map<String, dynamic>.from(decoded as Map),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Liste les couples de garde et le couple courant de l'AM connectée.
|
||||||
|
static Future<CouplesGardeResponse> getAmCouplesGarde() async {
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.assistantesMaternellesCouplesGarde}'),
|
||||||
|
headers: await _headers(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception(
|
||||||
|
_extractError(response.body, 'Erreur chargement des couples de garde (AM)'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final decoded = jsonDecode(response.body);
|
||||||
|
return CouplesGardeResponse.fromJson(
|
||||||
|
Map<String, dynamic>.from(decoded as Map),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persiste le couple courant (AM) et renvoie la liste à jour.
|
||||||
|
static Future<CouplesGardeResponse> definirAmCoupleCourant(
|
||||||
|
String coupleId,
|
||||||
|
) async {
|
||||||
|
final response = await http.put(
|
||||||
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.assistantesMaternellesCoupleGardeCourant}'),
|
||||||
|
headers: await _headers(),
|
||||||
|
body: jsonEncode({'couple_id': coupleId}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception(
|
||||||
|
_extractError(response.body, 'Erreur sélection du couple de garde (AM)'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final decoded = jsonDecode(response.body);
|
||||||
|
return CouplesGardeResponse.fromJson(
|
||||||
|
Map<String, dynamic>.from(decoded as Map),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class CoupleSelectorBandeau extends StatelessWidget {
|
|||||||
|
|
||||||
CoupleGarde? get _selected {
|
CoupleGarde? get _selected {
|
||||||
if (couples.isEmpty) return null;
|
if (couples.isEmpty) return null;
|
||||||
if (selectedCoupleId != null) {
|
if (selectedCoupleId != null) {
|
||||||
for (final c in couples) {
|
for (final c in couples) {
|
||||||
if (c.id == selectedCoupleId) return c;
|
if (c.id == selectedCoupleId) return c;
|
||||||
}
|
}
|
||||||
@@ -133,8 +133,47 @@ class _CoupleCard extends StatelessWidget {
|
|||||||
required this.colorIndex,
|
required this.colorIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Libellé parents côté AM : « Sophie » ou « Thomas et Claire ».
|
||||||
|
static String _parentsLabel(List<CoupleMembre> parents) {
|
||||||
|
if (parents.isEmpty) return 'Parents';
|
||||||
|
String prenomOf(CoupleMembre p) {
|
||||||
|
final prenom = (p.prenom ?? '').trim();
|
||||||
|
if (prenom.isNotEmpty) return prenom;
|
||||||
|
return p.displayName(fallback: 'Parent');
|
||||||
|
}
|
||||||
|
if (parents.length == 1) return prenomOf(parents.first);
|
||||||
|
return '${prenomOf(parents[0])} et ${prenomOf(parents[1])}';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// Parent mode: enfant | AM (photo + prénom)
|
||||||
|
// AM mode: enfant | parent(s) — texte seul, pas de vignette parent
|
||||||
|
final isParentMode = mode == CoupleBandeauMode.parent;
|
||||||
|
|
||||||
|
Widget amOrParentsWidget;
|
||||||
|
|
||||||
|
if (isParentMode) {
|
||||||
|
amOrParentsWidget = _MembreTile(
|
||||||
|
photoUrl: couple.am?.photoUrl,
|
||||||
|
name: (couple.am?.prenom != null && couple.am!.prenom!.isNotEmpty)
|
||||||
|
? couple.am!.prenom!
|
||||||
|
: (couple.am?.displayName(fallback: 'Nounou') ?? 'Nounou'),
|
||||||
|
fallbackIcon: Icons.volunteer_activism,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
amOrParentsWidget = Text(
|
||||||
|
_parentsLabel(couple.parents),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: GoogleFonts.merienda(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: QuotidienTheme.ink,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 90,
|
height: 90,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -171,19 +210,7 @@ class _CoupleCard extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(left: 12),
|
padding: const EdgeInsets.only(left: 12),
|
||||||
child: _MembreTile(
|
child: amOrParentsWidget,
|
||||||
photoUrl: couple.am.photoUrl,
|
|
||||||
name: (couple.am.prenom != null && couple.am.prenom!.isNotEmpty)
|
|
||||||
? couple.am.prenom!
|
|
||||||
: couple.am.displayName(
|
|
||||||
fallback: mode == CoupleBandeauMode.parent
|
|
||||||
? 'Nounou'
|
|
||||||
: 'Parent',
|
|
||||||
),
|
|
||||||
fallbackIcon: mode == CoupleBandeauMode.parent
|
|
||||||
? Icons.volunteer_activism
|
|
||||||
: Icons.person_outline,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
|||||||
@@ -183,15 +183,16 @@ class _UserMenu extends StatelessWidget {
|
|||||||
title: Text('Profil'),
|
title: Text('Profil'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const PopupMenuItem(
|
if (onSearchAmTap != null)
|
||||||
value: 'search_am',
|
const PopupMenuItem(
|
||||||
child: ListTile(
|
value: 'search_am',
|
||||||
dense: true,
|
child: ListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
dense: true,
|
||||||
leading: Icon(Icons.search, size: 20),
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text('Recherche AM'),
|
leading: Icon(Icons.search, size: 20),
|
||||||
|
title: Text('Recherche AM'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const PopupMenuItem(
|
const PopupMenuItem(
|
||||||
value: 'settings',
|
value: 'settings',
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
|||||||
Reference in New Issue
Block a user