@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.
- 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 +200 -155
- 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/MetadataPanel.tsx +26 -17
- package/src/common-components/transaction/ProductSelectionPanel.tsx +7 -5
- package/src/common-components/transaction/ProductTransactionScreen.tsx +35 -5
- package/src/common-components/transaction/types.ts +4 -0
- package/src/components/shared/ImageUploadComponent.tsx +3 -0
- package/src/finance-components/DirectTransactionDetailModal.tsx +153 -0
- package/src/index.tsx +3 -0
|
@@ -56,6 +56,8 @@ export interface PaymentSectionProps {
|
|
|
56
56
|
onPaymentsChange: (payments: PaymentEntry[]) => void;
|
|
57
57
|
/** Optional: show compact variant */
|
|
58
58
|
compact?: boolean;
|
|
59
|
+
/** When true, initially show only Cash/Card with a 'More Options' toggle for other modes (used in POS) */
|
|
60
|
+
collapseModes?: boolean;
|
|
59
61
|
/** Fetcher for payment modes */
|
|
60
62
|
fetchPaymentModes: () => Promise<{ is_success: boolean; result?: any }>;
|
|
61
63
|
/** Fetcher for bank accounts */
|
|
@@ -72,11 +74,12 @@ export interface PaymentSectionProps {
|
|
|
72
74
|
|
|
73
75
|
/* ── Component ── */
|
|
74
76
|
|
|
75
|
-
export function PaymentSection({ totalAmount, payments, onPaymentsChange, compact, fetchPaymentModes, fetchBankAccounts, materialTypes = [], allowedSpecialModes = [], fetchChequeLeaves, variant = "default" }: PaymentSectionProps) {
|
|
77
|
+
export function PaymentSection({ totalAmount, payments, onPaymentsChange, compact, collapseModes, fetchPaymentModes, fetchBankAccounts, materialTypes = [], allowedSpecialModes = [], fetchChequeLeaves, variant = "default" }: PaymentSectionProps) {
|
|
76
78
|
const [paymentModes, setPaymentModes] = useState<PaymentMode[]>([]);
|
|
77
79
|
const [bankAccounts, setBankAccounts] = useState<BankAccount[]>([]);
|
|
78
80
|
const [isLoading, setIsLoading] = useState(false);
|
|
79
81
|
const [selectionMode, setSelectionMode] = useState<"single" | "split">("single");
|
|
82
|
+
const [showMoreModes, setShowMoreModes] = useState(false);
|
|
80
83
|
const paymentsRef = useRef(payments);
|
|
81
84
|
|
|
82
85
|
// Keep ref in sync
|
|
@@ -180,11 +183,14 @@ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compac
|
|
|
180
183
|
|
|
181
184
|
// ── Auto-sync single mode amount when totalAmount changes ──
|
|
182
185
|
useEffect(() => {
|
|
183
|
-
if (selectionMode === "single" &&
|
|
184
|
-
|
|
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
|
+
}
|
|
185
191
|
}
|
|
186
192
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
187
|
-
}, [totalAmount, selectionMode]); //
|
|
193
|
+
}, [totalAmount, selectionMode, payments.length]); // payments.length triggers sync after initial cash auto-selection
|
|
188
194
|
|
|
189
195
|
const activateSplitMode = useCallback(() => {
|
|
190
196
|
setSelectionMode("split");
|
|
@@ -214,41 +220,79 @@ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compac
|
|
|
214
220
|
return (
|
|
215
221
|
<div className="space-y-3">
|
|
216
222
|
{/* ── Mode Selection Buttons ── */}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
{(() => {
|
|
224
|
+
const activeModes = paymentModes.filter((m) => m.is_active);
|
|
225
|
+
const primaryModes = activeModes.filter((m) => ["cash", "card"].includes(m.code));
|
|
226
|
+
const secondaryModes = activeModes.filter((m) => !["cash", "card"].includes(m.code));
|
|
227
|
+
// If a secondary mode is currently selected, auto-expand
|
|
228
|
+
const isSecondaryActive = selectionMode === "single" && payments.length === 1 && secondaryModes.some(m => m.id === payments[0].mode_id);
|
|
229
|
+
const shouldShowMore = !collapseModes || showMoreModes || isSecondaryActive || selectionMode === "split";
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div className="flex flex-wrap gap-2">
|
|
233
|
+
{primaryModes.map((mode) => {
|
|
234
|
+
const isActive = selectionMode === "single" && payments.length === 1 && payments[0].mode_id === mode.id;
|
|
235
|
+
return (
|
|
236
|
+
<button key={mode.id} type="button" onClick={() => selectSingleMode(mode)}
|
|
237
|
+
className={cn(
|
|
238
|
+
"inline-flex items-center gap-2 px-4 py-2.5 text-[12px] font-semibold rounded-[10px] border transition-all",
|
|
239
|
+
isActive
|
|
240
|
+
? "bg-primary text-primary-foreground border-primary shadow-sm"
|
|
241
|
+
: "bg-surface-0 border-border-subtle text-foreground-1 hover:border-primary/40 hover:bg-primary/5 hover:text-primary"
|
|
242
|
+
)}>
|
|
243
|
+
{getModeIcon(mode.code)}
|
|
244
|
+
{mode.name}
|
|
245
|
+
</button>
|
|
246
|
+
);
|
|
247
|
+
})}
|
|
248
|
+
|
|
249
|
+
{/* More Options toggle */}
|
|
250
|
+
{secondaryModes.length > 0 && !shouldShowMore && (
|
|
251
|
+
<button type="button" onClick={() => setShowMoreModes(true)}
|
|
252
|
+
className="inline-flex items-center gap-1.5 px-3.5 py-2.5 text-[12px] font-semibold rounded-[10px] border border-dashed border-border-subtle text-foreground-subtle hover:border-primary/40 hover:text-primary hover:bg-primary/5 transition-all">
|
|
253
|
+
<span className="text-[14px]">···</span>
|
|
254
|
+
More Options
|
|
255
|
+
</button>
|
|
256
|
+
)}
|
|
257
|
+
|
|
258
|
+
{/* Secondary modes (shown when expanded) */}
|
|
259
|
+
{shouldShowMore && secondaryModes.map((mode) => {
|
|
260
|
+
const isActive = selectionMode === "single" && payments.length === 1 && payments[0].mode_id === mode.id;
|
|
261
|
+
return (
|
|
262
|
+
<button key={mode.id} type="button" onClick={() => selectSingleMode(mode)}
|
|
263
|
+
className={cn(
|
|
264
|
+
"inline-flex items-center gap-2 px-4 py-2.5 text-[12px] font-semibold rounded-[10px] border transition-all",
|
|
265
|
+
isActive
|
|
266
|
+
? "bg-primary text-primary-foreground border-primary shadow-sm"
|
|
267
|
+
: "bg-surface-0 border-border-subtle text-foreground-1 hover:border-primary/40 hover:bg-primary/5 hover:text-primary"
|
|
268
|
+
)}>
|
|
269
|
+
{getModeIcon(mode.code)}
|
|
270
|
+
{mode.name}
|
|
271
|
+
</button>
|
|
272
|
+
);
|
|
273
|
+
})}
|
|
274
|
+
|
|
275
|
+
{/* Split Button (shown when expanded) */}
|
|
276
|
+
{shouldShowMore && (
|
|
277
|
+
<button type="button" onClick={activateSplitMode}
|
|
224
278
|
className={cn(
|
|
225
279
|
"inline-flex items-center gap-2 px-4 py-2.5 text-[12px] font-semibold rounded-[10px] border transition-all",
|
|
226
|
-
|
|
280
|
+
selectionMode === "split"
|
|
227
281
|
? "bg-primary text-primary-foreground border-primary shadow-sm"
|
|
228
282
|
: "bg-surface-0 border-border-subtle text-foreground-1 hover:border-primary/40 hover:bg-primary/5 hover:text-primary"
|
|
229
283
|
)}>
|
|
230
|
-
{
|
|
231
|
-
|
|
284
|
+
<Split size={14} />
|
|
285
|
+
Multiple Methods
|
|
232
286
|
</button>
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
className={cn(
|
|
238
|
-
"inline-flex items-center gap-2 px-4 py-2.5 text-[12px] font-semibold rounded-[10px] border transition-all",
|
|
239
|
-
selectionMode === "split"
|
|
240
|
-
? "bg-primary text-primary-foreground border-primary shadow-sm"
|
|
241
|
-
: "bg-surface-0 border-border-subtle text-foreground-1 hover:border-primary/40 hover:bg-primary/5 hover:text-primary"
|
|
242
|
-
)}>
|
|
243
|
-
<Split size={14} />
|
|
244
|
-
Multiple Methods
|
|
245
|
-
</button>
|
|
246
|
-
</div>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
})()}
|
|
247
291
|
|
|
248
292
|
{/* ── Single Mode: Entry with fields (no delete) ── */}
|
|
249
293
|
{selectionMode === "single" && payments.length === 1 && (
|
|
250
294
|
<PaymentEntryRow entry={payments[0]} mode={paymentModes.find((m) => m.id === payments[0].mode_id)}
|
|
251
|
-
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} />
|
|
252
296
|
)}
|
|
253
297
|
|
|
254
298
|
{/* ── Split Mode: Multi-entry UI ── */}
|
|
@@ -381,137 +425,138 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
|
|
|
381
425
|
}
|
|
382
426
|
|
|
383
427
|
return (
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
<AsyncSearchableSelect
|
|
433
|
-
multiple
|
|
434
|
-
option={{ label: 'label', value: 'value' }}
|
|
435
|
-
defaultValue={
|
|
436
|
-
Array.isArray(entry.metadata[field.key])
|
|
437
|
-
? (field.options || []).filter(o => (entry.metadata[field.key] as any).includes(o.value))
|
|
438
|
-
: []
|
|
439
|
-
}
|
|
440
|
-
onChange={(val, selectedObjs: any) => {
|
|
441
|
-
const sum = Array.isArray(selectedObjs)
|
|
442
|
-
? selectedObjs.reduce((acc, curr) => acc + (curr.unapplied_amount || 0), 0)
|
|
443
|
-
: 0;
|
|
444
|
-
|
|
445
|
-
onUpdate({
|
|
446
|
-
amount: sum,
|
|
447
|
-
metadata: { ...entry.metadata, [field.key]: val as any }
|
|
448
|
-
});
|
|
449
|
-
}}
|
|
450
|
-
disabled={field.readonly}
|
|
451
|
-
required={field.required}
|
|
452
|
-
placeholder="Select..."
|
|
453
|
-
loadOptions={async () => field.options || []}
|
|
454
|
-
/>
|
|
455
|
-
) : field.key === "cheque_number" && fetchChequeLeaves ? (
|
|
456
|
-
<>
|
|
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 ? (
|
|
457
476
|
<AsyncSearchableSelect
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
loadOptions={async (search, page) => {
|
|
475
|
-
if (!fetchChequeLeaves) return [];
|
|
476
|
-
const res = await fetchChequeLeaves(search, page);
|
|
477
|
-
return res.is_success && res.result ? res.result : [];
|
|
478
|
-
}}
|
|
479
|
-
option={{
|
|
480
|
-
label: "cheque_number",
|
|
481
|
-
value: "cheque_number",
|
|
482
|
-
renderOption: (item: any) => (
|
|
483
|
-
<div className="flex flex-col">
|
|
484
|
-
<span>{item.cheque_number}</span>
|
|
485
|
-
{item.cheque_book?.bank_account?.bank_name && (
|
|
486
|
-
<span className="text-[11px] text-foreground-subtle mt-0.5">
|
|
487
|
-
{item.cheque_book.bank_account.bank_name} {item.cheque_book.bank_account.branch ? `— ${item.cheque_book.bank_account.branch}` : ""}
|
|
488
|
-
</span>
|
|
489
|
-
)}
|
|
490
|
-
</div>
|
|
491
|
-
)
|
|
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
|
+
});
|
|
492
493
|
}}
|
|
493
|
-
placeholder="Search cheque number..."
|
|
494
494
|
disabled={field.readonly}
|
|
495
495
|
required={field.required}
|
|
496
|
+
placeholder="Select..."
|
|
497
|
+
loadOptions={async () => field.options || []}
|
|
496
498
|
/>
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
{entry.metadata.
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
+
})}
|
|
515
560
|
</div>
|
|
516
561
|
)}
|
|
517
562
|
</div>
|