Fixed sales invoice issues in Ecommerce

This commit is contained in:
2026-09-03 08:34:23 +05:30
parent b4772ccf19
commit b123a34e8f
18 changed files with 2275 additions and 453 deletions

View File

@@ -0,0 +1,54 @@
package com.kifi.api.controller.sales;
import com.kifi.api.entity.sales.SalesChannel;
import com.kifi.api.service.sales.SalesChannelService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/kifi-v2/sales-channels")
@RequiredArgsConstructor
public class SalesChannelController {
private final SalesChannelService salesChannelService;
@GetMapping
public Flux<SalesChannel> getSalesChannels(Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return salesChannelService.getActiveChannels(userId);
}
@GetMapping("/all")
public Flux<SalesChannel> getAllSalesChannels(Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return salesChannelService.getAllChannels(userId);
}
@PostMapping
public Mono<SalesChannel> createSalesChannel(
Authentication authentication,
@RequestBody SalesChannel channel) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return salesChannelService.createChannel(userId, channel);
}
@PutMapping("/{id}")
public Mono<SalesChannel> updateSalesChannel(
@PathVariable Long id,
Authentication authentication,
@RequestBody SalesChannel channel) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return salesChannelService.updateChannel(userId, id, channel);
}
@DeleteMapping("/{id}")
public Mono<Void> deleteSalesChannel(
@PathVariable Long id,
Authentication authentication) {
Long userId = Long.valueOf(authentication.getDetails().toString());
return salesChannelService.deleteChannel(userId, id);
}
}

View File

@@ -33,6 +33,9 @@ public class BusinessFeature {
@Column("barcode_source")
private String barcodeSource;
@Column("enable_ecommerce_channels")
private Boolean enableEcommerceChannels;
@Column("created_at")
private LocalDateTime createdAt;
}

View File

