@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
|
@@ -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=
|
|
37
|
-
className={cn(
|
|
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
|
|
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-
|
|
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 = `
|
|
290
|
-
? "bg-
|
|
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="
|
|
272
|
-
<
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|