@apptimate/ui 5.6.0 → 5.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": "5.6.0",
3
+ "version": "5.7.0",
4
4
  "main": "src/index.tsx",
5
5
  "types": "src/index.tsx",
6
6
  "dependencies": {
@@ -63,6 +63,9 @@ export interface ButtonProps {
63
63
 
64
64
  /** Test ID for testing */
65
65
  testId?: string;
66
+
67
+ /** Tooltip text on hover */
68
+ title?: string;
66
69
  }
67
70
 
68
71
  const iconSizeMap = {
@@ -219,6 +222,7 @@ export const Button = ({
219
222
  prefix = 'aceui',
220
223
  testId,
221
224
  disableWave = false,
225
+ title,
222
226
  }: ButtonProps) => {
223
227
  const isIconOnly = Boolean(!children && icon);
224
228
  const isActuallyDisabled = isDisabled || isLoading;
@@ -334,6 +338,7 @@ export const Button = ({
334
338
  aria-label={ariaLabel}
335
339
  aria-busy={isLoading ? 'true' : undefined}
336
340
  data-testid={testId}
341
+ title={title}
337
342
  >
338
343
  {wave && (
339
344
  <motion.span
@@ -33,11 +33,13 @@ export const DesktopFilterPopover = ({
33
33
  <div className="relative">
34
34
  <Button
35
35
  variant="soft"
36
- color="default"
37
- className={cn("!p-0 text-gray-500", triggerClassName)}
36
+ color={hasActiveFilter ? 'primary' : 'default'}
37
+ className={cn(triggerClassName)}
38
38
  icon={<SlidersHorizontal size={18} strokeWidth={2} />}
39
39
  ariaLabel={label}
40
- />
40
+ >
41
+ {label}
42
+ </Button>
41
43
  {hasActiveFilter && (
42
44
  <span className="absolute -top-0.5 -right-0.5 w-2 h-2 bg-primary-500 rounded-full ring-2 ring-white" />
43
45
  )}
@@ -73,7 +73,9 @@ export function DashboardLayout({
73
73
 
74
74
  // Find which main menu should be active based on current path.
75
75
  // When basePath is set, prefer the menu whose items are mostly within basePath.
76
- const fullPath = basePath + (pathname === "/" && basePath ? "" : pathname);
76
+ const fullPath = (basePath && pathname.startsWith(basePath))
77
+ ? pathname
78
+ : basePath + (pathname === "/" && basePath ? "" : pathname);
77
79
  const currentMainMenu = (() => {
78
80
  let bestMenu = menus[0];
79
81
  let bestScore = -1;
@@ -211,15 +213,18 @@ export function DashboardLayout({
211
213
  setIsSubSidebarOpen(true);
212
214
  }
213
215
  }}
214
- 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]"
216
+ className={`flex flex-col items-center justify-center gap-1.5 cursor-pointer transition-all duration-200 w-full px-1 py-1 relative ${isActive ? "text-indigo-600" : "text-gray-400 hover:text-[#2D3142]"
215
217
  }`}
216
218
  >
219
+ {isActive && (
220
+ <span className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-indigo-600 rounded-r-full" />
221
+ )}
217
222
  {/* Clone the icon to dynamically apply styling based on active state */}
218
223
  {React.cloneElement(menu.icon as React.ReactElement<any>, {
219
224
  size: 22,
220
225
  className: isActive ? "stroke-[2.5]" : "stroke-[2]"
221
226
  })}
222
- <span className={`text-[11px] text-center leading-tight truncate block w-full max-w-[70px] ${isActive ? "font-bold" : "font-medium"}`}>{menu.label}</span>
227
+ <span className={`text-[11px] text-center leading-tight truncate block w-full max-w-[70px] ${isActive ? "font-bold text-indigo-700" : "font-medium"}`}>{menu.label}</span>
223
228
  </div>
224
229
  );
225
230
  })}
@@ -286,9 +291,9 @@ export function DashboardLayout({
286
291
  : !externalPaths.some(ext => item.path.startsWith(ext));
287
292
  const href = basePath && isInternal ? item.path.replace(basePath, "") || "/" : item.path;
288
293
 
289
- const className = `px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between ${isItemActive
290
- ? "bg-[#F4F5F7] text-[#2D3142] font-semibold"
291
- : "text-gray-500 font-medium hover:bg-gray-50"
294
+ const className = `pl-3 pr-4 py-2 rounded-r-lg rounded-l-none text-sm transition-all duration-200 flex items-center justify-between border-l-4 ${isItemActive
295
+ ? "bg-indigo-50/70 border-indigo-600 text-indigo-950 font-bold"
296
+ : "border-transparent text-gray-500 font-medium hover:bg-gray-50/50"
292
297
  }`;
293
298
 
294
299
  const content = (
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
 
3
- import React, { useState, useCallback, useEffect } from "react";
3
+ import React, { useState, useCallback, useEffect, useRef } from "react";
4
4
  import { Button } from '../../base-components/Button';
5
5
  import { Input } from '../../base-components/Input';
6
6
  import { Modal } from '../../base-components/Modal';
7
7
  import { ModalFooter } from '../../base-components/Modal';
8
+ import { AsyncSearchableSelect } from '../../base-components/SearchableSelect';
8
9
 
9
10
  import { Plus, User } from "lucide-react";
10
11
  import {
@@ -12,7 +13,7 @@ import {
12
13
  PickerItem,
13
14
  PickerTrigger,
14
15
  } from "./EntityPickerModal";
15
- import { lookupParties, createParty } from "@apptimate/core-lib";
16
+ import { lookupParties, createParty, sendRequest } from "@apptimate/core-lib";
16
17
  import toast from "react-hot-toast";
17
18
 
18
19
  interface PartyPickerProps {
@@ -52,9 +53,45 @@ export function PartyPicker({
52
53
  const [quickAddSecondaryContact, setQuickAddSecondaryContact] = useState("");
53
54
  const [quickAddGender, setQuickAddGender] = useState("");
54
55
  const [quickAddDOB, setQuickAddDOB] = useState("");
56
+ const [quickAddCurrencyId, setQuickAddCurrencyId] = useState<string | number | undefined>(undefined);
57
+ const [quickAddCurrencyDisplay, setQuickAddCurrencyDisplay] = useState<any>(null);
55
58
  const [errors, setErrors] = useState<Record<string, string[]>>({});
56
59
  const [isCreating, setIsCreating] = useState(false);
57
60
 
61
+ const hasInitializedQuickAddCurrency = useRef(false);
62
+
63
+ useEffect(() => {
64
+ if (isQuickAddOpen && !hasInitializedQuickAddCurrency.current) {
65
+ hasInitializedQuickAddCurrency.current = true;
66
+ sendRequest({ url: `${process.env.NEXT_PUBLIC_API_URL}/api/settings`, method: 'GET' })
67
+ .then((res: any) => {
68
+ if (res.responseData?.is_success && res.responseData?.result) {
69
+ const configs = Array.isArray(res.responseData.result) ? res.responseData.result : res.responseData.result.data;
70
+ const baseConfig = configs?.find((c: any) => c.key === 'base_currency');
71
+ if (baseConfig?.resolved_item) {
72
+ const currency = baseConfig.resolved_item;
73
+ setQuickAddCurrencyId(currency.id);
74
+ setQuickAddCurrencyDisplay({ id: currency.id, code: currency.code, displayLabel: `${currency.code} — ${currency.name}` });
75
+ } else if (baseConfig?.value) {
76
+ // Fallback just in case
77
+ const baseId = baseConfig.value;
78
+ sendRequest({ url: `${process.env.NEXT_PUBLIC_API_URL}/api/currencies`, method: 'GET', params: { search: '' } })
79
+ .then((cRes: any) => {
80
+ if (cRes.responseData?.is_success && cRes.responseData?.result?.data) {
81
+ const cData = Array.isArray(cRes.responseData.result.data) ? cRes.responseData.result.data : cRes.responseData.result;
82
+ const baseCurrency = cData.find((c: any) => String(c.id) === String(baseId));
83
+ if (baseCurrency) {
84
+ setQuickAddCurrencyId(baseCurrency.id);
85
+ setQuickAddCurrencyDisplay({ id: baseCurrency.id, code: baseCurrency.code, displayLabel: `${baseCurrency.code} — ${baseCurrency.name}` });
86
+ }
87
+ }
88
+ });
89
+ }
90
+ }
91
+ });
92
+ }
93
+ }, [isQuickAddOpen]);
94
+
58
95
  const fetchData = useCallback(async (query: string = "") => {
59
96
  setIsLoading(true);
60
97
  try {
@@ -104,6 +141,7 @@ export function PartyPicker({
104
141
  secondary_contact: quickAddSecondaryContact.trim() || undefined,
105
142
  gender: quickAddGender || undefined,
106
143
  date_of_birth: quickAddDOB || undefined,
144
+ default_currency_id: quickAddCurrencyId || undefined,
107
145
  type: [partyType === "all" ? "customer" : partyType],
108
146
  status: "active"
109
147
  };
@@ -268,20 +306,49 @@ export function PartyPicker({
268
306
  error={errors.date_of_birth?.[0]}
269
307
  />
270
308
  </div>
271
- <div className="space-y-1">
272
- <label className="text-sm font-semibold text-gray-700">Gender</label>
273
- <select
274
- value={quickAddGender}
275
- onChange={(e) => setQuickAddGender(e.target.value)}
276
- className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white"
277
- >
278
- <option value="">Select gender</option>
279
- <option value="male">Male</option>
280
- <option value="female">Female</option>
281
- <option value="other">Other</option>
282
- <option value="prefer_not_to_say">Prefer not to say</option>
283
- </select>
284
- {errors.gender?.[0] && <p className="text-xs text-red-500">{errors.gender[0]}</p>}
309
+ <div className="grid grid-cols-2 gap-3">
310
+ <div className="space-y-1">
311
+ <label className="text-sm font-semibold text-gray-700">Gender</label>
312
+ <select
313
+ value={quickAddGender}
314
+ onChange={(e) => setQuickAddGender(e.target.value)}
315
+ className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white"
316
+ >
317
+ <option value="">Select gender</option>
318
+ <option value="male">Male</option>
319
+ <option value="female">Female</option>
320
+ <option value="other">Other</option>
321
+ <option value="prefer_not_to_say">Prefer not to say</option>
322
+ </select>
323
+ {errors.gender?.[0] && <p className="text-xs text-red-500">{errors.gender[0]}</p>}
324
+ </div>
325
+ {(partyType === "all" || partyType === "customer" || partyType === "supplier") && (
326
+ <AsyncSearchableSelect
327
+ label="Default Currency"
328
+ placeholder="Search currency..."
329
+ loadOptions={async (search, page) => {
330
+ try {
331
+ const res: any = await sendRequest({
332
+ url: `${process.env.NEXT_PUBLIC_API_URL}/api/currencies`,
333
+ method: 'GET',
334
+ params: { search, page, per_page: 20 }
335
+ });
336
+ if (res.responseData?.is_success && res.responseData?.result?.data) {
337
+ return res.responseData.result.data.map((c: any) => ({
338
+ ...c,
339
+ displayLabel: `${c.code} — ${c.name}`
340
+ }));
341
+ }
342
+ return [];
343
+ } catch { return []; }
344
+ }}
345
+ option={{ label: "displayLabel", value: "id", keysToSearch: ["name", "code"] }}
346
+ onChange={(val) => setQuickAddCurrencyId(val as string | number)}
347
+ key={quickAddCurrencyDisplay ? quickAddCurrencyDisplay.code : 'empty'}
348
+ defaultValue={quickAddCurrencyDisplay || undefined}
349
+ error={errors.default_currency_id?.[0]}
350
+ />
351
+ )}
285
352
  </div>
286
353
  <ModalFooter>
287
354
  <Button