@apptimate/ui 4.5.0 → 4.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 +1 -1
- package/src/base-components/SearchableSelect.tsx +7 -3
- package/src/common-components/PaymentSection.tsx +41 -7
- package/src/common-components/pickers/EntityPickerModal.tsx +5 -1
- package/src/common-components/pickers/PartyPicker.tsx +3 -0
- package/src/common-components/pickers/WarehousePicker.tsx +3 -0
- package/src/common-components/transaction/MetadataPanel.tsx +4 -1
- package/src/common-components/transaction/ProductSelectionPanel.tsx +218 -59
- package/src/common-components/transaction/ProductTransactionScreen.tsx +325 -26
- package/src/common-components/transaction/types.ts +23 -7
package/package.json
CHANGED
|
@@ -49,6 +49,8 @@ export interface SearchableSelectProps<T = Record<string, unknown>> {
|
|
|
49
49
|
creatable?: CreatableConfig<T>;
|
|
50
50
|
/** Additional className on the outer wrapper */
|
|
51
51
|
className?: string;
|
|
52
|
+
/** Additional className on the control div (the trigger) */
|
|
53
|
+
controlClassName?: string;
|
|
52
54
|
/** Enable search filtering (default: true) */
|
|
53
55
|
isSearchable?: boolean;
|
|
54
56
|
/** Show clear button when a value is selected */
|
|
@@ -119,6 +121,7 @@ function SelectCore<T extends Record<string, unknown>>({
|
|
|
119
121
|
error,
|
|
120
122
|
isRequired,
|
|
121
123
|
surface = 0,
|
|
124
|
+
controlClassName,
|
|
122
125
|
}: SelectCoreProps<T>) {
|
|
123
126
|
const opt = { ...DEFAULT_OPTION_CONFIG, ...optionConfig } as SearchableSelectOptionConfig<T>;
|
|
124
127
|
|
|
@@ -560,11 +563,11 @@ function SelectCore<T extends Record<string, unknown>>({
|
|
|
560
563
|
)}
|
|
561
564
|
|
|
562
565
|
{/* Option label */}
|
|
563
|
-
<
|
|
566
|
+
<div className="flex-1 min-w-0">
|
|
564
567
|
{opt.renderOption
|
|
565
568
|
? opt.renderOption(item)
|
|
566
|
-
: getLabel(item)}
|
|
567
|
-
</
|
|
569
|
+
: <span className="truncate block">{getLabel(item)}</span>}
|
|
570
|
+
</div>
|
|
568
571
|
</li>
|
|
569
572
|
))}
|
|
570
573
|
|
|
@@ -616,6 +619,7 @@ function SelectCore<T extends Record<string, unknown>>({
|
|
|
616
619
|
ref={controlRef}
|
|
617
620
|
className={cn(
|
|
618
621
|
'w-full border-[1.5px] rounded-[10px] px-3.5 py-2.5 flex items-center gap-2 cursor-pointer transition-all min-h-[42px]',
|
|
622
|
+
controlClassName,
|
|
619
623
|
isOpen
|
|
620
624
|
? `border-gray-300 bg-surface-${surface === 0 ? 1 : 2}`
|
|
621
625
|
: `border-border-subtle bg-surface-${surface} hover:border-gray-300`,
|
|
@@ -11,9 +11,10 @@ import { AsyncSearchableSelect } from "../base-components/SearchableSelect";
|
|
|
11
11
|
export interface PaymentModeField {
|
|
12
12
|
key: string;
|
|
13
13
|
label: string;
|
|
14
|
-
type: "text" | "number" | "date" | "bank_account_select" | "material_type_select";
|
|
14
|
+
type: "text" | "number" | "date" | "bank_account_select" | "material_type_select" | "select" | "multi_select";
|
|
15
15
|
required: boolean;
|
|
16
16
|
readonly?: boolean;
|
|
17
|
+
options?: { label: string; value: string }[];
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export interface PaymentMode {
|
|
@@ -65,11 +66,13 @@ export interface PaymentSectionProps {
|
|
|
65
66
|
allowedSpecialModes?: string[];
|
|
66
67
|
/** Fetcher for cheque leaves (for auto-complete) */
|
|
67
68
|
fetchChequeLeaves?: (query: string, page: number) => Promise<{ is_success: boolean; result?: any[] }>;
|
|
69
|
+
/** UI Variant */
|
|
70
|
+
variant?: "default" | "advance";
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
/* ── Component ── */
|
|
71
74
|
|
|
72
|
-
export function PaymentSection({ totalAmount, payments, onPaymentsChange, compact, fetchPaymentModes, fetchBankAccounts, materialTypes = [], allowedSpecialModes = [], fetchChequeLeaves }: PaymentSectionProps) {
|
|
75
|
+
export function PaymentSection({ totalAmount, payments, onPaymentsChange, compact, fetchPaymentModes, fetchBankAccounts, materialTypes = [], allowedSpecialModes = [], fetchChequeLeaves, variant = "default" }: PaymentSectionProps) {
|
|
73
76
|
const [paymentModes, setPaymentModes] = useState<PaymentMode[]>([]);
|
|
74
77
|
const [bankAccounts, setBankAccounts] = useState<BankAccount[]>([]);
|
|
75
78
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -280,14 +283,14 @@ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compac
|
|
|
280
283
|
{/* ── Totals ── */}
|
|
281
284
|
{payments.length > 0 && (
|
|
282
285
|
<div className="bg-surface-0 rounded-[12px] border border-border-subtle p-4 space-y-2">
|
|
283
|
-
<TotalRow label="Amount to Pay" value={formatCurrency(totalAmount)} bold />
|
|
286
|
+
<TotalRow label={variant === "advance" ? "Target Amount" : "Amount to Pay"} value={formatCurrency(totalAmount)} bold />
|
|
284
287
|
<div className="border-t border-border-subtle my-2" />
|
|
285
288
|
<TotalRow label="Total Paid" value={formatCurrency(totalPaid)} />
|
|
286
289
|
{creditAmount > 0.01 && (
|
|
287
290
|
<TotalRow label="Credit Amount" value={formatCurrency(creditAmount)} className="text-amber-600" />
|
|
288
291
|
)}
|
|
289
292
|
<TotalRow
|
|
290
|
-
label={balance > 0.01 ? "Outstanding Balance" : balance < -0.01 ? "Change Due" : "Settled"}
|
|
293
|
+
label={balance > 0.01 ? "Outstanding Balance" : balance < -0.01 ? "Change Due" : (variant === "advance" ? "Matched" : "Settled")}
|
|
291
294
|
value={formatCurrency(Math.abs(balance))}
|
|
292
295
|
className={balance > 0.01 ? "text-danger-alt" : "text-green-600"}
|
|
293
296
|
bold
|
|
@@ -299,7 +302,7 @@ export function PaymentSection({ totalAmount, payments, onPaymentsChange, compac
|
|
|
299
302
|
)}
|
|
300
303
|
{Math.abs(balance) < 0.01 && creditAmount <= 0.01 && (
|
|
301
304
|
<div className="flex items-center gap-1.5 mt-1 text-[11px] text-green-600 font-medium">
|
|
302
|
-
<Check size={12} /><span>Fully paid</span>
|
|
305
|
+
<Check size={12} /><span>{variant === "advance" ? "Amount matched" : "Fully paid"}</span>
|
|
303
306
|
</div>
|
|
304
307
|
)}
|
|
305
308
|
</div>
|
|
@@ -365,8 +368,8 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
|
|
|
365
368
|
<div className="flex flex-col gap-1">
|
|
366
369
|
<label className="text-[11px] text-foreground-subtle font-bold uppercase tracking-wider">Amount</label>
|
|
367
370
|
<input type="number" value={entry.amount || ""} onChange={(e) => onUpdate({ amount: Number(e.target.value) || 0 })}
|
|
368
|
-
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"
|
|
369
|
-
step="0.01" min={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 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
372
|
+
step="0.01" min={0} disabled={entry.mode_code === "advance"} />
|
|
370
373
|
</div>
|
|
371
374
|
|
|
372
375
|
{/* Dynamic sub-fields from field_config */}
|
|
@@ -414,6 +417,37 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
|
|
|
414
417
|
<option value="">Select Material...</option>
|
|
415
418
|
{materialTypes.map((m) => (<option key={m.id} value={m.id}>{m.name}</option>))}
|
|
416
419
|
</select>
|
|
420
|
+
) : field.type === "select" ? (
|
|
421
|
+
<select value={entry.metadata[field.key] || ""}
|
|
422
|
+
onChange={(e) => onUpdate({ metadata: { ...entry.metadata, [field.key]: e.target.value } })}
|
|
423
|
+
disabled={field.readonly}
|
|
424
|
+
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
|
+
<option value="">Select...</option>
|
|
426
|
+
{field.options?.map((o) => (<option key={o.value} value={o.value}>{o.label}</option>))}
|
|
427
|
+
</select>
|
|
428
|
+
) : field.type === "multi_select" as any ? (
|
|
429
|
+
<AsyncSearchableSelect
|
|
430
|
+
multiple
|
|
431
|
+
option={{ label: 'label', value: 'value' }}
|
|
432
|
+
defaultValue={
|
|
433
|
+
Array.isArray(entry.metadata[field.key])
|
|
434
|
+
? (field.options || []).filter(o => (entry.metadata[field.key] as any).includes(o.value))
|
|
435
|
+
: []
|
|
436
|
+
}
|
|
437
|
+
onChange={(val, selectedObjs: any) => {
|
|
438
|
+
const sum = Array.isArray(selectedObjs)
|
|
439
|
+
? selectedObjs.reduce((acc, curr) => acc + (curr.unapplied_amount || 0), 0)
|
|
440
|
+
: 0;
|
|
441
|
+
|
|
442
|
+
onUpdate({
|
|
443
|
+
amount: sum,
|
|
444
|
+
metadata: { ...entry.metadata, [field.key]: val as any }
|
|
445
|
+
});
|
|
446
|
+
}}
|
|
447
|
+
disabled={field.readonly}
|
|
448
|
+
placeholder="Select..."
|
|
449
|
+
loadOptions={async () => field.options || []}
|
|
450
|
+
/>
|
|
417
451
|
) : field.key === "cheque_number" && fetchChequeLeaves ? (
|
|
418
452
|
<>
|
|
419
453
|
<AsyncSearchableSelect
|
|
@@ -172,6 +172,7 @@ export interface PickerTriggerProps {
|
|
|
172
172
|
placeholder?: string;
|
|
173
173
|
isRequired?: boolean;
|
|
174
174
|
error?: string;
|
|
175
|
+
disabled?: boolean;
|
|
175
176
|
onClick: () => void;
|
|
176
177
|
onClear?: () => void;
|
|
177
178
|
}
|
|
@@ -182,6 +183,7 @@ export function PickerTrigger({
|
|
|
182
183
|
placeholder = "Select…",
|
|
183
184
|
isRequired = false,
|
|
184
185
|
error,
|
|
186
|
+
disabled = false,
|
|
185
187
|
onClick,
|
|
186
188
|
onClear,
|
|
187
189
|
}: PickerTriggerProps) {
|
|
@@ -193,10 +195,12 @@ export function PickerTrigger({
|
|
|
193
195
|
</label>
|
|
194
196
|
<button
|
|
195
197
|
type="button"
|
|
198
|
+
disabled={disabled}
|
|
196
199
|
onClick={onClick}
|
|
197
200
|
className={`
|
|
198
201
|
w-full flex items-center justify-between px-3.5 py-2.5 bg-surface-0 border-[1.5px] rounded-[10px] text-[13.5px] text-left transition-all
|
|
199
|
-
${
|
|
202
|
+
${disabled ? "opacity-60 cursor-not-allowed bg-surface-hover" : ""}
|
|
203
|
+
${error && !disabled
|
|
200
204
|
? "border-red-500 text-red-500"
|
|
201
205
|
: value
|
|
202
206
|
? "border-border-subtle text-foreground-1"
|
|
@@ -18,6 +18,7 @@ interface PartyPickerProps {
|
|
|
18
18
|
label?: string;
|
|
19
19
|
placeholder?: string;
|
|
20
20
|
isRequired?: boolean;
|
|
21
|
+
disabled?: boolean;
|
|
21
22
|
partyType?: "customer" | "supplier" | "all" | "artisan";
|
|
22
23
|
customTrigger?: (onClick: () => void) => React.ReactNode;
|
|
23
24
|
}
|
|
@@ -29,6 +30,7 @@ export function PartyPicker({
|
|
|
29
30
|
label = "Party",
|
|
30
31
|
placeholder = "Select party…",
|
|
31
32
|
isRequired = false,
|
|
33
|
+
disabled = false,
|
|
32
34
|
partyType = "all",
|
|
33
35
|
customTrigger,
|
|
34
36
|
}: PartyPickerProps) {
|
|
@@ -126,6 +128,7 @@ export function PartyPicker({
|
|
|
126
128
|
value={displayValue || null}
|
|
127
129
|
placeholder={placeholder}
|
|
128
130
|
isRequired={isRequired}
|
|
131
|
+
disabled={disabled}
|
|
129
132
|
onClick={handleOpen}
|
|
130
133
|
onClear={value ? handleClear : undefined}
|
|
131
134
|
/>
|
|
@@ -18,6 +18,7 @@ interface WarehousePickerProps {
|
|
|
18
18
|
label?: string;
|
|
19
19
|
placeholder?: string;
|
|
20
20
|
isRequired?: boolean;
|
|
21
|
+
disabled?: boolean;
|
|
21
22
|
fetchFromAllUserOrgs?: boolean;
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -28,6 +29,7 @@ export function WarehousePicker({
|
|
|
28
29
|
label = "Warehouse",
|
|
29
30
|
placeholder = "Select warehouse…",
|
|
30
31
|
isRequired = false,
|
|
32
|
+
disabled = false,
|
|
31
33
|
fetchFromAllUserOrgs = false,
|
|
32
34
|
}: WarehousePickerProps) {
|
|
33
35
|
const [isOpen, setIsOpen] = useState(false);
|
|
@@ -86,6 +88,7 @@ export function WarehousePicker({
|
|
|
86
88
|
value={displayValue || null}
|
|
87
89
|
placeholder={placeholder}
|
|
88
90
|
isRequired={isRequired}
|
|
91
|
+
disabled={disabled}
|
|
89
92
|
onClick={handleOpen}
|
|
90
93
|
onClear={value ? handleClear : undefined}
|
|
91
94
|
/>
|
|
@@ -146,6 +146,7 @@ export function MetadataPanel({
|
|
|
146
146
|
label={config.partyLabel}
|
|
147
147
|
isRequired
|
|
148
148
|
fetchFromAllUserOrgs={true}
|
|
149
|
+
disabled={config.disablePartySelection}
|
|
149
150
|
/>
|
|
150
151
|
) : (
|
|
151
152
|
<PartyPicker
|
|
@@ -155,6 +156,7 @@ export function MetadataPanel({
|
|
|
155
156
|
label={config.partyLabel}
|
|
156
157
|
partyType={config.partyType}
|
|
157
158
|
isRequired
|
|
159
|
+
disabled={config.disablePartySelection}
|
|
158
160
|
/>
|
|
159
161
|
)
|
|
160
162
|
)}
|
|
@@ -167,6 +169,7 @@ export function MetadataPanel({
|
|
|
167
169
|
onChange={(w) => onWarehouseChange(w?.id ?? null, w?.name)}
|
|
168
170
|
label="Warehouse"
|
|
169
171
|
isRequired
|
|
172
|
+
disabled={config.disableWarehouseSelection}
|
|
170
173
|
/>
|
|
171
174
|
)}
|
|
172
175
|
|
|
@@ -187,7 +190,7 @@ export function MetadataPanel({
|
|
|
187
190
|
|
|
188
191
|
{/* ── Expected Delivery Date ── */}
|
|
189
192
|
{config.showExpectedDeliveryDate && onExpectedDeliveryDateChange && (
|
|
190
|
-
<Section label="Expected Delivery">
|
|
193
|
+
<Section label={config.expectedDeliveryDateLabel || "Expected Delivery"}>
|
|
191
194
|
<div className="relative">
|
|
192
195
|
<Calendar size={15} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-foreground-disabled pointer-events-none" />
|
|
193
196
|
<input
|