feat(front): add app icon for android, web and windows + agenda stub
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
class AbsenceGarde {
|
||||
final String id;
|
||||
final String idPlacement;
|
||||
final String type;
|
||||
final String dateDebut;
|
||||
final String dateFin;
|
||||
final String statut;
|
||||
final String expireAt;
|
||||
final String? creePar;
|
||||
final String? motif;
|
||||
final String? idEnfant;
|
||||
final String? prenomEnfant;
|
||||
final String? idAm;
|
||||
final String? prenomAm;
|
||||
final String? nomAm;
|
||||
final String creeLe;
|
||||
final String modifieLe;
|
||||
|
||||
AbsenceGarde({
|
||||
required this.id,
|
||||
required this.idPlacement,
|
||||
required this.type,
|
||||
required this.dateDebut,
|
||||
required this.dateFin,
|
||||
required this.statut,
|
||||
required this.expireAt,
|
||||
this.creePar,
|
||||
this.motif,
|
||||
this.idEnfant,
|
||||
this.prenomEnfant,
|
||||
this.idAm,
|
||||
this.prenomAm,
|
||||
this.nomAm,
|
||||
required this.creeLe,
|
||||
required this.modifieLe,
|
||||
});
|
||||
|
||||
factory AbsenceGarde.fromJson(Map<String, dynamic> json) {
|
||||
return AbsenceGarde(
|
||||
id: json['id'] ?? '',
|
||||
idPlacement: json['id_placement'] ?? '',
|
||||
type: json['type'] ?? '',
|
||||
dateDebut: json['date_debut'] ?? '',
|
||||
dateFin: json['date_fin'] ?? '',
|
||||
statut: json['statut'] ?? '',
|
||||
expireAt: json['expire_at'] ?? '',
|
||||
creePar: json['cree_par'],
|
||||
motif: json['motif'],
|
||||
idEnfant: json['id_enfant'],
|
||||
prenomEnfant: json['prenom_enfant'],
|
||||
idAm: json['id_am'],
|
||||
prenomAm: json['prenom_am'],
|
||||
nomAm: json['nom_am'],
|
||||
creeLe: json['cree_le'] ?? '',
|
||||
modifieLe: json['modifie_le'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ 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_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).
|
||||
@@ -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).',
|
||||
|
||||
@@ -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')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:p_tits_pas/models/absence_garde.dart';
|
||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||
import 'package:p_tits_pas/services/auth_service.dart';
|
||||
|
||||
class AbsencesGardeService {
|
||||
static Future<List<AbsenceGarde>> getAbsences({String? placementId}) async {
|
||||
final token = await AuthService.getToken();
|
||||
final headers = token != null ? ApiConfig.authHeaders(token) : ApiConfig.headers;
|
||||
|
||||
final uri = Uri.parse(ApiConfig.baseUrl +
|
||||
'/absences-garde' +
|
||||
(placementId != null ? '?placementId=$placementId' : ''));
|
||||
|
||||
final res = await http.get(uri, headers: headers);
|
||||
if (res.statusCode == 200) {
|
||||
final json = jsonDecode(res.body);
|
||||
final List items = json['items'] ?? [];
|
||||
return items.map((e) => AbsenceGarde.fromJson(e)).toList();
|
||||
} else {
|
||||
throw Exception('Erreur de chargement des absences : ${res.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<AbsenceGarde> creerAbsence({
|
||||
required String idPlacement,
|
||||
required String type,
|
||||
required String dateDebut,
|
||||
required String dateFin,
|
||||
String? motif,
|
||||
}) async {
|
||||
final token = await AuthService.getToken();
|
||||
final headers = token != null ? ApiConfig.authHeaders(token) : ApiConfig.headers;
|
||||
|
||||
final uri = Uri.parse('${ApiConfig.baseUrl}/absences-garde');
|
||||
final res = await http.post(
|
||||
uri,
|
||||
headers: headers,
|
||||
body: jsonEncode({
|
||||
'id_placement': idPlacement,
|
||||
'type': type,
|
||||
'date_debut': dateDebut,
|
||||
'date_fin': dateFin,
|
||||
if (motif != null) 'motif': motif,
|
||||
}),
|
||||
);
|
||||
if (res.statusCode == 201) {
|
||||
return AbsenceGarde.fromJson(jsonDecode(res.body));
|
||||
} else {
|
||||
throw Exception("Erreur de création d'absence : ${res.statusCode}");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<AbsenceGarde> modifierAbsence(
|
||||
String id, {
|
||||
String? dateDebut,
|
||||
String? dateFin,
|
||||
String? statut,
|
||||
String? motif,
|
||||
}) async {
|
||||
final token = await AuthService.getToken();
|
||||
final headers = token != null ? ApiConfig.authHeaders(token) : ApiConfig.headers;
|
||||
|
||||
final uri = Uri.parse('${ApiConfig.baseUrl}/absences-garde/$id');
|
||||
final Map<String, dynamic> body = {};
|
||||
if (dateDebut != null) body['date_debut'] = dateDebut;
|
||||
if (dateFin != null) body['date_fin'] = dateFin;
|
||||
if (statut != null) body['statut'] = statut;
|
||||
if (motif != null) body['motif'] = motif;
|
||||
|
||||
final res = await http.patch(
|
||||
uri,
|
||||
headers: headers,
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
if (res.statusCode == 200) {
|
||||
return AbsenceGarde.fromJson(jsonDecode(res.body));
|
||||
} else {
|
||||
throw Exception("Erreur de modification d'absence : ${res.statusCode}");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> supprimerAbsence(String id) async {
|
||||
final token = await AuthService.getToken();
|
||||
final headers = token != null ? ApiConfig.authHeaders(token) : ApiConfig.headers;
|
||||
|
||||
final uri = Uri.parse('${ApiConfig.baseUrl}/absences-garde/$id');
|
||||
final res = await http.delete(uri, headers: headers);
|
||||
if (res.statusCode != 204) {
|
||||
throw Exception("Erreur de suppression d'absence : ${res.statusCode}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -383,6 +383,13 @@ class AuthService {
|
||||
}
|
||||
|
||||
/// Récupère l'utilisateur connecté depuis le cache
|
||||
static const String tokenKey = 'auth_token';
|
||||
|
||||
static Future<String?> getToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(tokenKey);
|
||||
}
|
||||
|
||||
static Future<AppUser?> getCurrentUser() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final userJson = prefs.getString(_currentUserKey);
|
||||
|
||||
@@ -6,6 +6,7 @@ abstract final class QuotidienTheme {
|
||||
static const Color ink = Color(0xFF2F2F2F);
|
||||
static const Color ivory = Color(0xFFFFFEF9);
|
||||
static const Color turquoise = Color(0xFF8AD0C8);
|
||||
static const Color peach = Color(0xFFFFCCB6);
|
||||
static const Color lavender = Color(0xFFC6A3D8);
|
||||
static const Color coral = Color(0xFFF4A28C);
|
||||
static const Color softGreenPill = Color(0xFFB8D9A8);
|
||||
|
||||
Reference in New Issue
Block a user