Fixed sales invoice issues in Ecommerce
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,9 @@ public class BusinessFeature {
|
|||||||
@Column("barcode_source")
|
@Column("barcode_source")
|
||||||
private String barcodeSource;
|
private String barcodeSource;
|
||||||
|
|
||||||
|
@Column("enable_ecommerce_channels")
|
||||||
|
private Boolean enableEcommerceChannels;
|
||||||
|
|
||||||
@Column("created_at")
|
@Column("created_at")
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,21 @@ public class Invoice {
|
|||||||
@Column("emi_start_date")
|
@Column("emi_start_date")
|
||||||
private LocalDate emiStartDate;
|
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")
|
@Column("created_at")
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ public class BusinessService {
|
|||||||
.multiLocation(false)
|
.multiLocation(false)
|
||||||
.bomReductionStrategy("COMPONENTS_ONLY")
|
.bomReductionStrategy("COMPONENTS_ONLY")
|
||||||
.stockDeductionOnInvoice(true)
|
.stockDeductionOnInvoice(true)
|
||||||
|
.enableEcommerceChannels(false)
|
||||||
.createdAt(LocalDateTime.now())
|
.createdAt(LocalDateTime.now())
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
@@ -51,6 +52,7 @@ public class BusinessService {
|
|||||||
if (feature.getBomReductionStrategy() != null) existing.setBomReductionStrategy(feature.getBomReductionStrategy());
|
if (feature.getBomReductionStrategy() != null) existing.setBomReductionStrategy(feature.getBomReductionStrategy());
|
||||||
if (feature.getStockDeductionOnInvoice() != null) existing.setStockDeductionOnInvoice(feature.getStockDeductionOnInvoice());
|
if (feature.getStockDeductionOnInvoice() != null) existing.setStockDeductionOnInvoice(feature.getStockDeductionOnInvoice());
|
||||||
if (feature.getBarcodeSource() != null) existing.setBarcodeSource(feature.getBarcodeSource());
|
if (feature.getBarcodeSource() != null) existing.setBarcodeSource(feature.getBarcodeSource());
|
||||||
|
if (feature.getEnableEcommerceChannels() != null) existing.setEnableEcommerceChannels(feature.getEnableEcommerceChannels());
|
||||||
return businessFeatureRepository.save(existing);
|
return businessFeatureRepository.save(existing);
|
||||||
})
|
})
|
||||||
.switchIfEmpty(Mono.defer(() -> {
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
|
|||||||
@@ -225,6 +225,16 @@ public class InvoiceService {
|
|||||||
if (availItems.isEmpty()) {
|
if (availItems.isEmpty()) {
|
||||||
return Mono.just(BigDecimal.ZERO);
|
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;
|
BigDecimal needed = soldQty;
|
||||||
java.util.List<Mono<Void>> saves = new java.util.ArrayList<>();
|
java.util.List<Mono<Void>> saves = new java.util.ArrayList<>();
|
||||||
BigDecimal totalCost = BigDecimal.ZERO;
|
BigDecimal totalCost = BigDecimal.ZERO;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ class BusinessFeature {
|
|||||||
final String bomReductionStrategy;
|
final String bomReductionStrategy;
|
||||||
final bool stockDeductionOnInvoice;
|
final bool stockDeductionOnInvoice;
|
||||||
final String barcodeSource;
|
final String barcodeSource;
|
||||||
|
final bool enableEcommerceChannels;
|
||||||
final DateTime? createdAt;
|
final DateTime? createdAt;
|
||||||
|
|
||||||
BusinessFeature({
|
BusinessFeature({
|
||||||
@@ -20,6 +21,7 @@ class BusinessFeature {
|
|||||||
this.bomReductionStrategy = 'COMPONENTS_ONLY',
|
this.bomReductionStrategy = 'COMPONENTS_ONLY',
|
||||||
this.stockDeductionOnInvoice = true,
|
this.stockDeductionOnInvoice = true,
|
||||||
this.barcodeSource = 'SKU',
|
this.barcodeSource = 'SKU',
|
||||||
|
this.enableEcommerceChannels = false,
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ class BusinessFeature {
|
|||||||
bomReductionStrategy: json['bomReductionStrategy'] ?? 'COMPONENTS_ONLY',
|
bomReductionStrategy: json['bomReductionStrategy'] ?? 'COMPONENTS_ONLY',
|
||||||
stockDeductionOnInvoice: json['stockDeductionOnInvoice'] ?? true,
|
stockDeductionOnInvoice: json['stockDeductionOnInvoice'] ?? true,
|
||||||
barcodeSource: json['barcodeSource'] ?? 'SKU',
|
barcodeSource: json['barcodeSource'] ?? 'SKU',
|
||||||
|
enableEcommerceChannels: json['enableEcommerceChannels'] ?? json['enable_ecommerce_channels'] ?? false,
|
||||||
createdAt: json['createdAt'] != null
|
createdAt: json['createdAt'] != null
|
||||||
? DateTime.parse(json['createdAt'])
|
? DateTime.parse(json['createdAt'])
|
||||||
: null,
|
: null,
|
||||||
@@ -51,6 +54,7 @@ class BusinessFeature {
|
|||||||
'bomReductionStrategy': bomReductionStrategy,
|
'bomReductionStrategy': bomReductionStrategy,
|
||||||
'stockDeductionOnInvoice': stockDeductionOnInvoice,
|
'stockDeductionOnInvoice': stockDeductionOnInvoice,
|
||||||
'barcodeSource': barcodeSource,
|
'barcodeSource': barcodeSource,
|
||||||
|
'enableEcommerceChannels': enableEcommerceChannels,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +68,7 @@ class BusinessFeature {
|
|||||||
String? bomReductionStrategy,
|
String? bomReductionStrategy,
|
||||||
bool? stockDeductionOnInvoice,
|
bool? stockDeductionOnInvoice,
|
||||||
String? barcodeSource,
|
String? barcodeSource,
|
||||||
|
bool? enableEcommerceChannels,
|
||||||
}) {
|
}) {
|
||||||
return BusinessFeature(
|
return BusinessFeature(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -76,6 +81,8 @@ class BusinessFeature {
|
|||||||
stockDeductionOnInvoice:
|
stockDeductionOnInvoice:
|
||||||
stockDeductionOnInvoice ?? this.stockDeductionOnInvoice,
|
stockDeductionOnInvoice ?? this.stockDeductionOnInvoice,
|
||||||
barcodeSource: barcodeSource ?? this.barcodeSource,
|
barcodeSource: barcodeSource ?? this.barcodeSource,
|
||||||
|
enableEcommerceChannels:
|
||||||
|
enableEcommerceChannels ?? this.enableEcommerceChannels,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import '../../providers/business_provider.dart';
|
import '../../providers/business_provider.dart';
|
||||||
|
import '../../../sales/presentation/sales_channels_screen.dart';
|
||||||
|
|
||||||
class BusinessSettingsScreen extends ConsumerStatefulWidget {
|
class BusinessSettingsScreen extends ConsumerStatefulWidget {
|
||||||
const BusinessSettingsScreen({super.key});
|
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()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -192,6 +192,11 @@ class Invoice {
|
|||||||
final double? emiAmount;
|
final double? emiAmount;
|
||||||
final String? emiCycle;
|
final String? emiCycle;
|
||||||
final DateTime? emiStartDate;
|
final DateTime? emiStartDate;
|
||||||
|
final int? salesChannelId;
|
||||||
|
final String? salesChannel;
|
||||||
|
final String? marketplaceOrderId;
|
||||||
|
final int? placeOfSupplyStateId;
|
||||||
|
final String? placeOfSupply;
|
||||||
final List<InvoiceItem> items;
|
final List<InvoiceItem> items;
|
||||||
final List<InvoicePayment>? payments;
|
final List<InvoicePayment>? payments;
|
||||||
|
|
||||||
@@ -219,6 +224,11 @@ class Invoice {
|
|||||||
this.emiAmount,
|
this.emiAmount,
|
||||||
this.emiCycle,
|
this.emiCycle,
|
||||||
this.emiStartDate,
|
this.emiStartDate,
|
||||||
|
this.salesChannelId,
|
||||||
|
this.salesChannel,
|
||||||
|
this.marketplaceOrderId,
|
||||||
|
this.placeOfSupplyStateId,
|
||||||
|
this.placeOfSupply,
|
||||||
this.items = const [],
|
this.items = const [],
|
||||||
this.payments,
|
this.payments,
|
||||||
});
|
});
|
||||||
@@ -254,6 +264,11 @@ class Invoice {
|
|||||||
emiStartDate: json['emiStartDate'] != null
|
emiStartDate: json['emiStartDate'] != null
|
||||||
? DateTime.parse(json['emiStartDate'])
|
? DateTime.parse(json['emiStartDate'])
|
||||||
: (json['emi_start_date'] != null ? DateTime.parse(json['emi_start_date']) : null),
|
: (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
|
items: json['items'] != null
|
||||||
? (json['items'] as List).map((i) => InvoiceItem.fromJson(i)).toList()
|
? (json['items'] as List).map((i) => InvoiceItem.fromJson(i)).toList()
|
||||||
: [],
|
: [],
|
||||||
@@ -296,6 +311,11 @@ class Invoice {
|
|||||||
if (emiStartDate != null) {
|
if (emiStartDate != null) {
|
||||||
data['emiStartDate'] = emiStartDate!.toIso8601String().split('T')[0];
|
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();
|
data['items'] = items.map((i) => i.toJson()).toList();
|
||||||
if (payments != null) {
|
if (payments != null) {
|
||||||
data['payments'] = payments!.map((i) => i.toJson()).toList();
|
data['payments'] = payments!.map((i) => i.toJson()).toList();
|
||||||
|
|||||||
49
kifi-app/lib/features/sales/domain/sales_channel.dart
Normal file
49
kifi-app/lib/features/sales/domain/sales_channel.dart
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,8 @@ import 'package:pdf/widgets.dart' as pw;
|
|||||||
import 'package:printing/printing.dart';
|
import 'package:printing/printing.dart';
|
||||||
import '../../inventory/providers/products_provider.dart';
|
import '../../inventory/providers/products_provider.dart';
|
||||||
import '../../inventory/providers/product_categories_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 '../domain/invoice.dart';
|
||||||
import '../providers/invoices_provider.dart';
|
import '../providers/invoices_provider.dart';
|
||||||
import '../providers/customers_provider.dart';
|
import '../providers/customers_provider.dart';
|
||||||
@@ -21,6 +23,46 @@ import '../../../core/network/dio_client.dart';
|
|||||||
import 'widgets/receive_payment_sheet.dart';
|
import 'widgets/receive_payment_sheet.dart';
|
||||||
import 'invoice_builder_screen.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) {
|
String _resolveImageUrl(String path) {
|
||||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||||
return path;
|
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 Text('INVOICE NO', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey)),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(latestInvoice.invoiceNumber, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: Colors.blue)),
|
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 SizedBox(height: 10),
|
||||||
const Text('INVOICE DATE', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey)),
|
const Text('INVOICE DATE', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.grey)),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(formatDate.format(latestInvoice.issueDate), style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
|
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
|
// Item Cards
|
||||||
...(latestInvoice.items).map((item) {
|
...(latestInvoice.items).map((item) {
|
||||||
final product = products.where((p) => p.id == item.productId).firstOrNull;
|
final product = products.where((p) => p.id == item.productId).firstOrNull;
|
||||||
final category = categories.where((c) => c.id == product?.categoryId).firstOrNull;
|
final category = categories.where((c) => c.name == item.categoryName || c.id == product?.categoryId).firstOrNull;
|
||||||
final weight = item.weight ?? item.quantity;
|
final uoms = ref.watch(uomsProvider).value ?? [];
|
||||||
final metalAmount = weight * item.unitPrice;
|
|
||||||
|
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;
|
double makingChargeAmt = 0.0;
|
||||||
if (item.makingChargesType == 'PERCENTAGE') {
|
if (isItemCommodity) {
|
||||||
makingChargeAmt = metalAmount * (item.makingCharge / 100.0);
|
if (item.makingChargesType == 'PERCENTAGE') {
|
||||||
} else if (item.makingChargesType == 'PER_PIECE') {
|
makingChargeAmt = baseAmount * (item.makingCharge / 100.0);
|
||||||
makingChargeAmt = item.makingCharge;
|
} else if (item.makingChargesType == 'PER_PIECE') {
|
||||||
} else {
|
makingChargeAmt = item.makingCharge;
|
||||||
makingChargeAmt = weight * item.makingCharge;
|
} else {
|
||||||
|
makingChargeAmt = qtyOrWeight * item.makingCharge;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final taxableAmount = baseAmount + makingChargeAmt + item.otherCharges - item.discount;
|
||||||
|
final taxAmount = (taxableAmount * item.taxRate) / 100.0;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
@@ -278,7 +369,7 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
item.productName ?? product?.name ?? item.description ?? 'Jewellery Item',
|
item.productName ?? product?.name ?? item.description ?? 'Item',
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
@@ -290,10 +381,12 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
|
|||||||
_buildDetailBadge(item.categoryName ?? category!.name, const Color(0xFFD4AF37)),
|
_buildDetailBadge(item.categoryName ?? category!.name, const Color(0xFFD4AF37)),
|
||||||
if (item.sku != null || product?.sku != null)
|
if (item.sku != null || product?.sku != null)
|
||||||
_buildDetailBadge('SKU: ${item.sku ?? product!.sku}', Colors.blue),
|
_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),
|
_buildDetailBadge('HUID: ${item.huid}', Colors.purple),
|
||||||
if (item.hsnCode != null)
|
if (item.hsnCode != null && item.hsnCode!.isNotEmpty)
|
||||||
_buildDetailBadge('HSN: ${item.hsnCode}', Colors.grey),
|
_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),
|
const Divider(height: 18),
|
||||||
Row(
|
if (isItemCommodity) ...[
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
Row(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
Text(
|
children: [
|
||||||
'Metal (${weight.toStringAsFixed(3)}g @ ₹${item.unitPrice.toStringAsFixed(2)}/g):',
|
Text(
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
'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)),
|
|
||||||
],
|
],
|
||||||
),
|
] else ...[
|
||||||
const SizedBox(height: 4),
|
Row(
|
||||||
Row(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
children: [
|
||||||
children: [
|
Text(
|
||||||
Text(
|
'Rate: ₹${item.unitPrice.toStringAsFixed(2)} / $unit • Qty: ${qtyOrWeight.toStringAsFixed(qtyOrWeight.truncateToDouble() == qtyOrWeight ? 0 : 2)} $unit',
|
||||||
'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, fontWeight: FontWeight.w500),
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
),
|
||||||
),
|
Text(formatCurrency.format(baseAmount), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12)),
|
||||||
Text(formatCurrency.format(makingChargeAmt), style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12, color: Colors.indigo)),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
|
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) ...[
|
if (item.taxRate > 0) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
@@ -334,7 +452,7 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
|
|||||||
children: [
|
children: [
|
||||||
Text('GST (${item.taxRate.toStringAsFixed(1)}%):', style: TextStyle(fontSize: 11, color: Colors.grey.shade500)),
|
Text('GST (${item.taxRate.toStringAsFixed(1)}%):', style: TextStyle(fontSize: 11, color: Colors.grey.shade500)),
|
||||||
Text(
|
Text(
|
||||||
'+ ${formatCurrency.format(((metalAmount + makingChargeAmt) * (item.taxRate / 100.0)))}',
|
'+ ${formatCurrency.format(taxAmount)}',
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
|
style: TextStyle(fontSize: 11, color: Colors.grey.shade600),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -556,6 +674,7 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
|
|||||||
final business = ref.read(businessProfileProvider).value;
|
final business = ref.read(businessProfileProvider).value;
|
||||||
final products = ref.read(productsProvider).value ?? [];
|
final products = ref.read(productsProvider).value ?? [];
|
||||||
final categories = ref.read(productCategoriesProvider).value ?? [];
|
final categories = ref.read(productCategoriesProvider).value ?? [];
|
||||||
|
final uoms = ref.read(uomsProvider).value ?? [];
|
||||||
|
|
||||||
final formatCurrency = NumberFormat.currency(symbol: 'Rs. ', decimalDigits: 2);
|
final formatCurrency = NumberFormat.currency(symbol: 'Rs. ', decimalDigits: 2);
|
||||||
final formatDate = DateFormat('dd MMM yyyy');
|
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 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 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(
|
return pw.Container(
|
||||||
margin: const pw.EdgeInsets.only(bottom: 14),
|
margin: const pw.EdgeInsets.only(bottom: 14),
|
||||||
decoration: pw.BoxDecoration(
|
decoration: pw.BoxDecoration(
|
||||||
@@ -839,22 +971,40 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
|
|||||||
style: const pw.TextStyle(fontSize: 7.5, color: PdfColors.grey800),
|
style: const pw.TextStyle(fontSize: 7.5, color: PdfColors.grey800),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
pw.Container(
|
if (isGroupCommodity)
|
||||||
padding: const pw.EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
pw.Container(
|
||||||
decoration: pw.BoxDecoration(
|
padding: const pw.EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
color: PdfColors.amber50,
|
decoration: pw.BoxDecoration(
|
||||||
border: pw.Border.all(color: PdfColors.amber600, width: 0.5),
|
color: PdfColors.amber50,
|
||||||
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(3)),
|
border: pw.Border.all(color: PdfColors.amber600, width: 0.5),
|
||||||
),
|
borderRadius: const pw.BorderRadius.all(pw.Radius.circular(3)),
|
||||||
child: pw.Text(
|
),
|
||||||
'Purity: $purityStr',
|
child: pw.Text(
|
||||||
style: pw.TextStyle(
|
'Purity: $purityStr',
|
||||||
fontSize: 7.5,
|
style: pw.TextStyle(
|
||||||
fontWeight: pw.FontWeight.bold,
|
fontSize: 7.5,
|
||||||
color: PdfColors.brown800,
|
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 != '-')
|
if (hsnStr != '-')
|
||||||
pw.Container(
|
pw.Container(
|
||||||
padding: const pw.EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
padding: const pw.EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
@@ -900,49 +1050,71 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
|
|||||||
4: pw.Alignment.centerRight,
|
4: pw.Alignment.centerRight,
|
||||||
5: pw.Alignment.centerRight,
|
5: pw.Alignment.centerRight,
|
||||||
},
|
},
|
||||||
headers: [
|
headers: isGroupCommodity
|
||||||
'Item / HUID',
|
? [
|
||||||
'Weight / Qty',
|
'Item / HUID',
|
||||||
'Rate',
|
'Weight / Qty',
|
||||||
'Making Chg',
|
'Rate',
|
||||||
'Other Chg',
|
'Making Chg',
|
||||||
'Total Amt',
|
'Other Chg',
|
||||||
],
|
'Total Amt',
|
||||||
data: group.map((item) {
|
]
|
||||||
String particular = '';
|
: [
|
||||||
if (item.huid != null && item.huid!.isNotEmpty) {
|
'Item / SKU',
|
||||||
particular = 'HUID: ${item.huid}';
|
'Qty',
|
||||||
} else if (item.sku != null && item.sku!.isNotEmpty) {
|
'Unit Price',
|
||||||
particular = 'SKU: ${item.sku}';
|
'Discount',
|
||||||
} else {
|
'GST %',
|
||||||
particular = 'Standard Item';
|
'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 weight = item.weight ?? item.quantity;
|
||||||
final weightQty = item.weight != null && item.weight! > 0
|
final weightQty = item.weight != null && item.weight! > 0
|
||||||
? '${item.weight!.toStringAsFixed(item.weight!.truncateToDouble() == item.weight ? 0 : 3)} g'
|
? '${item.weight!.toStringAsFixed(item.weight!.truncateToDouble() == item.weight ? 0 : 3)} $groupUnit'
|
||||||
: '${item.quantity.toStringAsFixed(item.quantity.truncateToDouble() == item.quantity ? 0 : 0)} pcs';
|
: '${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;
|
double makingAmt = 0.0;
|
||||||
if (item.makingChargesType == 'PERCENTAGE') {
|
if (item.makingChargesType == 'PERCENTAGE') {
|
||||||
makingAmt = (weight * item.unitPrice) * (item.makingCharge / 100.0);
|
makingAmt = (weight * item.unitPrice) * (item.makingCharge / 100.0);
|
||||||
} else if (item.makingChargesType == 'PER_PIECE') {
|
} else if (item.makingChargesType == 'PER_PIECE') {
|
||||||
makingAmt = item.makingCharge;
|
makingAmt = item.makingCharge;
|
||||||
} else {
|
} else {
|
||||||
makingAmt = weight * item.makingCharge;
|
makingAmt = weight * item.makingCharge;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
particular,
|
particular,
|
||||||
weightQty,
|
weightQty,
|
||||||
'${formatCurrency.format(item.unitPrice)} $rateUnit',
|
'${formatCurrency.format(item.unitPrice)} $rateUnit',
|
||||||
makingAmt > 0 ? formatCurrency.format(makingAmt) : '-',
|
makingAmt > 0 ? formatCurrency.format(makingAmt) : '-',
|
||||||
item.otherCharges > 0 ? formatCurrency.format(item.otherCharges) : '-',
|
item.otherCharges > 0 ? formatCurrency.format(item.otherCharges) : '-',
|
||||||
formatCurrency.format(item.total),
|
formatCurrency.format(item.total),
|
||||||
];
|
];
|
||||||
}).toList(),
|
}).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
|
// Group Subtotal & GST Calculation Box
|
||||||
@@ -959,7 +1131,9 @@ class _InvoiceDetailsScreenState extends ConsumerState<InvoiceDetailsScreen> {
|
|||||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
pw.Text(
|
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),
|
style: const pw.TextStyle(fontSize: 8, color: PdfColors.grey700),
|
||||||
),
|
),
|
||||||
pw.Text(
|
pw.Text(
|
||||||
|
|||||||
@@ -198,11 +198,35 @@ class _InvoicesListScreenState extends ConsumerState<InvoicesListScreen> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Expanded(
|
||||||
invoice.invoiceNumber,
|
child: Row(
|
||||||
style: const TextStyle(
|
children: [
|
||||||
fontWeight: FontWeight.bold,
|
Flexible(
|
||||||
fontSize: 16,
|
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(
|
Container(
|
||||||
|
|||||||
@@ -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')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
@@ -601,6 +601,7 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
if (_items.isEmpty)
|
if (_items.isEmpty)
|
||||||
Container(
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(32),
|
padding: const EdgeInsets.all(32),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isDark ? const Color(0xFF1E293B) : Colors.grey.shade100,
|
color: isDark ? const Color(0xFF1E293B) : Colors.grey.shade100,
|
||||||
@@ -608,6 +609,8 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
border: Border.all(color: Colors.grey.withValues(alpha: 0.2)),
|
border: Border.all(color: Colors.grey.withValues(alpha: 0.2)),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(LucideIcons.packageOpen, size: 48, color: Colors.grey.shade400),
|
Icon(LucideIcons.packageOpen, size: 48, color: Colors.grey.shade400),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -807,6 +810,22 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
final effectiveTaxable = (rawSubtotal - discountAmount).clamp(0.0, double.infinity);
|
final effectiveTaxable = (rawSubtotal - discountAmount).clamp(0.0, double.infinity);
|
||||||
final grandTotal = effectiveTaxable + taxTotal;
|
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(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -848,7 +867,7 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
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)}'),
|
Text('₹${(taxTotal / 2.0).toStringAsFixed(2)}'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -856,7 +875,7 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
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)}'),
|
Text('₹${(taxTotal / 2.0).toStringAsFixed(2)}'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -864,7 +883,7 @@ class _PurchaseOrderBuilderScreenState
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
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)}'),
|
Text('₹${taxTotal.toStringAsFixed(2)}'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user