@@ -80,6 +80,21 @@ public class Invoice {
@Column("emi_start_date")
private LocalDate emiStartDate;
@Column("sales_channel_id")
private Long salesChannelId;
@Column("sales_channel")
private String salesChannel;
@Column("marketplace_order_id")
private String marketplaceOrderId;
@Column("place_of_supply_state_id")
private Long placeOfSupplyStateId;
@Column("place_of_supply")
private String placeOfSupply;
@Column("created_at")
private LocalDateTime createdAt;

View File

@@ -0,0 +1,43 @@
package com.kifi.api.entity.sales;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table("sales_channels")
public class SalesChannel {
@Id
private Long id;
@Column("user_id")
private Long userId;
private String name;
private String code;
private String icon;
@Column("default_ledger_id")
private Long defaultLedgerId;
@Column("is_active")
@Builder.Default
private Boolean isActive = true;
@Column("created_at")
private LocalDateTime createdAt;
@Column("updated_at")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,18 @@
package com.kifi.api.repository.sales;
import com.kifi.api.entity.sales.SalesChannel;
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface SalesChannelRepository extends ReactiveCrudRepository<SalesChannel, Long> {
@Query("SELECT * FROM sales_channels WHERE (user_id = :userId OR user_id IS NULL) AND is_active = true ORDER BY id ASC")
Flux<SalesChannel> findActiveChannelsForUser(Long userId);
@Query("SELECT * FROM sales_channels WHERE (user_id = :userId OR user_id IS NULL) ORDER BY id ASC")
Flux<SalesChannel> findAllChannelsForUser(Long userId);
Mono<SalesChannel> findByIdAndUserId(Long id, Long userId);
}

View File

@@ -37,6 +37,7 @@ public class BusinessService {
.multiLocation(false)
.bomReductionStrategy("COMPONENTS_ONLY")
.stockDeductionOnInvoice(true)
.enableEcommerceChannels(false)
.createdAt(LocalDateTime.now())
.build());
}
@@ -51,6 +52,7 @@ public class BusinessService {
if (feature.getBomReductionStrategy() != null) existing.setBomReductionStrategy(feature.getBomReductionStrategy());
if (feature.getStockDeductionOnInvoice() != null) existing.setStockDeductionOnInvoice(feature.getStockDeductionOnInvoice());
if (feature.getBarcodeSource() != null) existing.setBarcodeSource(feature.getBarcodeSource());
if (feature.getEnableEcommerceChannels() != null) existing.setEnableEcommerceChannels(feature.getEnableEcommerceChannels());
return businessFeatureRepository.save(existing);
})
.switchIfEmpty(Mono.defer(() -> {

View File

@@ -225,6 +225,16 @@ public class InvoiceService {
if (availItems.isEmpty()) {
return Mono.just(BigDecimal.ZERO);
}
// Strict FIFO: Sort by purchase/creation date & time in ASCENDING order
availItems.sort((a, b) -> {
if (a.getCreatedAt() == null && b.getCreatedAt() == null) {
return Long.compare(a.getId() != null ? a.getId() : 0L, b.getId() != null ? b.getId() : 0L);
}
if (a.getCreatedAt() == null) return 1;
if (b.getCreatedAt() == null) return -1;
int cmp = a.getCreatedAt().compareTo(b.getCreatedAt());
return cmp != 0 ? cmp : Long.compare(a.getId() != null ? a.getId() : 0L, b.getId() != null ? b.getId() : 0L);
});
BigDecimal needed = soldQty;
java.util.List<Mono<Void>> saves = new java.util.ArrayList<>();
BigDecimal totalCost = BigDecimal.ZERO;

View File

@@ -0,0 +1,55 @@
package com.kifi.api.service.sales;
import com.kifi.api.entity.sales.SalesChannel;
import com.kifi.api.repository.sales.SalesChannelRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
@Transactional
public class SalesChannelService {
private final SalesChannelRepository salesChannelRepository;
public Flux<SalesChannel> getActiveChannels(Long userId) {
return salesChannelRepository.findActiveChannelsForUser(userId);
}
public Flux<SalesChannel> getAllChannels(Long userId) {
return salesChannelRepository.findAllChannelsForUser(userId);
}
public Mono<SalesChannel> createChannel(Long userId, SalesChannel channel) {
channel.setUserId(userId);
channel.setCreatedAt(LocalDateTime.now());
channel.setUpdatedAt(LocalDateTime.now());
if (channel.getIsActive() == null) {
channel.setIsActive(true);
}
return salesChannelRepository.save(channel);
}
public Mono<SalesChannel> updateChannel(Long userId, Long id, SalesChannel updated) {
return salesChannelRepository.findByIdAndUserId(id, userId)
.flatMap(existing -> {
if (updated.getName() != null) existing.setName(updated.getName());
if (updated.getCode() != null) existing.setCode(updated.getCode());
if (updated.getIcon() != null) existing.setIcon(updated.getIcon());
if (updated.getDefaultLedgerId() != null) existing.setDefaultLedgerId(updated.getDefaultLedgerId());
if (updated.getIsActive() != null) existing.setIsActive(updated.getIsActive());
existing.setUpdatedAt(LocalDateTime.now());
return salesChannelRepository.save(existing);
});
}
public Mono<Void> deleteChannel(Long userId, Long id) {
return salesChannelRepository.findByIdAndUserId(id, userId)
.flatMap(salesChannelRepository::delete);
}
}

View File

@@ -8,6 +8,7 @@ class BusinessFeature {
final String bomReductionStrategy;
final bool stockDeductionOnInvoice;
final String barcodeSource;
final bool enableEcommerceChannels;
final DateTime? createdAt;
BusinessFeature({
@@ -20,6 +21,7 @@ class BusinessFeature {
this.bomReductionStrategy = 'COMPONENTS_ONLY',
this.stockDeductionOnInvoice = true,
this.barcodeSource = 'SKU',
this.enableEcommerceChannels = false,
this.createdAt,
});
@@ -34,6 +36,7 @@ class BusinessFeature {
bomReductionStrategy: json['bomReductionStrategy'] ?? 'COMPONENTS_ONLY',
stockDeductionOnInvoice: json['stockDeductionOnInvoice'] ?? true,
barcodeSource: json['barcodeSource'] ?? 'SKU',
enableEcommerceChannels: json['enableEcommerceChannels'] ?? json['enable_ecommerce_channels'] ?? false,
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'])
: null,
@@ -51,6 +54,7 @@ class BusinessFeature {
'bomReductionStrategy': bomReductionStrategy,
'stockDeductionOnInvoice': stockDeductionOnInvoice,
'barcodeSource': barcodeSource,
'enableEcommerceChannels': enableEcommerceChannels,
};
}
@@ -64,6 +68,7 @@ class BusinessFeature {
String? bomReductionStrategy,
bool? stockDeductionOnInvoice,
String? barcodeSource,
bool? enableEcommerceChannels,
}) {
return BusinessFeature(
id: id ?? this.id,
@@ -76,6 +81,8 @@ class BusinessFeature {
stockDeductionOnInvoice:
stockDeductionOnInvoice ?? this.stockDeductionOnInvoice,
barcodeSource: barcodeSource ?? this.barcodeSource,
enableEcommerceChannels:
enableEcommerceChannels ?? this.enableEcommerceChannels,
createdAt: createdAt,
);
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../providers/business_provider.dart';
import '../../../sales/presentation/sales_channels_screen.dart';
class BusinessSettingsScreen extends ConsumerStatefulWidget {
const BusinessSettingsScreen({super.key});
@@ -176,6 +177,58 @@ class _BusinessSettingsScreenState
}
},
),
const SizedBox(height: 16),
Builder(
builder: (context) {
final businessProfile = ref.watch(businessProfileProvider).value;
final nature = (businessProfile?.natureOfBusiness ?? businessProfile?.industry ?? 'JEWELLERY').toUpperCase();
final isJewellery = nature == 'JEWELLERY';
final isChannelEnabled = featureState.value?.enableEcommerceChannels ?? (!isJewellery);
return SwitchListTile(
title: const Text('Enable E-Commerce & Marketplace Channels'),
subtitle: const Text(
'Track multi-channel sales across Amazon, Flipkart, Shopify, etc. in Invoices.',
),
value: isChannelEnabled,
secondary: const Icon(LucideIcons.shoppingBag),
onChanged: (val) {
if (featureState.value != null) {
final updated = featureState.value!.copyWith(
enableEcommerceChannels: val,
);
ref
.read(businessFeatureProvider.notifier)
.updateFeatures(updated);
}
},
);
},
),
Consumer(
builder: (context, ref, child) {
final featureState = ref.watch(businessFeatureProvider);
final businessProfile = ref.watch(businessProfileProvider).value;
final nature = (businessProfile?.natureOfBusiness ?? businessProfile?.industry ?? 'JEWELLERY').toUpperCase();
final isJewellery = nature == 'JEWELLERY';
final isChannelEnabled = featureState.value?.enableEcommerceChannels ?? (!isJewellery);
if (!isChannelEnabled) return const SizedBox.shrink();
return ListTile(
leading: const Icon(LucideIcons.store, color: Colors.blue),
title: const Text('Manage Sales Channels', style: TextStyle(fontWeight: FontWeight.w600)),
subtitle: const Text('Configure marketplace channels and custom platforms'),
trailing: const Icon(LucideIcons.chevronRight, size: 18),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SalesChannelsScreen()),
);
},
);
},
),
],
);
},

