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';
|
return 'assets/images/cartes/icon_carte_maladie.png';
|
||||||
case 'absence_enfant':
|
case 'absence_enfant':
|
||||||
case 'absence_enfant_modif':
|
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
|
// soin_enfant : icône selon sous-type payload — à brancher plus tard
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.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/models/carte_bulle.dart';
|
||||||
import 'package:p_tits_pas/services/api/cards_service.dart';
|
import 'package:p_tits_pas/services/api/cards_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/quotidien/carte.dart';
|
import 'package:p_tits_pas/widgets/quotidien/carte.dart';
|
||||||
@@ -138,8 +139,10 @@ class _CartesFeedState extends State<CartesFeed> {
|
|||||||
style: GoogleFonts.merienda(fontWeight: FontWeight.w600),
|
style: GoogleFonts.merienda(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
content: Text(
|
content: Text(
|
||||||
'Confirmez-vous l’acceptation de « ${card.titreAffiche} » '
|
card.isCongeAm
|
||||||
'(${_periodeCourte(card)}) ?',
|
? 'Confirmez-vous les congés ${_periodeDuAu(card)} ?'
|
||||||
|
: 'Confirmez-vous l’acceptation de « ${card.titreAffiche} » '
|
||||||
|
'(${_periodeCourte(card)}) ?',
|
||||||
style: GoogleFonts.merriweather(fontSize: 14, height: 1.4),
|
style: GoogleFonts.merriweather(fontSize: 14, height: 1.4),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -168,8 +171,14 @@ class _CartesFeedState extends State<CartesFeed> {
|
|||||||
style: GoogleFonts.merienda(fontWeight: FontWeight.w600),
|
style: GoogleFonts.merienda(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
content: Text(
|
content: Text(
|
||||||
'Confirmez-vous avoir bien reçu cet arrêt maladie '
|
card.isArretMaladie
|
||||||
'(${_periodeCourte(card)}) ?',
|
? '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),
|
style: GoogleFonts.merriweather(fontSize: 14, height: 1.4),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -190,59 +199,36 @@ class _CartesFeedState extends State<CartesFeed> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onRefuse(CarteBulle card) async {
|
Future<void> _onRefuse(CarteBulle card) async {
|
||||||
final controller = TextEditingController();
|
final comment = await showDialog<String>(
|
||||||
final ok = await showDialog<bool>(
|
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => _RefuseDialog(
|
||||||
title: Text(
|
title: card.isCongeAm ? 'Refuser les congés ?' : 'Refuser la demande',
|
||||||
'Refuser la demande',
|
message: card.isCongeAm
|
||||||
style: GoogleFonts.merienda(fontWeight: FontWeight.w600),
|
? 'Indiquez le motif du refus des congés '
|
||||||
),
|
'${_periodeDuAu(card)}.'
|
||||||
content: Column(
|
: 'Indiquez le motif du refus pour « ${card.titreAffiche} ».',
|
||||||
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'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (ok == true && controller.text.trim().isNotEmpty) {
|
if (comment != null && comment.isNotEmpty) {
|
||||||
await _respond(
|
await _respond(card, action: 'refuse', comment: comment);
|
||||||
card,
|
|
||||||
action: 'refuse',
|
|
||||||
comment: controller.text.trim(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
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) {
|
static String _periodeCourte(CarteBulle card) {
|
||||||
final d = card.dateDebut;
|
final d = card.dateDebut;
|
||||||
final f = card.dateFin;
|
final f = card.dateFin;
|
||||||
@@ -258,6 +244,23 @@ class _CartesFeedState extends State<CartesFeed> {
|
|||||||
return '${fmt(d)} → ${fmt(f)}';
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
@@ -404,14 +407,60 @@ class _CartesFeedState extends State<CartesFeed> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BulleTile extends StatelessWidget {
|
/// Encre (titre / date / lien) accordée à la teinte de fond de la bulle.
|
||||||
/// Encre aqua foncé — harmonisée fond aqua / valise (pas le violet).
|
class _Encre {
|
||||||
static const Color _aquaInk = Color(0xFF315F63);
|
final Color ink;
|
||||||
/// Date : même famille, ~12 % plus claire.
|
final Color date;
|
||||||
static const Color _aquaDate = Color(0xFF4A7276);
|
final Color link;
|
||||||
/// Lien agenda : légère dérive bleue.
|
const _Encre(this.ink, this.date, this.link);
|
||||||
static const Color _aquaLink = Color(0xFF2B5868);
|
|
||||||
|
|
||||||
|
/// 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 CarteBulle card;
|
||||||
final bool busy;
|
final bool busy;
|
||||||
final VoidCallback onAccept;
|
final VoidCallback onAccept;
|
||||||
@@ -481,12 +530,8 @@ class _BulleTile extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (card.isCongeAm) return _buildConge(context);
|
|
||||||
return _buildGeneric(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildConge(BuildContext context) {
|
|
||||||
final periode = _fmtPeriodeUneLigne();
|
final periode = _fmtPeriodeUneLigne();
|
||||||
|
final encre = _Encre.pour(card.colorFamily);
|
||||||
|
|
||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: _attenuee ? 0.55 : 1,
|
opacity: _attenuee ? 0.55 : 1,
|
||||||
@@ -511,7 +556,7 @@ class _BulleTile extends StatelessWidget {
|
|||||||
style: GoogleFonts.merienda(
|
style: GoogleFonts.merienda(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: _aquaInk,
|
color: encre.ink,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (periode.isNotEmpty) ...[
|
if (periode.isNotEmpty) ...[
|
||||||
@@ -525,6 +570,8 @@ class _BulleTile extends StatelessWidget {
|
|||||||
height: 28,
|
height: 28,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
|
color: encre.ink,
|
||||||
|
colorBlendMode: BlendMode.srcIn,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -535,7 +582,7 @@ class _BulleTile extends StatelessWidget {
|
|||||||
style: GoogleFonts.merienda(
|
style: GoogleFonts.merienda(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: _aquaDate,
|
color: encre.date,
|
||||||
height: 1.25,
|
height: 1.25,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -555,7 +602,7 @@ class _BulleTile extends StatelessWidget {
|
|||||||
style: GoogleFonts.merriweather(
|
style: GoogleFonts.merriweather(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
color: _aquaInk,
|
color: encre.ink,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
] else if (card.statut == CarteStatut.ouverte &&
|
] else if (card.statut == CarteStatut.ouverte &&
|
||||||
@@ -595,25 +642,51 @@ class _BulleTile extends StatelessWidget {
|
|||||||
height: 28,
|
height: 28,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
)
|
)
|
||||||
: Row(
|
: card.responseMode == CarteResponseMode.ack
|
||||||
mainAxisSize: MainAxisSize.min,
|
? _PilulePeinteButton(
|
||||||
children: [
|
|
||||||
_PilulePeinteButton(
|
|
||||||
asset:
|
asset:
|
||||||
'assets/images/cartes/btn_coche_pilule.png',
|
'assets/images/cartes/btn_coche_pilule.png',
|
||||||
tooltip: 'Accepter',
|
tooltip: 'Bien reçu',
|
||||||
onTap: onAccept,
|
onTap: onAck,
|
||||||
),
|
)
|
||||||
const SizedBox(width: 10),
|
: Row(
|
||||||
_PilulePeinteButton(
|
mainAxisSize: MainAxisSize.min,
|
||||||
asset:
|
children: [
|
||||||
'assets/images/cartes/btn_croix_pilule.png',
|
_PilulePeinteButton(
|
||||||
tooltip: 'Refuser',
|
asset:
|
||||||
onTap: onRefuse,
|
'assets/images/cartes/btn_coche_pilule.png',
|
||||||
),
|
tooltip: 'Accepter',
|
||||||
],
|
onTap: onAccept,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const SizedBox(width: 10),
|
||||||
|
_PilulePeinteButton(
|
||||||
|
asset:
|
||||||
|
'assets/images/cartes/btn_croix_pilule.png',
|
||||||
|
tooltip: 'Refuser',
|
||||||
|
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(
|
InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -631,12 +704,15 @@ class _BulleTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Ouvrir dans l’agenda ↗',
|
'Ouvrir dans l’agenda ↗',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
style: GoogleFonts.merienda(
|
style: GoogleFonts.merienda(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: _aquaLink,
|
color: encre.link,
|
||||||
decoration: TextDecoration.underline,
|
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.
|
/// Bouton pilule peinte (coche / croix intégrées) — carte congés.
|
||||||
class _PilulePeinteButton extends StatelessWidget {
|
class _PilulePeinteButton extends StatelessWidget {
|
||||||
final String asset;
|
final String asset;
|
||||||
|
|||||||
Reference in New Issue
Block a user