feat(front): implémentation UI soins enfant (#207)
- Ajout des formulaires spécifiques (fièvre, bobo, médicament) dans la modale d'ajout - Formatage personnalisé du résumé des soins dans le feed - Boutons supprimer / corriger (24h) pour l'AM - Icônes contextuelles selon le sous-type de soin Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -38,6 +38,17 @@ class _AjouterCarteModalState extends State<AjouterCarteModal> {
|
||||
bool _submitting = false;
|
||||
String? _error;
|
||||
|
||||
TimeOfDay _heureReleve = TimeOfDay.now();
|
||||
|
||||
// --- Controllers Soins ---
|
||||
final _motifController = TextEditingController();
|
||||
final _temperatureController = TextEditingController();
|
||||
final _medicamentDonneController = TextEditingController();
|
||||
String? _boboNature;
|
||||
final _boboDescriptionController = TextEditingController();
|
||||
final _medicamentNomController = TextEditingController();
|
||||
final _medicamentContexteController = TextEditingController();
|
||||
|
||||
CarteTypeUi get _ui =>
|
||||
_selected?.ui ??
|
||||
CarteTypeUi.resolve(typeCode: _selected?.code ?? 'inconnu');
|
||||
@@ -51,6 +62,16 @@ class _AjouterCarteModalState extends State<AjouterCarteModal> {
|
||||
_loadTypes();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_temperatureController.dispose();
|
||||
_medicamentDonneController.dispose();
|
||||
_boboDescriptionController.dispose();
|
||||
_medicamentNomController.dispose();
|
||||
_medicamentContexteController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadTypes() async {
|
||||
setState(() {
|
||||
_loadingTypes = true;
|
||||
@@ -90,6 +111,22 @@ class _AjouterCarteModalState extends State<AjouterCarteModal> {
|
||||
return '$day/$m/${d.year}';
|
||||
}
|
||||
|
||||
Future<void> _pickTime() async {
|
||||
final picked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: _heureReleve,
|
||||
builder: (context, child) {
|
||||
return MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true),
|
||||
child: child ?? const SizedBox(),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (picked != null && mounted) {
|
||||
setState(() => _heureReleve = picked);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickSingleDay() async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
@@ -180,11 +217,69 @@ class _AjouterCarteModalState extends State<AjouterCarteModal> {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// V1 : pas de texte libre ; seul un sous-type catalogue peut alimenter motif.
|
||||
String? _motifOptionnel() => _sousTypeLabel();
|
||||
/// V1 : pas de texte libre pour le motif si la logique change.
|
||||
/// (On retire `motifOptionnel` car le backend va dériver du payload).
|
||||
Map<String, dynamic>? _buildPayload() {
|
||||
if (_selected?.code != 'soin_enfant') {
|
||||
final m = _motifController.text.trim();
|
||||
if (m.isNotEmpty) return {'motif': m};
|
||||
return null;
|
||||
}
|
||||
|
||||
final base = <String, dynamic>{
|
||||
'sous_type': _sousTypeCode,
|
||||
'heure_releve':
|
||||
'${_heureReleve.hour.toString().padLeft(2, '0')}:${_heureReleve.minute.toString().padLeft(2, '0')}',
|
||||
};
|
||||
|
||||
if (_sousTypeCode == 'fievre') {
|
||||
final t = _temperatureController.text.trim().replaceAll(',', '.');
|
||||
if (t.isNotEmpty) {
|
||||
base['temperature_c'] = double.tryParse(t);
|
||||
}
|
||||
base['medicament_donne'] = _medicamentDonneController.text.trim();
|
||||
} else if (_sousTypeCode == 'bobo') {
|
||||
base['bobo_nature'] = _boboNature;
|
||||
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;
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (_selected == null) return;
|
||||
|
||||
// Validations locales spécifiques soins
|
||||
if (_selected?.code == 'soin_enfant') {
|
||||
if (_sousTypeCode == null) {
|
||||
setState(() => _error = 'Veuillez choisir un sous-type.');
|
||||
return;
|
||||
}
|
||||
if (_sousTypeCode == 'fievre') {
|
||||
final t = _temperatureController.text.trim().replaceAll(',', '.');
|
||||
if (t.isNotEmpty && double.tryParse(t) == null) {
|
||||
setState(() => _error = 'La température est invalide.');
|
||||
return;
|
||||
}
|
||||
} else if (_sousTypeCode == 'bobo' && _boboNature == null) {
|
||||
setState(() => _error = 'Veuillez indiquer la nature du bobo.');
|
||||
return;
|
||||
} else if (_sousTypeCode == 'medicament' &&
|
||||
_medicamentNomController.text.trim().isEmpty) {
|
||||
setState(() => _error = 'Le nom du médicament est obligatoire.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_dateFin.isBefore(_dateDebut)) {
|
||||
setState(() => _error = 'La date de fin doit être après la date de début.');
|
||||
return;
|
||||
@@ -204,7 +299,7 @@ class _AjouterCarteModalState extends State<AjouterCarteModal> {
|
||||
? _dateDebut
|
||||
: _dateFin,
|
||||
),
|
||||
motif: _motifOptionnel(),
|
||||
payload: _buildPayload(),
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(created);
|
||||
@@ -255,10 +350,27 @@ class _AjouterCarteModalState extends State<AjouterCarteModal> {
|
||||
_buildTypeSelector(),
|
||||
const SizedBox(height: 16),
|
||||
if (_ui.periodeMode == CartePeriodeMode.singleDay)
|
||||
_DateField(
|
||||
label: 'Date',
|
||||
value: _fmtUi(_dateDebut),
|
||||
onTap: _submitting ? null : _pickSingleDay,
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _DateField(
|
||||
label: 'Date',
|
||||
value: _fmtUi(_dateDebut),
|
||||
onTap: _submitting ? null : _pickSingleDay,
|
||||
),
|
||||
),
|
||||
if (_selected?.code == 'soin_enfant') ...[
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _DateField(
|
||||
label: 'Heure',
|
||||
value: _heureReleve.format(context),
|
||||
icon: Icons.access_time,
|
||||
onTap: _submitting ? null : _pickTime,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
)
|
||||
else
|
||||
_DateField(
|
||||
@@ -297,6 +409,26 @@ class _AjouterCarteModalState extends State<AjouterCarteModal> {
|
||||
],
|
||||
),
|
||||
],
|
||||
if (_selected?.code == 'soin_enfant') _buildSoinForm(),
|
||||
if (_selected?.code != 'soin_enfant') ...[
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _motifController,
|
||||
enabled: !_submitting,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
labelText: _ui.motifRequired ? 'Motif *' : 'Motif',
|
||||
hintText: _ui.motifHint,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
validator: (v) {
|
||||
if (!_ui.motifRequired) return null;
|
||||
final text = v?.trim() ?? '';
|
||||
if (text.isEmpty) return 'Motif obligatoire';
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -374,17 +506,116 @@ class _AjouterCarteModalState extends State<AjouterCarteModal> {
|
||||
},
|
||||
);
|
||||
}
|
||||
Widget _buildSoinForm() {
|
||||
if (_sousTypeCode == null) return const SizedBox.shrink();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
if (_sousTypeCode == 'fievre') ...[
|
||||
TextFormField(
|
||||
controller: _temperatureController,
|
||||
enabled: !_submitting,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Température (°C) *',
|
||||
hintText: 'ex: 38.5',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _medicamentDonneController,
|
||||
enabled: !_submitting,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Médicament donné (optionnel)',
|
||||
hintText: 'ex: Doliprane 2.4kg',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
] else if (_sousTypeCode == 'bobo') ...[
|
||||
DropdownButtonFormField<String>(
|
||||
value: _boboNature,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nature du bobo *',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'Chute', child: Text('Chute')),
|
||||
DropdownMenuItem(value: 'Egratignure', child: Text('Égratignure')),
|
||||
DropdownMenuItem(value: 'Coup', child: Text('Coup')),
|
||||
DropdownMenuItem(value: 'Morsure', child: Text('Morsure')),
|
||||
DropdownMenuItem(value: 'Autre', child: Text('Autre')),
|
||||
],
|
||||
onChanged: _submitting
|
||||
? null
|
||||
: (val) {
|
||||
setState(() {
|
||||
_boboNature = val;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _boboDescriptionController,
|
||||
enabled: !_submitting,
|
||||
maxLines: 2,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Description / Zone touchée *',
|
||||
hintText: 'ex: Égratignure sur le genou droit',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
] else if (_sousTypeCode == 'medicament') ...[
|
||||
TextFormField(
|
||||
controller: _medicamentNomController,
|
||||
enabled: !_submitting,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom du médicament *',
|
||||
hintText: 'ex: Ventoline',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _medicamentContexteController,
|
||||
enabled: !_submitting,
|
||||
maxLines: 2,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Contexte / Posologie *',
|
||||
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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DateField extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final VoidCallback? onTap;
|
||||
final IconData icon;
|
||||
|
||||
const _DateField({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.onTap,
|
||||
this.icon = Icons.calendar_today_outlined,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -396,7 +627,7 @@ class _DateField extends StatelessWidget {
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: const Icon(Icons.calendar_today_outlined, size: 18),
|
||||
suffixIcon: Icon(icon, size: 18),
|
||||
),
|
||||
child: Text(
|
||||
value,
|
||||
|
||||
Reference in New Issue
Block a user