feat(#206,#207): cartes rose soin_enfant (backend + UI)
Closes #206 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,704 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:p_tits_pas/models/carte_bulle.dart';
|
||||
import 'package:p_tits_pas/models/carte_type_ui.dart';
|
||||
import 'package:p_tits_pas/services/api/cards_service.dart';
|
||||
import 'package:p_tits_pas/widgets/quotidien/quotidien_theme.dart';
|
||||
|
||||
/// Ouvre la modale Material générique « Ajouter une carte » (#175).
|
||||
/// Retourne la bulle créée, ou `null` si annulé.
|
||||
Future<CarteBulle?> showAjouterCarteModal(
|
||||
BuildContext context, {
|
||||
required String placementId,
|
||||
CarteBulle? carteToEdit,
|
||||
}) {
|
||||
return showDialog<CarteBulle>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AjouterCarteModal(
|
||||
placementId: placementId,
|
||||
carteToEdit: carteToEdit,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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,
|
||||
this.carteToEdit,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AjouterCarteModal> createState() => _AjouterCarteModalState();
|
||||
}
|
||||
|
||||
class _AjouterCarteModalState extends State<AjouterCarteModal> {
|
||||
List<CarteTypeEmissible> _types = const [];
|
||||
CarteTypeEmissible? _selected;
|
||||
String? _sousTypeCode;
|
||||
DateTime _dateDebut = DateTime.now();
|
||||
DateTime _dateFin = DateTime.now();
|
||||
bool _loadingTypes = true;
|
||||
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');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final now = DateTime.now();
|
||||
_dateDebut = DateTime(now.year, now.month, now.day);
|
||||
_dateFin = _dateDebut;
|
||||
_loadTypes();
|
||||
}
|
||||
|
||||
void _prefillIfEdit(List<CarteTypeEmissible> 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();
|
||||
_medicamentDonneController.dispose();
|
||||
_boboDescriptionController.dispose();
|
||||
_medicamentNomController.dispose();
|
||||
_medicamentContexteController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadTypes() async {
|
||||
setState(() {
|
||||
_loadingTypes = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final types = await CardsService.listerTypes();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_types = types;
|
||||
_selected = types.isNotEmpty ? types.first : null;
|
||||
_sousTypeCode = null;
|
||||
_loadingTypes = false;
|
||||
if (types.isEmpty) {
|
||||
_error = 'Aucun type de carte disponible pour votre rôle.';
|
||||
} else {
|
||||
_prefillIfEdit(types);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_loadingTypes = false;
|
||||
_error = e.toString().replaceFirst('Exception: ', '');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String _fmtIso(DateTime d) {
|
||||
final y = d.year.toString().padLeft(4, '0');
|
||||
final m = d.month.toString().padLeft(2, '0');
|
||||
final day = d.day.toString().padLeft(2, '0');
|
||||
return '$y-$m-$day';
|
||||
}
|
||||
|
||||
String _fmtUi(DateTime d) {
|
||||
final day = d.day.toString().padLeft(2, '0');
|
||||
final m = d.month.toString().padLeft(2, '0');
|
||||
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,
|
||||
useRootNavigator: true,
|
||||
initialDate: _dateDebut,
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime(2040),
|
||||
helpText: 'Date',
|
||||
cancelText: 'Annuler',
|
||||
confirmText: 'Valider',
|
||||
);
|
||||
if (picked == null || !mounted) return;
|
||||
final day = DateTime(picked.year, picked.month, picked.day);
|
||||
setState(() {
|
||||
_dateDebut = day;
|
||||
_dateFin = day;
|
||||
});
|
||||
}
|
||||
|
||||
/// Plage Material (jours surlignés), en boîte compacte — pas plein écran.
|
||||
/// Astuce : le mode calendrier de [DateRangePickerDialog] prend
|
||||
/// `MediaQuery.size` comme taille de dialog ; on la bride.
|
||||
Future<void> _pickDateRange() async {
|
||||
final initialEnd =
|
||||
_dateFin.isBefore(_dateDebut) ? _dateDebut : _dateFin;
|
||||
final picked = await showDialog<DateTimeRange>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
barrierDismissible: true,
|
||||
builder: (dialogContext) {
|
||||
final base = MediaQuery.of(dialogContext);
|
||||
final theme = Theme.of(dialogContext);
|
||||
return MediaQuery(
|
||||
data: base.copyWith(size: const Size(420, 560)),
|
||||
child: Theme(
|
||||
data: theme.copyWith(
|
||||
datePickerTheme: theme.datePickerTheme.copyWith(
|
||||
rangePickerShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
dialogTheme: theme.dialogTheme.copyWith(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: DateRangePickerDialog(
|
||||
initialDateRange: DateTimeRange(
|
||||
start: _dateDebut,
|
||||
end: initialEnd,
|
||||
),
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime(2040),
|
||||
currentDate: DateTime.now(),
|
||||
helpText: 'Début, puis fin (même jour : 2×)',
|
||||
saveText: 'Valider',
|
||||
cancelText: 'Annuler',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (picked == null || !mounted) return;
|
||||
setState(() {
|
||||
_dateDebut = DateTime(
|
||||
picked.start.year,
|
||||
picked.start.month,
|
||||
picked.start.day,
|
||||
);
|
||||
_dateFin = DateTime(
|
||||
picked.end.year,
|
||||
picked.end.month,
|
||||
picked.end.day,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
String? _sousTypeLabel() {
|
||||
final sous = _sousTypeCode;
|
||||
if (sous == null) return null;
|
||||
for (final s in _ui.sousTypes) {
|
||||
if (s.code == sous) return s.label;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 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') {
|
||||
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') {
|
||||
// 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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_submitting = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
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) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_submitting = false;
|
||||
_error = e.toString().replaceFirst('Exception: ', '');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'Ajouter une carte',
|
||||
style: GoogleFonts.merienda(fontWeight: FontWeight.w600),
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 420,
|
||||
child: _loadingTypes
|
||||
? const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 24),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (_error != null) ...[
|
||||
Text(
|
||||
_error!,
|
||||
style: GoogleFonts.merriweather(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
_buildTypeSelector(),
|
||||
const SizedBox(height: 16),
|
||||
if (_ui.periodeMode == CartePeriodeMode.singleDay)
|
||||
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(
|
||||
label: 'Période',
|
||||
value: _dateDebut == _dateFin
|
||||
? _fmtUi(_dateDebut)
|
||||
: '${_fmtUi(_dateDebut)} → ${_fmtUi(_dateFin)}',
|
||||
onTap: _submitting ? null : _pickDateRange,
|
||||
),
|
||||
if (_ui.sousTypes.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Sous-type',
|
||||
style: GoogleFonts.merriweather(
|
||||
fontSize: 13,
|
||||
color: QuotidienTheme.ink,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final s in _ui.sousTypes)
|
||||
ChoiceChip(
|
||||
label: Text(s.label),
|
||||
selected: _sousTypeCode == s.code,
|
||||
onSelected: _submitting
|
||||
? null
|
||||
: (sel) {
|
||||
setState(() {
|
||||
_sousTypeCode = sel ? s.code : null;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
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;
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _submitting ? null : () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: (_submitting ||
|
||||
_loadingTypes ||
|
||||
_selected == null ||
|
||||
_types.isEmpty)
|
||||
? null
|
||||
: _submit,
|
||||
child: _submitting
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('Envoyer'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTypeSelector() {
|
||||
if (_types.isEmpty) {
|
||||
return Text(
|
||||
'Aucun type disponible.',
|
||||
style: GoogleFonts.merriweather(fontSize: 13),
|
||||
);
|
||||
}
|
||||
if (_types.length == 1) {
|
||||
return Text(
|
||||
_types.first.label,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: QuotidienTheme.ink,
|
||||
),
|
||||
);
|
||||
}
|
||||
return DropdownButtonFormField<String>(
|
||||
value: _selected?.code,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Type de carte',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
for (final t in _types)
|
||||
DropdownMenuItem(value: t.code, child: Text(t.label)),
|
||||
],
|
||||
onChanged: _submitting
|
||||
? null
|
||||
: (code) {
|
||||
if (code == null) return;
|
||||
CarteTypeEmissible? next;
|
||||
for (final t in _types) {
|
||||
if (t.code == code) {
|
||||
next = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (next == null) return;
|
||||
setState(() {
|
||||
_selected = next;
|
||||
_sousTypeCode = null;
|
||||
if (next!.ui.periodeMode == CartePeriodeMode.singleDay) {
|
||||
_dateFin = _dateDebut;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
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: 'Égratignure', child: Text('Égratignure')),
|
||||
DropdownMenuItem(value: 'Coup', child: Text('Coup')),
|
||||
DropdownMenuItem(value: 'Piqûre', child: Text('Piqûre')),
|
||||
DropdownMenuItem(value: 'Brûlure', child: Text('Brûlure')),
|
||||
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 (optionnel)',
|
||||
hintText: 'ex: 2 bouffées suite à une quinte de toux',
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: Icon(icon, size: 18),
|
||||
),
|
||||
child: Text(
|
||||
value,
|
||||
style: GoogleFonts.merriweather(fontSize: 14),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user