@apptimate/ui 5.5.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 {
|
|
@@ -49,8 +50,48 @@ export function PartyPicker({
|
|
|
49
50
|
const [quickAddLastName, setQuickAddLastName] = useState("");
|
|
50
51
|
const [quickAddEmail, setQuickAddEmail] = useState("");
|
|
51
52
|
const [quickAddPhone, setQuickAddPhone] = useState("");
|
|
53
|
+
const [quickAddSecondaryContact, setQuickAddSecondaryContact] = useState("");
|
|
54
|
+
const [quickAddGender, setQuickAddGender] = useState("");
|
|
55
|
+
const [quickAddDOB, setQuickAddDOB] = useState("");
|
|
56
|
+
const [quickAddCurrencyId, setQuickAddCurrencyId] = useState<string | number | undefined>(undefined);
|
|
57
|
+
const [quickAddCurrencyDisplay, setQuickAddCurrencyDisplay] = useState<any>(null);
|
|
58
|
+
const [errors, setErrors] = useState<Record<string, string[]>>({});
|
|
52
59
|
const [isCreating, setIsCreating] = useState(false);
|
|
53
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
|
+
|
|
54
95
|
const fetchData = useCallback(async (query: string = "") => {
|
|
55
96
|
setIsLoading(true);
|
|
56
97
|
try {
|
|
@@ -90,12 +131,17 @@ export function PartyPicker({
|
|
|
90
131
|
const handleQuickAdd = async () => {
|
|
91
132
|
if (!quickAddFirstName.trim()) return;
|
|
92
133
|
setIsCreating(true);
|
|
134
|
+
setErrors({});
|
|
93
135
|
try {
|
|
94
136
|
const payload = {
|
|
95
137
|
first_name: quickAddFirstName.trim(),
|
|
96
138
|
last_name: quickAddLastName.trim() || undefined,
|
|
97
139
|
email: quickAddEmail.trim() || undefined,
|
|
98
140
|
primary_contact: quickAddPhone.trim() || undefined,
|
|
141
|
+
secondary_contact: quickAddSecondaryContact.trim() || undefined,
|
|
142
|
+
gender: quickAddGender || undefined,
|
|
143
|
+
date_of_birth: quickAddDOB || undefined,
|
|
144
|
+
default_currency_id: quickAddCurrencyId || undefined,
|
|
99
145
|
type: [partyType === "all" ? "customer" : partyType],
|
|
100
146
|
status: "active"
|
|
101
147
|
};
|
|
@@ -110,7 +156,13 @@ export function PartyPicker({
|
|
|
110
156
|
setQuickAddLastName("");
|
|
111
157
|
setQuickAddEmail("");
|
|
112
158
|
setQuickAddPhone("");
|
|
159
|
+
setQuickAddSecondaryContact("");
|
|
160
|
+
setQuickAddGender("");
|
|
161
|
+
setQuickAddDOB("");
|
|
162
|
+
setErrors({});
|
|
113
163
|
setIsOpen(false);
|
|
164
|
+
} else if ((res as any).errors) {
|
|
165
|
+
setErrors((res as any).errors);
|
|
114
166
|
} else {
|
|
115
167
|
toast.error(res.message || "Failed to create");
|
|
116
168
|
}
|
|
@@ -157,6 +209,10 @@ export function PartyPicker({
|
|
|
157
209
|
setQuickAddLastName("");
|
|
158
210
|
setQuickAddEmail("");
|
|
159
211
|
setQuickAddPhone("");
|
|
212
|
+
setQuickAddSecondaryContact("");
|
|
213
|
+
setQuickAddGender("");
|
|
214
|
+
setQuickAddDOB("");
|
|
215
|
+
setErrors({});
|
|
160
216
|
}}
|
|
161
217
|
className="p-1.5 rounded-lg bg-primary-50 text-primary-600 hover:bg-primary-100 transition-colors flex-shrink-0"
|
|
162
218
|
title={`Quick Add ${label}`}
|
|
@@ -207,12 +263,14 @@ export function PartyPicker({
|
|
|
207
263
|
placeholder={`Enter ${label.toLowerCase()} first name`}
|
|
208
264
|
value={quickAddFirstName}
|
|
209
265
|
onChange={(e) => setQuickAddFirstName(e.target.value)}
|
|
266
|
+
error={errors.first_name?.[0]}
|
|
210
267
|
/>
|
|
211
268
|
<Input
|
|
212
269
|
label="Last Name"
|
|
213
270
|
placeholder={`Enter ${label.toLowerCase()} last name`}
|
|
214
271
|
value={quickAddLastName}
|
|
215
272
|
onChange={(e) => setQuickAddLastName(e.target.value)}
|
|
273
|
+
error={errors.last_name?.[0]}
|
|
216
274
|
/>
|
|
217
275
|
</div>
|
|
218
276
|
<div className="grid grid-cols-2 gap-3">
|
|
@@ -222,14 +280,76 @@ export function PartyPicker({
|
|
|
222
280
|
placeholder="Email address"
|
|
223
281
|
value={quickAddEmail}
|
|
224
282
|
onChange={(e) => setQuickAddEmail(e.target.value)}
|
|
283
|
+
error={errors.email?.[0]}
|
|
225
284
|
/>
|
|
226
285
|
<Input
|
|
227
|
-
label="
|
|
228
|
-
placeholder="
|
|
286
|
+
label="Primary Contact"
|
|
287
|
+
placeholder="Primary phone number"
|
|
229
288
|
value={quickAddPhone}
|
|
230
289
|
onChange={(e) => setQuickAddPhone(e.target.value)}
|
|
290
|
+
error={errors.primary_contact?.[0]}
|
|
231
291
|
/>
|
|
232
292
|
</div>
|
|
293
|
+
<div className="grid grid-cols-2 gap-3">
|
|
294
|
+
<Input
|
|
295
|
+
label="Secondary Contact"
|
|
296
|
+
placeholder="Alternative phone (optional)"
|
|
297
|
+
value={quickAddSecondaryContact}
|
|
298
|
+
onChange={(e) => setQuickAddSecondaryContact(e.target.value)}
|
|
299
|
+
error={errors.secondary_contact?.[0]}
|
|
300
|
+
/>
|
|
301
|
+
<Input
|
|
302
|
+
label="Date of Birth"
|
|
303
|
+
type="date"
|
|
304
|
+
value={quickAddDOB}
|
|
305
|
+
onChange={(e) => setQuickAddDOB(e.target.value)}
|
|
306
|
+
error={errors.date_of_birth?.[0]}
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
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
|
+
)}
|
|
352
|
+
</div>
|
|
233
353
|
<ModalFooter>
|
|
234
354
|
<Button
|
|
235
355
|
variant="flat"
|