Files
petitspas/frontend/lib/models/carte_bulle.dart
T
jmartinandCursor a36e9ef176 feat(#175,#176): modale générique Ajouter une carte + CTA pilule
Creation absence (parent) et conge/arret (AM) via une seule modale
Material, catalogue UI extensible, calendrier de periode compact.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-05 22:36:29 +02:00

247 lines
7.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
});
CarteBulle copyWith({
CarteStatut? statut,
bool? isCreator,
String? lastRefuseComment,
DateTime? modifieLe,
Map<String, dynamic>? payload,
}) {
return CarteBulle(
id: id,
typeCode: typeCode,
titre: titre,
idPlacement: idPlacement,
idAbsence: idAbsence,
operation: operation,
statut: statut ?? this.statut,
payload: payload ?? this.payload,
purgeAt: purgeAt,
creePar: creePar,
isCreator: isCreator ?? this.isCreator,
responseMode: responseMode,
lastRefuseComment: lastRefuseComment ?? this.lastRefuseComment,
creeLe: creeLe,
modifieLe: modifieLe ?? 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';
bool get isAbsenceEnfant =>
typeCode == 'absence_enfant' || typeCode == 'absence_enfant_modif';
/// Info seule (`response_mode: none`) : créée déjà `traitee`, mais reste
/// pertinente dans la file tant que la période n’est pas finie.
bool get restePleineDansFile {
if (statut == CarteStatut.ouverte) return true;
if (statut != CarteStatut.traitee) return false;
if (!periodeEncoreActive) return false;
// Arrêt maladie ack : opaque jusqu’à date_fin.
if (isArretMaladie) return true;
// Absence (et autres infos) : pas d’atténuation tant que période active.
if (responseMode == CarteResponseMode.none) return true;
return false;
}
/// 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());
}
}