@apptimate/ui 7.2.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.
- package/package.json +1 -1
- package/src/base-components/FormattedNumberInput.tsx +118 -0
- package/src/base-components/Select.tsx +1 -1
- package/src/common-components/DashboardLayout.tsx +101 -57
- package/src/common-components/PaymentSection.tsx +134 -130
- package/src/common-components/item-wizard/ItemFormWizard.tsx +118 -60
- package/src/common-components/pickers/PartyPicker.tsx +28 -401
- package/src/common-components/pickers/QuickPartyAddModal.tsx +414 -0
- package/src/common-components/pickers/UomPicker.tsx +20 -20
- package/src/common-components/pickers/modals/BatchSelectionModal.tsx +167 -114
- package/src/common-components/transaction/ProductSelectionPanel.tsx +6 -4
- package/src/common-components/transaction/ProductTransactionScreen.tsx +3 -3
- package/src/common-components/transaction/types.ts +2 -0
- package/src/components/shared/ImageUploadComponent.tsx +3 -0
- package/src/finance-components/DirectTransactionDetailModal.tsx +9 -1
- package/src/index.tsx +2 -0
package/package.json
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { cn } from '@apptimate/core-lib';
|
|
5
|
+
|
|
6
|
+
export interface FormattedNumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange' | 'type'> {
|
|
7
|
+
value?: number | string | "";
|
|
8
|
+
onChange?: (value: number | "") => void;
|
|
9
|
+
decimals?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const FormattedNumberInput = React.forwardRef<HTMLInputElement, FormattedNumberInputProps>(
|
|
13
|
+
({ value, onChange, decimals = 2, className, ...props }, ref) => {
|
|
14
|
+
const [displayValue, setDisplayValue] = useState("");
|
|
15
|
+
|
|
16
|
+
const formatNumber = (val: number | string | undefined) => {
|
|
17
|
+
if (val === "" || val === null || val === undefined) return "";
|
|
18
|
+
const num = Number(val);
|
|
19
|
+
if (isNaN(num)) return "";
|
|
20
|
+
return num.toLocaleString('en-US', { maximumFractionDigits: decimals });
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (value === "" || value === null || value === undefined) {
|
|
25
|
+
setDisplayValue("");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check if user is typing something that evaluates to the same number (like "20." or "20.0")
|
|
30
|
+
// If they are, don't overwrite displayValue to preserve their cursor/typing state
|
|
31
|
+
const currentRaw = displayValue.replace(/,/g, '');
|
|
32
|
+
if (Number(value) !== Number(currentRaw)) {
|
|
33
|
+
setDisplayValue(formatNumber(value));
|
|
34
|
+
}
|
|
35
|
+
}, [value, decimals, displayValue]);
|
|
36
|
+
|
|
37
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
38
|
+
let val = e.target.value;
|
|
39
|
+
|
|
40
|
+
// Handle empty
|
|
41
|
+
if (val === "") {
|
|
42
|
+
setDisplayValue("");
|
|
43
|
+
onChange?.("");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Remove non-numeric characters except period and minus sign
|
|
48
|
+
const isNegative = val.startsWith('-');
|
|
49
|
+
val = val.replace(/[^0-9.]/g, '');
|
|
50
|
+
|
|
51
|
+
// Prevent multiple periods
|
|
52
|
+
const parts = val.split('.');
|
|
53
|
+
if (parts.length > 2) {
|
|
54
|
+
val = parts[0] + '.' + parts.slice(1).join('');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Limit decimal places
|
|
58
|
+
if (parts.length > 1 && parts[1].length > decimals) {
|
|
59
|
+
val = parts[0] + '.' + parts[1].substring(0, decimals);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (isNegative) val = '-' + val;
|
|
63
|
+
|
|
64
|
+
// Update display value with comma formatting for the integer part
|
|
65
|
+
let formatted = val;
|
|
66
|
+
const currentParts = val.split('.');
|
|
67
|
+
const integerPart = currentParts[0];
|
|
68
|
+
|
|
69
|
+
if (integerPart && integerPart !== '-' && integerPart !== '-0') {
|
|
70
|
+
const numInt = parseInt(integerPart, 10);
|
|
71
|
+
if (!isNaN(numInt)) {
|
|
72
|
+
const formattedInt = numInt.toLocaleString('en-US');
|
|
73
|
+
formatted = (isNegative && numInt > 0) ? '-' + formattedInt : formattedInt;
|
|
74
|
+
// Re-attach decimal part if it exists
|
|
75
|
+
if (currentParts.length > 1) {
|
|
76
|
+
formatted += '.' + currentParts[1];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} else if (integerPart === '-0') {
|
|
80
|
+
formatted = '-0' + (currentParts.length > 1 ? '.' + currentParts[1] : '');
|
|
81
|
+
} else if (integerPart === '-') {
|
|
82
|
+
formatted = '-';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setDisplayValue(formatted);
|
|
86
|
+
|
|
87
|
+
if (val === "-" || val === "-0") {
|
|
88
|
+
onChange?.("");
|
|
89
|
+
} else {
|
|
90
|
+
const numericValue = parseFloat(val);
|
|
91
|
+
if (!isNaN(numericValue)) {
|
|
92
|
+
onChange?.(numericValue);
|
|
93
|
+
} else if (val === ".") {
|
|
94
|
+
onChange?.("");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
100
|
+
setDisplayValue(formatNumber(value));
|
|
101
|
+
props.onBlur?.(e);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<input
|
|
106
|
+
ref={ref}
|
|
107
|
+
type="text"
|
|
108
|
+
value={displayValue}
|
|
109
|
+
onChange={handleChange}
|
|
110
|
+
onBlur={handleBlur}
|
|
111
|
+
className={cn('outline-none', className)}
|
|
112
|
+
{...props}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
FormattedNumberInput.displayName = 'FormattedNumberInput';
|
|
@@ -6,7 +6,7 @@ import React from 'react';
|
|
|
6
6
|
import { Label } from './Label';
|
|
7
7
|
|
|
8
8
|
export interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
|
9
|
-
label?: string;
|
|
9
|
+
label?: React.ReactNode | string;
|
|
10
10
|
error?: string;
|
|
11
11
|
isRequired?: boolean;
|
|
12
12
|
options: { label: string; value: string | number }[];
|
|
@@ -70,6 +70,13 @@ export function DashboardLayout({
|
|
|
70
70
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
71
71
|
const [isOrgModalOpen, setIsOrgModalOpen] = useState(false);
|
|
72
72
|
const [isSubSidebarOpen, setIsSubSidebarOpen] = useState(true);
|
|
73
|
+
const [isMenusLoading, setIsMenusLoading] = useState(true);
|
|
74
|
+
|
|
75
|
+
React.useEffect(() => {
|
|
76
|
+
if (menus && menus.length > 0) {
|
|
77
|
+
setIsMenusLoading(false);
|
|
78
|
+
}
|
|
79
|
+
}, [menus]);
|
|
73
80
|
const pathname = usePathname() || "";
|
|
74
81
|
const router = useRouter();
|
|
75
82
|
|
|
@@ -247,7 +254,14 @@ export function DashboardLayout({
|
|
|
247
254
|
}
|
|
248
255
|
`}} />
|
|
249
256
|
<nav className="flex flex-col gap-6 flex-1 w-full sidebar-scroll pb-4">
|
|
250
|
-
{
|
|
257
|
+
{isMenusLoading ? (
|
|
258
|
+
Array.from({ length: 6 }).map((_, i) => (
|
|
259
|
+
<div key={`skel-${i}`} className="flex flex-col items-center justify-center gap-2 w-full px-1">
|
|
260
|
+
<div className="w-[22px] h-[22px] bg-gray-200/80 rounded-md animate-pulse" />
|
|
261
|
+
<div className="w-12 h-2.5 bg-gray-200/80 rounded animate-pulse" />
|
|
262
|
+
</div>
|
|
263
|
+
))
|
|
264
|
+
) : effectiveMenus.map((menu) => {
|
|
251
265
|
const isActive = activeMenuId === menu.id;
|
|
252
266
|
return (
|
|
253
267
|
<div
|
|
@@ -318,68 +332,91 @@ export function DashboardLayout({
|
|
|
318
332
|
</aside>
|
|
319
333
|
|
|
320
334
|
{/* Desktop Secondary Sidebar */}
|
|
321
|
-
{activeMenu && activeMenu.groups.length > 0
|
|
335
|
+
{isMenusLoading || (activeMenu && activeMenu.groups.length > 0) ? (
|
|
322
336
|
<>
|
|
323
337
|
<aside className={cn(
|
|
324
338
|
"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",
|
|
325
339
|
isSubSidebarOpen ? "w-[210px]" : "w-0 opacity-0 px-0"
|
|
326
340
|
)}>
|
|
327
|
-
|
|
328
|
-
<
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
</button>
|
|
332
|
-
</div>
|
|
333
|
-
<div className="flex-1 overflow-y-auto px-4 space-y-6">
|
|
334
|
-
{activeMenu.groups.map((group) => (
|
|
335
|
-
<div key={group.id}>
|
|
336
|
-
{group.label && (
|
|
337
|
-
<h3 className="px-2 text-xs font-bold text-gray-400 uppercase tracking-wider mb-2 empty:hidden">{group.label}</h3>
|
|
338
|
-
)}
|
|
339
|
-
<nav className="flex flex-col gap-1">
|
|
340
|
-
{group.items.map((item: any) => {
|
|
341
|
-
// Find the longest matching path in this group to avoid parent paths being active
|
|
342
|
-
const allGroupItems = activeMenu.groups.flatMap(g => g.items);
|
|
343
|
-
const matchingItems = allGroupItems.filter(i => fullPath === i.path || fullPath.startsWith(`${i.path}/`));
|
|
344
|
-
const longestMatch = matchingItems.sort((a, b) => b.path.length - a.path.length)[0];
|
|
345
|
-
const isItemActive = longestMatch?.path === item.path;
|
|
346
|
-
const isInternal = basePath
|
|
347
|
-
? item.path.startsWith(basePath)
|
|
348
|
-
: !externalPaths.some(ext => item.path.startsWith(ext));
|
|
349
|
-
const href = basePath && isInternal ? item.path.replace(basePath, "") || "/" : item.path;
|
|
350
|
-
|
|
351
|
-
const className = `px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${isItemActive
|
|
352
|
-
? "bg-[#F4F5F7] text-[#2D3142] font-semibold"
|
|
353
|
-
: "text-gray-500 font-medium hover:bg-gray-50"
|
|
354
|
-
}`;
|
|
355
|
-
|
|
356
|
-
const content = (
|
|
357
|
-
<>
|
|
358
|
-
<div className="flex items-center gap-3">
|
|
359
|
-
{item.icon && <span className={`${isItemActive ? 'text-[#2D3142]' : 'text-gray-400'}`}>{item.icon}</span>}
|
|
360
|
-
<span>{item.label}</span>
|
|
361
|
-
</div>
|
|
362
|
-
{item.badge && <div>{item.badge}</div>}
|
|
363
|
-
</>
|
|
364
|
-
);
|
|
365
|
-
|
|
366
|
-
return isInternal ? (
|
|
367
|
-
<Link key={item.id} href={href} className={className}>
|
|
368
|
-
{content}
|
|
369
|
-
</Link>
|
|
370
|
-
) : (
|
|
371
|
-
<a key={item.id} href={href} className={className}>
|
|
372
|
-
{content}
|
|
373
|
-
</a>
|
|
374
|
-
);
|
|
375
|
-
})}
|
|
376
|
-
</nav>
|
|
341
|
+
{isMenusLoading ? (
|
|
342
|
+
<div className="flex-1 px-4 w-[210px]">
|
|
343
|
+
<div className="flex items-center px-2 mb-8 mt-1">
|
|
344
|
+
<div className="w-24 h-6 bg-gray-200/80 rounded animate-pulse" />
|
|
377
345
|
</div>
|
|
378
|
-
|
|
379
|
-
|
|
346
|
+
<div className="space-y-8">
|
|
347
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
348
|
+
<div key={`skel-group-${i}`} className="space-y-3">
|
|
349
|
+
<div className="w-16 h-3 bg-gray-200/80 rounded animate-pulse mx-2 mb-4" />
|
|
350
|
+
{Array.from({ length: 4 }).map((_, j) => (
|
|
351
|
+
<div key={`skel-item-${j}`} className="flex items-center gap-3 px-2 py-2">
|
|
352
|
+
<div className="w-4 h-4 bg-gray-200/80 rounded animate-pulse shrink-0" />
|
|
353
|
+
<div className="w-full h-3 bg-gray-200/80 rounded animate-pulse" />
|
|
354
|
+
</div>
|
|
355
|
+
))}
|
|
356
|
+
</div>
|
|
357
|
+
))}
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
) : (
|
|
361
|
+
<>
|
|
362
|
+
<div className="flex items-center justify-between px-6 mb-6 w-[210px]">
|
|
363
|
+
<h2 className="text-xl font-bold text-[#2D3142]/80 truncate">{activeMenu!.label}</h2>
|
|
364
|
+
<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">
|
|
365
|
+
<PanelLeftClose size={18} />
|
|
366
|
+
</button>
|
|
367
|
+
</div>
|
|
368
|
+
<div className="flex-1 overflow-y-auto px-4 space-y-6">
|
|
369
|
+
{activeMenu!.groups.map((group) => (
|
|
370
|
+
<div key={group.id}>
|
|
371
|
+
{group.label && (
|
|
372
|
+
<h3 className="px-2 text-xs font-bold text-gray-400 uppercase tracking-wider mb-2 empty:hidden">{group.label}</h3>
|
|
373
|
+
)}
|
|
374
|
+
<nav className="flex flex-col gap-1">
|
|
375
|
+
{group.items.map((item: any) => {
|
|
376
|
+
// Find the longest matching path in this group to avoid parent paths being active
|
|
377
|
+
const allGroupItems = activeMenu!.groups.flatMap(g => g.items);
|
|
378
|
+
const matchingItems = allGroupItems.filter(i => fullPath === i.path || fullPath.startsWith(`${i.path}/`));
|
|
379
|
+
const longestMatch = matchingItems.sort((a, b) => b.path.length - a.path.length)[0];
|
|
380
|
+
const isItemActive = longestMatch?.path === item.path;
|
|
381
|
+
const isInternal = basePath
|
|
382
|
+
? item.path.startsWith(basePath)
|
|
383
|
+
: !externalPaths.some(ext => item.path.startsWith(ext));
|
|
384
|
+
const href = basePath && isInternal ? item.path.replace(basePath, "") || "/" : item.path;
|
|
385
|
+
|
|
386
|
+
const className = `px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${isItemActive
|
|
387
|
+
? "bg-[#F4F5F7] text-[#2D3142] font-semibold"
|
|
388
|
+
: "text-gray-500 font-medium hover:bg-gray-50"
|
|
389
|
+
}`;
|
|
390
|
+
|
|
391
|
+
const content = (
|
|
392
|
+
<>
|
|
393
|
+
<div className="flex items-center gap-3">
|
|
394
|
+
{item.icon && <span className={`${isItemActive ? 'text-[#2D3142]' : 'text-gray-400'}`}>{item.icon}</span>}
|
|
395
|
+
<span>{item.label}</span>
|
|
396
|
+
</div>
|
|
397
|
+
{item.badge && <div>{item.badge}</div>}
|
|
398
|
+
</>
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
return isInternal ? (
|
|
402
|
+
<Link key={item.id} href={href} className={className}>
|
|
403
|
+
{content}
|
|
404
|
+
</Link>
|
|
405
|
+
) : (
|
|
406
|
+
<a key={item.id} href={href} className={className}>
|
|
407
|
+
{content}
|
|
408
|
+
</a>
|
|
409
|
+
);
|
|
410
|
+
})}
|
|
411
|
+
</nav>
|
|
412
|
+
</div>
|
|
413
|
+
))}
|
|
414
|
+
</div>
|
|
415
|
+
</>
|
|
416
|
+
)}
|
|
380
417
|
|
|
381
418
|
{/* Organization Selector - Bottom of Secondary Sidebar */}
|
|
382
|
-
{organizations.length > 0 && (
|
|
419
|
+
{organizations.length > 0 && !isMenusLoading && (
|
|
383
420
|
<div className="px-4 pt-4 mt-2 border-t border-gray-100">
|
|
384
421
|
<button
|
|
385
422
|
id="org-selector-desktop"
|
|
@@ -405,7 +442,7 @@ export function DashboardLayout({
|
|
|
405
442
|
</aside>
|
|
406
443
|
|
|
407
444
|
</>
|
|
408
|
-
)}
|
|
445
|
+
) : null}
|
|
409
446
|
|
|
410
447
|
{/* Main Content Area & Mobile Header */}
|
|
411
448
|
<div className="flex-1 flex flex-col min-w-0 overflow-hidden print:overflow-visible">
|
|
@@ -481,7 +518,14 @@ export function DashboardLayout({
|
|
|
481
518
|
|
|
482
519
|
<div className="flex-1 overflow-y-auto py-6 px-4">
|
|
483
520
|
<nav className="flex flex-col gap-8">
|
|
484
|
-
{
|
|
521
|
+
{isMenusLoading ? (
|
|
522
|
+
Array.from({ length: 4 }).map((_, i) => (
|
|
523
|
+
<div key={`skel-mob-${i}`} className="flex items-center gap-3 mb-3 px-2">
|
|
524
|
+
<div className="w-5 h-5 bg-gray-200/80 rounded-md animate-pulse" />
|
|
525
|
+
<div className="w-24 h-4 bg-gray-200/80 rounded animate-pulse" />
|
|
526
|
+
</div>
|
|
527
|
+
))
|
|
528
|
+
) : effectiveMenus.map((menu) => (
|
|
485
529
|
<div key={menu.id}>
|
|
486
530
|
<div className="flex items-center gap-3 text-[#2D3142] font-bold mb-3 px-2">
|
|
487
531
|
{React.cloneElement(menu.icon as React.ReactElement<any>, {
|
|
@@ -183,11 +183,14 @@ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compac
|
|
|
183
183
|
|
|
184
184
|
// ── Auto-sync single mode amount when totalAmount changes ──
|
|
185
185
|
useEffect(() => {
|
|
186
|
-
if (selectionMode === "single" &&
|
|
187
|
-
|
|
186
|
+
if (selectionMode === "single" && paymentsRef.current.length === 1 && totalAmount > 0) {
|
|
187
|
+
const current = paymentsRef.current[0];
|
|
188
|
+
if (current.amount !== totalAmount) {
|
|
189
|
+
onPaymentsChange([{ ...current, amount: totalAmount }]);
|
|
190
|
+
}
|
|
188
191
|
}
|
|
189
192
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
190
|
-
}, [totalAmount, selectionMode]); //
|
|
193
|
+
}, [totalAmount, selectionMode, payments.length]); // payments.length triggers sync after initial cash auto-selection
|
|
191
194
|
|
|
192
195
|
const activateSplitMode = useCallback(() => {
|
|
193
196
|
setSelectionMode("split");
|
|
@@ -289,7 +292,7 @@ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compac
|
|
|
289
292
|
{/* ── Single Mode: Entry with fields (no delete) ── */}
|
|
290
293
|
{selectionMode === "single" && payments.length === 1 && (
|
|
291
294
|
<PaymentEntryRow entry={payments[0]} mode={paymentModes.find((m) => m.id === payments[0].mode_id)}
|
|
292
|
-
bankAccounts={bankAccounts} materialTypes={materialTypes} onUpdate={(updates) => updatePayment(0, updates)} onRemove={() => {}} showRemove={false} fetchChequeLeaves={fetchChequeLeaves} />
|
|
295
|
+
bankAccounts={bankAccounts} materialTypes={materialTypes} onUpdate={(updates) => updatePayment(0, updates)} onRemove={() => { }} showRemove={false} fetchChequeLeaves={fetchChequeLeaves} />
|
|
293
296
|
)}
|
|
294
297
|
|
|
295
298
|
{/* ── Split Mode: Multi-entry UI ── */}
|
|
@@ -422,137 +425,138 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
|
|
|
422
425
|
}
|
|
423
426
|
|
|
424
427
|
return (
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
<AsyncSearchableSelect
|
|
474
|
-
multiple
|
|
475
|
-
option={{ label: 'label', value: 'value' }}
|
|
476
|
-
defaultValue={
|
|
477
|
-
Array.isArray(entry.metadata[field.key])
|
|
478
|
-
? (field.options || []).filter(o => (entry.metadata[field.key] as any).includes(o.value))
|
|
479
|
-
: []
|
|
480
|
-
}
|
|
481
|
-
onChange={(val, selectedObjs: any) => {
|
|
482
|
-
const sum = Array.isArray(selectedObjs)
|
|
483
|
-
? selectedObjs.reduce((acc, curr) => acc + (curr.unapplied_amount || 0), 0)
|
|
484
|
-
: 0;
|
|
485
|
-
|
|
486
|
-
onUpdate({
|
|
487
|
-
amount: sum,
|
|
488
|
-
metadata: { ...entry.metadata, [field.key]: val as any }
|
|
489
|
-
});
|
|
490
|
-
}}
|
|
491
|
-
disabled={field.readonly}
|
|
492
|
-
required={field.required}
|
|
493
|
-
placeholder="Select..."
|
|
494
|
-
loadOptions={async () => field.options || []}
|
|
495
|
-
/>
|
|
496
|
-
) : field.key === "cheque_number" && fetchChequeLeaves ? (
|
|
497
|
-
<>
|
|
428
|
+
<div key={field.key} className="flex flex-col gap-1">
|
|
429
|
+
<label className="text-[11px] text-foreground-subtle font-bold uppercase tracking-wider flex items-center">
|
|
430
|
+
{field.label}
|
|
431
|
+
{field.required && <span className="text-danger-alt ml-0.5">*</span>}
|
|
432
|
+
{field.key === "cheque_number" && fetchChequeLeaves && !!entry.metadata[field.key] && (
|
|
433
|
+
<span className="ml-1.5 flex items-center">
|
|
434
|
+
{entry.metadata.cheque_leaf_id ? (
|
|
435
|
+
<HintIcon
|
|
436
|
+
text="Selected from your cheque book. This will be automatically issued."
|
|
437
|
+
icon={<CheckCircle size={13} className="text-green-500" />}
|
|
438
|
+
/>
|
|
439
|
+
) : (
|
|
440
|
+
<HintIcon
|
|
441
|
+
text="Not selected from cheque book. This cheque will not be auto-issued."
|
|
442
|
+
icon={<AlertTriangle size={13} className="text-amber-500" />}
|
|
443
|
+
/>
|
|
444
|
+
)}
|
|
445
|
+
</span>
|
|
446
|
+
)}
|
|
447
|
+
</label>
|
|
448
|
+
{field.type === "bank_account_select" ? (
|
|
449
|
+
<select value={entry.metadata[field.key] || ""}
|
|
450
|
+
onChange={(e) => onUpdate({ metadata: { ...entry.metadata, [field.key]: e.target.value ? Number(e.target.value) : "" } })}
|
|
451
|
+
disabled={field.readonly}
|
|
452
|
+
required={field.required}
|
|
453
|
+
className="w-full bg-surface-1 border-[1.5px] border-border-subtle rounded-[8px] px-2.5 py-2.5 text-[12px] text-foreground-1 outline-none focus:border-primary/40 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
|
|
454
|
+
<option value="">Select...</option>
|
|
455
|
+
{bankAccounts.map((b) => (<option key={b.id} value={b.id}>{b.bank_name} - {b.account_number}</option>))}
|
|
456
|
+
</select>
|
|
457
|
+
) : field.type === "material_type_select" ? (
|
|
458
|
+
<select value={entry.metadata[field.key] || ""}
|
|
459
|
+
onChange={(e) => onUpdate({ metadata: { ...entry.metadata, [field.key]: e.target.value ? Number(e.target.value) : "" } })}
|
|
460
|
+
disabled={field.readonly}
|
|
461
|
+
required={field.required}
|
|
462
|
+
className="w-full bg-surface-1 border-[1.5px] border-border-subtle rounded-[8px] px-2.5 py-2.5 text-[12px] text-foreground-1 outline-none focus:border-primary/40 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
|
|
463
|
+
<option value="">Select Material...</option>
|
|
464
|
+
{materialTypes.map((m) => (<option key={m.id} value={m.id}>{m.name}</option>))}
|
|
465
|
+
</select>
|
|
466
|
+
) : field.type === "select" ? (
|
|
467
|
+
<select value={entry.metadata[field.key] || ""}
|
|
468
|
+
onChange={(e) => onUpdate({ metadata: { ...entry.metadata, [field.key]: e.target.value } })}
|
|
469
|
+
disabled={field.readonly}
|
|
470
|
+
required={field.required}
|
|
471
|
+
className="w-full bg-surface-1 border-[1.5px] border-border-subtle rounded-[8px] px-2.5 py-2.5 text-[12px] text-foreground-1 outline-none focus:border-primary/40 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
|
|
472
|
+
<option value="">Select...</option>
|
|
473
|
+
{field.options?.map((o) => (<option key={o.value} value={o.value}>{o.label}</option>))}
|
|
474
|
+
</select>
|
|
475
|
+
) : field.type === "multi_select" as any ? (
|
|
498
476
|
<AsyncSearchableSelect
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
loadOptions={async (search, page) => {
|
|
516
|
-
if (!fetchChequeLeaves) return [];
|
|
517
|
-
const res = await fetchChequeLeaves(search, page);
|
|
518
|
-
return res.is_success && res.result ? res.result : [];
|
|
519
|
-
}}
|
|
520
|
-
option={{
|
|
521
|
-
label: "cheque_number",
|
|
522
|
-
value: "cheque_number",
|
|
523
|
-
renderOption: (item: any) => (
|
|
524
|
-
<div className="flex flex-col">
|
|
525
|
-
<span>{item.cheque_number}</span>
|
|
526
|
-
{item.cheque_book?.bank_account?.bank_name && (
|
|
527
|
-
<span className="text-[11px] text-foreground-subtle mt-0.5">
|
|
528
|
-
{item.cheque_book.bank_account.bank_name} {item.cheque_book.bank_account.branch ? `— ${item.cheque_book.bank_account.branch}` : ""}
|
|
529
|
-
</span>
|
|
530
|
-
)}
|
|
531
|
-
</div>
|
|
532
|
-
)
|
|
477
|
+
multiple
|
|
478
|
+
option={{ label: 'label', value: 'value' }}
|
|
479
|
+
defaultValue={
|
|
480
|
+
Array.isArray(entry.metadata[field.key])
|
|
481
|
+
? (field.options || []).filter(o => (entry.metadata[field.key] as any).includes(o.value))
|
|
482
|
+
: []
|
|
483
|
+
}
|
|
484
|
+
onChange={(val, selectedObjs: any) => {
|
|
485
|
+
const sum = Array.isArray(selectedObjs)
|
|
486
|
+
? selectedObjs.reduce((acc, curr) => acc + (curr.unapplied_amount || 0), 0)
|
|
487
|
+
: 0;
|
|
488
|
+
|
|
489
|
+
onUpdate({
|
|
490
|
+
amount: sum,
|
|
491
|
+
metadata: { ...entry.metadata, [field.key]: val as any }
|
|
492
|
+
});
|
|
533
493
|
}}
|
|
534
|
-
placeholder="Search cheque number..."
|
|
535
494
|
disabled={field.readonly}
|
|
536
495
|
required={field.required}
|
|
496
|
+
placeholder="Select..."
|
|
497
|
+
loadOptions={async () => field.options || []}
|
|
537
498
|
/>
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
{entry.metadata.
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
499
|
+
) : field.key === "cheque_number" && fetchChequeLeaves ? (
|
|
500
|
+
<>
|
|
501
|
+
<AsyncSearchableSelect
|
|
502
|
+
defaultValue={entry.metadata[field.key] ? { cheque_number: entry.metadata[field.key] } : undefined}
|
|
503
|
+
onChange={(val, selected: any) => {
|
|
504
|
+
const leafObj = Array.isArray(selected) ? selected[0] : selected;
|
|
505
|
+
const newMetadata = { ...entry.metadata, [field.key]: val, cheque_leaf_id: leafObj?.id || "" };
|
|
506
|
+
if (leafObj?.cheque_book?.bank_account) {
|
|
507
|
+
const ba = leafObj.cheque_book.bank_account;
|
|
508
|
+
newMetadata.bank_account_id = ba.id;
|
|
509
|
+
newMetadata._display_bank_name = ba.bank_name;
|
|
510
|
+
newMetadata._display_branch = ba.branch;
|
|
511
|
+
} else {
|
|
512
|
+
delete newMetadata.bank_account_id;
|
|
513
|
+
delete newMetadata._display_bank_name;
|
|
514
|
+
delete newMetadata._display_branch;
|
|
515
|
+
}
|
|
516
|
+
onUpdate({ metadata: newMetadata });
|
|
517
|
+
}}
|
|
518
|
+
loadOptions={async (search, page) => {
|
|
519
|
+
if (!fetchChequeLeaves) return [];
|
|
520
|
+
const res = await fetchChequeLeaves(search, page);
|
|
521
|
+
return res.is_success && res.result ? res.result : [];
|
|
522
|
+
}}
|
|
523
|
+
option={{
|
|
524
|
+
label: "cheque_number",
|
|
525
|
+
value: "cheque_number",
|
|
526
|
+
renderOption: (item: any) => (
|
|
527
|
+
<div className="flex flex-col">
|
|
528
|
+
<span>{item.cheque_number}</span>
|
|
529
|
+
{item.cheque_book?.bank_account?.bank_name && (
|
|
530
|
+
<span className="text-[11px] text-foreground-subtle mt-0.5">
|
|
531
|
+
{item.cheque_book.bank_account.bank_name} {item.cheque_book.bank_account.branch ? `— ${item.cheque_book.bank_account.branch}` : ""}
|
|
532
|
+
</span>
|
|
533
|
+
)}
|
|
534
|
+
</div>
|
|
535
|
+
)
|
|
536
|
+
}}
|
|
537
|
+
placeholder="Search cheque number..."
|
|
538
|
+
disabled={field.readonly}
|
|
539
|
+
required={field.required}
|
|
540
|
+
/>
|
|
541
|
+
{entry.metadata._display_bank_name && (
|
|
542
|
+
<div className="text-[11px] text-foreground-subtle mt-1 flex items-center gap-1.5 font-medium px-1">
|
|
543
|
+
<Building size={12} className="text-foreground-disabled" />
|
|
544
|
+
{entry.metadata._display_bank_name} {entry.metadata._display_branch ? `— ${entry.metadata._display_branch}` : ""}
|
|
545
|
+
</div>
|
|
546
|
+
)}
|
|
547
|
+
</>
|
|
548
|
+
) : (
|
|
549
|
+
<input type={field.type === "number" ? "number" : field.type === "date" ? "date" : "text"}
|
|
550
|
+
{...(field.type === "number" ? { step: "any" } : {})}
|
|
551
|
+
value={entry.metadata[field.key] || ""}
|
|
552
|
+
onChange={(e) => onUpdate({ metadata: { ...entry.metadata, [field.key]: e.target.value } })}
|
|
553
|
+
readOnly={field.readonly}
|
|
554
|
+
required={field.required}
|
|
555
|
+
className={cn("w-full bg-surface-1 border-[1.5px] border-border-subtle rounded-[8px] px-2.5 py-2.5 text-[12px] text-foreground-1 outline-none focus:border-primary/40 transition-all [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none", field.readonly && "opacity-60 bg-surface-0 cursor-not-allowed border-border")} />
|
|
556
|
+
)}
|
|
557
|
+
</div>
|
|
558
|
+
)
|
|
559
|
+
})}
|
|
556
560
|
</div>
|
|
557
561
|
)}
|
|
558
562
|
</div>
|