View File

@@ -192,6 +192,11 @@ class Invoice {
final double? emiAmount;
final String? emiCycle;
final DateTime? emiStartDate;
final int? salesChannelId;
final String? salesChannel;
final String? marketplaceOrderId;
final int? placeOfSupplyStateId;
final String? placeOfSupply;
final List<InvoiceItem> items;
final List<InvoicePayment>? payments;
@@ -219,6 +224,11 @@ class Invoice {
this.emiAmount,
this.emiCycle,
this.emiStartDate,
this.salesChannelId,
this.salesChannel,
this.marketplaceOrderId,
this.placeOfSupplyStateId,
this.placeOfSupply,
this.items = const [],
this.payments,
});
@@ -254,6 +264,11 @@ class Invoice {
emiStartDate: json['emiStartDate'] != null
? DateTime.parse(json['emiStartDate'])
: (json['emi_start_date'] != null ? DateTime.parse(json['emi_start_date']) : null),
salesChannelId: json['salesChannelId'] ?? json['sales_channel_id'],
salesChannel: json['salesChannel'] ?? json['sales_channel'],
marketplaceOrderId: json['marketplaceOrderId'] ?? json['marketplace_order_id'],
placeOfSupplyStateId: json['placeOfSupplyStateId'] ?? json['place_of_supply_state_id'],
placeOfSupply: json['placeOfSupply'] ?? json['place_of_supply'],
items: json['items'] != null
? (json['items'] as List).map((i) => InvoiceItem.fromJson(i)).toList()
: [],
@@ -296,6 +311,11 @@ class Invoice {
if (emiStartDate != null) {
data['emiStartDate'] = emiStartDate!.toIso8601String().split('T')[0];
}
if (salesChannelId != null) data['salesChannelId'] = salesChannelId;
if (salesChannel != null) data['salesChannel'] = salesChannel;
if (marketplaceOrderId != null) data['marketplaceOrderId'] = marketplaceOrderId;
if (placeOfSupplyStateId != null) data['placeOfSupplyStateId'] = placeOfSupplyStateId;
if (placeOfSupply != null) data['placeOfSupply'] = placeOfSupply;
data['items'] = items.map((i) => i.toJson()).toList();
if (payments != null) {
data['payments'] = payments!.map((i) => i.toJson()).toList();

View File

@@ -0,0 +1,49 @@
class SalesChannel {
final int? id;
final int? userId;
final String name;
final String? code;
final String? icon;
final int? defaultLedgerId;
final bool isActive;
final DateTime? createdAt;
final DateTime? updatedAt;
SalesChannel({
this.id,
this.userId,
required this.name,
this.code,
this.icon,
this.defaultLedgerId,
this.isActive = true,
this.createdAt,
this.updatedAt,
});
factory SalesChannel.fromJson(Map<String, dynamic> json) {
return SalesChannel(
id: json['id'],
userId: json['userId'] ?? json['user_id'],
name: json['name'] ?? '',
code: json['code'],
icon: json['icon'],
defaultLedgerId: json['defaultLedgerId'] ?? json['default_ledger_id'],
isActive: json['isActive'] ?? json['is_active'] ?? true,
createdAt: json['createdAt'] != null ? DateTime.tryParse(json['createdAt']) : null,
updatedAt: json['updatedAt'] != null ? DateTime.tryParse(json['updatedAt']) : null,
);
}
Map<String, dynamic> toJson() {
return {
if (id != null) 'id': id,
if (userId != null) 'userId': userId,
'name': name,
if (code != null) 'code': code,
if (icon != null) 'icon': icon,
if (defaultLedgerId != null) 'defaultLedgerId': defaultLedgerId,
'isActive': isActive,
};
}
}

View File

@@ -12,6 +12,8 @@ import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart';
import '../../inventory/providers/products_provider.dart';
import '../../inventory/providers/product_categories_provider.dart';
import '../../inventory/providers/uoms_provider.dart';
import '../../inventory/domain/product.dart';
import '../domain/invoice.dart';
import '../providers/invoices_provider.dart';
import '../providers/customers_provider.dart';
@@ -21,6 +23,46 @@ import '../../../core/network/dio_client.dart';
import 'widgets/receive_payment_sheet.dart';
import 'invoice_builder_screen.dart';
bool _isCommodityProduct({
ProductCategory? category,
Product? product,
InvoiceItem? invoiceItem,
String? commodityCode,
String? huid,
}) {
final h = huid ?? invoiceItem?.huid;
if (h != null && h.trim().isNotEmpty) return true;
final code = commodityCode ??
category?.commodityCode ??
invoiceItem?.commodityCode;
if (code != null && code.trim().isNotEmpty && code.trim().toUpperCase() != 'NONE') {
return true;
}
return false;
}
String _resolveProductUnit({
required Product? product,
required ProductCategory? category,
required List<UnitOfMeasure> uoms,
bool isCommodity = false,
}) {
if (isCommodity) {
return (category != null && category.baseUnit.isNotEmpty) ? category.baseUnit : 'g';
}
if (product?.uomId != null) {
final uom = uoms.where((u) => u.id == product!.uomId).firstOrNull;
if (uom != null && uom.abbreviation != null && uom.abbreviation!.isNotEmpty) return uom.abbreviation!;
if (uom != null && uom.name.isNotEmpty) return uom.name;
}
if (category != null && category.baseUnit.isNotEmpty) {
return category.baseUnit;
}
return 'pcs';
}
String _resolveImageUrl(String path) {
if (path.startsWith('http://') || path.startsWith('https://')) {
return path;
@@ -185,10 +227,45 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
const Text('INVOICE NO', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey)),
const SizedBox(height: 2),
Text(latestInvoice.invoiceNumber, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: Colors.blue)),
if (latestInvoice.salesChannel != null && latestInvoice.salesChannel!.isNotEmpty) ...[
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.blue.withValues(alpha: 0.25)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(LucideIcons.shoppingBag, size: 10, color: Colors.blue),
const SizedBox(width: 4),
Text(
latestInvoice.salesChannel!,
style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.blue),
),
],
),
),
],
if (latestInvoice.marketplaceOrderId != null && latestInvoice.marketplaceOrderId!.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
'Ref: ${latestInvoice.marketplaceOrderId}',
style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: Colors.grey.shade700),
),
],
const SizedBox(height: 10),
const Text('INVOICE DATE', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey)),
const SizedBox(height: 2),
Text(formatDate.format(latestInvoice.issueDate), style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
if (latestInvoice.placeOfSupply != null && latestInvoice.placeOfSupply!.isNotEmpty) ...[
const SizedBox(height: 8),
const Text('PLACE OF SUPPLY', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey)),
const SizedBox(height: 2),
Text(latestInvoice.placeOfSupply!, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600)),
],
],
),
),
@@ -233,19 +310,33 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
// Item Cards
...(latestInvoice.items).map((item) {
final product = products.where((p) => p.id == item.productId).firstOrNull;
final category = categories.where((c) => c.id == product?.categoryId).firstOrNull;
final weight = item.weight ?? item.quantity;
final metalAmount = weight * item.unitPrice;
final category = categories.where((c) => c.name == item.categoryName || c.id == product?.categoryId).firstOrNull;
final uoms = ref.watch(uomsProvider).value ?? [];
final isItemCommodity = _isCommodityProduct(
category: category,
product: product,
invoiceItem: item,
);
final qtyOrWeight = item.weight != null && item.weight! > 0 ? item.weight! : (item.quantity > 0 ? item.quantity : 1.0);
final baseAmount = qtyOrWeight * item.unitPrice;
final unit = _resolveProductUnit(product: product, category: category, uoms: uoms, isCommodity: isItemCommodity);
double makingChargeAmt = 0.0;
if (item.makingChargesType == 'PERCENTAGE') {
makingChargeAmt = metalAmount * (item.makingCharge / 100.0);
} else if (item.makingChargesType == 'PER_PIECE') {
makingChargeAmt = item.makingCharge;
} else {
makingChargeAmt = weight * item.makingCharge;
if (isItemCommodity) {
if (item.makingChargesType == 'PERCENTAGE') {
makingChargeAmt = baseAmount * (item.makingCharge / 100.0);
} else if (item.makingChargesType == 'PER_PIECE') {
makingChargeAmt = item.makingCharge;
} else {
makingChargeAmt = qtyOrWeight * item.makingCharge;
}
}
final taxableAmount = baseAmount + makingChargeAmt + item.otherCharges - item.discount;
final taxAmount = (taxableAmount * item.taxRate) / 100.0;
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
@@ -278,7 +369,7 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.productName ?? product?.name ?? item.description ?? 'Jewellery Item',
item.productName ?? product?.name ?? item.description ?? 'Item',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
),
const SizedBox(height: 4),
@@ -290,10 +381,12 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
_buildDetailBadge(item.categoryName ?? category!.name, const Color(0xFFD4AF37)),
if (item.sku != null || product?.sku != null)
_buildDetailBadge('SKU: ${item.sku ?? product!.sku}', Colors.blue),
if (item.huid != null)
if (isItemCommodity && item.huid != null && item.huid!.isNotEmpty)
_buildDetailBadge('HUID: ${item.huid}', Colors.purple),
if (item.hsnCode != null)
if (item.hsnCode != null && item.hsnCode!.isNotEmpty)
_buildDetailBadge('HSN: ${item.hsnCode}', Colors.grey),
if (!isItemCommodity)
_buildDetailBadge('Unit: $unit', Colors.teal),
],
),
],
@@ -306,27 +399,52 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
],
),
const Divider(height: 18),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Metal (${weight.toStringAsFixed(3)}g @ ₹${item.unitPrice.toStringAsFixed(2)}/g):',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
if (isItemCommodity) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Metal (${qtyOrWeight.toStringAsFixed(3)}$unit @ ₹${item.unitPrice.toStringAsFixed(2)}/$unit):',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
Text(formatCurrency.format(baseAmount), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12)),
],
),
if (makingChargeAmt > 0) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Making Charges (${item.makingChargesType == 'PERCENTAGE' ? '${item.makingCharge}%' : (item.makingChargesType == 'PER_PIECE' ? '${item.makingCharge}/pc' : '${item.makingCharge}/g')}):',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
Text(formatCurrency.format(makingChargeAmt), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12, color: Colors.indigo)),
],
),
Text(formatCurrency.format(metalAmount), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12)),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Making Charges (${item.makingChargesType == 'PERCENTAGE' ? '${item.makingCharge}%' : (item.makingChargesType == 'PER_PIECE' ? '${item.makingCharge}/pc' : '${item.makingCharge}/g')}):',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
Text(formatCurrency.format(makingChargeAmt), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12, color: Colors.indigo)),
],
),
] else ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Rate: ₹${item.unitPrice.toStringAsFixed(2)} / $unit • Qty: ${qtyOrWeight.toStringAsFixed(qtyOrWeight.truncateToDouble() == qtyOrWeight ? 0 : 2)} $unit',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600, fontWeight: FontWeight.w500),
),
Text(formatCurrency.format(baseAmount), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12)),
],
),
],
if (item.discount > 0) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Discount:', style: TextStyle(fontSize: 11, color: Colors.red)),
Text('- ${formatCurrency.format(item.discount)}', style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.red)),
],
),
],
if (item.taxRate > 0) ...[
const SizedBox(height: 4),
Row(
@@ -334,7 +452,7 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
children: [
Text('GST (${item.taxRate.toStringAsFixed(1)}%):', style: TextStyle(fontSize: 11, color: Colors.grey.shade500)),
Text(
'+ ${formatCurrency.format(((metalAmount + makingChargeAmt) * (item.taxRate / 100.0)))}',
'+ ${formatCurrency.format(taxAmount)}',
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
),
],
@@ -556,6 +674,7 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
final business = ref.read(businessProfileProvider).value;
final products = ref.read(productsProvider).value ?? [];
final categories = ref.read(productCategoriesProvider).value ?? [];
final uoms = ref.read(uomsProvider).value ?? [];
final formatCurrency = NumberFormat.currency(symbol: 'Rs. ', decimalDigits: 2);
final formatDate = DateFormat('dd MMM yyyy');
@@ -776,6 +895,19 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
final groupTotal = group.fold(0.0, (sum, i) => sum + i.total);
final gstRate = group.first.taxRate > 0 ? group.first.taxRate : (product?.gstRate ?? (category?.defaultGst ?? 0.0));
final isGroupCommodity = group.any((item) => _isCommodityProduct(
category: category,
product: product,
invoiceItem: item,
));
final groupUnit = _resolveProductUnit(
product: product,
category: category,
uoms: uoms,
isCommodity: isGroupCommodity,
);
return pw.Container(
margin: const pw.EdgeInsets.only(bottom: 14),
decoration: pw.BoxDecoration(
@@ -839,22 +971,40 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
style: const pw.TextStyle(fontSize: 7.5, color: PdfColors.grey800),
),
),
pw.Container(
padding: const pw.EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: pw.BoxDecoration(
color: PdfColors.amber50,
border: pw.Border.all(color: PdfColors.amber600, width: 0.5),
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(3)),
),
child: pw.Text(
'Purity: $purityStr',
style: pw.TextStyle(
fontSize: 7.5,
fontWeight: pw.FontWeight.bold,
color: PdfColors.brown800,
if (isGroupCommodity)
pw.Container(
padding: const pw.EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: pw.BoxDecoration(
color: PdfColors.amber50,
border: pw.Border.all(color: PdfColors.amber600, width: 0.5),
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(3)),
),
child: pw.Text(
'Purity: $purityStr',
style: pw.TextStyle(
fontSize: 7.5,
fontWeight: pw.FontWeight.bold,
color: PdfColors.brown800,
),
),
),
if (!isGroupCommodity)
pw.Container(
padding: const pw.EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: pw.BoxDecoration(
color: PdfColors.teal50,
border: pw.Border.all(color: PdfColors.teal400, width: 0.5),
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(3)),
),
child: pw.Text(
'Unit: $groupUnit',
style: pw.TextStyle(
fontSize: 7.5,
fontWeight: pw.FontWeight.bold,
color: PdfColors.teal900,
),
),
),
),
if (hsnStr != '-')
pw.Container(
padding: const pw.EdgeInsets.symmetric(horizontal: 6, vertical: 2),
@@ -900,49 +1050,71 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
4: pw.Alignment.centerRight,
5: pw.Alignment.centerRight,
},
headers: [
'Item / HUID',
'Weight / Qty',
'Rate',
'Making Chg',
'Other Chg',
'Total Amt',
],
data: group.map((item) {
String particular = '';
if (item.huid != null && item.huid!.isNotEmpty) {
particular = 'HUID: ${item.huid}';
} else if (item.sku != null && item.sku!.isNotEmpty) {
particular = 'SKU: ${item.sku}';
} else {
particular = 'Standard Item';
}
headers: isGroupCommodity
? [
'Item / HUID',
'Weight / Qty',
'Rate',
'Making Chg',
'Other Chg',
'Total Amt',
]
: [
'Item / SKU',
'Qty',
'Unit Price',
'Discount',
'GST %',
'Total Amt',
],
data: isGroupCommodity
? group.map((item) {
String particular = '';
if (item.huid != null && item.huid!.isNotEmpty) {
particular = 'HUID: ${item.huid}';
} else if (item.sku != null && item.sku!.isNotEmpty) {
particular = 'SKU: ${item.sku}';
} else {
particular = 'Standard Item';
}
final weight = item.weight ?? item.quantity;
final weightQty = item.weight != null && item.weight! > 0
? '${item.weight!.toStringAsFixed(item.weight!.truncateToDouble() == item.weight ? 0 : 3)} g'
: '${item.quantity.toStringAsFixed(item.quantity.truncateToDouble() == item.quantity ? 0 : 0)} pcs';
final weight = item.weight ?? item.quantity;
final weightQty = item.weight != null && item.weight! > 0
? '${item.weight!.toStringAsFixed(item.weight!.truncateToDouble() == item.weight ? 0 : 3)} $groupUnit'
: '${item.quantity.toStringAsFixed(item.quantity.truncateToDouble() == item.quantity ? 0 : 0)} pcs';
final rateUnit = item.weight != null && item.weight! > 0 ? '/ g' : '/ pc';
final rateUnit = item.weight != null && item.weight! > 0 ? '/ $groupUnit' : '/ pc';
double makingAmt = 0.0;
if (item.makingChargesType == 'PERCENTAGE') {
makingAmt = (weight * item.unitPrice) * (item.makingCharge / 100.0);
} else if (item.makingChargesType == 'PER_PIECE') {
makingAmt = item.makingCharge;
} else {
makingAmt = weight * item.makingCharge;
}
double makingAmt = 0.0;
if (item.makingChargesType == 'PERCENTAGE') {
makingAmt = (weight * item.unitPrice) * (item.makingCharge / 100.0);
} else if (item.makingChargesType == 'PER_PIECE') {
makingAmt = item.makingCharge;
} else {
makingAmt = weight * item.makingCharge;
}
return [
particular,
weightQty,
'${formatCurrency.format(item.unitPrice)} $rateUnit',
makingAmt > 0 ? formatCurrency.format(makingAmt) : '-',
item.otherCharges > 0 ? formatCurrency.format(item.otherCharges) : '-',
formatCurrency.format(item.total),
];
}).toList(),
return [
particular,
weightQty,
'${formatCurrency.format(item.unitPrice)} $rateUnit',
makingAmt > 0 ? formatCurrency.format(makingAmt) : '-',
item.otherCharges > 0 ? formatCurrency.format(item.otherCharges) : '-',
formatCurrency.format(item.total),
];
}).toList()
: group.map((item) {
final qty = item.weight != null && item.weight! > 0 ? item.weight! : (item.quantity > 0 ? item.quantity : 1.0);
final qtyStr = '${qty.toStringAsFixed(qty.truncateToDouble() == qty ? 0 : 2)} $groupUnit';
return [
item.sku ?? skuStr ?? 'Item',
qtyStr,
formatCurrency.format(item.unitPrice),
item.discount > 0 ? '-${formatCurrency.format(item.discount)}' : '-',
'${item.taxRate.toStringAsFixed(1)}%',
formatCurrency.format(item.total),
];
}).toList(),
),
// Group Subtotal & GST Calculation Box
@@ -959,7 +1131,9 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text(
'Metal Subtotal: ${formatCurrency.format(groupSubtotal)} | Making: ${formatCurrency.format(groupMaking)}',
isGroupCommodity
? 'Metal Subtotal: ${formatCurrency.format(groupSubtotal)} | Making: ${formatCurrency.format(groupMaking)}'
: 'Items Subtotal: ${formatCurrency.format(groupSubtotal)}',
style: const pw.TextStyle(fontSize: 8, color: PdfColors.grey700),
),
pw.Text(

View File

@@ -198,11 +198,35 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
invoice.invoiceNumber,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
Expanded(
child: Row(
children: [
Flexible(
child: Text(
invoice.invoiceNumber,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
overflow: TextOverflow.ellipsis,
),
),
if (invoice.salesChannel != null && invoice.salesChannel!.isNotEmpty) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.blue.withValues(alpha: 0.25)),
),
child: Text(
invoice.salesChannel!,
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.blue),
),
),
],
],
),
),
Container(

View File

@@ -0,0 +1,278 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../domain/sales_channel.dart';
import '../providers/sales_channels_provider.dart';
class SalesChannelsScreen extends ConsumerStatefulWidget {
const SalesChannelsScreen({super.key});
@override
ConsumerState<SalesChannelsScreen> createState() => _SalesChannelsScreenState();
}
class _SalesChannelsScreenState extends ConsumerState<SalesChannelsScreen> {
void _showAddEditChannelSheet([SalesChannel? channel]) {
final nameCtrl = TextEditingController(text: channel?.name ?? '');
final codeCtrl = TextEditingController(text: channel?.code ?? '');
final isEditing = channel != null;
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(ctx).viewInsets.bottom + 20,
left: 20,
right: 20,
top: 20,
),
decoration: BoxDecoration(
color: Theme.of(ctx).scaffoldBackgroundColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
isEditing ? 'Edit Sales Channel' : 'Add Sales Channel',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(LucideIcons.x),
onPressed: () => Navigator.pop(ctx),
),
],
),
const SizedBox(height: 16),
TextField(
controller: nameCtrl,
decoration: const InputDecoration(
labelText: 'Channel Name *',
hintText: 'e.g. Nykaa, Myntra, Tata CLiQ, Etsy',
border: OutlineInputBorder(),
prefixIcon: Icon(LucideIcons.store),
),
),
const SizedBox(height: 16),
TextField(
controller: codeCtrl,
decoration: const InputDecoration(
labelText: 'Channel Code',
hintText: 'e.g. NYKAA, MYNTRA, ETSY',
border: OutlineInputBorder(),
prefixIcon: Icon(LucideIcons.tag),
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 48,
child: FilledButton(
onPressed: () async {
final name = nameCtrl.text.trim();
if (name.isEmpty) return;
Navigator.pop(ctx);
try {
if (isEditing) {
await ref.read(salesChannelsProvider.notifier).updateChannel(
channel.id!,
SalesChannel(
id: channel.id,
name: name,
code: codeCtrl.text.trim().isNotEmpty
? codeCtrl.text.trim().toUpperCase()
: null,
icon: channel.icon,
isActive: channel.isActive,
),
);
} else {
await ref.read(salesChannelsProvider.notifier).createChannel(
SalesChannel(
name: name,
code: codeCtrl.text.trim().isNotEmpty
? codeCtrl.text.trim().toUpperCase()
: null,
icon: 'shopping-bag',
isActive: true,
),
);
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(isEditing
? 'Channel updated successfully'
: 'Channel created successfully'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e'), backgroundColor: Colors.red),
);
}
}
},
child: Text(isEditing ? 'Save Changes' : 'Add Channel'),
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
final channelsState = ref.watch(salesChannelsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Sales Channels & Marketplaces'),
actions: [
IconButton(
icon: const Icon(LucideIcons.plus),
tooltip: 'Add Sales Channel',
onPressed: () => _showAddEditChannelSheet(),
),
],
),
body: channelsState.when(
data: (channels) {
if (channels.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(LucideIcons.shoppingBag, size: 48, color: Colors.grey),
const SizedBox(height: 16),
const Text(
'No sales channels found',
style: TextStyle(color: Colors.grey, fontSize: 16),
),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: () => _showAddEditChannelSheet(),
icon: const Icon(LucideIcons.plus),
label: const Text('Add First Channel'),
),
],
),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: channels.length,
itemBuilder: (context, index) {
final channel = channels[index];
IconData iconData = LucideIcons.store;
if (channel.code == 'AMAZON' || channel.name.toLowerCase().contains('amazon')) {
iconData = LucideIcons.shoppingCart;
} else if (channel.code == 'FLIPKART' || channel.name.toLowerCase().contains('flipkart')) {
iconData = LucideIcons.package;
} else if (channel.code == 'SHOPIFY' || channel.name.toLowerCase().contains('shopify')) {
iconData = LucideIcons.globe;
} else if (channel.code == 'MEESHO' || channel.name.toLowerCase().contains('meesho')) {
iconData = LucideIcons.tag;
} else if (channel.code == 'QUICK_COMMERCE' || channel.name.toLowerCase().contains('quick')) {
iconData = LucideIcons.zap;
}
final isSystemDefault = channel.userId == null;
return Card(
elevation: 0,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.grey.withValues(alpha: 0.2)),
),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.blue.withValues(alpha: 0.1),
child: Icon(iconData, color: Colors.blue, size: 20),
),
title: Row(
children: [
Text(
channel.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
if (isSystemDefault) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.grey.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'System',
style: TextStyle(fontSize: 10, color: Colors.grey, fontWeight: FontWeight.bold),
),
),
],
],
),
subtitle: channel.code != null
? Text('Code: ${channel.code}', style: const TextStyle(fontSize: 12))
: null,
trailing: isSystemDefault
? const Icon(LucideIcons.lock, size: 16, color: Colors.grey)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(LucideIcons.edit2, size: 18),
onPressed: () => _showAddEditChannelSheet(channel),
),
IconButton(
icon: const Icon(LucideIcons.trash2, size: 18, color: Colors.red),
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Delete Channel?'),
content: Text('Are you sure you want to delete ${channel.name}?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Delete', style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirm == true) {
await ref.read(salesChannelsProvider.notifier).deleteChannel(channel.id!);
}
},
),
],
),
),
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error: $e')),
),
);
}
}

