@apptimate/ui 4.7.0 → 4.9.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.7.0",
3
+ "version": "4.9.0",
4
4
  "main": "src/index.tsx",
5
5
  "types": "src/index.tsx",
6
6
  "dependencies": {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import React, { useState } from 'react';
4
- import { Briefcase, CreditCard, Settings, Menu, X, LogOut, User as UserIcon, Building2, ChevronRight, Check } from 'lucide-react';
4
+ import { Briefcase, CreditCard, Settings, Menu, X, LogOut, User as UserIcon, Building2, ChevronRight, Check, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
5
5
  import { Dropdown, DropdownItem } from '../base-components/Dropdown';
6
6
 
7
7
  import Link from 'next/link';
@@ -67,6 +67,7 @@ export function DashboardLayout({
67
67
  }) {
68
68
  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
69
69
  const [isOrgModalOpen, setIsOrgModalOpen] = useState(false);
70
+ const [isSubSidebarOpen, setIsSubSidebarOpen] = useState(true);
70
71
  const pathname = usePathname() || "";
71
72
  const router = useRouter();
72
73
 
@@ -140,11 +141,22 @@ export function DashboardLayout({
140
141
  return (
141
142
  <div className="flex h-screen bg-[#F4F5F7] overflow-hidden font-sans print:h-auto print:overflow-visible print:bg-white">
142
143
  {/* Desktop Global Sidebar */}
143
- <aside className="hidden md:flex print:hidden w-[80px] bg-white border-r border-gray-200 flex-col items-center py-6 shrink-0 z-20">
144
+ <aside className="hidden md:flex print:hidden w-[80px] bg-white border-r border-gray-200 flex-col items-center py-6 shrink-0 z-30 relative group">
144
145
  <div className="w-12 h-12 flex items-center justify-center mb-8">
145
146
  {logo}
146
147
  </div>
147
148
 
149
+ {/* Floating Open Button when Sub-sidebar is closed */}
150
+ {!isSubSidebarOpen && activeMenu && activeMenu.groups.length > 0 && (
151
+ <button
152
+ onClick={() => setIsSubSidebarOpen(true)}
153
+ className="hidden md:flex absolute -right-4 top-6 z-40 w-8 h-8 bg-white border border-gray-200 rounded-full shadow-sm items-center justify-center text-gray-500 hover:text-[#2D3142] hover:bg-gray-50 transition-all outline-none opacity-0 group-hover:opacity-100 cursor-pointer"
154
+ title="Expand Sidebar"
155
+ >
156
+ <PanelLeftOpen size={16} />
157
+ </button>
158
+ )}
159
+
148
160
  <style dangerouslySetInnerHTML={{
149
161
  __html: `
150
162
  .sidebar-scroll {
@@ -179,7 +191,10 @@ export function DashboardLayout({
179
191
  return (
180
192
  <div
181
193
  key={menu.id}
182
- onClick={() => setActiveMenuId(menu.id)}
194
+ onClick={() => {
195
+ setActiveMenuId(menu.id);
196
+ setIsSubSidebarOpen(true);
197
+ }}
183
198
  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]"
184
199
  }`}
185
200
  >
@@ -226,8 +241,17 @@ export function DashboardLayout({
226
241
 
227
242
  {/* Desktop Secondary Sidebar */}
228
243
  {activeMenu && activeMenu.groups.length > 0 && (
229
- <aside className="hidden md:flex print:hidden w-[210px] bg-white border-gray-200 flex-col py-8 shrink-0 z-10">
230
- <h2 className="px-6 text-xl font-bold text-[#2D3142]/80 mb-6">{activeMenu.label}</h2>
244
+ <>
245
+ <aside className={cn(
246
+ "hidden md:flex print:hidden bg-white border-gray-200 flex-col py-8 shrink-0 z-10 transition-all duration-300 overflow-hidden group",
247
+ isSubSidebarOpen ? "w-[210px]" : "w-0 opacity-0 px-0"
248
+ )}>
249
+ <div className="flex items-center justify-between px-6 mb-6 w-[210px]">
250
+ <h2 className="text-xl font-bold text-[#2D3142]/80 truncate">{activeMenu.label}</h2>
251
+ <button onClick={() => setIsSubSidebarOpen(false)} className="text-gray-400 hover:text-[#2D3142] transition-colors flex-shrink-0 outline-none opacity-0 group-hover:opacity-100 cursor-pointer">
252
+ <PanelLeftClose size={18} />
253
+ </button>
254
+ </div>
231
255
  <div className="flex-1 overflow-y-auto px-4 space-y-6">
232
256
  {activeMenu.groups.map((group) => (
233
257
  <div key={group.id}>
@@ -296,6 +320,8 @@ export function DashboardLayout({
296
320
  </div>
297
321
  )}
298
322
  </aside>
323
+
324
+ </>
299
325
  )}
300
326
 
301
327
  {/* Main Content Area & Mobile Header */}
@@ -14,7 +14,7 @@ interface SerialSelectionModalProps {
14
14
  allowCreate?: boolean;
15
15
  initialSerials?: string[];
16
16
  warehouseId?: number;
17
- onConfirm: (serials: string[]) => void;
17
+ onConfirm: (serials: string[], serialObjects?: {id: number, serial_number: string}[]) => void;
18
18
  }
19
19
 
20
20
  export function SerialSelectionModal({
@@ -119,7 +119,15 @@ export function SerialSelectionModal({
119
119
  toast.error("Please add at least one serial number.");
120
120
  return;
121
121
  }
122
- onConfirm(serials);
122
+
123
+ if (!allowCreate) {
124
+ const serialObjects = availableSerials
125
+ .filter(s => serials.includes(s.serial_number))
126
+ .map(s => ({ id: s.id, serial_number: s.serial_number }));
127
+ onConfirm(serials, serialObjects);
128
+ } else {
129
+ onConfirm(serials);
130
+ }
123
131
  };
124
132
 
125
133
  // Display available serials from backend
@@ -20,7 +20,7 @@ interface ProductSelectionPanelProps {
20
20
  warehouseId?: number;
21
21
  partyId?: number | null;
22
22
  lineItems: TransactionLineItem[];
23
- onAddItem: (item: TransactionItem, variant?: any, extraData?: { quantity?: number; batches?: any[]; serial_numbers?: string[] }) => void;
23
+ onAddItem: (item: TransactionItem, variant?: any, extraData?: { quantity?: number; batches?: any[]; serial_numbers?: string[]; serial_number_objects?: {id: number, serial_number: string}[] }) => void;
24
24
  onUpdateLine: (uid: string, updates: Partial<TransactionLineItem>) => void;
25
25
  onRemoveLine: (uid: string) => void;
26
26
  onToggleExpand: (uid: string) => void;
@@ -96,7 +96,7 @@ export function ProductSelectionPanel({
96
96
  onAddItem(entity.item, entity.variant, { serial_numbers: [entity.serial_number], quantity: 1 });
97
97
  } else if (matchType === 'batch') {
98
98
  const batchVariant = entity.variant || (entity.variant_id && entity.item?.variants ? entity.item.variants.find((v: any) => v.id === entity.variant_id) : null);
99
- if (['sales', 'quotation'].includes(config.type)) {
99
+ if (['sales', 'quotation', 'pos'].includes(config.type)) {
100
100
  onAddItem(entity.item, batchVariant, {
101
101
  batches: [{ batch_id: entity.id, batch_number: entity.batch_number, selling_price: entity.selling_price, quantity: 1 }],
102
102
  quantity: 1
@@ -132,7 +132,7 @@ export function ProductSelectionPanel({
132
132
  setShowResults(false);
133
133
 
134
134
  if (!config.disableTrackingSelection && item.tracking_type === 'batch') {
135
- if (['sales', 'quotation'].includes(config.type)) {
135
+ if (['sales', 'quotation', 'pos'].includes(config.type)) {
136
136
  onAddItem(item, variant);
137
137
  } else {
138
138
  setActiveBatchItem({ item, variant });
@@ -167,10 +167,10 @@ export function ProductSelectionPanel({
167
167
  }
168
168
  };
169
169
 
170
- const handleSerialConfirm = (serials: string[]) => {
170
+ const handleSerialConfirm = (serials: string[], serialObjects?: {id: number, serial_number: string}[]) => {
171
171
  if (!activeSerialItem) return;
172
172
 
173
- onAddItem(activeSerialItem.item, activeSerialItem.variant, { serial_numbers: serials, quantity: serials.length });
173
+ onAddItem(activeSerialItem.item, activeSerialItem.variant, { serial_numbers: serials, serial_number_objects: serialObjects, quantity: serials.length });
174
174
  setActiveSerialItem(null);
175
175
  };
176
176
 
@@ -582,7 +582,7 @@ export function ProductSelectionPanel({
582
582
  </THeader>
583
583
  <TBody>
584
584
  {lineItems.map((line) => {
585
- const isBatchPending = ['sales', 'quotation'].includes(config.type) && line.item.tracking_type === 'batch' && (!line.batches || line.batches.length === 0);
585
+ const isBatchPending = ['sales', 'quotation', 'pos'].includes(config.type) && line.item.tracking_type === 'batch' && (!line.batches || line.batches.length === 0);
586
586
  return (
587
587
  <TRow key={line.uid}>
588
588
  <TCell label="Item" className="py-2.5">
@@ -604,14 +604,20 @@ export function ProductSelectionPanel({
604
604
  )}
605
605
  </span>
606
606
  <span className="text-[11px] text-foreground-subtle font-mono mt-0.5 break-words">
607
- {line.item.has_variants && line.variant_id && line.item.variants?.find(v => v.id === line.variant_id)
608
- ? `${line.item.variants.find(v => v.id === line.variant_id)?.variant_name} • ${line.item.variants.find(v => v.id === line.variant_id)?.sku}`
609
- : line.item.sku}
607
+ {(() => {
608
+ if (line.item.has_variants && line.variant_id) {
609
+ const variant = line.variant || line.item.variants?.find(v => v.id === line.variant_id);
610
+ if (variant) {
611
+ return `${variant.variant_name} • ${variant.sku}`;
612
+ }
613
+ }
614
+ return line.item.sku || '';
615
+ })()}
610
616
  </span>
611
- {((!config.disableTrackingSelection && (line.item.tracking_type === 'batch' || line.item.tracking_type === 'serial')) || (line.item.tracking_type === 'batch' && ['sales', 'quotation'].includes(config.type))) && (
617
+ {((!config.disableTrackingSelection && (line.item.tracking_type === 'batch' || line.item.tracking_type === 'serial')) || (line.item.tracking_type === 'batch' && ['sales', 'quotation', 'pos'].includes(config.type))) && (
612
618
  <div className="flex flex-wrap gap-1 mt-1.5">
613
619
  {line.item.tracking_type === 'batch' && (() => {
614
- if (['sales', 'quotation'].includes(config.type)) {
620
+ if (['sales', 'quotation', 'pos'].includes(config.type)) {
615
621
  return (
616
622
  <div className="mt-1.5 flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
617
623
  {(!line.batches || line.batches.length === 0) && (
@@ -746,11 +752,16 @@ export function ProductSelectionPanel({
746
752
  ? (() => {
747
753
  if (line.parent_line_uids.length === 1) {
748
754
  const p = lineItems.find((l) => l.uid === line.parent_line_uids?.[0]);
749
- return p
750
- ? p.item.has_variants && p.variant_id && p.item.variants?.find((v) => v.id === p.variant_id)
751
- ? `${p.item.name} (${p.item.variants.find((v) => v.id === p.variant_id)?.variant_name})`
752
- : p.item.name
753
- : "Select Parent Line...";
755
+ if (p) {
756
+ if (p.item.has_variants && p.variant_id) {
757
+ const pVariant = p.variant || p.item.variants?.find((v) => v.id === p.variant_id);
758
+ if (pVariant && pVariant.variant_name) {
759
+ return `${p.item.name} (${pVariant.variant_name})`;
760
+ }
761
+ }
762
+ return p.item.name;
763
+ }
764
+ return "Select Parent Line...";
754
765
  } else {
755
766
  return `${line.parent_line_uids.length} Parents Selected`;
756
767
  }
@@ -799,7 +810,7 @@ export function ProductSelectionPanel({
799
810
  const variantPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'sale_price';
800
811
  const actualVariant = line.variant_id ? line.item.variants?.find((v: any) => v.id === line.variant_id) : (line.item.variants?.[0] || null);
801
812
  const batchPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'selling_price';
802
- const batchPrice = ['sales', 'quotation'].includes(config.type) && line.batches?.[0] ? (line.batches[0] as any)[batchPriceField] : undefined;
813
+ const batchPrice = ['sales', 'quotation', 'pos'].includes(config.type) && line.batches?.[0] ? (line.batches[0] as any)[batchPriceField] : undefined;
803
814
  const basePrice = (batchPrice !== undefined && batchPrice !== null)
804
815
  ? Number(batchPrice)
805
816
  : (actualVariant
@@ -920,7 +931,7 @@ export function ProductSelectionPanel({
920
931
  warehouseId={warehouseId}
921
932
  allowCreate={config.allowCreateBatchAndSerial}
922
933
  initialSerials={lineItems.find(l => l.uid === activeSerialLineUid)?.serial_numbers || []}
923
- onConfirm={(serials) => {
934
+ onConfirm={(serials, serialObjects) => {
924
935
  const line = lineItems.find(l => l.uid === activeSerialLineUid);
925
936
  if (!line) return;
926
937
 
@@ -929,6 +940,7 @@ export function ProductSelectionPanel({
929
940
 
930
941
  onUpdateLine(line.uid, {
931
942
  serial_numbers: serials,
943
+ serial_number_objects: serialObjects,
932
944
  quantity: newQty,
933
945
  discount_amount: discAmt,
934
946
  line_total: line.unit_price * newQty - discAmt,
@@ -57,24 +57,24 @@ export function ProductTransactionScreen({
57
57
  setLineItems(prev => prev.map(l => {
58
58
  if (l.is_free_qty || !l.variant_id || !evaluatedRules[l.variant_id]) return l;
59
59
  const rules = evaluatedRules[l.variant_id];
60
-
60
+
61
61
  const variantPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'sale_price';
62
62
  const actualVariant = l.item.variants?.find((v: any) => v.id === l.variant_id);
63
63
  const batchPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'selling_price';
64
64
  const batchPrice = ['sales', 'quotation'].includes(config.type) && l.batches?.[0] ? (l.batches[0] as any)[batchPriceField] : undefined;
65
65
  const basePrice = (batchPrice !== undefined && batchPrice !== null)
66
66
  ? Number(batchPrice)
67
- : (actualVariant
68
- ? Number(actualVariant[variantPriceField] || l.item[config.priceField] || 0)
67
+ : (actualVariant
68
+ ? Number(actualVariant[variantPriceField] || l.item[config.priceField] || 0)
69
69
  : Number(l.item[config.priceField] || 0));
70
-
70
+
71
71
  const uoms = l.item.uom?.uom_group?.uoms || [];
72
72
  const currentUom = uoms.find((u: any) => u.id === (l.uom_id || l.item.uom_id));
73
73
  const factor = Number(currentUom?.conversion_factor) || 1;
74
74
  const effectiveQty = l.quantity * factor;
75
75
 
76
76
  const priceDetails = PriceCalculationService.calculatePriceDetails(basePrice, effectiveQty, rules, l.batches?.[0]?.batch_id);
77
-
77
+
78
78
  return {
79
79
  ...l,
80
80
  pricing_rules: rules,
@@ -111,6 +111,13 @@ export function ProductTransactionScreen({
111
111
  setWarehouseId(initialData.warehouse_id);
112
112
  if (initialData.warehouse?.name) setWarehouseName(initialData.warehouse.name);
113
113
  }
114
+ if (initialData.transaction_date || initialData.order_date || initialData.quotation_date || initialData.date) {
115
+ setTransactionDate(initialData.transaction_date || initialData.order_date || initialData.quotation_date || initialData.date);
116
+ }
117
+ if (initialData.expected_delivery_date || initialData.expected_delivery) {
118
+ setExpectedDeliveryDate(initialData.expected_delivery_date || initialData.expected_delivery);
119
+ }
120
+
114
121
  if (initialData.lines && Array.isArray(initialData.lines)) {
115
122
  setLineItems(
116
123
  initialData.lines.map((l: any) => {
@@ -120,6 +127,7 @@ export function ProductTransactionScreen({
120
127
  uid: l.uid || Math.random().toString(36).slice(2, 7),
121
128
  item: l.item,
122
129
  variant_id: l.variant_id || null,
130
+ variant: l.variant || null,
123
131
  batches: l.batches || [],
124
132
  serial_numbers: l.serial_numbers || [],
125
133
  quantity: qty,
@@ -135,7 +143,7 @@ export function ProductTransactionScreen({
135
143
  };
136
144
  })
137
145
  );
138
-
146
+
139
147
  // Populate pricing rules for existing items if party is selected
140
148
  if (initialData.party_id && ['sales', 'pos', 'quotation'].includes(config.type)) {
141
149
  evaluatePriceLists({
@@ -152,7 +160,7 @@ export function ProductTransactionScreen({
152
160
  return line;
153
161
  }));
154
162
  }
155
- }).catch(() => {});
163
+ }).catch(() => { });
156
164
  }
157
165
  }
158
166
  }
@@ -186,7 +194,7 @@ export function ProductTransactionScreen({
186
194
  ? Number(batchPrice)
187
195
  : (actualVariant ? Number(actualVariant[variantPriceField] || itemWithVariants[config.priceField] || 0) : Number(itemWithVariants[config.priceField] || 0));
188
196
  const qty = extra?.quantity || 1;
189
-
197
+
190
198
  const uomId = itemWithVariants.uom_id;
191
199
  const uoms = itemWithVariants.uom?.uom_group?.uoms || [];
192
200
  const currentUom = uoms.find((u: any) => u.id === uomId);
@@ -206,7 +214,7 @@ export function ProductTransactionScreen({
206
214
  discountAmt = priceDetails.discountAmount * factor;
207
215
  appliedRule = priceDetails.appliedRule;
208
216
  }
209
-
217
+
210
218
  const newLine: TransactionLineItem = {
211
219
  uid: `${itemWithVariants.id}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
212
220
  item: itemWithVariants,
@@ -253,14 +261,14 @@ export function ProductTransactionScreen({
253
261
  const batchPrice = ['sales', 'quotation'].includes(config.type) && updatedLine.batches?.[0] ? (updatedLine.batches[0] as any)[batchPriceField] : undefined;
254
262
  const basePrice = (batchPrice !== undefined && batchPrice !== null)
255
263
  ? Number(batchPrice)
256
- : (actualVariant
257
- ? Number(actualVariant[variantPriceField] || updatedLine.item[config.priceField] || 0)
264
+ : (actualVariant
265
+ ? Number(actualVariant[variantPriceField] || updatedLine.item[config.priceField] || 0)
258
266
  : Number(updatedLine.item[config.priceField] || 0));
259
-
267
+
260
268
  const uoms = updatedLine.item.uom?.uom_group?.uoms || [];
261
269
  const currentUom = uoms.find((u: any) => u.id === (updatedLine.uom_id || updatedLine.item.uom_id));
262
270
  const factor = Number(currentUom?.conversion_factor) || 1;
263
-
271
+
264
272
  const effectiveQty = updatedLine.quantity * factor;
265
273
 
266
274
  console.log("DEBUG: handleUpdateLine rules -> ", {
@@ -275,7 +283,7 @@ export function ProductTransactionScreen({
275
283
  updatedLine.line_total = (updatedLine.unit_price * updatedLine.quantity) - updatedLine.discount_amount;
276
284
  } else if (isQtyChanged || isUomChanged || isPriceChanged || isDiscountChanged) {
277
285
  updatedLine.line_total = (updatedLine.unit_price * updatedLine.quantity) - updatedLine.discount_amount;
278
-
286
+
279
287
  const isManualPriceOverride = isPriceChanged && !isUomChanged;
280
288
  if (isManualPriceOverride) {
281
289
  updatedLine.applied_price_rule = undefined;
@@ -286,6 +294,14 @@ export function ProductTransactionScreen({
286
294
  updatedLine.batches[0].quantity = updatedLine.quantity;
287
295
  }
288
296
 
297
+ if (updatedLine.is_free_qty) {
298
+ updatedLine.unit_price = 0;
299
+ updatedLine.discount_amount = 0;
300
+ updatedLine.discount_percent = 0;
301
+ updatedLine.line_total = 0;
302
+ updatedLine.applied_price_rule = undefined;
303
+ }
304
+
289
305
  return updatedLine;
290
306
  })
291
307
  );
@@ -397,7 +413,9 @@ export function ProductTransactionScreen({
397
413
  expiry_date: undefined,
398
414
  supplier_reference: undefined,
399
415
  selling_price: undefined,
400
- serial_numbers: l.serial_numbers,
416
+ serial_numbers: l.serial_number_objects && l.serial_number_objects.length > 0
417
+ ? l.serial_number_objects.map(o => o.id)
418
+ : l.serial_numbers,
401
419
  quantity: l.quantity,
402
420
  uid: l.uid,
403
421
  is_free_qty: l.is_free_qty,
@@ -470,7 +488,7 @@ export function ProductTransactionScreen({
470
488
  expectedDeliveryDate={expectedDeliveryDate}
471
489
  notes={notes}
472
490
  payments={payments}
473
- onPartyChange={async (id, party) => {
491
+ onPartyChange={async (id, party) => {
474
492
  if (partyId !== id && lineItems.length > 0 && ['sales', 'pos', 'quotation'].includes(config.type)) {
475
493
  if (hasActivePriceLists) {
476
494
  toast.loading('Checking price changes...', { id: 'price-calc' });
@@ -480,12 +498,12 @@ export function ProductTransactionScreen({
480
498
  transaction_type: config.type,
481
499
  items: lineItems.filter(l => l.variant_id && !l.is_free_qty).map(l => ({ variant_id: l.variant_id! }))
482
500
  });
483
-
501
+
484
502
  toast.dismiss('price-calc');
485
-
503
+
486
504
  if (res.is_success && res.result) {
487
505
  const evaluatedRules = res.result as Record<number, any[]>;
488
-
506
+
489
507
  let hasActualPriceChanges = false;
490
508
  for (const l of lineItems) {
491
509
  if (!l.is_free_qty && l.variant_id && evaluatedRules[l.variant_id]) {
@@ -496,24 +514,24 @@ export function ProductTransactionScreen({
496
514
  const batchPrice = ['sales', 'quotation'].includes(config.type) && l.batches?.[0] ? (l.batches[0] as any)[batchPriceField] : undefined;
497
515
  const basePrice = (batchPrice !== undefined && batchPrice !== null)
498
516
  ? Number(batchPrice)
499
- : (actualVariant
500
- ? Number(actualVariant[variantPriceField] || l.item[config.priceField] || 0)
517
+ : (actualVariant
518
+ ? Number(actualVariant[variantPriceField] || l.item[config.priceField] || 0)
501
519
  : Number(l.item[config.priceField] || 0));
502
-
520
+
503
521
  const uoms = l.item.uom?.uom_group?.uoms || [];
504
522
  const currentUom = uoms.find((u: any) => u.id === (l.uom_id || l.item.uom_id));
505
523
  const factor = Number(currentUom?.conversion_factor) || 1;
506
524
  const effectiveQty = l.quantity * factor;
507
525
 
508
526
  const priceDetails = PriceCalculationService.calculatePriceDetails(basePrice, effectiveQty, rules, l.batches?.[0]?.batch_id);
509
-
527
+
510
528
  if (Math.abs((priceDetails.unitPrice * factor) - l.unit_price) > 0.001) {
511
529
  hasActualPriceChanges = true;
512
530
  break;
513
531
  }
514
532
  }
515
533
  }
516
-
534
+
517
535
  if (hasActualPriceChanges) {
518
536
  const previousRulesApplied = lineItems.some(l => l.applied_price_rule);
519
537
  setPriceEvaluationModal({
@@ -542,8 +560,8 @@ export function ProductTransactionScreen({
542
560
  }
543
561
  }
544
562
  }
545
- setPartyId(id);
546
- setPartyName(party?.name ?? null);
563
+ setPartyId(id);
564
+ setPartyName(party?.name ?? null);
547
565
  }}
548
566
  onWarehouseChange={(id, name) => { setWarehouseId(id); setWarehouseName(name ?? null); }}
549
567
  onDateChange={setTransactionDate}
@@ -574,15 +592,15 @@ export function ProductTransactionScreen({
574
592
  size="sm"
575
593
  >
576
594
  <div className="text-[14px] text-foreground-1 leading-relaxed">
577
- {priceEvaluationModal.type === 'confirm'
595
+ {priceEvaluationModal.type === 'confirm'
578
596
  ? "A few items have price changes based on the price list. Do you want to apply them?"
579
597
  : "Prices need to be changed based on the party selection."}
580
598
  </div>
581
599
  <ModalFooter>
582
600
  {priceEvaluationModal.type === 'confirm' && (
583
- <Button
584
- variant="flat"
585
- color="secondary"
601
+ <Button
602
+ variant="flat"
603
+ color="secondary"
586
604
  onClick={() => {
587
605
  setPartyId(priceEvaluationModal.pendingPartyId);
588
606
  setPartyName(priceEvaluationModal.pendingParty?.name ?? null);
@@ -592,8 +610,8 @@ export function ProductTransactionScreen({
592
610
  No
593
611
  </Button>
594
612
  )}
595
- <Button
596
- color="primary"
613
+ <Button
614
+ color="primary"
597
615
  onClick={() => {
598
616
  applyEvaluatedPrices(priceEvaluationModal.evaluatedRules);
599
617
  setPartyId(priceEvaluationModal.pendingPartyId);
@@ -72,8 +72,10 @@ export interface TransactionLineItem {
72
72
  uid: string; // Client-side unique key for React
73
73
  item: TransactionItem;
74
74
  variant_id?: number | null;
75
+ variant?: TransactionItemVariant | null;
75
76
  batches?: TransactionLineBatch[];
76
77
  serial_numbers?: string[];
78
+ serial_number_objects?: {id: number, serial_number: string}[];
77
79
  quantity: number;
78
80
  is_free_qty?: boolean;
79
81
  parent_line_uids?: string[];
@@ -333,7 +335,7 @@ export interface TransactionPayload {
333
335
  expiry_date?: string;
334
336
  supplier_reference?: string;
335
337
  selling_price?: number;
336
- serial_numbers?: string[];
338
+ serial_numbers?: string[] | number[];
337
339
  quantity: number;
338
340
  uid: string;
339
341
  is_free_qty?: boolean;
@@ -0,0 +1,142 @@
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { Button, Input, ModalFooter } from "../index";
5
+ import { PaymentSection, PaymentEntry } from "../common-components/PaymentSection";
6
+ import toast from "react-hot-toast";
7
+
8
+ interface PaymentFormProps {
9
+ invoice: any;
10
+ onSuccess: () => void;
11
+ onCancel: () => void;
12
+ onRecordPayment: (payload: any) => Promise<{ is_success: boolean; message?: string }>;
13
+ fetchPaymentModes: () => Promise<any>;
14
+ fetchBankAccounts: () => Promise<any>;
15
+ fetchChequeLeaves?: (query: string, page?: number) => Promise<any>;
16
+ }
17
+
18
+ export function PaymentForm({
19
+ invoice,
20
+ onSuccess,
21
+ onCancel,
22
+ onRecordPayment,
23
+ fetchPaymentModes,
24
+ fetchBankAccounts,
25
+ fetchChequeLeaves,
26
+ }: PaymentFormProps) {
27
+ const [isSubmitting, setIsSubmitting] = useState(false);
28
+ const outstanding = Math.abs(Number(invoice.amount_due || 0) - Number(invoice.amount_settled || invoice.amount_received || 0));
29
+
30
+ const [paymentDate, setPaymentDate] = useState(new Date().toISOString().split("T")[0]);
31
+ const [reference, setReference] = useState("");
32
+ const [remarks, setRemarks] = useState("");
33
+ const [payments, setPayments] = useState<PaymentEntry[]>([]);
34
+
35
+ const handleSubmit = async (e: React.FormEvent) => {
36
+ e.preventDefault();
37
+ if (payments.length === 0) {
38
+ toast.error("Please add at least one payment");
39
+ return;
40
+ }
41
+
42
+ const totalToPay = payments.reduce((sum, p) => sum + (Number(p.amount) || 0), 0);
43
+ if (totalToPay > outstanding + 0.01) {
44
+ toast.error(`Payment amount exceeds outstanding balance (${outstanding.toFixed(2)})`);
45
+ return;
46
+ }
47
+
48
+ setIsSubmitting(true);
49
+ let successCount = 0;
50
+
51
+ try {
52
+ for (const p of payments) {
53
+ if (Number(p.amount) <= 0) continue;
54
+ const payload: any = {
55
+ invoice_id: invoice.id,
56
+ payment_date: paymentDate,
57
+ payment_amount: p.amount,
58
+ payment_method: p.mode_code,
59
+ payment_mode_id: p.mode_id,
60
+ remarks: remarks || null,
61
+ reference: reference || null,
62
+ };
63
+
64
+ if (p.metadata) {
65
+ if (p.metadata.reference) payload.reference = p.metadata.reference;
66
+ if (p.metadata.bank_account_id) payload.bank_account_id = p.metadata.bank_account_id;
67
+ payload.mode_metadata = p.metadata;
68
+ }
69
+
70
+ const res = await onRecordPayment(payload);
71
+ if (res.is_success) successCount++;
72
+ else toast.error(`Failed for ${p.mode_name}: ${res.message}`);
73
+ }
74
+
75
+ if (successCount > 0) {
76
+ toast.success("Payment recorded successfully");
77
+ onSuccess();
78
+ }
79
+ } catch (e: any) {
80
+ toast.error(e.message || "Error recording payment");
81
+ } finally {
82
+ setIsSubmitting(false);
83
+ }
84
+ };
85
+
86
+ return (
87
+ <form onSubmit={handleSubmit} className="space-y-4">
88
+ <div className="flex flex-col gap-1.5">
89
+ <label className="text-[11px] font-bold text-gray-500 uppercase tracking-wider">Payment Date *</label>
90
+ <Input
91
+ type="date"
92
+ value={paymentDate}
93
+ onChange={(e) => setPaymentDate(e.target.value)}
94
+ />
95
+ </div>
96
+
97
+ <PaymentSection
98
+ totalAmount={outstanding}
99
+ payments={payments}
100
+ onPaymentsChange={setPayments}
101
+ fetchPaymentModes={async () => {
102
+ const res = await fetchPaymentModes();
103
+ if (res.is_success && Array.isArray(res.result)) {
104
+ res.result = res.result.filter((m: any) => {
105
+ const code = m.code?.toLowerCase();
106
+ return code !== "credit" && code !== "credit-metal";
107
+ });
108
+ }
109
+ return res;
110
+ }}
111
+ fetchBankAccounts={fetchBankAccounts}
112
+ fetchChequeLeaves={fetchChequeLeaves}
113
+ />
114
+
115
+ <div className="flex flex-col gap-4 mt-2">
116
+ <div className="flex flex-col gap-1.5">
117
+ <label className="text-[11px] font-bold text-gray-500 uppercase tracking-wider">Reference No</label>
118
+ <Input
119
+ value={reference}
120
+ onChange={(e) => setReference(e.target.value)}
121
+ placeholder="Leave blank to auto-generate"
122
+ />
123
+ </div>
124
+ <div className="flex flex-col gap-1.5">
125
+ <label className="text-[11px] font-bold text-gray-500 uppercase tracking-wider">Remarks</label>
126
+ <textarea
127
+ value={remarks}
128
+ onChange={(e) => setRemarks(e.target.value)}
129
+ rows={2}
130
+ placeholder="Optional notes..."
131
+ className="w-full bg-white border-[1.5px] border-gray-200 rounded-[10px] px-3.5 py-2.5 text-[13px] text-gray-800 outline-none transition-all hover:border-gray-300 focus:border-primary/50 resize-none"
132
+ />
133
+ </div>
134
+ </div>
135
+
136
+ <ModalFooter>
137
+ <Button type="button" variant="flat" color="default" onClick={onCancel}>Cancel</Button>
138
+ <Button type="submit" color="primary" isLoading={isSubmitting}>Record Payment</Button>
139
+ </ModalFooter>
140
+ </form>
141
+ );
142
+ }
package/src/index.tsx CHANGED
@@ -56,3 +56,4 @@ export * from './common-components/item-wizard/SkuConfigModal';
56
56
 
57
57
  // Transaction
58
58
  export * from './common-components/transaction';
59
+ export * from './finance-components/PaymentForm';