@apptimate/ui 4.5.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/base-components/SearchableSelect.tsx +7 -3
- package/src/common-components/PaymentSection.tsx +41 -7
- package/src/common-components/pickers/EntityPickerModal.tsx +5 -1
- package/src/common-components/pickers/PartyPicker.tsx +3 -0
- package/src/common-components/pickers/WarehousePicker.tsx +3 -0
- package/src/common-components/transaction/MetadataPanel.tsx +4 -1
- package/src/common-components/transaction/ProductSelectionPanel.tsx +218 -59
- package/src/common-components/transaction/ProductTransactionScreen.tsx +325 -26
- package/src/common-components/transaction/types.ts +23 -7
|
@@ -4,7 +4,7 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
|
4
4
|
import { cn } from '@apptimate/core-lib';
|
|
5
5
|
import {
|
|
6
6
|
Search, ScanBarcode, X, ShoppingCart, Package, Loader2,
|
|
7
|
-
Trash2, ChevronDown, ChevronRight, AlertTriangle
|
|
7
|
+
Trash2, ChevronDown, ChevronRight, AlertTriangle, Tag
|
|
8
8
|
} from 'lucide-react';
|
|
9
9
|
import { Badge } from '../../base-components/Badge';
|
|
10
10
|
import { Table, THeader, TBody, TRow, TCell } from '../../base-components/Table';
|
|
@@ -13,10 +13,12 @@ import type { TransactionItem, TransactionLineItem, TransactionScreenConfig } fr
|
|
|
13
13
|
import { BatchSelectionModal, SerialSelectionModal } from "../pickers";
|
|
14
14
|
import { ParentLineSelectionModal } from "./ParentLineSelectionModal";
|
|
15
15
|
import { HintIcon } from '../../base-components/HintIcon';
|
|
16
|
+
import { AsyncSearchableSelect } from '../../base-components/SearchableSelect';
|
|
16
17
|
|
|
17
18
|
interface ProductSelectionPanelProps {
|
|
18
19
|
config: TransactionScreenConfig;
|
|
19
20
|
warehouseId?: number;
|
|
21
|
+
partyId?: number | null;
|
|
20
22
|
lineItems: TransactionLineItem[];
|
|
21
23
|
onAddItem: (item: TransactionItem, variant?: any, extraData?: { quantity?: number; batches?: any[]; serial_numbers?: string[] }) => void;
|
|
22
24
|
onUpdateLine: (uid: string, updates: Partial<TransactionLineItem>) => void;
|
|
@@ -27,6 +29,7 @@ interface ProductSelectionPanelProps {
|
|
|
27
29
|
export function ProductSelectionPanel({
|
|
28
30
|
config,
|
|
29
31
|
warehouseId,
|
|
32
|
+
partyId,
|
|
30
33
|
lineItems,
|
|
31
34
|
onAddItem,
|
|
32
35
|
onUpdateLine,
|
|
@@ -40,7 +43,7 @@ export function ProductSelectionPanel({
|
|
|
40
43
|
const [expandedItemIds, setExpandedItemIds] = useState<number[]>([]);
|
|
41
44
|
const [editingRates, setEditingRates] = useState<Record<string, string>>({});
|
|
42
45
|
const [focusedIndex, setFocusedIndex] = useState<number>(-1);
|
|
43
|
-
|
|
46
|
+
const [discountTypes, setDiscountTypes] = useState<Record<string, 'amount' | 'percent'>>({});
|
|
44
47
|
const [activeBatchItem, setActiveBatchItem] = useState<{ item: TransactionItem; variant?: any; preSelectedBatch?: any } | null>(null);
|
|
45
48
|
const [activeBatchLineUid, setActiveBatchLineUid] = useState<string | null>(null);
|
|
46
49
|
const [activeSerialItem, setActiveSerialItem] = useState<{ item: TransactionItem; variant?: any } | null>(null);
|
|
@@ -51,6 +54,7 @@ export function ProductSelectionPanel({
|
|
|
51
54
|
const panelRef = React.useRef<HTMLDivElement>(null);
|
|
52
55
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
53
56
|
|
|
57
|
+
const showDiscountCol = config.showPrices && ['sales', 'quotation', 'pos'].includes(config.type);
|
|
54
58
|
|
|
55
59
|
// ── Search handler ──
|
|
56
60
|
const handleSearch = useCallback(
|
|
@@ -69,6 +73,13 @@ export function ProductSelectionPanel({
|
|
|
69
73
|
try {
|
|
70
74
|
const params: Record<string, string | number> = { search: value, limit: 20, type: config.type };
|
|
71
75
|
if (warehouseId) params.warehouse_id = warehouseId;
|
|
76
|
+
|
|
77
|
+
if (['sales', 'quotation', 'pos'].includes(config.type)) {
|
|
78
|
+
params.apply_sales_prices = 1;
|
|
79
|
+
params.transaction_type = config.type;
|
|
80
|
+
if (partyId) params.party_id = partyId;
|
|
81
|
+
}
|
|
82
|
+
|
|
72
83
|
const res = await transactionLookup(params);
|
|
73
84
|
if (res.is_success && res.result) {
|
|
74
85
|
const payload = res.result as any;
|
|
@@ -84,8 +95,15 @@ export function ProductSelectionPanel({
|
|
|
84
95
|
if (matchType === 'serial') {
|
|
85
96
|
onAddItem(entity.item, entity.variant, { serial_numbers: [entity.serial_number], quantity: 1 });
|
|
86
97
|
} else if (matchType === 'batch') {
|
|
87
|
-
const batchVariant = entity.variant_id && entity.item?.variants ? entity.item.variants.find((v: any) => v.id === entity.variant_id) : null;
|
|
88
|
-
|
|
98
|
+
const batchVariant = entity.variant || (entity.variant_id && entity.item?.variants ? entity.item.variants.find((v: any) => v.id === entity.variant_id) : null);
|
|
99
|
+
if (['sales', 'quotation'].includes(config.type)) {
|
|
100
|
+
onAddItem(entity.item, batchVariant, {
|
|
101
|
+
batches: [{ batch_id: entity.id, batch_number: entity.batch_number, selling_price: entity.selling_price, quantity: 1 }],
|
|
102
|
+
quantity: 1
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
setActiveBatchItem({ item: entity.item, variant: batchVariant, preSelectedBatch: entity });
|
|
106
|
+
}
|
|
89
107
|
} else if (matchType === 'variant') {
|
|
90
108
|
handleSelectItem(entity.item, entity);
|
|
91
109
|
} else if (matchType === 'item') {
|
|
@@ -103,7 +121,7 @@ export function ProductSelectionPanel({
|
|
|
103
121
|
}
|
|
104
122
|
}, 250);
|
|
105
123
|
},
|
|
106
|
-
[warehouseId, autoAddEnabled, config.type]
|
|
124
|
+
[warehouseId, autoAddEnabled, config.type, partyId]
|
|
107
125
|
);
|
|
108
126
|
|
|
109
127
|
// ── Select item → check tracking type or add to cart ──
|
|
@@ -114,14 +132,18 @@ export function ProductSelectionPanel({
|
|
|
114
132
|
setShowResults(false);
|
|
115
133
|
|
|
116
134
|
if (!config.disableTrackingSelection && item.tracking_type === 'batch') {
|
|
117
|
-
|
|
135
|
+
if (['sales', 'quotation'].includes(config.type)) {
|
|
136
|
+
onAddItem(item, variant);
|
|
137
|
+
} else {
|
|
138
|
+
setActiveBatchItem({ item, variant });
|
|
139
|
+
}
|
|
118
140
|
} else if (!config.disableTrackingSelection && item.tracking_type === 'serial') {
|
|
119
141
|
setActiveSerialItem({ item, variant });
|
|
120
142
|
} else {
|
|
121
143
|
onAddItem(item, variant);
|
|
122
144
|
}
|
|
123
145
|
},
|
|
124
|
-
[onAddItem]
|
|
146
|
+
[onAddItem, config.type, config.disableTrackingSelection]
|
|
125
147
|
);
|
|
126
148
|
|
|
127
149
|
const handleBatchConfirm = (selections: { batch: any; quantity: number }[]) => {
|
|
@@ -227,18 +249,38 @@ export function ProductSelectionPanel({
|
|
|
227
249
|
}
|
|
228
250
|
};
|
|
229
251
|
|
|
230
|
-
const updateLineInput = (uid: string, field: 'quantity' | 'unit_price' | 'free_qty' | 'uom_id', value: string) => {
|
|
252
|
+
const updateLineInput = (uid: string, field: 'quantity' | 'unit_price' | 'free_qty' | 'uom_id' | 'discount_amount' | 'discount_percent', value: string | number) => {
|
|
231
253
|
const num = value === '' ? 0 : Number(value);
|
|
232
254
|
const line = lineItems.find(l => l.uid === uid);
|
|
233
255
|
if (!line) return;
|
|
234
256
|
|
|
235
257
|
let qty = line.quantity;
|
|
236
258
|
let price = line.unit_price;
|
|
259
|
+
let discAmt = line.discount_amount;
|
|
260
|
+
let discPercent = line.discount_percent;
|
|
237
261
|
|
|
238
262
|
if (field === 'quantity') qty = Math.max(0.001, num);
|
|
239
263
|
if (field === 'unit_price') price = Math.max(0, num);
|
|
240
|
-
|
|
241
|
-
|
|
264
|
+
if (field === 'discount_amount') {
|
|
265
|
+
const maxDiscount = price;
|
|
266
|
+
const unitDiscount = Math.min(Math.max(0, num), maxDiscount);
|
|
267
|
+
discAmt = unitDiscount * qty;
|
|
268
|
+
discPercent = (price * qty) > 0 ? (discAmt * 100) / (price * qty) : 0;
|
|
269
|
+
} else if (field === 'discount_percent') {
|
|
270
|
+
discPercent = Math.min(Math.max(0, num), 100);
|
|
271
|
+
discAmt = (price * qty * discPercent) / 100;
|
|
272
|
+
} else {
|
|
273
|
+
// If price or qty changes, check which discount type is active
|
|
274
|
+
const activeDiscountType = discountTypes[uid] || 'amount';
|
|
275
|
+
|
|
276
|
+
if (activeDiscountType === 'percent') {
|
|
277
|
+
// Preserve percentage, recalculate amount
|
|
278
|
+
discAmt = (price * qty * discPercent) / 100;
|
|
279
|
+
} else {
|
|
280
|
+
// Preserve amount (we will clamp on blur to avoid ruining it while typing), recalculate percentage
|
|
281
|
+
discPercent = (price * qty) > 0 ? (discAmt * 100) / (price * qty) : 0;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
242
284
|
|
|
243
285
|
if (field === 'uom_id') {
|
|
244
286
|
const newUomId = Number(value);
|
|
@@ -260,8 +302,6 @@ export function ProductSelectionPanel({
|
|
|
260
302
|
|
|
261
303
|
const newDiscAmt = (newPrice * line.quantity * line.discount_percent) / 100;
|
|
262
304
|
|
|
263
|
-
setEditingRates(prev => ({ ...prev, [uid]: newPrice.toFixed(2) }));
|
|
264
|
-
|
|
265
305
|
onUpdateLine(uid, {
|
|
266
306
|
uom_id: newUomId,
|
|
267
307
|
unit_price: newPrice,
|
|
@@ -275,6 +315,7 @@ export function ProductSelectionPanel({
|
|
|
275
315
|
quantity: qty,
|
|
276
316
|
unit_price: price,
|
|
277
317
|
discount_amount: discAmt,
|
|
318
|
+
discount_percent: discPercent,
|
|
278
319
|
line_total: price * qty - discAmt,
|
|
279
320
|
});
|
|
280
321
|
};
|
|
@@ -531,15 +572,18 @@ export function ProductSelectionPanel({
|
|
|
531
572
|
<THeader>
|
|
532
573
|
<TRow>
|
|
533
574
|
<TCell isHeader className="w-[45%]">Item</TCell>
|
|
534
|
-
<TCell isHeader>Qty</TCell>
|
|
535
|
-
{config.enablePurchaseUnit && <TCell isHeader>Unit</TCell>}
|
|
536
|
-
{config.showPrices && <TCell isHeader>Rate</TCell>}
|
|
575
|
+
<TCell isHeader className="text-center">Qty</TCell>
|
|
576
|
+
{(config.enablePurchaseUnit || config.enableSalesUnit) && <TCell isHeader>Unit</TCell>}
|
|
577
|
+
{config.showPrices && <TCell isHeader className="text-right">Rate</TCell>}
|
|
578
|
+
{showDiscountCol && <TCell isHeader className="text-right">Disc.</TCell>}
|
|
537
579
|
{config.showPrices && <TCell isHeader className="text-right">Total</TCell>}
|
|
538
580
|
<TCell isHeader className="w-10 text-center"></TCell>
|
|
539
581
|
</TRow>
|
|
540
582
|
</THeader>
|
|
541
583
|
<TBody>
|
|
542
|
-
{lineItems.map((line) =>
|
|
584
|
+
{lineItems.map((line) => {
|
|
585
|
+
const isBatchPending = ['sales', 'quotation'].includes(config.type) && line.item.tracking_type === 'batch' && (!line.batches || line.batches.length === 0);
|
|
586
|
+
return (
|
|
543
587
|
<TRow key={line.uid}>
|
|
544
588
|
<TCell label="Item" className="py-2.5">
|
|
545
589
|
<div className="flex items-start gap-3">
|
|
@@ -564,9 +608,56 @@ export function ProductSelectionPanel({
|
|
|
564
608
|
? `${line.item.variants.find(v => v.id === line.variant_id)?.variant_name} • ${line.item.variants.find(v => v.id === line.variant_id)?.sku}`
|
|
565
609
|
: line.item.sku}
|
|
566
610
|
</span>
|
|
567
|
-
{!config.disableTrackingSelection && (line.item.tracking_type === 'batch' || line.item.tracking_type === 'serial') && (
|
|
611
|
+
{((!config.disableTrackingSelection && (line.item.tracking_type === 'batch' || line.item.tracking_type === 'serial')) || (line.item.tracking_type === 'batch' && ['sales', 'quotation'].includes(config.type))) && (
|
|
568
612
|
<div className="flex flex-wrap gap-1 mt-1.5">
|
|
569
613
|
{line.item.tracking_type === 'batch' && (() => {
|
|
614
|
+
if (['sales', 'quotation'].includes(config.type)) {
|
|
615
|
+
return (
|
|
616
|
+
<div className="mt-1.5 flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
|
617
|
+
{(!line.batches || line.batches.length === 0) && (
|
|
618
|
+
<AlertTriangle size={16} className="text-danger shrink-0" />
|
|
619
|
+
)}
|
|
620
|
+
<div className="w-56">
|
|
621
|
+
<AsyncSearchableSelect
|
|
622
|
+
controlClassName="!min-h-8 h-8 !py-1 text-sm"
|
|
623
|
+
placeholder="Select Batch..."
|
|
624
|
+
loadOptions={async (search) => {
|
|
625
|
+
const params: any = {};
|
|
626
|
+
if (line.variant_id) params.variant_id = line.variant_id;
|
|
627
|
+
if (warehouseId) params.warehouse_id = warehouseId;
|
|
628
|
+
const res = await getItemBatches(line.item.id, params);
|
|
629
|
+
const batches = res?.result?.data || res?.result || [];
|
|
630
|
+
return batches.filter((b: any) => b.batch_number.toLowerCase().includes(search.toLowerCase()));
|
|
631
|
+
}}
|
|
632
|
+
option={{
|
|
633
|
+
label: 'batch_number',
|
|
634
|
+
value: 'id',
|
|
635
|
+
renderOption: (b: any) => (
|
|
636
|
+
<div className="flex items-center justify-between w-full pr-1">
|
|
637
|
+
<span>{b.batch_number}</span>
|
|
638
|
+
{warehouseId ? (
|
|
639
|
+
<span className="text-foreground-subtle text-[11px] font-mono" title="Available Stock">{b.qty_available ?? 0}</span>
|
|
640
|
+
) : null}
|
|
641
|
+
</div>
|
|
642
|
+
)
|
|
643
|
+
}}
|
|
644
|
+
defaultValue={line.batches?.[0] ? { id: line.batches[0].batch_id, batch_number: line.batches[0].batch_number, selling_price: line.batches[0].selling_price } : undefined}
|
|
645
|
+
onChange={(val, selected: any) => {
|
|
646
|
+
if (selected) {
|
|
647
|
+
onUpdateLine(line.uid, {
|
|
648
|
+
batches: [{ batch_id: selected.id, batch_number: selected.batch_number, selling_price: selected.selling_price, quantity: line.quantity }],
|
|
649
|
+
unit_price: selected.selling_price || 0
|
|
650
|
+
});
|
|
651
|
+
} else {
|
|
652
|
+
onUpdateLine(line.uid, { batches: [] });
|
|
653
|
+
}
|
|
654
|
+
}}
|
|
655
|
+
/>
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
570
661
|
const batchesCount = line.batches?.length || 0;
|
|
571
662
|
const selectedTotal = line.batches?.reduce((sum, b) => sum + b.quantity, 0) || 0;
|
|
572
663
|
const isMismatch = selectedTotal !== line.quantity;
|
|
@@ -619,22 +710,22 @@ export function ProductSelectionPanel({
|
|
|
619
710
|
})()}
|
|
620
711
|
</div>
|
|
621
712
|
)}
|
|
622
|
-
{
|
|
713
|
+
{Number(line.unit_price) === 0 && (
|
|
623
714
|
<div className="mt-2 flex items-center gap-2">
|
|
624
|
-
<label className={cn("flex items-center gap-1.5", !!line.purchase_order_line_id ? "cursor-not-allowed" : "cursor-pointer")}>
|
|
715
|
+
<label className={cn("flex items-center gap-1.5", !!(line.purchase_order_line_id || line.sales_order_line_id || line.sales_quotation_line_id) ? "cursor-not-allowed" : "cursor-pointer")}>
|
|
625
716
|
<input
|
|
626
717
|
type="checkbox"
|
|
627
718
|
checked={line.is_free_qty || false}
|
|
628
719
|
onChange={(e) => handleToggleFreeQty(line.uid, e.target.checked)}
|
|
629
|
-
disabled={!!line.purchase_order_line_id}
|
|
720
|
+
disabled={!!(line.purchase_order_line_id || line.sales_order_line_id || line.sales_quotation_line_id)}
|
|
630
721
|
className={cn(
|
|
631
722
|
"rounded border-border-subtle text-primary h-3 w-3",
|
|
632
|
-
!!line.purchase_order_line_id ? "opacity-50 cursor-not-allowed" : "focus:ring-primary"
|
|
723
|
+
!!(line.purchase_order_line_id || line.sales_order_line_id || line.sales_quotation_line_id) ? "opacity-50 cursor-not-allowed" : "focus:ring-primary"
|
|
633
724
|
)}
|
|
634
725
|
/>
|
|
635
726
|
<span className={cn(
|
|
636
727
|
"text-[11.5px] font-medium transition-colors",
|
|
637
|
-
!!line.purchase_order_line_id ? "text-foreground-disabled" : "text-foreground-subtle hover:text-foreground-1"
|
|
728
|
+
!!(line.purchase_order_line_id || line.sales_order_line_id || line.sales_quotation_line_id) ? "text-foreground-disabled" : "text-foreground-subtle hover:text-foreground-1"
|
|
638
729
|
)}>
|
|
639
730
|
Free Quantity
|
|
640
731
|
</span>
|
|
@@ -644,17 +735,17 @@ export function ProductSelectionPanel({
|
|
|
644
735
|
<div className="flex items-center">
|
|
645
736
|
<button
|
|
646
737
|
onClick={() => setActiveParentLineUidFor(line.uid)}
|
|
647
|
-
disabled={!!line.purchase_order_line_id}
|
|
738
|
+
disabled={!!(line.purchase_order_line_id || line.sales_order_line_id || line.sales_quotation_line_id)}
|
|
648
739
|
className={cn(
|
|
649
740
|
"h-6 text-[11.5px] bg-surface-1 border border-border-subtle rounded-md px-2 ml-2 min-w-[120px] max-w-[180px] text-left truncate transition-colors flex justify-between items-center",
|
|
650
|
-
!!line.purchase_order_line_id ? "opacity-60 cursor-not-allowed" : "hover:bg-surface-hover focus:border-primary/50 focus:outline-none"
|
|
741
|
+
!!(line.purchase_order_line_id || line.sales_order_line_id || line.sales_quotation_line_id) ? "opacity-60 cursor-not-allowed" : "hover:bg-surface-hover focus:border-primary/50 focus:outline-none"
|
|
651
742
|
)}
|
|
652
743
|
>
|
|
653
744
|
<span>
|
|
654
745
|
{line.parent_line_uids && line.parent_line_uids.length > 0
|
|
655
746
|
? (() => {
|
|
656
747
|
if (line.parent_line_uids.length === 1) {
|
|
657
|
-
const p = lineItems.find((l) => l.uid === line.parent_line_uids
|
|
748
|
+
const p = lineItems.find((l) => l.uid === line.parent_line_uids?.[0]);
|
|
658
749
|
return p
|
|
659
750
|
? p.item.has_variants && p.variant_id && p.item.variants?.find((v) => v.id === p.variant_id)
|
|
660
751
|
? `${p.item.name} (${p.item.variants.find((v) => v.id === p.variant_id)?.variant_name})`
|
|
@@ -675,25 +766,24 @@ export function ProductSelectionPanel({
|
|
|
675
766
|
</div>
|
|
676
767
|
</div>
|
|
677
768
|
</TCell>
|
|
678
|
-
|
|
679
|
-
<TCell label="Qty" className="py-2.5">
|
|
769
|
+
<TCell label="Qty" className="py-2.5 text-center">
|
|
680
770
|
<input
|
|
681
771
|
type="number"
|
|
682
772
|
value={line.quantity}
|
|
683
773
|
onChange={(e) => updateLineInput(line.uid, 'quantity', e.target.value)}
|
|
684
|
-
className="w-20 h-
|
|
774
|
+
className="w-20 h-10 px-2 text-[13px] text-center font-medium bg-surface-1 border border-border-subtle rounded-md focus:border-primary/50 focus:outline-none transition-colors"
|
|
685
775
|
min={0.001}
|
|
686
776
|
step="any"
|
|
687
777
|
/>
|
|
688
778
|
</TCell>
|
|
689
779
|
|
|
690
|
-
{config.enablePurchaseUnit && (
|
|
780
|
+
{(config.enablePurchaseUnit || config.enableSalesUnit) && (
|
|
691
781
|
<TCell label="Unit" className="py-2.5">
|
|
692
782
|
<select
|
|
693
783
|
value={line.uom_id || line.item.uom_id || ''}
|
|
694
784
|
onChange={(e) => updateLineInput(line.uid, 'uom_id', e.target.value)}
|
|
695
|
-
disabled={line.item.tracking_type === 'serial'}
|
|
696
|
-
className="h-
|
|
785
|
+
disabled={line.item.tracking_type === 'serial' || isBatchPending}
|
|
786
|
+
className="h-10 px-2 text-[13px] font-medium bg-surface-1 border border-border-subtle rounded-md focus:border-primary/50 focus:outline-none transition-colors min-w-[80px] disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-50"
|
|
697
787
|
>
|
|
698
788
|
{line.item.uom?.uom_group?.uoms?.map((uom: any) => (
|
|
699
789
|
<option key={uom.id} value={uom.id}>{uom.abbreviation}</option>
|
|
@@ -703,30 +793,93 @@ export function ProductSelectionPanel({
|
|
|
703
793
|
)}
|
|
704
794
|
|
|
705
795
|
{config.showPrices && (
|
|
706
|
-
<TCell label="Rate" className="py-2.5">
|
|
707
|
-
<
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
796
|
+
<TCell label="Rate" className="py-2.5 text-right relative">
|
|
797
|
+
<div className="flex items-center justify-end w-full gap-2">
|
|
798
|
+
{line.applied_price_rule?.price_list_name && (() => {
|
|
799
|
+
const variantPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'sale_price';
|
|
800
|
+
const actualVariant = line.variant_id ? line.item.variants?.find((v: any) => v.id === line.variant_id) : (line.item.variants?.[0] || null);
|
|
801
|
+
const batchPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'selling_price';
|
|
802
|
+
const batchPrice = ['sales', 'quotation'].includes(config.type) && line.batches?.[0] ? (line.batches[0] as any)[batchPriceField] : undefined;
|
|
803
|
+
const basePrice = (batchPrice !== undefined && batchPrice !== null)
|
|
804
|
+
? Number(batchPrice)
|
|
805
|
+
: (actualVariant
|
|
806
|
+
? Number(actualVariant[variantPriceField] || line.item[config.priceField] || 0)
|
|
807
|
+
: Number(line.item[config.priceField] || 0));
|
|
808
|
+
const finalPrice = Number(line.applied_price_rule?.final_rate || 0);
|
|
809
|
+
if (basePrice === finalPrice) return null;
|
|
810
|
+
|
|
811
|
+
return (
|
|
812
|
+
<HintIcon
|
|
813
|
+
text={`Applied Rule: ${line.applied_price_rule?.price_list_name}\nBase Rate: ${basePrice.toFixed(2)}${
|
|
814
|
+
line.applied_price_rule?.rate ? `\nNew Rate: ${line.applied_price_rule?.rate}` :
|
|
815
|
+
line.applied_price_rule?.markdown_percentage ? `\nMarkdown: ${line.applied_price_rule?.markdown_percentage}%` : ''
|
|
816
|
+
}\nFinal Rate: ${Number(line.applied_price_rule?.final_rate || 0).toFixed(2)}`}
|
|
817
|
+
icon={<Tag size={15} className="text-primary-600 opacity-80 hover:opacity-100 transition-opacity" />}
|
|
818
|
+
/>
|
|
819
|
+
);
|
|
820
|
+
})()}
|
|
821
|
+
<input
|
|
822
|
+
type="text"
|
|
823
|
+
value={editingRates[line.uid] ?? Number(line.unit_price || 0).toFixed(2)}
|
|
824
|
+
onChange={(e) => {
|
|
825
|
+
const val = e.target.value;
|
|
826
|
+
if (/^\d*\.?\d{0,2}$/.test(val)) {
|
|
827
|
+
setEditingRates(prev => ({ ...prev, [line.uid]: val }));
|
|
828
|
+
updateLineInput(line.uid, 'unit_price', val);
|
|
829
|
+
}
|
|
830
|
+
}}
|
|
831
|
+
onBlur={() => {
|
|
832
|
+
setEditingRates(prev => {
|
|
833
|
+
const next = { ...prev };
|
|
834
|
+
delete next[line.uid];
|
|
835
|
+
return next;
|
|
836
|
+
});
|
|
837
|
+
// Clamp the discount amount if it exceeds the new rate
|
|
838
|
+
if (discountTypes[line.uid] !== 'percent') {
|
|
839
|
+
updateLineInput(line.uid, 'discount_amount', line.quantity > 0 ? line.discount_amount / line.quantity : 0);
|
|
840
|
+
}
|
|
841
|
+
}}
|
|
842
|
+
disabled={!config.allowEditPrice || line.is_free_qty || !line.item.is_discountable || isBatchPending}
|
|
843
|
+
className={cn(
|
|
844
|
+
"w-24 h-10 px-2 text-[13px] text-right font-medium bg-surface-1 border border-border-subtle rounded-md focus:border-primary/50 focus:outline-none transition-colors",
|
|
845
|
+
(!config.allowEditPrice || line.is_free_qty || !line.item.is_discountable || isBatchPending) && "opacity-60 cursor-not-allowed bg-surface-hover"
|
|
846
|
+
)}
|
|
847
|
+
/>
|
|
848
|
+
</div>
|
|
849
|
+
</TCell>
|
|
850
|
+
)}
|
|
851
|
+
|
|
852
|
+
{showDiscountCol && (
|
|
853
|
+
<TCell label="Disc." className="py-2.5 text-right">
|
|
854
|
+
<div className={cn(
|
|
855
|
+
"flex items-center w-32 h-10 bg-surface-1 border border-border-subtle rounded-md overflow-hidden focus-within:border-primary/50 transition-colors ml-auto",
|
|
856
|
+
(!config.allowEditPrice || line.is_free_qty || !line.item.is_discountable || line.unit_price === 0 || isBatchPending) && "opacity-60 bg-surface-hover"
|
|
857
|
+
)}>
|
|
858
|
+
<select
|
|
859
|
+
className="h-full bg-surface-hover text-[11px] font-medium text-foreground-subtle border-r border-border-subtle pl-1.5 pr-0.5 focus:outline-none cursor-pointer disabled:cursor-not-allowed"
|
|
860
|
+
value={discountTypes[line.uid] || 'amount'}
|
|
861
|
+
onChange={(e) => {
|
|
862
|
+
setDiscountTypes(prev => ({ ...prev, [line.uid]: e.target.value as 'amount' | 'percent' }));
|
|
863
|
+
}}
|
|
864
|
+
disabled={!config.allowEditPrice || line.is_free_qty || !line.item.is_discountable || line.unit_price === 0 || isBatchPending}
|
|
865
|
+
>
|
|
866
|
+
<option value="amount">Amt</option>
|
|
867
|
+
<option value="percent">%</option>
|
|
868
|
+
</select>
|
|
869
|
+
<input
|
|
870
|
+
type="number"
|
|
871
|
+
value={(discountTypes[line.uid] === 'percent' ? line.discount_percent : (line.quantity > 0 ? Number((line.discount_amount / line.quantity).toFixed(2)) : 0)) || ''}
|
|
872
|
+
placeholder="0.00"
|
|
873
|
+
onChange={(e) => {
|
|
874
|
+
const val = e.target.value;
|
|
875
|
+
updateLineInput(line.uid, discountTypes[line.uid] === 'percent' ? 'discount_percent' : 'discount_amount', val ? Number(val) : 0);
|
|
876
|
+
}}
|
|
877
|
+
disabled={!config.allowEditPrice || line.is_free_qty || !line.item.is_discountable || line.unit_price === 0 || isBatchPending}
|
|
878
|
+
className="flex-1 w-full min-w-0 h-full px-2 text-[13px] text-right font-medium bg-transparent focus:outline-none disabled:cursor-not-allowed"
|
|
879
|
+
min={0}
|
|
880
|
+
step="any"
|
|
881
|
+
/>
|
|
882
|
+
</div>
|
|
730
883
|
</TCell>
|
|
731
884
|
)}
|
|
732
885
|
|
|
@@ -739,14 +892,15 @@ export function ProductSelectionPanel({
|
|
|
739
892
|
<TCell className="text-center py-2.5">
|
|
740
893
|
<button
|
|
741
894
|
onClick={() => onRemoveLine(line.uid)}
|
|
742
|
-
className="w-
|
|
895
|
+
className="w-10 h-10 inline-flex items-center justify-center rounded-md text-foreground-disabled hover:bg-danger-alt/10 hover:text-danger-alt transition-colors"
|
|
743
896
|
title="Remove Item"
|
|
744
897
|
>
|
|
745
898
|
<Trash2 size={16} />
|
|
746
899
|
</button>
|
|
747
900
|
</TCell>
|
|
748
901
|
</TRow>
|
|
749
|
-
)
|
|
902
|
+
);
|
|
903
|
+
})}
|
|
750
904
|
</TBody>
|
|
751
905
|
</Table>
|
|
752
906
|
)}
|
|
@@ -788,10 +942,15 @@ export function ProductSelectionPanel({
|
|
|
788
942
|
{lineItems.length > 0 && (
|
|
789
943
|
<div className="mt-3 pt-3 border-t border-border-subtle">
|
|
790
944
|
<div className="flex items-center justify-between text-[12px] text-foreground-subtle">
|
|
791
|
-
<span>{lineItems.length} item{lineItems.length > 1 ? 's' : ''} · {lineItems.reduce((s, l) =>
|
|
945
|
+
<span>{lineItems.length} item{lineItems.length > 1 ? 's' : ''} · {lineItems.reduce((s, l) => {
|
|
946
|
+
const uoms = l.item.uom?.uom_group?.uoms || [];
|
|
947
|
+
const currentUom = uoms.find((u: any) => u.id === (l.uom_id || l.item.uom_id));
|
|
948
|
+
const factor = Number(currentUom?.conversion_factor) || 1;
|
|
949
|
+
return s + (l.quantity * factor);
|
|
950
|
+
}, 0)} units</span>
|
|
792
951
|
{config.showPrices && (
|
|
793
952
|
<span className="text-[15px] font-bold text-foreground-0">
|
|
794
|
-
{formatCurrency(lineItems.reduce((s, l) => s + l.line_total, 0))}
|
|
953
|
+
{formatCurrency(lineItems.reduce((s, l) => s + Number(l.line_total || 0), 0))}
|
|
795
954
|
</span>
|
|
796
955
|
)}
|
|
797
956
|
</div>
|