Tuned ui to support ecommerce product catalogue and purchases.
This commit is contained in:
@@ -21,6 +21,8 @@ class InventoryItem {
|
||||
final int? vendorId;
|
||||
final int? branchId;
|
||||
final String? purchaseRef;
|
||||
final String? salesRef;
|
||||
final double? saleRate;
|
||||
final String? photoUrl;
|
||||
final String? status;
|
||||
final DateTime? createdAt;
|
||||
@@ -48,6 +50,8 @@ class InventoryItem {
|
||||
this.vendorId,
|
||||
this.branchId,
|
||||
this.purchaseRef,
|
||||
this.salesRef,
|
||||
this.saleRate,
|
||||
this.photoUrl,
|
||||
this.status,
|
||||
this.createdAt,
|
||||
@@ -77,6 +81,8 @@ class InventoryItem {
|
||||
vendorId: json['vendorId'],
|
||||
branchId: json['branchId'],
|
||||
purchaseRef: json['purchaseRef'],
|
||||
salesRef: json['salesRef'] ?? json['sales_ref'],
|
||||
saleRate: (json['saleRate'] ?? json['sale_rate'])?.toDouble(),
|
||||
photoUrl: json['photoUrl'] ?? json['photo_url'],
|
||||
status: json['status'],
|
||||
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null,
|
||||
|
||||
@@ -14,6 +14,26 @@ class Product {
|
||||
final String? dimensions;
|
||||
final String? color;
|
||||
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 bool autoCalculatePrice;
|
||||
final double? purityFactor;
|
||||
@@ -39,6 +59,21 @@ class Product {
|
||||
this.dimensions,
|
||||
this.color,
|
||||
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.autoCalculatePrice = false,
|
||||
this.purityFactor,
|
||||
@@ -67,6 +102,21 @@ class Product {
|
||||
dimensions: json['dimensions'],
|
||||
color: json['color'],
|
||||
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:
|
||||
json['priceCalcRule'] ?? json['price_calc_rule'] ?? 'MANUAL',
|
||||
autoCalculatePrice:
|
||||
@@ -116,6 +166,21 @@ class Product {
|
||||
'dimensions': dimensions,
|
||||
'color': color,
|
||||
'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,
|
||||
'autoCalculatePrice': autoCalculatePrice,
|
||||
'purityFactor': purityFactor,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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_provider.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
@@ -307,6 +307,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
String _commodityCode = 'XAU';
|
||||
String _makingChargeType = 'PER_GRAM';
|
||||
String _baseUnit = 'pcs';
|
||||
String _defaultLengthUom = 'cm';
|
||||
|
||||
final List<Map<String, String>> _commodities = [
|
||||
{'code': 'XAU', 'label': 'XAU - Gold'},
|
||||
@@ -325,6 +326,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
|
||||
_baseUnit = isJewellery ? 'g' : 'pcs';
|
||||
_commodityCode = isJewellery ? 'XAU' : 'NONE';
|
||||
_defaultLengthUom = 'cm';
|
||||
|
||||
if (widget.categoryToEdit != null) {
|
||||
final c = widget.categoryToEdit!;
|
||||
@@ -343,8 +345,9 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
_makingChargeType = c.makingChargeType ?? 'PER_GRAM';
|
||||
if (_makingChargeType == 'PER_GM') _makingChargeType = 'PER_GRAM';
|
||||
|
||||
_baseUnit = c.baseUnit ?? (isJewellery ? 'g' : 'pcs');
|
||||
_baseUnit = c.baseUnit;
|
||||
if (_baseUnit == 'gm') _baseUnit = 'g';
|
||||
_defaultLengthUom = c.defaultLengthUom;
|
||||
}
|
||||
} else if (widget.parent != null) {
|
||||
_selectedParentId = widget.parent!.id;
|
||||
@@ -380,6 +383,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
defaultMakingCharge: _isLeaf && isJewellery ? double.tryParse(_makingChargeController.text) : null,
|
||||
makingChargeType: _isLeaf && isJewellery ? _makingChargeType : null,
|
||||
baseUnit: _isLeaf ? _baseUnit : 'pcs',
|
||||
defaultLengthUom: _isLeaf ? _defaultLengthUom : 'cm',
|
||||
);
|
||||
|
||||
if (widget.categoryToEdit != null && widget.categoryToEdit!.id != null) {
|
||||
@@ -398,6 +402,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
final businessProfile = ref.watch(businessProfileProvider).value;
|
||||
final nature = (businessProfile?.natureOfBusiness ?? businessProfile?.industry ?? 'JEWELLERY').toUpperCase();
|
||||
final isJewellery = nature == 'JEWELLERY';
|
||||
final uomsState = ref.watch(uomsProvider);
|
||||
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.85,
|
||||
@@ -623,30 +628,68 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
DropdownButtonFormField<String>(
|
||||
value: _baseUnit,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Base Unit',
|
||||
uomsState.when(
|
||||
loading: () => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
child: LinearProgressIndicator(),
|
||||
),
|
||||
items: [
|
||||
if (isJewellery) ...const [
|
||||
DropdownMenuItem(value: 'g', child: Text('Grams (g)')),
|
||||
DropdownMenuItem(value: 'kg', child: Text('Kilograms (kg)')),
|
||||
DropdownMenuItem(value: 'pcs', child: Text('Pieces (pcs)')),
|
||||
] 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)')),
|
||||
error: (err, stack) => DropdownButtonFormField<String>(
|
||||
value: _baseUnit,
|
||||
decoration: const InputDecoration(labelText: 'Base Unit'),
|
||||
items: [
|
||||
DropdownMenuItem(value: _baseUnit, child: Text(_baseUnit)),
|
||||
],
|
||||
],
|
||||
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),
|
||||
],
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import 'package:flutter/material.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/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/presentation/add_product_screen.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 {
|
||||
final Product product;
|
||||
@@ -70,79 +75,333 @@ class ProductDetailScreen extends ConsumerWidget {
|
||||
}
|
||||
|
||||
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(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Image Header
|
||||
if (p.imageIds.isNotEmpty)
|
||||
Container(
|
||||
height: 200,
|
||||
width: double.infinity,
|
||||
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),
|
||||
child: MaxContentWidth(
|
||||
maxWidth: 900,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Images Header & Gallery
|
||||
if (p.imageIds.isNotEmpty)
|
||||
_buildImagesSection(context, p),
|
||||
|
||||
if (p.imageIds.isNotEmpty)
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Product Details Card
|
||||
Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"Product Details",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
const Divider(),
|
||||
_buildDetailRow("SKU / Barcode", p.sku ?? "-"),
|
||||
_buildDetailRow("HSN Code", p.hsnCode ?? "-"),
|
||||
_buildDetailRow(
|
||||
"GST Rate",
|
||||
"${p.gstRate?.toStringAsFixed(1) ?? '0'}%",
|
||||
),
|
||||
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"),
|
||||
// Product Details Card
|
||||
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.package, size: 20, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
"Product Details",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
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: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/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 '../../transactions/presentation/widgets/attachment_gallery_screen.dart';
|
||||
|
||||
@@ -90,6 +94,10 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
||||
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 commodityRatesState = ref.watch(commodityRatesProvider);
|
||||
|
||||
@@ -97,20 +105,24 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
||||
(c) => c.id == widget.product.categoryId,
|
||||
).firstOrNull;
|
||||
|
||||
final bool isCommodity = isJewellery && category?.commodityCode != null && category!.commodityCode!.isNotEmpty;
|
||||
|
||||
final String unit = category?.baseUnit ?? 'g';
|
||||
final double purityFactor = resolvePurity(categoryPurity: category?.purityFactor, productPurity: widget.product.purityFactor);
|
||||
|
||||
double commodityRate = 0.0;
|
||||
if (category?.commodityCode != null && commodityRatesState.value != null) {
|
||||
if (isCommodity && commodityRatesState.value != null && category.commodityCode != null) {
|
||||
final match = commodityRatesState.value!.where(
|
||||
(r) => r.commodityCode.toUpperCase() == category!.commodityCode!.toUpperCase()
|
||||
(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);
|
||||
final double currentRate = isCommodity
|
||||
? (commodityRate > 0 ? (commodityRate * purityFactor) : (category.dailyRate ?? 0.0))
|
||||
: 0.0;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
@@ -147,6 +159,31 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
||||
final isSold = item.status == 'SOLD';
|
||||
final purchaseOrdersState = ref.watch(purchaseOrdersProvider);
|
||||
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)
|
||||
? item.grossWeight!
|
||||
@@ -202,10 +239,13 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
||||
}
|
||||
|
||||
final purchasePrice = weight * purchaseRate;
|
||||
final currentPrice = weight * currentRate;
|
||||
final gainLoss = currentPrice - purchasePrice;
|
||||
final double displayedSellingRate = effectiveSaleRate > 0 ? effectiveSaleRate : (isCommodity ? currentRate : purchaseRate);
|
||||
final currentPrice = weight * (isSold ? displayedSellingRate : currentRate);
|
||||
final gainLoss = isSold ? ((weight * displayedSellingRate) - purchasePrice) : (currentPrice - purchasePrice);
|
||||
final isGain = gainLoss >= 0;
|
||||
|
||||
final String? salesInvoiceNumber = matchingSaleInvoice?.invoiceNumber ?? item.salesRef;
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
@@ -325,46 +365,91 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
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);
|
||||
if (isSold) ...[
|
||||
if (salesInvoiceNumber != null && salesInvoiceNumber.isNotEmpty)
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
try {
|
||||
var inv = matchingSaleInvoice;
|
||||
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,
|
||||
orElse: () => throw Exception('Purchase invoice not found'),
|
||||
);
|
||||
if (mounted) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
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,
|
||||
},
|
||||
child: Text(
|
||||
'INV: $salesInvoiceNumber',
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
decoration: TextDecoration.underline,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (item.huid != null && item.huid!.isNotEmpty)
|
||||
] else ...[
|
||||
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: const EdgeInsets.only(top: 4.0),
|
||||
padding: const EdgeInsets.only(top: 2.0),
|
||||
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,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1.5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.amber.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Text(
|
||||
'Purity: ${formatPurity(double.tryParse(item.purity ?? '') ?? purityFactor)}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.amber.shade900,
|
||||
if (isCommodity) ...[
|
||||
const SizedBox(height: 3),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1.5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.amber.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Text(
|
||||
'Purity: ${formatPurity(double.tryParse(item.purity ?? '') ?? purityFactor)}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.amber.shade900,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
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)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
if (isCommodity) ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Icon(
|
||||
isGain ? LucideIcons.trendingUp : LucideIcons.trendingDown,
|
||||
color: isGain ? Colors.green : Colors.red,
|
||||
size: 16,
|
||||
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)),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
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('₹${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 String? makingChargeType;
|
||||
final String baseUnit;
|
||||
final String defaultLengthUom;
|
||||
final bool isActive;
|
||||
final int sortOrder;
|
||||
final double? dailyRate;
|
||||
@@ -34,6 +35,7 @@ class ProductCategory {
|
||||
this.defaultMakingCharge,
|
||||
this.makingChargeType,
|
||||
this.baseUnit = 'pcs',
|
||||
this.defaultLengthUom = 'cm',
|
||||
this.isActive = true,
|
||||
this.sortOrder = 0,
|
||||
this.dailyRate,
|
||||
@@ -54,6 +56,7 @@ class ProductCategory {
|
||||
defaultMakingCharge: (json['defaultMakingCharge'] ?? json['default_making_charge'] as num?)?.toDouble(),
|
||||
makingChargeType: json['makingChargeType'] ?? json['making_charge_type'],
|
||||
baseUnit: json['baseUnit'] ?? json['base_unit'] ?? 'pcs',
|
||||
defaultLengthUom: json['defaultLengthUom'] ?? json['default_length_uom'] ?? 'cm',
|
||||
isActive: json['isActive'] ?? json['is_active'] ?? true,
|
||||
sortOrder: json['sortOrder'] ?? json['sort_order'] ?? 0,
|
||||
dailyRate: (json['dailyRate'] ?? json['daily_rate'] as num?)?.toDouble(),
|
||||
@@ -75,6 +78,7 @@ class ProductCategory {
|
||||
'defaultMakingCharge': defaultMakingCharge,
|
||||
'makingChargeType': makingChargeType,
|
||||
'baseUnit': baseUnit,
|
||||
'defaultLengthUom': defaultLengthUom,
|
||||
'isActive': isActive,
|
||||
'sortOrder': sortOrder,
|
||||
};
|
||||
|
||||
@@ -57,11 +57,20 @@ class UomsNotifier extends AsyncNotifier<List<UnitOfMeasure>> {
|
||||
// If empty in database, seed standard units of measure
|
||||
try {
|
||||
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': 'Kilograms', 'abbreviation': 'kg'},
|
||||
{'name': 'Pieces', 'abbreviation': 'pcs'},
|
||||
{'name': 'Carats', 'abbreviation': 'ct'},
|
||||
{'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) {
|
||||
await DioClient().dio.post('/inventory/uom', data: def);
|
||||
|
||||
Reference in New Issue
Block a user