feat(#173): layout bulles unifié, sac_lapin, statut Validé/Refusé
Icône absence aquarelle, calendrier teinté à la volée, encres par famille de couleur, modales congés clarifiées, et libellé Validé/Refusé dans la barre d’actions une fois la demande traitée. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 591 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 683 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 289 KiB |
@@ -114,7 +114,7 @@ class CarteBulle {
|
||||
return 'assets/images/cartes/icon_carte_maladie.png';
|
||||
case 'absence_enfant':
|
||||
case 'absence_enfant_modif':
|
||||
return 'assets/images/cartes/icon_carte_absence.png';
|
||||
return 'assets/images/sac_lapin.png';
|
||||
// soin_enfant : icône selon sous-type payload — à brancher plus tard
|
||||
default:
|
||||
return null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:p_tits_pas/models/card_assets.dart';
|
||||
import 'package:p_tits_pas/models/carte_bulle.dart';
|
||||
import 'package:p_tits_pas/services/api/cards_service.dart';
|
||||
import 'package:p_tits_pas/widgets/quotidien/carte.dart';
|
||||
@@ -138,7 +139,9 @@ class _CartesFeedState extends State<CartesFeed> {
|
||||
style: GoogleFonts.merienda(fontWeight: FontWeight.w600),
|
||||
),
|
||||
content: Text(
|
||||
'Confirmez-vous l’acceptation de « ${card.titreAffiche} » '
|
||||
card.isCongeAm
|
||||
? 'Confirmez-vous les congés ${_periodeDuAu(card)} ?'
|
||||
: 'Confirmez-vous l’acceptation de « ${card.titreAffiche} » '
|
||||
'(${_periodeCourte(card)}) ?',
|
||||
style: GoogleFonts.merriweather(fontSize: 14, height: 1.4),
|
||||
),
|
||||
@@ -168,7 +171,13 @@ class _CartesFeedState extends State<CartesFeed> {
|
||||
style: GoogleFonts.merienda(fontWeight: FontWeight.w600),
|
||||
),
|
||||
content: Text(
|
||||
'Confirmez-vous avoir bien reçu cet arrêt maladie '
|
||||
card.isArretMaladie
|
||||
? 'Confirmez-vous avoir pris connaissance de cet arrêt maladie '
|
||||
'${_periodeDuAu(card)} ?'
|
||||
: card.typeCode.startsWith('absence_enfant')
|
||||
? 'Confirmez-vous avoir pris connaissance de l’absence '
|
||||
'${_periodeDuAu(card)} ?'
|
||||
: 'Confirmez-vous avoir bien reçu « ${card.titreAffiche} » '
|
||||
'(${_periodeCourte(card)}) ?',
|
||||
style: GoogleFonts.merriweather(fontSize: 14, height: 1.4),
|
||||
),
|
||||
@@ -190,59 +199,36 @@ class _CartesFeedState extends State<CartesFeed> {
|
||||
}
|
||||
|
||||
Future<void> _onRefuse(CarteBulle card) async {
|
||||
final controller = TextEditingController();
|
||||
final ok = await showDialog<bool>(
|
||||
final comment = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(
|
||||
'Refuser la demande',
|
||||
style: GoogleFonts.merienda(fontWeight: FontWeight.w600),
|
||||
),
|
||||
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(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
if (controller.text.trim().isEmpty) return;
|
||||
Navigator.pop(ctx, true);
|
||||
},
|
||||
child: const Text('Refuser'),
|
||||
),
|
||||
],
|
||||
builder: (ctx) => _RefuseDialog(
|
||||
title: card.isCongeAm ? 'Refuser les congés ?' : 'Refuser la demande',
|
||||
message: card.isCongeAm
|
||||
? 'Indiquez le motif du refus des congés '
|
||||
'${_periodeDuAu(card)}.'
|
||||
: 'Indiquez le motif du refus pour « ${card.titreAffiche} ».',
|
||||
),
|
||||
);
|
||||
if (ok == true && controller.text.trim().isNotEmpty) {
|
||||
await _respond(
|
||||
card,
|
||||
action: 'refuse',
|
||||
comment: controller.text.trim(),
|
||||
);
|
||||
if (comment != null && comment.isNotEmpty) {
|
||||
await _respond(card, action: 'refuse', comment: comment);
|
||||
}
|
||||
controller.dispose();
|
||||
}
|
||||
|
||||
static const _mois = [
|
||||
'janvier',
|
||||
'février',
|
||||
'mars',
|
||||
'avril',
|
||||
'mai',
|
||||
'juin',
|
||||
'juillet',
|
||||
'août',
|
||||
'septembre',
|
||||
'octobre',
|
||||
'novembre',
|
||||
'décembre',
|
||||
];
|
||||
|
||||
static String _periodeCourte(CarteBulle card) {
|
||||
final d = card.dateDebut;
|
||||
final f = card.dateFin;
|
||||
@@ -258,6 +244,23 @@ class _CartesFeedState extends State<CartesFeed> {
|
||||
return '${fmt(d)} → ${fmt(f)}';
|
||||
}
|
||||
|
||||
/// « du 04 novembre 2026 au 06 novembre 2026 »
|
||||
static String _periodeDuAu(CarteBulle card) {
|
||||
String fmtLong(String? iso) {
|
||||
if (iso == null || iso.isEmpty) return '…';
|
||||
final dt = DateTime.tryParse(iso);
|
||||
if (dt == null) return iso;
|
||||
final jj = dt.day.toString().padLeft(2, '0');
|
||||
return '$jj ${_mois[dt.month - 1]} ${dt.year}';
|
||||
}
|
||||
|
||||
final d = card.dateDebut;
|
||||
final f = card.dateFin;
|
||||
if (d == null && f == null) return 'pour une période non précisée';
|
||||
if (d != null && (f == null || d == f)) return 'du ${fmtLong(d)}';
|
||||
return 'du ${fmtLong(d)} au ${fmtLong(f)}';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@@ -404,14 +407,60 @@ class _CartesFeedState extends State<CartesFeed> {
|
||||
}
|
||||
}
|
||||
|
||||
class _BulleTile extends StatelessWidget {
|
||||
/// Encre aqua foncé — harmonisée fond aqua / valise (pas le violet).
|
||||
static const Color _aquaInk = Color(0xFF315F63);
|
||||
/// Date : même famille, ~12 % plus claire.
|
||||
static const Color _aquaDate = Color(0xFF4A7276);
|
||||
/// Lien agenda : légère dérive bleue.
|
||||
static const Color _aquaLink = Color(0xFF2B5868);
|
||||
/// Encre (titre / date / lien) accordée à la teinte de fond de la bulle.
|
||||
class _Encre {
|
||||
final Color ink;
|
||||
final Color date;
|
||||
final Color link;
|
||||
const _Encre(this.ink, this.date, this.link);
|
||||
|
||||
/// Aqua → pétrole désaturé ; pêche → brun chaud ; bleu → marine doux ;
|
||||
/// rose → bois de rose ; lavande → prune.
|
||||
static _Encre pour(CardColorHorizontal c) {
|
||||
switch (c) {
|
||||
case CardColorHorizontal.aqua:
|
||||
return const _Encre(
|
||||
Color(0xFF315F63),
|
||||
Color(0xFF4A7276),
|
||||
Color(0xFF2B5868),
|
||||
);
|
||||
case CardColorHorizontal.peach:
|
||||
return const _Encre(
|
||||
Color(0xFF7A4A2E),
|
||||
Color(0xFF8F6248),
|
||||
Color(0xFF6E4530),
|
||||
);
|
||||
case CardColorHorizontal.blue:
|
||||
return const _Encre(
|
||||
Color(0xFF2F4A6E),
|
||||
Color(0xFF486083),
|
||||
Color(0xFF2A4670),
|
||||
);
|
||||
case CardColorHorizontal.pink:
|
||||
return const _Encre(
|
||||
Color(0xFF7A3E52),
|
||||
Color(0xFF8E5668),
|
||||
Color(0xFF6E3A50),
|
||||
);
|
||||
case CardColorHorizontal.lavender:
|
||||
return const _Encre(
|
||||
Color(0xFF5C3D5C),
|
||||
Color(0xFF70536F),
|
||||
Color(0xFF553A60),
|
||||
);
|
||||
case CardColorHorizontal.red:
|
||||
case CardColorHorizontal.lime:
|
||||
case CardColorHorizontal.green:
|
||||
return const _Encre(
|
||||
Color(0xFF2F2F2F),
|
||||
Color(0xFF6B6B6B),
|
||||
Color(0xFF3A3A3A),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _BulleTile extends StatelessWidget {
|
||||
final CarteBulle card;
|
||||
final bool busy;
|
||||
final VoidCallback onAccept;
|
||||
@@ -481,12 +530,8 @@ class _BulleTile extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (card.isCongeAm) return _buildConge(context);
|
||||
return _buildGeneric(context);
|
||||
}
|
||||
|
||||
Widget _buildConge(BuildContext context) {
|
||||
final periode = _fmtPeriodeUneLigne();
|
||||
final encre = _Encre.pour(card.colorFamily);
|
||||
|
||||
return Opacity(
|
||||
opacity: _attenuee ? 0.55 : 1,
|
||||
@@ -511,7 +556,7 @@ class _BulleTile extends StatelessWidget {
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _aquaInk,
|
||||
color: encre.ink,
|
||||
),
|
||||
),
|
||||
if (periode.isNotEmpty) ...[
|
||||
@@ -525,6 +570,8 @@ class _BulleTile extends StatelessWidget {
|
||||
height: 28,
|
||||
fit: BoxFit.contain,
|
||||
filterQuality: FilterQuality.high,
|
||||
color: encre.ink,
|
||||
colorBlendMode: BlendMode.srcIn,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
@@ -535,7 +582,7 @@ class _BulleTile extends StatelessWidget {
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _aquaDate,
|
||||
color: encre.date,
|
||||
height: 1.25,
|
||||
),
|
||||
),
|
||||
@@ -555,7 +602,7 @@ class _BulleTile extends StatelessWidget {
|
||||
style: GoogleFonts.merriweather(
|
||||
fontSize: 12,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: _aquaInk,
|
||||
color: encre.ink,
|
||||
),
|
||||
),
|
||||
] else if (card.statut == CarteStatut.ouverte &&
|
||||
@@ -595,6 +642,13 @@ class _BulleTile extends StatelessWidget {
|
||||
height: 28,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: card.responseMode == CarteResponseMode.ack
|
||||
? _PilulePeinteButton(
|
||||
asset:
|
||||
'assets/images/cartes/btn_coche_pilule.png',
|
||||
tooltip: 'Bien reçu',
|
||||
onTap: onAck,
|
||||
)
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -612,7 +666,26 @@ class _BulleTile extends StatelessWidget {
|
||||
onTap: onRefuse,
|
||||
),
|
||||
],
|
||||
)
|
||||
else if (card.statut == CarteStatut.traitee ||
|
||||
card.statut == CarteStatut.refusee)
|
||||
Expanded(
|
||||
child: Text(
|
||||
card.statut == CarteStatut.refusee
|
||||
? 'Refusé'
|
||||
: 'Validé',
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: encre.ink.withValues(alpha: 0.75),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (card.peutRepondre) const SizedBox(width: 8),
|
||||
if (!(card.statut == CarteStatut.traitee ||
|
||||
card.statut == CarteStatut.refusee))
|
||||
const Spacer(),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
@@ -631,12 +704,15 @@ class _BulleTile extends StatelessWidget {
|
||||
),
|
||||
child: Text(
|
||||
'Ouvrir dans l’agenda ↗',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _aquaLink,
|
||||
color: encre.link,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: _aquaLink,
|
||||
decorationColor: encre.link,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -807,6 +883,80 @@ class _BulleTile extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Modale de refus : le controller vit avec le State (Échap / Annuler OK).
|
||||
class _RefuseDialog extends StatefulWidget {
|
||||
final String title;
|
||||
final String message;
|
||||
|
||||
const _RefuseDialog({
|
||||
required this.title,
|
||||
required this.message,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_RefuseDialog> createState() => _RefuseDialogState();
|
||||
}
|
||||
|
||||
class _RefuseDialogState extends State<_RefuseDialog> {
|
||||
late final TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
widget.title,
|
||||
style: GoogleFonts.merienda(fontWeight: FontWeight.w600),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
widget.message,
|
||||
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(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final text = _controller.text.trim();
|
||||
if (text.isEmpty) return;
|
||||
Navigator.pop(context, text);
|
||||
},
|
||||
child: const Text('Refuser'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bouton pilule peinte (coche / croix intégrées) — carte congés.
|
||||
class _PilulePeinteButton extends StatelessWidget {
|
||||
final String asset;
|
||||
|
||||
Reference in New Issue
Block a user