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>
This commit is contained in:
2026-10-05 22:36:29 +02:00
co-authored by Cursor
parent 5d36b0f4c1
commit a36e9ef176
8 changed files with 677 additions and 32 deletions
+82 -19
View File
@@ -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) {
@@ -612,9 +635,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;
}
@@ -1057,6 +1081,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;