@apptimate/ui 4.0.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apptimate/ui",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "main": "src/index.tsx",
5
5
  "types": "src/index.tsx",
6
6
  "dependencies": {
@@ -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={() => setActiveMenuId(menu.id)}
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,
@@ -22,22 +23,25 @@ interface MetadataPanelProps {
22
23
  warehouseId: number | null;
23
24
  warehouseName?: string | null;
24
25
  transactionDate?: string;
26
+ expectedDeliveryDate?: string;
25
27
  notes: string;
26
28
  payments: PaymentEntry[];
27
29
  onPartyChange: (id: number | null, party?: PartyLookup) => void;
28
30
  onWarehouseChange: (id: number | null, name?: string) => void;
29
31
  onDateChange?: (date: string) => void;
32
+ onExpectedDeliveryDateChange?: (date: string) => void;
30
33
  onNotesChange: (v: string) => void;
31
34
  onPaymentsChange: (payments: PaymentEntry[]) => void;
32
35
  onSubmit: () => void;
33
36
  isSubmitting: boolean;
34
37
  renderExtraMeta?: () => React.ReactNode;
38
+ fetchChequeLeaves?: (query: string, page: number) => Promise<{ is_success: boolean; result?: any[] }>;
35
39
  }
36
40
 
37
41
  export function MetadataPanel({
38
- config, lineItems, partyId, partyName, warehouseId, warehouseName, transactionDate, notes, payments,
39
- onPartyChange, onWarehouseChange, onDateChange, onNotesChange, onPaymentsChange, onSubmit, isSubmitting,
40
- renderExtraMeta,
42
+ config, lineItems, partyId, partyName, warehouseId, warehouseName, transactionDate, expectedDeliveryDate, notes, payments,
43
+ onPartyChange, onWarehouseChange, onDateChange, onExpectedDeliveryDateChange, onNotesChange, onPaymentsChange, onSubmit, isSubmitting,
44
+ renderExtraMeta, fetchChequeLeaves,
41
45
  }: MetadataPanelProps) {
42
46
  const [paymentModes, setPaymentModes] = useState<PaymentMode[]>([]);
43
47
  const [bankAccounts, setBankAccounts] = useState<BankAccount[]>([]);
@@ -150,6 +154,7 @@ export function MetadataPanel({
150
154
  onChange={(p) => onPartyChange(p?.id ?? null, p ? { id: p.id, name: p.name, code: p.code, type: p.type } : undefined)}
151
155
  label={config.partyLabel}
152
156
  partyType={config.partyType}
157
+ isRequired
153
158
  />
154
159
  )
155
160
  )}
@@ -180,6 +185,21 @@ export function MetadataPanel({
180
185
  </Section>
181
186
  )}
182
187
 
188
+ {/* ── Expected Delivery Date ── */}
189
+ {config.showExpectedDeliveryDate && onExpectedDeliveryDateChange && (
190
+ <Section label="Expected Delivery">
191
+ <div className="relative">
192
+ <Calendar size={15} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-foreground-disabled pointer-events-none" />
193
+ <input
194
+ type="date"
195
+ value={expectedDeliveryDate || ''}
196
+ onChange={(e) => onExpectedDeliveryDateChange(e.target.value)}
197
+ className="w-full bg-surface-0 border-[1.5px] border-border-subtle rounded-[10px] pl-10 pr-3.5 py-3 text-[13.5px] text-foreground-1 outline-none transition-all hover:border-gray-300 focus:border-primary/40"
198
+ />
199
+ </div>
200
+ </Section>
201
+ )}
202
+
183
203
  {/* ── Notes ── */}
184
204
  {config.showNotes && (
185
205
  <Section label="Notes">
@@ -208,77 +228,15 @@ export function MetadataPanel({
208
228
  {/* ── Payment Section ── */}
209
229
  {config.showPayment && (
210
230
  <Section label="Payment">
211
- {isLoadingMeta ? (
212
- <div className="flex items-center justify-center py-4">
213
- <Loader2 size={18} className="animate-spin text-primary" />
214
- </div>
215
- ) : (
216
- <div className="space-y-2.5">
217
- {/* Existing payment entries */}
218
- {payments.map((entry, idx) => {
219
- const mode = paymentModes.find((m) => m.id === entry.mode_id);
220
- return (
221
- <PaymentEntryRow
222
- key={idx}
223
- entry={entry}
224
- mode={mode}
225
- bankAccounts={bankAccounts}
226
- onUpdate={(updates) => updatePayment(idx, updates)}
227
- onRemove={() => removePayment(idx)}
228
- />
229
- );
230
- })}
231
-
232
- {/* Add payment mode buttons */}
233
- {(lineItems.length > 0) && (
234
- <div className="flex flex-wrap gap-2 pt-1">
235
- {paymentModes
236
- .filter((m) => m.is_active)
237
- .map((mode) => {
238
- const modeIcon = mode.code === 'cash' ? <Banknote size={14} /> :
239
- mode.code === 'card' ? <CreditCard size={14} /> :
240
- mode.code === 'bank_transfer' ? <Building size={14} /> :
241
- <CreditCard size={14} />;
242
- return (
243
- <button
244
- key={mode.id}
245
- onClick={() => addPayment(mode)}
246
- 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"
247
- >
248
- {modeIcon}
249
- {mode.name}
250
- </button>
251
- );
252
- })}
253
- </div>
254
- )}
255
-
256
- {/* Balance */}
257
- {payments.length > 0 && (
258
- <div className="bg-surface-0 rounded-[10px] border border-border-subtle p-3.5 mt-2 space-y-1.5">
259
- <TotalRow label="Total Paid" value={formatCurrency(totalPaid)} />
260
- <TotalRow
261
- label={balance > 0.01 ? "Balance Due" : balance < -0.01 ? "Change Due" : "Settled"}
262
- value={formatCurrency(Math.abs(balance))}
263
- className={balance > 0.01 ? 'text-danger-alt' : balance < -0.01 ? 'text-amber-600' : 'text-green-600'}
264
- bold
265
- />
266
- {balance < -0.01 && (
267
- <div className="flex items-center gap-1.5 mt-1 text-[11px] text-amber-600 font-medium">
268
- <AlertCircle size={12} />
269
- <span>Change: {formatCurrency(Math.abs(balance))}</span>
270
- </div>
271
- )}
272
- {Math.abs(balance) < 0.01 && (
273
- <div className="flex items-center gap-1.5 mt-1 text-[11px] text-green-600 font-medium">
274
- <Check size={12} />
275
- <span>Fully paid</span>
276
- </div>
277
- )}
278
- </div>
279
- )}
280
- </div>
281
- )}
231
+ <PaymentSection
232
+ totalAmount={grandTotal}
233
+ payments={payments}
234
+ onPaymentsChange={onPaymentsChange}
235
+ fetchPaymentModes={getPaymentModes}
236
+ fetchBankAccounts={getBankAccountsLookup}
237
+ fetchChequeLeaves={fetchChequeLeaves}
238
+ compact={true}
239
+ />
282
240
  </Section>
283
241
  )}
284
242
 
@@ -0,0 +1,149 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { Modal, ModalFooter } from "../../base-components/Modal";
3
+ import { Button } from "../../base-components/Button";
4
+ import { Table, THeader, TBody, TRow, TCell } from "../../base-components/Table";
5
+ import { TransactionLineItem } from "./types";
6
+ import { cn } from "@apptimate/core-lib";
7
+
8
+ interface ParentLineSelectionModalProps {
9
+ isOpen: boolean;
10
+ onClose: () => void;
11
+ lineItems: TransactionLineItem[];
12
+ selectedUids?: string[];
13
+ onSelect: (uids: string[]) => void;
14
+ }
15
+
16
+ export function ParentLineSelectionModal({
17
+ isOpen,
18
+ onClose,
19
+ lineItems,
20
+ selectedUids = [],
21
+ onSelect,
22
+ }: ParentLineSelectionModalProps) {
23
+ const [selected, setSelected] = useState<string[]>([]);
24
+
25
+ useEffect(() => {
26
+ if (isOpen) {
27
+ setSelected(selectedUids || []);
28
+ }
29
+ }, [isOpen, selectedUids]);
30
+
31
+ const formatCurrency = (v: number | string) =>
32
+ Number(v).toLocaleString("en-US", {
33
+ minimumFractionDigits: 2,
34
+ maximumFractionDigits: 2,
35
+ });
36
+
37
+ const handleToggle = (uid: string) => {
38
+ setSelected((prev) =>
39
+ prev.includes(uid) ? prev.filter((id) => id !== uid) : [...prev, uid]
40
+ );
41
+ };
42
+
43
+ const handleSave = () => {
44
+ onSelect(selected);
45
+ onClose();
46
+ };
47
+
48
+ return (
49
+ <Modal
50
+ isOpen={isOpen}
51
+ onClose={onClose}
52
+ title="Select Parent Lines"
53
+ size="xl"
54
+ >
55
+ <div className="p-4 max-h-[60vh] overflow-y-auto custom-scrollbar">
56
+ {lineItems.length === 0 ? (
57
+ <div className="text-center text-sm text-foreground-disabled py-8">
58
+ No parent lines available.
59
+ </div>
60
+ ) : (
61
+ <Table>
62
+ <THeader>
63
+ <TRow>
64
+ <TCell isHeader className="w-10 text-center"></TCell>
65
+ <TCell isHeader>Item</TCell>
66
+ <TCell isHeader>Qty</TCell>
67
+ <TCell isHeader>Unit</TCell>
68
+ <TCell isHeader>Rate</TCell>
69
+ <TCell isHeader className="text-right">
70
+ Total
71
+ </TCell>
72
+ </TRow>
73
+ </THeader>
74
+ <TBody>
75
+ {lineItems.map((line) => {
76
+ const isSelected = selected.includes(line.uid);
77
+ return (
78
+ <TRow
79
+ key={line.uid}
80
+ onClick={() => handleToggle(line.uid)}
81
+ className={cn(
82
+ "cursor-pointer hover:bg-surface-hover transition-colors",
83
+ isSelected ? "bg-primary/5" : ""
84
+ )}
85
+ >
86
+ <TCell className="text-center">
87
+ <input
88
+ type="checkbox"
89
+ checked={isSelected}
90
+ readOnly
91
+ className="rounded border-border-subtle text-primary focus:ring-primary h-4 w-4 mt-1 cursor-pointer"
92
+ />
93
+ </TCell>
94
+ <TCell label="Item">
95
+ <div className="font-medium text-[13px] text-foreground-1">
96
+ {line.item.name}
97
+ </div>
98
+ {line.variant_id && (
99
+ <div className="text-[11px] text-foreground-subtle font-mono mt-0.5">
100
+ {line.item.variants?.find((v) => v.id === line.variant_id)
101
+ ?.variant_name || ""}{" "}
102
+ •{" "}
103
+ {line.item.variants?.find((v) => v.id === line.variant_id)
104
+ ?.sku || ""}
105
+ </div>
106
+ )}
107
+ </TCell>
108
+ <TCell label="Qty">
109
+ <span className="text-[13px] font-medium">
110
+ {line.quantity}
111
+ </span>
112
+ </TCell>
113
+ <TCell label="Unit">
114
+ <span className="text-[13px] text-foreground-subtle">
115
+ {line.item.uom?.uom_group?.uoms?.find((u: any) => String(u.id) === String(line.uom_id))?.abbreviation ||
116
+ line.item.uom?.abbreviation ||
117
+ "pcs"}
118
+ </span>
119
+ </TCell>
120
+ <TCell label="Rate">
121
+ <span className="text-[13px] text-foreground-subtle">
122
+ {formatCurrency(line.unit_price)}
123
+ </span>
124
+ </TCell>
125
+ <TCell label="Total" className="text-right">
126
+ <span className="text-[13px] font-semibold text-foreground-1">
127
+ {formatCurrency(line.line_total)}
128
+ </span>
129
+ </TCell>
130
+ </TRow>
131
+ );
132
+ })}
133
+ </TBody>
134
+ </Table>
135
+ )}
136
+ </div>
137
+ <ModalFooter>
138
+ <div className="flex justify-end gap-3 w-full">
139
+ <Button variant="flat" color="secondary" onClick={onClose}>
140
+ Cancel
141
+ </Button>
142
+ <Button variant="solid" color="primary" onClick={handleSave}>
143
+ Save Selections
144
+ </Button>
145
+ </div>
146
+ </ModalFooter>
147
+ </Modal>
148
+ );
149
+ }
@@ -11,7 +11,9 @@ import { Table, THeader, TBody, TRow, TCell } from '../../base-components/Table'
11
11
  import { transactionLookup, getItemBatches } from '@apptimate/core-lib';
12
12
  import type { TransactionItem, TransactionLineItem, TransactionScreenConfig } from './types';
13
13
  import { BatchSelectionModal, SerialSelectionModal } from "../pickers";
14
+ import { ParentLineSelectionModal } from "./ParentLineSelectionModal";
14
15
  import { HintIcon } from '../../base-components/HintIcon';
16
+
15
17
  interface ProductSelectionPanelProps {
16
18
  config: TransactionScreenConfig;
17
19
  warehouseId?: number;
@@ -43,6 +45,7 @@ export function ProductSelectionPanel({
43
45
  const [activeBatchLineUid, setActiveBatchLineUid] = useState<string | null>(null);
44
46
  const [activeSerialItem, setActiveSerialItem] = useState<{ item: TransactionItem; variant?: any } | null>(null);
45
47
  const [activeSerialLineUid, setActiveSerialLineUid] = useState<string | null>(null);
48
+ const [activeParentLineUidFor, setActiveParentLineUidFor] = useState<string | null>(null);
46
49
  const [autoAddEnabled, setAutoAddEnabled] = useState(true);
47
50
  const searchTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
48
51
  const panelRef = React.useRef<HTMLDivElement>(null);
@@ -110,9 +113,9 @@ export function ProductSelectionPanel({
110
113
  setSearchResults([]);
111
114
  setShowResults(false);
112
115
 
113
- if (item.tracking_type === 'batch') {
116
+ if (!config.disableTrackingSelection && item.tracking_type === 'batch') {
114
117
  setActiveBatchItem({ item, variant });
115
- } else if (item.tracking_type === 'serial') {
118
+ } else if (!config.disableTrackingSelection && item.tracking_type === 'serial') {
116
119
  setActiveSerialItem({ item, variant });
117
120
  } else {
118
121
  onAddItem(item, variant);
@@ -134,28 +137,10 @@ export function ProductSelectionPanel({
134
137
  const totalQty = batchesData.reduce((sum, b) => sum + b.quantity, 0);
135
138
 
136
139
  if (activeBatchItem) {
137
- const resolvedVariantId = activeBatchItem.variant?.id || (activeBatchItem.item.variants && activeBatchItem.item.variants.length > 0 ? activeBatchItem.item.variants[0].id : null);
138
- const existingLine = lineItems.find(
139
- (l) => l.item.id === activeBatchItem.item.id &&
140
- l.variant_id === resolvedVariantId
141
- );
142
-
143
- if (existingLine) {
144
- const newQty = totalQty > 0 ? totalQty : 1;
145
- const discAmt = (existingLine.unit_price * newQty * existingLine.discount_percent) / 100;
146
-
147
- onUpdateLine(existingLine.uid, {
148
- batches: batchesData,
149
- quantity: newQty,
150
- discount_amount: discAmt,
151
- line_total: existingLine.unit_price * newQty - discAmt,
152
- });
153
- } else {
154
- onAddItem(activeBatchItem.item, activeBatchItem.variant, {
155
- batches: batchesData,
156
- quantity: totalQty > 0 ? totalQty : 1
157
- });
158
- }
140
+ onAddItem(activeBatchItem.item, activeBatchItem.variant, {
141
+ batches: batchesData,
142
+ quantity: totalQty > 0 ? totalQty : 1
143
+ });
159
144
  setActiveBatchItem(null);
160
145
  }
161
146
  };
@@ -163,25 +148,7 @@ export function ProductSelectionPanel({
163
148
  const handleSerialConfirm = (serials: string[]) => {
164
149
  if (!activeSerialItem) return;
165
150
 
166
- const resolvedVariantId = activeSerialItem.variant?.id || (activeSerialItem.item.variants && activeSerialItem.item.variants.length > 0 ? activeSerialItem.item.variants[0].id : null);
167
- const existingLine = lineItems.find(
168
- (l) => l.item.id === activeSerialItem.item.id &&
169
- l.variant_id === resolvedVariantId
170
- );
171
-
172
- if (existingLine) {
173
- const newQty = serials.length;
174
- const discAmt = (existingLine.unit_price * newQty * existingLine.discount_percent) / 100;
175
-
176
- onUpdateLine(existingLine.uid, {
177
- serial_numbers: serials,
178
- quantity: newQty,
179
- discount_amount: discAmt,
180
- line_total: existingLine.unit_price * newQty - discAmt,
181
- });
182
- } else {
183
- onAddItem(activeSerialItem.item, activeSerialItem.variant, { serial_numbers: serials, quantity: serials.length });
184
- }
151
+ onAddItem(activeSerialItem.item, activeSerialItem.variant, { serial_numbers: serials, quantity: serials.length });
185
152
  setActiveSerialItem(null);
186
153
  };
187
154
 
@@ -273,13 +240,34 @@ export function ProductSelectionPanel({
273
240
 
274
241
  const discAmt = (price * qty * line.discount_percent) / 100;
275
242
 
276
- if (field === 'free_qty') {
277
- onUpdateLine(uid, { free_qty: Math.max(0, num) });
278
- return;
279
- }
280
-
281
243
  if (field === 'uom_id') {
282
- onUpdateLine(uid, { uom_id: Number(value) });
244
+ const newUomId = Number(value);
245
+ const uoms = line.item.uom?.uom_group?.uoms || [];
246
+
247
+ const oldUom = uoms.find((u: any) => u.id === (line.uom_id || line.item.uom_id));
248
+ const newUom = uoms.find((u: any) => u.id === newUomId);
249
+
250
+ let newPrice = line.unit_price;
251
+
252
+ if (oldUom && newUom) {
253
+ const oldFactor = Number(oldUom.conversion_factor) || 1;
254
+ const newFactor = Number(newUom.conversion_factor) || 1;
255
+
256
+ // Calculate the base price then multiply by the new factor
257
+ const basePrice = line.unit_price / oldFactor;
258
+ newPrice = basePrice * newFactor;
259
+ }
260
+
261
+ const newDiscAmt = (newPrice * line.quantity * line.discount_percent) / 100;
262
+
263
+ setEditingRates(prev => ({ ...prev, [uid]: newPrice.toFixed(2) }));
264
+
265
+ onUpdateLine(uid, {
266
+ uom_id: newUomId,
267
+ unit_price: newPrice,
268
+ discount_amount: newDiscAmt,
269
+ line_total: (newPrice * line.quantity) - newDiscAmt
270
+ });
283
271
  return;
284
272
  }
285
273
 
@@ -291,12 +279,43 @@ export function ProductSelectionPanel({
291
279
  });
292
280
  };
293
281
 
282
+ const handleToggleFreeQty = (uid: string, isFree: boolean) => {
283
+ const line = lineItems.find(l => l.uid === uid);
284
+ if (!line) return;
285
+
286
+ let newPrice = 0;
287
+ if (!isFree) {
288
+ const variantPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'sale_price';
289
+ const actualVariant = line.variant_id ? line.item.variants?.find(v => v.id === line.variant_id) : (line.item.variants?.[0] || null);
290
+ newPrice = actualVariant ? Number(actualVariant[variantPriceField] || 0) : 0;
291
+ }
292
+
293
+ const discAmt = (newPrice * line.quantity * line.discount_percent) / 100;
294
+ onUpdateLine(uid, {
295
+ is_free_qty: isFree,
296
+ unit_price: newPrice,
297
+ discount_amount: discAmt,
298
+ line_total: newPrice * line.quantity - discAmt,
299
+ parent_line_uids: isFree ? line.parent_line_uids : undefined,
300
+ });
301
+
302
+ if (isFree) {
303
+ lineItems.forEach(l => {
304
+ if (l.parent_line_uids?.includes(uid)) {
305
+ const newUids = l.parent_line_uids.filter(id => id !== uid);
306
+ onUpdateLine(l.uid, { parent_line_uids: newUids.length > 0 ? newUids : undefined });
307
+ }
308
+ });
309
+ }
310
+ };
311
+
294
312
  return (
295
313
  <div className="flex flex-col h-full">
296
314
  {/* ── Search Bar ── */}
297
- <div ref={panelRef} className="relative mb-4">
298
- <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">
299
- <Search size={18} className="text-foreground-disabled shrink-0" />
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" />
300
319
  <input
301
320
  ref={inputRef}
302
321
  type="text"
@@ -495,6 +514,7 @@ export function ProductSelectionPanel({
495
514
  </div>
496
515
  )}
497
516
  </div>
517
+ )}
498
518
 
499
519
  {/* ── Selected Items Table ── */}
500
520
  <div className="flex-1 overflow-y-auto custom-scrollbar w-full">
@@ -513,7 +533,6 @@ export function ProductSelectionPanel({
513
533
  <TCell isHeader className="w-[45%]">Item</TCell>
514
534
  <TCell isHeader>Qty</TCell>
515
535
  {config.enablePurchaseUnit && <TCell isHeader>Unit</TCell>}
516
- {config.enableFreeQty && <TCell isHeader>Free Qty</TCell>}
517
536
  {config.showPrices && <TCell isHeader>Rate</TCell>}
518
537
  {config.showPrices && <TCell isHeader className="text-right">Total</TCell>}
519
538
  <TCell isHeader className="w-10 text-center"></TCell>
@@ -532,13 +551,20 @@ export function ProductSelectionPanel({
532
551
  )}
533
552
  </div>
534
553
  <div className="flex flex-col flex-1 min-w-0">
535
- <span className="text-[13px] font-semibold text-foreground-1 leading-tight break-words">{line.item.name}</span>
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>
536
562
  <span className="text-[11px] text-foreground-subtle font-mono mt-0.5 break-words">
537
- {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)
538
564
  ? `${line.item.variants.find(v => v.id === line.variant_id)?.variant_name} • ${line.item.variants.find(v => v.id === line.variant_id)?.sku}`
539
565
  : line.item.sku}
540
566
  </span>
541
- {((line.batches && line.batches.length > 0) || (line.serial_numbers && line.serial_numbers.length > 0)) && (
567
+ {!config.disableTrackingSelection && (line.item.tracking_type === 'batch' || line.item.tracking_type === 'serial') && (
542
568
  <div className="flex flex-wrap gap-1 mt-1.5">
543
569
  {line.item.tracking_type === 'batch' && (() => {
544
570
  const batchesCount = line.batches?.length || 0;
@@ -593,6 +619,59 @@ export function ProductSelectionPanel({
593
619
  })()}
594
620
  </div>
595
621
  )}
622
+ {(config.type === 'purchase' || config.type === 'grn') && Number(line.unit_price) === 0 && (
623
+ <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")}>
625
+ <input
626
+ type="checkbox"
627
+ checked={line.is_free_qty || false}
628
+ onChange={(e) => handleToggleFreeQty(line.uid, e.target.checked)}
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
+ )}
634
+ />
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>
641
+ </label>
642
+
643
+ {line.is_free_qty && (
644
+ <div className="flex items-center">
645
+ <button
646
+ onClick={() => setActiveParentLineUidFor(line.uid)}
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
+ )}
652
+ >
653
+ <span>
654
+ {line.parent_line_uids && line.parent_line_uids.length > 0
655
+ ? (() => {
656
+ if (line.parent_line_uids.length === 1) {
657
+ const p = lineItems.find((l) => l.uid === line.parent_line_uids![0]);
658
+ return p
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
662
+ : "Select Parent Line...";
663
+ } else {
664
+ return `${line.parent_line_uids.length} Parents Selected`;
665
+ }
666
+ })()
667
+ : "Select Parent Line..."}
668
+ </span>
669
+ </button>
670
+ <HintIcon text="Select the original line item that this free quantity is associated with." />
671
+ </div>
672
+ )}
673
+ </div>
674
+ )}
596
675
  </div>
597
676
  </div>
598
677
  </TCell>
@@ -613,7 +692,8 @@ export function ProductSelectionPanel({
613
692
  <select
614
693
  value={line.uom_id || line.item.uom_id || ''}
615
694
  onChange={(e) => updateLineInput(line.uid, 'uom_id', e.target.value)}
616
- 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]"
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"
617
697
  >
618
698
  {line.item.uom?.uom_group?.uoms?.map((uom: any) => (
619
699
  <option key={uom.id} value={uom.id}>{uom.abbreviation}</option>
@@ -622,24 +702,11 @@ export function ProductSelectionPanel({
622
702
  </TCell>
623
703
  )}
624
704
 
625
- {config.enableFreeQty && (
626
- <TCell label="Free Qty" className="py-2.5">
627
- <input
628
- type="number"
629
- value={line.free_qty || 0}
630
- onChange={(e) => updateLineInput(line.uid, 'free_qty', e.target.value)}
631
- className="w-20 h-8 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"
632
- min={0}
633
- step="any"
634
- />
635
- </TCell>
636
- )}
637
-
638
705
  {config.showPrices && (
639
706
  <TCell label="Rate" className="py-2.5">
640
707
  <input
641
708
  type="text"
642
- value={editingRates[line.uid] ?? line.unit_price.toFixed(2)}
709
+ value={editingRates[line.uid] ?? Number(line.unit_price || 0).toFixed(2)}
643
710
  onChange={(e) => {
644
711
  const val = e.target.value;
645
712
  if (/^\d*\.?\d{0,2}$/.test(val)) {
@@ -654,10 +721,10 @@ export function ProductSelectionPanel({
654
721
  return next;
655
722
  });
656
723
  }}
657
- disabled={!config.allowEditPrice}
724
+ disabled={!config.allowEditPrice || line.is_free_qty}
658
725
  className={cn(
659
726
  "w-24 h-8 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",
660
- !config.allowEditPrice && "opacity-60 cursor-not-allowed bg-surface-hover border-transparent"
727
+ (!config.allowEditPrice || line.is_free_qty) && "opacity-60 cursor-not-allowed bg-surface-hover border-transparent"
661
728
  )}
662
729
  />
663
730
  </TCell>
@@ -750,19 +817,9 @@ export function ProductSelectionPanel({
750
817
  return lineItems.find(l => l.uid === activeBatchLineUid)?.batches || [];
751
818
  }
752
819
  if (activeBatchItem) {
753
- const resolvedVariantId = activeBatchItem.variant?.id || (activeBatchItem.item.variants && activeBatchItem.item.variants.length > 0 ? activeBatchItem.item.variants[0].id : null);
754
- const existingLine = lineItems.find(l => l.item.id === activeBatchItem.item.id && l.variant_id === resolvedVariantId);
755
-
756
- const existingBatches = existingLine?.batches ? [...existingLine.batches] : [];
757
-
820
+ const existingBatches: any[] = [];
758
821
  if (activeBatchItem.preSelectedBatch) {
759
- const existingBatchIndex = existingBatches.findIndex((b: any) => b.batch_id === activeBatchItem.preSelectedBatch.id);
760
- if (existingBatchIndex >= 0) {
761
- const b = existingBatches[existingBatchIndex];
762
- existingBatches[existingBatchIndex] = { ...b, quantity: Number(b.quantity) + 1 };
763
- } else {
764
- existingBatches.push({ ...activeBatchItem.preSelectedBatch, batch_id: activeBatchItem.preSelectedBatch.id, quantity: 1 });
765
- }
822
+ existingBatches.push({ ...activeBatchItem.preSelectedBatch, batch_id: activeBatchItem.preSelectedBatch.id, quantity: 1 });
766
823
  }
767
824
  return existingBatches;
768
825
  }
@@ -804,12 +861,24 @@ export function ProductSelectionPanel({
804
861
  variant={activeSerialItem?.variant || null}
805
862
  warehouseId={warehouseId}
806
863
  allowCreate={config.allowCreateBatchAndSerial}
807
- initialSerials={
808
- activeSerialItem
809
- ? lineItems.find(l => l.item.id === activeSerialItem.item.id && l.variant_id === (activeSerialItem.variant?.id || (activeSerialItem.item.variants && activeSerialItem.item.variants.length > 0 ? activeSerialItem.item.variants[0].id : null)))?.serial_numbers || []
864
+ initialSerials={[]}
865
+ onConfirm={handleSerialConfirm}
866
+ />
867
+
868
+ <ParentLineSelectionModal
869
+ isOpen={!!activeParentLineUidFor}
870
+ onClose={() => setActiveParentLineUidFor(null)}
871
+ lineItems={lineItems.filter((l) => l.uid !== activeParentLineUidFor && !l.is_free_qty)}
872
+ selectedUids={
873
+ activeParentLineUidFor
874
+ ? lineItems.find((l) => l.uid === activeParentLineUidFor)?.parent_line_uids || []
810
875
  : []
811
876
  }
812
- onConfirm={handleSerialConfirm}
877
+ onSelect={(uids) => {
878
+ if (activeParentLineUidFor) {
879
+ onUpdateLine(activeParentLineUidFor, { parent_line_uids: uids.length > 0 ? uids : undefined });
880
+ }
881
+ }}
813
882
  />
814
883
  </div>
815
884
  );
@@ -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[]>([]);
@@ -35,12 +38,50 @@ export function ProductTransactionScreen({
35
38
  const [transactionDate, setTransactionDate] = useState<string>(() => {
36
39
  return new Date().toISOString().split('T')[0];
37
40
  });
41
+ const [expectedDeliveryDate, setExpectedDeliveryDate] = useState<string>('');
38
42
  const [notes, setNotes] = useState('');
39
43
  const [payments, setPayments] = useState<PaymentEntry[]>([]);
40
44
  const [isSubmitting, setIsSubmitting] = useState(false);
41
45
 
42
46
  const submitting = externalSubmitting ?? isSubmitting;
43
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
+
44
85
  // ── Add item to line items ──
45
86
  const handleAddItem = useCallback(
46
87
  (
@@ -62,51 +103,6 @@ export function ProductTransactionScreen({
62
103
  }
63
104
  }
64
105
 
65
- // Check if item+variant already exists
66
- const existingIdx = prevLineItems.findIndex(
67
- (l) => l.item.id === itemWithVariants.id && l.variant_id === resolvedVariantId
68
- );
69
-
70
- if (existingIdx >= 0) {
71
- // Increment quantity
72
- const updated = [...prevLineItems];
73
- const existing = updated[existingIdx];
74
- let mergedSerials = existing.serial_numbers || [];
75
- let newSerialsCount = 0;
76
- if (extra?.serial_numbers?.length) {
77
- const uniqueNewSerials = extra.serial_numbers.filter(s => !mergedSerials.includes(s));
78
- mergedSerials = [...mergedSerials, ...uniqueNewSerials];
79
- newSerialsCount = uniqueNewSerials.length;
80
- }
81
-
82
- // If this addition includes serial numbers, only increment quantity by the number of actually new serials added.
83
- // Otherwise, fallback to the requested quantity or 1.
84
- const addedQty = extra?.serial_numbers ? newSerialsCount : (extra?.quantity || 1);
85
-
86
- if (addedQty === 0 && extra?.serial_numbers) {
87
- // The serial number was already added, do nothing to avoid duplicates
88
- return prevLineItems;
89
- }
90
-
91
- const newQty = existing.quantity + addedQty;
92
- const discountAmt = (existing.unit_price * newQty * existing.discount_percent) / 100;
93
-
94
- let mergedBatches = existing.batches;
95
- if (extra?.batches?.length) {
96
- mergedBatches = [...(mergedBatches || []), ...extra.batches];
97
- }
98
-
99
- updated[existingIdx] = {
100
- ...existing,
101
- quantity: newQty,
102
- discount_amount: discountAmt,
103
- line_total: existing.unit_price * newQty - discountAmt,
104
- batches: mergedBatches,
105
- serial_numbers: mergedSerials,
106
- };
107
- return updated;
108
- }
109
-
110
106
  const variantPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'sale_price';
111
107
  const unitPrice = actualVariant ? Number(actualVariant[variantPriceField] || 0) : 0;
112
108
  const qty = extra?.quantity || 1;
@@ -124,6 +120,7 @@ export function ProductTransactionScreen({
124
120
  discount_amount: discountAmt,
125
121
  line_total: unitPrice * qty - discountAmt,
126
122
  expanded: false,
123
+ uom_id: itemWithVariants.uom_id,
127
124
  };
128
125
  return [...prevLineItems, newLine];
129
126
  });
@@ -159,11 +156,39 @@ export function ProductTransactionScreen({
159
156
  const handleSubmit = useCallback(async () => {
160
157
  if (lineItems.length === 0) return;
161
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
+
162
186
  const payload: TransactionPayload = {
163
187
  type: config.type,
164
188
  party_id: partyId,
165
189
  warehouse_id: warehouseId!,
166
190
  transaction_date: transactionDate,
191
+ expected_delivery_date: expectedDeliveryDate || undefined,
167
192
  notes: notes || undefined,
168
193
  lines: lineItems.flatMap((l): TransactionPayload['lines'] => {
169
194
  if (l.batches && l.batches.length > 0) {
@@ -178,8 +203,11 @@ export function ProductTransactionScreen({
178
203
  selling_price: b.selling_price,
179
204
  serial_numbers: undefined,
180
205
  quantity: b.quantity,
181
- free_qty: l.free_qty,
206
+ uid: l.uid,
207
+ is_free_qty: l.is_free_qty,
208
+ parent_line_uids: l.parent_line_uids,
182
209
  uom_id: l.uom_id,
210
+ purchase_order_line_id: l.purchase_order_line_id,
183
211
  unit_price: l.unit_price,
184
212
  discount_percent: l.discount_percent,
185
213
  discount_amount: (l.discount_amount / l.quantity) * b.quantity,
@@ -197,8 +225,11 @@ export function ProductTransactionScreen({
197
225
  selling_price: undefined,
198
226
  serial_numbers: l.serial_numbers,
199
227
  quantity: l.quantity,
200
- free_qty: l.free_qty,
228
+ uid: l.uid,
229
+ is_free_qty: l.is_free_qty,
230
+ parent_line_uids: l.parent_line_uids,
201
231
  uom_id: l.uom_id,
232
+ purchase_order_line_id: l.purchase_order_line_id,
202
233
  unit_price: l.unit_price,
203
234
  discount_percent: l.discount_percent,
204
235
  discount_amount: l.discount_amount,
@@ -219,6 +250,7 @@ export function ProductTransactionScreen({
219
250
  setPayments([]);
220
251
  setNotes('');
221
252
  setTransactionDate(new Date().toISOString().split('T')[0]);
253
+ setExpectedDeliveryDate('');
222
254
  if (!defaultWarehouseId) { setWarehouseId(null); setWarehouseName(null); }
223
255
  } catch (error) {
224
256
  // The parent usually displays a toast error. We catch it here
@@ -228,7 +260,7 @@ export function ProductTransactionScreen({
228
260
  } finally {
229
261
  setIsSubmitting(false);
230
262
  }
231
- }, [lineItems, config, partyId, warehouseId, transactionDate, notes, payments, subtotal, discountTotal, grandTotal, onSubmit, defaultWarehouseId]);
263
+ }, [lineItems, config, partyId, warehouseId, transactionDate, expectedDeliveryDate, notes, payments, subtotal, discountTotal, grandTotal, onSubmit, defaultWarehouseId]);
232
264
 
233
265
  return (
234
266
  <div className="flex flex-col h-full">
@@ -260,16 +292,19 @@ export function ProductTransactionScreen({
260
292
  warehouseId={warehouseId}
261
293
  warehouseName={warehouseName}
262
294
  transactionDate={transactionDate}
295
+ expectedDeliveryDate={expectedDeliveryDate}
263
296
  notes={notes}
264
297
  payments={payments}
265
298
  onPartyChange={(id, party) => { setPartyId(id); setPartyName(party?.name ?? null); }}
266
299
  onWarehouseChange={(id, name) => { setWarehouseId(id); setWarehouseName(name ?? null); }}
267
300
  onDateChange={setTransactionDate}
301
+ onExpectedDeliveryDateChange={setExpectedDeliveryDate}
268
302
  onNotesChange={setNotes}
269
303
  onPaymentsChange={setPayments}
270
304
  onSubmit={handleSubmit}
271
305
  isSubmitting={submitting}
272
306
  renderExtraMeta={renderExtraMeta}
307
+ fetchChequeLeaves={fetchChequeLeaves}
273
308
  />
274
309
  </div>
275
310
  </div>
@@ -25,7 +25,7 @@ export interface TransactionItem {
25
25
  abbreviation: string;
26
26
  uom_group?: {
27
27
  id: number;
28
- uoms: { id: number; name: string; abbreviation: string; base_uom_id?: number | null }[];
28
+ uoms: { id: number; name: string; abbreviation: string; base_uom_id?: number | null; conversion_factor: number }[];
29
29
  };
30
30
  };
31
31
  variants?: TransactionItemVariant[];
@@ -74,8 +74,10 @@ export interface TransactionLineItem {
74
74
  batches?: TransactionLineBatch[];
75
75
  serial_numbers?: string[];
76
76
  quantity: number;
77
- free_qty?: number;
77
+ is_free_qty?: boolean;
78
+ parent_line_uids?: string[];
78
79
  uom_id?: number;
80
+ purchase_order_line_id?: number | null;
79
81
  unit_price: number;
80
82
  discount_percent: number;
81
83
  discount_amount: number;
@@ -136,6 +138,8 @@ export interface TransactionScreenConfig {
136
138
  partyIsWarehouse?: boolean;
137
139
  /** Whether to show the party selector at all (defaults to true) */
138
140
  showParty?: boolean;
141
+ /** Disable the search bar (useful for PO to GRN conversions where items are fixed) */
142
+ disableItemSelection?: boolean;
139
143
  /** Allow creating new batches and serials during selection */
140
144
  allowCreateBatchAndSerial?: boolean;
141
145
  /** If true, user must select a warehouse before searching/adding items */
@@ -144,10 +148,14 @@ export interface TransactionScreenConfig {
144
148
  allowEditPrice?: boolean;
145
149
  /** Whether to show available stock for variants in the search dropdown */
146
150
  showStock?: boolean;
147
- /** Enable free quantity input (dynamic setting) */
148
- enableFreeQty?: boolean;
149
151
  /** Enable purchase unit selection (dynamic setting) */
150
152
  enablePurchaseUnit?: boolean;
153
+ /** Show expected delivery date */
154
+ showExpectedDeliveryDate?: boolean;
155
+ /** Disable batch/serial selection completely (e.g. for Purchase Orders and Quotations) */
156
+ disableTrackingSelection?: boolean;
157
+ /** Enforce that the sum of payments equals the grand total before allowing submission */
158
+ requireFullPayment?: boolean;
151
159
  }
152
160
 
153
161
  // ── Module Presets ──
@@ -184,6 +192,7 @@ export const TRANSACTION_PRESETS: Record<TransactionType, TransactionScreenConfi
184
192
  allowCreateBatchAndSerial: false,
185
193
  allowEditPrice: false,
186
194
  showStock: true,
195
+ disableTrackingSelection: true,
187
196
  },
188
197
  purchase: {
189
198
  type: 'purchase',
@@ -200,6 +209,8 @@ export const TRANSACTION_PRESETS: Record<TransactionType, TransactionScreenConfi
200
209
  allowCreateBatchAndSerial: true,
201
210
  allowEditPrice: true,
202
211
  showStock: true,
212
+ showExpectedDeliveryDate: true,
213
+ disableTrackingSelection: true,
203
214
  },
204
215
  transfer: {
205
216
  type: 'transfer',
@@ -257,7 +268,7 @@ export const TRANSACTION_PRESETS: Record<TransactionType, TransactionScreenConfi
257
268
  priceField: 'default_cost_price',
258
269
  showPrices: true,
259
270
  allowDiscount: true,
260
- showPayment: false,
271
+ showPayment: true,
261
272
  partyLabel: 'Supplier',
262
273
  partyType: 'supplier',
263
274
  showWarehouse: true,
@@ -267,6 +278,7 @@ export const TRANSACTION_PRESETS: Record<TransactionType, TransactionScreenConfi
267
278
  allowCreateBatchAndSerial: true,
268
279
  allowEditPrice: true,
269
280
  showStock: true,
281
+ requireFullPayment: true,
270
282
  },
271
283
  };
272
284
 
@@ -283,6 +295,10 @@ export interface ProductTransactionScreenProps {
283
295
  isSubmitting?: boolean;
284
296
  /** Optional extra content rendered at the top of the metadata (right) panel */
285
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;
286
302
  }
287
303
 
288
304
  export interface TransactionPayload {
@@ -290,6 +306,7 @@ export interface TransactionPayload {
290
306
  party_id?: number | null;
291
307
  warehouse_id: number;
292
308
  transaction_date?: string;
309
+ expected_delivery_date?: string;
293
310
  notes?: string;
294
311
  lines: Array<{
295
312
  item_id: number;
@@ -302,8 +319,11 @@ export interface TransactionPayload {
302
319
  selling_price?: number;
303
320
  serial_numbers?: string[];
304
321
  quantity: number;
305
- free_qty?: number;
322
+ uid: string;
323
+ is_free_qty?: boolean;
324
+ parent_line_uids?: string[];
306
325
  uom_id?: number;
326
+ purchase_order_line_id?: number | null;
307
327
  unit_price: number;
308
328
  discount_percent: number;
309
329
  discount_amount: number;