@apptimate/ui 6.2.0 → 6.4.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/Modal.tsx +1 -1
- package/src/base-components/SearchableSelect.tsx +2 -0
- package/src/base-components/Table.tsx +1 -1
- package/src/common-components/DashboardLayout.tsx +107 -79
- package/src/common-components/PaymentSection.tsx +6 -0
- package/src/common-components/pickers/modals/SerialSelectionModal.tsx +20 -10
- package/src/common-components/transaction/MetadataPanel.tsx +2 -2
- package/src/common-components/transaction/ProductSelectionPanel.tsx +520 -515
- package/src/common-components/transaction/ProductTransactionScreen.tsx +50 -7
- package/src/common-components/transaction/types.ts +2 -0
package/package.json
CHANGED
|
@@ -102,7 +102,7 @@ export const Modal = ({ isOpen, onClose, title, children, className, footer, bac
|
|
|
102
102
|
{/* Body */}
|
|
103
103
|
<div className={cn(
|
|
104
104
|
"flex-1",
|
|
105
|
-
size === 'full' ? "overflow-hidden p-0" : "overflow-
|
|
105
|
+
size === 'full' ? "overflow-hidden p-0" : "overflow-auto px-5 sm:px-8 py-5 sm:py-7"
|
|
106
106
|
)}>
|
|
107
107
|
{children}
|
|
108
108
|
</div>
|
|
@@ -57,6 +57,8 @@ export interface SearchableSelectProps<T = Record<string, unknown>> {
|
|
|
57
57
|
allowClear?: boolean;
|
|
58
58
|
/** Disabled state */
|
|
59
59
|
disabled?: boolean;
|
|
60
|
+
/** Required state */
|
|
61
|
+
required?: boolean;
|
|
60
62
|
/** Label text above the select */
|
|
61
63
|
label?: string;
|
|
62
64
|
/** Error message below the select */
|
|
@@ -10,7 +10,7 @@ export interface TableProps {
|
|
|
10
10
|
|
|
11
11
|
export const Table = ({ children, className }: TableProps) => {
|
|
12
12
|
return (
|
|
13
|
-
<div className={cn("w-full", className)}>
|
|
13
|
+
<div className={cn("w-full overflow-x-auto", className)}>
|
|
14
14
|
<table className="w-full text-left lg:min-w-[600px] border-separate border-spacing-0 block lg:table">
|
|
15
15
|
{children}
|
|
16
16
|
</table>
|
|
@@ -6,7 +6,7 @@ import { Dropdown, DropdownItem } from '../base-components/Dropdown';
|
|
|
6
6
|
|
|
7
7
|
import Link from 'next/link';
|
|
8
8
|
import { usePathname, useRouter } from 'next/navigation';
|
|
9
|
-
import { cn } from '@apptimate/core-lib';
|
|
9
|
+
import { cn, getProjectConstructionGroups } from '@apptimate/core-lib';
|
|
10
10
|
|
|
11
11
|
export type DashboardMenuConfig = {
|
|
12
12
|
id: string;
|
|
@@ -75,7 +75,7 @@ export function DashboardLayout({
|
|
|
75
75
|
// Helper to build hierarchy
|
|
76
76
|
const flattenedOrganizations = React.useMemo(() => {
|
|
77
77
|
if (!organizations || organizations.length === 0) return [];
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
const orgMap = new Map<number, any>();
|
|
80
80
|
const roots: any[] = [];
|
|
81
81
|
|
|
@@ -103,14 +103,42 @@ export function DashboardLayout({
|
|
|
103
103
|
return flattened;
|
|
104
104
|
}, [organizations]);
|
|
105
105
|
|
|
106
|
+
|
|
107
|
+
// Override menus if it's a project organization
|
|
108
|
+
const effectiveMenus = React.useMemo(() => {
|
|
109
|
+
if (selectedOrganization?.organization_type !== 'project') return menus;
|
|
110
|
+
|
|
111
|
+
const projectMatch = pathname.match(/^(?:\/construction)?\/projects\/(\d+)(?:\/|$)/);
|
|
112
|
+
const projectId = projectMatch ? projectMatch[1] : 'current';
|
|
113
|
+
|
|
114
|
+
return menus.map(menu => {
|
|
115
|
+
if (menu.id === 'construction') {
|
|
116
|
+
return {
|
|
117
|
+
...menu,
|
|
118
|
+
groups: getProjectConstructionGroups(projectId)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return menu;
|
|
122
|
+
});
|
|
123
|
+
}, [menus, selectedOrganization, pathname]);
|
|
124
|
+
|
|
106
125
|
// Find which main menu should be active based on current path.
|
|
107
126
|
// When basePath is set, prefer the menu whose items are mostly within basePath.
|
|
108
127
|
const fullPath = basePath + (pathname === "/" && basePath ? "" : pathname);
|
|
109
128
|
const currentMainMenu = (() => {
|
|
110
|
-
let bestMenu =
|
|
129
|
+
let bestMenu = effectiveMenus[0];
|
|
130
|
+
|
|
131
|
+
// Default to construction if organization is a project
|
|
132
|
+
if (selectedOrganization?.organization_type === 'project') {
|
|
133
|
+
const constructionMenu = effectiveMenus.find(m => m.id === 'construction');
|
|
134
|
+
if (constructionMenu) {
|
|
135
|
+
bestMenu = constructionMenu;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
111
139
|
let bestScore = -1;
|
|
112
140
|
let bestInternalCount = -1;
|
|
113
|
-
for (const menu of
|
|
141
|
+
for (const menu of effectiveMenus) {
|
|
114
142
|
for (const group of menu.groups) {
|
|
115
143
|
for (const item of group.items) {
|
|
116
144
|
if (fullPath === item.path || fullPath.startsWith(item.path + "/")) {
|
|
@@ -138,7 +166,7 @@ export function DashboardLayout({
|
|
|
138
166
|
}
|
|
139
167
|
}, [currentMainMenu?.id]);
|
|
140
168
|
|
|
141
|
-
const activeMenu =
|
|
169
|
+
const activeMenu = effectiveMenus.find(m => m.id === activeMenuId) || effectiveMenus[0];
|
|
142
170
|
|
|
143
171
|
const handleMainMenuSelect = (menu: DashboardMenuConfig) => {
|
|
144
172
|
setActiveMenuId(menu.id);
|
|
@@ -218,7 +246,7 @@ export function DashboardLayout({
|
|
|
218
246
|
}
|
|
219
247
|
`}} />
|
|
220
248
|
<nav className="flex flex-col gap-6 flex-1 w-full sidebar-scroll pb-4">
|
|
221
|
-
{
|
|
249
|
+
{effectiveMenus.map((menu) => {
|
|
222
250
|
const isActive = activeMenuId === menu.id;
|
|
223
251
|
return (
|
|
224
252
|
<div
|
|
@@ -300,79 +328,79 @@ export function DashboardLayout({
|
|
|
300
328
|
<PanelLeftClose size={18} />
|
|
301
329
|
</button>
|
|
302
330
|
</div>
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
</div>
|
|
348
|
-
))}
|
|
349
|
-
</div>
|
|
350
|
-
|
|
351
|
-
{/* Organization Selector - Bottom of Secondary Sidebar */}
|
|
352
|
-
{organizations.length > 0 && (
|
|
353
|
-
<div className="px-4 pt-4 mt-2 border-t border-gray-100">
|
|
354
|
-
<button
|
|
355
|
-
id="org-selector-desktop"
|
|
356
|
-
onClick={() => setIsOrgModalOpen(true)}
|
|
357
|
-
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl bg-[#F4F5F7] hover:bg-gray-200/70 transition-all duration-200 group cursor-pointer"
|
|
358
|
-
>
|
|
359
|
-
<div className={cn(
|
|
360
|
-
"w-8 h-8 rounded-lg flex items-center justify-center shrink-0 shadow-sm",
|
|
361
|
-
selectedOrganization?.organization_type === 'project'
|
|
362
|
-
? "bg-gradient-to-br from-teal-400 to-teal-600"
|
|
363
|
-
: "bg-gradient-to-br from-indigo-500 to-purple-600"
|
|
364
|
-
)}>
|
|
365
|
-
<Building2 size={14} className="text-white" />
|
|
331
|
+
<div className="flex-1 overflow-y-auto px-4 space-y-6">
|
|
332
|
+
{activeMenu.groups.map((group) => (
|
|
333
|
+
<div key={group.id}>
|
|
334
|
+
{group.label && (
|
|
335
|
+
<h3 className="px-2 text-xs font-bold text-gray-400 uppercase tracking-wider mb-2 empty:hidden">{group.label}</h3>
|
|
336
|
+
)}
|
|
337
|
+
<nav className="flex flex-col gap-1">
|
|
338
|
+
{group.items.map((item) => {
|
|
339
|
+
// Find the longest matching path in this group to avoid parent paths being active
|
|
340
|
+
const allGroupItems = activeMenu.groups.flatMap(g => g.items);
|
|
341
|
+
const matchingItems = allGroupItems.filter(i => fullPath === i.path || fullPath.startsWith(`${i.path}/`));
|
|
342
|
+
const longestMatch = matchingItems.sort((a, b) => b.path.length - a.path.length)[0];
|
|
343
|
+
const isItemActive = longestMatch?.path === item.path;
|
|
344
|
+
const isInternal = basePath
|
|
345
|
+
? item.path.startsWith(basePath)
|
|
346
|
+
: !externalPaths.some(ext => item.path.startsWith(ext));
|
|
347
|
+
const href = basePath && isInternal ? item.path.replace(basePath, "") || "/" : item.path;
|
|
348
|
+
|
|
349
|
+
const className = `px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${isItemActive
|
|
350
|
+
? "bg-[#F4F5F7] text-[#2D3142] font-semibold"
|
|
351
|
+
: "text-gray-500 font-medium hover:bg-gray-50"
|
|
352
|
+
}`;
|
|
353
|
+
|
|
354
|
+
const content = (
|
|
355
|
+
<>
|
|
356
|
+
<div className="flex items-center gap-3">
|
|
357
|
+
{item.icon && <span className={`${isItemActive ? 'text-[#2D3142]' : 'text-gray-400'}`}>{item.icon}</span>}
|
|
358
|
+
<span>{item.label}</span>
|
|
359
|
+
</div>
|
|
360
|
+
{item.badge && <div>{item.badge}</div>}
|
|
361
|
+
</>
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
return isInternal ? (
|
|
365
|
+
<Link key={item.id} href={href} className={className}>
|
|
366
|
+
{content}
|
|
367
|
+
</Link>
|
|
368
|
+
) : (
|
|
369
|
+
<a key={item.id} href={href} className={className}>
|
|
370
|
+
{content}
|
|
371
|
+
</a>
|
|
372
|
+
);
|
|
373
|
+
})}
|
|
374
|
+
</nav>
|
|
366
375
|
</div>
|
|
367
|
-
|
|
368
|
-
<p className="text-[11px] font-semibold text-gray-400 uppercase tracking-wider leading-none mb-0.5">Organization</p>
|
|
369
|
-
<p className="text-[13px] font-bold text-[#2D3142] truncate leading-tight">{orgDisplayName}</p>
|
|
370
|
-
</div>
|
|
371
|
-
<ChevronRight size={14} className="text-gray-400 group-hover:text-[#2D3142] transition-colors shrink-0" />
|
|
372
|
-
</button>
|
|
376
|
+
))}
|
|
373
377
|
</div>
|
|
374
|
-
|
|
375
|
-
|
|
378
|
+
|
|
379
|
+
{/* Organization Selector - Bottom of Secondary Sidebar */}
|
|
380
|
+
{organizations.length > 0 && (
|
|
381
|
+
<div className="px-4 pt-4 mt-2 border-t border-gray-100">
|
|
382
|
+
<button
|
|
383
|
+
id="org-selector-desktop"
|
|
384
|
+
onClick={() => setIsOrgModalOpen(true)}
|
|
385
|
+
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl bg-[#F4F5F7] hover:bg-gray-200/70 transition-all duration-200 group cursor-pointer"
|
|
386
|
+
>
|
|
387
|
+
<div className={cn(
|
|
388
|
+
"w-8 h-8 rounded-lg flex items-center justify-center shrink-0 shadow-sm",
|
|
389
|
+
selectedOrganization?.organization_type === 'project'
|
|
390
|
+
? "bg-gradient-to-br from-teal-400 to-teal-600"
|
|
391
|
+
: "bg-gradient-to-br from-indigo-500 to-purple-600"
|
|
392
|
+
)}>
|
|
393
|
+
<Building2 size={14} className="text-white" />
|
|
394
|
+
</div>
|
|
395
|
+
<div className="flex-1 min-w-0 text-left">
|
|
396
|
+
<p className="text-[11px] font-semibold text-gray-400 uppercase tracking-wider leading-none mb-0.5">Organization</p>
|
|
397
|
+
<p className="text-[13px] font-bold text-[#2D3142] truncate leading-tight">{orgDisplayName}</p>
|
|
398
|
+
</div>
|
|
399
|
+
<ChevronRight size={14} className="text-gray-400 group-hover:text-[#2D3142] transition-colors shrink-0" />
|
|
400
|
+
</button>
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
</aside>
|
|
376
404
|
|
|
377
405
|
</>
|
|
378
406
|
)}
|
|
@@ -451,7 +479,7 @@ export function DashboardLayout({
|
|
|
451
479
|
|
|
452
480
|
<div className="flex-1 overflow-y-auto py-6 px-4">
|
|
453
481
|
<nav className="flex flex-col gap-8">
|
|
454
|
-
{
|
|
482
|
+
{effectiveMenus.map((menu) => (
|
|
455
483
|
<div key={menu.id}>
|
|
456
484
|
<div className="flex items-center gap-3 text-[#2D3142] font-bold mb-3 px-2">
|
|
457
485
|
{React.cloneElement(menu.icon as React.ReactElement<any>, {
|
|
@@ -567,7 +595,7 @@ export function DashboardLayout({
|
|
|
567
595
|
const isSelected = selectedOrganization?.id === org.id;
|
|
568
596
|
const pl = org._level > 0 ? org._level * 24 : 0;
|
|
569
597
|
const isProject = org.organization_type === 'project';
|
|
570
|
-
|
|
598
|
+
|
|
571
599
|
return (
|
|
572
600
|
<button
|
|
573
601
|
key={org.id}
|
|
@@ -405,6 +405,7 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
|
|
|
405
405
|
<select value={entry.metadata[field.key] || ""}
|
|
406
406
|
onChange={(e) => onUpdate({ metadata: { ...entry.metadata, [field.key]: e.target.value ? Number(e.target.value) : "" } })}
|
|
407
407
|
disabled={field.readonly}
|
|
408
|
+
required={field.required}
|
|
408
409
|
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">
|
|
409
410
|
<option value="">Select...</option>
|
|
410
411
|
{bankAccounts.map((b) => (<option key={b.id} value={b.id}>{b.bank_name} - {b.account_number}</option>))}
|
|
@@ -413,6 +414,7 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
|
|
|
413
414
|
<select value={entry.metadata[field.key] || ""}
|
|
414
415
|
onChange={(e) => onUpdate({ metadata: { ...entry.metadata, [field.key]: e.target.value ? Number(e.target.value) : "" } })}
|
|
415
416
|
disabled={field.readonly}
|
|
417
|
+
required={field.required}
|
|
416
418
|
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">
|
|
417
419
|
<option value="">Select Material...</option>
|
|
418
420
|
{materialTypes.map((m) => (<option key={m.id} value={m.id}>{m.name}</option>))}
|
|
@@ -421,6 +423,7 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
|
|
|
421
423
|
<select value={entry.metadata[field.key] || ""}
|
|
422
424
|
onChange={(e) => onUpdate({ metadata: { ...entry.metadata, [field.key]: e.target.value } })}
|
|
423
425
|
disabled={field.readonly}
|
|
426
|
+
required={field.required}
|
|
424
427
|
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">
|
|
425
428
|
<option value="">Select...</option>
|
|
426
429
|
{field.options?.map((o) => (<option key={o.value} value={o.value}>{o.label}</option>))}
|
|
@@ -445,6 +448,7 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
|
|
|
445
448
|
});
|
|
446
449
|
}}
|
|
447
450
|
disabled={field.readonly}
|
|
451
|
+
required={field.required}
|
|
448
452
|
placeholder="Select..."
|
|
449
453
|
loadOptions={async () => field.options || []}
|
|
450
454
|
/>
|
|
@@ -488,6 +492,7 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
|
|
|
488
492
|
}}
|
|
489
493
|
placeholder="Search cheque number..."
|
|
490
494
|
disabled={field.readonly}
|
|
495
|
+
required={field.required}
|
|
491
496
|
/>
|
|
492
497
|
{entry.metadata._display_bank_name && (
|
|
493
498
|
<div className="text-[11px] text-foreground-subtle mt-1 flex items-center gap-1.5 font-medium px-1">
|
|
@@ -502,6 +507,7 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
|
|
|
502
507
|
value={entry.metadata[field.key] || ""}
|
|
503
508
|
onChange={(e) => onUpdate({ metadata: { ...entry.metadata, [field.key]: e.target.value } })}
|
|
504
509
|
readOnly={field.readonly}
|
|
510
|
+
required={field.required}
|
|
505
511
|
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")} />
|
|
506
512
|
)}
|
|
507
513
|
</div>
|
|
@@ -13,6 +13,7 @@ interface SerialSelectionModalProps {
|
|
|
13
13
|
variant: TransactionItemVariant | null;
|
|
14
14
|
allowCreate?: boolean;
|
|
15
15
|
initialSerials?: string[];
|
|
16
|
+
initialSerialObjects?: {id: number, serial_number: string}[];
|
|
16
17
|
warehouseId?: number;
|
|
17
18
|
onConfirm: (serials: string[], serialObjects?: {id: number, serial_number: string}[]) => void;
|
|
18
19
|
}
|
|
@@ -24,10 +25,12 @@ export function SerialSelectionModal({
|
|
|
24
25
|
variant,
|
|
25
26
|
allowCreate = false,
|
|
26
27
|
initialSerials = [],
|
|
28
|
+
initialSerialObjects = [],
|
|
27
29
|
warehouseId,
|
|
28
30
|
onConfirm
|
|
29
31
|
}: SerialSelectionModalProps) {
|
|
30
32
|
const [serials, setSerials] = useState<string[]>([]);
|
|
33
|
+
const [selectedSerialObjects, setSelectedSerialObjects] = useState<{id: number, serial_number: string}[]>([]);
|
|
31
34
|
const [inputValue, setInputValue] = useState("");
|
|
32
35
|
const [availableSerials, setAvailableSerials] = useState<any[]>([]);
|
|
33
36
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -35,6 +38,7 @@ export function SerialSelectionModal({
|
|
|
35
38
|
useEffect(() => {
|
|
36
39
|
if (isOpen) {
|
|
37
40
|
setSerials(initialSerials);
|
|
41
|
+
setSelectedSerialObjects(initialSerialObjects);
|
|
38
42
|
setInputValue("");
|
|
39
43
|
}
|
|
40
44
|
}, [isOpen, item, allowCreate]);
|
|
@@ -81,6 +85,7 @@ export function SerialSelectionModal({
|
|
|
81
85
|
|
|
82
86
|
const handleAddSerial = (e?: React.FormEvent) => {
|
|
83
87
|
e?.preventDefault();
|
|
88
|
+
e?.stopPropagation();
|
|
84
89
|
// Allow comma or space separated parsing
|
|
85
90
|
const rawTokens = inputValue.split(/[\s,]+/);
|
|
86
91
|
let addedCount = 0;
|
|
@@ -106,11 +111,13 @@ export function SerialSelectionModal({
|
|
|
106
111
|
setSerials(serials.filter(v => v !== s));
|
|
107
112
|
};
|
|
108
113
|
|
|
109
|
-
const toggleSelectSerial = (s:
|
|
110
|
-
if (serials.includes(s)) {
|
|
111
|
-
setSerials(serials.filter(v => v !== s));
|
|
114
|
+
const toggleSelectSerial = (s: any) => {
|
|
115
|
+
if (serials.includes(s.serial_number)) {
|
|
116
|
+
setSerials(serials.filter(v => v !== s.serial_number));
|
|
117
|
+
setSelectedSerialObjects(prev => prev.filter(v => v.serial_number !== s.serial_number));
|
|
112
118
|
} else {
|
|
113
|
-
setSerials([...serials, s]);
|
|
119
|
+
setSerials([...serials, s.serial_number]);
|
|
120
|
+
setSelectedSerialObjects(prev => [...prev, { id: s.id, serial_number: s.serial_number }]);
|
|
114
121
|
}
|
|
115
122
|
};
|
|
116
123
|
|
|
@@ -121,10 +128,9 @@ export function SerialSelectionModal({
|
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
if (!allowCreate) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
onConfirm(serials, serialObjects);
|
|
131
|
+
// Ensure we only pass objects that are currently in the selected serials array
|
|
132
|
+
const finalObjects = selectedSerialObjects.filter(obj => serials.includes(obj.serial_number));
|
|
133
|
+
onConfirm(serials, finalObjects);
|
|
128
134
|
} else {
|
|
129
135
|
onConfirm(serials);
|
|
130
136
|
}
|
|
@@ -172,7 +178,11 @@ export function SerialSelectionModal({
|
|
|
172
178
|
|
|
173
179
|
<div className="flex flex-col border border-border-subtle rounded-xl overflow-hidden bg-surface-0 min-h-[250px] shadow-sm">
|
|
174
180
|
<div className="p-3 border-b border-border-subtle bg-surface-hover">
|
|
175
|
-
<form
|
|
181
|
+
<form
|
|
182
|
+
onSubmit={handleAddSerial}
|
|
183
|
+
onKeyDown={(e) => e.key === 'Enter' && e.stopPropagation()}
|
|
184
|
+
className="flex gap-2"
|
|
185
|
+
>
|
|
176
186
|
<input
|
|
177
187
|
type="text"
|
|
178
188
|
autoFocus
|
|
@@ -233,7 +243,7 @@ export function SerialSelectionModal({
|
|
|
233
243
|
type="checkbox"
|
|
234
244
|
className="w-4 h-4 rounded border-border-subtle text-primary focus:ring-primary"
|
|
235
245
|
checked={serials.includes(s.serial_number)}
|
|
236
|
-
onChange={() => toggleSelectSerial(s
|
|
246
|
+
onChange={() => toggleSelectSerial(s)}
|
|
237
247
|
/>
|
|
238
248
|
<span className="font-mono text-[13px] text-foreground-1">{s.serial_number}</span>
|
|
239
249
|
</label>
|
|
@@ -155,7 +155,7 @@ export function MetadataPanel({
|
|
|
155
155
|
onChange={(p) => onPartyChange(p?.id ?? null, p ? { id: p.id, name: p.name, code: p.code, type: p.type } : undefined)}
|
|
156
156
|
label={config.partyLabel}
|
|
157
157
|
partyType={config.partyType}
|
|
158
|
-
isRequired
|
|
158
|
+
isRequired={config.type !== 'pos'}
|
|
159
159
|
disabled={config.disablePartySelection}
|
|
160
160
|
/>
|
|
161
161
|
)
|
|
@@ -248,8 +248,8 @@ export function MetadataPanel({
|
|
|
248
248
|
|
|
249
249
|
{/* ── Submit Button ── */}
|
|
250
250
|
<Button
|
|
251
|
+
type="submit"
|
|
251
252
|
color="primary"
|
|
252
|
-
onClick={onSubmit}
|
|
253
253
|
isDisabled={lineItems.length === 0}
|
|
254
254
|
isLoading={isSubmitting}
|
|
255
255
|
className="!w-full !py-4 !text-[14px] !font-semibold !rounded-[10px]"
|