@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,6 +1,6 @@
1
1
  {
2
2
  "name": "@apptimate/ui",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "main": "src/index.tsx",
5
5
  "types": "src/index.tsx",
6
6
  "dependencies": {
@@ -21,4 +21,4 @@
21
21
  "peerDependencies": {
22
22
  "next": "^16.0.0"
23
23
  }
24
- }
24
+ }
@@ -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
- <nav className="flex flex-col gap-6 flex-1 w-full">
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 px-1 ${isActive ? "font-bold" : "font-medium"}`}>{menu.label}</span>
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
- <ChequeAutocomplete
413
- value={String(entry.metadata[field.key] || "")}
414
- onChange={(val, leafId, suggestion) => {
415
- const newMetadata = { ...entry.metadata, [field.key]: val, cheque_leaf_id: leafId || "" };
416
- if (suggestion?.cheque_book?.bank_account) {
417
- const ba = suggestion.cheque_book.bank_account;
418
- if (ba.bank_name) newMetadata.bank_name = ba.bank_name;
419
- if (ba.branch) newMetadata.branch = ba.branch;
420
- }
421
- onUpdate({ metadata: newMetadata });
422
- }}
423
- fetchChequeLeaves={fetchChequeLeaves}
424
- readonly={field.readonly}
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>