@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apptimate/ui",
3
- "version": "4.5.0",
3
+ "version": "4.7.0",
4
4
  "main": "src/index.tsx",
5
5
  "types": "src/index.tsx",
6
6
  "dependencies": {
@@ -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
- <span className="truncate">
566
+ <div className="flex-1 min-w-0">
564
567
  {opt.renderOption
565
568
  ? opt.renderOption(item)
566
- : getLabel(item)}
567
- </span>
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
- ${error
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