From 535af3b14655a438b59db2efb0e968ebd7bdee14 Mon Sep 17 00:00:00 2001 From: Julien Martin Date: Fri, 25 Sep 2026 16:12:47 +0200 Subject: [PATCH] =?UTF-8?q?fix(#173):=20actions=20en=20pilules=20+=20modal?= =?UTF-8?q?es,=20fin=20des=20libell=C3=A9s=20=C3=80=20traiter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Arrêt maladie reste visible jusqu’à la date de fin ; accepter / refuser / ack passent par une confirmation modale. Co-authored-by: Cursor --- frontend/lib/models/carte_bulle.dart | 21 ++ .../lib/widgets/quotidien/cartes_feed.dart | 226 +++++++++++++----- .../widgets/quotidien/quotidien_theme.dart | 1 + 3 files changed, 187 insertions(+), 61 deletions(-) diff --git a/frontend/lib/models/carte_bulle.dart b/frontend/lib/models/carte_bulle.dart index ff00c35..1fa1013 100644 --- a/frontend/lib/models/carte_bulle.dart +++ b/frontend/lib/models/carte_bulle.dart @@ -101,6 +101,27 @@ class CarteBulle { } } + bool get isArretMaladie => typeCode == 'arret_maladie_am'; + bool get isCongeAm => typeCode == 'conge_am'; + + /// 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); + } + /// Asset horizontal pastel (palette `assets/cards/` uniquement). String get backgroundAsset { final key = (couleur ?? '').trim().toLowerCase(); diff --git a/frontend/lib/widgets/quotidien/cartes_feed.dart b/frontend/lib/widgets/quotidien/cartes_feed.dart index ced7692..6c8e62d 100644 --- a/frontend/lib/widgets/quotidien/cartes_feed.dart +++ b/frontend/lib/widgets/quotidien/cartes_feed.dart @@ -94,23 +94,94 @@ class _CartesFeedState extends State { } } + Future _onAccept(CarteBulle card) async { + final ok = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text( + 'Accepter la demande ?', + style: GoogleFonts.merienda(fontWeight: FontWeight.w600), + ), + content: Text( + 'Confirmez-vous l’acceptation de « ${card.titreAffiche} » ' + '(${_periodeCourte(card)}) ?', + style: GoogleFonts.merriweather(fontSize: 14, height: 1.4), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('Annuler'), + ), + FilledButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('Confirmer'), + ), + ], + ), + ); + if (ok == true) { + await _respond(card, action: 'accept'); + } + } + + Future _onAck(CarteBulle card) async { + final ok = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text( + 'Prise en compte', + style: GoogleFonts.merienda(fontWeight: FontWeight.w600), + ), + content: Text( + 'Confirmez-vous avoir bien reçu cet arrêt maladie ' + '(${_periodeCourte(card)}) ?', + style: GoogleFonts.merriweather(fontSize: 14, height: 1.4), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('Annuler'), + ), + FilledButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('Bien reçu'), + ), + ], + ), + ); + if (ok == true) { + await _respond(card, action: 'ack'); + } + } + Future _onRefuse(CarteBulle card) async { final controller = TextEditingController(); final ok = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text( - 'Motiver le refus', + 'Refuser la demande', style: GoogleFonts.merienda(fontWeight: FontWeight.w600), ), - content: TextField( - controller: controller, - maxLines: 3, - autofocus: true, - decoration: const InputDecoration( - hintText: 'Expliquez le motif du refus…', - border: OutlineInputBorder(), - ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Indiquez le motif du refus pour « ${card.titreAffiche} ».', + style: GoogleFonts.merriweather(fontSize: 14, height: 1.4), + ), + const SizedBox(height: 12), + TextField( + controller: controller, + maxLines: 3, + autofocus: true, + decoration: const InputDecoration( + hintText: 'Motif du refus…', + border: OutlineInputBorder(), + ), + ), + ], ), actions: [ TextButton( @@ -137,6 +208,21 @@ class _CartesFeedState extends State { controller.dispose(); } + static String _periodeCourte(CarteBulle card) { + final d = card.dateDebut; + final f = card.dateFin; + String fmt(String? iso) { + if (iso == null || iso.isEmpty) return '…'; + final dt = DateTime.tryParse(iso); + if (dt == null) return iso; + return '${dt.day.toString().padLeft(2, '0')}/' + '${dt.month.toString().padLeft(2, '0')}/${dt.year}'; + } + if (d == null && f == null) return 'période non précisée'; + if (d != null && f != null && d == f) return fmt(d); + return '${fmt(d)} → ${fmt(f)}'; + } + @override Widget build(BuildContext context) { return Column( @@ -229,8 +315,8 @@ class _CartesFeedState extends State { return _BulleTile( card: card, busy: _busyCardId == card.id, - onAccept: () => _respond(card, action: 'accept'), - onAck: () => _respond(card, action: 'ack'), + onAccept: () => _onAccept(card), + onAck: () => _onAck(card), onRefuse: () => _onRefuse(card), ); }, @@ -271,22 +357,26 @@ class _BulleTile extends StatelessWidget { return '$jj/$mm/${dt.year}'; } - String get _statutLabel { - switch (card.statut) { - case CarteStatut.ouverte: - return card.isCreator ? 'En attente' : 'À traiter'; - case CarteStatut.refusee: - return 'Refusé'; - case CarteStatut.traitee: - return 'Traité'; + /// Pas de « À traiter / Traité » : seulement un indice utile (refus / attente créateur). + String? get _indice { + if (card.statut == CarteStatut.refusee) return 'Refusé'; + if (card.statut == CarteStatut.ouverte && card.isCreator) { + return 'En attente de réponse'; } + return null; + } + + /// Arrêt maladie : reste opaque tant que la période n’est pas finie. + bool get _attenuee { + if (card.isArretMaladie && card.periodeEncoreActive) return false; + return card.statut == CarteStatut.traitee; } @override Widget build(BuildContext context) { - final treated = card.statut == CarteStatut.traitee; + final indice = _indice; return Opacity( - opacity: treated ? 0.55 : 1, + opacity: _attenuee ? 0.55 : 1, child: Carte( assetPath: card.backgroundAsset, constraints: const BoxConstraints(minHeight: 96), @@ -295,6 +385,7 @@ class _BulleTile extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( @@ -308,13 +399,14 @@ class _BulleTile extends StatelessWidget { ), ), ), - Text( - _statutLabel, - style: GoogleFonts.merriweather( - fontSize: 11, - color: QuotidienTheme.muted, + if (indice != null) + Text( + indice, + style: GoogleFonts.merriweather( + fontSize: 11, + color: QuotidienTheme.muted, + ), ), - ), ], ), if (_periode.isNotEmpty) ...[ @@ -344,7 +436,7 @@ class _BulleTile extends StatelessWidget { card.lastRefuseComment!.trim().isNotEmpty) ...[ const SizedBox(height: 4), Text( - 'Motif refus : ${card.lastRefuseComment}', + 'Motif : ${card.lastRefuseComment}', maxLines: 2, overflow: TextOverflow.ellipsis, style: GoogleFonts.merriweather( @@ -355,10 +447,10 @@ class _BulleTile extends StatelessWidget { ), ], if (card.peutRepondre) ...[ - const SizedBox(height: 8), + const SizedBox(height: 10), if (busy) const Align( - alignment: Alignment.centerLeft, + alignment: Alignment.centerRight, child: SizedBox( width: 18, height: 18, @@ -366,25 +458,30 @@ class _BulleTile extends StatelessWidget { ), ) else - Wrap( - spacing: 8, - runSpacing: 4, + Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ if (card.responseMode == CarteResponseMode.acceptRefuse) ...[ - _ActionChip( - label: 'Accepter', + _ActionPill( + asset: QuotidienTheme.pillTurquoiseAsset, + iconAsset: 'assets/images/coche.png', + tooltip: 'Accepter', onTap: onAccept, ), - _ActionChip( - label: 'Refuser', + const SizedBox(width: 10), + _ActionPill( + asset: QuotidienTheme.pillCorailAsset, + iconAsset: 'assets/images/red_cross2.png', + tooltip: 'Refuser', onTap: onRefuse, - destructive: true, ), ], if (card.responseMode == CarteResponseMode.ack) - _ActionChip( - label: 'Bien reçu', + _ActionPill( + asset: QuotidienTheme.pillTurquoiseAsset, + iconAsset: 'assets/images/coche.png', + tooltip: 'Bien reçu', onTap: onAck, ), ], @@ -397,36 +494,43 @@ class _BulleTile extends StatelessWidget { } } -class _ActionChip extends StatelessWidget { - final String label; +/// Bouton pilule dessiné (fond pastel + coche / croix). +class _ActionPill extends StatelessWidget { + final String asset; + final String iconAsset; + final String tooltip; final VoidCallback onTap; - final bool destructive; - const _ActionChip({ - required this.label, + const _ActionPill({ + required this.asset, + required this.iconAsset, + required this.tooltip, required this.onTap, - this.destructive = false, }); @override Widget build(BuildContext context) { - return Material( - color: Colors.white.withValues(alpha: 0.55), - borderRadius: BorderRadius.circular(16), + return Tooltip( + message: tooltip, child: InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(16), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: Text( - label, - style: GoogleFonts.merriweather( - fontSize: 12, - fontWeight: FontWeight.w600, - color: destructive - ? const Color(0xFFB00020) - : QuotidienTheme.ink, - ), + borderRadius: BorderRadius.circular(20), + child: SizedBox( + width: 52, + height: 32, + child: Stack( + alignment: Alignment.center, + children: [ + Positioned.fill( + child: Image.asset(asset, fit: BoxFit.fill), + ), + Image.asset( + iconAsset, + height: 18, + width: 18, + fit: BoxFit.contain, + ), + ], ), ), ), diff --git a/frontend/lib/widgets/quotidien/quotidien_theme.dart b/frontend/lib/widgets/quotidien/quotidien_theme.dart index dd5c3bf..9c99701 100644 --- a/frontend/lib/widgets/quotidien/quotidien_theme.dart +++ b/frontend/lib/widgets/quotidien/quotidien_theme.dart @@ -49,6 +49,7 @@ abstract final class QuotidienTheme { 'assets/images/bg_turquoise_pill.png'; static const String pillLavenderAsset = 'assets/images/bg_lavender_pill.png'; + static const String pillCorailAsset = 'assets/images/bg_corail_pill.png'; /// Trait crayon gris (2400×28, transparent) : séparateurs bandeau / corps / /// footer. Version verticale (28×2400) entre les colonnes.