@apptimate/ui 3.5.0 → 3.7.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 +3 -2
- package/src/base-components/Input.tsx +6 -3
- package/src/base-components/TagsInput.tsx +106 -0
- package/src/base-components/WizardModal.tsx +162 -0
- package/src/common-components/item-wizard/ItemFormWizard.tsx +705 -0
- package/src/common-components/item-wizard/SkuConfigModal.tsx +314 -0
- package/src/common-components/pickers/BrandPicker.tsx +194 -0
- package/src/common-components/pickers/CategoryPicker.tsx +342 -0
- package/src/common-components/pickers/EntityPickerModal.tsx +221 -0
- package/src/common-components/pickers/PartyPicker.tsx +231 -0
- package/src/common-components/pickers/TransactionItemPicker.tsx +166 -0
- package/src/common-components/pickers/UomGroupPicker.tsx +196 -0
- package/src/common-components/pickers/UomPicker.tsx +306 -0
- package/src/common-components/pickers/WarehousePicker.tsx +135 -0
- package/src/common-components/pickers/index.ts +11 -0
- package/src/common-components/pickers/modals/BatchSelectionModal.tsx +359 -0
- package/src/common-components/pickers/modals/SerialSelectionModal.tsx +247 -0
- package/src/common-components/pickers/modals/types.ts +30 -0
- package/src/common-components/transaction/MetadataPanel.tsx +417 -0
- package/src/common-components/transaction/ProductSelectionPanel.tsx +587 -0
- package/src/common-components/transaction/ProductTransactionScreen.tsx +250 -0
- package/src/common-components/transaction/index.ts +17 -0
- package/src/common-components/transaction/types.ts +300 -0
- package/src/components/shared/ImageUploadComponent.tsx +1 -2
- package/src/index.tsx +20 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
4
|
+
import { cn } from '@apptimate/core-lib';
|
|
5
|
+
import { Button } from '../../base-components/Button';
|
|
6
|
+
import {
|
|
7
|
+
CreditCard, Banknote, Building,
|
|
8
|
+
Trash2, Loader2, AlertCircle, Check, Calendar,
|
|
9
|
+
} from 'lucide-react';
|
|
10
|
+
import { getPaymentModes, getBankAccountsLookup } from '@apptimate/core-lib';
|
|
11
|
+
import { PartyPicker, WarehousePicker } from '../pickers';
|
|
12
|
+
import type {
|
|
13
|
+
TransactionScreenConfig, PaymentMode, PaymentEntry, BankAccount,
|
|
14
|
+
PartyLookup, PaymentModeField, TransactionLineItem,
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
interface MetadataPanelProps {
|
|
18
|
+
config: TransactionScreenConfig;
|
|
19
|
+
lineItems: TransactionLineItem[];
|
|
20
|
+
partyId: number | null;
|
|
21
|
+
partyName?: string | null;
|
|
22
|
+
warehouseId: number | null;
|
|
23
|
+
warehouseName?: string | null;
|
|
24
|
+
transactionDate?: string;
|
|
25
|
+
notes: string;
|
|
26
|
+
payments: PaymentEntry[];
|
|
27
|
+
onPartyChange: (id: number | null, party?: PartyLookup) => void;
|
|
28
|
+
onWarehouseChange: (id: number | null, name?: string) => void;
|
|
29
|
+
onDateChange?: (date: string) => void;
|
|
30
|
+
onNotesChange: (v: string) => void;
|
|
31
|
+
onPaymentsChange: (payments: PaymentEntry[]) => void;
|
|
32
|
+
onSubmit: () => void;
|
|
33
|
+
isSubmitting: boolean;
|
|
34
|
+
renderExtraMeta?: () => React.ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function MetadataPanel({
|
|
38
|
+
config, lineItems, partyId, partyName, warehouseId, warehouseName, transactionDate, notes, payments,
|
|
39
|
+
onPartyChange, onWarehouseChange, onDateChange, onNotesChange, onPaymentsChange, onSubmit, isSubmitting,
|
|
40
|
+
renderExtraMeta,
|
|
41
|
+
}: MetadataPanelProps) {
|
|
42
|
+
const [paymentModes, setPaymentModes] = useState<PaymentMode[]>([]);
|
|
43
|
+
const [bankAccounts, setBankAccounts] = useState<BankAccount[]>([]);
|
|
44
|
+
const [isLoadingMeta, setIsLoadingMeta] = useState(false);
|
|
45
|
+
|
|
46
|
+
// ── Load payment data on mount ──
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!config.showPayment) return;
|
|
49
|
+
const load = async () => {
|
|
50
|
+
setIsLoadingMeta(true);
|
|
51
|
+
try {
|
|
52
|
+
const [modesRes, bankRes] = await Promise.all([
|
|
53
|
+
getPaymentModes(),
|
|
54
|
+
getBankAccountsLookup(),
|
|
55
|
+
]);
|
|
56
|
+
if (modesRes.is_success) setPaymentModes(modesRes.result || []);
|
|
57
|
+
if (bankRes.is_success) setBankAccounts(bankRes.result || []);
|
|
58
|
+
} finally {
|
|
59
|
+
setIsLoadingMeta(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
load();
|
|
63
|
+
}, [config.showPayment]);
|
|
64
|
+
|
|
65
|
+
// ── Calculations ──
|
|
66
|
+
const subtotal = useMemo(() => lineItems.reduce((s, l) => s + l.unit_price * l.quantity, 0), [lineItems]);
|
|
67
|
+
const discountTotal = useMemo(() => lineItems.reduce((s, l) => s + l.discount_amount, 0), [lineItems]);
|
|
68
|
+
const grandTotal = useMemo(() => subtotal - discountTotal, [subtotal, discountTotal]);
|
|
69
|
+
const totalPaid = useMemo(() => payments.reduce((s, p) => s + p.amount, 0), [payments]);
|
|
70
|
+
const balance = useMemo(() => grandTotal - totalPaid, [grandTotal, totalPaid]);
|
|
71
|
+
|
|
72
|
+
const formatCurrency = (v: number) => v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
73
|
+
|
|
74
|
+
// ── Payment management ──
|
|
75
|
+
const addPayment = useCallback(
|
|
76
|
+
(mode: PaymentMode) => {
|
|
77
|
+
const entry: PaymentEntry = {
|
|
78
|
+
mode_id: mode.id,
|
|
79
|
+
mode_code: mode.code,
|
|
80
|
+
mode_name: mode.name,
|
|
81
|
+
amount: Math.max(0, balance),
|
|
82
|
+
metadata: {},
|
|
83
|
+
};
|
|
84
|
+
onPaymentsChange([...payments, entry]);
|
|
85
|
+
},
|
|
86
|
+
[payments, balance, onPaymentsChange]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const updatePayment = useCallback(
|
|
90
|
+
(index: number, updates: Partial<PaymentEntry>) => {
|
|
91
|
+
const next = [...payments];
|
|
92
|
+
next[index] = { ...next[index], ...updates };
|
|
93
|
+
|
|
94
|
+
if (
|
|
95
|
+
Object.prototype.hasOwnProperty.call(updates, 'amount') &&
|
|
96
|
+
next[index].mode_code !== 'credit' &&
|
|
97
|
+
next.length > 1
|
|
98
|
+
) {
|
|
99
|
+
const targetIndex = next.findIndex((payment, paymentIndex) =>
|
|
100
|
+
paymentIndex !== index &&
|
|
101
|
+
payment.mode_code !== 'credit' &&
|
|
102
|
+
Number(payment.amount || 0) <= 0
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (targetIndex >= 0) {
|
|
106
|
+
const paidExceptTarget = next.reduce((sum, payment, paymentIndex) => {
|
|
107
|
+
if (paymentIndex === targetIndex || payment.mode_code === 'credit') return sum;
|
|
108
|
+
return sum + Number(payment.amount || 0);
|
|
109
|
+
}, 0);
|
|
110
|
+
|
|
111
|
+
next[targetIndex] = {
|
|
112
|
+
...next[targetIndex],
|
|
113
|
+
amount: Math.max(0, Math.round((grandTotal - paidExceptTarget) * 100) / 100),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onPaymentsChange(next);
|
|
119
|
+
},
|
|
120
|
+
[payments, grandTotal, onPaymentsChange]
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const removePayment = useCallback(
|
|
124
|
+
(index: number) => {
|
|
125
|
+
onPaymentsChange(payments.filter((_, i) => i !== index));
|
|
126
|
+
},
|
|
127
|
+
[payments, onPaymentsChange]
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className="flex flex-col h-full gap-6">
|
|
132
|
+
{/* ── Extra Module-Specific Meta (e.g. adjustment type/reason) ── */}
|
|
133
|
+
{renderExtraMeta && renderExtraMeta()}
|
|
134
|
+
|
|
135
|
+
{/* ── Party Selector (picker modal) ── */}
|
|
136
|
+
{config.showParty !== false && (
|
|
137
|
+
config.partyIsWarehouse ? (
|
|
138
|
+
<WarehousePicker
|
|
139
|
+
value={partyId}
|
|
140
|
+
displayValue={partyName}
|
|
141
|
+
onChange={(w) => onPartyChange(w?.id ?? null, w ? { id: w.id, name: w.name } : undefined)}
|
|
142
|
+
label={config.partyLabel}
|
|
143
|
+
isRequired
|
|
144
|
+
fetchFromAllUserOrgs={true}
|
|
145
|
+
/>
|
|
146
|
+
) : (
|
|
147
|
+
<PartyPicker
|
|
148
|
+
value={partyId}
|
|
149
|
+
displayValue={partyName}
|
|
150
|
+
onChange={(p) => onPartyChange(p?.id ?? null, p ? { id: p.id, name: p.name, code: p.code, type: p.type } : undefined)}
|
|
151
|
+
label={config.partyLabel}
|
|
152
|
+
partyType={config.partyType}
|
|
153
|
+
/>
|
|
154
|
+
)
|
|
155
|
+
)}
|
|
156
|
+
|
|
157
|
+
{/* ── Warehouse Selector (picker modal) ── */}
|
|
158
|
+
{config.showWarehouse && (
|
|
159
|
+
<WarehousePicker
|
|
160
|
+
value={warehouseId}
|
|
161
|
+
displayValue={warehouseName}
|
|
162
|
+
onChange={(w) => onWarehouseChange(w?.id ?? null, w?.name)}
|
|
163
|
+
label="Warehouse"
|
|
164
|
+
isRequired
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{/* ── Transaction Date ── */}
|
|
169
|
+
{onDateChange && (
|
|
170
|
+
<Section label="Date">
|
|
171
|
+
<div className="relative">
|
|
172
|
+
<Calendar size={15} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-foreground-disabled pointer-events-none" />
|
|
173
|
+
<input
|
|
174
|
+
type="date"
|
|
175
|
+
value={transactionDate || ''}
|
|
176
|
+
onChange={(e) => onDateChange(e.target.value)}
|
|
177
|
+
className="w-full bg-surface-0 border-[1.5px] border-border-subtle rounded-[10px] pl-10 pr-3.5 py-3 text-[13.5px] text-foreground-1 outline-none transition-all hover:border-gray-300 focus:border-primary/40"
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
</Section>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
{/* ── Notes ── */}
|
|
184
|
+
{config.showNotes && (
|
|
185
|
+
<Section label="Notes">
|
|
186
|
+
<textarea
|
|
187
|
+
value={notes}
|
|
188
|
+
onChange={(e) => onNotesChange(e.target.value)}
|
|
189
|
+
placeholder="Add transaction notes..."
|
|
190
|
+
className="w-full bg-surface-0 border-[1.5px] border-border-subtle rounded-[10px] px-3.5 py-3 text-[13px] text-foreground-1 outline-none transition-all hover:border-gray-300 focus:border-primary/40 resize-none"
|
|
191
|
+
rows={2}
|
|
192
|
+
/>
|
|
193
|
+
</Section>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{/* ── Totals ── */}
|
|
197
|
+
{config.showPrices && lineItems.length > 0 && (
|
|
198
|
+
<div className="bg-surface-0 rounded-[12px] border border-border-subtle p-4 space-y-2">
|
|
199
|
+
<TotalRow label="Subtotal" value={formatCurrency(subtotal)} />
|
|
200
|
+
{discountTotal > 0 && (
|
|
201
|
+
<TotalRow label="Discount" value={`-${formatCurrency(discountTotal)}`} className="text-green-600" />
|
|
202
|
+
)}
|
|
203
|
+
<div className="border-t border-border-subtle my-2" />
|
|
204
|
+
<TotalRow label="Grand Total" value={formatCurrency(grandTotal)} bold />
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
{/* ── Payment Section ── */}
|
|
209
|
+
{config.showPayment && (
|
|
210
|
+
<Section label="Payment">
|
|
211
|
+
{isLoadingMeta ? (
|
|
212
|
+
<div className="flex items-center justify-center py-4">
|
|
213
|
+
<Loader2 size={18} className="animate-spin text-primary" />
|
|
214
|
+
</div>
|
|
215
|
+
) : (
|
|
216
|
+
<div className="space-y-2.5">
|
|
217
|
+
{/* Existing payment entries */}
|
|
218
|
+
{payments.map((entry, idx) => {
|
|
219
|
+
const mode = paymentModes.find((m) => m.id === entry.mode_id);
|
|
220
|
+
return (
|
|
221
|
+
<PaymentEntryRow
|
|
222
|
+
key={idx}
|
|
223
|
+
entry={entry}
|
|
224
|
+
mode={mode}
|
|
225
|
+
bankAccounts={bankAccounts}
|
|
226
|
+
onUpdate={(updates) => updatePayment(idx, updates)}
|
|
227
|
+
onRemove={() => removePayment(idx)}
|
|
228
|
+
/>
|
|
229
|
+
);
|
|
230
|
+
})}
|
|
231
|
+
|
|
232
|
+
{/* Add payment mode buttons */}
|
|
233
|
+
{(lineItems.length > 0) && (
|
|
234
|
+
<div className="flex flex-wrap gap-2 pt-1">
|
|
235
|
+
{paymentModes
|
|
236
|
+
.filter((m) => m.is_active)
|
|
237
|
+
.map((mode) => {
|
|
238
|
+
const modeIcon = mode.code === 'cash' ? <Banknote size={14} /> :
|
|
239
|
+
mode.code === 'card' ? <CreditCard size={14} /> :
|
|
240
|
+
mode.code === 'bank_transfer' ? <Building size={14} /> :
|
|
241
|
+
<CreditCard size={14} />;
|
|
242
|
+
return (
|
|
243
|
+
<button
|
|
244
|
+
key={mode.id}
|
|
245
|
+
onClick={() => addPayment(mode)}
|
|
246
|
+
className="inline-flex items-center gap-2 px-4 py-2 text-[12px] font-semibold rounded-[10px] bg-surface-0 border border-border-subtle text-foreground-1 hover:border-primary/40 hover:bg-primary/5 hover:text-primary transition-all"
|
|
247
|
+
>
|
|
248
|
+
{modeIcon}
|
|
249
|
+
{mode.name}
|
|
250
|
+
</button>
|
|
251
|
+
);
|
|
252
|
+
})}
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
|
|
256
|
+
{/* Balance */}
|
|
257
|
+
{payments.length > 0 && (
|
|
258
|
+
<div className="bg-surface-0 rounded-[10px] border border-border-subtle p-3.5 mt-2 space-y-1.5">
|
|
259
|
+
<TotalRow label="Total Paid" value={formatCurrency(totalPaid)} />
|
|
260
|
+
<TotalRow
|
|
261
|
+
label={balance > 0.01 ? "Balance Due" : balance < -0.01 ? "Change Due" : "Settled"}
|
|
262
|
+
value={formatCurrency(Math.abs(balance))}
|
|
263
|
+
className={balance > 0.01 ? 'text-danger-alt' : balance < -0.01 ? 'text-amber-600' : 'text-green-600'}
|
|
264
|
+
bold
|
|
265
|
+
/>
|
|
266
|
+
{balance < -0.01 && (
|
|
267
|
+
<div className="flex items-center gap-1.5 mt-1 text-[11px] text-amber-600 font-medium">
|
|
268
|
+
<AlertCircle size={12} />
|
|
269
|
+
<span>Change: {formatCurrency(Math.abs(balance))}</span>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
{Math.abs(balance) < 0.01 && (
|
|
273
|
+
<div className="flex items-center gap-1.5 mt-1 text-[11px] text-green-600 font-medium">
|
|
274
|
+
<Check size={12} />
|
|
275
|
+
<span>Fully paid</span>
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
</Section>
|
|
283
|
+
)}
|
|
284
|
+
|
|
285
|
+
{/* ── Spacer ── */}
|
|
286
|
+
<div className="flex-1" />
|
|
287
|
+
|
|
288
|
+
{/* ── Submit Button ── */}
|
|
289
|
+
<Button
|
|
290
|
+
color="primary"
|
|
291
|
+
onClick={onSubmit}
|
|
292
|
+
isDisabled={lineItems.length === 0}
|
|
293
|
+
isLoading={isSubmitting}
|
|
294
|
+
className="!w-full !py-4 !text-[14px] !font-semibold !rounded-[10px]"
|
|
295
|
+
>
|
|
296
|
+
{config.submitLabel}
|
|
297
|
+
</Button>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Section wrapper ──
|
|
303
|
+
|
|
304
|
+
function Section({ label, children }: { label: string; children: React.ReactNode }) {
|
|
305
|
+
return (
|
|
306
|
+
<div className="flex flex-col gap-2">
|
|
307
|
+
<div className="text-[11px] font-bold text-foreground-subtle uppercase tracking-wider">
|
|
308
|
+
{label}
|
|
309
|
+
</div>
|
|
310
|
+
{children}
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Total Row ──
|
|
316
|
+
|
|
317
|
+
function TotalRow({ label, value, bold, className }: { label: string; value: string; bold?: boolean; className?: string }) {
|
|
318
|
+
return (
|
|
319
|
+
<div className="flex items-center justify-between">
|
|
320
|
+
<span className={cn("text-[12px]", bold ? "font-bold text-foreground-1" : "text-foreground-subtle")}>{label}</span>
|
|
321
|
+
<span className={cn("text-[12px]", bold ? "font-bold text-foreground-0 text-[14px]" : "text-foreground-1 font-medium", className)}>{value}</span>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── Payment Entry Row ──
|
|
327
|
+
|
|
328
|
+
interface PaymentEntryRowProps {
|
|
329
|
+
entry: PaymentEntry;
|
|
330
|
+
mode?: PaymentMode;
|
|
331
|
+
bankAccounts: BankAccount[];
|
|
332
|
+
onUpdate: (updates: Partial<PaymentEntry>) => void;
|
|
333
|
+
onRemove: () => void;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function PaymentEntryRow({ entry, mode, bankAccounts, onUpdate, onRemove }: PaymentEntryRowProps) {
|
|
337
|
+
const fields = mode?.field_config?.fields || [];
|
|
338
|
+
|
|
339
|
+
const getModeIcon = () => {
|
|
340
|
+
switch (entry.mode_code) {
|
|
341
|
+
case 'cash': return <Banknote size={13} />;
|
|
342
|
+
case 'card': return <CreditCard size={13} />;
|
|
343
|
+
case 'bank_transfer': return <Building size={13} />;
|
|
344
|
+
default: return <CreditCard size={13} />;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<div className="bg-surface-0 border-[1.5px] border-border-subtle rounded-[10px] p-4 space-y-3">
|
|
350
|
+
{/* Header */}
|
|
351
|
+
<div className="flex items-center justify-between">
|
|
352
|
+
<div className="flex items-center gap-1.5 text-[12px] font-bold text-foreground-1">
|
|
353
|
+
{getModeIcon()}
|
|
354
|
+
{entry.mode_name}
|
|
355
|
+
</div>
|
|
356
|
+
<button
|
|
357
|
+
onClick={onRemove}
|
|
358
|
+
className="w-7 h-7 rounded-[6px] flex items-center justify-center text-foreground-disabled hover:text-danger-alt hover:bg-danger-alt/10 transition-all"
|
|
359
|
+
>
|
|
360
|
+
<Trash2 size={13} />
|
|
361
|
+
</button>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
{/* Amount */}
|
|
365
|
+
<div className="flex flex-col gap-1">
|
|
366
|
+
<label className="text-[11px] text-foreground-subtle font-bold uppercase tracking-wider">Amount</label>
|
|
367
|
+
<input
|
|
368
|
+
type="number"
|
|
369
|
+
value={entry.amount || ''}
|
|
370
|
+
onChange={(e) => onUpdate({ amount: Number(e.target.value) || 0 })}
|
|
371
|
+
className="w-full bg-surface-1 border-[1.5px] border-border-subtle rounded-[8px] px-3 py-2.5 text-[13px] font-medium 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"
|
|
372
|
+
step="0.01"
|
|
373
|
+
min={0}
|
|
374
|
+
/>
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
{/* Dynamic sub-fields from field_config */}
|
|
378
|
+
{fields.length > 0 && (
|
|
379
|
+
<div className="grid grid-cols-2 gap-2.5">
|
|
380
|
+
{fields.map((field: PaymentModeField) => (
|
|
381
|
+
<div key={field.key} className="flex flex-col gap-1">
|
|
382
|
+
<label className="text-[11px] text-foreground-subtle font-bold uppercase tracking-wider">
|
|
383
|
+
{field.label}
|
|
384
|
+
{field.required && <span className="text-danger-alt ml-0.5">*</span>}
|
|
385
|
+
</label>
|
|
386
|
+
{field.type === 'bank_account_select' ? (
|
|
387
|
+
<select
|
|
388
|
+
value={entry.metadata[field.key] || ''}
|
|
389
|
+
onChange={(e) =>
|
|
390
|
+
onUpdate({ metadata: { ...entry.metadata, [field.key]: e.target.value ? Number(e.target.value) : '' } })
|
|
391
|
+
}
|
|
392
|
+
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"
|
|
393
|
+
>
|
|
394
|
+
<option value="">Select...</option>
|
|
395
|
+
{bankAccounts.map((b) => (
|
|
396
|
+
<option key={b.id} value={b.id}>
|
|
397
|
+
{b.bank_name} - {b.account_number}
|
|
398
|
+
</option>
|
|
399
|
+
))}
|
|
400
|
+
</select>
|
|
401
|
+
) : (
|
|
402
|
+
<input
|
|
403
|
+
type={field.type === 'number' ? 'number' : field.type === 'date' ? 'date' : 'text'}
|
|
404
|
+
value={entry.metadata[field.key] || ''}
|
|
405
|
+
onChange={(e) =>
|
|
406
|
+
onUpdate({ metadata: { ...entry.metadata, [field.key]: e.target.value } })
|
|
407
|
+
}
|
|
408
|
+
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 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
409
|
+
/>
|
|
410
|
+
)}
|
|
411
|
+
</div>
|
|
412
|
+
))}
|
|
413
|
+
</div>
|
|
414
|
+
)}
|
|
415
|
+
</div>
|
|
416
|
+
);
|
|
417
|
+
}
|