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:
@@ -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')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user