@apptimate/ui 3.2.0 → 3.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
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import toast from "react-hot-toast";
|
|
3
|
-
import { Button } from "./Button";
|
|
4
|
-
|
|
5
|
-
export const handleFinanceError = (error: any, defaultMessage: string = "An error occurred") => {
|
|
6
|
-
const errMsg = typeof error === "string" ? error : (error?.message || defaultMessage);
|
|
7
|
-
|
|
8
|
-
if (errMsg.toLowerCase().includes("fiscal year")) {
|
|
9
|
-
toast.error((t) => (
|
|
10
|
-
<div className="flex flex-col gap-2 mt-1">
|
|
11
|
-
<span>{errMsg}</span>
|
|
12
|
-
<Button size="small" variant="solid" color="primary" className="w-fit mt-1" onClick={() => {
|
|
13
|
-
toast.dismiss(t.id);
|
|
14
|
-
window.open("/finance/fiscal-years", "_blank");
|
|
15
|
-
}}>
|
|
16
|
-
Open Fiscal Years
|
|
17
|
-
</Button>
|
|
18
|
-
</div>
|
|
19
|
-
), { duration: 8000 });
|
|
20
|
-
} else {
|
|
21
|
-
toast.error(errMsg);
|
|
22
|
-
}
|
|
23
|
-
};
|
|
1
|
+
import React from "react";
|
|
2
|
+
import toast from "react-hot-toast";
|
|
3
|
+
import { Button } from "./Button";
|
|
4
|
+
|
|
5
|
+
export const handleFinanceError = (error: any, defaultMessage: string = "An error occurred") => {
|
|
6
|
+
const errMsg = typeof error === "string" ? error : (error?.message || defaultMessage);
|
|
7
|
+
|
|
8
|
+
if (errMsg.toLowerCase().includes("fiscal year")) {
|
|
9
|
+
toast.error((t) => (
|
|
10
|
+
<div className="flex flex-col gap-2 mt-1">
|
|
11
|
+
<span>{errMsg}</span>
|
|
12
|
+
<Button size="small" variant="solid" color="primary" className="w-fit mt-1" onClick={() => {
|
|
13
|
+
toast.dismiss(t.id);
|
|
14
|
+
window.open("/finance/fiscal-years", "_blank");
|
|
15
|
+
}}>
|
|
16
|
+
Open Fiscal Years
|
|
17
|
+
</Button>
|
|
18
|
+
</div>
|
|
19
|
+
), { duration: 8000 });
|
|
20
|
+
} else {
|
|
21
|
+
toast.error(errMsg);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -124,14 +124,42 @@ export function DashboardLayout({
|
|
|
124
124
|
{logo}
|
|
125
125
|
</div>
|
|
126
126
|
|
|
127
|
-
<
|
|
127
|
+
<style dangerouslySetInnerHTML={{
|
|
128
|
+
__html: `
|
|
129
|
+
.sidebar-scroll {
|
|
130
|
+
overflow-y: hidden;
|
|
131
|
+
}
|
|
132
|
+
.sidebar-scroll:hover {
|
|
133
|
+
overflow-y: auto;
|
|
134
|
+
}
|
|
135
|
+
/* Custom Scrollbar for Webkit */
|
|
136
|
+
.sidebar-scroll::-webkit-scrollbar {
|
|
137
|
+
width: 4px;
|
|
138
|
+
}
|
|
139
|
+
.sidebar-scroll::-webkit-scrollbar-track {
|
|
140
|
+
background: transparent;
|
|
141
|
+
}
|
|
142
|
+
.sidebar-scroll::-webkit-scrollbar-thumb {
|
|
143
|
+
background: #cbd5e1;
|
|
144
|
+
border-radius: 4px;
|
|
145
|
+
}
|
|
146
|
+
/* For Firefox */
|
|
147
|
+
.sidebar-scroll {
|
|
148
|
+
scrollbar-width: thin;
|
|
149
|
+
scrollbar-color: transparent transparent;
|
|
150
|
+
}
|
|
151
|
+
.sidebar-scroll:hover {
|
|
152
|
+
scrollbar-color: #cbd5e1 transparent;
|
|
153
|
+
}
|
|
154
|
+
`}} />
|
|
155
|
+
<nav className="flex flex-col gap-6 flex-1 w-full sidebar-scroll pb-4">
|
|
128
156
|
{menus.map((menu) => {
|
|
129
157
|
const isActive = activeMenuId === menu.id;
|
|
130
158
|
return (
|
|
131
159
|
<div
|
|
132
160
|
key={menu.id}
|
|
133
161
|
onClick={() => setActiveMenuId(menu.id)}
|
|
134
|
-
className={`flex flex-col items-center justify-center gap-1.5 cursor-pointer transition-colors ${isActive ? "text-[#2D3142]" : "text-gray-400 hover:text-[#2D3142]"
|
|
162
|
+
className={`flex flex-col items-center justify-center gap-1.5 cursor-pointer transition-colors w-full px-1 ${isActive ? "text-[#2D3142]" : "text-gray-400 hover:text-[#2D3142]"
|
|
135
163
|
}`}
|
|
136
164
|
>
|
|
137
165
|
{/* Clone the icon to dynamically apply styling based on active state */}
|
|
@@ -139,7 +167,7 @@ export function DashboardLayout({
|
|
|
139
167
|
size: 22,
|
|
140
168
|
className: isActive ? "stroke-[2.5]" : "stroke-[2]"
|
|
141
169
|
})}
|
|
142
|
-
<span className={`text-[11px] text-center leading-tight
|
|
170
|
+
<span className={`text-[11px] text-center leading-tight truncate block w-full max-w-[70px] ${isActive ? "font-bold" : "font-medium"}`}>{menu.label}</span>
|
|
143
171
|
</div>
|
|
144
172
|
);
|
|
145
173
|
})}
|
|
@@ -4,6 +4,7 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"
|
|
|
4
4
|
import { cn } from "@apptimate/core-lib";
|
|
5
5
|
import { CreditCard, Banknote, Building, Trash2, Loader2, Split, Check, AlertCircle, AlertTriangle, CheckCircle } from "lucide-react";
|
|
6
6
|
import { HintIcon } from "../base-components/HintIcon";
|
|
7
|
+
import { AsyncSearchableSelect } from "../base-components/SearchableSelect";
|
|
7
8
|
|
|
8
9
|
/* ── Types (mirroring sales POS types) ── */
|
|
9
10
|
|
|
@@ -63,7 +64,7 @@ export interface PaymentSectionProps {
|
|
|
63
64
|
/** Special mode codes that are allowed (e.g. 'credit-gold') */
|
|
64
65
|
allowedSpecialModes?: string[];
|
|
65
66
|
/** Fetcher for cheque leaves (for auto-complete) */
|
|
66
|
-
fetchChequeLeaves?: (query: string) => Promise<{ is_success: boolean; result?: any[] }>;
|
|
67
|
+
fetchChequeLeaves?: (query: string, page: number) => Promise<{ is_success: boolean; result?: any[] }>;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
/* ── Component ── */
|
|
@@ -328,7 +329,7 @@ interface PaymentEntryRowProps {
|
|
|
328
329
|
onUpdate: (updates: Partial<PaymentEntry>) => void;
|
|
329
330
|
onRemove: () => void;
|
|
330
331
|
showRemove?: boolean;
|
|
331
|
-
fetchChequeLeaves?: (query: string) => Promise<{ is_success: boolean; result?: any[] }>;
|
|
332
|
+
fetchChequeLeaves?: (query: string, page: number) => Promise<{ is_success: boolean; result?: any[] }>;
|
|
332
333
|
}
|
|
333
334
|
|
|
334
335
|
function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpdate, onRemove, showRemove = true, fetchChequeLeaves }: PaymentEntryRowProps) {
|
|
@@ -371,7 +372,12 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
|
|
|
371
372
|
{/* Dynamic sub-fields from field_config */}
|
|
372
373
|
{fields.length > 0 && (
|
|
373
374
|
<div className="grid grid-cols-2 gap-2.5">
|
|
374
|
-
{fields.map((field: PaymentModeField) =>
|
|
375
|
+
{fields.map((field: PaymentModeField) => {
|
|
376
|
+
if ((field.key === "bank_account_id" || field.key === "bank_name") && entry.mode_code === "cheque" && fetchChequeLeaves) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return (
|
|
375
381
|
<div key={field.key} className="flex flex-col gap-1">
|
|
376
382
|
<label className="text-[11px] text-foreground-subtle font-bold uppercase tracking-wider flex items-center">
|
|
377
383
|
{field.label}
|
|
@@ -409,20 +415,53 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
|
|
|
409
415
|
{materialTypes.map((m) => (<option key={m.id} value={m.id}>{m.name}</option>))}
|
|
410
416
|
</select>
|
|
411
417
|
) : field.key === "cheque_number" && fetchChequeLeaves ? (
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
if (
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
418
|
+
<>
|
|
419
|
+
<AsyncSearchableSelect
|
|
420
|
+
defaultValue={entry.metadata[field.key] ? { cheque_number: entry.metadata[field.key] } : undefined}
|
|
421
|
+
onChange={(val, selected: any) => {
|
|
422
|
+
const leafObj = Array.isArray(selected) ? selected[0] : selected;
|
|
423
|
+
const newMetadata = { ...entry.metadata, [field.key]: val, cheque_leaf_id: leafObj?.id || "" };
|
|
424
|
+
if (leafObj?.cheque_book?.bank_account) {
|
|
425
|
+
const ba = leafObj.cheque_book.bank_account;
|
|
426
|
+
newMetadata.bank_account_id = ba.id;
|
|
427
|
+
newMetadata._display_bank_name = ba.bank_name;
|
|
428
|
+
newMetadata._display_branch = ba.branch;
|
|
429
|
+
} else {
|
|
430
|
+
delete newMetadata.bank_account_id;
|
|
431
|
+
delete newMetadata._display_bank_name;
|
|
432
|
+
delete newMetadata._display_branch;
|
|
433
|
+
}
|
|
434
|
+
onUpdate({ metadata: newMetadata });
|
|
435
|
+
}}
|
|
436
|
+
loadOptions={async (search, page) => {
|
|
437
|
+
if (!fetchChequeLeaves) return [];
|
|
438
|
+
const res = await fetchChequeLeaves(search, page);
|
|
439
|
+
return res.is_success && res.result ? res.result : [];
|
|
440
|
+
}}
|
|
441
|
+
option={{
|
|
442
|
+
label: "cheque_number",
|
|
443
|
+
value: "cheque_number",
|
|
444
|
+
renderOption: (item: any) => (
|
|
445
|
+
<div className="flex flex-col">
|
|
446
|
+
<span>{item.cheque_number}</span>
|
|
447
|
+
{item.cheque_book?.bank_account?.bank_name && (
|
|
448
|
+
<span className="text-[11px] text-foreground-subtle mt-0.5">
|
|
449
|
+
{item.cheque_book.bank_account.bank_name} {item.cheque_book.bank_account.branch ? `— ${item.cheque_book.bank_account.branch}` : ""}
|
|
450
|
+
</span>
|
|
451
|
+
)}
|
|
452
|
+
</div>
|
|
453
|
+
)
|
|
454
|
+
}}
|
|
455
|
+
placeholder="Search cheque number..."
|
|
456
|
+
disabled={field.readonly}
|
|
457
|
+
/>
|
|
458
|
+
{entry.metadata._display_bank_name && (
|
|
459
|
+
<div className="text-[11px] text-foreground-subtle mt-1 flex items-center gap-1.5 font-medium px-1">
|
|
460
|
+
<Building size={12} className="text-foreground-disabled" />
|
|
461
|
+
{entry.metadata._display_bank_name} {entry.metadata._display_branch ? `— ${entry.metadata._display_branch}` : ""}
|
|
462
|
+
</div>
|
|
463
|
+
)}
|
|
464
|
+
</>
|
|
426
465
|
) : (
|
|
427
466
|
<input type={field.type === "number" ? "number" : field.type === "date" ? "date" : "text"}
|
|
428
467
|
{...(field.type === "number" ? { step: "any" } : {})}
|
|
@@ -432,113 +471,7 @@ function PaymentEntryRow({ entry, mode, bankAccounts, materialTypes = [], onUpda
|
|
|
432
471
|
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")} />
|
|
433
472
|
)}
|
|
434
473
|
</div>
|
|
435
|
-
))}
|
|
436
|
-
</div>
|
|
437
|
-
)}
|
|
438
|
-
</div>
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/* ── ChequeAutocomplete ── */
|
|
443
|
-
|
|
444
|
-
function ChequeAutocomplete({
|
|
445
|
-
value,
|
|
446
|
-
onChange,
|
|
447
|
-
fetchChequeLeaves,
|
|
448
|
-
readonly
|
|
449
|
-
}: {
|
|
450
|
-
value: string;
|
|
451
|
-
onChange: (val: string, leafId?: number, suggestion?: any) => void;
|
|
452
|
-
fetchChequeLeaves: (q: string) => Promise<{ is_success: boolean; result?: any[] }>;
|
|
453
|
-
readonly?: boolean;
|
|
454
|
-
}) {
|
|
455
|
-
const [query, setQuery] = useState(value);
|
|
456
|
-
const [suggestions, setSuggestions] = useState<any[]>([]);
|
|
457
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
458
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
459
|
-
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
460
|
-
|
|
461
|
-
useEffect(() => {
|
|
462
|
-
setQuery(value);
|
|
463
|
-
}, [value]);
|
|
464
|
-
|
|
465
|
-
useEffect(() => {
|
|
466
|
-
const handleClickOutside = (e: MouseEvent) => {
|
|
467
|
-
if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
|
|
468
|
-
setIsOpen(false);
|
|
469
|
-
}
|
|
470
|
-
};
|
|
471
|
-
document.addEventListener("mousedown", handleClickOutside);
|
|
472
|
-
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
473
|
-
}, []);
|
|
474
|
-
|
|
475
|
-
const fetchSuggestions = useCallback(async (search: string) => {
|
|
476
|
-
setIsLoading(true);
|
|
477
|
-
try {
|
|
478
|
-
const res = await fetchChequeLeaves(search);
|
|
479
|
-
if (res.is_success && res.result) {
|
|
480
|
-
setSuggestions(res.result);
|
|
481
|
-
setIsOpen(true);
|
|
482
|
-
}
|
|
483
|
-
} catch (e) {
|
|
484
|
-
console.error(e);
|
|
485
|
-
} finally {
|
|
486
|
-
setIsLoading(false);
|
|
487
|
-
}
|
|
488
|
-
}, [fetchChequeLeaves]);
|
|
489
|
-
|
|
490
|
-
useEffect(() => {
|
|
491
|
-
if (!isOpen) return;
|
|
492
|
-
const timer = setTimeout(() => {
|
|
493
|
-
fetchSuggestions(query);
|
|
494
|
-
}, 300);
|
|
495
|
-
return () => clearTimeout(timer);
|
|
496
|
-
}, [query, isOpen, fetchSuggestions]);
|
|
497
|
-
|
|
498
|
-
return (
|
|
499
|
-
<div ref={wrapperRef} className="relative">
|
|
500
|
-
<input
|
|
501
|
-
type="text"
|
|
502
|
-
value={query}
|
|
503
|
-
onChange={(e) => {
|
|
504
|
-
setQuery(e.target.value);
|
|
505
|
-
onChange(e.target.value, undefined, undefined); // reset ID when typed manually
|
|
506
|
-
setIsOpen(true);
|
|
507
|
-
}}
|
|
508
|
-
onFocus={() => {
|
|
509
|
-
if (!readonly) {
|
|
510
|
-
setIsOpen(true);
|
|
511
|
-
fetchSuggestions(query);
|
|
512
|
-
}
|
|
513
|
-
}}
|
|
514
|
-
readOnly={readonly}
|
|
515
|
-
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", readonly && "opacity-60 bg-surface-0 cursor-not-allowed border-border")}
|
|
516
|
-
placeholder="Type to search cheques..."
|
|
517
|
-
/>
|
|
518
|
-
{isLoading && (
|
|
519
|
-
<div className="absolute right-2 top-2.5">
|
|
520
|
-
<Loader2 size={14} className="animate-spin text-foreground-subtle" />
|
|
521
|
-
</div>
|
|
522
|
-
)}
|
|
523
|
-
{isOpen && suggestions.length > 0 && !readonly && (
|
|
524
|
-
<div className="absolute z-50 mt-1 w-full bg-surface-0 border border-border-subtle rounded-[8px] shadow-lg max-h-48 overflow-y-auto">
|
|
525
|
-
{suggestions.map((s) => (
|
|
526
|
-
<div
|
|
527
|
-
key={s.id}
|
|
528
|
-
className="px-3 py-2 text-[12px] text-foreground-1 hover:bg-primary/5 hover:text-primary cursor-pointer transition-colors"
|
|
529
|
-
onClick={() => {
|
|
530
|
-
setQuery(s.cheque_number);
|
|
531
|
-
onChange(s.cheque_number, s.id, s);
|
|
532
|
-
setIsOpen(false);
|
|
533
|
-
}}
|
|
534
|
-
>
|
|
535
|
-
<div className="font-semibold">{s.cheque_number}</div>
|
|
536
|
-
<div className="text-[10px] text-foreground-subtle flex items-center gap-2">
|
|
537
|
-
{s.cheque_book?.bank_account?.bank_name && <span>{s.cheque_book.bank_account.bank_name}</span>}
|
|
538
|
-
{s.amount && <span>Amount: {s.amount}</span>}
|
|
539
|
-
</div>
|
|
540
|
-
</div>
|
|
541
|
-
))}
|
|
474
|
+
)})}
|
|
542
475
|
</div>
|
|
543
476
|
)}
|
|
544
477
|
</div>
|