@apptimate/ui 7.1.0 → 7.3.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.
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect } from "react";
2
- import { Search, Plus, X } from "lucide-react";
2
+ import { Search, Plus, X, Save } from "lucide-react";
3
3
  import { Modal, ModalFooter } from "../../../base-components/Modal";
4
4
  import { Button } from "../../../base-components/Button";
5
5
  import { Table, THeader, TBody, TRow, TCell } from "../../../base-components/Table";
@@ -84,7 +84,7 @@ export function BatchSelectionModal({
84
84
  const params = new URLSearchParams({ item_id: String(item.id) });
85
85
  if (variant) params.append("variant_id", String(variant.id));
86
86
  if (warehouseId) params.append("warehouse_id", String(warehouseId));
87
-
87
+
88
88
  const res = await sendRequest({ url: `${process.env.NEXT_PUBLIC_API_URL}/api/inventory/batches/lookup?${params.toString()}`, method: "GET" });
89
89
  if (res.responseData?.result) {
90
90
  setBatches(res.responseData.result);
@@ -127,7 +127,7 @@ export function BatchSelectionModal({
127
127
  }
128
128
  const batch = batches.find(b => String(b.id) === String(batchId));
129
129
  if (!batch) return;
130
-
130
+
131
131
  // Check if already in rows
132
132
  if (rows.some(r => String(r.batch_id) === String(batch.id))) {
133
133
  toast.error("Batch already added to the list.");
@@ -150,30 +150,69 @@ export function BatchSelectionModal({
150
150
  setSelectKey(prev => prev + 1);
151
151
  };
152
152
 
153
- const updateRow = (uid: string, field: keyof BatchRow, value: string) => {
153
+ const updateRow = (uid: string, field: keyof BatchRow, value: any) => {
154
154
  setRows(rows.map(r => r.uid === uid ? { ...r, [field]: value } : r));
155
155
  };
156
156
 
157
+ const saveBatchToDB = async (row: BatchRow) => {
158
+ if (!item) return;
159
+ const price = parseFloat(row.selling_price);
160
+ if (isNaN(price) || price < 0) {
161
+ toast.error("Please enter a valid selling price to save.");
162
+ return;
163
+ }
164
+
165
+ const payload = {
166
+ item_id: item.id,
167
+ variant_id: variant?.id,
168
+ warehouse_id: warehouseId,
169
+ batch_number: row.batch_number || undefined,
170
+ manufacturing_date: row.manufacturing_date || undefined,
171
+ expiry_date: row.expiry_date || undefined,
172
+ supplier_reference: row.supplier_reference || undefined,
173
+ selling_price: price,
174
+ };
175
+
176
+ const loadingToast = toast.loading("Saving batch...");
177
+ try {
178
+ const res = await sendRequest({
179
+ url: `${process.env.NEXT_PUBLIC_API_URL}/api/inventory/batches`,
180
+ method: "POST",
181
+ data: payload
182
+ });
183
+ if (res.ok) {
184
+ toast.success("Batch saved successfully", { id: loadingToast });
185
+ const newBatch = res.responseData?.result;
186
+ if (newBatch) {
187
+ setRows(rows.map(r => r.uid === row.uid ? {
188
+ ...r,
189
+ isNew: false,
190
+ batch_id: newBatch.id,
191
+ batch_number: newBatch.batch_number
192
+ } : r));
193
+ fetchBatches();
194
+ }
195
+ } else {
196
+ toast.error(res.responseData?.message || "Failed to save batch", { id: loadingToast });
197
+ }
198
+ } catch (e: any) {
199
+ toast.error("Failed to save batch", { id: loadingToast });
200
+ }
201
+ };
202
+
157
203
  const removeRow = (uid: string) => {
158
204
  setRows(rows.filter(r => r.uid !== uid));
159
205
  };
160
206
 
161
207
  const handleConfirm = () => {
162
208
  const selections: { batch: TransactionBatch; quantity: number }[] = [];
163
-
209
+
164
210
  for (const row of rows) {
165
211
  const qty = parseFloat(row.quantity);
166
212
  if (isNaN(qty) || qty <= 0) {
167
213
  toast.error("Please enter a valid quantity for all selected batches.");
168
214
  return;
169
215
  }
170
- if (row.isNew) {
171
- const price = parseFloat(row.selling_price);
172
- if (isNaN(price) || price < 0) {
173
- toast.error("Please enter a valid selling price for new batches.");
174
- return;
175
- }
176
- }
177
216
  if (!row.isNew && !allowCreate && row.qty_available !== undefined && qty > row.qty_available) {
178
217
  toast.error(`Quantity exceeds available stock for batch ${row.batch_number}.`);
179
218
  return;
@@ -206,37 +245,39 @@ export function BatchSelectionModal({
206
245
  return (
207
246
  <Modal isOpen={isOpen} onClose={onClose} size="xl" title={`Batch Selection - ${item?.name} ${variant ? `(${variant.variant_name})` : ""}`}>
208
247
  <div className="flex flex-col gap-4">
209
-
248
+
210
249
  <div className="flex justify-between items-center">
211
250
  <div className="flex gap-2 items-center">
212
251
  {allowCreate && (
213
252
  <Button size="small" onClick={handleAddRow} icon={<Plus size={16} />}>New Batch</Button>
214
253
  )}
215
254
  <div className="w-64">
216
- <SearchableSelect
255
+ <SearchableSelect
217
256
  key={selectKey}
218
257
  options={batches as any[]}
219
258
  onChange={(val) => {
220
- if(val) handleSelectExisting(val as string | number);
259
+ if (val) handleSelectExisting(val as string | number);
221
260
  }}
222
261
  placeholder="Select Existing Batch..."
223
- option={{ label: 'batch_number', value: 'id', renderOption: (opt: any) => (
224
- <div className="flex flex-col w-full py-0.5 gap-0.5">
225
- <div className="flex justify-between items-center w-full gap-2">
226
- <span className="truncate font-medium">{opt.batch_number}</span>
227
- {opt.qty_available !== undefined && (
228
- <span className="text-[10px] font-medium text-foreground-subtle bg-surface-hover px-1.5 py-0.5 rounded whitespace-nowrap shrink-0">
229
- {Number(opt.qty_available).toFixed(2)} in stock
262
+ option={{
263
+ label: 'batch_number', value: 'id', renderOption: (opt: any) => (
264
+ <div className="flex flex-col w-full py-0.5 gap-0.5">
265
+ <div className="flex justify-between items-center w-full gap-2">
266
+ <span className="truncate font-medium">{opt.batch_number}</span>
267
+ {opt.qty_available !== undefined && (
268
+ <span className="text-[10px] font-medium text-foreground-subtle bg-surface-hover px-1.5 py-0.5 rounded whitespace-nowrap shrink-0">
269
+ {Number(opt.qty_available).toFixed(2)} in stock
270
+ </span>
271
+ )}
272
+ </div>
273
+ {opt.expiry_date && (
274
+ <span className="text-[10px] text-foreground-subtle">
275
+ Exp: {opt.expiry_date.substring(0, 10)}
230
276
  </span>
231
277
  )}
232
278
  </div>
233
- {opt.expiry_date && (
234
- <span className="text-[10px] text-foreground-subtle">
235
- Exp: {opt.expiry_date.substring(0, 10)}
236
- </span>
237
- )}
238
- </div>
239
- ) }}
279
+ )
280
+ }}
240
281
  />
241
282
  </div>
242
283
  </div>
@@ -265,93 +306,105 @@ export function BatchSelectionModal({
265
306
  rows.map(row => {
266
307
  const batchInfo = batches.find(b => String(b.id) === String(row.batch_id));
267
308
  const displayQtyAvailable = row.qty_available !== undefined ? row.qty_available : batchInfo?.qty_available;
268
-
309
+
269
310
  return (
270
- <TRow key={row.uid}>
271
- <TCell label="Batch Number">
272
- {row.isNew ? (
273
- <input
274
- value={row.batch_number}
275
- onChange={(e) => updateRow(row.uid, 'batch_number', e.target.value)}
276
- placeholder="Auto generate if empty"
277
- className="w-full 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"
278
- />
279
- ) : (
280
- <div className="font-mono text-sm font-medium">{row.batch_number}</div>
281
- )}
282
- </TCell>
283
- <TCell label="Mfg Batch#">
284
- {row.isNew ? (
285
- <input
286
- value={row.supplier_reference}
287
- onChange={(e) => updateRow(row.uid, 'supplier_reference', e.target.value)}
288
- className="w-full 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"
289
- />
290
- ) : (
291
- <div className="text-sm text-gray-500">{row.supplier_reference || '—'}</div>
292
- )}
293
- </TCell>
294
- <TCell label="Mfg Date">
295
- {row.isNew ? (
296
- <input
297
- type="date"
298
- value={row.manufacturing_date}
299
- onChange={(e) => updateRow(row.uid, 'manufacturing_date', e.target.value)}
300
- className="w-full 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"
301
- />
302
- ) : (
303
- <div className="text-sm text-gray-500">{row.manufacturing_date || '—'}</div>
304
- )}
305
- </TCell>
306
- <TCell label="Expiry Date">
307
- {row.isNew ? (
308
- <input
309
- type="date"
310
- value={row.expiry_date}
311
- onChange={(e) => updateRow(row.uid, 'expiry_date', e.target.value)}
312
- className="w-full 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"
313
- />
314
- ) : (
315
- <div className="text-sm text-gray-500">{row.expiry_date ? new Date(row.expiry_date).toLocaleDateString() : '—'}</div>
316
- )}
317
- </TCell>
318
- <TCell label="Selling Price">
319
- {row.isNew ? (
320
- <input
321
- type="number"
322
- value={row.selling_price}
323
- onChange={(e) => updateRow(row.uid, 'selling_price', e.target.value)}
324
- className="w-full 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"
325
- />
326
- ) : (
327
- <div className="text-sm text-gray-500">{row.selling_price || '—'}</div>
328
- )}
329
- </TCell>
330
- <TCell label="Quantity">
331
- <div className="flex flex-col items-end">
332
- <input
333
- type="number"
334
- min="0"
335
- max={!allowCreate && displayQtyAvailable !== undefined ? displayQtyAvailable : undefined}
336
- value={row.quantity}
337
- onChange={(e) => updateRow(row.uid, 'quantity', e.target.value)}
338
- placeholder="0"
339
- className="w-full 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"
340
- />
341
- {!row.isNew && displayQtyAvailable !== undefined && (
342
- <span className="text-[10px] text-gray-400 mt-0.5">Avail: {Number(displayQtyAvailable).toFixed(4)}</span>
311
+ <TRow key={row.uid}>
312
+ <TCell label="Batch Number">
313
+ {row.isNew ? (
314
+ <input
315
+ value={row.batch_number}
316
+ onChange={(e) => updateRow(row.uid, 'batch_number', e.target.value)}
317
+ placeholder="Auto generate if empty"
318
+ className="w-full 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"
319
+ />
320
+ ) : (
321
+ <div className="font-mono text-sm font-medium">{row.batch_number}</div>
343
322
  )}
344
- </div>
345
- </TCell>
346
- <TCell label="">
347
- <button
348
- onClick={() => removeRow(row.uid)}
349
- className="p-1 text-gray-400 hover:text-danger-500 rounded-md hover:bg-danger-50 transition-colors w-full flex justify-center"
350
- >
351
- <X size={16} />
352
- </button>
353
- </TCell>
354
- </TRow>
323
+ </TCell>
324
+ <TCell label="Mfg Batch#">
325
+ {row.isNew ? (
326
+ <input
327
+ value={row.supplier_reference}
328
+ onChange={(e) => updateRow(row.uid, 'supplier_reference', e.target.value)}
329
+ className="w-full 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"
330
+ />
331
+ ) : (
332
+ <div className="text-sm text-gray-500">{row.supplier_reference || '—'}</div>
333
+ )}
334
+ </TCell>
335
+ <TCell label="Mfg Date">
336
+ {row.isNew ? (
337
+ <input
338
+ type="date"
339
+ value={row.manufacturing_date}
340
+ onChange={(e) => updateRow(row.uid, 'manufacturing_date', e.target.value)}
341
+ className="w-full 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"
342
+ />
343
+ ) : (
344
+ <div className="text-sm text-gray-500">{row.manufacturing_date || '—'}</div>
345
+ )}
346
+ </TCell>
347
+ <TCell label="Expiry Date">
348
+ {row.isNew ? (
349
+ <input
350
+ type="date"
351
+ value={row.expiry_date}
352
+ onChange={(e) => updateRow(row.uid, 'expiry_date', e.target.value)}
353
+ className="w-full 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"
354
+ />
355
+ ) : (
356
+ <div className="text-sm text-gray-500">{row.expiry_date ? new Date(row.expiry_date).toLocaleDateString() : '—'}</div>
357
+ )}
358
+ </TCell>
359
+ <TCell label="Selling Price">
360
+ {row.isNew ? (
361
+ <input
362
+ type="number"
363
+ value={row.selling_price}
364
+ onChange={(e) => updateRow(row.uid, 'selling_price', e.target.value)}
365
+ className="w-full 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"
366
+ />
367
+ ) : (
368
+ <div className="text-sm text-gray-500">{row.selling_price || '—'}</div>
369
+ )}
370
+ </TCell>
371
+ <TCell label="Quantity">
372
+ <div className="flex flex-col items-end">
373
+ <input
374
+ type="number"
375
+ min="0"
376
+ max={!allowCreate && displayQtyAvailable !== undefined ? displayQtyAvailable : undefined}
377
+ value={row.quantity}
378
+ onChange={(e) => updateRow(row.uid, 'quantity', e.target.value)}
379
+ placeholder="0"
380
+ className="w-full 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"
381
+ />
382
+ {!row.isNew && displayQtyAvailable !== undefined && (
383
+ <span className="text-[10px] text-gray-400 mt-0.5">Avail: {Number(displayQtyAvailable).toFixed(4)}</span>
384
+ )}
385
+ </div>
386
+ </TCell>
387
+ <TCell label="">
388
+ <div className="flex justify-center items-center gap-1 w-full">
389
+ {row.isNew && (
390
+ <button
391
+ onClick={() => saveBatchToDB(row)}
392
+ className="p-1 text-primary-600 hover:text-primary-700 rounded-md hover:bg-primary-50 transition-colors"
393
+ title="Save Batch"
394
+ >
395
+ <Save size={16} />
396
+ </button>
397
+ )}
398
+ <button
399
+ onClick={() => removeRow(row.uid)}
400
+ className="p-1 text-gray-400 hover:text-danger-500 rounded-md hover:bg-danger-50 transition-colors flex justify-center"
401
+ title="Remove"
402
+ >
403
+ <X size={16} />
404
+ </button>
405
+ </div>
406
+ </TCell>
407
+ </TRow>
355
408
  );
356
409
  })
357
410
  ) : (
@@ -36,12 +36,13 @@ interface MetadataPanelProps {
36
36
  isSubmitting: boolean;
37
37
  renderExtraMeta?: () => React.ReactNode;
38
38
  fetchChequeLeaves?: (query: string, page: number) => Promise<{ is_success: boolean; result?: any[] }>;
39
+ isFullscreen?: boolean;
39
40
  }
40
41
 
41
42
  export function MetadataPanel({
42
43
  config, lineItems, partyId, partyName, warehouseId, warehouseName, transactionDate, expectedDeliveryDate, notes, payments,
43
44
  onPartyChange, onWarehouseChange, onDateChange, onExpectedDeliveryDateChange, onNotesChange, onPaymentsChange, onSubmit, isSubmitting,
44
- renderExtraMeta, fetchChequeLeaves,
45
+ renderExtraMeta, fetchChequeLeaves, isFullscreen,
45
46
  }: MetadataPanelProps) {
46
47
  const [paymentModes, setPaymentModes] = useState<PaymentMode[]>([]);
47
48
  const [bankAccounts, setBankAccounts] = useState<BankAccount[]>([]);
@@ -132,7 +133,9 @@ export function MetadataPanel({
132
133
  );
133
134
 
134
135
  return (
135
- <div className="flex flex-col h-full gap-6">
136
+ <div className="flex flex-col h-full">
137
+ {/* ── Scrollable Content ── */}
138
+ <div className="flex-1 overflow-y-auto custom-scrollbar space-y-6 pr-1">
136
139
  {/* ── Extra Module-Specific Meta (e.g. adjustment type/reason) ── */}
137
140
  {renderExtraMeta && renderExtraMeta()}
138
141
 
@@ -216,8 +219,8 @@ export function MetadataPanel({
216
219
  </Section>
217
220
  )}
218
221
 
219
- {/* ── Totals ── */}
220
- {config.showPrices && lineItems.length > 0 && (
222
+ {/* ── Totals (hidden for POS — shown at bottom of left panel instead) ── */}
223
+ {config.showPrices && config.type !== 'pos' && lineItems.length > 0 && (
221
224
  <div className="bg-surface-0 rounded-[12px] border border-border-subtle p-4 space-y-2">
222
225
  <TotalRow label="Subtotal" value={formatCurrency(subtotal)} />
223
226
  {discountTotal > 0 && (
@@ -239,23 +242,29 @@ export function MetadataPanel({
239
242
  fetchBankAccounts={getBankAccountsLookup}
240
243
  fetchChequeLeaves={fetchChequeLeaves}
241
244
  compact={true}
245
+ collapseModes={config.type === 'pos'}
242
246
  />
243
247
  </Section>
244
248
  )}
249
+ </div>
245
250
 
246
- {/* ── Spacer ── */}
247
- <div className="flex-1" />
248
-
249
- {/* ── Submit Button ── */}
250
- <Button
251
- type="submit"
252
- color="primary"
253
- isDisabled={lineItems.length === 0}
254
- isLoading={isSubmitting}
255
- className="!w-full !py-4 !text-[14px] !font-semibold !rounded-[10px]"
256
- >
257
- {config.submitLabel}
258
- </Button>
251
+ {/* ── Submit Button (sticky bottom) ── */}
252
+ <div className="shrink-0 pt-4">
253
+ <Button
254
+ type="submit"
255
+ color="primary"
256
+ isDisabled={lineItems.length === 0}
257
+ isLoading={isSubmitting}
258
+ className={cn(
259
+ "!w-full !font-semibold transition-all",
260
+ isFullscreen
261
+ ? "!h-[72px] !text-[18px] !rounded-[12px] shadow-lg shadow-primary/20"
262
+ : "!py-4 !text-[14px] !rounded-[10px]"
263
+ )}
264
+ >
265
+ {config.submitLabel}
266
+ </Button>
267
+ </div>
259
268
  </div>
260
269
  );
261
270
  }
@@ -75,6 +75,7 @@ export function ProductSelectionPanel({
75
75
  try {
76
76
  const params: Record<string, string | number> = { search: value, limit: 20, type: config.type };
77
77
  if (warehouseId) params.warehouse_id = warehouseId;
78
+ if (config.allowPhantomBOM) params.allow_phantom_bom = 1;
78
79
 
79
80
  if (['sales', 'quotation', 'pos'].includes(config.type)) {
80
81
  params.apply_sales_prices = 1;
@@ -261,7 +262,7 @@ export function ProductSelectionPanel({
261
262
  let discAmt = line.discount_amount;
262
263
  let discPercent = line.discount_percent;
263
264
 
264
- if (field === 'quantity') qty = Math.max(0.001, num);
265
+ if (field === 'quantity') qty = Math.max(0, num);
265
266
  if (field === 'unit_price') price = Math.max(0, num);
266
267
  if (field === 'discount_amount') {
267
268
  const maxDiscount = price;
@@ -353,7 +354,7 @@ export function ProductSelectionPanel({
353
354
  };
354
355
 
355
356
  return (
356
- <div className="flex flex-col h-full">
357
+ <div className="flex flex-col flex-1 min-h-0 w-full">
357
358
  {/* ── Search Bar ── */}
358
359
  {!config.disableItemSelection && (
359
360
  <div ref={panelRef} className="relative mb-4">
@@ -637,7 +638,8 @@ export function ProductSelectionPanel({
637
638
  <AsyncSearchableSelect
638
639
  controlClassName="!min-h-8 h-8 !py-1 text-sm"
639
640
  placeholder="Select Batch..."
640
- loadOptions={async (search) => {
641
+ loadOptions={async (search, page) => {
642
+ if (page > 1) return []; // Backend is unpaginated, don't repeat on scroll
641
643
  const params: any = {};
642
644
  if (line.variant_id) params.variant_id = line.variant_id;
643
645
  if (warehouseId) params.warehouse_id = warehouseId;
@@ -728,7 +730,7 @@ export function ProductSelectionPanel({
728
730
  })()}
729
731
  </div>
730
732
  )}
731
- {Number(line.unit_price) === 0 && (
733
+ {Number(line.unit_price) === 0 && ['purchase', 'grn', 'quotation', 'sales', 'pos'].includes(config.type) && (
732
734
  <div className="mt-2 flex items-center gap-2">
733
735
  <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")}>
734
736
  <input
@@ -793,7 +795,7 @@ export function ProductSelectionPanel({
793
795
  <TCell label="Qty" className="py-2.5 text-center">
794
796
  <input
795
797
  type="number"
796
- value={line.quantity}
798
+ value={line.quantity === 0 ? '' : line.quantity}
797
799
  onChange={(e) => updateLineInput(line.uid, 'quantity', e.target.value)}
798
800
  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"
799
801
  min={0.001}
@@ -31,6 +31,7 @@ export function ProductTransactionScreen({
31
31
  fetchChequeLeaves,
32
32
  initialData,
33
33
  onAddNewItem,
34
+ isFullscreen,
34
35
  }: ProductTransactionScreenProps) {
35
36
  // ── State ──
36
37
  const [lineItems, setLineItems] = useState<TransactionLineItem[]>([]);
@@ -62,7 +63,7 @@ export function ProductTransactionScreen({
62
63
  const variantPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'sale_price';
63
64
  const actualVariant = l.item.variants?.find((v: any) => v.id === l.variant_id);
64
65
  const batchPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'selling_price';
65
- const batchPrice = ['sales', 'quotation'].includes(config.type) && l.batches?.[0] ? (l.batches[0] as any)[batchPriceField] : undefined;
66
+ const batchPrice = ['sales', 'quotation', 'pos'].includes(config.type) && l.batches?.[0] ? (l.batches[0] as any)[batchPriceField] : undefined;
66
67
  const basePrice = (batchPrice !== undefined && batchPrice !== null)
67
68
  ? Number(batchPrice)
68
69
  : (actualVariant
@@ -260,7 +261,7 @@ export function ProductTransactionScreen({
260
261
  const variantPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'sale_price';
261
262
  const actualVariant = updatedLine.item.variants?.find((v: any) => v.id === updatedLine.variant_id);
262
263
  const batchPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'selling_price';
263
- const batchPrice = ['sales', 'quotation'].includes(config.type) && updatedLine.batches?.[0] ? (updatedLine.batches[0] as any)[batchPriceField] : undefined;
264
+ const batchPrice = ['sales', 'quotation', 'pos'].includes(config.type) && updatedLine.batches?.[0] ? (updatedLine.batches[0] as any)[batchPriceField] : undefined;
264
265
  const basePrice = (batchPrice !== undefined && batchPrice !== null)
265
266
  ? Number(batchPrice)
266
267
  : (actualVariant
@@ -503,7 +504,7 @@ export function ProductTransactionScreen({
503
504
  {/* ── Two-Panel Layout ── */}
504
505
  <div className="flex-1 flex gap-5 min-h-0">
505
506
  {/* Left: Product Selection (75%) */}
506
- <div className="flex-[3] min-w-0 flex flex-col">
507
+ <div className="flex-[3] min-w-0 flex flex-col min-h-0">
507
508
  <ProductSelectionPanel
508
509
  config={config}
509
510
  partyId={partyId}
@@ -515,13 +516,41 @@ export function ProductTransactionScreen({
515
516
  onToggleExpand={handleToggleExpand}
516
517
  onAddNewItem={onAddNewItem}
517
518
  />
519
+
520
+ {/* ── POS: Amount to Pay Summary Bar ── */}
521
+ {config.type === 'pos' && lineItems.length > 0 && (
522
+ <div className="shrink-0 mt-3 bg-surface-0 rounded-[14px] border border-border-subtle px-5 py-3.5 flex items-center justify-between gap-6">
523
+ <div className="flex items-center gap-6">
524
+ <div className="flex items-center gap-2">
525
+ <span className="text-[11.5px] font-semibold text-foreground-subtle uppercase tracking-wide">Subtotal</span>
526
+ <span className="text-[13.5px] font-bold text-foreground-1">
527
+ {subtotal.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
528
+ </span>
529
+ </div>
530
+ {discountTotal > 0 && (
531
+ <div className="flex items-center gap-2">
532
+ <span className="text-[11.5px] font-semibold text-foreground-subtle uppercase tracking-wide">Discount</span>
533
+ <span className="text-[13.5px] font-bold text-green-600">
534
+ -{discountTotal.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
535
+ </span>
536
+ </div>
537
+ )}
538
+ </div>
539
+ <div className="flex items-center gap-3">
540
+ <span className="text-[12px] font-bold text-foreground-subtle uppercase tracking-wide">Amount to Pay</span>
541
+ <span className="text-[22px] font-extrabold text-foreground-0 tracking-tight tabular-nums">
542
+ {grandTotal.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
543
+ </span>
544
+ </div>
545
+ </div>
546
+ )}
518
547
  </div>
519
548
 
520
549
  {/* Divider */}
521
550
  <div className="w-px bg-border-subtle/60 shrink-0" />
522
551
 
523
552
  {/* Right: Metadata (25%) */}
524
- <div className="flex-1 min-w-0 flex flex-col overflow-y-auto custom-scrollbar pr-1">
553
+ <div className="flex-1 min-w-0 flex flex-col">
525
554
  <MetadataPanel
526
555
  config={config}
527
556
  lineItems={lineItems}
@@ -556,7 +585,7 @@ export function ProductTransactionScreen({
556
585
  const variantPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'sale_price';
557
586
  const actualVariant = l.item.variants?.find((v: any) => v.id === l.variant_id);
558
587
  const batchPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'selling_price';
559
- const batchPrice = ['sales', 'quotation'].includes(config.type) && l.batches?.[0] ? (l.batches[0] as any)[batchPriceField] : undefined;
588
+ const batchPrice = ['sales', 'quotation', 'pos'].includes(config.type) && l.batches?.[0] ? (l.batches[0] as any)[batchPriceField] : undefined;
560
589
  const basePrice = (batchPrice !== undefined && batchPrice !== null)
561
590
  ? Number(batchPrice)
562
591
  : (actualVariant
@@ -617,6 +646,7 @@ export function ProductTransactionScreen({
617
646
  isSubmitting={submitting}
618
647
  renderExtraMeta={renderExtraMeta}
619
648
  fetchChequeLeaves={fetchChequeLeaves}
649
+ isFullscreen={isFullscreen}
620
650
  />
621
651
  </div>
622
652
  </div>
@@ -171,6 +171,8 @@ export interface TransactionScreenConfig {
171
171
  disablePartySelection?: boolean;
172
172
  /** Disable warehouse selection (e.g. for revisions) */
173
173
  disableWarehouseSelection?: boolean;
174
+ /** Allow phantom BOMs to be selected (e.g. for stock decrease) */
175
+ allowPhantomBOM?: boolean;
174
176
  }
175
177
 
176
178
  // ── Module Presets ──
@@ -319,6 +321,8 @@ export interface ProductTransactionScreenProps {
319
321
  initialData?: any;
320
322
  /** Optional callback to create a new item directly from the search bar */
321
323
  onAddNewItem?: (callbacks: { setItemSearch: (value: string) => void }) => void;
324
+ /** Whether the screen is currently in fullscreen mode */
325
+ isFullscreen?: boolean;
322
326
  }
323
327
 
324
328
  export interface TransactionPayload {
@@ -81,6 +81,9 @@ export default function ImageUploadComponent({
81
81
 
82
82
  const getAuthHeaders = (): Record<string, string> => {
83
83
  const headers: Record<string, string> = {};
84
+ if (typeof window !== "undefined") {
85
+ headers["X-Tenant-Domain"] = window.location.hostname;
86
+ }
84
87
  // Add org header
85
88
  try {
86
89
  const orgRaw = typeof window !== "undefined" ? localStorage.getItem("selected_organization") : null;