feat(#206,#207): cartes rose soin_enfant (backend + UI)
Closes #206 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,7 @@ 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_stream_client.dart';
|
||||
import 'package:p_tits_pas/services/auth_service.dart';
|
||||
import 'package:p_tits_pas/widgets/quotidien/ajouter_carte_modal.dart';
|
||||
import 'package:p_tits_pas/widgets/quotidien/carte.dart';
|
||||
import 'package:p_tits_pas/widgets/quotidien/quotidien_theme.dart';
|
||||
|
||||
@@ -14,6 +15,8 @@ import 'package:p_tits_pas/widgets/quotidien/quotidien_theme.dart';
|
||||
/// Widget partagé ; les actions dépendent du rôle + payload API.
|
||||
class CartesFeed extends StatefulWidget {
|
||||
final String? placementId;
|
||||
|
||||
/// Conservé pour override éventuel ; sinon le CTA ouvre [showAjouterCarteModal].
|
||||
final VoidCallback? onAjouterBulle;
|
||||
|
||||
const CartesFeed({
|
||||
@@ -171,24 +174,25 @@ class _CartesFeedState extends State<CartesFeed> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tri stable partagé : refusées → ouvertes → traitées, puis
|
||||
/// `cree_le` DESC + `id` (pas `modifie_le` : une validation ne
|
||||
/// re-mélange plus la file différemment selon les clients).
|
||||
/// Tri stable partagé : refusées → encore actives (ouvertes / infos
|
||||
/// période en cours) → traitées passées, puis `cree_le` DESC + `id`.
|
||||
static List<CarteBulle> _trier(List<CarteBulle> items) {
|
||||
int rang(CarteStatut s) {
|
||||
switch (s) {
|
||||
int rang(CarteBulle c) {
|
||||
switch (c.statut) {
|
||||
case CarteStatut.refusee:
|
||||
return 0;
|
||||
case CarteStatut.ouverte:
|
||||
return 1;
|
||||
case CarteStatut.traitee:
|
||||
return 2;
|
||||
// Absence info / arrêt encore dans la période : même rang que
|
||||
// ouverte (sinon elles tombent en bas dès la création).
|
||||
return c.restePleineDansFile ? 1 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
final out = List<CarteBulle>.from(items);
|
||||
out.sort((a, b) {
|
||||
final byStatut = rang(a.statut).compareTo(rang(b.statut));
|
||||
final byStatut = rang(a).compareTo(rang(b));
|
||||
if (byStatut != 0) return byStatut;
|
||||
final ac = a.creeLe?.millisecondsSinceEpoch ?? 0;
|
||||
final bc = b.creeLe?.millisecondsSinceEpoch ?? 0;
|
||||
@@ -372,23 +376,42 @@ class _CartesFeedState extends State<CartesFeed> {
|
||||
color: QuotidienTheme.ink,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: widget.onAjouterBulle,
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
label: Text(
|
||||
'Ajouter',
|
||||
style: GoogleFonts.merriweather(fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(child: _buildBody()),
|
||||
_AjouterCarteCta(onTap: _onAjouterCarte),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onAjouterCarte() async {
|
||||
if (widget.onAjouterBulle != null) {
|
||||
widget.onAjouterBulle!();
|
||||
return;
|
||||
}
|
||||
final placementId = widget.placementId;
|
||||
if (placementId == null || placementId.isEmpty) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Sélectionnez un couple de garde pour ajouter une carte.',
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final created = await showAjouterCarteModal(
|
||||
context,
|
||||
placementId: placementId,
|
||||
);
|
||||
if (created != null && mounted) {
|
||||
// SSE peut précéder ; refresh pour garantir la cohérence locale.
|
||||
await _load();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
if (_loading) {
|
||||
return const Center(
|
||||
@@ -477,7 +500,7 @@ class _CartesFeedState extends State<CartesFeed> {
|
||||
onRefresh: _load,
|
||||
child: ListView.separated(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 8),
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 4),
|
||||
itemCount: _items.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||
itemBuilder: (context, index) {
|
||||
@@ -588,8 +611,18 @@ class _BulleTile extends StatelessWidget {
|
||||
return '$jj ${_mois[dt.month - 1]} ${dt.year}';
|
||||
}
|
||||
|
||||
/// Une ligne type maquette : « Du 04 au 06 novembre 2026 ».
|
||||
String _fmtPeriodeUneLigne() {
|
||||
// Soin : « Le 08 octobre 2026 à 18h19 » (détail en italique sous la ligne).
|
||||
if (card.isSoinEnfant) {
|
||||
final jour = card.dateDebut != null
|
||||
? 'Le ${_fmtLong(card.dateDebut)}'
|
||||
: '';
|
||||
final h = card.heureReleveAffichee;
|
||||
if (jour.isEmpty) return h != null ? 'À $h' : '';
|
||||
if (h == null || h.isEmpty) return jour;
|
||||
return '$jour à $h';
|
||||
}
|
||||
|
||||
final d1 = DateTime.tryParse(card.dateDebut ?? '');
|
||||
final d2 = DateTime.tryParse(card.dateFin ?? '');
|
||||
if (d1 == null && d2 == null) return '';
|
||||
@@ -612,9 +645,10 @@ class _BulleTile extends StatelessWidget {
|
||||
'au $j2 ${_mois[b.month - 1]} ${b.year}';
|
||||
}
|
||||
|
||||
/// Arrêt maladie : reste opaque tant que la période n’est pas finie.
|
||||
/// Arrêt / absence info : reste opaque tant que la période n’est pas finie
|
||||
/// (même si `traitee` — absence = info seule dès la création).
|
||||
bool get _attenuee {
|
||||
if (card.isArretMaladie && card.periodeEncoreActive) return false;
|
||||
if (card.restePleineDansFile) return false;
|
||||
return card.statut == CarteStatut.traitee;
|
||||
}
|
||||
|
||||
@@ -692,8 +726,6 @@ class _BulleTile extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Text(
|
||||
periode,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -705,14 +737,27 @@ class _BulleTile extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
],
|
||||
if (card.isSoinEnfant &&
|
||||
(card.detailSoin?.trim().isNotEmpty ?? false)) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
card.detailSoin!,
|
||||
softWrap: true,
|
||||
style: GoogleFonts.merriweather(
|
||||
fontSize: 13,
|
||||
fontStyle: FontStyle.italic,
|
||||
height: 1.35,
|
||||
color: encre.ink,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (card.isCongeAm &&
|
||||
card.statut == CarteStatut.refusee &&
|
||||
card.motifRefus != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Motif : ${card.motifRefus}',
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: GoogleFonts.merriweather(
|
||||
fontSize: 13,
|
||||
fontStyle: FontStyle.italic,
|
||||
@@ -826,6 +871,11 @@ class _BulleTile extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildGeneric(BuildContext context) {
|
||||
final encre = _Encre.pour(card.colorFamily);
|
||||
final isEdited = card.modifieLe != null && (card.creeLe == null || card.modifieLe!.isAfter(card.creeLe!));
|
||||
final createdAt = card.creeLe ?? DateTime.now();
|
||||
final canEdit = card.isCreator && DateTime.now().difference(createdAt).inHours < 24;
|
||||
|
||||
final memeJour = card.dateDebut != null &&
|
||||
card.dateFin != null &&
|
||||
card.dateDebut == card.dateFin;
|
||||
@@ -846,23 +896,41 @@ class _BulleTile extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
card.titreAffiche,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: QuotidienTheme.ink,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
card.titreAffiche,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: QuotidienTheme.ink,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isEdited)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Text(
|
||||
'(Corrigée)',
|
||||
style: GoogleFonts.merriweather(
|
||||
fontSize: 11,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: QuotidienTheme.ink.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (card.dateDebut != null || card.dateFin != null) ...[
|
||||
if (card.dateDebut != null || card.dateFin != null || card.isSoinEnfant) ...[
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12),
|
||||
child: memeJour
|
||||
child: (memeJour || card.isSoinEnfant)
|
||||
? Text(
|
||||
'Le ${_fmtLong(card.dateDebut)}',
|
||||
_fmtPeriodeUneLigne(),
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -936,6 +1004,72 @@ class _BulleTile extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Edit / Delete buttons for creator if within 24h
|
||||
if (canEdit && !busy) ...[
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.edit, size: 16),
|
||||
label: const Text('Corriger'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: encre.link,
|
||||
textStyle: GoogleFonts.merriweather(fontSize: 12),
|
||||
),
|
||||
onPressed: () {
|
||||
showAjouterCarteModal(
|
||||
context,
|
||||
placementId: card.idPlacement,
|
||||
carteToEdit: card,
|
||||
);
|
||||
},
|
||||
),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.delete_outline, size: 16),
|
||||
label: const Text('Supprimer'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.red[700],
|
||||
textStyle: GoogleFonts.merriweather(fontSize: 12),
|
||||
),
|
||||
onPressed: () async {
|
||||
final confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('Supprimer cette carte ?', style: GoogleFonts.merienda()),
|
||||
content: const Text('Cette action est définitive.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: FilledButton.styleFrom(backgroundColor: Colors.red[700]),
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirm == true && context.mounted) {
|
||||
try {
|
||||
await CardsService.supprimer(card.id);
|
||||
// The stream will handle the removal locally via card.deleted
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Erreur: ${e.toString().replaceFirst('Exception: ', '')}')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
if (card.peutRepondre) ...[
|
||||
const SizedBox(height: 14),
|
||||
if (busy)
|
||||
@@ -1057,6 +1191,45 @@ class _RefuseDialogState extends State<_RefuseDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
/// CTA « Ajouter une carte » — pilule aquarelle en bas du feed (#175).
|
||||
class _AjouterCarteCta extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _AjouterCarteCta({required this.onTap});
|
||||
|
||||
static const _asset =
|
||||
'assets/images/cartes/btn_ajouter_carte_pilule.png';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 12, 4, 14),
|
||||
child: Center(
|
||||
child: Tooltip(
|
||||
message: 'Ajouter une carte',
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: 'Ajouter une carte',
|
||||
child: Image.asset(
|
||||
_asset,
|
||||
height: 48,
|
||||
fit: BoxFit.contain,
|
||||
filterQuality: FilterQuality.high,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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