Files
Kifi/scratch/rewrite_add_product.py

482 lines
19 KiB
Python

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<AddProductScreen> createState() => _AddProductScreenState();
}
class _AddProductScreenState extends ConsumerState<AddProductScreen> {
final _formKey = GlobalKey<FormState>();
// 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<XFile> _images = [];
final List<int> _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<void> _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<XFile> 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<ImageProvider> 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<void> _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<String>(
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)