View File

@@ -0,0 +1,74 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/network/dio_client.dart';
import '../domain/sales_channel.dart';
class SalesChannelsNotifier extends AsyncNotifier<List<SalesChannel>> {
@override
FutureOr<List<SalesChannel>> build() async {
return _fetchChannels();
}
Future<List<SalesChannel>> _fetchChannels() async {
try {
final response = await DioClient().dio.get('/sales-channels');
if (response.statusCode == 200) {
final List<dynamic> data = response.data;
return data.map((e) => SalesChannel.fromJson(e)).toList();
}
return [];
} catch (e) {
print('Error fetching sales channels: $e');
return [];
}
}
Future<void> refresh() async {
state = const AsyncValue.loading();
try {
final channels = await _fetchChannels();
state = AsyncValue.data(channels);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
}
}
Future<SalesChannel?> createChannel(SalesChannel channel) async {
try {
final response = await DioClient().dio.post('/sales-channels', data: channel.toJson());
await refresh();
if (response.data != null) {
return SalesChannel.fromJson(response.data);
}
return null;
} catch (e) {
throw Exception('Failed to create sales channel: $e');
}
}
Future<SalesChannel?> updateChannel(int id, SalesChannel channel) async {
try {
final response = await DioClient().dio.put('/sales-channels/$id', data: channel.toJson());
await refresh();
if (response.data != null) {
return SalesChannel.fromJson(response.data);
}
return null;
} catch (e) {
throw Exception('Failed to update sales channel: $e');
}
}
Future<void> deleteChannel(int id) async {
try {
await DioClient().dio.delete('/sales-channels/$id');
await refresh();
} catch (e) {
throw Exception('Failed to delete sales channel: $e');
}
}
}
final salesChannelsProvider = AsyncNotifierProvider<SalesChannelsNotifier, List<SalesChannel>>(() {
return SalesChannelsNotifier();
});

