@apptimate/ui 4.1.0 → 4.2.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/common-components/DashboardLayout.tsx +23 -2
- package/src/common-components/transaction/MetadataPanel.tsx +12 -72
- package/src/common-components/transaction/ProductSelectionPanel.tsx +38 -19
- package/src/common-components/transaction/ProductTransactionScreen.tsx +72 -1
- package/src/common-components/transaction/types.ts +12 -1
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import { Briefcase, CreditCard, Settings, Menu, X, LogOut, User as UserIcon, Bui
|
|
|
5
5
|
import { Dropdown, DropdownItem } from '../base-components/Dropdown';
|
|
6
6
|
|
|
7
7
|
import Link from 'next/link';
|
|
8
|
-
import { usePathname } from 'next/navigation';
|
|
8
|
+
import { usePathname, useRouter } from 'next/navigation';
|
|
9
9
|
import { cn } from '@apptimate/core-lib';
|
|
10
10
|
|
|
11
11
|
export type DashboardMenuConfig = {
|
|
@@ -68,6 +68,7 @@ export function DashboardLayout({
|
|
|
68
68
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
69
69
|
const [isOrgModalOpen, setIsOrgModalOpen] = useState(false);
|
|
70
70
|
const pathname = usePathname() || "";
|
|
71
|
+
const router = useRouter();
|
|
71
72
|
|
|
72
73
|
// Find which main menu should be active based on current path.
|
|
73
74
|
// When basePath is set, prefer the menu whose items are mostly within basePath.
|
|
@@ -106,6 +107,26 @@ export function DashboardLayout({
|
|
|
106
107
|
|
|
107
108
|
const activeMenu = menus.find(m => m.id === activeMenuId) || menus[0];
|
|
108
109
|
|
|
110
|
+
const handleMainMenuSelect = (menu: DashboardMenuConfig) => {
|
|
111
|
+
setActiveMenuId(menu.id);
|
|
112
|
+
|
|
113
|
+
const firstItem = menu.groups.flatMap(group => group.items)[0];
|
|
114
|
+
if (!firstItem) return;
|
|
115
|
+
|
|
116
|
+
const isInternal = basePath
|
|
117
|
+
? firstItem.path.startsWith(basePath)
|
|
118
|
+
: !externalPaths.some(path => firstItem.path.startsWith(path));
|
|
119
|
+
const href = basePath && isInternal
|
|
120
|
+
? firstItem.path.replace(basePath, "") || "/"
|
|
121
|
+
: firstItem.path;
|
|
122
|
+
|
|
123
|
+
if (isInternal) {
|
|
124
|
+
router.push(href);
|
|
125
|
+
} else {
|
|
126
|
+
window.location.href = href;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
109
130
|
const handleSelectOrg = (org: OrganizationConfig) => {
|
|
110
131
|
onOrganizationChange?.(org);
|
|
111
132
|
setIsOrgModalOpen(false);
|
|
@@ -158,7 +179,7 @@ export function DashboardLayout({
|
|
|
158
179
|
return (
|
|
159
180
|
<div
|
|
160
181
|
key={menu.id}
|
|
161
|
-
onClick={() =>
|
|
182
|
+
onClick={() => handleMainMenuSelect(menu)}
|
|
162
183
|
className={`flex flex-col items-center justify-center gap-1.5 cursor-pointer transition-colors w-full px-1 ${isActive ? "text-[#2D3142]" : "text-gray-400 hover:text-[#2D3142]"
|
|
163
184
|
}`}
|
|
164
185
|
>
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from 'lucide-react';
|
|
10
10
|
import { getPaymentModes, getBankAccountsLookup } from '@apptimate/core-lib';
|
|
11
11
|
import { PartyPicker, WarehousePicker } from '../pickers';
|
|
12
|
+
import { PaymentSection } from '../PaymentSection';
|
|
12
13
|
import type {
|
|
13
14
|
TransactionScreenConfig, PaymentMode, PaymentEntry, BankAccount,
|
|
14
15
|
PartyLookup, PaymentModeField, TransactionLineItem,
|
|
@@ -34,12 +35,13 @@ interface MetadataPanelProps {
|
|
|
34
35
|
onSubmit: () => void;
|
|
35
36
|
isSubmitting: boolean;
|
|
36
37
|
renderExtraMeta?: () => React.ReactNode;
|
|
38
|
+
fetchChequeLeaves?: (query: string, page: number) => Promise<{ is_success: boolean; result?: any[] }>;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export function MetadataPanel({
|
|
40
42
|
config, lineItems, partyId, partyName, warehouseId, warehouseName, transactionDate, expectedDeliveryDate, notes, payments,
|
|
41
43
|
onPartyChange, onWarehouseChange, onDateChange, onExpectedDeliveryDateChange, onNotesChange, onPaymentsChange, onSubmit, isSubmitting,
|
|
42
|
-
renderExtraMeta,
|
|
44
|
+
renderExtraMeta, fetchChequeLeaves,
|
|
43
45
|
}: MetadataPanelProps) {
|
|
44
46
|
const [paymentModes, setPaymentModes] = useState<PaymentMode[]>([]);
|
|
45
47
|
const [bankAccounts, setBankAccounts] = useState<BankAccount[]>([]);
|
|
@@ -226,77 +228,15 @@ export function MetadataPanel({
|
|
|
226
228
|
{/* ── Payment Section ── */}
|
|
227
229
|
{config.showPayment && (
|
|
228
230
|
<Section label="Payment">
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return (
|
|
239
|
-
<PaymentEntryRow
|
|
240
|
-
key={idx}
|
|
241
|
-
entry={entry}
|
|
242
|
-
mode={mode}
|
|
243
|
-
bankAccounts={bankAccounts}
|
|
244
|
-
onUpdate={(updates) => updatePayment(idx, updates)}
|
|
245
|
-
onRemove={() => removePayment(idx)}
|
|
246
|
-
/>
|
|
247
|
-
);
|
|
248
|
-
})}
|
|
249
|
-
|
|
250
|
-
{/* Add payment mode buttons */}
|
|
251
|
-
{(lineItems.length > 0) && (
|
|
252
|
-
<div className="flex flex-wrap gap-2 pt-1">
|
|
253
|
-
{paymentModes
|
|
254
|
-
.filter((m) => m.is_active)
|
|
255
|
-
.map((mode) => {
|
|
256
|
-
const modeIcon = mode.code === 'cash' ? <Banknote size={14} /> :
|
|
257
|
-
mode.code === 'card' ? <CreditCard size={14} /> :
|
|
258
|
-
mode.code === 'bank_transfer' ? <Building size={14} /> :
|
|
259
|
-
<CreditCard size={14} />;
|
|
260
|
-
return (
|
|
261
|
-
<button
|
|
262
|
-
key={mode.id}
|
|
263
|
-
onClick={() => addPayment(mode)}
|
|
264
|
-
className="inline-flex items-center gap-2 px-4 py-2 text-[12px] font-semibold rounded-[10px] bg-surface-0 border border-border-subtle text-foreground-1 hover:border-primary/40 hover:bg-primary/5 hover:text-primary transition-all"
|
|
265
|
-
>
|
|
266
|
-
{modeIcon}
|
|
267
|
-
{mode.name}
|
|
268
|
-
</button>
|
|
269
|
-
);
|
|
270
|
-
})}
|
|
271
|
-
</div>
|
|
272
|
-
)}
|
|
273
|
-
|
|
274
|
-
{/* Balance */}
|
|
275
|
-
{payments.length > 0 && (
|
|
276
|
-
<div className="bg-surface-0 rounded-[10px] border border-border-subtle p-3.5 mt-2 space-y-1.5">
|
|
277
|
-
<TotalRow label="Total Paid" value={formatCurrency(totalPaid)} />
|
|
278
|
-
<TotalRow
|
|
279
|
-
label={balance > 0.01 ? "Balance Due" : balance < -0.01 ? "Change Due" : "Settled"}
|
|
280
|
-
value={formatCurrency(Math.abs(balance))}
|
|
281
|
-
className={balance > 0.01 ? 'text-danger-alt' : balance < -0.01 ? 'text-amber-600' : 'text-green-600'}
|
|
282
|
-
bold
|
|
283
|
-
/>
|
|
284
|
-
{balance < -0.01 && (
|
|
285
|
-
<div className="flex items-center gap-1.5 mt-1 text-[11px] text-amber-600 font-medium">
|
|
286
|
-
<AlertCircle size={12} />
|
|
287
|
-
<span>Change: {formatCurrency(Math.abs(balance))}</span>
|
|
288
|
-
</div>
|
|
289
|
-
)}
|
|
290
|
-
{Math.abs(balance) < 0.01 && (
|
|
291
|
-
<div className="flex items-center gap-1.5 mt-1 text-[11px] text-green-600 font-medium">
|
|
292
|
-
<Check size={12} />
|
|
293
|
-
<span>Fully paid</span>
|
|
294
|
-
</div>
|
|
295
|
-
)}
|
|
296
|
-
</div>
|
|
297
|
-
)}
|
|
298
|
-
</div>
|
|
299
|
-
)}
|
|
231
|
+
<PaymentSection
|
|
232
|
+
totalAmount={grandTotal}
|
|
233
|
+
payments={payments}
|
|
234
|
+
onPaymentsChange={onPaymentsChange}
|
|
235
|
+
fetchPaymentModes={getPaymentModes}
|
|
236
|
+
fetchBankAccounts={getBankAccountsLookup}
|
|
237
|
+
fetchChequeLeaves={fetchChequeLeaves}
|
|
238
|
+
compact={true}
|
|
239
|
+
/>
|
|
300
240
|
</Section>
|
|
301
241
|
)}
|
|
302
242
|
|
|
@@ -312,9 +312,10 @@ export function ProductSelectionPanel({
|
|
|
312
312
|
return (
|
|
313
313
|
<div className="flex flex-col h-full">
|
|
314
314
|
{/* ── Search Bar ── */}
|
|
315
|
-
|
|
316
|
-
<div className="
|
|
317
|
-
<
|
|
315
|
+
{!config.disableItemSelection && (
|
|
316
|
+
<div ref={panelRef} className="relative mb-4">
|
|
317
|
+
<div className="flex items-center gap-2 bg-surface-0 border-[1.5px] border-border-subtle rounded-[12px] px-4 py-2.5 focus-within:border-primary/50 transition-all">
|
|
318
|
+
<Search size={18} className="text-foreground-disabled shrink-0" />
|
|
318
319
|
<input
|
|
319
320
|
ref={inputRef}
|
|
320
321
|
type="text"
|
|
@@ -513,6 +514,7 @@ export function ProductSelectionPanel({
|
|
|
513
514
|
</div>
|
|
514
515
|
)}
|
|
515
516
|
</div>
|
|
517
|
+
)}
|
|
516
518
|
|
|
517
519
|
{/* ── Selected Items Table ── */}
|
|
518
520
|
<div className="flex-1 overflow-y-auto custom-scrollbar w-full">
|
|
@@ -549,9 +551,16 @@ export function ProductSelectionPanel({
|
|
|
549
551
|
)}
|
|
550
552
|
</div>
|
|
551
553
|
<div className="flex flex-col flex-1 min-w-0">
|
|
552
|
-
<span className="text-[13px] font-semibold text-foreground-1 leading-tight break-words">
|
|
554
|
+
<span className="text-[13px] font-semibold text-foreground-1 leading-tight break-words">
|
|
555
|
+
{line.item.name}
|
|
556
|
+
{line.is_free_qty && (
|
|
557
|
+
<span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-[9px] font-bold bg-green-100 text-green-700 uppercase tracking-wider align-middle">
|
|
558
|
+
Free
|
|
559
|
+
</span>
|
|
560
|
+
)}
|
|
561
|
+
</span>
|
|
553
562
|
<span className="text-[11px] text-foreground-subtle font-mono mt-0.5 break-words">
|
|
554
|
-
{line.variant_id && line.item.variants?.find(v => v.id === line.variant_id)
|
|
563
|
+
{line.item.has_variants && line.variant_id && line.item.variants?.find(v => v.id === line.variant_id)
|
|
555
564
|
? `${line.item.variants.find(v => v.id === line.variant_id)?.variant_name} • ${line.item.variants.find(v => v.id === line.variant_id)?.sku}`
|
|
556
565
|
: line.item.sku}
|
|
557
566
|
</span>
|
|
@@ -610,23 +619,36 @@ export function ProductSelectionPanel({
|
|
|
610
619
|
})()}
|
|
611
620
|
</div>
|
|
612
621
|
)}
|
|
613
|
-
{(config.type === 'purchase' || config.type === 'grn') && line.unit_price === 0 && (
|
|
622
|
+
{(config.type === 'purchase' || config.type === 'grn') && Number(line.unit_price) === 0 && (
|
|
614
623
|
<div className="mt-2 flex items-center gap-2">
|
|
615
|
-
<label className="flex items-center gap-1.5 cursor-pointer">
|
|
624
|
+
<label className={cn("flex items-center gap-1.5", !!line.purchase_order_line_id ? "cursor-not-allowed" : "cursor-pointer")}>
|
|
616
625
|
<input
|
|
617
626
|
type="checkbox"
|
|
618
627
|
checked={line.is_free_qty || false}
|
|
619
628
|
onChange={(e) => handleToggleFreeQty(line.uid, e.target.checked)}
|
|
620
|
-
|
|
629
|
+
disabled={!!line.purchase_order_line_id}
|
|
630
|
+
className={cn(
|
|
631
|
+
"rounded border-border-subtle text-primary h-3 w-3",
|
|
632
|
+
!!line.purchase_order_line_id ? "opacity-50 cursor-not-allowed" : "focus:ring-primary"
|
|
633
|
+
)}
|
|
621
634
|
/>
|
|
622
|
-
<span className=
|
|
635
|
+
<span className={cn(
|
|
636
|
+
"text-[11.5px] font-medium transition-colors",
|
|
637
|
+
!!line.purchase_order_line_id ? "text-foreground-disabled" : "text-foreground-subtle hover:text-foreground-1"
|
|
638
|
+
)}>
|
|
639
|
+
Free Quantity
|
|
640
|
+
</span>
|
|
623
641
|
</label>
|
|
624
642
|
|
|
625
643
|
{line.is_free_qty && (
|
|
626
644
|
<div className="flex items-center">
|
|
627
645
|
<button
|
|
628
646
|
onClick={() => setActiveParentLineUidFor(line.uid)}
|
|
629
|
-
|
|
647
|
+
disabled={!!line.purchase_order_line_id}
|
|
648
|
+
className={cn(
|
|
649
|
+
"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"
|
|
651
|
+
)}
|
|
630
652
|
>
|
|
631
653
|
<span>
|
|
632
654
|
{line.parent_line_uids && line.parent_line_uids.length > 0
|
|
@@ -634,13 +656,9 @@ export function ProductSelectionPanel({
|
|
|
634
656
|
if (line.parent_line_uids.length === 1) {
|
|
635
657
|
const p = lineItems.find((l) => l.uid === line.parent_line_uids![0]);
|
|
636
658
|
return p
|
|
637
|
-
?
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
p.item.variants?.find((v) => v.id === p.variant_id)?.variant_name
|
|
641
|
-
})`
|
|
642
|
-
: ""
|
|
643
|
-
}`
|
|
659
|
+
? p.item.has_variants && p.variant_id && p.item.variants?.find((v) => v.id === p.variant_id)
|
|
660
|
+
? `${p.item.name} (${p.item.variants.find((v) => v.id === p.variant_id)?.variant_name})`
|
|
661
|
+
: p.item.name
|
|
644
662
|
: "Select Parent Line...";
|
|
645
663
|
} else {
|
|
646
664
|
return `${line.parent_line_uids.length} Parents Selected`;
|
|
@@ -674,7 +692,8 @@ export function ProductSelectionPanel({
|
|
|
674
692
|
<select
|
|
675
693
|
value={line.uom_id || line.item.uom_id || ''}
|
|
676
694
|
onChange={(e) => updateLineInput(line.uid, 'uom_id', e.target.value)}
|
|
677
|
-
|
|
695
|
+
disabled={line.item.tracking_type === 'serial'}
|
|
696
|
+
className="h-8 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"
|
|
678
697
|
>
|
|
679
698
|
{line.item.uom?.uom_group?.uoms?.map((uom: any) => (
|
|
680
699
|
<option key={uom.id} value={uom.id}>{uom.abbreviation}</option>
|
|
@@ -687,7 +706,7 @@ export function ProductSelectionPanel({
|
|
|
687
706
|
<TCell label="Rate" className="py-2.5">
|
|
688
707
|
<input
|
|
689
708
|
type="text"
|
|
690
|
-
value={editingRates[line.uid] ?? line.unit_price.toFixed(2)}
|
|
709
|
+
value={editingRates[line.uid] ?? Number(line.unit_price || 0).toFixed(2)}
|
|
691
710
|
onChange={(e) => {
|
|
692
711
|
const val = e.target.value;
|
|
693
712
|
if (/^\d*\.?\d{0,2}$/.test(val)) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState, useCallback, useMemo } from 'react';
|
|
3
|
+
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
|
4
4
|
import { cn } from '@apptimate/core-lib';
|
|
5
|
+
import toast from 'react-hot-toast';
|
|
5
6
|
import { ProductSelectionPanel } from './ProductSelectionPanel';
|
|
6
7
|
import { MetadataPanel } from './MetadataPanel';
|
|
7
8
|
import type {
|
|
@@ -25,6 +26,8 @@ export function ProductTransactionScreen({
|
|
|
25
26
|
onSubmit,
|
|
26
27
|
isSubmitting: externalSubmitting,
|
|
27
28
|
renderExtraMeta,
|
|
29
|
+
fetchChequeLeaves,
|
|
30
|
+
initialData,
|
|
28
31
|
}: ProductTransactionScreenProps) {
|
|
29
32
|
// ── State ──
|
|
30
33
|
const [lineItems, setLineItems] = useState<TransactionLineItem[]>([]);
|
|
@@ -42,6 +45,43 @@ export function ProductTransactionScreen({
|
|
|
42
45
|
|
|
43
46
|
const submitting = externalSubmitting ?? isSubmitting;
|
|
44
47
|
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (initialData) {
|
|
50
|
+
if (initialData.party_id) {
|
|
51
|
+
setPartyId(initialData.party_id);
|
|
52
|
+
if (initialData.party?.name) setPartyName(initialData.party.name);
|
|
53
|
+
}
|
|
54
|
+
if (initialData.warehouse_id) {
|
|
55
|
+
setWarehouseId(initialData.warehouse_id);
|
|
56
|
+
}
|
|
57
|
+
if (initialData.lines && Array.isArray(initialData.lines)) {
|
|
58
|
+
setLineItems(
|
|
59
|
+
initialData.lines.map((l: any) => {
|
|
60
|
+
const qty = l.quantity || 1;
|
|
61
|
+
const unitPrice = l.unit_price || 0;
|
|
62
|
+
return {
|
|
63
|
+
uid: l.uid || Math.random().toString(36).slice(2, 7),
|
|
64
|
+
item: l.item,
|
|
65
|
+
variant_id: l.variant_id || null,
|
|
66
|
+
batches: [],
|
|
67
|
+
serial_numbers: [],
|
|
68
|
+
quantity: qty,
|
|
69
|
+
unit_price: unitPrice,
|
|
70
|
+
discount_percent: 0,
|
|
71
|
+
discount_amount: 0,
|
|
72
|
+
line_total: unitPrice * qty,
|
|
73
|
+
expanded: false,
|
|
74
|
+
is_free_qty: l.is_free_qty,
|
|
75
|
+
parent_line_uids: l.parent_line_uids,
|
|
76
|
+
uom_id: l.uom_id,
|
|
77
|
+
purchase_order_line_id: l.purchase_order_line_id,
|
|
78
|
+
};
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}, [initialData]);
|
|
84
|
+
|
|
45
85
|
// ── Add item to line items ──
|
|
46
86
|
const handleAddItem = useCallback(
|
|
47
87
|
(
|
|
@@ -80,6 +120,7 @@ export function ProductTransactionScreen({
|
|
|
80
120
|
discount_amount: discountAmt,
|
|
81
121
|
line_total: unitPrice * qty - discountAmt,
|
|
82
122
|
expanded: false,
|
|
123
|
+
uom_id: itemWithVariants.uom_id,
|
|
83
124
|
};
|
|
84
125
|
return [...prevLineItems, newLine];
|
|
85
126
|
});
|
|
@@ -115,6 +156,33 @@ export function ProductTransactionScreen({
|
|
|
115
156
|
const handleSubmit = useCallback(async () => {
|
|
116
157
|
if (lineItems.length === 0) return;
|
|
117
158
|
|
|
159
|
+
if (config.requireFullPayment) {
|
|
160
|
+
const totalPayments = payments.reduce((sum, p) => sum + (Number(p.amount) || 0), 0);
|
|
161
|
+
if (Math.abs(totalPayments - grandTotal) > 0.01) {
|
|
162
|
+
toast.error('Total payments must equal the Grand Total (Use Credit mode to pay later).');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!config.disableTrackingSelection) {
|
|
168
|
+
for (const line of lineItems) {
|
|
169
|
+
if (line.item.tracking_type === 'batch') {
|
|
170
|
+
const selectedTotal = line.batches?.reduce((sum, b) => sum + b.quantity, 0) || 0;
|
|
171
|
+
if (selectedTotal !== line.quantity) {
|
|
172
|
+
toast.error(`Please select batches for ${line.item.name} to match the total quantity.`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (line.item.tracking_type === 'serial') {
|
|
177
|
+
const selectedCount = line.serial_numbers?.length || 0;
|
|
178
|
+
if (selectedCount !== line.quantity) {
|
|
179
|
+
toast.error(`Please select exactly ${line.quantity} serial numbers for ${line.item.name}.`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
118
186
|
const payload: TransactionPayload = {
|
|
119
187
|
type: config.type,
|
|
120
188
|
party_id: partyId,
|
|
@@ -139,6 +207,7 @@ export function ProductTransactionScreen({
|
|
|
139
207
|
is_free_qty: l.is_free_qty,
|
|
140
208
|
parent_line_uids: l.parent_line_uids,
|
|
141
209
|
uom_id: l.uom_id,
|
|
210
|
+
purchase_order_line_id: l.purchase_order_line_id,
|
|
142
211
|
unit_price: l.unit_price,
|
|
143
212
|
discount_percent: l.discount_percent,
|
|
144
213
|
discount_amount: (l.discount_amount / l.quantity) * b.quantity,
|
|
@@ -160,6 +229,7 @@ export function ProductTransactionScreen({
|
|
|
160
229
|
is_free_qty: l.is_free_qty,
|
|
161
230
|
parent_line_uids: l.parent_line_uids,
|
|
162
231
|
uom_id: l.uom_id,
|
|
232
|
+
purchase_order_line_id: l.purchase_order_line_id,
|
|
163
233
|
unit_price: l.unit_price,
|
|
164
234
|
discount_percent: l.discount_percent,
|
|
165
235
|
discount_amount: l.discount_amount,
|
|
@@ -234,6 +304,7 @@ export function ProductTransactionScreen({
|
|
|
234
304
|
onSubmit={handleSubmit}
|
|
235
305
|
isSubmitting={submitting}
|
|
236
306
|
renderExtraMeta={renderExtraMeta}
|
|
307
|
+
fetchChequeLeaves={fetchChequeLeaves}
|
|
237
308
|
/>
|
|
238
309
|
</div>
|
|
239
310
|
</div>
|
|
@@ -77,6 +77,7 @@ export interface TransactionLineItem {
|
|
|
77
77
|
is_free_qty?: boolean;
|
|
78
78
|
parent_line_uids?: string[];
|
|
79
79
|
uom_id?: number;
|
|
80
|
+
purchase_order_line_id?: number | null;
|
|
80
81
|
unit_price: number;
|
|
81
82
|
discount_percent: number;
|
|
82
83
|
discount_amount: number;
|
|
@@ -137,6 +138,8 @@ export interface TransactionScreenConfig {
|
|
|
137
138
|
partyIsWarehouse?: boolean;
|
|
138
139
|
/** Whether to show the party selector at all (defaults to true) */
|
|
139
140
|
showParty?: boolean;
|
|
141
|
+
/** Disable the search bar (useful for PO to GRN conversions where items are fixed) */
|
|
142
|
+
disableItemSelection?: boolean;
|
|
140
143
|
/** Allow creating new batches and serials during selection */
|
|
141
144
|
allowCreateBatchAndSerial?: boolean;
|
|
142
145
|
/** If true, user must select a warehouse before searching/adding items */
|
|
@@ -151,6 +154,8 @@ export interface TransactionScreenConfig {
|
|
|
151
154
|
showExpectedDeliveryDate?: boolean;
|
|
152
155
|
/** Disable batch/serial selection completely (e.g. for Purchase Orders and Quotations) */
|
|
153
156
|
disableTrackingSelection?: boolean;
|
|
157
|
+
/** Enforce that the sum of payments equals the grand total before allowing submission */
|
|
158
|
+
requireFullPayment?: boolean;
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
// ── Module Presets ──
|
|
@@ -263,7 +268,7 @@ export const TRANSACTION_PRESETS: Record<TransactionType, TransactionScreenConfi
|
|
|
263
268
|
priceField: 'default_cost_price',
|
|
264
269
|
showPrices: true,
|
|
265
270
|
allowDiscount: true,
|
|
266
|
-
showPayment:
|
|
271
|
+
showPayment: true,
|
|
267
272
|
partyLabel: 'Supplier',
|
|
268
273
|
partyType: 'supplier',
|
|
269
274
|
showWarehouse: true,
|
|
@@ -273,6 +278,7 @@ export const TRANSACTION_PRESETS: Record<TransactionType, TransactionScreenConfi
|
|
|
273
278
|
allowCreateBatchAndSerial: true,
|
|
274
279
|
allowEditPrice: true,
|
|
275
280
|
showStock: true,
|
|
281
|
+
requireFullPayment: true,
|
|
276
282
|
},
|
|
277
283
|
};
|
|
278
284
|
|
|
@@ -289,6 +295,10 @@ export interface ProductTransactionScreenProps {
|
|
|
289
295
|
isSubmitting?: boolean;
|
|
290
296
|
/** Optional extra content rendered at the top of the metadata (right) panel */
|
|
291
297
|
renderExtraMeta?: () => React.ReactNode;
|
|
298
|
+
/** Fetcher for cheque leaves (for auto-complete) */
|
|
299
|
+
fetchChequeLeaves?: (query: string, page: number) => Promise<{ is_success: boolean; result?: any[] }>;
|
|
300
|
+
/** Initial data for populating the transaction screen (e.g., from a PO) */
|
|
301
|
+
initialData?: any;
|
|
292
302
|
}
|
|
293
303
|
|
|
294
304
|
export interface TransactionPayload {
|
|
@@ -313,6 +323,7 @@ export interface TransactionPayload {
|
|
|
313
323
|
is_free_qty?: boolean;
|
|
314
324
|
parent_line_uids?: string[];
|
|
315
325
|
uom_id?: number;
|
|
326
|
+
purchase_order_line_id?: number | null;
|
|
316
327
|
unit_price: number;
|
|
317
328
|
discount_percent: number;
|
|
318
329
|
discount_amount: number;
|