import 'dart:io'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import '../models/product.dart'; import '../viewmodels/product_list_view_model.dart'; class ProductFormView extends StatefulWidget { final String? productId; const ProductFormView({ super.key, this.productId, }); bool get isEditing => productId != null; @override State createState() => _ProductFormViewState(); } class _ProductFormViewState extends State { final _formKey = GlobalKey(); final _nameController = TextEditingController(); final _priceController = TextEditingController(); final _stockController = TextEditingController(); final _lowStockController = TextEditingController(); String? _selectedCategory; final _categoryController = TextEditingController(); final _imagePicker = ImagePicker(); Product? _existingProduct; String? _imagePath; bool _isLoading = true; bool _isSaving = false; @override void initState() { super.initState(); _loadProductIfNeeded(); } Future _loadProductIfNeeded() async { if (!widget.isEditing) { setState(() => _isLoading = false); return; } final viewModel = context.read(); final product = await viewModel.getProductById(widget.productId!); if (!mounted) return; if (product == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Product not found.'), ), ); context.go('/products'); return; } _existingProduct = product; _nameController.text = product.name; _selectedCategory = product.category; _priceController.text = (product.priceInCents / 100).toStringAsFixed(2); _stockController.text = product.stockQuantity.toString(); _lowStockController.text = product.lowStockThreshold.toString(); _imagePath = product.imagePath; setState(() => _isLoading = false); } @override void dispose() { _nameController.dispose(); _priceController.dispose(); _stockController.dispose(); _lowStockController.dispose(); super.dispose(); } Future _copyImageToAppFolder(XFile pickedFile) async { final appDirectory = await getApplicationSupportDirectory(); final imagesDirectory = Directory( path.join(appDirectory.path, 'product_images'), ); if (!await imagesDirectory.exists()) { await imagesDirectory.create(recursive: true); } final extension = path.extension(pickedFile.path); final fileName = 'product_${DateTime.now().millisecondsSinceEpoch}$extension'; final newPath = path.join(imagesDirectory.path, fileName); final copiedFile = await File(pickedFile.path).copy(newPath); return copiedFile.path; } Future _pickImage() async { final pickedFile = await _imagePicker.pickImage( source: ImageSource.gallery, imageQuality: 85, maxWidth: 1000, ); if (pickedFile == null) return; final copiedImagePath = await _copyImageToAppFolder(pickedFile); if (!mounted) return; setState(() { _imagePath = copiedImagePath; }); } int _parsePriceToCents(String value) { final normalized = value.replaceAll(',', '.'); final euros = double.tryParse(normalized) ?? 0; return (euros * 100).round(); } Future _save() async { if (!_formKey.currentState!.validate()) return; if (_imagePath == null || _imagePath!.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Choose a product image.'), ), ); return; } if (_selectedCategory == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Please select a category.'), ), ); return; } setState(() => _isSaving = true); final viewModel = context.read(); final name = _nameController.text.trim(); final category = _selectedCategory!; final priceInCents = _parsePriceToCents(_priceController.text); final stockQuantity = int.parse(_stockController.text); final lowStockThreshold = int.parse(_lowStockController.text); if (widget.isEditing) { final updatedProduct = _existingProduct!.copyWith( name: name, category: category, priceInCents: priceInCents, stockQuantity: stockQuantity, lowStockThreshold: lowStockThreshold, imagePath: _imagePath, ); await viewModel.updateProduct(updatedProduct); } else { await viewModel.addProduct( name: name, category: category, priceInCents: priceInCents, stockQuantity: stockQuantity, lowStockThreshold: lowStockThreshold, imagePath: _imagePath, ); } if (!mounted) return; setState(() => _isSaving = false); context.go('/products'); } Future _delete() async { if (!widget.isEditing) return; final confirmed = await showDialog( context: context, builder: (dialogContext) { return AlertDialog( title: const Text('Delete product?'), content: const Text( 'This will remove the product from the product list.', ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), child: const Text('Cancel'), ), FilledButton( onPressed: () => Navigator.of(dialogContext).pop(true), child: const Text('Delete'), ), ], ); }, ); if (confirmed != true) return; final viewModel = context.read(); await viewModel.deleteProduct(widget.productId!); if (!mounted) return; context.go('/products'); } Widget _buildImagePicker(BuildContext context) { final hasImage = _imagePath != null && File(_imagePath!).existsSync(); return InkWell( onTap: _pickImage, borderRadius: BorderRadius.circular(12), child: Container( height: 220, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all( color: Theme.of(context).colorScheme.outline, ), ), clipBehavior: Clip.antiAlias, child: hasImage ? Stack( fit: StackFit.expand, children: [ Image.file( File(_imagePath!), fit: BoxFit.fitHeight, ), Positioned( right: 12, bottom: 12, child: FilledButton.icon( onPressed: _pickImage, icon: const Icon(Icons.image), label: const Text('Change image'), ), ), ], ) : Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.add_photo_alternate_outlined, size: 48), const SizedBox(height: 12), Text( 'Choose product image', style: Theme.of(context).textTheme.titleMedium, ), ], ), ), ), ); } @override Widget build(BuildContext context) { final title = widget.isEditing ? 'Edit product' : 'Add product'; return Scaffold( appBar: AppBar( title: Text(title), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => context.go('/products'), ), actions: [ if (widget.isEditing) IconButton( onPressed: _delete, icon: const Icon(Icons.delete_outline), ), ], ), resizeToAvoidBottomInset: false, body: _isLoading ? const Center( child: CircularProgressIndicator(), ) : Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), child: Form( key: _formKey, child: ListView( padding: const EdgeInsets.all(24), children: [ _buildImagePicker(context), const SizedBox(height: 24), TextFormField( controller: _nameController, decoration: const InputDecoration( labelText: 'Product name', border: OutlineInputBorder(), ), textInputAction: TextInputAction.next, validator: (value) { if (value == null || value.trim().isEmpty) { return 'Enter a product name.'; } return null; }, ), const SizedBox(height: 16), Consumer( builder: (context, viewModel, child) { return LayoutBuilder( builder: (context, constraints) { return DropdownMenu( width: constraints.maxWidth, initialSelection: _selectedCategory, enableFilter: true, enableSearch: true, controller: _categoryController, requestFocusOnTap: true, label: const Text('Category'), hintText: 'Select a category', dropdownMenuEntries: viewModel.categories .map( (category) => DropdownMenuEntry( value: category, label: category, ), ) .toList(), onSelected: (value) { setState(() { _selectedCategory = value; }); }, ); }, ); }, ), const SizedBox(height: 16), TextFormField( controller: _priceController, decoration: const InputDecoration( labelText: 'Price', prefixText: '€ ', border: OutlineInputBorder(), ), keyboardType: const TextInputType.numberWithOptions( decimal: true, ), textInputAction: TextInputAction.next, validator: (value) { if (value == null || value.trim().isEmpty) { return 'Enter a price.'; } final normalized = value.replaceAll(',', '.'); final price = double.tryParse(normalized); if (price == null || price < 0) { return 'Enter a valid price.'; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: _stockController, decoration: const InputDecoration( labelText: 'Current stock', border: OutlineInputBorder(), ), keyboardType: TextInputType.number, textInputAction: TextInputAction.next, validator: (value) { final number = int.tryParse(value ?? ''); if (number == null || number < 0) { return 'Enter a valid stock amount.'; } return null; }, ), const SizedBox(height: 16), TextFormField( controller: _lowStockController, decoration: const InputDecoration( labelText: 'Low stock warning threshold', border: OutlineInputBorder(), ), keyboardType: TextInputType.number, validator: (value) { final number = int.tryParse(value ?? ''); if (number == null || number < 0) { return 'Enter a valid threshold.'; } return null; }, ), const SizedBox(height: 24), FilledButton.icon( onPressed: _isSaving ? null : _save, icon: _isSaving ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( strokeWidth: 2, ), ) : const Icon(Icons.save), label: Text( widget.isEditing ? 'Save changes' : 'Add product', ), ), ], ), ), ), ), ); } }