View File

@@ -601,6 +601,7 @@ class _PurchaseOrderBuilderScreenState
const SizedBox(height: 12),
if (_items.isEmpty)
Container(
width: double.infinity,
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1E293B) : Colors.grey.shade100,
@@ -608,6 +609,8 @@ class _PurchaseOrderBuilderScreenState
border: Border.all(color: Colors.grey.withValues(alpha: 0.2)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(LucideIcons.packageOpen, size: 48, color: Colors.grey.shade400),
const SizedBox(height: 12),
@@ -807,6 +810,22 @@ class _PurchaseOrderBuilderScreenState
final effectiveTaxable = (rawSubtotal - discountAmount).clamp(0.0, double.infinity);
final grandTotal = effectiveTaxable + taxTotal;
final effectiveGstRate = (effectiveTaxable > 0 && taxTotal > 0)
? (taxTotal / effectiveTaxable) * 100.0
: (_items.isNotEmpty ? _items.first.gstRate : (isJewellery ? 3.0 : 18.0));
final cgstRate = effectiveGstRate / 2.0;
final sgstRate = effectiveGstRate / 2.0;
final igstRate = effectiveGstRate;
String formatRate(double rate) {
if (rate <= 0) return '0%';
final rounded = (rate * 100).round() / 100;
if (rounded.truncateToDouble() == rounded) {
return '${rounded.toInt()}%';
}
return '${rounded.toStringAsFixed(1)}%';
}
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
@@ -848,7 +867,7 @@ class _PurchaseOrderBuilderScreenState
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('CGST (${(taxTotal > 0 ? '1.5%' : '0%')}):', style: TextStyle(color: Colors.grey.shade600)),
Text('CGST (${formatRate(cgstRate)}):', style: TextStyle(color: Colors.grey.shade600)),
Text('${(taxTotal / 2.0).toStringAsFixed(2)}'),
],
),
@@ -856,7 +875,7 @@ class _PurchaseOrderBuilderScreenState
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('SGST (${(taxTotal > 0 ? '1.5%' : '0%')}):', style: TextStyle(color: Colors.grey.shade600)),
Text('SGST (${formatRate(sgstRate)}):', style: TextStyle(color: Colors.grey.shade600)),
Text('${(taxTotal / 2.0).toStringAsFixed(2)}'),
],
),
@@ -864,7 +883,7 @@ class _PurchaseOrderBuilderScreenState
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('IGST (${(taxTotal > 0 ? '3.0%' : '0%')}):', style: TextStyle(color: Colors.grey.shade600)),
Text('IGST (${formatRate(igstRate)}):', style: TextStyle(color: Colors.grey.shade600)),
Text('${taxTotal.toStringAsFixed(2)}'),
],
),