import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/utils/date_time_utils.dart'; import '../../domain/entities/injection_site.dart'; import '../../domain/value_objects/glp1_compound.dart'; import '../controllers/medication_controller.dart'; /// Opens the interactive injection logging dialog. /// /// Resolves to `true` when the injection was persisted, `false` when saving /// failed, and `null` when the user dismissed the dialog. Future showLogInjectionDialog( BuildContext context, { Glp1Compound? initialCompound, InjectionSite? initialSite, }) { return showDialog( context: context, builder: (context) => LogInjectionDialog( initialCompound: initialCompound, initialSite: initialSite, ), ); } /// Form for logging a GLP-1 injection: compound, dosage, administration /// timestamp, and the 12-quadrant rotation site selector. class LogInjectionDialog extends ConsumerStatefulWidget { const LogInjectionDialog({ super.key, this.initialCompound, this.initialSite, }); /// Compound pre-selected from the latest logged dose. final Glp1Compound? initialCompound; /// Quadrant pre-selected from the rotation policy's suggestion. final InjectionSite? initialSite; @override ConsumerState createState() => _LogInjectionDialogState(); } class _LogInjectionDialogState extends ConsumerState { late Glp1Compound _compound = widget.initialCompound ?? Glp1Compound.semaglutide; late InjectionSite _site = widget.initialSite ?? InjectionSite.abdomenRightUpper; late DateTime _administeredAt = DateTime.now(); late final TextEditingController _doseController = TextEditingController( text: (widget.initialCompound ?? Glp1Compound.semaglutide) .defaultDoseMg .toString(), ); final GlobalKey _formKey = GlobalKey(); bool _isSaving = false; @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Log Injection'), content: SizedBox( width: 440, child: Form( key: _formKey, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ DropdownButtonFormField( initialValue: _compound, // Long labels ("Tirzepatide (Mounjaro / Zepbound)") must // ellipsize in the narrow mobile dialog instead of // overflowing the selected-item row. isExpanded: true, decoration: const InputDecoration( labelText: 'Compound', border: OutlineInputBorder(), ), items: [ for (final compound in Glp1Compound.values) DropdownMenuItem( value: compound, child: Text( '${compound.displayName} (${compound.brandNames})', maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], onChanged: (compound) { if (compound == null) return; setState(() { _compound = compound; _doseController.text = compound.defaultDoseMg.toString(); }); }, ), const SizedBox(height: 16), TextFormField( controller: _doseController, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration( labelText: 'Dosage (mg)', border: OutlineInputBorder(), suffixText: 'mg', ), validator: (value) { final dose = double.tryParse(value?.trim() ?? ''); if (dose == null || dose <= 0) { return 'Enter a positive dose in mg'; } return null; }, ), const SizedBox(height: 16), ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.schedule), title: const Text('Administered at'), subtitle: Text(formatDateTime(_administeredAt)), trailing: const Icon(Icons.edit_calendar_outlined), onTap: _isSaving ? null : _pickAdministeredAt, ), const SizedBox(height: 8), Text( 'Injection site', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 4), for (final region in InjectionBodyRegion.values) ...[ Padding( padding: const EdgeInsets.only(top: 8, bottom: 4), child: Text( region.displayName, style: Theme.of(context) .textTheme .labelLarge ?.copyWith(color: Colors.grey), ), ), Wrap( spacing: 8, runSpacing: 4, children: [ for (final site in InjectionSite.values .where((site) => site.region == region)) ChoiceChip( label: Text(site.shortLabel), selected: _site == site, onSelected: _isSaving ? null : (_) => setState(() => _site = site), ), ], ), ], ], ), ), ), ), actions: [ TextButton( onPressed: _isSaving ? null : () => Navigator.of(context).pop(), child: const Text('Cancel'), ), FilledButton.icon( onPressed: _isSaving ? null : _save, icon: _isSaving ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.check_circle_outline), label: const Text('Log Injection'), ), ], ); } /// Lets the user backdate the injection via sequential date/time pickers. Future _pickAdministeredAt() async { final now = DateTime.now(); final pickedDate = await showDatePicker( context: context, initialDate: _administeredAt, firstDate: DateTime(now.year - 1), lastDate: now, ); if (pickedDate == null || !mounted) return; final pickedTime = await showTimePicker( context: context, initialTime: TimeOfDay.fromDateTime(_administeredAt), ); if (!mounted) return; setState(() { _administeredAt = DateTime( pickedDate.year, pickedDate.month, pickedDate.day, pickedTime?.hour ?? _administeredAt.hour, pickedTime?.minute ?? _administeredAt.minute, ); }); } Future _save() async { if (!(_formKey.currentState?.validate() ?? false)) return; final doseMg = double.parse(_doseController.text.trim()); setState(() => _isSaving = true); final saved = await ref.read(medicationControllerProvider.notifier).logInjection( compoundName: _compound.displayName, doseMg: doseMg, site: _site, administeredAt: _administeredAt, ); if (!mounted) return; Navigator.of(context).pop(saved); } @override void dispose() { _doseController.dispose(); super.dispose(); } }