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 showAjouterCarteModal( BuildContext context, { required String placementId, }) { return showDialog( context: context, barrierDismissible: false, builder: (ctx) => AjouterCarteModal(placementId: placementId), ); } /// Modale générique : types API + catalogue UI, période (pas de motif V1). class AjouterCarteModal extends StatefulWidget { final String placementId; const AjouterCarteModal({super.key, required this.placementId}); @override State createState() => _AjouterCarteModalState(); } class _AjouterCarteModalState extends State { List _types = const []; CarteTypeEmissible? _selected; String? _sousTypeCode; DateTime _dateDebut = DateTime.now(); DateTime _dateFin = DateTime.now(); bool _loadingTypes = true; bool _submitting = false; String? _error; 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(); } Future _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.'; } }); } 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 _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 _pickDateRange() async { final initialEnd = _dateFin.isBefore(_dateDebut) ? _dateDebut : _dateFin; final picked = await showDialog( 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 ; seul un sous-type catalogue peut alimenter motif. String? _motifOptionnel() => _sousTypeLabel(); Future _submit() async { if (_selected == null) 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 created = await CardsService.creer( typeCode: _selected!.code, idPlacement: widget.placementId, dateDebut: _fmtIso(_dateDebut), dateFin: _fmtIso( _ui.periodeMode == CartePeriodeMode.singleDay ? _dateDebut : _dateFin, ), motif: _motifOptionnel(), ); 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) _DateField( label: 'Date', value: _fmtUi(_dateDebut), onTap: _submitting ? null : _pickSingleDay, ) 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; }); }, ), ], ), ], ], ), ), ), 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( 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; } }); }, ); } } class _DateField extends StatelessWidget { final String label; final String value; final VoidCallback? onTap; const _DateField({ required this.label, required this.value, required this.onTap, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: InputDecorator( decoration: InputDecoration( labelText: label, border: const OutlineInputBorder(), suffixIcon: const Icon(Icons.calendar_today_outlined, size: 18), ), child: Text( value, style: GoogleFonts.merriweather(fontSize: 14), ), ), ); } }