diff --git a/kifi-api/src/main/java/com/kifi/api/controller/inventory/CommodityRateController.java b/kifi-api/src/main/java/com/kifi/api/controller/inventory/CommodityRateController.java index 20f67c3..c48eade 100644 --- a/kifi-api/src/main/java/com/kifi/api/controller/inventory/CommodityRateController.java +++ b/kifi-api/src/main/java/com/kifi/api/controller/inventory/CommodityRateController.java @@ -9,20 +9,27 @@ import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.Map; + @RestController @RequestMapping("/api/kifi-v2/inventory/commodity-rates") @RequiredArgsConstructor public class CommodityRateController { private final CommodityRateService rateService; - @GetMapping("/{commodityName}") - public Flux getRateHistory(@PathVariable String commodityName) { - return rateService.getRateHistory(commodityName); + @GetMapping("/latest") + public Flux getAllLatestRates() { + return rateService.getAllLatestRates(); } - @GetMapping("/{commodityName}/latest") - public Mono> getLatestRate(@PathVariable String commodityName) { - return rateService.getLatestRate(commodityName) + @GetMapping("/{commodityCode}") + public Flux getRateHistory(@PathVariable String commodityCode) { + return rateService.getRateHistory(commodityCode); + } + + @GetMapping("/{commodityCode}/latest") + public Mono> getLatestRate(@PathVariable String commodityCode) { + return rateService.getLatestRate(commodityCode) .map(ResponseEntity::ok) .defaultIfEmpty(ResponseEntity.notFound().build()); } @@ -32,12 +39,28 @@ public class CommodityRateController { Authentication authentication, @RequestBody CommodityRateHistory rateHistory) { Long userId = Long.valueOf(authentication.getDetails().toString()); + String code = rateHistory.getCommodityCode() != null ? rateHistory.getCommodityCode() : rateHistory.getCommodityName(); + String name = rateHistory.getCommodityName() != null ? rateHistory.getCommodityName() : code; return rateService.addRate( - rateHistory.getCommodityName(), + code, + name, rateHistory.getPurity(), rateHistory.getRate(), rateHistory.getSource() != null ? rateHistory.getSource() : "MANUAL", userId ).map(ResponseEntity::ok); } + + @PostMapping("/{commodityCode}/sync") + public Mono>> syncRatesToProducts( + Authentication authentication, + @PathVariable String commodityCode) { + Long userId = Long.valueOf(authentication.getDetails().toString()); + return rateService.syncRatesToProducts(commodityCode, userId) + .map(count -> ResponseEntity.ok(Map.of( + "commodityCode", commodityCode, + "updatedCount", count, + "message", "Successfully synced rates for " + count + " products" + ))); + } } diff --git a/kifi-api/src/main/java/com/kifi/api/controller/inventory/ProductCategoryController.java b/kifi-api/src/main/java/com/kifi/api/controller/inventory/ProductCategoryController.java index b435021..8fed691 100644 --- a/kifi-api/src/main/java/com/kifi/api/controller/inventory/ProductCategoryController.java +++ b/kifi-api/src/main/java/com/kifi/api/controller/inventory/ProductCategoryController.java @@ -47,6 +47,7 @@ public class ProductCategoryController { existing.setParentCategoryId(category.getParentCategoryId()); existing.setHasChild(category.getHasChild()); existing.setCommodityCode(category.getCommodityCode()); + existing.setPurityFactor(category.getPurityFactor() != null ? category.getPurityFactor() : 1.0); existing.setDefaultHsn(category.getDefaultHsn()); existing.setDefaultGst(category.getDefaultGst()); existing.setHuidRequired(category.getHuidRequired()); diff --git a/kifi-api/src/main/java/com/kifi/api/entity/inventory/CommodityRateHistory.java b/kifi-api/src/main/java/com/kifi/api/entity/inventory/CommodityRateHistory.java index 9f0e1d4..e007c8c 100644 --- a/kifi-api/src/main/java/com/kifi/api/entity/inventory/CommodityRateHistory.java +++ b/kifi-api/src/main/java/com/kifi/api/entity/inventory/CommodityRateHistory.java @@ -18,6 +18,7 @@ import java.time.LocalDateTime; public class CommodityRateHistory { @Id private Long id; + private String commodityCode; private String commodityName; private String purity; private BigDecimal rate; diff --git a/kifi-api/src/main/java/com/kifi/api/entity/inventory/ProductCategory.java b/kifi-api/src/main/java/com/kifi/api/entity/inventory/ProductCategory.java index 73123d6..2667148 100644 --- a/kifi-api/src/main/java/com/kifi/api/entity/inventory/ProductCategory.java +++ b/kifi-api/src/main/java/com/kifi/api/entity/inventory/ProductCategory.java @@ -24,6 +24,8 @@ public class ProductCategory { @Builder.Default private Boolean hasChild = false; private String commodityCode; + @Builder.Default + private Double purityFactor = 1.0; private String defaultHsn; private java.math.BigDecimal defaultGst; @Builder.Default diff --git a/kifi-api/src/main/java/com/kifi/api/repository/TransactionItemRepository.java b/kifi-api/src/main/java/com/kifi/api/repository/TransactionItemRepository.java index dd58c6e..475240b 100644 --- a/kifi-api/src/main/java/com/kifi/api/repository/TransactionItemRepository.java +++ b/kifi-api/src/main/java/com/kifi/api/repository/TransactionItemRepository.java @@ -4,8 +4,10 @@ import com.kifi.api.entity.TransactionItem; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.stereotype.Repository; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; @Repository public interface TransactionItemRepository extends ReactiveCrudRepository { Flux findByTransactionId(Long transactionId); + Mono deleteByTransactionId(Long transactionId); } diff --git a/kifi-api/src/main/java/com/kifi/api/repository/inventory/CommodityRateHistoryRepository.java b/kifi-api/src/main/java/com/kifi/api/repository/inventory/CommodityRateHistoryRepository.java index f0fc4b6..cca7cd9 100644 --- a/kifi-api/src/main/java/com/kifi/api/repository/inventory/CommodityRateHistoryRepository.java +++ b/kifi-api/src/main/java/com/kifi/api/repository/inventory/CommodityRateHistoryRepository.java @@ -1,10 +1,15 @@ package com.kifi.api.repository.inventory; import com.kifi.api.entity.inventory.CommodityRateHistory; +import org.springframework.data.r2dbc.repository.Query; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public interface CommodityRateHistoryRepository extends ReactiveCrudRepository { + Flux findByCommodityCodeOrderByEffectiveAtDesc(String commodityCode); Flux findByCommodityNameOrderByEffectiveAtDesc(String commodityName); + + @Query("SELECT DISTINCT ON (commodity_code) * FROM commodity_rate_history WHERE commodity_code IS NOT NULL ORDER BY commodity_code, effective_at DESC") + Flux findLatestRatesForAllCommodities(); } diff --git a/kifi-api/src/main/java/com/kifi/api/repository/inventory/ProductCategoryRepository.java b/kifi-api/src/main/java/com/kifi/api/repository/inventory/ProductCategoryRepository.java index 3fc2bc4..e362d28 100644 --- a/kifi-api/src/main/java/com/kifi/api/repository/inventory/ProductCategoryRepository.java +++ b/kifi-api/src/main/java/com/kifi/api/repository/inventory/ProductCategoryRepository.java @@ -6,4 +6,5 @@ import reactor.core.publisher.Flux; public interface ProductCategoryRepository extends ReactiveCrudRepository { Flux findByUserId(Long userId); + Flux findByUserIdAndCommodityCode(Long userId, String commodityCode); } diff --git a/kifi-api/src/main/java/com/kifi/api/service/TransactionService.java b/kifi-api/src/main/java/com/kifi/api/service/TransactionService.java index 2139b9c..4a05d92 100644 --- a/kifi-api/src/main/java/com/kifi/api/service/TransactionService.java +++ b/kifi-api/src/main/java/com/kifi/api/service/TransactionService.java @@ -137,6 +137,7 @@ public class TransactionService { public Mono updateTransaction(Long id, Long userId, Transaction updatedTransaction) { return transactionRepository.findById(id) .filter(t -> t.getUserId().equals(userId)) + .switchIfEmpty(Mono.error(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.NOT_FOUND, "Transaction not found"))) .flatMap(t -> { // Revert old balances Mono revertBalances = updateWalletBalances(t.getToWalletId(), t.getFromWalletId(), t.getAmount()); @@ -152,9 +153,26 @@ public class TransactionService { t.setAmount(updatedTransaction.getAmount()); t.setDate(updatedTransaction.getDate()); t.setDescription(updatedTransaction.getDescription()); + t.setDueDate(updatedTransaction.getDueDate()); + t.setAlertSchedule(updatedTransaction.getAlertSchedule()); + t.setAlertTime(updatedTransaction.getAlertTime()); return revertBalances.then(applyNewBalances).then(transactionRepository.save(t)); - }).flatMap(this::populateItemsAndAttachments); + }) + .flatMap(savedTx -> { + Mono deleteOldItems = transactionItemRepository.deleteByTransactionId(savedTx.getId()); + Mono saveNewItems = Mono.empty(); + if (updatedTransaction.getItems() != null && !updatedTransaction.getItems().isEmpty()) { + for (TransactionItem item : updatedTransaction.getItems()) { + item.setId(null); + item.setTransactionId(savedTx.getId()); + item.setCreatedAt(LocalDateTime.now()); + } + saveNewItems = transactionItemRepository.saveAll(updatedTransaction.getItems()).then(); + } + return deleteOldItems.then(saveNewItems).thenReturn(savedTx); + }) + .flatMap(this::populateItemsAndAttachments); } @Transactional diff --git a/kifi-api/src/main/java/com/kifi/api/service/inventory/CommodityRateService.java b/kifi-api/src/main/java/com/kifi/api/service/inventory/CommodityRateService.java index af0afe1..e97063a 100644 --- a/kifi-api/src/main/java/com/kifi/api/service/inventory/CommodityRateService.java +++ b/kifi-api/src/main/java/com/kifi/api/service/inventory/CommodityRateService.java @@ -1,38 +1,82 @@ package com.kifi.api.service.inventory; import com.kifi.api.entity.inventory.CommodityRateHistory; +import com.kifi.api.entity.inventory.Product; +import com.kifi.api.entity.inventory.ProductCategory; import com.kifi.api.repository.inventory.CommodityRateHistoryRepository; +import com.kifi.api.repository.inventory.ProductCategoryRepository; +import com.kifi.api.repository.inventory.ProductRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.math.BigDecimal; +import java.math.RoundingMode; import java.time.LocalDateTime; @Service @RequiredArgsConstructor public class CommodityRateService { private final CommodityRateHistoryRepository rateHistoryRepository; + private final ProductCategoryRepository productCategoryRepository; + private final ProductRepository productRepository; - public Mono addRate(String commodityName, String purity, BigDecimal rate, String source, Long userId) { + public Mono addRate(String commodityCode, String commodityName, String purity, BigDecimal rate, String source, Long userId) { CommodityRateHistory history = CommodityRateHistory.builder() - .commodityName(commodityName) + .commodityCode(commodityCode) + .commodityName(commodityName != null && !commodityName.isEmpty() ? commodityName : commodityCode) .purity(purity) .rate(rate) .effectiveAt(LocalDateTime.now()) - .source(source) + .source(source != null ? source : "MANUAL") .createdBy(userId) .createdAt(LocalDateTime.now()) .build(); return rateHistoryRepository.save(history); } - public Flux getRateHistory(String commodityName) { - return rateHistoryRepository.findByCommodityNameOrderByEffectiveAtDesc(commodityName); + public Flux getRateHistory(String commodityCode) { + return rateHistoryRepository.findByCommodityCodeOrderByEffectiveAtDesc(commodityCode) + .switchIfEmpty(rateHistoryRepository.findByCommodityNameOrderByEffectiveAtDesc(commodityCode)); } - public Mono getLatestRate(String commodityName) { - return getRateHistory(commodityName).next(); + public Mono getLatestRate(String commodityCode) { + return getRateHistory(commodityCode).next(); + } + + public Flux getAllLatestRates() { + return rateHistoryRepository.findLatestRatesForAllCommodities(); + } + + public Mono syncRatesToProducts(String commodityCode, Long userId) { + return getLatestRate(commodityCode) + .flatMap(latestRate -> + productCategoryRepository.findByUserIdAndCommodityCode(userId, commodityCode) + .flatMap(category -> { + double categoryPurity = category.getPurityFactor() != null ? category.getPurityFactor() : 1.0; + return productRepository.findByCategoryId(category.getId()) + .flatMap(product -> { + double productPurity = product.getPurityFactor() != null ? product.getPurityFactor() : categoryPurity; + BigDecimal baseRate = latestRate.getRate().multiply(BigDecimal.valueOf(productPurity)); + + if ("WEIGHT_BASED".equalsIgnoreCase(product.getPriceCalcRule())) { + BigDecimal making = product.getMakingCharges() != null ? BigDecimal.valueOf(product.getMakingCharges()) : BigDecimal.ZERO; + BigDecimal newPrice = baseRate; + if ("PERCENTAGE".equalsIgnoreCase(product.getMakingChargesType())) { + newPrice = baseRate.add(baseRate.multiply(making).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)); + } else { + newPrice = baseRate.add(making); + } + product.setSellingPrice(newPrice.setScale(2, RoundingMode.HALF_UP)); + } + product.setUpdatedAt(LocalDateTime.now()); + return productRepository.save(product); + }); + }) + .collectList() + .map(list -> list.size()) + ) + .defaultIfEmpty(0); } } diff --git a/kifi-api/src/main/resources/schema.sql b/kifi-api/src/main/resources/schema.sql index a8d8144..0a1b619 100644 --- a/kifi-api/src/main/resources/schema.sql +++ b/kifi-api/src/main/resources/schema.sql @@ -201,7 +201,8 @@ CREATE TABLE IF NOT EXISTS product_categories ( default_gst DECIMAL(5, 2), huid_required BOOLEAN DEFAULT FALSE, default_making_charge DECIMAL(15, 2), - making_charge_type VARCHAR(50), + commodity_code VARCHAR(10), + purity_factor DECIMAL(5, 4) DEFAULT 1.0, base_unit VARCHAR(20) DEFAULT 'pcs', is_active BOOLEAN DEFAULT TRUE, sort_order INTEGER DEFAULT 0, @@ -210,7 +211,8 @@ CREATE TABLE IF NOT EXISTS product_categories ( CREATE TABLE IF NOT EXISTS commodity_rate_history ( id SERIAL PRIMARY KEY, - commodity_name VARCHAR(100) NOT NULL, + commodity_code VARCHAR(100) NOT NULL, + commodity_name VARCHAR(100), purity VARCHAR(50), rate DECIMAL(15, 2) NOT NULL, effective_at TIMESTAMP NOT NULL, diff --git a/kifi-app/lib/core/network/dio_client.dart b/kifi-app/lib/core/network/dio_client.dart index be93690..c539d91 100644 --- a/kifi-app/lib/core/network/dio_client.dart +++ b/kifi-app/lib/core/network/dio_client.dart @@ -26,8 +26,9 @@ class DioClient { DioClient._internal() : dio = Dio(BaseOptions( - baseUrl: 'https://app.technobeesolutions.in/api/kifi-v2', + //baseUrl: 'https://app.technobeesolutions.in/api/kifi-v2', //baseUrl: 'http://192.168.0.104:8080/api/kifi-v2', // Local Mac IP for physical device testing + baseUrl: 'http://192.168.1.5:8080/api/kifi-v2', // Current Local Mac IP (192.168.1.5) connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), )), diff --git a/kifi-app/lib/features/inventory/domain/commodity_rate.dart b/kifi-app/lib/features/inventory/domain/commodity_rate.dart new file mode 100644 index 0000000..baa78c8 --- /dev/null +++ b/kifi-app/lib/features/inventory/domain/commodity_rate.dart @@ -0,0 +1,54 @@ +class CommodityRateHistory { + final int? id; + final String commodityCode; + final String? commodityName; + final String? purity; + final double rate; + final DateTime effectiveAt; + final String? source; + final int? createdBy; + final DateTime? createdAt; + + CommodityRateHistory({ + this.id, + required this.commodityCode, + this.commodityName, + this.purity, + required this.rate, + required this.effectiveAt, + this.source, + this.createdBy, + this.createdAt, + }); + + factory CommodityRateHistory.fromJson(Map json) { + return CommodityRateHistory( + id: json['id'] is int ? json['id'] : int.tryParse(json['id']?.toString() ?? ''), + commodityCode: (json['commodityCode'] ?? json['commodity_code'] ?? json['commodityName'] ?? json['commodity_name'] ?? '').toString(), + commodityName: json['commodityName'] ?? json['commodity_name'], + purity: json['purity'], + rate: (json['rate'] as num?)?.toDouble() ?? 0.0, + effectiveAt: json['effectiveAt'] != null + ? DateTime.parse(json['effectiveAt'].toString()) + : (json['effective_at'] != null ? DateTime.parse(json['effective_at'].toString()) : DateTime.now()), + source: json['source'], + createdBy: json['createdBy'] ?? json['created_by'], + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt'].toString()) + : (json['created_at'] != null ? DateTime.parse(json['created_at'].toString()) : null), + ); + } + + Map toJson() { + return { + if (id != null) 'id': id, + 'commodityCode': commodityCode, + 'commodityName': commodityName ?? commodityCode, + if (purity != null) 'purity': purity, + 'rate': rate, + 'effectiveAt': effectiveAt.toIso8601String(), + if (source != null) 'source': source, + if (createdBy != null) 'createdBy': createdBy, + }; + } +} diff --git a/kifi-app/lib/features/inventory/presentation/category_management_screen.dart b/kifi-app/lib/features/inventory/presentation/category_management_screen.dart index 4045bc5..83f010a 100644 --- a/kifi-app/lib/features/inventory/presentation/category_management_screen.dart +++ b/kifi-app/lib/features/inventory/presentation/category_management_screen.dart @@ -243,9 +243,10 @@ class _CategoryFormSheetState extends ConsumerState { final _hsnController = TextEditingController(); final _gstController = TextEditingController(); final _makingChargeController = TextEditingController(); + final _purityFactorController = TextEditingController(text: '1.0'); int? _selectedParentId; - bool _isLeaf = false; + bool _isLeaf = true; bool _huidRequired = false; String _commodityCode = 'XAU'; String _makingChargeType = 'PER_GRAM'; @@ -271,6 +272,7 @@ class _CategoryFormSheetState extends ConsumerState { _hsnController.text = c.defaultHsn ?? ''; _gstController.text = c.defaultGst?.toString() ?? ''; _makingChargeController.text = c.defaultMakingCharge?.toString() ?? ''; + _purityFactorController.text = (c.purityFactor ?? 1.0).toString(); _huidRequired = c.huidRequired; _commodityCode = c.commodityCode ?? 'XAU'; @@ -293,6 +295,7 @@ class _CategoryFormSheetState extends ConsumerState { _hsnController.dispose(); _gstController.dispose(); _makingChargeController.dispose(); + _purityFactorController.dispose(); super.dispose(); } @@ -304,6 +307,7 @@ class _CategoryFormSheetState extends ConsumerState { parentCategoryId: _selectedParentId, hasChild: !_isLeaf, commodityCode: _isLeaf ? _commodityCode : null, + purityFactor: _isLeaf ? (double.tryParse(_purityFactorController.text) ?? 1.0) : 1.0, defaultHsn: _isLeaf ? _hsnController.text.trim() : null, defaultGst: _isLeaf ? double.tryParse(_gstController.text) : null, huidRequired: _isLeaf ? _huidRequired : false, @@ -457,17 +461,34 @@ class _CategoryFormSheetState extends ConsumerState { const SizedBox(height: 16), if (isBusinessMode) ...[ - DropdownButtonFormField( - value: _commodityCode, - decoration: InputDecoration( - labelText: 'Commodity Code', - prefixIcon: const Icon(LucideIcons.barChart2), - ), - items: _commodities.map((c) => DropdownMenuItem( - value: c['code'], - child: Text(c['label']!), - )).toList(), - onChanged: (val) => setState(() => _commodityCode = val!), + Row( + children: [ + Expanded( + flex: 3, + child: DropdownButtonFormField( + value: _commodityCode, + decoration: const InputDecoration( + labelText: 'Commodity Code', + prefixIcon: Icon(LucideIcons.barChart2), + ), + items: _commodities.map((c) => DropdownMenuItem( + value: c['code'], + child: Text(c['label']!), + )).toList(), + onChanged: (val) => setState(() => _commodityCode = val!), + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 2, + child: _buildTextField( + controller: _purityFactorController, + label: 'Purity (e.g. 0.916)', + icon: LucideIcons.gem, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + ), + ), + ], ), const SizedBox(height: 16), ], diff --git a/kifi-app/lib/features/inventory/presentation/daily_rates_screen.dart b/kifi-app/lib/features/inventory/presentation/daily_rates_screen.dart index 880f9a3..9942796 100644 --- a/kifi-app/lib/features/inventory/presentation/daily_rates_screen.dart +++ b/kifi-app/lib/features/inventory/presentation/daily_rates_screen.dart @@ -3,7 +3,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:intl/intl.dart'; import '../providers/product_categories_provider.dart'; -import '../domain/category_rate_history.dart'; +import '../providers/commodity_rates_provider.dart'; +import '../domain/commodity_rate.dart'; +import '../../../core/theme/app_theme.dart'; +import '../../../core/utils/snackbar_service.dart'; class DailyRatesScreen extends ConsumerStatefulWidget { const DailyRatesScreen({super.key}); @@ -13,10 +16,17 @@ class DailyRatesScreen extends ConsumerStatefulWidget { } class _DailyRatesScreenState extends ConsumerState { - final Map _rateControllers = {}; - final Map _isExpanded = {}; - final Map> _historyCache = {}; - final Map _isLoadingHistory = {}; + final Map _rateControllers = {}; + final Map _isSaving = {}; + final Map _isSyncing = {}; + + final Map _commodityLabels = { + 'XAU': 'Gold (XAU)', + 'XAG': 'Silver (XAG)', + 'XPT': 'Platinum (XPT)', + 'XPD': 'Palladium (XPD)', + 'OTH': 'Other Commodity', + }; @override void dispose() { @@ -26,302 +36,775 @@ class _DailyRatesScreenState extends ConsumerState { super.dispose(); } - Future _fetchHistory(int categoryId) async { - setState(() => _isLoadingHistory[categoryId] = true); - final historyData = await ref.read(productCategoriesProvider.notifier).fetchRateHistory(categoryId); - setState(() { - _historyCache[categoryId] = historyData.map((e) => CategoryRateHistory.fromJson(e)).toList(); - _isLoadingHistory[categoryId] = false; - }); - } - - void _toggleExpand(int categoryId) { - setState(() { - _isExpanded[categoryId] = !(_isExpanded[categoryId] ?? false); - }); - if (_isExpanded[categoryId] == true && _historyCache[categoryId] == null) { - _fetchHistory(categoryId); + Color _getCommodityColor(String code, bool isDark) { + switch (code.toUpperCase()) { + case 'XAU': + return const Color(0xFFD4AF37); // Classic Gold + case 'XAG': + return const Color(0xFF8A9BA8); // Silver metallic + case 'XPT': + return const Color(0xFF00A896); // Platinum teal + case 'XPD': + return const Color(0xFF7209B7); // Palladium purple + default: + return const Color(0xFF3B82F6); // Modern Blue } } - Future _saveRate(int categoryId) async { - final text = _rateControllers[categoryId]?.text; - if (text == null || text.isEmpty) return; - + IconData _getCommodityIcon(String code) { + switch (code.toUpperCase()) { + case 'XAU': + return LucideIcons.gem; + case 'XAG': + return LucideIcons.sparkles; + case 'XPT': + return LucideIcons.shieldCheck; + case 'XPD': + return LucideIcons.cpu; + default: + return LucideIcons.boxes; + } + } + + Future _saveRate(String commodityCode, String baseUnit) async { + final text = _rateControllers[commodityCode]?.text.trim(); + if (text == null || text.isEmpty) { + SnackBarService.showError(context, 'Please enter a valid rate'); + return; + } + final rate = double.tryParse(text); - if (rate == null) return; + if (rate == null || rate <= 0) { + SnackBarService.showError(context, 'Please enter a positive numeric rate'); + return; + } + setState(() => _isSaving[commodityCode] = true); try { - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => const Center(child: CircularProgressIndicator()), + final name = _commodityLabels[commodityCode] ?? commodityCode; + await ref.read(commodityRatesProvider.notifier).addRate( + commodityCode, + rate, + commodityName: name, + source: 'MANUAL', ); - - await ref.read(productCategoriesProvider.notifier).updateCategoryRate(categoryId, rate); - if (mounted) { - Navigator.pop(context); // close loading - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Rate updated successfully!'), backgroundColor: Colors.green), - ); - _fetchHistory(categoryId); // refresh history + SnackBarService.showSuccess(context, '$name rate updated to ₹${NumberFormat('#,##,##0.00').format(rate)} / $baseUnit'); } } catch (e) { if (mounted) { - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error updating rate: $e'), backgroundColor: Colors.red), - ); + SnackBarService.showError(context, 'Failed to save rate: $e'); + } + } finally { + if (mounted) { + setState(() => _isSaving[commodityCode] = false); } } } - Future _syncRates(int categoryId) async { + Future _syncRates(String commodityCode) async { + setState(() => _isSyncing[commodityCode] = true); try { - showDialog( - context: context, - barrierDismissible: false, - builder: (_) => const Center(child: CircularProgressIndicator()), - ); - - final count = await ref.read(productCategoriesProvider.notifier).syncRates(categoryId); - + final count = await ref.read(commodityRatesProvider.notifier).syncRateToProducts(commodityCode); if (mounted) { - Navigator.pop(context); // close loading - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Successfully synced rates for $count products!'), backgroundColor: Colors.green), + SnackBarService.showSuccess( + context, + count > 0 + ? 'Successfully synced $commodityCode rates to $count products!' + : 'Rates synced. No products currently require rate updates.', ); } } catch (e) { if (mounted) { - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error syncing rates: $e'), backgroundColor: Colors.red), - ); + SnackBarService.showError(context, 'Failed to sync rates: $e'); + } + } finally { + if (mounted) { + setState(() => _isSyncing[commodityCode] = false); } } } + void _showHistorySheet(BuildContext context, String commodityCode, String baseUnit) { + final color = _getCommodityColor(commodityCode, Theme.of(context).brightness == Brightness.dark); + final label = _commodityLabels[commodityCode] ?? commodityCode; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (ctx) { + return DraggableScrollableSheet( + initialChildSize: 0.65, + minChildSize: 0.4, + maxChildSize: 0.9, + builder: (_, scrollController) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(28)), + boxShadow: [ + BoxShadow(color: Colors.black.withValues(alpha: 0.2), blurRadius: 25, offset: const Offset(0, -5)), + ], + ), + child: Column( + children: [ + // Handle + Center( + child: Container( + margin: const EdgeInsets.only(top: 12, bottom: 8), + width: 44, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(2), + ), + ), + ), + // Title Header + Padding( + padding: const EdgeInsets.fromLTRB(24, 8, 16, 16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.15), + shape: BoxShape.circle, + ), + child: Icon(_getCommodityIcon(commodityCode), color: color, size: 22), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '$label History', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 2), + Text( + 'Historical rate log per $baseUnit', + style: TextStyle(fontSize: 12, color: Colors.grey.shade600), + ), + ], + ), + ), + IconButton( + icon: const Icon(LucideIcons.x, size: 20), + onPressed: () => Navigator.pop(ctx), + ), + ], + ), + ), + const Divider(height: 1), + // History List + Expanded( + child: FutureBuilder>( + future: ref.read(commodityRatesProvider.notifier).fetchRateHistory(commodityCode), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (snapshot.hasError) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text('Error loading history: ${snapshot.error}', style: const TextStyle(color: Colors.red)), + ), + ); + } + final list = snapshot.data ?? []; + if (list.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(LucideIcons.history, size: 48, color: Colors.grey.shade400), + const SizedBox(height: 12), + Text('No rate history recorded yet', style: TextStyle(color: Colors.grey.shade600, fontWeight: FontWeight.w500)), + ], + ), + ); + } + + return ListView.separated( + controller: scrollController, + padding: const EdgeInsets.all(20), + itemCount: list.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, idx) { + final item = list[idx]; + final isLatest = idx == 0; + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isLatest ? color.withValues(alpha: 0.08) : Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isLatest ? color.withValues(alpha: 0.4) : Colors.grey.withValues(alpha: 0.15), + width: isLatest ? 1.5 : 1, + ), + ), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: isLatest ? color : Colors.grey.shade400, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + DateFormat('dd MMM yyyy, hh:mm a').format(item.effectiveAt), + style: TextStyle( + fontSize: 13, + fontWeight: isLatest ? FontWeight.bold : FontWeight.w500, + color: isLatest ? null : Colors.grey.shade700, + ), + ), + if (isLatest) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(6), + ), + child: const Text('LATEST', style: TextStyle(color: Colors.white, fontSize: 9, fontWeight: FontWeight.bold)), + ), + ], + ], + ), + if (item.source != null && item.source!.isNotEmpty) ...[ + const SizedBox(height: 2), + Text('Source: ${item.source}', style: TextStyle(fontSize: 11, color: Colors.grey.shade500)), + ], + ], + ), + ), + Text( + '₹${NumberFormat('#,##,##0.00').format(item.rate)}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isLatest ? color : null, + ), + ), + ], + ), + ); + }, + ); + }, + ), + ), + ], + ), + ); + }, + ); + }, + ); + } + @override Widget build(BuildContext context) { final categoriesState = ref.watch(productCategoriesProvider); + final commodityRatesState = ref.watch(commodityRatesProvider); + final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( - backgroundColor: Colors.grey[100], + backgroundColor: isDark ? const Color(0xFF0F172A) : const Color(0xFFF8FAFC), appBar: AppBar( - title: const Text('Daily Commodity Rates', style: TextStyle(fontWeight: FontWeight.bold)), - backgroundColor: Colors.white, - foregroundColor: Colors.black, + title: const Text( + 'Daily Commodity Rates', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 19), + ), elevation: 0, + backgroundColor: isDark ? const Color(0xFF1E293B) : Colors.white, + foregroundColor: isDark ? Colors.white : const Color(0xFF0F172A), centerTitle: true, + actions: [ + IconButton( + icon: const Icon(LucideIcons.refreshCw, size: 20), + tooltip: 'Refresh Rates', + onPressed: () { + ref.invalidate(productCategoriesProvider); + ref.invalidate(commodityRatesProvider); + }, + ), + ], ), body: categoriesState.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (err, _) => Center(child: Text('Error loading categories: $err')), data: (categories) { - final commodities = categories.where((c) => c.commodityCode != null && c.commodityCode!.isNotEmpty).toList(); - - if (commodities.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(LucideIcons.boxes, size: 64, color: Colors.grey.shade300), - const SizedBox(height: 16), - Text('No Commodity Categories', style: TextStyle(fontSize: 18, color: Colors.grey.shade600)), - const SizedBox(height: 8), - Text('Mark a category as a commodity to manage its daily rates.', style: TextStyle(color: Colors.grey.shade500)), - ], - ), - ); + // Leaf categories that have a commodity code assigned + final leafCommodities = categories.where((c) => + !c.hasChild && c.commodityCode != null && c.commodityCode!.trim().isNotEmpty + ).toList(); + + if (leafCommodities.isEmpty) { + return _buildEmptyState(isDark); } + // Group leaf categories by commodity code + final Map> grouped = {}; + for (var cat in leafCommodities) { + final code = cat.commodityCode!.trim().toUpperCase(); + grouped.putIfAbsent(code, () => []).add(cat); + } + + final commodityCodes = grouped.keys.toList()..sort(); + final latestRates = commodityRatesState.value ?? []; + return RefreshIndicator( onRefresh: () async { ref.invalidate(productCategoriesProvider); - await ref.read(productCategoriesProvider.future); + ref.invalidate(commodityRatesProvider); + await Future.wait([ + ref.read(productCategoriesProvider.future), + ref.read(commodityRatesProvider.future), + ]); }, - child: ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: commodities.length, - itemBuilder: (context, index) { - final category = commodities[index]; - if (!_rateControllers.containsKey(category.id)) { - _rateControllers[category.id!] = TextEditingController(text: category.dailyRate?.toString() ?? ''); - } - final controller = _rateControllers[category.id]!; - final isExpanded = _isExpanded[category.id] ?? false; - final history = _historyCache[category.id]; - final isLoadingHistory = _isLoadingHistory[category.id] ?? false; - - DateTime? lastSyncDate; - if (history != null && history.isNotEmpty) { - lastSyncDate = history.first.updatedAt; - } + child: ListView( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), + children: [ + // Info Banner + _buildInfoBanner(isDark, commodityCodes.length, leafCommodities.length), + const SizedBox(height: 16), - return Container( - margin: const EdgeInsets.only(bottom: 16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow(color: Colors.grey.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 4)), - ], - border: Border.all(color: Colors.grey.shade200), + // Commodity Cards + ...commodityCodes.map((code) { + final linkedCats = grouped[code]!; + final baseUnit = linkedCats.first.baseUnit; + + // Find latest rate for this code + final rateEntry = latestRates.where((r) => + r.commodityCode.toUpperCase() == code || (r.commodityName != null && r.commodityName!.toUpperCase() == code) + ).firstOrNull; + + final currentRate = rateEntry?.rate ?? 0.0; + + // Initialize controller if not present + if (!_rateControllers.containsKey(code) || _rateControllers[code]!.text.isEmpty) { + _rateControllers[code] = TextEditingController( + text: currentRate > 0 ? currentRate.toStringAsFixed(2) : '', + ); + } + + return _buildCommodityCard( + context: context, + commodityCode: code, + linkedCategories: linkedCats, + currentRate: currentRate, + rateEntry: rateEntry, + baseUnit: baseUnit, + isDark: isDark, + ); + }), + ], + ), + ); + }, + ), + ); + } + + Widget _buildInfoBanner(bool isDark, int commodityCount, int categoryCount) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1E293B) : Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: isDark ? Colors.white10 : Colors.black.withValues(alpha: 0.06)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.03), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon(LucideIcons.trendingUp, color: AppTheme.primaryColor, size: 24), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Pricing Flow & Valuation', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), ), - child: Column( - children: [ - // Main Card Header - Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + const SizedBox(height: 2), + Text( + '$commodityCount active commodities governing $categoryCount leaf product categories.', + style: TextStyle(fontSize: 12, color: Colors.grey.shade600), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildCommodityCard({ + required BuildContext context, + required String commodityCode, + required List linkedCategories, + required double currentRate, + required CommodityRateHistory? rateEntry, + required String baseUnit, + required bool isDark, + }) { + final color = _getCommodityColor(commodityCode, isDark); + final label = _commodityLabels[commodityCode] ?? commodityCode; + final controller = _rateControllers[commodityCode]!; + final isSaving = _isSaving[commodityCode] ?? false; + final isSyncing = _isSyncing[commodityCode] ?? false; + + return Container( + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1E293B) : Colors.white, + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: color.withValues(alpha: isDark ? 0.35 : 0.25), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: color.withValues(alpha: isDark ? 0.15 : 0.08), + blurRadius: 18, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header banner + Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 14), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: color.withValues(alpha: 0.3)), + ), + child: Icon(_getCommodityIcon(commodityCode), color: color, size: 24), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Colors.blue.shade50, - borderRadius: BorderRadius.circular(12), - ), - child: Icon(LucideIcons.trendingUp, color: Colors.blue.shade700, size: 24), - ), - const SizedBox(width: 16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - category.name, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 4), - if (lastSyncDate != null) - Text( - 'Last updated: ${DateFormat('MMM dd, yyyy HH:mm').format(lastSyncDate)}', - style: TextStyle(fontSize: 12, color: Colors.grey.shade500), - ) - else - Text( - 'Base Unit: ${category.baseUnit}', - style: TextStyle(fontSize: 12, color: Colors.grey.shade500), - ), - ], - ), - ], - ), - IconButton( - icon: Icon(isExpanded ? LucideIcons.chevronUp : LucideIcons.history, color: Colors.grey.shade600), - onPressed: () => _toggleExpand(category.id!), - tooltip: 'View History', - ), - ], + Text( + label, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + letterSpacing: -0.3, + ), ), - const SizedBox(height: 24), - Row( - children: [ - Expanded( - flex: 3, - child: TextFormField( - controller: controller, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - decoration: InputDecoration( - labelText: 'Today\'s Rate (per ${category.baseUnit})', - prefixText: '₹ ', - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - ), - ), - ), - const SizedBox(width: 12), - Expanded( - flex: 2, - child: ElevatedButton.icon( - onPressed: () => _saveRate(category.id!), - icon: const Icon(LucideIcons.save, size: 18), - label: const Text('Save'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue.shade700, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), - ), - ), - ], - ), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: () => _syncRates(category.id!), - icon: Icon(LucideIcons.refreshCw, size: 18, color: Colors.blue.shade700), - label: const Text('Sync Rate to All Products', style: TextStyle(fontWeight: FontWeight.bold)), - style: OutlinedButton.styleFrom( - foregroundColor: Colors.blue.shade700, - side: BorderSide(color: Colors.blue.shade700), - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + commodityCode, + style: TextStyle( + color: color, + fontSize: 11, + fontWeight: FontWeight.bold, ), ), ), ], ), - ), - - // Expandable History Section - if (isExpanded) - Container( - decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: const BorderRadius.vertical(bottom: Radius.circular(16)), - border: Border(top: BorderSide(color: Colors.grey.shade200)), - ), - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Rate History', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)), - const SizedBox(height: 12), - if (isLoadingHistory) - const Center(child: Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator())) - else if (history == null || history.isEmpty) - const Padding( - padding: EdgeInsets.all(16.0), - child: Text('No history found.'), - ) - else - ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: history.length > 5 ? 5 : history.length, // Show up to 5 recent - separatorBuilder: (_, __) => Divider(color: Colors.grey.shade300), - itemBuilder: (context, idx) { - final item = history[idx]; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(DateFormat('MMM dd, yyyy').format(item.date), style: const TextStyle(fontWeight: FontWeight.w500)), - Text('₹ ${item.rate.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold)), - ], - ), - ); - }, - ), - ], - ), + const SizedBox(height: 3), + Text( + rateEntry != null + ? 'Updated ${DateFormat('dd MMM, hh:mm a').format(rateEntry.effectiveAt)}' + : 'No rate recorded yet', + style: TextStyle(fontSize: 12, color: Colors.grey.shade500), ), + ], + ), + ), + // History Button + IconButton.filledTonal( + onPressed: () => _showHistorySheet(context, commodityCode, baseUnit), + style: IconButton.styleFrom( + backgroundColor: isDark ? Colors.white10 : Colors.grey.shade100, + foregroundColor: isDark ? Colors.white : Colors.black87, + ), + icon: const Icon(LucideIcons.history, size: 18), + tooltip: 'View History', + ), + ], + ), + ), + + // Rate Display & Editor Box + Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF0F172A) : const Color(0xFFF1F5F9), + borderRadius: BorderRadius.circular(18), + border: Border.all(color: isDark ? Colors.white12 : Colors.black.withValues(alpha: 0.05)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'CURRENT RATE', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + color: Colors.grey.shade500, + ), + ), + Text( + currentRate > 0 + ? '₹ ${NumberFormat('#,##,##0.00').format(currentRate)} / $baseUnit' + : 'Not Set', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: currentRate > 0 ? color : Colors.grey.shade400, + ), + ), ], ), - ); - }, - )); - }, - loading: () => const Center(child: CircularProgressIndicator()), - error: (err, stack) => Center(child: Text('Error: $err')), + const SizedBox(height: 12), + // Inline Updater Field & Save Button + Row( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1E293B) : Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: isDark ? Colors.white24 : Colors.grey.shade300), + ), + child: TextField( + controller: controller, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + decoration: InputDecoration( + hintText: '0.00', + prefixIcon: const Padding( + padding: EdgeInsets.symmetric(horizontal: 12), + child: Text('₹', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + ), + prefixIconConstraints: const BoxConstraints(minWidth: 0, minHeight: 0), + suffixText: '/ $baseUnit', + suffixStyle: TextStyle(color: Colors.grey.shade500, fontSize: 12), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), + border: InputBorder.none, + ), + ), + ), + ), + const SizedBox(width: 10), + ElevatedButton( + onPressed: isSaving ? null : () => _saveRate(commodityCode, baseUnit), + style: ElevatedButton.styleFrom( + backgroundColor: color, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + elevation: 0, + ), + child: isSaving + ? const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) + : const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(LucideIcons.check, size: 16), + SizedBox(width: 6), + Text('Save', style: TextStyle(fontWeight: FontWeight.bold)), + ], + ), + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 14), + + // Linked Leaf Categories Section + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'LINKED LEAF CATEGORIES', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + color: Colors.grey.shade500, + ), + ), + Text( + '${linkedCategories.length} linked', + style: TextStyle(fontSize: 11, color: Colors.grey.shade500), + ), + ], + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: linkedCategories.map((cat) { + final purity = cat.purityFactor ?? 1.0; + final effectiveUnitRate = currentRate * purity; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: isDark ? const Color(0xFF0F172A) : Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: isDark ? Colors.white10 : Colors.grey.shade300), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(LucideIcons.tag, size: 12, color: color), + const SizedBox(width: 6), + Text( + cat.name, + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600), + ), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + '${(purity * 100).toStringAsFixed(1)}% (₹${effectiveUnitRate.toStringAsFixed(0)}/g)', + style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: color), + ), + ), + ], + ), + ); + }).toList(), + ), + ], + ), + ), + + const SizedBox(height: 16), + + // Sync to Products Action Bar + Container( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 14), + decoration: BoxDecoration( + color: isDark ? Colors.black.withValues(alpha: 0.2) : Colors.grey.shade50, + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(24)), + ), + child: Row( + children: [ + Expanded( + child: Text( + 'Sync live rates to all products under this commodity:', + style: TextStyle(fontSize: 12, color: Colors.grey.shade600), + ), + ), + OutlinedButton.icon( + onPressed: isSyncing ? null : () => _syncRates(commodityCode), + style: OutlinedButton.styleFrom( + foregroundColor: color, + side: BorderSide(color: color.withValues(alpha: 0.5)), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + ), + icon: isSyncing + ? SizedBox(width: 14, height: 14, child: CircularProgressIndicator(strokeWidth: 2, color: color)) + : Icon(LucideIcons.refreshCw, size: 14, color: color), + label: const Text('Sync Products', style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildEmptyState(bool isDark) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: const Color(0xFFD4AF37).withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: const Icon(LucideIcons.coins, size: 56, color: Color(0xFFD4AF37)), + ), + const SizedBox(height: 20), + const Text( + 'No Commodity Categories Configured', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + Text( + 'Assign a commodity code (e.g. Gold XAU, Silver XAG) and purity factor to your leaf categories in Category Management to track their live daily rates.', + style: TextStyle(fontSize: 13, color: Colors.grey.shade600), + textAlign: TextAlign.center, + ), + ], + ), ), ); } diff --git a/kifi-app/lib/features/inventory/presentation/stock_ledger_tab.dart b/kifi-app/lib/features/inventory/presentation/stock_ledger_tab.dart index c093943..8c2585f 100644 --- a/kifi-app/lib/features/inventory/presentation/stock_ledger_tab.dart +++ b/kifi-app/lib/features/inventory/presentation/stock_ledger_tab.dart @@ -4,6 +4,7 @@ import 'package:kifi_app/features/inventory/domain/inventory_item.dart'; import 'package:kifi_app/features/inventory/domain/product.dart'; import 'package:kifi_app/features/inventory/providers/products_provider.dart'; import 'package:kifi_app/features/inventory/providers/product_categories_provider.dart'; +import 'package:kifi_app/features/inventory/providers/commodity_rates_provider.dart'; import 'package:kifi_app/core/network/dio_client.dart'; import 'package:intl/intl.dart'; import 'package:lucide_icons/lucide_icons.dart'; @@ -63,12 +64,26 @@ class _StockLedgerTabState extends ConsumerState { } final categoriesState = ref.watch(productCategoriesProvider); - final category = categoriesState.value?.firstWhere( + final commodityRatesState = ref.watch(commodityRatesProvider); + + final category = categoriesState.value?.where( (c) => c.id == widget.product.categoryId, - ); + ).firstOrNull; final String unit = category?.baseUnit ?? 'g'; - final double currentRate = category?.dailyRate ?? 0.0; + final double purityFactor = category?.purityFactor ?? (widget.product.purityFactor ?? 1.0); + + double commodityRate = 0.0; + if (category?.commodityCode != null && commodityRatesState.value != null) { + final match = commodityRatesState.value!.where( + (r) => r.commodityCode.toUpperCase() == category!.commodityCode!.toUpperCase() + ).firstOrNull; + if (match != null) { + commodityRate = match.rate; + } + } + + final double currentRate = commodityRate > 0 ? (commodityRate * purityFactor) : (category?.dailyRate ?? 0.0); return RefreshIndicator( onRefresh: _fetchLedger, diff --git a/kifi-app/lib/features/inventory/providers/commodity_rates_provider.dart b/kifi-app/lib/features/inventory/providers/commodity_rates_provider.dart new file mode 100644 index 0000000..fc82e86 --- /dev/null +++ b/kifi-app/lib/features/inventory/providers/commodity_rates_provider.dart @@ -0,0 +1,88 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/network/dio_client.dart'; +import '../domain/commodity_rate.dart'; + +class CommodityRatesNotifier extends AsyncNotifier> { + @override + FutureOr> build() async { + return _fetchLatestRates(); + } + + Future> _fetchLatestRates() async { + try { + final response = await DioClient().dio.get('/inventory/commodity-rates/latest'); + if (response.statusCode == 200) { + final List data = response.data; + return data.map((e) => CommodityRateHistory.fromJson(e)).toList(); + } + } catch (e) { + // Fallback if empty or endpoint fails + } + return []; + } + + Future> fetchRateHistory(String commodityCode) async { + try { + final response = await DioClient().dio.get('/inventory/commodity-rates/$commodityCode'); + if (response.statusCode == 200) { + final List data = response.data; + return data.map((e) => CommodityRateHistory.fromJson(e)).toList(); + } + } catch (e) { + // Error fetching history + } + return []; + } + + Future addRate( + String commodityCode, + double rate, { + String? commodityName, + String? purity, + String? source, + }) async { + try { + final payload = { + 'commodityCode': commodityCode, + 'commodityName': commodityName ?? commodityCode, + if (purity != null) 'purity': purity, + 'rate': rate, + 'source': source ?? 'MANUAL', + }; + + final response = await DioClient().dio.post( + '/inventory/commodity-rates', + data: payload, + ); + + if (response.statusCode == 200 && response.data != null) { + final newRate = CommodityRateHistory.fromJson(response.data); + state = await AsyncValue.guard(() => _fetchLatestRates()); + return newRate; + } + } catch (e) { + rethrow; + } + return null; + } + + Future syncRateToProducts(String commodityCode) async { + try { + final response = await DioClient().dio.post( + '/inventory/commodity-rates/$commodityCode/sync', + ); + if (response.statusCode == 200 && response.data != null) { + return (response.data['updatedCount'] as num?)?.toInt() ?? 0; + } + } catch (e) { + rethrow; + } + return 0; + } +} + +final commodityRatesProvider = + AsyncNotifierProvider>(() { + return CommodityRatesNotifier(); +}); diff --git a/kifi-app/lib/features/inventory/providers/product_categories_provider.dart b/kifi-app/lib/features/inventory/providers/product_categories_provider.dart index 8cd64f7..75b874a 100644 --- a/kifi-app/lib/features/inventory/providers/product_categories_provider.dart +++ b/kifi-app/lib/features/inventory/providers/product_categories_provider.dart @@ -9,6 +9,7 @@ class ProductCategory { final int? parentCategoryId; final bool hasChild; final String? commodityCode; + final double? purityFactor; final String? defaultHsn; final double? defaultGst; final bool huidRequired; @@ -26,6 +27,7 @@ class ProductCategory { this.parentCategoryId, this.hasChild = false, this.commodityCode, + this.purityFactor = 1.0, this.defaultHsn, this.defaultGst, this.huidRequired = false, @@ -44,16 +46,17 @@ class ProductCategory { name: json['name'], parentCategoryId: json['parentCategoryId'], hasChild: json['hasChild'] ?? false, - commodityCode: json['commodityCode'], - defaultHsn: json['defaultHsn'], - defaultGst: (json['defaultGst'] as num?)?.toDouble(), - huidRequired: json['huidRequired'] ?? false, - defaultMakingCharge: (json['defaultMakingCharge'] as num?)?.toDouble(), - makingChargeType: json['makingChargeType'], - baseUnit: json['baseUnit'] ?? 'pcs', - isActive: json['isActive'] ?? true, - sortOrder: json['sortOrder'] ?? 0, - dailyRate: (json['dailyRate'] as num?)?.toDouble(), + commodityCode: json['commodityCode'] ?? json['commodity_code'], + purityFactor: (json['purityFactor'] ?? json['purity_factor'] as num?)?.toDouble() ?? 1.0, + defaultHsn: json['defaultHsn'] ?? json['default_hsn'], + defaultGst: (json['defaultGst'] ?? json['default_gst'] as num?)?.toDouble(), + huidRequired: json['huidRequired'] ?? json['huid_required'] ?? false, + defaultMakingCharge: (json['defaultMakingCharge'] ?? json['default_making_charge'] as num?)?.toDouble(), + makingChargeType: json['makingChargeType'] ?? json['making_charge_type'], + baseUnit: json['baseUnit'] ?? json['base_unit'] ?? 'pcs', + isActive: json['isActive'] ?? json['is_active'] ?? true, + sortOrder: json['sortOrder'] ?? json['sort_order'] ?? 0, + dailyRate: (json['dailyRate'] ?? json['daily_rate'] as num?)?.toDouble(), ); } @@ -65,6 +68,7 @@ class ProductCategory { 'parentCategoryId': parentCategoryId, 'hasChild': hasChild, 'commodityCode': commodityCode, + 'purityFactor': purityFactor ?? 1.0, 'defaultHsn': defaultHsn, 'defaultGst': defaultGst, 'huidRequired': huidRequired, diff --git a/kifi-app/lib/features/transactions/presentation/add_transaction_screen.dart b/kifi-app/lib/features/transactions/presentation/add_transaction_screen.dart index 74145ab..6e998ba 100644 --- a/kifi-app/lib/features/transactions/presentation/add_transaction_screen.dart +++ b/kifi-app/lib/features/transactions/presentation/add_transaction_screen.dart @@ -570,117 +570,135 @@ class _AddTransactionScreenState extends ConsumerState { final amount = double.tryParse(amountText); if (amount == null) return; - if (fromWallet != null && fromWallet!.balance < amount) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Insufficient balance in From Account'))); - return; + final isFromPayableOrLiability = fromWallet != null && (fromWallet!.nature == 'PAYABLES' || fromWallet!.nature == 'LOAN'); + if (fromWallet != null && !isFromPayableOrLiability) { + double effectiveBalance = fromWallet!.balance; + if (widget.transaction != null && widget.transaction!.fromWalletId == fromWallet!.id) { + effectiveBalance += widget.transaction!.amount; + } + if (effectiveBalance < amount) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Insufficient balance in From Account'))); + return; + } } setState(() => isSaving = true); - // Determine internal type based on natures - String type = 'TRANSFER'; - if (fromWallet == null) { - type = 'INCOME'; - } else if (toWallet == null) { // Though toWallet is mandatory, just in case - type = 'EXPENSE'; - } else if (fromWallet?.nature == 'INCOME') { - type = 'INCOME'; - } else if (toWallet?.nature == 'PAYABLES' || toWallet?.nature == 'EXPENSE') { - type = 'EXPENSE'; - } else if (toWallet?.nature == 'INVESTMENTS') { - type = 'INVESTMENT'; - } - - if (isRecurring && widget.transaction == null) { - DateTime nextDate = selectedDate; - if (recurringFrequency == 'DAILY') { - nextDate = nextDate.add(const Duration(days: 1)); - } else if (recurringFrequency == 'WEEKLY') { - nextDate = nextDate.add(const Duration(days: 7)); - } else if (recurringFrequency == 'MONTHLY') { - nextDate = DateTime(nextDate.year, nextDate.month + 1, nextDate.day); + try { + // Determine internal type based on natures + String type = 'TRANSFER'; + if (fromWallet == null) { + type = 'INCOME'; + } else if (toWallet == null) { // Though toWallet is mandatory, just in case + type = 'EXPENSE'; + } else if (fromWallet?.nature == 'INCOME') { + type = 'INCOME'; + } else if (toWallet?.nature == 'PAYABLES' || toWallet?.nature == 'EXPENSE') { + type = 'EXPENSE'; + } else if (toWallet?.nature == 'INVESTMENTS') { + type = 'INVESTMENT'; } - final rt = RecurringTransaction( - id: 0, - type: type, - amount: amount, - categoryId: selectedCategory?.id, - fromWalletId: fromWallet?.id, - toWalletId: toWallet?.id, - frequency: recurringFrequency, - description: descriptionController.text.trim(), - nextExecutionDate: nextDate, - status: 'ACTIVE', - ); - await ref.read(recurringTransactionProvider.notifier).addRecurringTransaction(rt); - - final tx = Transaction( - id: 0, - type: type, - amount: amount, - date: selectedDate, - description: descriptionController.text.trim(), - categoryId: selectedCategory?.id, - fromWalletId: fromWallet?.id, - toWalletId: toWallet?.id, - dueDate: dueDate, - alertSchedule: alertSchedule == 'NONE' ? null : alertSchedule, - alertTime: alertSchedule == 'NONE' ? null : '${alertTime.hour.toString().padLeft(2, '0')}:${alertTime.minute.toString().padLeft(2, '0')}:00', - items: lineItems.isNotEmpty ? lineItems : null, - ); - await ref.read(transactionProvider.notifier).addTransaction(tx, base64Attachments: base64Attachments); - } else { - final tx = Transaction( - id: widget.transaction?.id ?? 0, - type: type, - amount: amount, - date: selectedDate, - description: descriptionController.text.trim(), - categoryId: selectedCategory?.id, - fromWalletId: fromWallet?.id, - toWalletId: toWallet?.id, - dueDate: dueDate, - alertSchedule: alertSchedule == 'NONE' ? null : alertSchedule, - alertTime: alertSchedule == 'NONE' ? null : '${alertTime.hour.toString().padLeft(2, '0')}:${alertTime.minute.toString().padLeft(2, '0')}:00', - items: lineItems.isNotEmpty ? lineItems : null, - ); + if (isRecurring && widget.transaction == null) { + DateTime nextDate = selectedDate; + if (recurringFrequency == 'DAILY') { + nextDate = nextDate.add(const Duration(days: 1)); + } else if (recurringFrequency == 'WEEKLY') { + nextDate = nextDate.add(const Duration(days: 7)); + } else if (recurringFrequency == 'MONTHLY') { + nextDate = DateTime(nextDate.year, nextDate.month + 1, nextDate.day); + } - if (widget.transaction == null) { + final rt = RecurringTransaction( + id: 0, + type: type, + amount: amount, + categoryId: selectedCategory?.id, + fromWalletId: fromWallet?.id, + toWalletId: toWallet?.id, + frequency: recurringFrequency, + description: descriptionController.text.trim(), + nextExecutionDate: nextDate, + status: 'ACTIVE', + ); + await ref.read(recurringTransactionProvider.notifier).addRecurringTransaction(rt); + + final tx = Transaction( + id: 0, + type: type, + amount: amount, + date: selectedDate, + description: descriptionController.text.trim(), + categoryId: selectedCategory?.id, + fromWalletId: fromWallet?.id, + toWalletId: toWallet?.id, + dueDate: dueDate, + alertSchedule: alertSchedule == 'NONE' ? null : alertSchedule, + alertTime: alertSchedule == 'NONE' ? null : '${alertTime.hour.toString().padLeft(2, '0')}:${alertTime.minute.toString().padLeft(2, '0')}:00', + items: lineItems.isNotEmpty ? lineItems : null, + ); await ref.read(transactionProvider.notifier).addTransaction(tx, base64Attachments: base64Attachments); } else { - await ref.read(transactionProvider.notifier).updateTransaction( - tx, - base64Attachments: base64Attachments, - deletedAttachmentIds: deletedAttachmentIds, + final tx = Transaction( + id: widget.transaction?.id ?? 0, + type: type, + amount: amount, + date: selectedDate, + description: descriptionController.text.trim(), + categoryId: selectedCategory?.id, + fromWalletId: fromWallet?.id, + toWalletId: toWallet?.id, + dueDate: dueDate, + alertSchedule: alertSchedule == 'NONE' ? null : alertSchedule, + alertTime: alertSchedule == 'NONE' ? null : '${alertTime.hour.toString().padLeft(2, '0')}:${alertTime.minute.toString().padLeft(2, '0')}:00', + items: lineItems.isNotEmpty ? lineItems : null, ); - } - - if (alertSchedule != 'NONE' && dueDate != null) { - DateTime alertDate = dueDate!; - if (alertSchedule == '1_DAY_BEFORE') { - alertDate = dueDate!.subtract(const Duration(days: 1)); - } else if (alertSchedule == '2_DAYS_BEFORE') { - alertDate = dueDate!.subtract(const Duration(days: 2)); - } else if (alertSchedule == '1_WEEK_BEFORE') { - alertDate = dueDate!.subtract(const Duration(days: 7)); - } - - final notifyDate = DateTime(alertDate.year, alertDate.month, alertDate.day, alertTime.hour, alertTime.minute); - if (notifyDate.isAfter(DateTime.now())) { - NotificationService().scheduleNotification( - id: DateTime.now().millisecondsSinceEpoch.remainder(100000), - title: 'Payment Due!', - body: 'Rs. ${amount.toStringAsFixed(0)} is due for ${toWallet?.name}', - scheduledDate: notifyDate, + + if (widget.transaction == null) { + await ref.read(transactionProvider.notifier).addTransaction(tx, base64Attachments: base64Attachments); + } else { + await ref.read(transactionProvider.notifier).updateTransaction( + tx, + base64Attachments: base64Attachments, + deletedAttachmentIds: deletedAttachmentIds, ); } + + if (alertSchedule != 'NONE' && dueDate != null) { + DateTime alertDate = dueDate!; + if (alertSchedule == '1_DAY_BEFORE') { + alertDate = dueDate!.subtract(const Duration(days: 1)); + } else if (alertSchedule == '2_DAYS_BEFORE') { + alertDate = dueDate!.subtract(const Duration(days: 2)); + } else if (alertSchedule == '1_WEEK_BEFORE') { + alertDate = dueDate!.subtract(const Duration(days: 7)); + } + + final notifyDate = DateTime(alertDate.year, alertDate.month, alertDate.day, alertTime.hour, alertTime.minute); + if (notifyDate.isAfter(DateTime.now())) { + NotificationService().scheduleNotification( + id: DateTime.now().millisecondsSinceEpoch.remainder(100000), + title: 'Payment Due!', + body: 'Rs. ${amount.toStringAsFixed(0)} is due for ${toWallet?.name}', + scheduledDate: notifyDate, + ); + } + } } - } - if (mounted) { - SnackBarService.showSuccess(context, 'Transaction saved successfully'); - Navigator.pop(context); + if (mounted) { + SnackBarService.showSuccess(context, 'Transaction saved successfully'); + Navigator.pop(context); + } + } catch (e) { + debugPrint('Error saving transaction: $e'); + if (mounted) { + SnackBarService.showError(context, 'Failed to save transaction: ${e.toString()}'); + } + } finally { + if (mounted) { + setState(() => isSaving = false); + } } } diff --git a/kifi-app/lib/features/transactions/providers/paginated_transaction_provider.dart b/kifi-app/lib/features/transactions/providers/paginated_transaction_provider.dart index fa0c91f..42c4847 100644 --- a/kifi-app/lib/features/transactions/providers/paginated_transaction_provider.dart +++ b/kifi-app/lib/features/transactions/providers/paginated_transaction_provider.dart @@ -108,6 +108,11 @@ class PaginatedTransactionNotifier extends Notifier { final updatedList = state.transactions.where((t) => t.id != id).toList(); state = state.copyWith(transactions: updatedList); } + + void updateTransactionInState(Transaction tx) { + final updatedList = state.transactions.map((t) => t.id == tx.id ? tx : t).toList(); + state = state.copyWith(transactions: updatedList); + } } final paginatedTransactionProvider = NotifierProvider(() { diff --git a/kifi-app/lib/features/transactions/providers/providers.dart b/kifi-app/lib/features/transactions/providers/providers.dart index 7c886a7..76d1f5e 100644 --- a/kifi-app/lib/features/transactions/providers/providers.dart +++ b/kifi-app/lib/features/transactions/providers/providers.dart @@ -52,7 +52,6 @@ class TransactionNotifier extends AsyncNotifier> { Future updateTransaction(Transaction transaction, {List>? base64Attachments, List? deletedAttachmentIds}) async { final updatedTransaction = await ref.read(apiRepositoryProvider).updateTransaction(transaction); - bool hasChanges = false; if (base64Attachments != null && base64Attachments.isNotEmpty) { for (var attachment in base64Attachments) { await ref.read(apiRepositoryProvider).addAttachment( @@ -62,28 +61,21 @@ class TransactionNotifier extends AsyncNotifier> { attachment['base64Content']! ); } - hasChanges = true; } if (deletedAttachmentIds != null && deletedAttachmentIds.isNotEmpty) { for (var id in deletedAttachmentIds) { await ref.read(apiRepositoryProvider).deleteAttachment(id); } - hasChanges = true; } - if (hasChanges) { - ref.invalidateSelf(); - } else { - if (state.value != null) { - final updatedList = state.value!.map((t) => t.id == updatedTransaction.id ? updatedTransaction : t).toList(); - state = AsyncValue.data(updatedList); - } - } + ref.invalidate(walletProvider); + ref.invalidateSelf(); } Future deleteTransaction(int id) async { await ref.read(apiRepositoryProvider).deleteTransaction(id); + ref.invalidate(walletProvider); if (state.value != null) { state = AsyncValue.data(state.value!.where((t) => t.id != id).toList()); } diff --git a/kifi-app/lib/features/vendor/presentation/purchase_order_builder_screen.dart b/kifi-app/lib/features/vendor/presentation/purchase_order_builder_screen.dart index d02a148..524a995 100644 --- a/kifi-app/lib/features/vendor/presentation/purchase_order_builder_screen.dart +++ b/kifi-app/lib/features/vendor/presentation/purchase_order_builder_screen.dart @@ -10,6 +10,7 @@ import '../../../core/widgets/premium_text_field.dart'; import '../../../core/widgets/smart_search_dropdown.dart'; import '../../inventory/providers/products_provider.dart'; import '../../inventory/providers/product_categories_provider.dart'; +import '../../inventory/providers/commodity_rates_provider.dart'; import '../providers/vendors_provider.dart'; import '../../business/providers/business_provider.dart'; import '../../transactions/presentation/widgets/attachment_gallery_screen.dart'; @@ -735,13 +736,34 @@ class _AddItemSheetState extends ConsumerState<_AddItemSheet> { if (_selectedProductId == null) return; final qty = int.tryParse(_qtyCtrl.text) ?? 1; + final products = ref.read(productsProvider).value ?? []; + final categories = ref.read(productCategoriesProvider).value ?? []; + final commodityRates = ref.read(commodityRatesProvider).value ?? []; + + final selectedProduct = products.where((p) => p.id == _selectedProductId).firstOrNull; + final selectedCat = categories.where((c) => c.id == selectedProduct?.categoryId).firstOrNull; + + double initialRate = 0.0; + if (selectedCat?.commodityCode != null) { + final matchRate = commodityRates.where( + (r) => r.commodityCode.toUpperCase() == selectedCat!.commodityCode!.toUpperCase() + ).firstOrNull; + if (matchRate != null && matchRate.rate > 0) { + final purity = selectedCat?.purityFactor ?? (selectedProduct?.purityFactor ?? 1.0); + initialRate = matchRate.rate * purity; + } + } + if (initialRate == 0.0) { + initialRate = selectedProduct?.sellingPrice ?? 0.0; + } + final List items = []; for (int i = 0; i < qty; i++) { items.add(PurchaseOrderItem( productId: _selectedProductId, - quantity: 1.0, // Each row is 1 unit initially, or could represent pieces/weight - unitPrice: 0.0, - total: 0.0, + quantity: 1.0, + unitPrice: initialRate, + total: initialRate > 0 ? initialRate : 0.0, )); } Navigator.pop(context, items);