import os file_path = "/Users/maddy/Projects/Kifi/kifi-app/lib/features/inventory/presentation/add_product_screen.dart" 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 '../../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(); // Basic String _name = ''; String _sku = ''; String _hsnCode = ''; ProductCategory? _selectedCategory; // Jewellery Properties double _weight = 0; // Gross Weight double _purityFactor = 1.0; double _wastagePercentage = 0; // Pricing double _makingCharges = 0; String _makingChargesType = 'FLAT'; // FLAT, PER_GRAM double _gstRate = 3.0; // Default 3% for jewellery // Inventory bool _trackInventory = true; // 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 ?? ''; _hsnCode = p.hsnCode ?? ''; _weight = p.weight ?? 0; _gstRate = p.gstRate ?? 3.0; _trackInventory = p.trackInventory; _purityFactor = p.purityFactor ?? 1.0; _makingCharges = p.makingCharges ?? 0.0; _makingChargesType = p.makingChargesType ?? 'FLAT'; _wastagePercentage = p.wastagePercentage ?? 0.0; if (p.imageIds.isNotEmpty) { _existingImageIds.addAll(p.imageIds); } WidgetsBinding.instance.addPostFrameCallback((_) { final cats = ref.read(productCategoriesProvider).value ?? []; if (cats.isNotEmpty && p.categoryId != null) { setState(() { _selectedCategory = cats.firstWhere((c) => c.id == p.categoryId, orElse: () => cats.first); }); } }); } } 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: _hsnCode.isNotEmpty ? _hsnCode : null, sku: _sku.isNotEmpty ? _sku : null, categoryId: _selectedCategory?.id, gstRate: _gstRate, weight: _weight, purityFactor: _purityFactor, makingCharges: _makingCharges, makingChargesType: _makingChargesType, wastagePercentage: _wastagePercentage, trackInventory: _trackInventory, ); 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); } } double get _netWeight { final weightAfterWastage = _weight * (1 - (_wastagePercentage / 100)); return weightAfterWastage; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar( title: Text(widget.product != null ? 'Edit Jewellery Product' : 'New Jewellery Product', style: const TextStyle(fontWeight: FontWeight.bold)), backgroundColor: Colors.transparent, elevation: 0, ), body: Form( key: _formKey, child: ListView( padding: const EdgeInsets.all(24), children: [ _buildSectionTitle('Basic Details'), _buildPremiumTextField( label: 'Product Name*', initialValue: _name, validator: (val) => val == null || val.isEmpty ? 'Required' : null, onChanged: (val) => _name = val, ), const SizedBox(height: 16), Row( children: [ Expanded( child: _buildPremiumTextField( label: 'SKU / HUID', initialValue: _sku, onChanged: (val) => _sku = val, ), ), const SizedBox(width: 16), Expanded( child: _buildCategoryDropdown(), ), ], ), const SizedBox(height: 32), _buildSectionTitle('Metal & Weight'), Row( children: [ Expanded( child: _buildPremiumTextField( label: 'Gross Weight (g)*', initialValue: _weight > 0 ? _weight.toString() : '', keyboardType: TextInputType.number, validator: (val) => val == null || double.tryParse(val) == null ? 'Invalid' : null, onChanged: (val) { _weight = double.tryParse(val) ?? 0; setState(() {}); }, ), ), const SizedBox(width: 16), Expanded( child: _buildPremiumTextField( label: 'Purity Factor (e.g. 0.916)*', initialValue: _purityFactor.toString(), keyboardType: TextInputType.number, validator: (val) => val == null || double.tryParse(val) == null ? 'Invalid' : null, onChanged: (val) => _purityFactor = double.tryParse(val) ?? 1.0, ), ), ], ), const SizedBox(height: 16), Row( children: [ Expanded( child: _buildPremiumTextField( label: 'Wastage (%)', initialValue: _wastagePercentage.toString(), keyboardType: TextInputType.number, onChanged: (val) { _wastagePercentage = double.tryParse(val) ?? 0.0; setState(() {}); }, ), ), const SizedBox(width: 16), Expanded( child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.05), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.blue.withOpacity(0.2)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Net Weight', style: TextStyle(fontSize: 12, color: Colors.blue.shade700)), const SizedBox(height: 4), Text('${_netWeight.toStringAsFixed(3)} g', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.blue)), ], ), ), ), ], ), const SizedBox(height: 32), _buildSectionTitle('Pricing Details'), Row( children: [ Expanded( child: _buildPremiumTextField( label: 'Making Charges*', initialValue: _makingCharges.toString(), keyboardType: TextInputType.number, validator: (val) => val == null || double.tryParse(val) == null ? 'Invalid' : null, onChanged: (val) => _makingCharges = double.tryParse(val) ?? 0, ), ), const SizedBox(width: 16), Expanded( child: DropdownButtonFormField( value: _makingChargesType, decoration: InputDecoration( labelText: 'Charge Type', filled: true, fillColor: Colors.grey.shade50, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300)), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300)), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2)), ), items: const [ DropdownMenuItem(value: 'FLAT', child: Text('Flat Amount')), DropdownMenuItem(value: 'PER_GRAM', child: Text('Per Gram')), ], onChanged: (val) => setState(() => _makingChargesType = val!), ), ), ], ), const SizedBox(height: 16), Row( children: [ Expanded( child: _buildPremiumTextField( label: 'HSN Code', initialValue: _hsnCode, onChanged: (val) => _hsnCode = val, ), ), const SizedBox(width: 16), Expanded( child: _buildPremiumTextField( label: 'GST Rate (%)', initialValue: _gstRate.toString(), keyboardType: TextInputType.number, onChanged: (val) => _gstRate = double.tryParse(val) ?? 3.0, ), ), ], ), const SizedBox(height: 32), _buildSectionTitle('Media'), _buildImagePicker(), const SizedBox(height: 32), SwitchListTile( title: const Text('Track Inventory', style: TextStyle(fontWeight: FontWeight.bold)), subtitle: const Text('Manage stock levels for this item'), value: _trackInventory, onChanged: (val) => setState(() => _trackInventory = val), activeColor: Theme.of(context).colorScheme.primary, contentPadding: EdgeInsets.zero, ), const SizedBox(height: 32), ElevatedButton( onPressed: _isSaving ? null : _save, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), ), 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)), ), const SizedBox(height: 32), ], ), ), ); } Widget _buildSectionTitle(String title) { return Padding( padding: const EdgeInsets.only(bottom: 16), child: Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87)), ); } Widget _buildPremiumTextField({ required String label, required String initialValue, required Function(String) onChanged, TextInputType? keyboardType, String? Function(String?)? validator, }) { return TextFormField( initialValue: initialValue, decoration: InputDecoration( labelText: label, labelStyle: TextStyle(color: Colors.grey.shade600), filled: true, fillColor: Colors.grey.shade50, border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300)), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300)), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2)), errorBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Colors.red)), ), keyboardType: keyboardType, validator: validator, onChanged: onChanged, ); } Widget _buildCategoryDropdown() { final categoriesState = ref.watch(productCategoriesProvider); return categoriesState.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, s) => Text('Error: $e'), data: (categories) { if (categories.isEmpty) return const Text('No categories available'); final items = categories.where((c) => !c.hasChild).map((c) => SmartSearchItem(id: c.id.toString(), label: c.name, subtitle: c.parentCategoryId != null ? 'Subcategory' : 'Root Category')).toList(); return SmartSearchDropdown( label: 'Category*', items: items, initialValue: _selectedCategory?.id.toString(), onChanged: (val) { if (val != null) setState(() => _selectedCategory = categories.firstWhere((c) => c.id.toString() == val)); }, validator: (val) => val == null || val.isEmpty ? 'Required' : null, ); }, ); } Widget _buildImagePicker() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ ...List.generate(_existingImageIds.length, (index) { final imageId = _existingImageIds[index]; return Padding( padding: const EdgeInsets.only(right: 12), child: Stack( children: [ GestureDetector( onTap: () => _previewImage(index), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Container( width: 100, height: 100, color: Colors.grey.shade200, child: _token == null ? const Icon(LucideIcons.image, color: Colors.grey) : Image.network('${DioClient().dio.options.baseUrl}/inventory/products/${widget.product!.id}/images/$imageId/content', fit: BoxFit.cover, headers: {'Authorization': 'Bearer $_token'}, errorBuilder: (ctx, err, trace) => const Icon(LucideIcons.imageOff, color: Colors.grey)), ), ), ), Positioned(top: 4, right: 4, child: GestureDetector(onTap: () => setState(() => _existingImageIds.removeAt(index)), child: Container(padding: const EdgeInsets.all(4), decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle), child: const Icon(LucideIcons.x, color: Colors.white, size: 16)))), ], ), ); }), ...List.generate(_images.length, (index) { return Padding( padding: const EdgeInsets.only(right: 12), child: Stack( children: [ GestureDetector(onTap: () => _previewImage(_existingImageIds.length + index), child: ClipRRect(borderRadius: BorderRadius.circular(12), child: Image.file(File(_images[index].path), width: 100, height: 100, fit: BoxFit.cover))), Positioned(top: 4, right: 4, child: GestureDetector(onTap: () => _removeImage(index), child: Container(padding: const EdgeInsets.all(4), decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle), child: const Icon(LucideIcons.x, color: Colors.white, size: 16)))), ], ), ); }), if ((_images.length + _existingImageIds.length) < 4) GestureDetector( onTap: _pickImages, child: Container( width: 100, height: 100, decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade300, style: BorderStyle.solid)), child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(LucideIcons.plus, color: Colors.grey.shade600), const SizedBox(height: 4), Text('Add Photo', style: TextStyle(color: Colors.grey.shade600, fontSize: 12))]), ), ), ], ), ), ], ); } } """ with open(file_path, "w") as f: f.write(content)