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
+58
View File
@@ -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'] ?? '',
);
}
}
+5 -3
View File
@@ -5,7 +5,8 @@ enum CardColorVertical {
lime('assets/cards/card_lime.png'),
lavender('assets/cards/card_lavender.png'),
green('assets/cards/card_green.png'),
blue('assets/cards/card_blue.png');
blue('assets/cards/card_blue.png'),
aqua('assets/cards/card_aqua.png');
final String path;
const CardColorVertical(this.path);
@@ -18,8 +19,9 @@ enum CardColorHorizontal {
lime('assets/cards/card_lime_h.png'),
lavender('assets/cards/card_lavender_h.png'),
green('assets/cards/card_green_h.png'),
blue('assets/cards/card_blue_h.png');
blue('assets/cards/card_blue_h.png'),
aqua('assets/cards/card_aqua_h.png');
final String path;
const CardColorHorizontal(this.path);
}
}
+205
View File
@@ -0,0 +1,205 @@
import 'package:p_tits_pas/models/card_assets.dart';
/// Statut d'une instance de carte (file d'attention) — contrat #194 / #173.
enum CarteStatut {
ouverte('ouverte'),
refusee('refusee'),
traitee('traitee');
final String apiValue;
const CarteStatut(this.apiValue);
static CarteStatut fromApi(String? raw) {
switch (raw) {
case 'refusee':
return CarteStatut.refusee;
case 'traitee':
return CarteStatut.traitee;
case 'ouverte':
default:
return CarteStatut.ouverte;
}
}
}
/// Mode de réponse attendu selon le type SYSTEM.
enum CarteResponseMode {
none('none'),
ack('ack'),
acceptRefuse('accept_refuse');
final String apiValue;
const CarteResponseMode(this.apiValue);
static CarteResponseMode fromApi(String? raw) {
switch (raw) {
case 'ack':
return CarteResponseMode.ack;
case 'accept_refuse':
return CarteResponseMode.acceptRefuse;
case 'none':
default:
return CarteResponseMode.none;
}
}
}
/// Bulle / carte de la file d'attention.
class CarteBulle {
final String id;
final String typeCode;
final String titre;
final String idPlacement;
final String? idAbsence;
final String operation;
final CarteStatut statut;
final Map<String, dynamic> payload;
final DateTime? purgeAt;
final String? creePar;
final bool isCreator;
final CarteResponseMode responseMode;
final String? lastRefuseComment;
final DateTime? creeLe;
final DateTime? modifieLe;
const CarteBulle({
required this.id,
required this.typeCode,
required this.titre,
required this.idPlacement,
this.idAbsence,
this.operation = 'create',
required this.statut,
this.payload = const {},
this.purgeAt,
this.creePar,
this.isCreator = false,
this.responseMode = CarteResponseMode.none,
this.lastRefuseComment,
this.creeLe,
this.modifieLe,
});
String? get dateDebut => payload['date_debut']?.toString();
String? get dateFin => payload['date_fin']?.toString();
String? get motif => payload['motif']?.toString();
/// Motif de refus : réponse API, sinon motif payload (co-parent / reload).
String? get motifRefus {
final fromResponse = lastRefuseComment?.trim();
if (fromResponse != null && fromResponse.isNotEmpty) return fromResponse;
final fromPayload = motif?.trim();
if (fromPayload != null && fromPayload.isNotEmpty) return fromPayload;
return null;
}
/// Libellé UI (sémantique produit) — pas le titre brut API.
String get titreAffiche {
switch (typeCode) {
case 'conge_am':
return 'Demande de congés';
case 'arret_maladie_am':
return 'Arrêt maladie';
case 'absence_enfant':
case 'absence_enfant_modif':
return 'Notification d’absence';
case 'soin_enfant':
return 'Soin / bobo';
default:
return titre;
}
}
bool get isArretMaladie => typeCode == 'arret_maladie_am';
bool get isCongeAm => typeCode == 'conge_am';
bool get isSoinEnfant => typeCode == 'soin_enfant';
/// Icône dessin (haut droite) selon le type de bulle.
String? get iconAsset {
switch (typeCode) {
case 'conge_am':
return 'assets/images/valise.png';
case 'arret_maladie_am':
return 'assets/images/thermo_croix.png';
case 'absence_enfant':
case 'absence_enfant_modif':
return 'assets/images/sac_lapin.png';
// soin_enfant : icône selon sous-type payload — à brancher plus tard
default:
return null;
}
}
/// Fin de période (jour inclus). Null si dates absentes / invalides.
DateTime? get dateFinJour {
final raw = dateFin;
if (raw == null || raw.isEmpty) return null;
final dt = DateTime.tryParse(raw);
if (dt == null) return null;
return DateTime(dt.year, dt.month, dt.day);
}
/// Vrai tant que la date de fin n’est pas dépassée (jour courant inclus).
bool get periodeEncoreActive {
final fin = dateFinJour;
if (fin == null) return true;
final today = DateTime.now();
final todayDay = DateTime(today.year, today.month, today.day);
return !todayDay.isAfter(fin);
}
/// Teinte pastel UI dérivée du `type_code` (pas de `couleur` API).
/// Voir `docs/32_MINI-SPEC-BULLES-CARTES.md` §4.
CardColorHorizontal get colorFamily {
switch (typeCode) {
case 'conge_am':
return CardColorHorizontal.aqua;
case 'arret_maladie_am':
return CardColorHorizontal.blue;
case 'absence_enfant':
case 'absence_enfant_modif':
return CardColorHorizontal.peach;
case 'soin_enfant':
return CardColorHorizontal.pink;
default:
return CardColorHorizontal.lavender;
}
}
/// Asset horizontal pastel (palette `assets/cards/` uniquement).
String get backgroundAsset => colorFamily.path;
/// Le destinataire peut répondre (pas le créateur, carte ouverte).
bool get peutRepondre =>
!isCreator &&
statut == CarteStatut.ouverte &&
responseMode != CarteResponseMode.none;
factory CarteBulle.fromJson(Map<String, dynamic> json) {
final rawPayload = json['payload'];
return CarteBulle(
id: (json['id'] ?? '').toString(),
typeCode: (json['type_code'] ?? '').toString(),
titre: (json['titre'] ?? json['type_code'] ?? 'Carte').toString(),
idPlacement: (json['id_placement'] ?? '').toString(),
idAbsence: json['id_absence']?.toString(),
operation: (json['operation'] ?? 'create').toString(),
statut: CarteStatut.fromApi(json['statut']?.toString()),
payload: rawPayload is Map
? Map<String, dynamic>.from(rawPayload)
: const {},
purgeAt: _parseDate(json['purge_at']),
creePar: json['cree_par']?.toString(),
isCreator: json['is_creator'] == true,
responseMode: CarteResponseMode.fromApi(json['response_mode']?.toString()),
lastRefuseComment: json['last_refuse_comment']?.toString(),
creeLe: _parseDate(json['cree_le']),
modifieLe: _parseDate(json['modifie_le']),
);
}
static DateTime? _parseDate(dynamic v) {
if (v == null) return null;
return DateTime.tryParse(v.toString());
}
}
+14 -5
View File
@@ -39,13 +39,15 @@ class CoupleMembre {
class CoupleGarde {
final String id;
final CoupleMembre enfant;
final CoupleMembre am;
final CoupleMembre? am;
final List<CoupleMembre> parents;
final bool courant;
const CoupleGarde({
required this.id,
required this.enfant,
required this.am,
this.am,
this.parents = const [],
this.courant = false,
});
@@ -55,9 +57,15 @@ class CoupleGarde {
enfant: CoupleMembre.fromJson(
Map<String, dynamic>.from(json['enfant'] ?? const {}),
),
am: CoupleMembre.fromJson(
Map<String, dynamic>.from(json['am'] ?? const {}),
),
am: json['am'] != null
? 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,
);
}
@@ -67,6 +75,7 @@ class CoupleGarde {
id: id,
enfant: enfant,
am: am,
parents: parents,
courant: courant ?? this.courant,
);
}