diff --git a/frontend/lib/models/carte_bulle.dart b/frontend/lib/models/carte_bulle.dart index 81fbe6f..54e851e 100644 --- a/frontend/lib/models/carte_bulle.dart +++ b/frontend/lib/models/carte_bulle.dart @@ -13,6 +13,7 @@ enum CarteStatut { switch (raw) { case 'refusee': return CarteStatut.refusee; + case 'fermee': case 'traitee': return CarteStatut.traitee; case 'ouverte': @@ -113,23 +114,7 @@ class CarteBulle { /// Helper pour extraire les informations de soin String? get resumerSoin { if (!isSoinEnfant) return null; - - final st = payload['sous_type']?.toString(); - final h = payload['heure_releve']?.toString(); - final hStr = h != null && h.isNotEmpty ? ' à $h' : ''; - - if (st == 'fievre') { - final t = payload['temperature_c']?.toString() ?? '?'; - return 'Température : $t°C$hStr'; - } else if (st == 'bobo') { - final nature = payload['bobo_nature']?.toString() ?? 'Bobo'; - final desc = payload['description']?.toString() ?? ''; - return desc.isNotEmpty ? '$nature$hStr ($desc)' : '$nature$hStr'; - } else if (st == 'medicament') { - final nom = payload['medicament_nom']?.toString() ?? 'Médicament'; - return '$nom donné$hStr'; - } - return null; + return payload['motif']?.toString(); } /// Motif de refus : réponse API, sinon motif payload (co-parent / reload). diff --git a/frontend/lib/models/carte_type_ui.dart b/frontend/lib/models/carte_type_ui.dart index 8f36750..133661a 100644 --- a/frontend/lib/models/carte_type_ui.dart +++ b/frontend/lib/models/carte_type_ui.dart @@ -49,18 +49,14 @@ class CarteTypeUi { label: 'Arrêt maladie', motifHint: 'Précisions (optionnel)', ), - // Point d’extension #soins — visible dès que le back l’émet. 'soin_enfant': CarteTypeUi( typeCode: 'soin_enfant', label: 'Soin / bobo', - motifHint: 'Décrivez le soin ou le bobo', - motifRequired: true, periodeMode: CartePeriodeMode.singleDay, sousTypes: [ - CarteSousTypeUi(code: 'temperature', label: 'Température'), + CarteSousTypeUi(code: 'fievre', label: 'Fièvre'), CarteSousTypeUi(code: 'bobo', label: 'Bobo'), CarteSousTypeUi(code: 'medicament', label: 'Médicament'), - CarteSousTypeUi(code: 'autre', label: 'Autre'), ], ), }; diff --git a/frontend/lib/services/api/cards_service.dart b/frontend/lib/services/api/cards_service.dart index 3574330..c505173 100644 --- a/frontend/lib/services/api/cards_service.dart +++ b/frontend/lib/services/api/cards_service.dart @@ -91,6 +91,7 @@ class CardsService { required String dateDebut, required String dateFin, String? motif, + Map? payload, }) async { final response = await http.post( Uri.parse('${ApiConfig.baseUrl}${ApiConfig.cards}'), @@ -101,6 +102,7 @@ class CardsService { 'date_debut': dateDebut, 'date_fin': dateFin, if (motif != null && motif.trim().isNotEmpty) 'motif': motif.trim(), + if (payload != null) 'payload': payload, }), ); @@ -114,6 +116,49 @@ class CardsService { return CarteBulle.fromJson(Map.from(decoded as Map)); } + /// Modifier une carte (corriger un soin, dates en attente...). + static Future maj({ + required String cardId, + String? dateDebut, + String? dateFin, + String? motif, + Map? payload, + }) async { + final response = await http.patch( + Uri.parse('${ApiConfig.baseUrl}${ApiConfig.cards}/$cardId'), + headers: await _headers(), + body: jsonEncode({ + if (dateDebut != null) 'date_debut': dateDebut, + if (dateFin != null) 'date_fin': dateFin, + if (motif != null && motif.trim().isNotEmpty) 'motif': motif.trim(), + if (payload != null) 'payload': payload, + }), + ); + + if (response.statusCode != 200) { + throw Exception( + _extractError(response.body, 'Erreur lors de la mise à jour'), + ); + } + + final decoded = jsonDecode(response.body); + return CarteBulle.fromJson(Map.from(decoded as Map)); + } + + /// Supprimer une bulle (AM 24h ou avant traitement). + static Future supprimer(String cardId) async { + final response = await http.delete( + Uri.parse('${ApiConfig.baseUrl}${ApiConfig.cards}/$cardId'), + headers: await _headers(), + ); + + if (response.statusCode != 204 && response.statusCode != 200) { + throw Exception( + _extractError(response.body, 'Erreur lors de la suppression'), + ); + } + } + /// Répondre : accept | refuse | ack. static Future repondre({ required String cardId, diff --git a/frontend/lib/widgets/quotidien/ajouter_carte_modal.dart b/frontend/lib/widgets/quotidien/ajouter_carte_modal.dart index 087c599..e0507de 100644 --- a/frontend/lib/widgets/quotidien/ajouter_carte_modal.dart +++ b/frontend/lib/widgets/quotidien/ajouter_carte_modal.dart @@ -10,19 +10,28 @@ import 'package:p_tits_pas/widgets/quotidien/quotidien_theme.dart'; Future showAjouterCarteModal( BuildContext context, { required String placementId, + CarteBulle? carteToEdit, }) { return showDialog( context: context, barrierDismissible: false, - builder: (ctx) => AjouterCarteModal(placementId: placementId), + builder: (ctx) => AjouterCarteModal( + placementId: placementId, + carteToEdit: carteToEdit, + ), ); } -/// Modale générique : types API + catalogue UI, période (pas de motif V1). +/// Modale générique : types API + catalogue UI, période, soins class AjouterCarteModal extends StatefulWidget { final String placementId; + final CarteBulle? carteToEdit; - const AjouterCarteModal({super.key, required this.placementId}); + const AjouterCarteModal({ + super.key, + required this.placementId, + this.carteToEdit, + }); @override State createState() => _AjouterCarteModalState(); @@ -62,6 +71,57 @@ class _AjouterCarteModalState extends State { _loadTypes(); } + void _prefillIfEdit(List types) { + if (widget.carteToEdit == null) return; + final card = widget.carteToEdit!; + final type = types.where((t) => t.code == card.typeCode).firstOrNull; + if (type != null) { + _selected = type; + } + + if (card.dateDebut != null) { + _dateDebut = DateTime.tryParse(card.dateDebut!) ?? _dateDebut; + } + if (card.dateFin != null) { + _dateFin = DateTime.tryParse(card.dateFin!) ?? _dateFin; + } + + _motifController.text = card.motif ?? ''; + + if (card.typeCode == 'soin_enfant') { + final p = card.payload; + _sousTypeCode = p['sous_type']?.toString(); + final h = p['heure_releve']?.toString(); + if (h != null && h.contains(':')) { + final parts = h.split(':'); + _heureReleve = TimeOfDay( + hour: int.tryParse(parts[0]) ?? 0, + minute: int.tryParse(parts[1]) ?? 0, + ); + } + + if (_sousTypeCode == 'fievre') { + _temperatureController.text = p['temperature_c']?.toString() ?? ''; + _medicamentDonneController.text = p['medicament_donne']?.toString() ?? ''; + } else if (_sousTypeCode == 'bobo') { + final nature = p['bobo_nature']?.toString() ?? ''; + final naturesValides = ['Chute', 'Coup', 'Égratignure', 'Piqûre', 'Brûlure', 'Autre']; + if (naturesValides.map((n) => n.toLowerCase().replaceAll('é', 'e').replaceAll('û', 'u')).contains(nature.toLowerCase())) { + _boboNature = naturesValides.firstWhere( + (n) => n.toLowerCase().replaceAll('é', 'e').replaceAll('û', 'u') == nature.toLowerCase().replaceAll('é', 'e').replaceAll('û', 'u'), + orElse: () => 'Autre' + ); + } else { + _boboNature = 'Autre'; + } + _boboDescriptionController.text = p['description']?.toString() ?? ''; + } else if (_sousTypeCode == 'medicament') { + _medicamentNomController.text = p['medicament_nom']?.toString() ?? ''; + _medicamentContexteController.text = p['contexte']?.toString() ?? ''; + } + } + } + @override void dispose() { _temperatureController.dispose(); @@ -87,6 +147,8 @@ class _AjouterCarteModalState extends State { _loadingTypes = false; if (types.isEmpty) { _error = 'Aucun type de carte disponible pour votre rôle.'; + } else { + _prefillIfEdit(types); } }); } catch (e) { @@ -221,8 +283,6 @@ class _AjouterCarteModalState extends State { /// (On retire `motifOptionnel` car le backend va dériver du payload). Map? _buildPayload() { if (_selected?.code != 'soin_enfant') { - final m = _motifController.text.trim(); - if (m.isNotEmpty) return {'motif': m}; return null; } @@ -238,20 +298,16 @@ class _AjouterCarteModalState extends State { base['temperature_c'] = double.tryParse(t); } base['medicament_donne'] = _medicamentDonneController.text.trim(); - } else if (_sousTypeCode == 'bobo') { - base['bobo_nature'] = _boboNature; + } else if (_sousTypeCode == 'bobo') { + // Normaliser l'enregistrement pour le back : + final nature = _boboNature?.toLowerCase().replaceAll('é', 'e').replaceAll('û', 'u') ?? 'autre'; + base['bobo_nature'] = nature; base['description'] = _boboDescriptionController.text.trim(); } else if (_sousTypeCode == 'medicament') { base['medicament_nom'] = _medicamentNomController.text.trim(); base['contexte'] = _medicamentContexteController.text.trim(); } - // Add motif to base payload for soin_enfant too - final m = _motifController.text.trim(); - if (m.isNotEmpty) { - base['motif'] = m; - } - return base; } @@ -290,17 +346,36 @@ class _AjouterCarteModalState extends State { _error = null; }); try { - final created = await CardsService.creer( - typeCode: _selected!.code, - idPlacement: widget.placementId, - dateDebut: _fmtIso(_dateDebut), - dateFin: _fmtIso( - _ui.periodeMode == CartePeriodeMode.singleDay - ? _dateDebut - : _dateFin, - ), - payload: _buildPayload(), - ); + final payload = _buildPayload(); + + final dynamic created; + if (widget.carteToEdit != null) { + created = await CardsService.maj( + cardId: widget.carteToEdit!.id, + dateDebut: _fmtIso(_dateDebut), + dateFin: _fmtIso( + _ui.periodeMode == CartePeriodeMode.singleDay + ? _dateDebut + : _dateFin, + ), + motif: _selected?.code == 'soin_enfant' ? null : _motifController.text.trim(), + payload: payload, + ); + } else { + created = await CardsService.creer( + typeCode: _selected!.code, + idPlacement: widget.placementId, + dateDebut: _fmtIso(_dateDebut), + dateFin: _fmtIso( + _ui.periodeMode == CartePeriodeMode.singleDay + ? _dateDebut + : _dateFin, + ), + motif: _selected?.code == 'soin_enfant' ? null : _motifController.text.trim(), + payload: payload, + ); + } + if (!mounted) return; Navigator.of(context).pop(created); } catch (e) { @@ -543,9 +618,10 @@ class _AjouterCarteModalState extends State { ), items: const [ DropdownMenuItem(value: 'Chute', child: Text('Chute')), - DropdownMenuItem(value: 'Egratignure', child: Text('Égratignure')), + DropdownMenuItem(value: 'Égratignure', child: Text('Égratignure')), DropdownMenuItem(value: 'Coup', child: Text('Coup')), - DropdownMenuItem(value: 'Morsure', child: Text('Morsure')), + DropdownMenuItem(value: 'Piqûre', child: Text('Piqûre')), + DropdownMenuItem(value: 'Brûlure', child: Text('Brûlure')), DropdownMenuItem(value: 'Autre', child: Text('Autre')), ], onChanged: _submitting @@ -583,23 +659,12 @@ class _AjouterCarteModalState extends State { enabled: !_submitting, maxLines: 2, decoration: const InputDecoration( - labelText: 'Contexte / Posologie *', + labelText: 'Contexte / Posologie (optionnel)', hintText: 'ex: 2 bouffées suite à une quinte de toux', border: OutlineInputBorder(), ), ), ], - const SizedBox(height: 16), - TextFormField( - controller: _motifController, - enabled: !_submitting, - maxLines: 2, - decoration: const InputDecoration( - labelText: 'Remarques additionnelles (optionnel)', - hintText: 'Observations générales...', - border: OutlineInputBorder(), - ), - ), ], ); } diff --git a/frontend/lib/widgets/quotidien/cartes_feed.dart b/frontend/lib/widgets/quotidien/cartes_feed.dart index 69cd6f6..86fc0a1 100644 --- a/frontend/lib/widgets/quotidien/cartes_feed.dart +++ b/frontend/lib/widgets/quotidien/cartes_feed.dart @@ -1005,11 +1005,10 @@ class _BulleTile extends StatelessWidget { textStyle: GoogleFonts.merriweather(fontSize: 12), ), onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('La correction de carte sera implémentée ultérieurement.'), - duration: Duration(seconds: 2), - ), + showAjouterCarteModal( + context, + placementId: card.idPlacement, + carteToEdit: card, ); }, ),