Fixed upto purchases and rate sync process
This commit is contained in:
54
kifi-app/lib/features/inventory/domain/commodity_rate.dart
Normal file
54
kifi-app/lib/features/inventory/domain/commodity_rate.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
class CommodityRateHistory {
|
||||
final int? id;
|
||||
final String commodityCode;
|
||||
final String? commodityName;
|
||||
final String? purity;
|
||||
final double rate;
|
||||
final DateTime effectiveAt;
|
||||
final String? source;
|
||||
final int? createdBy;
|
||||
final DateTime? createdAt;
|
||||
|
||||
CommodityRateHistory({
|
||||
this.id,
|
||||
required this.commodityCode,
|
||||
this.commodityName,
|
||||
this.purity,
|
||||
required this.rate,
|
||||
required this.effectiveAt,
|
||||
this.source,
|
||||
this.createdBy,
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
factory CommodityRateHistory.fromJson(Map<String, dynamic> json) {
|
||||
return CommodityRateHistory(
|
||||
id: json['id'] is int ? json['id'] : int.tryParse(json['id']?.toString() ?? ''),
|
||||
commodityCode: (json['commodityCode'] ?? json['commodity_code'] ?? json['commodityName'] ?? json['commodity_name'] ?? '').toString(),
|
||||
commodityName: json['commodityName'] ?? json['commodity_name'],
|
||||
purity: json['purity'],
|
||||
rate: (json['rate'] as num?)?.toDouble() ?? 0.0,
|
||||
effectiveAt: json['effectiveAt'] != null
|
||||
? DateTime.parse(json['effectiveAt'].toString())
|
||||
: (json['effective_at'] != null ? DateTime.parse(json['effective_at'].toString()) : DateTime.now()),
|
||||
source: json['source'],
|
||||
createdBy: json['createdBy'] ?? json['created_by'],
|
||||
createdAt: json['createdAt'] != null
|
||||
? DateTime.parse(json['createdAt'].toString())
|
||||
: (json['created_at'] != null ? DateTime.parse(json['created_at'].toString()) : null),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
if (id != null) 'id': id,
|
||||
'commodityCode': commodityCode,
|
||||
'commodityName': commodityName ?? commodityCode,
|
||||
if (purity != null) 'purity': purity,
|
||||
'rate': rate,
|
||||
'effectiveAt': effectiveAt.toIso8601String(),
|
||||
if (source != null) 'source': source,
|
||||
if (createdBy != null) 'createdBy': createdBy,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -243,9 +243,10 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
final _hsnController = TextEditingController();
|
||||
final _gstController = TextEditingController();
|
||||
final _makingChargeController = TextEditingController();
|
||||
final _purityFactorController = TextEditingController(text: '1.0');
|
||||
|
||||
int? _selectedParentId;
|
||||
bool _isLeaf = false;
|
||||
bool _isLeaf = true;
|
||||
bool _huidRequired = false;
|
||||
String _commodityCode = 'XAU';
|
||||
String _makingChargeType = 'PER_GRAM';
|
||||
@@ -271,6 +272,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
_hsnController.text = c.defaultHsn ?? '';
|
||||
_gstController.text = c.defaultGst?.toString() ?? '';
|
||||
_makingChargeController.text = c.defaultMakingCharge?.toString() ?? '';
|
||||
_purityFactorController.text = (c.purityFactor ?? 1.0).toString();
|
||||
_huidRequired = c.huidRequired;
|
||||
_commodityCode = c.commodityCode ?? 'XAU';
|
||||
|
||||
@@ -293,6 +295,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
_hsnController.dispose();
|
||||
_gstController.dispose();
|
||||
_makingChargeController.dispose();
|
||||
_purityFactorController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -304,6 +307,7 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
parentCategoryId: _selectedParentId,
|
||||
hasChild: !_isLeaf,
|
||||
commodityCode: _isLeaf ? _commodityCode : null,
|
||||
purityFactor: _isLeaf ? (double.tryParse(_purityFactorController.text) ?? 1.0) : 1.0,
|
||||
defaultHsn: _isLeaf ? _hsnController.text.trim() : null,
|
||||
defaultGst: _isLeaf ? double.tryParse(_gstController.text) : null,
|
||||
huidRequired: _isLeaf ? _huidRequired : false,
|
||||
@@ -457,17 +461,34 @@ class _CategoryFormSheetState extends ConsumerState<CategoryFormSheet> {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (isBusinessMode) ...[
|
||||
DropdownButtonFormField<String>(
|
||||
value: _commodityCode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Commodity Code',
|
||||
prefixIcon: const Icon(LucideIcons.barChart2),
|
||||
),
|
||||
items: _commodities.map((c) => DropdownMenuItem(
|
||||
value: c['code'],
|
||||
child: Text(c['label']!),
|
||||
)).toList(),
|
||||
onChanged: (val) => setState(() => _commodityCode = val!),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _commodityCode,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Commodity Code',
|
||||
prefixIcon: Icon(LucideIcons.barChart2),
|
||||
),
|
||||
items: _commodities.map((c) => DropdownMenuItem(
|
||||
value: c['code'],
|
||||
child: Text(c['label']!),
|
||||
)).toList(),
|
||||
onChanged: (val) => setState(() => _commodityCode = val!),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildTextField(
|
||||
controller: _purityFactorController,
|
||||
label: 'Purity (e.g. 0.916)',
|
||||
icon: LucideIcons.gem,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import 'package:kifi_app/features/inventory/domain/inventory_item.dart';
|
||||
import 'package:kifi_app/features/inventory/domain/product.dart';
|
||||
import 'package:kifi_app/features/inventory/providers/products_provider.dart';
|
||||
import 'package:kifi_app/features/inventory/providers/product_categories_provider.dart';
|
||||
import 'package:kifi_app/features/inventory/providers/commodity_rates_provider.dart';
|
||||
import 'package:kifi_app/core/network/dio_client.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
@@ -63,12 +64,26 @@ class _StockLedgerTabState extends ConsumerState<StockLedgerTab> {
|
||||
}
|
||||
|
||||
final categoriesState = ref.watch(productCategoriesProvider);
|
||||
final category = categoriesState.value?.firstWhere(
|
||||
final commodityRatesState = ref.watch(commodityRatesProvider);
|
||||
|
||||
final category = categoriesState.value?.where(
|
||||
(c) => c.id == widget.product.categoryId,
|
||||
);
|
||||
).firstOrNull;
|
||||
|
||||
final String unit = category?.baseUnit ?? 'g';
|
||||
final double currentRate = category?.dailyRate ?? 0.0;
|
||||
final double purityFactor = category?.purityFactor ?? (widget.product.purityFactor ?? 1.0);
|
||||
|
||||
double commodityRate = 0.0;
|
||||
if (category?.commodityCode != null && commodityRatesState.value != null) {
|
||||
final match = commodityRatesState.value!.where(
|
||||
(r) => r.commodityCode.toUpperCase() == category!.commodityCode!.toUpperCase()
|
||||
).firstOrNull;
|
||||
if (match != null) {
|
||||
commodityRate = match.rate;
|
||||
}
|
||||
}
|
||||
|
||||
final double currentRate = commodityRate > 0 ? (commodityRate * purityFactor) : (category?.dailyRate ?? 0.0);
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: _fetchLedger,
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/network/dio_client.dart';
|
||||
import '../domain/commodity_rate.dart';
|
||||
|
||||
class CommodityRatesNotifier extends AsyncNotifier<List<CommodityRateHistory>> {
|
||||
@override
|
||||
FutureOr<List<CommodityRateHistory>> build() async {
|
||||
return _fetchLatestRates();
|
||||
}
|
||||
|
||||
Future<List<CommodityRateHistory>> _fetchLatestRates() async {
|
||||
try {
|
||||
final response = await DioClient().dio.get('/inventory/commodity-rates/latest');
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data;
|
||||
return data.map((e) => CommodityRateHistory.fromJson(e)).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback if empty or endpoint fails
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<List<CommodityRateHistory>> fetchRateHistory(String commodityCode) async {
|
||||
try {
|
||||
final response = await DioClient().dio.get('/inventory/commodity-rates/$commodityCode');
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = response.data;
|
||||
return data.map((e) => CommodityRateHistory.fromJson(e)).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
// Error fetching history
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<CommodityRateHistory?> addRate(
|
||||
String commodityCode,
|
||||
double rate, {
|
||||
String? commodityName,
|
||||
String? purity,
|
||||
String? source,
|
||||
}) async {
|
||||
try {
|
||||
final payload = {
|
||||
'commodityCode': commodityCode,
|
||||
'commodityName': commodityName ?? commodityCode,
|
||||
if (purity != null) 'purity': purity,
|
||||
'rate': rate,
|
||||
'source': source ?? 'MANUAL',
|
||||
};
|
||||
|
||||
final response = await DioClient().dio.post(
|
||||
'/inventory/commodity-rates',
|
||||
data: payload,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
final newRate = CommodityRateHistory.fromJson(response.data);
|
||||
state = await AsyncValue.guard(() => _fetchLatestRates());
|
||||
return newRate;
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<int> syncRateToProducts(String commodityCode) async {
|
||||
try {
|
||||
final response = await DioClient().dio.post(
|
||||
'/inventory/commodity-rates/$commodityCode/sync',
|
||||
);
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
return (response.data['updatedCount'] as num?)?.toInt() ?? 0;
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
final commodityRatesProvider =
|
||||
AsyncNotifierProvider<CommodityRatesNotifier, List<CommodityRateHistory>>(() {
|
||||
return CommodityRatesNotifier();
|
||||
});
|
||||
@@ -9,6 +9,7 @@ class ProductCategory {
|
||||
final int? parentCategoryId;
|
||||
final bool hasChild;
|
||||
final String? commodityCode;
|
||||
final double? purityFactor;
|
||||
final String? defaultHsn;
|
||||
final double? defaultGst;
|
||||
final bool huidRequired;
|
||||
@@ -26,6 +27,7 @@ class ProductCategory {
|
||||
this.parentCategoryId,
|
||||
this.hasChild = false,
|
||||
this.commodityCode,
|
||||
this.purityFactor = 1.0,
|
||||
this.defaultHsn,
|
||||
this.defaultGst,
|
||||
this.huidRequired = false,
|
||||
@@ -44,16 +46,17 @@ class ProductCategory {
|
||||
name: json['name'],
|
||||
parentCategoryId: json['parentCategoryId'],
|
||||
hasChild: json['hasChild'] ?? false,
|
||||
commodityCode: json['commodityCode'],
|
||||
defaultHsn: json['defaultHsn'],
|
||||
defaultGst: (json['defaultGst'] as num?)?.toDouble(),
|
||||
huidRequired: json['huidRequired'] ?? false,
|
||||
defaultMakingCharge: (json['defaultMakingCharge'] as num?)?.toDouble(),
|
||||
makingChargeType: json['makingChargeType'],
|
||||
baseUnit: json['baseUnit'] ?? 'pcs',
|
||||
isActive: json['isActive'] ?? true,
|
||||
sortOrder: json['sortOrder'] ?? 0,
|
||||
dailyRate: (json['dailyRate'] as num?)?.toDouble(),
|
||||
commodityCode: json['commodityCode'] ?? json['commodity_code'],
|
||||
purityFactor: (json['purityFactor'] ?? json['purity_factor'] as num?)?.toDouble() ?? 1.0,
|
||||
defaultHsn: json['defaultHsn'] ?? json['default_hsn'],
|
||||
defaultGst: (json['defaultGst'] ?? json['default_gst'] as num?)?.toDouble(),
|
||||
huidRequired: json['huidRequired'] ?? json['huid_required'] ?? false,
|
||||
defaultMakingCharge: (json['defaultMakingCharge'] ?? json['default_making_charge'] as num?)?.toDouble(),
|
||||
makingChargeType: json['makingChargeType'] ?? json['making_charge_type'],
|
||||
baseUnit: json['baseUnit'] ?? json['base_unit'] ?? 'pcs',
|
||||
isActive: json['isActive'] ?? json['is_active'] ?? true,
|
||||
sortOrder: json['sortOrder'] ?? json['sort_order'] ?? 0,
|
||||
dailyRate: (json['dailyRate'] ?? json['daily_rate'] as num?)?.toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,6 +68,7 @@ class ProductCategory {
|
||||
'parentCategoryId': parentCategoryId,
|
||||
'hasChild': hasChild,
|
||||
'commodityCode': commodityCode,
|
||||
'purityFactor': purityFactor ?? 1.0,
|
||||
'defaultHsn': defaultHsn,
|
||||
'defaultGst': defaultGst,
|
||||
'huidRequired': huidRequired,
|
||||
|
||||
@@ -570,117 +570,135 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen> {
|
||||
final amount = double.tryParse(amountText);
|
||||
if (amount == null) return;
|
||||
|
||||
if (fromWallet != null && fromWallet!.balance < amount) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Insufficient balance in From Account')));
|
||||
return;
|
||||
final isFromPayableOrLiability = fromWallet != null && (fromWallet!.nature == 'PAYABLES' || fromWallet!.nature == 'LOAN');
|
||||
if (fromWallet != null && !isFromPayableOrLiability) {
|
||||
double effectiveBalance = fromWallet!.balance;
|
||||
if (widget.transaction != null && widget.transaction!.fromWalletId == fromWallet!.id) {
|
||||
effectiveBalance += widget.transaction!.amount;
|
||||
}
|
||||
if (effectiveBalance < amount) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Insufficient balance in From Account')));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() => isSaving = true);
|
||||
|
||||
// Determine internal type based on natures
|
||||
String type = 'TRANSFER';
|
||||
if (fromWallet == null) {
|
||||
type = 'INCOME';
|
||||
} else if (toWallet == null) { // Though toWallet is mandatory, just in case
|
||||
type = 'EXPENSE';
|
||||
} else if (fromWallet?.nature == 'INCOME') {
|
||||
type = 'INCOME';
|
||||
} else if (toWallet?.nature == 'PAYABLES' || toWallet?.nature == 'EXPENSE') {
|
||||
type = 'EXPENSE';
|
||||
} else if (toWallet?.nature == 'INVESTMENTS') {
|
||||
type = 'INVESTMENT';
|
||||
}
|
||||
|
||||
if (isRecurring && widget.transaction == null) {
|
||||
DateTime nextDate = selectedDate;
|
||||
if (recurringFrequency == 'DAILY') {
|
||||
nextDate = nextDate.add(const Duration(days: 1));
|
||||
} else if (recurringFrequency == 'WEEKLY') {
|
||||
nextDate = nextDate.add(const Duration(days: 7));
|
||||
} else if (recurringFrequency == 'MONTHLY') {
|
||||
nextDate = DateTime(nextDate.year, nextDate.month + 1, nextDate.day);
|
||||
try {
|
||||
// Determine internal type based on natures
|
||||
String type = 'TRANSFER';
|
||||
if (fromWallet == null) {
|
||||
type = 'INCOME';
|
||||
} else if (toWallet == null) { // Though toWallet is mandatory, just in case
|
||||
type = 'EXPENSE';
|
||||
} else if (fromWallet?.nature == 'INCOME') {
|
||||
type = 'INCOME';
|
||||
} else if (toWallet?.nature == 'PAYABLES' || toWallet?.nature == 'EXPENSE') {
|
||||
type = 'EXPENSE';
|
||||
} else if (toWallet?.nature == 'INVESTMENTS') {
|
||||
type = 'INVESTMENT';
|
||||
}
|
||||
|
||||
final rt = RecurringTransaction(
|
||||
id: 0,
|
||||
type: type,
|
||||
amount: amount,
|
||||
categoryId: selectedCategory?.id,
|
||||
fromWalletId: fromWallet?.id,
|
||||
toWalletId: toWallet?.id,
|
||||
frequency: recurringFrequency,
|
||||
description: descriptionController.text.trim(),
|
||||
nextExecutionDate: nextDate,
|
||||
status: 'ACTIVE',
|
||||
);
|
||||
await ref.read(recurringTransactionProvider.notifier).addRecurringTransaction(rt);
|
||||
|
||||
final tx = Transaction(
|
||||
id: 0,
|
||||
type: type,
|
||||
amount: amount,
|
||||
date: selectedDate,
|
||||
description: descriptionController.text.trim(),
|
||||
categoryId: selectedCategory?.id,
|
||||
fromWalletId: fromWallet?.id,
|
||||
toWalletId: toWallet?.id,
|
||||
dueDate: dueDate,
|
||||
alertSchedule: alertSchedule == 'NONE' ? null : alertSchedule,
|
||||
alertTime: alertSchedule == 'NONE' ? null : '${alertTime.hour.toString().padLeft(2, '0')}:${alertTime.minute.toString().padLeft(2, '0')}:00',
|
||||
items: lineItems.isNotEmpty ? lineItems : null,
|
||||
);
|
||||
await ref.read(transactionProvider.notifier).addTransaction(tx, base64Attachments: base64Attachments);
|
||||
} else {
|
||||
final tx = Transaction(
|
||||
id: widget.transaction?.id ?? 0,
|
||||
type: type,
|
||||
amount: amount,
|
||||
date: selectedDate,
|
||||
description: descriptionController.text.trim(),
|
||||
categoryId: selectedCategory?.id,
|
||||
fromWalletId: fromWallet?.id,
|
||||
toWalletId: toWallet?.id,
|
||||
dueDate: dueDate,
|
||||
alertSchedule: alertSchedule == 'NONE' ? null : alertSchedule,
|
||||
alertTime: alertSchedule == 'NONE' ? null : '${alertTime.hour.toString().padLeft(2, '0')}:${alertTime.minute.toString().padLeft(2, '0')}:00',
|
||||
items: lineItems.isNotEmpty ? lineItems : null,
|
||||
);
|
||||
if (isRecurring && widget.transaction == null) {
|
||||
DateTime nextDate = selectedDate;
|
||||
if (recurringFrequency == 'DAILY') {
|
||||
nextDate = nextDate.add(const Duration(days: 1));
|
||||
} else if (recurringFrequency == 'WEEKLY') {
|
||||
nextDate = nextDate.add(const Duration(days: 7));
|
||||
} else if (recurringFrequency == 'MONTHLY') {
|
||||
nextDate = DateTime(nextDate.year, nextDate.month + 1, nextDate.day);
|
||||
}
|
||||
|
||||
if (widget.transaction == null) {
|
||||
final rt = RecurringTransaction(
|
||||
id: 0,
|
||||
type: type,
|
||||
amount: amount,
|
||||
categoryId: selectedCategory?.id,
|
||||
fromWalletId: fromWallet?.id,
|
||||
toWalletId: toWallet?.id,
|
||||
frequency: recurringFrequency,
|
||||
description: descriptionController.text.trim(),
|
||||
nextExecutionDate: nextDate,
|
||||
status: 'ACTIVE',
|
||||
);
|
||||
await ref.read(recurringTransactionProvider.notifier).addRecurringTransaction(rt);
|
||||
|
||||
final tx = Transaction(
|
||||
id: 0,
|
||||
type: type,
|
||||
amount: amount,
|
||||
date: selectedDate,
|
||||
description: descriptionController.text.trim(),
|
||||
categoryId: selectedCategory?.id,
|
||||
fromWalletId: fromWallet?.id,
|
||||
toWalletId: toWallet?.id,
|
||||
dueDate: dueDate,
|
||||
alertSchedule: alertSchedule == 'NONE' ? null : alertSchedule,
|
||||
alertTime: alertSchedule == 'NONE' ? null : '${alertTime.hour.toString().padLeft(2, '0')}:${alertTime.minute.toString().padLeft(2, '0')}:00',
|
||||
items: lineItems.isNotEmpty ? lineItems : null,
|
||||
);
|
||||
await ref.read(transactionProvider.notifier).addTransaction(tx, base64Attachments: base64Attachments);
|
||||
} else {
|
||||
await ref.read(transactionProvider.notifier).updateTransaction(
|
||||
tx,
|
||||
base64Attachments: base64Attachments,
|
||||
deletedAttachmentIds: deletedAttachmentIds,
|
||||
final tx = Transaction(
|
||||
id: widget.transaction?.id ?? 0,
|
||||
type: type,
|
||||
amount: amount,
|
||||
date: selectedDate,
|
||||
description: descriptionController.text.trim(),
|
||||
categoryId: selectedCategory?.id,
|
||||
fromWalletId: fromWallet?.id,
|
||||
toWalletId: toWallet?.id,
|
||||
dueDate: dueDate,
|
||||
alertSchedule: alertSchedule == 'NONE' ? null : alertSchedule,
|
||||
alertTime: alertSchedule == 'NONE' ? null : '${alertTime.hour.toString().padLeft(2, '0')}:${alertTime.minute.toString().padLeft(2, '0')}:00',
|
||||
items: lineItems.isNotEmpty ? lineItems : null,
|
||||
);
|
||||
}
|
||||
|
||||
if (alertSchedule != 'NONE' && dueDate != null) {
|
||||
DateTime alertDate = dueDate!;
|
||||
if (alertSchedule == '1_DAY_BEFORE') {
|
||||
alertDate = dueDate!.subtract(const Duration(days: 1));
|
||||
} else if (alertSchedule == '2_DAYS_BEFORE') {
|
||||
alertDate = dueDate!.subtract(const Duration(days: 2));
|
||||
} else if (alertSchedule == '1_WEEK_BEFORE') {
|
||||
alertDate = dueDate!.subtract(const Duration(days: 7));
|
||||
}
|
||||
|
||||
final notifyDate = DateTime(alertDate.year, alertDate.month, alertDate.day, alertTime.hour, alertTime.minute);
|
||||
if (notifyDate.isAfter(DateTime.now())) {
|
||||
NotificationService().scheduleNotification(
|
||||
id: DateTime.now().millisecondsSinceEpoch.remainder(100000),
|
||||
title: 'Payment Due!',
|
||||
body: 'Rs. ${amount.toStringAsFixed(0)} is due for ${toWallet?.name}',
|
||||
scheduledDate: notifyDate,
|
||||
|
||||
if (widget.transaction == null) {
|
||||
await ref.read(transactionProvider.notifier).addTransaction(tx, base64Attachments: base64Attachments);
|
||||
} else {
|
||||
await ref.read(transactionProvider.notifier).updateTransaction(
|
||||
tx,
|
||||
base64Attachments: base64Attachments,
|
||||
deletedAttachmentIds: deletedAttachmentIds,
|
||||
);
|
||||
}
|
||||
|
||||
if (alertSchedule != 'NONE' && dueDate != null) {
|
||||
DateTime alertDate = dueDate!;
|
||||
if (alertSchedule == '1_DAY_BEFORE') {
|
||||
alertDate = dueDate!.subtract(const Duration(days: 1));
|
||||
} else if (alertSchedule == '2_DAYS_BEFORE') {
|
||||
alertDate = dueDate!.subtract(const Duration(days: 2));
|
||||
} else if (alertSchedule == '1_WEEK_BEFORE') {
|
||||
alertDate = dueDate!.subtract(const Duration(days: 7));
|
||||
}
|
||||
|
||||
final notifyDate = DateTime(alertDate.year, alertDate.month, alertDate.day, alertTime.hour, alertTime.minute);
|
||||
if (notifyDate.isAfter(DateTime.now())) {
|
||||
NotificationService().scheduleNotification(
|
||||
id: DateTime.now().millisecondsSinceEpoch.remainder(100000),
|
||||
title: 'Payment Due!',
|
||||
body: 'Rs. ${amount.toStringAsFixed(0)} is due for ${toWallet?.name}',
|
||||
scheduledDate: notifyDate,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
SnackBarService.showSuccess(context, 'Transaction saved successfully');
|
||||
Navigator.pop(context);
|
||||
if (mounted) {
|
||||
SnackBarService.showSuccess(context, 'Transaction saved successfully');
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error saving transaction: $e');
|
||||
if (mounted) {
|
||||
SnackBarService.showError(context, 'Failed to save transaction: ${e.toString()}');
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => isSaving = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -108,6 +108,11 @@ class PaginatedTransactionNotifier extends Notifier<PaginatedTransactionState> {
|
||||
final updatedList = state.transactions.where((t) => t.id != id).toList();
|
||||
state = state.copyWith(transactions: updatedList);
|
||||
}
|
||||
|
||||
void updateTransactionInState(Transaction tx) {
|
||||
final updatedList = state.transactions.map((t) => t.id == tx.id ? tx : t).toList();
|
||||
state = state.copyWith(transactions: updatedList);
|
||||
}
|
||||
}
|
||||
|
||||
final paginatedTransactionProvider = NotifierProvider<PaginatedTransactionNotifier, PaginatedTransactionState>(() {
|
||||
|
||||
@@ -52,7 +52,6 @@ class TransactionNotifier extends AsyncNotifier<List<Transaction>> {
|
||||
Future<void> updateTransaction(Transaction transaction, {List<Map<String, String>>? base64Attachments, List<int>? deletedAttachmentIds}) async {
|
||||
final updatedTransaction = await ref.read(apiRepositoryProvider).updateTransaction(transaction);
|
||||
|
||||
bool hasChanges = false;
|
||||
if (base64Attachments != null && base64Attachments.isNotEmpty) {
|
||||
for (var attachment in base64Attachments) {
|
||||
await ref.read(apiRepositoryProvider).addAttachment(
|
||||
@@ -62,28 +61,21 @@ class TransactionNotifier extends AsyncNotifier<List<Transaction>> {
|
||||
attachment['base64Content']!
|
||||
);
|
||||
}
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (deletedAttachmentIds != null && deletedAttachmentIds.isNotEmpty) {
|
||||
for (var id in deletedAttachmentIds) {
|
||||
await ref.read(apiRepositoryProvider).deleteAttachment(id);
|
||||
}
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
ref.invalidateSelf();
|
||||
} else {
|
||||
if (state.value != null) {
|
||||
final updatedList = state.value!.map((t) => t.id == updatedTransaction.id ? updatedTransaction : t).toList();
|
||||
state = AsyncValue.data(updatedList);
|
||||
}
|
||||
}
|
||||
ref.invalidate(walletProvider);
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
Future<void> deleteTransaction(int id) async {
|
||||
await ref.read(apiRepositoryProvider).deleteTransaction(id);
|
||||
ref.invalidate(walletProvider);
|
||||
if (state.value != null) {
|
||||
state = AsyncValue.data(state.value!.where((t) => t.id != id).toList());
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../../../core/widgets/premium_text_field.dart';
|
||||
import '../../../core/widgets/smart_search_dropdown.dart';
|
||||
import '../../inventory/providers/products_provider.dart';
|
||||
import '../../inventory/providers/product_categories_provider.dart';
|
||||
import '../../inventory/providers/commodity_rates_provider.dart';
|
||||
import '../providers/vendors_provider.dart';
|
||||
import '../../business/providers/business_provider.dart';
|
||||
import '../../transactions/presentation/widgets/attachment_gallery_screen.dart';
|
||||
@@ -735,13 +736,34 @@ class _AddItemSheetState extends ConsumerState<_AddItemSheet> {
|
||||
if (_selectedProductId == null) return;
|
||||
final qty = int.tryParse(_qtyCtrl.text) ?? 1;
|
||||
|
||||
final products = ref.read(productsProvider).value ?? [];
|
||||
final categories = ref.read(productCategoriesProvider).value ?? [];
|
||||
final commodityRates = ref.read(commodityRatesProvider).value ?? [];
|
||||
|
||||
final selectedProduct = products.where((p) => p.id == _selectedProductId).firstOrNull;
|
||||
final selectedCat = categories.where((c) => c.id == selectedProduct?.categoryId).firstOrNull;
|
||||
|
||||
double initialRate = 0.0;
|
||||
if (selectedCat?.commodityCode != null) {
|
||||
final matchRate = commodityRates.where(
|
||||
(r) => r.commodityCode.toUpperCase() == selectedCat!.commodityCode!.toUpperCase()
|
||||
).firstOrNull;
|
||||
if (matchRate != null && matchRate.rate > 0) {
|
||||
final purity = selectedCat?.purityFactor ?? (selectedProduct?.purityFactor ?? 1.0);
|
||||
initialRate = matchRate.rate * purity;
|
||||
}
|
||||
}
|
||||
if (initialRate == 0.0) {
|
||||
initialRate = selectedProduct?.sellingPrice ?? 0.0;
|
||||
}
|
||||
|
||||
final List<PurchaseOrderItem> items = [];
|
||||
for (int i = 0; i < qty; i++) {
|
||||
items.add(PurchaseOrderItem(
|
||||
productId: _selectedProductId,
|
||||
quantity: 1.0, // Each row is 1 unit initially, or could represent pieces/weight
|
||||
unitPrice: 0.0,
|
||||
total: 0.0,
|
||||
quantity: 1.0,
|
||||
unitPrice: initialRate,
|
||||
total: initialRate > 0 ? initialRate : 0.0,
|
||||
));
|
||||
}
|
||||
Navigator.pop(context, items);
|
||||
|
||||
Reference in New Issue
Block a user