glp-companion/lib/features/pantry/presentation/widgets/add_pantry_item_dialog.dart

210 lines
7.4 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/utils/date_time_utils.dart';
import '../../domain/entities/pantry_category.dart';
import '../../domain/value_objects/measurement_units.dart';
import '../controllers/pantry_inventory_controller.dart';
/// Opens the "Add Pantry Item" dialog.
///
/// Resolves to `true` when the item was persisted, `false` when saving
/// failed, and `null` when the user dismissed the dialog.
Future<bool?> showAddPantryItemDialog(BuildContext context) => showDialog<bool>(
context: context,
builder: (context) => const AddPantryItemDialog(),
);
/// Form for stocking the pantry: name, quantity, unit, grocery aisle, and an
/// optional expiration date.
class AddPantryItemDialog extends ConsumerStatefulWidget {
const AddPantryItemDialog({super.key});
@override
ConsumerState<AddPantryItemDialog> createState() =>
_AddPantryItemDialogState();
}
class _AddPantryItemDialogState extends ConsumerState<AddPantryItemDialog> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _quantityController = TextEditingController();
late String _unit = pantryUnitOptions.first;
PantryCategory _category = PantryCategory.other;
DateTime? _expirationDate;
bool _isSaving = false;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Add Pantry Item'),
content: SizedBox(
width: 440,
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Name',
hintText: 'e.g. chicken breast',
border: OutlineInputBorder(),
),
textCapitalization: TextCapitalization.sentences,
validator: _nameValidator,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: TextFormField(
controller: _quantityController,
decoration: const InputDecoration(
labelText: 'Quantity',
border: OutlineInputBorder(),
),
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
validator: _quantityValidator,
),
),
const SizedBox(width: 12),
Expanded(
child: DropdownButtonFormField<String>(
initialValue: _unit,
decoration: const InputDecoration(
labelText: 'Unit',
border: OutlineInputBorder(),
),
items: [
for (final unit in pantryUnitOptions)
DropdownMenuItem(value: unit, child: Text(unit)),
],
onChanged: (unit) {
if (unit == null) return;
setState(() => _unit = unit);
},
),
),
],
),
const SizedBox(height: 16),
DropdownButtonFormField<PantryCategory>(
initialValue: _category,
decoration: const InputDecoration(
labelText: 'Aisle',
border: OutlineInputBorder(),
),
items: [
for (final category in PantryCategory.values)
DropdownMenuItem(
value: category,
child: Text(category.displayName),
),
],
onChanged: (category) {
if (category == null) return;
setState(() => _category = category);
},
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Text(
_expirationDate == null
? 'No expiration date'
: 'Expires ${formatMonthDay(_expirationDate!)}',
style: Theme.of(context).textTheme.bodyMedium,
),
),
if (_expirationDate != null)
IconButton(
tooltip: 'Clear date',
icon: const Icon(Icons.clear),
onPressed: () =>
setState(() => _expirationDate = null),
),
TextButton(
onPressed: _pickExpirationDate,
child: const Text('Pick Date'),
),
],
),
],
),
),
),
),
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.add),
label: const Text('Add Item'),
),
],
);
}
Future<void> _pickExpirationDate() async {
final now = DateTime.now();
final picked = await showDatePicker(
context: context,
initialDate: _expirationDate ?? now,
firstDate: DateTime(now.year - 1),
lastDate: DateTime(now.year + 5),
);
if (picked == null || !mounted) return;
setState(() => _expirationDate = picked);
}
Future<void> _save() async {
if (!(_formKey.currentState?.validate() ?? false)) return;
setState(() => _isSaving = true);
final saved =
await ref.read(pantryInventoryControllerProvider.notifier).addItem(
name: _nameController.text,
quantityOnHand: double.parse(_quantityController.text.trim()),
unit: _unit,
category: _category,
expirationDate: _expirationDate,
);
if (!mounted) return;
Navigator.of(context).pop(saved);
}
String? _nameValidator(String? value) {
final trimmed = (value ?? '').trim();
if (trimmed.isEmpty) return 'Enter a name';
if (trimmed.length > 128) return 'Name is too long (max 128)';
return null;
}
String? _quantityValidator(String? value) {
final parsed = double.tryParse((value ?? '').trim());
if (parsed == null || parsed < 0) return 'Enter a valid quantity';
return null;
}
@override
void dispose() {
_nameController.dispose();
_quantityController.dispose();
super.dispose();
}
}