Tuned ui to support ecommerce product catalogue and purchases.
This commit is contained in:
@@ -55,6 +55,7 @@ public class ProductCategoryController {
|
|||||||
existing.setDefaultMakingCharge(category.getDefaultMakingCharge());
|
existing.setDefaultMakingCharge(category.getDefaultMakingCharge());
|
||||||
existing.setMakingChargeType(category.getMakingChargeType());
|
existing.setMakingChargeType(category.getMakingChargeType());
|
||||||
existing.setBaseUnit(category.getBaseUnit());
|
existing.setBaseUnit(category.getBaseUnit());
|
||||||
|
existing.setDefaultLengthUom(category.getDefaultLengthUom());
|
||||||
existing.setIsActive(category.getIsActive());
|
existing.setIsActive(category.getIsActive());
|
||||||
existing.setSortOrder(category.getSortOrder());
|
existing.setSortOrder(category.getSortOrder());
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ public class InventoryItem {
|
|||||||
private Long vendorId;
|
private Long vendorId;
|
||||||
private Long branchId;
|
private Long branchId;
|
||||||
private String purchaseRef;
|
private String purchaseRef;
|
||||||
|
private String salesRef;
|
||||||
|
private BigDecimal saleRate;
|
||||||
private String photoUrl;
|
private String photoUrl;
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private String status = "AVAILABLE";
|
private String status = "AVAILABLE";
|
||||||
|
|||||||
@@ -33,6 +33,26 @@ public class Product {
|
|||||||
private String dimensions;
|
private String dimensions;
|
||||||
private String color;
|
private String color;
|
||||||
private String size;
|
private String size;
|
||||||
|
|
||||||
|
private Double itemLength;
|
||||||
|
private Double itemWidth;
|
||||||
|
private Double itemHeight;
|
||||||
|
private String dimensionUom;
|
||||||
|
|
||||||
|
private Double packageLength;
|
||||||
|
private Double packageWidth;
|
||||||
|
private Double packageHeight;
|
||||||
|
private String packageDimensionUom;
|
||||||
|
|
||||||
|
private String manufacturerCode;
|
||||||
|
private String material;
|
||||||
|
private String brandName;
|
||||||
|
private String countryOfOrigin;
|
||||||
|
|
||||||
|
private Double weightInG;
|
||||||
|
private Double packageWeightInG;
|
||||||
|
private Double volumetricWeightInKg;
|
||||||
|
|
||||||
private String priceCalcRule;
|
private String priceCalcRule;
|
||||||
private Double purityFactor;
|
private Double purityFactor;
|
||||||
private Double makingCharges;
|
private Double makingCharges;
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ public class ProductCategory {
|
|||||||
private String makingChargeType;
|
private String makingChargeType;
|
||||||
|
|
||||||
private String baseUnit;
|
private String baseUnit;
|
||||||
|
@Builder.Default
|
||||||
|
private String defaultLengthUom = "cm";
|
||||||
|
|
||||||
@Builder.Default
|
@Builder.Default
|
||||||
private Boolean isActive = true;
|
private Boolean isActive = true;
|
||||||
|
|||||||
@@ -102,6 +102,26 @@ public class ProductService {
|
|||||||
existingProduct.setColor(updatedProduct.getColor());
|
existingProduct.setColor(updatedProduct.getColor());
|
||||||
existingProduct.setSize(updatedProduct.getSize());
|
existingProduct.setSize(updatedProduct.getSize());
|
||||||
existingProduct.setDimensions(updatedProduct.getDimensions());
|
existingProduct.setDimensions(updatedProduct.getDimensions());
|
||||||
|
|
||||||
|
existingProduct.setItemLength(updatedProduct.getItemLength());
|
||||||
|
existingProduct.setItemWidth(updatedProduct.getItemWidth());
|
||||||
|
existingProduct.setItemHeight(updatedProduct.getItemHeight());
|
||||||
|
existingProduct.setDimensionUom(updatedProduct.getDimensionUom());
|
||||||
|
|
||||||
|
existingProduct.setPackageLength(updatedProduct.getPackageLength());
|
||||||
|
existingProduct.setPackageWidth(updatedProduct.getPackageWidth());
|
||||||
|
existingProduct.setPackageHeight(updatedProduct.getPackageHeight());
|
||||||
|
existingProduct.setPackageDimensionUom(updatedProduct.getPackageDimensionUom());
|
||||||
|
|
||||||
|
existingProduct.setManufacturerCode(updatedProduct.getManufacturerCode());
|
||||||
|
existingProduct.setMaterial(updatedProduct.getMaterial());
|
||||||
|
existingProduct.setBrandName(updatedProduct.getBrandName());
|
||||||
|
existingProduct.setCountryOfOrigin(updatedProduct.getCountryOfOrigin());
|
||||||
|
|
||||||
|
existingProduct.setWeightInG(updatedProduct.getWeightInG());
|
||||||
|
existingProduct.setPackageWeightInG(updatedProduct.getPackageWeightInG());
|
||||||
|
existingProduct.setVolumetricWeightInKg(updatedProduct.getVolumetricWeightInKg());
|
||||||
|
|
||||||
existingProduct.setPriceCalcRule(updatedProduct.getPriceCalcRule());
|
existingProduct.setPriceCalcRule(updatedProduct.getPriceCalcRule());
|
||||||
existingProduct.setPurityFactor(normalizePurity(updatedProduct.getPurityFactor()));
|
existingProduct.setPurityFactor(normalizePurity(updatedProduct.getPurityFactor()));
|
||||||
existingProduct.setMakingCharges(updatedProduct.getMakingCharges());
|
existingProduct.setMakingCharges(updatedProduct.getMakingCharges());
|
||||||
|
|||||||
@@ -146,15 +146,64 @@ public class InvoiceService {
|
|||||||
if (shouldDeduct && invoice.getItems() != null && !invoice.getItems().isEmpty()) {
|
if (shouldDeduct && invoice.getItems() != null && !invoice.getItems().isEmpty()) {
|
||||||
return Flux.fromIterable(invoice.getItems())
|
return Flux.fromIterable(invoice.getItems())
|
||||||
.concatMap(item -> {
|
.concatMap(item -> {
|
||||||
|
BigDecimal soldQty = (item.getWeight() != null && item.getWeight().compareTo(BigDecimal.ZERO) > 0)
|
||||||
|
? item.getWeight()
|
||||||
|
: (item.getQuantity() != null && item.getQuantity().compareTo(BigDecimal.ZERO) > 0 ? item.getQuantity() : BigDecimal.ONE);
|
||||||
|
BigDecimal soldRate = item.getUnitPrice() != null ? item.getUnitPrice() : BigDecimal.ZERO;
|
||||||
|
|
||||||
if (item.getInventoryItemId() != null) {
|
if (item.getInventoryItemId() != null) {
|
||||||
return inventoryItemRepository.findById(item.getInventoryItemId())
|
return inventoryItemRepository.findById(item.getInventoryItemId())
|
||||||
.flatMap(invItem -> {
|
.flatMap(invItem -> {
|
||||||
invItem.setStatus("SOLD");
|
BigDecimal currentWeight = (invItem.getGrossWeight() != null && invItem.getGrossWeight().compareTo(BigDecimal.ZERO) > 0)
|
||||||
invItem.setUpdatedAt(LocalDateTime.now());
|
? invItem.getGrossWeight()
|
||||||
BigDecimal cost = invItem.getPurchaseCost() != null ? invItem.getPurchaseCost() : BigDecimal.ZERO;
|
: ((invItem.getNetWeight() != null && invItem.getNetWeight().compareTo(BigDecimal.ZERO) > 0)
|
||||||
return inventoryItemRepository.save(invItem).thenReturn(cost);
|
? invItem.getNetWeight()
|
||||||
|
: BigDecimal.ONE);
|
||||||
|
|
||||||
|
BigDecimal unitCost = invItem.getPurchaseCost() != null ? invItem.getPurchaseCost() : BigDecimal.ZERO;
|
||||||
|
BigDecimal cost = unitCost.multiply(soldQty);
|
||||||
|
|
||||||
|
if (currentWeight.compareTo(soldQty) > 0) {
|
||||||
|
// Partial sale: Reduce existing available item weight
|
||||||
|
BigDecimal remaining = currentWeight.subtract(soldQty);
|
||||||
|
invItem.setGrossWeight(remaining);
|
||||||
|
invItem.setNetWeight(remaining);
|
||||||
|
invItem.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
|
// Create new SOLD item record
|
||||||
|
com.kifi.api.entity.inventory.InventoryItem soldItem = com.kifi.api.entity.inventory.InventoryItem.builder()
|
||||||
|
.userId(userId)
|
||||||
|
.productId(invItem.getProductId())
|
||||||
|
.vendorId(invItem.getVendorId())
|
||||||
|
.purchaseRef(invItem.getPurchaseRef())
|
||||||
|
.salesRef(invoice.getInvoiceNumber())
|
||||||
|
.purchaseCost(invItem.getPurchaseCost())
|
||||||
|
.saleRate(soldRate)
|
||||||
|
.makingCharges(invItem.getMakingCharges())
|
||||||
|
.sku(invItem.getSku())
|
||||||
|
.huid(invItem.getHuid())
|
||||||
|
.grossWeight(soldQty)
|
||||||
|
.netWeight(soldQty)
|
||||||
|
.photoUrl(invItem.getPhotoUrl())
|
||||||
|
.status("SOLD")
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return inventoryItemRepository.save(invItem)
|
||||||
|
.then(inventoryItemRepository.save(soldItem))
|
||||||
|
.thenReturn(cost);
|
||||||
|
} else {
|
||||||
|
// Full sale
|
||||||
|
invItem.setStatus("SOLD");
|
||||||
|
invItem.setSalesRef(invoice.getInvoiceNumber());
|
||||||
|
invItem.setSaleRate(soldRate);
|
||||||
|
invItem.setUpdatedAt(LocalDateTime.now());
|
||||||
|
return inventoryItemRepository.save(invItem).thenReturn(cost);
|
||||||
|
}
|
||||||
}).defaultIfEmpty(BigDecimal.ZERO);
|
}).defaultIfEmpty(BigDecimal.ZERO);
|
||||||
} else if (item.getProductId() != null) {
|
} else if (item.getProductId() != null) {
|
||||||
|
// Product sale without explicit inventory item: adjust product stock & FIFO deduct inventory items
|
||||||
InventoryMovement movement = InventoryMovement.builder()
|
InventoryMovement movement = InventoryMovement.builder()
|
||||||
.userId(userId)
|
.userId(userId)
|
||||||
.type("REDUCTION")
|
.type("REDUCTION")
|
||||||
@@ -167,8 +216,72 @@ public class InvoiceService {
|
|||||||
movementItem.setQuantity(item.getQuantity());
|
movementItem.setQuantity(item.getQuantity());
|
||||||
movement.setItems(java.util.Collections.singletonList(movementItem));
|
movement.setItems(java.util.Collections.singletonList(movementItem));
|
||||||
|
|
||||||
return productService.adjustStock(userId, item.getProductId(), movement)
|
Mono<Void> adjustMono = productService.adjustStock(userId, item.getProductId(), movement).then();
|
||||||
.thenReturn(BigDecimal.ZERO);
|
|
||||||
|
Mono<BigDecimal> fifoMono = inventoryItemRepository.findByUserIdAndProductId(userId, item.getProductId())
|
||||||
|
.filter(i -> "AVAILABLE".equalsIgnoreCase(i.getStatus()))
|
||||||
|
.collectList()
|
||||||
|
.flatMap(availItems -> {
|
||||||
|
if (availItems.isEmpty()) {
|
||||||
|
return Mono.just(BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
BigDecimal needed = soldQty;
|
||||||
|
java.util.List<Mono<Void>> saves = new java.util.ArrayList<>();
|
||||||
|
BigDecimal totalCost = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
for (com.kifi.api.entity.inventory.InventoryItem invItem : availItems) {
|
||||||
|
if (needed.compareTo(BigDecimal.ZERO) <= 0) break;
|
||||||
|
BigDecimal itemWeight = (invItem.getGrossWeight() != null && invItem.getGrossWeight().compareTo(BigDecimal.ZERO) > 0)
|
||||||
|
? invItem.getGrossWeight()
|
||||||
|
: ((invItem.getNetWeight() != null && invItem.getNetWeight().compareTo(BigDecimal.ZERO) > 0)
|
||||||
|
? invItem.getNetWeight()
|
||||||
|
: BigDecimal.ONE);
|
||||||
|
|
||||||
|
BigDecimal take = needed.min(itemWeight);
|
||||||
|
needed = needed.subtract(take);
|
||||||
|
BigDecimal unitPurchase = invItem.getPurchaseCost() != null ? invItem.getPurchaseCost() : BigDecimal.ZERO;
|
||||||
|
totalCost = totalCost.add(unitPurchase.multiply(take));
|
||||||
|
|
||||||
|
if (itemWeight.compareTo(take) > 0) {
|
||||||
|
BigDecimal remaining = itemWeight.subtract(take);
|
||||||
|
invItem.setGrossWeight(remaining);
|
||||||
|
invItem.setNetWeight(remaining);
|
||||||
|
invItem.setUpdatedAt(LocalDateTime.now());
|
||||||
|
saves.add(inventoryItemRepository.save(invItem).then());
|
||||||
|
|
||||||
|
com.kifi.api.entity.inventory.InventoryItem soldRecord = com.kifi.api.entity.inventory.InventoryItem.builder()
|
||||||
|
.userId(userId)
|
||||||
|
.productId(invItem.getProductId())
|
||||||
|
.vendorId(invItem.getVendorId())
|
||||||
|
.purchaseRef(invItem.getPurchaseRef())
|
||||||
|
.salesRef(invoice.getInvoiceNumber())
|
||||||
|
.purchaseCost(invItem.getPurchaseCost())
|
||||||
|
.saleRate(soldRate)
|
||||||
|
.makingCharges(invItem.getMakingCharges())
|
||||||
|
.sku(invItem.getSku())
|
||||||
|
.huid(invItem.getHuid())
|
||||||
|
.grossWeight(take)
|
||||||
|
.netWeight(take)
|
||||||
|
.photoUrl(invItem.getPhotoUrl())
|
||||||
|
.status("SOLD")
|
||||||
|
.createdAt(LocalDateTime.now())
|
||||||
|
.updatedAt(LocalDateTime.now())
|
||||||
|
.build();
|
||||||
|
saves.add(inventoryItemRepository.save(soldRecord).then());
|
||||||
|
} else {
|
||||||
|
invItem.setStatus("SOLD");
|
||||||
|
invItem.setSalesRef(invoice.getInvoiceNumber());
|
||||||
|
invItem.setSaleRate(soldRate);
|
||||||
|
invItem.setUpdatedAt(LocalDateTime.now());
|
||||||
|
saves.add(inventoryItemRepository.save(invItem).then());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal finalCost = totalCost;
|
||||||
|
return Flux.concat(saves).then(Mono.just(finalCost));
|
||||||
|
});
|
||||||
|
|
||||||
|
return adjustMono.then(fifoMono);
|
||||||
}
|
}
|
||||||
return Mono.just(BigDecimal.ZERO);
|
return Mono.just(BigDecimal.ZERO);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ CREATE TABLE IF NOT EXISTS product_categories (
|
|||||||
commodity_code VARCHAR(10),
|
commodity_code VARCHAR(10),
|
||||||
purity_factor DECIMAL(5, 4) DEFAULT 1.0,
|
purity_factor DECIMAL(5, 4) DEFAULT 1.0,
|
||||||
base_unit VARCHAR(20) DEFAULT 'pcs',
|
base_unit VARCHAR(20) DEFAULT 'pcs',
|
||||||
|
default_length_uom VARCHAR(20) DEFAULT 'cm',
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
sort_order INTEGER DEFAULT 0,
|
sort_order INTEGER DEFAULT 0,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
@@ -249,6 +250,21 @@ CREATE TABLE IF NOT EXISTS products (
|
|||||||
weight DECIMAL(10, 3),
|
weight DECIMAL(10, 3),
|
||||||
color VARCHAR(50),
|
color VARCHAR(50),
|
||||||
size VARCHAR(50),
|
size VARCHAR(50),
|
||||||
|
item_length DECIMAL(10, 2),
|
||||||
|
item_width DECIMAL(10, 2),
|
||||||
|
item_height DECIMAL(10, 2),
|
||||||
|
dimension_uom VARCHAR(20) DEFAULT 'cm',
|
||||||
|
package_length DECIMAL(10, 2),
|
||||||
|
package_width DECIMAL(10, 2),
|
||||||
|
package_height DECIMAL(10, 2),
|
||||||
|
package_dimension_uom VARCHAR(20) DEFAULT 'cm',
|
||||||
|
manufacturer_code VARCHAR(100),
|
||||||
|
material VARCHAR(100),
|
||||||
|
brand_name VARCHAR(100),
|
||||||
|
country_of_origin VARCHAR(100),
|
||||||
|
weight_in_g DECIMAL(10, 3),
|
||||||
|
package_weight_in_g DECIMAL(10, 3),
|
||||||
|
volumetric_weight_in_kg DECIMAL(10, 3),
|
||||||
price_calc_rule VARCHAR(50) DEFAULT 'MANUAL',
|
price_calc_rule VARCHAR(50) DEFAULT 'MANUAL',
|
||||||
purity_factor DECIMAL(5, 4),
|
purity_factor DECIMAL(5, 4),
|
||||||
making_charges DECIMAL(15, 2) DEFAULT 0.0,
|
making_charges DECIMAL(15, 2) DEFAULT 0.0,
|
||||||
@@ -383,6 +399,8 @@ CREATE TABLE IF NOT EXISTS inventory_items (
|
|||||||
vendor_id INTEGER REFERENCES vendors(id) ON DELETE SET NULL,
|
vendor_id INTEGER REFERENCES vendors(id) ON DELETE SET NULL,
|
||||||
branch_id INTEGER,
|
branch_id INTEGER,
|
||||||
purchase_ref VARCHAR(100),
|
purchase_ref VARCHAR(100),
|
||||||
|
sales_ref VARCHAR(100),
|
||||||
|
sale_rate DECIMAL(15, 2),
|
||||||
photo_url VARCHAR(1024),
|
photo_url VARCHAR(1024),
|
||||||
status VARCHAR(50) DEFAULT 'AVAILABLE',
|
status VARCHAR(50) DEFAULT 'AVAILABLE',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class InventoryItem {
|
|||||||
final int? vendorId;
|
final int? vendorId;
|
||||||
final int? branchId;
|
final int? branchId;
|
||||||
final String? purchaseRef;
|
final String? purchaseRef;
|
||||||
|
final String? salesRef;
|
||||||
|
final double? saleRate;
|
||||||
final String? photoUrl;
|
final String? photoUrl;
|
||||||
final String? status;
|
final String? status;
|
||||||
final DateTime? createdAt;
|
final DateTime? createdAt;
|
||||||
@@ -48,6 +50,8 @@ class InventoryItem {
|
|||||||
this.vendorId,
|
this.vendorId,
|
||||||
this.branchId,
|
this.branchId,
|
||||||
this.purchaseRef,
|
this.purchaseRef,
|
||||||
|
this.salesRef,
|
||||||
|
this.saleRate,
|
||||||
this.photoUrl,
|
this.photoUrl,
|
||||||
this.status,
|
this.status,
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
@@ -77,6 +81,8 @@ class InventoryItem {
|
|||||||
vendorId: json['vendorId'],
|
vendorId: json['vendorId'],
|
||||||
branchId: json['branchId'],
|
branchId: json['branchId'],
|
||||||
purchaseRef: json['purchaseRef'],
|
purchaseRef: json['purchaseRef'],
|
||||||
|
salesRef: json['salesRef'] ?? json['sales_ref'],
|
||||||
|
saleRate: (json['saleRate'] ?? json['sale_rate'])?.toDouble(),
|
||||||
photoUrl: json['photoUrl'] ?? json['photo_url'],
|
photoUrl: json['photoUrl'] ?? json['photo_url'],
|
||||||
status: json['status'],
|
status: json['status'],
|
||||||
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
|
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
|
||||||
|
|||||||
@@ -14,6 +14,26 @@ class Product {
|
|||||||
final String? dimensions;
|
final String? dimensions;
|
||||||
final String? color;
|
final String? color;
|
||||||
final String? size;
|
final String? size;
|
||||||
|
|
||||||
|
final double? itemLength;
|
||||||
|
final double? itemWidth;
|
||||||
|
final double? itemHeight;
|
||||||
|
final String? dimensionUom;
|
||||||
|
|
||||||
|
final double? packageLength;
|
||||||
|
final double? packageWidth;
|
||||||
|
final double? packageHeight;
|
||||||
|
final String? packageDimensionUom;
|
||||||
|
|
||||||
|
final String? manufacturerCode;
|
||||||
|
final String? material;
|
||||||
|
final String? brandName;
|
||||||
|
final String? countryOfOrigin;
|
||||||
|
|
||||||
|
final double? weightInG;
|
||||||
|
final double? packageWeightInG;
|
||||||
|
final double? volumetricWeightInKg;
|
||||||
|
|
||||||
final String priceCalcRule;
|
final String priceCalcRule;
|
||||||
final bool autoCalculatePrice;
|
final bool autoCalculatePrice;
|
||||||
final double? purityFactor;
|
final double? purityFactor;
|
||||||
@@ -39,6 +59,21 @@ class Product {
|
|||||||
this.dimensions,
|
this.dimensions,
|
||||||
this.color,
|
this.color,
|
||||||
this.size,
|
this.size,
|
||||||
|
this.itemLength,
|
||||||
|
this.itemWidth,
|
||||||
|
this.itemHeight,
|
||||||
|
this.dimensionUom = 'cm',
|
||||||
|
this.packageLength,
|
||||||
|
this.packageWidth,
|
||||||
|
this.packageHeight,
|
||||||
|
this.packageDimensionUom = 'cm',
|
||||||
|
this.manufacturerCode,
|
||||||
|
this.material,
|
||||||
|
this.brandName,
|
||||||
|
this.countryOfOrigin,
|
||||||
|
this.weightInG,
|
||||||
|
this.packageWeightInG,
|
||||||
|
this.volumetricWeightInKg,
|
||||||
this.priceCalcRule = 'MANUAL',
|
this.priceCalcRule = 'MANUAL',
|
||||||
this.autoCalculatePrice = false,
|
this.autoCalculatePrice = false,
|
||||||
this.purityFactor,
|
this.purityFactor,
|
||||||
@@ -67,6 +102,21 @@ class Product {
|
|||||||
dimensions: json['dimensions'],
|
dimensions: json['dimensions'],
|
||||||
color: json['color'],
|
color: json['color'],
|
||||||
size: json['size'],
|
size: json['size'],
|
||||||
|
itemLength: (json['itemLength'] ?? json['item_length'] as num?)?.toDouble(),
|
||||||
|
itemWidth: (json['itemWidth'] ?? json['item_width'] as num?)?.toDouble(),
|
||||||
|
itemHeight: (json['itemHeight'] ?? json['item_height'] as num?)?.toDouble(),
|
||||||
|
dimensionUom: json['dimensionUom'] ?? json['dimension_uom'] ?? 'cm',
|
||||||
|
packageLength: (json['packageLength'] ?? json['package_length'] as num?)?.toDouble(),
|
||||||
|
packageWidth: (json['packageWidth'] ?? json['package_width'] as num?)?.toDouble(),
|
||||||
|
packageHeight: (json['packageHeight'] ?? json['package_height'] as num?)?.toDouble(),
|
||||||
|
packageDimensionUom: json['packageDimensionUom'] ?? json['package_dimension_uom'] ?? 'cm',
|
||||||
|
manufacturerCode: json['manufacturerCode'] ?? json['manufacturer_code'],
|
||||||
|
material: json['material'],
|
||||||
|
brandName: json['brandName'] ?? json['brand_name'],
|
||||||
|
countryOfOrigin: json['countryOfOrigin'] ?? json['country_of_origin'],
|
||||||
|
weightInG: (json['weightInG'] ?? json['weight_in_g'] as num?)?.toDouble(),
|
||||||
|
packageWeightInG: (json['packageWeightInG'] ?? json['package_weight_in_g'] as num?)?.toDouble(),
|
||||||
|
volumetricWeightInKg: (json['volumetricWeightInKg'] ?? json['volumetric_weight_in_kg'] as num?)?.toDouble(),
|
||||||
priceCalcRule:
|
priceCalcRule:
|
||||||
json['priceCalcRule'] ?? json['price_calc_rule'] ?? 'MANUAL',
|
json['priceCalcRule'] ?? json['price_calc_rule'] ?? 'MANUAL',
|
||||||
autoCalculatePrice:
|
autoCalculatePrice:
|
||||||
@@ -116,6 +166,21 @@ class Product {
|
|||||||
'dimensions': dimensions,
|
'dimensions': dimensions,
|
||||||
'color': color,
|
'color': color,
|
||||||
'size': size,
|
'size': size,
|
||||||
|
'itemLength': itemLength,
|
||||||
|
'itemWidth': itemWidth,
|
||||||
|
'itemHeight': itemHeight,
|
||||||
|
'dimensionUom': dimensionUom,
|
||||||
|
'packageLength': packageLength,
|
||||||
|
'packageWidth': packageWidth,
|
||||||
|
'packageHeight': packageHeight,
|
||||||
|
'packageDimensionUom': packageDimensionUom,
|
||||||
|
'manufacturerCode': manufacturerCode,
|
||||||
|
'material': material,
|
||||||
|
'brandName': brandName,
|
||||||
|
'countryOfOrigin': countryOfOrigin,
|
||||||
|
'weightInG': weightInG,
|
||||||
|
'packageWeightInG': packageWeightInG,
|
||||||
|
'volumetricWeightInKg': volumetricWeightInKg,
|
||||||
'priceCalcRule': priceCalcRule,
|
'priceCalcRule': priceCalcRule,
|
||||||
'autoCalculatePrice': autoCalculatePrice,
|
'autoCalculatePrice': autoCalculatePrice,
|
||||||
'purityFactor': purityFactor,
|
'purityFactor': purityFactor,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../providers/product_categories_provider.dart';
|
import '../providers/product_categories_provider.dart';
|
||||||
import '../../../core/theme/nature_colors.dart';
|
import '../providers/uoms_provider.dart';
|
||||||
import '../../business/providers/business_mode_provider.dart';
|
import '../../business/providers/business_mode_provider.dart';
|
||||||
import '../../business/providers/business_provider.dart';
|
import '../../business/providers/business_provider.dart';
|
||||||
import '../../../core/theme/app_theme.dart';
|
import '../../../core/theme/app_theme.dart';
|
||||||
@@ -307,6 +307,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
String _commodityCode = 'XAU';
|
String _commodityCode = 'XAU';
|
||||||
String _makingChargeType = 'PER_GRAM';
|
String _makingChargeType = 'PER_GRAM';
|
||||||
String _baseUnit = 'pcs';
|
String _baseUnit = 'pcs';
|
||||||
|
String _defaultLengthUom = 'cm';
|
||||||
|
|
||||||
final List<Map<String, String>> _commodities = [
|
final List<Map<String, String>> _commodities = [
|
||||||
{'code': 'XAU', 'label': 'XAU - Gold'},
|
{'code': 'XAU', 'label': 'XAU - Gold'},
|
||||||
@@ -325,6 +326,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
|
|
||||||
_baseUnit = isJewellery ? 'g' : 'pcs';
|
_baseUnit = isJewellery ? 'g' : 'pcs';
|
||||||
_commodityCode = isJewellery ? 'XAU' : 'NONE';
|
_commodityCode = isJewellery ? 'XAU' : 'NONE';
|
||||||
|
_defaultLengthUom = 'cm';
|
||||||
|
|
||||||
if (widget.categoryToEdit != null) {
|
if (widget.categoryToEdit != null) {
|
||||||
final c = widget.categoryToEdit!;
|
final c = widget.categoryToEdit!;
|
||||||
@@ -343,8 +345,9 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
_makingChargeType = c.makingChargeType ?? 'PER_GRAM';
|
_makingChargeType = c.makingChargeType ?? 'PER_GRAM';
|
||||||
if (_makingChargeType == 'PER_GM') _makingChargeType = 'PER_GRAM';
|
if (_makingChargeType == 'PER_GM') _makingChargeType = 'PER_GRAM';
|
||||||
|
|
||||||
_baseUnit = c.baseUnit ?? (isJewellery ? 'g' : 'pcs');
|
_baseUnit = c.baseUnit;
|
||||||
if (_baseUnit == 'gm') _baseUnit = 'g';
|
if (_baseUnit == 'gm') _baseUnit = 'g';
|
||||||
|
_defaultLengthUom = c.defaultLengthUom;
|
||||||
}
|
}
|
||||||
} else if (widget.parent != null) {
|
} else if (widget.parent != null) {
|
||||||
_selectedParentId = widget.parent!.id;
|
_selectedParentId = widget.parent!.id;
|
||||||
@@ -380,6 +383,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
defaultMakingCharge: _isLeaf && isJewellery ? double.tryParse(_makingChargeController.text) : null,
|
defaultMakingCharge: _isLeaf && isJewellery ? double.tryParse(_makingChargeController.text) : null,
|
||||||
makingChargeType: _isLeaf && isJewellery ? _makingChargeType : null,
|
makingChargeType: _isLeaf && isJewellery ? _makingChargeType : null,
|
||||||
baseUnit: _isLeaf ? _baseUnit : 'pcs',
|
baseUnit: _isLeaf ? _baseUnit : 'pcs',
|
||||||
|
defaultLengthUom: _isLeaf ? _defaultLengthUom : 'cm',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.categoryToEdit != null && widget.categoryToEdit!.id != null) {
|
if (widget.categoryToEdit != null && widget.categoryToEdit!.id != null) {
|
||||||
@@ -398,6 +402,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
final businessProfile = ref.watch(businessProfileProvider).value;
|
final businessProfile = ref.watch(businessProfileProvider).value;
|
||||||
final nature = (businessProfile?.natureOfBusiness ?? businessProfile?.industry ?? 'JEWELLERY').toUpperCase();
|
final nature = (businessProfile?.natureOfBusiness ?? businessProfile?.industry ?? 'JEWELLERY').toUpperCase();
|
||||||
final isJewellery = nature == 'JEWELLERY';
|
final isJewellery = nature == 'JEWELLERY';
|
||||||
|
final uomsState = ref.watch(uomsProvider);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: MediaQuery.of(context).size.height * 0.85,
|
height: MediaQuery.of(context).size.height * 0.85,
|
||||||
@@ -623,30 +628,68 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
|
|
||||||
DropdownButtonFormField<String>(
|
uomsState.when(
|
||||||
value: _baseUnit,
|
loading: () => const Padding(
|
||||||
decoration: const InputDecoration(
|
padding: EdgeInsets.symmetric(vertical: 8),
|
||||||
labelText: 'Base Unit',
|
child: LinearProgressIndicator(),
|
||||||
),
|
),
|
||||||
items: [
|
error: (err, stack) => DropdownButtonFormField<String>(
|
||||||
if (isJewellery) ...const [
|
value: _baseUnit,
|
||||||
DropdownMenuItem(value: 'g', child: Text('Grams (g)')),
|
decoration: const InputDecoration(labelText: 'Base Unit'),
|
||||||
DropdownMenuItem(value: 'kg', child: Text('Kilograms (kg)')),
|
items: [
|
||||||
DropdownMenuItem(value: 'pcs', child: Text('Pieces (pcs)')),
|
DropdownMenuItem(value: _baseUnit, child: Text(_baseUnit)),
|
||||||
] else ...const [
|
|
||||||
DropdownMenuItem(value: 'pcs', child: Text('Pieces (pcs)')),
|
|
||||||
DropdownMenuItem(value: 'unit', child: Text('Units (unit)')),
|
|
||||||
DropdownMenuItem(value: 'box', child: Text('Boxes (box)')),
|
|
||||||
DropdownMenuItem(value: 'set', child: Text('Sets (set)')),
|
|
||||||
DropdownMenuItem(value: 'pair', child: Text('Pairs (pair)')),
|
|
||||||
DropdownMenuItem(value: 'g', child: Text('Grams (g)')),
|
|
||||||
DropdownMenuItem(value: 'kg', child: Text('Kilograms (kg)')),
|
|
||||||
DropdownMenuItem(value: 'm', child: Text('Meters (m)')),
|
|
||||||
DropdownMenuItem(value: 'l', child: Text('Liters (l)')),
|
|
||||||
],
|
],
|
||||||
],
|
onChanged: (val) => setState(() => _baseUnit = val!),
|
||||||
onChanged: (val) => setState(() => _baseUnit = val!),
|
),
|
||||||
|
data: (uoms) {
|
||||||
|
final availableValues = uoms.map((u) => u.abbreviation ?? u.name).toList();
|
||||||
|
final match = uoms.where((u) =>
|
||||||
|
(u.abbreviation ?? u.name).toLowerCase() == _baseUnit.toLowerCase() ||
|
||||||
|
u.name.toLowerCase() == _baseUnit.toLowerCase()
|
||||||
|
).firstOrNull;
|
||||||
|
final effectiveValue = match != null
|
||||||
|
? (match.abbreviation ?? match.name)
|
||||||
|
: (availableValues.contains(_baseUnit) ? _baseUnit : (availableValues.isNotEmpty ? availableValues.first : null));
|
||||||
|
|
||||||
|
return DropdownButtonFormField<String>(
|
||||||
|
value: effectiveValue,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Base Unit',
|
||||||
|
prefixIcon: Icon(LucideIcons.scale),
|
||||||
|
),
|
||||||
|
isExpanded: true,
|
||||||
|
items: uoms.map((u) {
|
||||||
|
final val = u.abbreviation ?? u.name;
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: val,
|
||||||
|
child: Text(u.displayName),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (val) {
|
||||||
|
if (val != null) {
|
||||||
|
setState(() => _baseUnit = val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
if (!isJewellery) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: _defaultLengthUom,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Default Length UoM',
|
||||||
|
prefixIcon: Icon(LucideIcons.ruler),
|
||||||
|
),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: 'cm', child: Text('Centimeters (cm)')),
|
||||||
|
DropdownMenuItem(value: 'in', child: Text('Inches (in)')),
|
||||||
|
DropdownMenuItem(value: 'mm', child: Text('Millimeters (mm)')),
|
||||||
|
DropdownMenuItem(value: 'm', child: Text('Meters (m)')),
|
||||||
|
],
|
||||||
|
onChanged: (val) => setState(() => _defaultLengthUom = val ?? 'cm'),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:kifi_app/features/inventory/providers/products_provider.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:kifi_app/core/network/dio_client.dart';
|
import 'package:kifi_app/core/network/dio_client.dart';
|
||||||
|
import 'package:kifi_app/core/widgets/responsive_layout.dart';
|
||||||
|
import 'package:kifi_app/features/business/providers/business_provider.dart';
|
||||||
import 'package:kifi_app/features/inventory/domain/product.dart';
|
import 'package:kifi_app/features/inventory/domain/product.dart';
|
||||||
import 'package:kifi_app/features/inventory/presentation/add_product_screen.dart';
|
import 'package:kifi_app/features/inventory/presentation/add_product_screen.dart';
|
||||||
import 'package:kifi_app/features/inventory/presentation/stock_ledger_tab.dart';
|
import 'package:kifi_app/features/inventory/presentation/stock_ledger_tab.dart';
|
||||||
import 'package:kifi_app/features/inventory/presentation/widgets/adjust_stock_sheet.dart';
|
import 'package:kifi_app/features/inventory/providers/product_categories_provider.dart';
|
||||||
|
import 'package:kifi_app/features/inventory/providers/products_provider.dart';
|
||||||
|
import 'package:kifi_app/features/inventory/providers/uoms_provider.dart';
|
||||||
|
import 'package:kifi_app/features/transactions/presentation/widgets/attachment_gallery_screen.dart';
|
||||||
|
|
||||||
class ProductDetailScreen extends ConsumerWidget {
|
class ProductDetailScreen extends ConsumerWidget {
|
||||||
final Product product;
|
final Product product;
|
||||||
@@ -70,79 +75,333 @@ class ProductDetailScreen extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOverviewTab(BuildContext context, Product p, WidgetRef ref) {
|
Widget _buildOverviewTab(BuildContext context, Product p, WidgetRef ref) {
|
||||||
|
final businessProfile = ref.watch(businessProfileProvider).value;
|
||||||
|
final nature = (businessProfile?.natureOfBusiness ?? businessProfile?.industry ?? 'JEWELLERY').toUpperCase();
|
||||||
|
final isJewellery = nature == 'JEWELLERY';
|
||||||
|
|
||||||
|
final categories = ref.watch(productCategoriesProvider).value ?? [];
|
||||||
|
final category = p.categoryId != null
|
||||||
|
? categories.where((c) => c.id == p.categoryId).firstOrNull
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final uoms = ref.watch(uomsProvider).value ?? [];
|
||||||
|
final uom = p.uomId != null
|
||||||
|
? uoms.where((u) => u.id == p.uomId).firstOrNull
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final hasSpecs = (p.brandName != null && p.brandName!.isNotEmpty) ||
|
||||||
|
(p.manufacturerCode != null && p.manufacturerCode!.isNotEmpty) ||
|
||||||
|
(p.material != null && p.material!.isNotEmpty) ||
|
||||||
|
(p.color != null && p.color!.isNotEmpty) ||
|
||||||
|
(p.countryOfOrigin != null && p.countryOfOrigin!.isNotEmpty);
|
||||||
|
|
||||||
|
final hasDimensions = p.itemLength != null ||
|
||||||
|
p.itemWidth != null ||
|
||||||
|
p.itemHeight != null ||
|
||||||
|
p.weightInG != null;
|
||||||
|
|
||||||
|
final hasPackaging = p.packageLength != null ||
|
||||||
|
p.packageWidth != null ||
|
||||||
|
p.packageHeight != null ||
|
||||||
|
p.packageWeightInG != null ||
|
||||||
|
p.volumetricWeightInKg != null;
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: MaxContentWidth(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
maxWidth: 900,
|
||||||
children: [
|
child: Column(
|
||||||
// Image Header
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
if (p.imageIds.isNotEmpty)
|
children: [
|
||||||
Container(
|
// Images Header & Gallery
|
||||||
height: 200,
|
if (p.imageIds.isNotEmpty)
|
||||||
width: double.infinity,
|
_buildImagesSection(context, p),
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
color: Colors.grey[200],
|
|
||||||
),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: FutureBuilder<String?>(
|
|
||||||
future: DioClient().storage.read(key: 'jwt_token'),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
final token = snapshot.data;
|
|
||||||
if (token == null) {
|
|
||||||
return const Icon(
|
|
||||||
Icons.image,
|
|
||||||
size: 50,
|
|
||||||
color: Colors.grey,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Image.network(
|
|
||||||
'${DioClient().dio.options.baseUrl}/inventory/products/${p.id}/images/${p.imageIds.first}/content',
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
headers: {'Authorization': 'Bearer $token'},
|
|
||||||
errorBuilder: (context, error, stackTrace) =>
|
|
||||||
const Icon(Icons.image, size: 50, color: Colors.grey),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Product Details Card
|
if (p.imageIds.isNotEmpty)
|
||||||
Card(
|
const SizedBox(height: 20),
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(
|
// Product Details Card
|
||||||
borderRadius: BorderRadius.circular(12),
|
Card(
|
||||||
),
|
elevation: 0,
|
||||||
child: Padding(
|
shape: RoundedRectangleBorder(
|
||||||
padding: const EdgeInsets.all(16.0),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.all(16.0),
|
||||||
const Text(
|
child: Column(
|
||||||
"Product Details",
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
children: [
|
||||||
),
|
Row(
|
||||||
const Divider(),
|
children: [
|
||||||
_buildDetailRow("SKU / Barcode", p.sku ?? "-"),
|
Icon(LucideIcons.package, size: 20, color: Theme.of(context).colorScheme.primary),
|
||||||
_buildDetailRow("HSN Code", p.hsnCode ?? "-"),
|
const SizedBox(width: 8),
|
||||||
_buildDetailRow(
|
const Text(
|
||||||
"GST Rate",
|
"Product Details",
|
||||||
"${p.gstRate?.toStringAsFixed(1) ?? '0'}%",
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
),
|
),
|
||||||
if (p.makingCharges != null && p.makingCharges! > 0)
|
],
|
||||||
_buildDetailRow(
|
|
||||||
"Making Charges",
|
|
||||||
p.makingChargesType == 'PERCENTAGE'
|
|
||||||
? "${p.makingCharges}%"
|
|
||||||
: (p.makingChargesType == 'PER_PIECE'
|
|
||||||
? "₹${p.makingCharges!.toStringAsFixed(2)} / pc"
|
|
||||||
: "₹${p.makingCharges!.toStringAsFixed(2)} / g"),
|
|
||||||
),
|
),
|
||||||
],
|
const Divider(),
|
||||||
|
_buildDetailRow("SKU / Barcode", p.sku ?? "-"),
|
||||||
|
if (category != null)
|
||||||
|
_buildDetailRow("Category", category.name),
|
||||||
|
if (uom != null)
|
||||||
|
_buildDetailRow("Unit of Measure", uom.displayName),
|
||||||
|
_buildDetailRow("HSN Code", p.hsnCode ?? "-"),
|
||||||
|
_buildDetailRow(
|
||||||
|
"GST Rate",
|
||||||
|
"${p.gstRate?.toStringAsFixed(1) ?? '0'}%",
|
||||||
|
),
|
||||||
|
if (isJewellery && p.makingCharges != null && p.makingCharges! > 0)
|
||||||
|
_buildDetailRow(
|
||||||
|
"Making Charges",
|
||||||
|
p.makingChargesType == 'PERCENTAGE'
|
||||||
|
? "${p.makingCharges}%"
|
||||||
|
: (p.makingChargesType == 'PER_PIECE'
|
||||||
|
? "₹${p.makingCharges!.toStringAsFixed(2)} / pc"
|
||||||
|
: "₹${p.makingCharges!.toStringAsFixed(2)} / g"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Non-jewellery Specific Sections
|
||||||
|
if (!isJewellery) ...[
|
||||||
|
if (hasSpecs) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Card(
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(LucideIcons.sliders, size: 20, color: Theme.of(context).colorScheme.primary),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text(
|
||||||
|
"Product Specifications",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
if (p.brandName != null && p.brandName!.isNotEmpty)
|
||||||
|
_buildDetailRow("Brand Name", p.brandName!),
|
||||||
|
if (p.manufacturerCode != null && p.manufacturerCode!.isNotEmpty)
|
||||||
|
_buildDetailRow("Manufacturer Code", p.manufacturerCode!),
|
||||||
|
if (p.material != null && p.material!.isNotEmpty)
|
||||||
|
_buildDetailRow("Material", p.material!),
|
||||||
|
if (p.color != null && p.color!.isNotEmpty)
|
||||||
|
_buildDetailRow("Color", p.color!),
|
||||||
|
if (p.countryOfOrigin != null && p.countryOfOrigin!.isNotEmpty)
|
||||||
|
_buildDetailRow("Country of Origin", p.countryOfOrigin!),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (hasDimensions) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Card(
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(LucideIcons.ruler, size: 20, color: Theme.of(context).colorScheme.primary),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text(
|
||||||
|
"Item Dimensions & Weight",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
if (p.itemLength != null || p.itemWidth != null || p.itemHeight != null)
|
||||||
|
_buildDetailRow(
|
||||||
|
"Dimensions (L × W × H)",
|
||||||
|
"${p.itemLength ?? '-'} × ${p.itemWidth ?? '-'} × ${p.itemHeight ?? '-'} ${p.dimensionUom ?? 'cm'}",
|
||||||
|
),
|
||||||
|
if (p.weightInG != null)
|
||||||
|
_buildDetailRow("Item Weight", "${p.weightInG} g"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (hasPackaging) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Card(
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(LucideIcons.box, size: 20, color: Theme.of(context).colorScheme.primary),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text(
|
||||||
|
"Packaging Dimensions & Weight",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
if (p.packageLength != null || p.packageWidth != null || p.packageHeight != null)
|
||||||
|
_buildDetailRow(
|
||||||
|
"Package (L × W × H)",
|
||||||
|
"${p.packageLength ?? '-'} × ${p.packageWidth ?? '-'} × ${p.packageHeight ?? '-'} ${p.packageDimensionUom ?? 'cm'}",
|
||||||
|
),
|
||||||
|
if (p.packageWeightInG != null)
|
||||||
|
_buildDetailRow("Package Weight", "${p.packageWeightInG} g"),
|
||||||
|
if (p.volumetricWeightInKg != null)
|
||||||
|
_buildDetailRow(
|
||||||
|
"Volumetric Weight",
|
||||||
|
"${p.volumetricWeightInKg} kg",
|
||||||
|
highlight: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildImagesSection(BuildContext context, Product p) {
|
||||||
|
return FutureBuilder<String?>(
|
||||||
|
future: DioClient().storage.read(key: 'jwt_token'),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final token = snapshot.data;
|
||||||
|
if (token == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final allImages = p.imageIds.map<ImageProvider>((imageId) {
|
||||||
|
return NetworkImage(
|
||||||
|
'${DioClient().dio.options.baseUrl}/inventory/products/${p.id}/images/$imageId/content',
|
||||||
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => AttachmentGalleryScreen(
|
||||||
|
images: allImages,
|
||||||
|
initialIndex: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
height: 220,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
color: Colors.grey[200],
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Image.network(
|
||||||
|
'${DioClient().dio.options.baseUrl}/inventory/products/${p.id}/images/${p.imageIds.first}/content',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
errorBuilder: (context, error, stackTrace) =>
|
||||||
|
const Icon(Icons.image, size: 50, color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (p.imageIds.length > 1) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SizedBox(
|
||||||
|
height: 64,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: p.imageIds.length,
|
||||||
|
separatorBuilder: (context, index) => const SizedBox(width: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final imageId = p.imageIds[index];
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => AttachmentGalleryScreen(
|
||||||
|
images: allImages,
|
||||||
|
initialIndex: index,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 64,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Image.network(
|
||||||
|
'${DioClient().dio.options.baseUrl}/inventory/products/${p.id}/images/$imageId/content',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
headers: {'Authorization': 'Bearer $token'},
|
||||||
|
errorBuilder: (context, error, stackTrace) =>
|
||||||
|
const Icon(Icons.image, size: 24, color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDetailRow(String label, String value, {bool highlight = false}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(label, style: TextStyle(color: Colors.grey[600], fontSize: 14)),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
value,
|
||||||
|
textAlign: TextAlign.end,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 14,
|
||||||
|
color: highlight ? const Color(0xFF4F46E5) : Colors.black87,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -150,17 +409,5 @@ class ProductDetailScreen extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDetailRow(String label, String value) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(label, style: TextStyle(color: Colors.grey[600])),
|
|
||||||
Text(value, style: const TextStyle(fontWeight: FontWeight.w500)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:kifi_app/features/vendor/presentation/purchase_order_details_screen.dart';
|
import 'package:kifi_app/features/vendor/presentation/purchase_order_details_screen.dart';
|
||||||
import 'package:kifi_app/features/vendor/providers/purchase_orders_provider.dart';
|
import 'package:kifi_app/features/vendor/providers/purchase_orders_provider.dart';
|
||||||
|
import 'package:kifi_app/features/sales/presentation/invoice_details_screen.dart';
|
||||||
|
import 'package:kifi_app/features/sales/providers/invoices_provider.dart';
|
||||||
|
import 'package:kifi_app/features/sales/domain/invoice.dart';
|
||||||
|
import 'package:kifi_app/features/business/providers/business_provider.dart';
|
||||||
import 'package:kifi_app/core/utils/purity_utils.dart';
|
import 'package:kifi_app/core/utils/purity_utils.dart';
|
||||||
import '../../transactions/presentation/widgets/attachment_gallery_screen.dart';
|
import '../../transactions/presentation/widgets/attachment_gallery_screen.dart';
|
||||||
|
|
||||||
@@ -90,6 +94,10 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
|||||||
displayItems = _items!;
|
displayItems = _items!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final businessProfile = ref.watch(businessProfileProvider).value;
|
||||||
|
final nature = (businessProfile?.natureOfBusiness ?? businessProfile?.industry ?? 'JEWELLERY').toUpperCase();
|
||||||
|
final isJewellery = nature == 'JEWELLERY';
|
||||||
|
|
||||||
final categoriesState = ref.watch(productCategoriesProvider);
|
final categoriesState = ref.watch(productCategoriesProvider);
|
||||||
final commodityRatesState = ref.watch(commodityRatesProvider);
|
final commodityRatesState = ref.watch(commodityRatesProvider);
|
||||||
|
|
||||||
@@ -97,20 +105,24 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
|||||||
(c) => c.id == widget.product.categoryId,
|
(c) => c.id == widget.product.categoryId,
|
||||||
).firstOrNull;
|
).firstOrNull;
|
||||||
|
|
||||||
|
final bool isCommodity = isJewellery && category?.commodityCode != null && category!.commodityCode!.isNotEmpty;
|
||||||
|
|
||||||
final String unit = category?.baseUnit ?? 'g';
|
final String unit = category?.baseUnit ?? 'g';
|
||||||
final double purityFactor = resolvePurity(categoryPurity: category?.purityFactor, productPurity: widget.product.purityFactor);
|
final double purityFactor = resolvePurity(categoryPurity: category?.purityFactor, productPurity: widget.product.purityFactor);
|
||||||
|
|
||||||
double commodityRate = 0.0;
|
double commodityRate = 0.0;
|
||||||
if (category?.commodityCode != null && commodityRatesState.value != null) {
|
if (isCommodity && commodityRatesState.value != null && category.commodityCode != null) {
|
||||||
final match = commodityRatesState.value!.where(
|
final match = commodityRatesState.value!.where(
|
||||||
(r) => r.commodityCode.toUpperCase() == category!.commodityCode!.toUpperCase()
|
(r) => r.commodityCode.toUpperCase() == category.commodityCode!.toUpperCase()
|
||||||
).firstOrNull;
|
).firstOrNull;
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
commodityRate = match.rate;
|
commodityRate = match.rate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final double currentRate = commodityRate > 0 ? (commodityRate * purityFactor) : (category?.dailyRate ?? 0.0);
|
final double currentRate = isCommodity
|
||||||
|
? (commodityRate > 0 ? (commodityRate * purityFactor) : (category.dailyRate ?? 0.0))
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -147,6 +159,31 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
|||||||
final isSold = item.status == 'SOLD';
|
final isSold = item.status == 'SOLD';
|
||||||
final purchaseOrdersState = ref.watch(purchaseOrdersProvider);
|
final purchaseOrdersState = ref.watch(purchaseOrdersProvider);
|
||||||
final purchaseOrders = purchaseOrdersState.value ?? [];
|
final purchaseOrders = purchaseOrdersState.value ?? [];
|
||||||
|
final invoicesState = ref.watch(invoicesProvider);
|
||||||
|
final invoices = invoicesState.value ?? [];
|
||||||
|
|
||||||
|
// Resolve sales invoice for sold item
|
||||||
|
Invoice? matchingSaleInvoice;
|
||||||
|
if (isSold) {
|
||||||
|
if (item.salesRef != null && item.salesRef!.isNotEmpty) {
|
||||||
|
matchingSaleInvoice = invoices.where((i) => i.invoiceNumber == item.salesRef).firstOrNull;
|
||||||
|
}
|
||||||
|
matchingSaleInvoice ??= invoices.where((i) => i.items.any((invIt) =>
|
||||||
|
(invIt.inventoryItemId != null && invIt.inventoryItemId == item.id) ||
|
||||||
|
(item.huid != null && item.huid!.isNotEmpty && invIt.huid == item.huid)
|
||||||
|
)).firstOrNull;
|
||||||
|
}
|
||||||
|
|
||||||
|
double effectiveSaleRate = (item.saleRate != null && item.saleRate! > 0) ? item.saleRate! : 0.0;
|
||||||
|
if (isSold && effectiveSaleRate == 0.0 && matchingSaleInvoice != null) {
|
||||||
|
final matchItem = matchingSaleInvoice.items.where((invIt) =>
|
||||||
|
(invIt.inventoryItemId != null && invIt.inventoryItemId == item.id) ||
|
||||||
|
(item.huid != null && item.huid!.isNotEmpty && invIt.huid == item.huid)
|
||||||
|
).firstOrNull;
|
||||||
|
if (matchItem != null && matchItem.unitPrice > 0) {
|
||||||
|
effectiveSaleRate = matchItem.unitPrice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
double weight = (item.grossWeight != null && item.grossWeight! > 0)
|
double weight = (item.grossWeight != null && item.grossWeight! > 0)
|
||||||
? item.grossWeight!
|
? item.grossWeight!
|
||||||
@@ -202,10 +239,13 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final purchasePrice = weight * purchaseRate;
|
final purchasePrice = weight * purchaseRate;
|
||||||
final currentPrice = weight * currentRate;
|
final double displayedSellingRate = effectiveSaleRate > 0 ? effectiveSaleRate : (isCommodity ? currentRate : purchaseRate);
|
||||||
final gainLoss = currentPrice - purchasePrice;
|
final currentPrice = weight * (isSold ? displayedSellingRate : currentRate);
|
||||||
|
final gainLoss = isSold ? ((weight * displayedSellingRate) - purchasePrice) : (currentPrice - purchasePrice);
|
||||||
final isGain = gainLoss >= 0;
|
final isGain = gainLoss >= 0;
|
||||||
|
|
||||||
|
final String? salesInvoiceNumber = matchingSaleInvoice?.invoiceNumber ?? item.salesRef;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
@@ -325,46 +365,91 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
|||||||
style: TextStyle(color: Colors.grey[600], fontSize: 12),
|
style: TextStyle(color: Colors.grey[600], fontSize: 12),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
if (item.purchaseRef != null)
|
if (isSold) ...[
|
||||||
InkWell(
|
if (salesInvoiceNumber != null && salesInvoiceNumber.isNotEmpty)
|
||||||
onTap: () async {
|
InkWell(
|
||||||
try {
|
onTap: () async {
|
||||||
var pos = ref.read(purchaseOrdersProvider).value;
|
try {
|
||||||
if (pos == null || pos.isEmpty) {
|
var inv = matchingSaleInvoice;
|
||||||
pos = await ref.read(purchaseOrdersProvider.future);
|
if (inv == null) {
|
||||||
|
var invs = ref.read(invoicesProvider).value;
|
||||||
|
if (invs == null || invs.isEmpty) {
|
||||||
|
invs = await ref.read(invoicesProvider.future);
|
||||||
|
}
|
||||||
|
inv = invs?.where((i) => i.invoiceNumber == salesInvoiceNumber).firstOrNull;
|
||||||
|
}
|
||||||
|
if (inv == null) {
|
||||||
|
throw Exception('Sales invoice $salesInvoiceNumber not found');
|
||||||
|
}
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => InvoiceDetailsScreen(invoice: inv!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Could not open sales invoice: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
final po = pos!.firstWhere(
|
},
|
||||||
(p) => p.poNumber == item.purchaseRef,
|
child: Text(
|
||||||
orElse: () => throw Exception('Purchase invoice not found'),
|
'INV: $salesInvoiceNumber',
|
||||||
);
|
style: const TextStyle(
|
||||||
if (mounted) {
|
color: Colors.blue,
|
||||||
Navigator.push(
|
decoration: TextDecoration.underline,
|
||||||
context,
|
fontWeight: FontWeight.w600,
|
||||||
MaterialPageRoute(
|
fontSize: 12,
|
||||||
builder: (context) => PurchaseOrderDetailsScreen(po: po),
|
),
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Could not open purchase invoice: $e')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'INV: ${item.purchaseRef}',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.blue,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
] else ...[
|
||||||
if (item.huid != null && item.huid!.isNotEmpty)
|
if (item.purchaseRef != null)
|
||||||
|
InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
try {
|
||||||
|
var pos = ref.read(purchaseOrdersProvider).value;
|
||||||
|
if (pos == null || pos.isEmpty) {
|
||||||
|
pos = await ref.read(purchaseOrdersProvider.future);
|
||||||
|
}
|
||||||
|
final po = pos!.firstWhere(
|
||||||
|
(p) => p.poNumber == item.purchaseRef,
|
||||||
|
orElse: () => throw Exception('Purchase invoice not found'),
|
||||||
|
);
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PurchaseOrderDetailsScreen(po: po),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Could not open purchase invoice: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'INV: ${item.purchaseRef}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (isJewellery && item.huid != null && item.huid!.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 4.0),
|
padding: const EdgeInsets.only(top: 2.0),
|
||||||
child: Text('HUID: ${item.huid}', style: const TextStyle(fontWeight: FontWeight.w500)),
|
child: Text('HUID: ${item.huid}', style: const TextStyle(fontWeight: FontWeight.w500)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -399,94 +484,175 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
|||||||
color: isSold ? Colors.grey.shade600 : null,
|
color: isSold ? Colors.grey.shade600 : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 3),
|
if (isCommodity) ...[
|
||||||
Container(
|
const SizedBox(height: 3),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1.5),
|
Container(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1.5),
|
||||||
color: Colors.amber.withValues(alpha: 0.12),
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(4),
|
color: Colors.amber.withValues(alpha: 0.12),
|
||||||
border: Border.all(color: Colors.amber.withValues(alpha: 0.3)),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
border: Border.all(color: Colors.amber.withValues(alpha: 0.3)),
|
||||||
child: Text(
|
),
|
||||||
'Purity: ${formatPurity(double.tryParse(item.purity ?? '') ?? purityFactor)}',
|
child: Text(
|
||||||
style: TextStyle(
|
'Purity: ${formatPurity(double.tryParse(item.purity ?? '') ?? purityFactor)}',
|
||||||
fontSize: 10,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontSize: 10,
|
||||||
color: Colors.amber.shade900,
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.amber.shade900,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
Row(
|
if (isCommodity) ...[
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
Row(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text('Purchase Rate', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
|
||||||
Text('₹${purchaseRate.toStringAsFixed(2)}/$unit', style: const TextStyle(fontWeight: FontWeight.w600)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text('Purchase Amt', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
|
||||||
Text('₹${purchasePrice.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(isSold ? 'Selling Rate' : 'Current Rate', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
|
||||||
Text('₹${currentRate.toStringAsFixed(2)}/$unit', style: const TextStyle(fontWeight: FontWeight.w600)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text(isSold ? 'Valuation' : 'Current Amt', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
|
||||||
Text('₹${currentPrice.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isGain ? Colors.green.withValues(alpha: 0.1) : Colors.red.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Column(
|
||||||
isGain ? LucideIcons.trendingUp : LucideIcons.trendingDown,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
color: isGain ? Colors.green : Colors.red,
|
children: [
|
||||||
size: 16,
|
Text('Purchase Rate', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
||||||
|
Text('₹${purchaseRate.toStringAsFixed(2)}/$unit', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
Column(
|
||||||
Text(
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
'${isSold ? "Realized " : ""}${isGain ? 'Gain' : 'Loss'}: ₹${gainLoss.abs().toStringAsFixed(2)}',
|
children: [
|
||||||
style: TextStyle(
|
Text('Purchase Amt', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
||||||
color: isGain ? Colors.green : Colors.red,
|
Text('₹${purchasePrice.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
fontWeight: FontWeight.bold,
|
],
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(isSold ? 'Selling Rate' : 'Current Rate', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
||||||
|
Text('₹${displayedSellingRate.toStringAsFixed(2)}/$unit', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(isSold ? 'Valuation' : 'Current Amt', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
||||||
|
Text('₹${currentPrice.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isGain ? Colors.green.withValues(alpha: 0.1) : Colors.red.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isGain ? LucideIcons.trendingUp : LucideIcons.trendingDown,
|
||||||
|
color: isGain ? Colors.green : Colors.red,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'${isSold ? "Realized " : ""}${isGain ? 'Gain' : 'Loss'}: ₹${gainLoss.abs().toStringAsFixed(2)}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: isGain ? Colors.green : Colors.red,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
// Non-Commodity / E-Commerce Products
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Purchase Rate', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
||||||
|
Text('₹${purchaseRate.toStringAsFixed(2)}/$unit', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text('Purchase Amt', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
||||||
|
Text('₹${purchasePrice.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (isSold && effectiveSaleRate > 0) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('Sold Rate', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
||||||
|
Text('₹${effectiveSaleRate.toStringAsFixed(2)}/$unit', style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.purple)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text('Sold Amt', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
|
||||||
|
Text('₹${(weight * effectiveSaleRate).toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.purple)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Builder(
|
||||||
|
builder: (context) {
|
||||||
|
final soldAmt = weight * effectiveSaleRate;
|
||||||
|
final realizedMargin = soldAmt - purchasePrice;
|
||||||
|
final isProfit = realizedMargin >= 0;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isProfit ? Colors.green.withValues(alpha: 0.1) : Colors.red.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isProfit ? LucideIcons.trendingUp : LucideIcons.trendingDown,
|
||||||
|
color: isProfit ? Colors.green : Colors.red,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Realized ${isProfit ? 'Profit' : 'Loss'}: ₹${realizedMargin.abs().toStringAsFixed(2)}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: isProfit ? Colors.green : Colors.red,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class ProductCategory {
|
|||||||
final double? defaultMakingCharge;
|
final double? defaultMakingCharge;
|
||||||
final String? makingChargeType;
|
final String? makingChargeType;
|
||||||
final String baseUnit;
|
final String baseUnit;
|
||||||
|
final String defaultLengthUom;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
final int sortOrder;
|
final int sortOrder;
|
||||||
final double? dailyRate;
|
final double? dailyRate;
|
||||||
@@ -34,6 +35,7 @@ class ProductCategory {
|
|||||||
this.defaultMakingCharge,
|
this.defaultMakingCharge,
|
||||||
this.makingChargeType,
|
this.makingChargeType,
|
||||||
this.baseUnit = 'pcs',
|
this.baseUnit = 'pcs',
|
||||||
|
this.defaultLengthUom = 'cm',
|
||||||
this.isActive = true,
|
this.isActive = true,
|
||||||
this.sortOrder = 0,
|
this.sortOrder = 0,
|
||||||
this.dailyRate,
|
this.dailyRate,
|
||||||
@@ -54,6 +56,7 @@ class ProductCategory {
|
|||||||
defaultMakingCharge: (json['defaultMakingCharge'] ?? json['default_making_charge'] as num?)?.toDouble(),
|
defaultMakingCharge: (json['defaultMakingCharge'] ?? json['default_making_charge'] as num?)?.toDouble(),
|
||||||
makingChargeType: json['makingChargeType'] ?? json['making_charge_type'],
|
makingChargeType: json['makingChargeType'] ?? json['making_charge_type'],
|
||||||
baseUnit: json['baseUnit'] ?? json['base_unit'] ?? 'pcs',
|
baseUnit: json['baseUnit'] ?? json['base_unit'] ?? 'pcs',
|
||||||
|
defaultLengthUom: json['defaultLengthUom'] ?? json['default_length_uom'] ?? 'cm',
|
||||||
isActive: json['isActive'] ?? json['is_active'] ?? true,
|
isActive: json['isActive'] ?? json['is_active'] ?? true,
|
||||||
sortOrder: json['sortOrder'] ?? json['sort_order'] ?? 0,
|
sortOrder: json['sortOrder'] ?? json['sort_order'] ?? 0,
|
||||||
dailyRate: (json['dailyRate'] ?? json['daily_rate'] as num?)?.toDouble(),
|
dailyRate: (json['dailyRate'] ?? json['daily_rate'] as num?)?.toDouble(),
|
||||||
@@ -75,6 +78,7 @@ class ProductCategory {
|
|||||||
'defaultMakingCharge': defaultMakingCharge,
|
'defaultMakingCharge': defaultMakingCharge,
|
||||||
'makingChargeType': makingChargeType,
|
'makingChargeType': makingChargeType,
|
||||||
'baseUnit': baseUnit,
|
'baseUnit': baseUnit,
|
||||||
|
'defaultLengthUom': defaultLengthUom,
|
||||||
'isActive': isActive,
|
'isActive': isActive,
|
||||||
'sortOrder': sortOrder,
|
'sortOrder': sortOrder,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,11 +57,20 @@ class UomsNotifier extends AsyncNotifier<List<UnitOfMeasure>> {
|
|||||||
// If empty in database, seed standard units of measure
|
// If empty in database, seed standard units of measure
|
||||||
try {
|
try {
|
||||||
final defaults = [
|
final defaults = [
|
||||||
|
{'name': 'Pieces', 'abbreviation': 'pcs'},
|
||||||
|
{'name': 'Units', 'abbreviation': 'unit'},
|
||||||
|
{'name': 'Boxes', 'abbreviation': 'box'},
|
||||||
|
{'name': 'Sets', 'abbreviation': 'set'},
|
||||||
|
{'name': 'Pairs', 'abbreviation': 'pair'},
|
||||||
|
{'name': 'Packs', 'abbreviation': 'pk'},
|
||||||
{'name': 'Grams', 'abbreviation': 'g'},
|
{'name': 'Grams', 'abbreviation': 'g'},
|
||||||
{'name': 'Kilograms', 'abbreviation': 'kg'},
|
{'name': 'Kilograms', 'abbreviation': 'kg'},
|
||||||
{'name': 'Pieces', 'abbreviation': 'pcs'},
|
|
||||||
{'name': 'Carats', 'abbreviation': 'ct'},
|
|
||||||
{'name': 'Milligrams', 'abbreviation': 'mg'},
|
{'name': 'Milligrams', 'abbreviation': 'mg'},
|
||||||
|
{'name': 'Carats', 'abbreviation': 'ct'},
|
||||||
|
{'name': 'Meters', 'abbreviation': 'm'},
|
||||||
|
{'name': 'Liters', 'abbreviation': 'l'},
|
||||||
|
{'name': 'Milliliters', 'abbreviation': 'ml'},
|
||||||
|
{'name': 'Dozens', 'abbreviation': 'doz'},
|
||||||
];
|
];
|
||||||
for (final def in defaults) {
|
for (final def in defaults) {
|
||||||
await DioClient().dio.post('/inventory/uom', data: def);
|
await DioClient().dio.post('/inventory/uom', data: def);
|
||||||
|
|||||||
@@ -1175,6 +1175,9 @@ class _AddItemSheetState extends ConsumerState<_AddItemSheet> {
|
|||||||
}) {
|
}) {
|
||||||
final sku = product.sku;
|
final sku = product.sku;
|
||||||
final qtyCtrl = _getQtyController(product.id ?? 0);
|
final qtyCtrl = _getQtyController(product.id ?? 0);
|
||||||
|
final businessProfile = ref.watch(businessProfileProvider).value;
|
||||||
|
final nature = (businessProfile?.natureOfBusiness ?? businessProfile?.industry ?? 'JEWELLERY').toUpperCase();
|
||||||
|
final isJewellery = nature == 'JEWELLERY';
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
@@ -1228,28 +1231,31 @@ class _AddItemSheetState extends ConsumerState<_AddItemSheet> {
|
|||||||
_buildPOSheetBadge(category!.name, const Color(0xFFD4AF37)),
|
_buildPOSheetBadge(category!.name, const Color(0xFFD4AF37)),
|
||||||
if (sku != null && sku.isNotEmpty)
|
if (sku != null && sku.isNotEmpty)
|
||||||
_buildPOSheetBadge('SKU: $sku', Colors.blue),
|
_buildPOSheetBadge('SKU: $sku', Colors.blue),
|
||||||
_buildPOSheetBadge('Purity: ${formatPurity(resolvePurity(categoryPurity: category?.purityFactor, productPurity: product.purityFactor))}', Colors.teal),
|
if (isJewellery)
|
||||||
|
_buildPOSheetBadge('Purity: ${formatPurity(resolvePurity(categoryPurity: category?.purityFactor, productPurity: product.purityFactor))}', Colors.teal),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
if (isJewellery) ...[
|
||||||
// Small box to enter number of items
|
const SizedBox(width: 12),
|
||||||
SizedBox(
|
// Small box to enter number of items
|
||||||
width: 75,
|
SizedBox(
|
||||||
child: TextFormField(
|
width: 75,
|
||||||
controller: qtyCtrl,
|
child: TextFormField(
|
||||||
keyboardType: TextInputType.number,
|
controller: qtyCtrl,
|
||||||
textAlign: TextAlign.center,
|
keyboardType: TextInputType.number,
|
||||||
decoration: InputDecoration(
|
textAlign: TextAlign.center,
|
||||||
labelText: 'Qty',
|
decoration: InputDecoration(
|
||||||
isDense: true,
|
labelText: 'Qty',
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
isDense: true,
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(height: 20),
|
const Divider(height: 20),
|
||||||
@@ -1257,7 +1263,7 @@ class _AddItemSheetState extends ConsumerState<_AddItemSheet> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final qty = int.tryParse(qtyCtrl.text.trim()) ?? 1;
|
final qty = isJewellery ? (int.tryParse(qtyCtrl.text.trim()) ?? 1) : 1;
|
||||||
_addItemsFromCard(product, category, qty);
|
_addItemsFromCard(product, category, qty);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
@@ -1278,7 +1284,7 @@ class _AddItemSheetState extends ConsumerState<_AddItemSheet> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _POItemRow extends StatefulWidget {
|
class _POItemRow extends ConsumerStatefulWidget {
|
||||||
final PurchaseOrderItem item;
|
final PurchaseOrderItem item;
|
||||||
final String productName;
|
final String productName;
|
||||||
final Product? product;
|
final Product? product;
|
||||||
@@ -1300,14 +1306,15 @@ class _POItemRow extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_POItemRow> createState() => _POItemRowState();
|
ConsumerState<_POItemRow> createState() => _POItemRowState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _POItemRowState extends State<_POItemRow> {
|
class _POItemRowState extends ConsumerState<_POItemRow> {
|
||||||
late TextEditingController _weightCtrl;
|
late TextEditingController _weightCtrl;
|
||||||
late TextEditingController _rateCtrl;
|
late TextEditingController _rateCtrl;
|
||||||
late TextEditingController _huidCtrl;
|
late TextEditingController _huidCtrl;
|
||||||
late TextEditingController _totalCtrl;
|
late TextEditingController _totalCtrl;
|
||||||
|
bool _isUploadingPhoto = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -1339,15 +1346,13 @@ class _POItemRowState extends State<_POItemRow> {
|
|||||||
|
|
||||||
final updated = widget.item.copyWith(
|
final updated = widget.item.copyWith(
|
||||||
weight: weight,
|
weight: weight,
|
||||||
huid: _huidCtrl.text.isEmpty ? null : _huidCtrl.text,
|
|
||||||
unitPrice: rate,
|
unitPrice: rate,
|
||||||
total: total,
|
total: total,
|
||||||
|
huid: _huidCtrl.text.isEmpty ? null : _huidCtrl.text,
|
||||||
);
|
);
|
||||||
widget.onChanged(updated);
|
widget.onChanged(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isUploadingPhoto = false;
|
|
||||||
|
|
||||||
Future<void> _pickPhoto() async {
|
Future<void> _pickPhoto() async {
|
||||||
try {
|
try {
|
||||||
final picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
@@ -1393,6 +1398,10 @@ class _POItemRowState extends State<_POItemRow> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final businessProfile = ref.watch(businessProfileProvider).value;
|
||||||
|
final nature = (businessProfile?.natureOfBusiness ?? businessProfile?.industry ?? 'JEWELLERY').toUpperCase();
|
||||||
|
final isJewellery = nature == 'JEWELLERY';
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
@@ -1534,18 +1543,19 @@ class _POItemRowState extends State<_POItemRow> {
|
|||||||
style: TextStyle(fontSize: 10, color: Colors.grey[700]),
|
style: TextStyle(fontSize: 10, color: Colors.grey[700]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
if (isJewellery)
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
Container(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
color: Colors.amber.withValues(alpha: 0.15),
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(4),
|
color: Colors.amber.withValues(alpha: 0.15),
|
||||||
border: Border.all(color: Colors.amber.withValues(alpha: 0.4)),
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: Colors.amber.withValues(alpha: 0.4)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Purity: ${formatPurity(resolvePurity(categoryPurity: widget.category?.purityFactor, productPurity: widget.product?.purityFactor))}',
|
||||||
|
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.amber.shade900),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
|
||||||
'Purity: ${formatPurity(resolvePurity(categoryPurity: widget.category?.purityFactor, productPurity: widget.product?.purityFactor))}',
|
|
||||||
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.amber.shade900),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1558,83 +1568,174 @@ class _POItemRowState extends State<_POItemRow> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Row(
|
LayoutBuilder(
|
||||||
children: [
|
builder: (context, constraints) {
|
||||||
Expanded(
|
final isWide = constraints.maxWidth >= 600;
|
||||||
flex: 2,
|
|
||||||
child: PremiumTextField(
|
if (isWide) {
|
||||||
controller: _huidCtrl,
|
return Row(
|
||||||
labelText: 'HUID',
|
children: [
|
||||||
textCapitalization: TextCapitalization.characters,
|
if (isJewellery) ...[
|
||||||
onChanged: (_) => _updateItem(),
|
Expanded(
|
||||||
),
|
flex: 2,
|
||||||
),
|
child: PremiumTextField(
|
||||||
const SizedBox(width: 8),
|
controller: _huidCtrl,
|
||||||
Expanded(
|
labelText: 'HUID',
|
||||||
flex: 2,
|
textCapitalization: TextCapitalization.characters,
|
||||||
child: PremiumTextField(
|
onChanged: (_) => _updateItem(),
|
||||||
controller: _weightCtrl,
|
),
|
||||||
labelText: 'Weight/Pcs',
|
),
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
const SizedBox(width: 8),
|
||||||
onChanged: (_) => _updateItem(),
|
],
|
||||||
suffixIcon: (widget.category?.baseUnit != null && widget.category!.baseUnit.isNotEmpty)
|
Expanded(
|
||||||
? Container(
|
flex: isJewellery ? 2 : 3,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
child: PremiumTextField(
|
||||||
decoration: BoxDecoration(
|
controller: _weightCtrl,
|
||||||
color: Colors.grey.withValues(alpha: 0.1),
|
labelText: isJewellery ? 'Weight/Pcs' : 'Weight/Pcs/Length',
|
||||||
borderRadius: const BorderRadius.horizontal(right: Radius.circular(12)),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
border: Border(left: BorderSide(color: Colors.grey.withValues(alpha: 0.3))),
|
onChanged: (_) => _updateItem(),
|
||||||
|
suffixIcon: (widget.category?.baseUnit != null && widget.category!.baseUnit.isNotEmpty)
|
||||||
|
? Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.withValues(alpha: 0.1),
|
||||||
|
borderRadius: const BorderRadius.horizontal(right: Radius.circular(12)),
|
||||||
|
border: Border(left: BorderSide(color: Colors.grey.withValues(alpha: 0.3))),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [Text(widget.category?.baseUnit ?? '', style: TextStyle(color: Colors.grey[700]))],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
flex: isJewellery ? 2 : 3,
|
||||||
|
child: PremiumTextField(
|
||||||
|
controller: _rateCtrl,
|
||||||
|
labelText: 'Rate',
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
onChanged: (_) => _updateItem(),
|
||||||
|
suffixIcon: (widget.category?.baseUnit != null && widget.category!.baseUnit.isNotEmpty)
|
||||||
|
? Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.withValues(alpha: 0.1),
|
||||||
|
borderRadius: const BorderRadius.horizontal(right: Radius.circular(12)),
|
||||||
|
border: Border(left: BorderSide(color: Colors.grey.withValues(alpha: 0.3))),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [Text('per ${widget.category?.baseUnit ?? ''}', style: TextStyle(color: Colors.grey[700]))],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
flex: isJewellery ? 2 : 3,
|
||||||
|
child: PremiumTextField(
|
||||||
|
controller: _totalCtrl,
|
||||||
|
labelText: 'Purchase Amt',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
readOnly: true,
|
||||||
|
onChanged: (_) => _updateItem(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (isJewellery) ...[
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: PremiumTextField(
|
||||||
|
controller: _huidCtrl,
|
||||||
|
labelText: 'HUID',
|
||||||
|
textCapitalization: TextCapitalization.characters,
|
||||||
|
onChanged: (_) => _updateItem(),
|
||||||
),
|
),
|
||||||
child: Column(
|
),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
const SizedBox(width: 8),
|
||||||
mainAxisSize: MainAxisSize.min,
|
],
|
||||||
children: [Text(widget.category?.baseUnit ?? '', style: TextStyle(color: Colors.grey[700]))],
|
Expanded(
|
||||||
),
|
flex: 2,
|
||||||
)
|
child: PremiumTextField(
|
||||||
: null,
|
controller: _weightCtrl,
|
||||||
),
|
labelText: isJewellery ? 'Weight/Pcs' : 'Weight/Pcs/Length',
|
||||||
),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
],
|
onChanged: (_) => _updateItem(),
|
||||||
),
|
suffixIcon: (widget.category?.baseUnit != null && widget.category!.baseUnit.isNotEmpty)
|
||||||
const SizedBox(height: 12),
|
? Container(
|
||||||
Row(
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
children: [
|
decoration: BoxDecoration(
|
||||||
Expanded(
|
color: Colors.grey.withValues(alpha: 0.1),
|
||||||
flex: 2,
|
borderRadius: const BorderRadius.horizontal(right: Radius.circular(12)),
|
||||||
child: PremiumTextField(
|
border: Border(left: BorderSide(color: Colors.grey.withValues(alpha: 0.3))),
|
||||||
controller: _rateCtrl,
|
),
|
||||||
labelText: 'Rate',
|
child: Column(
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
onChanged: (_) => _updateItem(),
|
mainAxisSize: MainAxisSize.min,
|
||||||
suffixIcon: (widget.category?.baseUnit != null && widget.category!.baseUnit.isNotEmpty)
|
children: [Text(widget.category?.baseUnit ?? '', style: TextStyle(color: Colors.grey[700]))],
|
||||||
? Container(
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
)
|
||||||
decoration: BoxDecoration(
|
: null,
|
||||||
color: Colors.grey.withValues(alpha: 0.1),
|
),
|
||||||
borderRadius: const BorderRadius.horizontal(right: Radius.circular(12)),
|
),
|
||||||
border: Border(left: BorderSide(color: Colors.grey.withValues(alpha: 0.3))),
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
const SizedBox(height: 12),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [Text('per ${widget.category?.baseUnit ?? ''}', style: TextStyle(color: Colors.grey[700]))],
|
Expanded(
|
||||||
),
|
flex: 2,
|
||||||
)
|
child: PremiumTextField(
|
||||||
: null,
|
controller: _rateCtrl,
|
||||||
),
|
labelText: 'Rate',
|
||||||
),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
const SizedBox(width: 8),
|
onChanged: (_) => _updateItem(),
|
||||||
Expanded(
|
suffixIcon: (widget.category?.baseUnit != null && widget.category!.baseUnit.isNotEmpty)
|
||||||
flex: 2,
|
? Container(
|
||||||
child: PremiumTextField(
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
controller: _totalCtrl,
|
decoration: BoxDecoration(
|
||||||
labelText: 'Purchase Amt',
|
color: Colors.grey.withValues(alpha: 0.1),
|
||||||
keyboardType: TextInputType.number,
|
borderRadius: const BorderRadius.horizontal(right: Radius.circular(12)),
|
||||||
readOnly: true,
|
border: Border(left: BorderSide(color: Colors.grey.withValues(alpha: 0.3))),
|
||||||
onChanged: (_) => _updateItem(),
|
),
|
||||||
),
|
child: Column(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
],
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [Text('per ${widget.category?.baseUnit ?? ''}', style: TextStyle(color: Colors.grey[700]))],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: PremiumTextField(
|
||||||
|
controller: _totalCtrl,
|
||||||
|
labelText: 'Purchase Amt',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
readOnly: true,
|
||||||
|
onChanged: (_) => _updateItem(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildTaxAndTotal(),
|
_buildTaxAndTotal(),
|
||||||
|
|||||||
Reference in New Issue
Block a user