release: squash merge develop → master

Quotidien / Epic C : feed bulles parent (#173), module Cartes (#194/#195),
evenements_agenda (#205), couples garde AM (#169–#171), API absences (#172).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-05 15:44:10 +02:00
co-authored by Cursor
parent ba6078feab
commit da024d997a
94 changed files with 6263 additions and 236 deletions
@@ -3,13 +3,14 @@ import 'package:p_tits_pas/models/couple_garde.dart';
import 'package:p_tits_pas/models/user.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/cartes_feed.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';
/// Tableau de bord parent — coquille 3 colonnes quotidien (#166).
/// Colonne gauche : sélecteur de couple enfant–nounou (#167).
/// Métier cartes / blog / messagerie : tickets C/D/E.
/// Colonne gauche : sélecteur de couple (#167) + feed bulles (#173).
class ParentDashboardScreen extends StatefulWidget {
const ParentDashboardScreen({super.key});
@@ -62,7 +63,6 @@ class _ParentDashboardScreenState extends State<ParentDashboardScreen> {
Future<void> _selectCouple(CoupleGarde couple) async {
if (couple.id == _selectedCoupleId) return;
// Optimiste : on bascule tout de suite, l'API persiste ensuite.
setState(() => _selectedCoupleId = couple.id);
try {
final res = await CoupleGardeService.definirCoupleCourant(couple.id);
@@ -115,6 +115,7 @@ class _ParentDashboardScreenState extends State<ParentDashboardScreen> {
error: _couplesError,
onRetry: _loadCouples,
onCoupleSelected: _selectCouple,
onAjouterBulle: () => _soon('Ajouter une bulle'),
),
centerColumn: const QuotidienColumnPlaceholder(
title: 'Blog',
@@ -128,10 +129,7 @@ class _ParentDashboardScreenState extends State<ParentDashboardScreen> {
'Mess. AM · Mess. RPE\n(à brancher — ticket #184).',
icon: Icons.chat_bubble_outline,
),
agendaBody: const QuotidienStubPage(
title: 'Agenda',
message: 'Agenda — contenu à venir (stub #187).',
),
agendaBody: AgendaAbsencesStub(placementId: _selectedCoupleId),
contratBody: const QuotidienStubPage(
title: 'Contrat',
message: 'Contrat — contenu à venir (stub #187).',
@@ -140,7 +138,7 @@ class _ParentDashboardScreenState extends State<ParentDashboardScreen> {
}
}
/// Colonne gauche : bandeau couple (#167) puis flux de cartes (#173 à venir).
/// Colonne gauche : bandeau couple (#167) puis flux de cartes (#173).
class _LeftColumn extends StatelessWidget {
final List<CoupleGarde> couples;
final String? selectedCoupleId;
@@ -148,6 +146,7 @@ class _LeftColumn extends StatelessWidget {
final String? error;
final VoidCallback onRetry;
final ValueChanged<CoupleGarde> onCoupleSelected;
final VoidCallback onAjouterBulle;
const _LeftColumn({
required this.couples,
@@ -156,6 +155,7 @@ class _LeftColumn extends StatelessWidget {
required this.error,
required this.onRetry,
required this.onCoupleSelected,
required this.onAjouterBulle,
});
@override
@@ -174,12 +174,10 @@ class _LeftColumn extends StatelessWidget {
onCoupleSelected: onCoupleSelected,
),
const SizedBox(height: 14),
const Expanded(
child: QuotidienColumnPlaceholder(
title: 'Cartes',
subtitle:
'Absences, congés AM, sorties à valider\n(à brancher — ticket #173).',
icon: Icons.style_outlined,
Expanded(
child: CartesFeed(
placementId: selectedCoupleId,
onAjouterBulle: onAjouterBulle,
),
),
],
@@ -0,0 +1,192 @@
import 'package:flutter/material.dart';
import 'package:p_tits_pas/models/absence_garde.dart';
import 'package:p_tits_pas/services/api/absences_garde_service.dart';
import 'package:p_tits_pas/widgets/quotidien/quotidien_theme.dart';
class AgendaAbsencesStub extends StatefulWidget {
final String? placementId;
const AgendaAbsencesStub({super.key, this.placementId});
@override
State<AgendaAbsencesStub> createState() => _AgendaAbsencesStubState();
}
class _AgendaAbsencesStubState extends State<AgendaAbsencesStub> {
List<AbsenceGarde> _absences = [];
bool _loading = true;
String? _error;
@override
void initState() {
super.initState();
_loadAbsences();
}
@override
void didUpdateWidget(covariant AgendaAbsencesStub oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.placementId != widget.placementId) {
_loadAbsences();
}
}
Future<void> _loadAbsences() async {
setState(() {
_loading = true;
_error = null;
});
try {
final absences = await AbsencesGardeService.getAbsences(
placementId: widget.placementId,
);
if (!mounted) return;
setState(() {
_absences = absences;
_loading = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_error = e.toString();
_loading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Container(
color: QuotidienTheme.ivory,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Icon(
Icons.calendar_month_outlined,
size: 64,
color: QuotidienTheme.peach,
),
const SizedBox(height: 24),
Text(
"Agenda (Stub) - Lignes d'absence",
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: QuotidienTheme.ink,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
'ID Placement courant: ${widget.placementId ?? 'Tous'}',
textAlign: TextAlign.center,
style: const TextStyle(color: QuotidienTheme.muted),
),
const SizedBox(height: 24),
Expanded(
child: _buildList(),
),
],
),
),
),
);
}
Widget _buildList() {
if (_loading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, color: Colors.red, size: 48),
const SizedBox(height: 16),
Text(_error!, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadAbsences,
child: const Text('Réessayer'),
),
],
),
);
}
if (_absences.isEmpty) {
return const Center(
child: Text('Aucune absence ou congé trouvé.',
style: TextStyle(color: QuotidienTheme.muted)),
);
}
return ListView.separated(
itemCount: _absences.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (context, index) {
final abs = _absences[index];
return ListTile(
leading: _getIcon(abs.type),
title: Text('${abs.type} (${abs.statut})'),
subtitle: Text(
'Du ${abs.dateDebut} au ${abs.dateFin}\n'
'Enfant: ${abs.prenomEnfant ?? 'N/A'}, AM: ${abs.prenomAm ?? 'N/A'}',
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: () => _confirmDelete(abs),
),
);
},
);
}
Icon _getIcon(String type) {
switch (type) {
case 'absence_enfant':
return const Icon(Icons.child_care, color: QuotidienTheme.coral);
case 'conge_am':
return const Icon(Icons.beach_access, color: QuotidienTheme.turquoise);
case 'arret_maladie_am':
return const Icon(Icons.medical_services, color: QuotidienTheme.coral);
default:
return const Icon(Icons.event);
}
}
Future<void> _confirmDelete(AbsenceGarde abs) async {
final confirm = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Supprimer ?'),
content: Text("Supprimer l'absence ${abs.type} du ${abs.dateDebut} ?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Annuler'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Supprimer'),
),
],
),
);
if (confirm == true) {
try {
await AbsencesGardeService.supprimerAbsence(abs.id);
_loadAbsences();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e')),
);
}
}
}
}