import os new_content = """import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:image_picker/image_picker.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart' as import_storage; import '../../../core/network/dio_client.dart'; import '../../../../core/widgets/smart_search_dropdown.dart'; import '../domain/product.dart'; import '../providers/products_provider.dart'; import '../providers/product_categories_provider.dart'; import '../../business/providers/business_provider.dart'; import '../../transactions/presentation/widgets/attachment_gallery_screen.dart'; class AddProductScreen extends ConsumerStatefulWidget { final Product? product; const AddProductScreen({super.key, this.product}); @override ConsumerState createState() => _AddProductScreenState(); } class _AddProductScreenState extends ConsumerState { final _formKey = GlobalKey(); final _hsnController = TextEditingController(); final _gstController = TextEditingController(); // Basic String _name = ''; String _sku = ''; ProductCategory? _selectedCategory; int? _uomId; // Properties String _color = ''; // Pricing & Inventory double _purchasePrice = 0; double _sellingPrice = 0; // Media final List _images = []; final List _existingImageIds = []; bool _isSaving = false; String? _token; @override void initState() { super.initState(); import_storage.FlutterSecureStorage().read(key: 'jwt_token').then((val) { if (mounted) setState(() => _token = val); }); if (widget.product != null) { final p = widget.product!; _name = p.name; _sku = p.sku ?? ''; _hsnController.text = p.hsnCode ?? ''; _uomId = p.uomId; _color = p.color ?? ''; _gstController.text = p.gstRate != null && p.gstRate! > 0 ? p.gstRate.toString() : ''; _sellingPrice = p.sellingPrice ?? 0.0; if (p.imageIds.isNotEmpty) { _existingImageIds.addAll(p.imageIds); } WidgetsBinding.instance.addPostFrameCallback((_) { final cats = ref.read(productCategoriesProvider).value ?? []; if (cats.isNotEmpty) { setState(() { _selectedCategory = cats.firstWhere((c) => c.id == p.categoryId, orElse: () => cats.first); }); } }); } } @override void dispose() { _hsnController.dispose(); _gstController.dispose(); super.dispose(); } Future _pickImages() async { if ((_images.length + _existingImageIds.length) >= 4) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Maximum 4 images allowed'))); return; } final ImagePicker picker = ImagePicker(); final List picked = await picker.pickMultiImage(); if (picked.isNotEmpty) { setState(() { _images.addAll(picked.take(4 - (_images.length + _existingImageIds.length))); }); } } void _removeImage(int index) { setState(() { _images.removeAt(index); }); } void _previewImage(int index) { List allImages = []; for (final imageId in _existingImageIds) { if (_token != null) { allImages.add(NetworkImage('${DioClient().dio.options.baseUrl}/inventory/products/${widget.product!.id}/images/$imageId/content', headers: {'Authorization': 'Bearer $_token'})); } else { allImages.add(const AssetImage('assets/images/placeholder.png')); } } for (final file in _images) { allImages.add(FileImage(File(file.path))); } Navigator.push( context, MaterialPageRoute( builder: (context) => AttachmentGalleryScreen( images: allImages, initialIndex: index, ), ), ); } Future _save() async { if (!_formKey.currentState!.validate()) return; _formKey.currentState!.save(); if (_selectedCategory == null) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please select a category'))); return; } setState(() => _isSaving = true); try { final product = Product( id: widget.product?.id, name: _name, hsnCode: _hsnController.text.isNotEmpty ? _hsnController.text : null, sku: _sku.isNotEmpty ? _sku : null, categoryId: _selectedCategory?.id, uomId: _uomId, color: _color.isNotEmpty ? _color : null, sellingPrice: _sellingPrice, gstRate: double.tryParse(_gstController.text) ?? 0, priceCalcRule: 'MANUAL', autoCalculatePrice: false, ); if (widget.product != null) { await ref.read(productsProvider.notifier).updateProduct(widget.product!.id!, product, newImages: _images); } else { await ref.read(productsProvider.notifier).createProduct(product, images: _images); } if (mounted) Navigator.pop(context); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed: $e'))); } finally { if (mounted) setState(() => _isSaving = false); } } @override Widget build(BuildContext context) { final categoriesState = ref.watch(productCategoriesProvider); final taxInclusive = ref.watch(businessProfileProvider).value?.taxIncludedInPrice ?? false; return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar( title: Text(widget.product != null ? 'Edit Product' : 'New Product', style: const TextStyle(fontWeight: FontWeight.bold)), backgroundColor: Colors.transparent, elevation: 0, ), body: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: Column( children: [ Expanded( child: Form( key: _formKey, child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Basic Information', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 16), _buildPremiumTextField( label: 'Product Name*', initialValue: _name, validator: (val) => val == null || val.isEmpty ? 'Required' : null, onChanged: (val) => _name = val, ), const SizedBox(height: 16), _buildPremiumTextField( label: 'SKU / Barcode', initialValue: _sku, onChanged: (val) => _sku = val, ), const SizedBox(height: 24), const Text('Category & Unit', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 12), categoriesState.when( loading: () => const CircularProgressIndicator(), error: (err, stack) => Text('Error loading categories: $err'), data: (categories) { final leafCategories = categories.where((c) => !categories.any((child) => child.parentCategoryId == c.id)).toList(); return leafCategories.isEmpty ? const Text('No categories found. Manage categories in Business Hub.', style: TextStyle(color: Colors.orange)) : SmartSearchDropdown( hintText: 'Select Category*', value: _selectedCategory, items: leafCategories, itemAsString: (c) => c.name, itemBuilder: (context, item) { List path = []; ProductCategory? current = item; while (current != null) { path.insert(0, current.name); if (current.parentCategoryId != null) { current = categories.where((p) => p.id == current!.parentCategoryId).firstOrNull; } else { current = null; } } return Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item.name, style: const TextStyle(fontWeight: FontWeight.bold)), Text(path.join(' -> '), style: TextStyle(fontSize: 12, color: Colors.grey.shade600)), ], ), ); }, onChanged: (val) { setState(() { _selectedCategory = val; if (val != null) { if (val.defaultHsn != null && val.defaultHsn!.isNotEmpty) { _hsnController.text = val.defaultHsn!; } if (val.defaultGst != null && val.defaultGst! > 0) { _gstController.text = val.defaultGst.toString(); } } }); }, ); }, ), const SizedBox(height: 16), DropdownButtonFormField( decoration: const InputDecoration(labelText: 'Unit of Measure (Optional)'), value: _uomId, items: const [ DropdownMenuItem(value: 1, child: Text('Grams (g)')), DropdownMenuItem(value: 2, child: Text('Kilograms (kg)')), DropdownMenuItem(value: 3, child: Text('Pieces (pcs)')), ], onChanged: (val) => setState(() => _uomId = val), ), const SizedBox(height: 24), const Text('Pricing & Taxes', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 16), _buildPremiumTextField( label: taxInclusive ? 'Selling Price (Inc. Tax)*' : 'Selling Price (Exc. Tax)*', initialValue: _sellingPrice == 0 ? '' : _sellingPrice.toString(), prefixText: '₹ ', keyboardType: const TextInputType.numberWithOptions(decimal: true), onChanged: (val) => _sellingPrice = double.tryParse(val) ?? 0, ), const SizedBox(height: 16), Row( children: [ Expanded( child: TextFormField( controller: _hsnController, decoration: const InputDecoration(labelText: 'HSN Code'), ), ), const SizedBox(width: 16), Expanded( child: TextFormField( controller: _gstController, decoration: const InputDecoration(labelText: 'GST Rate (%)', suffixText: '%'), keyboardType: const TextInputType.numberWithOptions(decimal: true), ), ), ], ), const SizedBox(height: 24), const Text('Product Images', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)), const SizedBox(height: 16), if (_images.isNotEmpty || _existingImageIds.isNotEmpty) GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 16, mainAxisSpacing: 16, childAspectRatio: 1, ), itemCount: _images.length + _existingImageIds.length, itemBuilder: (context, index) { if (index < _existingImageIds.length) { final imageId = _existingImageIds[index]; final imageUrl = '${DioClient().dio.options.baseUrl}/inventory/products/${widget.product!.id}/images/$imageId/content'; return _buildImageThumbnail( isNetwork: true, url: imageUrl, onTap: () => _previewImage(index), onDelete: () async { try { setState(() => _isSaving = true); await DioClient().dio.delete('/inventory/products/images/$imageId'); setState(() => _existingImageIds.removeAt(index)); ref.invalidate(productsProvider); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to delete: $e'))); } finally { if (mounted) setState(() => _isSaving = false); } } ); } else { final localIndex = index - _existingImageIds.length; return _buildImageThumbnail( isNetwork: false, file: File(_images[localIndex].path), onTap: () => _previewImage(index), onDelete: () => _removeImage(localIndex), ); } }, ), if ((_images.length + _existingImageIds.length) < 4) ...[ const SizedBox(height: 16), GestureDetector( onTap: _pickImages, child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 24), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary.withOpacity(0.05), border: Border.all(color: Theme.of(context).colorScheme.primary.withOpacity(0.3)), borderRadius: BorderRadius.circular(16), ), child: Column( children: [ Icon(LucideIcons.uploadCloud, size: 32, color: Theme.of(context).colorScheme.primary), const SizedBox(height: 8), const Text('Tap to Upload Images', style: TextStyle(fontWeight: FontWeight.bold)), Text('${4 - (_images.length + _existingImageIds.length)} slots remaining', style: const TextStyle(color: Colors.grey, fontSize: 12)), ], ), ), ), ] ], ), ), ), ), _buildBottomBar(), ], ), ), ); } Widget _buildBottomBar() { return SafeArea( child: Padding( padding: const EdgeInsets.all(16.0), child: SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isSaving ? null : _save, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 4, ), child: _isSaving ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : Text(widget.product != null ? 'Update Product' : 'Publish Product', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), ), ), ), ); } Widget _buildImageThumbnail({required bool isNetwork, String? url, File? file, required VoidCallback onDelete, required VoidCallback onTap}) { return Stack( fit: StackFit.expand, children: [ GestureDetector( onTap: onTap, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey.withOpacity(0.2)), ), child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Container( color: Colors.grey[200], child: isNetwork ? (_token == null ? const Icon(LucideIcons.image, color: Colors.grey) : Image.network( url!, fit: BoxFit.cover, headers: {'Authorization': 'Bearer $_token'}, errorBuilder: (ctx, err, stack) => const Icon(LucideIcons.imageOff, color: Colors.grey), )) : Image.file(file!, fit: BoxFit.cover), ), ), ), ), Positioned( top: 8, right: 8, child: GestureDetector( onTap: onDelete, child: Container( decoration: BoxDecoration(color: Colors.black.withOpacity(0.6), shape: BoxShape.circle), padding: const EdgeInsets.all(6), child: const Icon(LucideIcons.x, color: Colors.white, size: 16), ), ), ) ], ); } Widget _buildPremiumTextField({ required String label, String? initialValue, String? prefixText, String? suffixText, TextInputType? keyboardType, void Function(String)? onChanged, void Function(String?)? onSaved, String? Function(String?)? validator, }) { return TextFormField( initialValue: initialValue, decoration: InputDecoration( labelText: label, prefixText: prefixText, suffixText: suffixText, ), keyboardType: keyboardType, onChanged: onChanged, onSaved: onSaved, validator: validator, ); } } """ with open('lib/features/inventory/presentation/add_product_screen.dart', 'w') as f: f.write(new_content)