@apptimate/ui 7.2.0 → 7.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 +1 -1
- package/src/base-components/FormattedNumberInput.tsx +118 -0
- package/src/base-components/Select.tsx +1 -1
- package/src/common-components/DashboardLayout.tsx +101 -57
- package/src/common-components/PaymentSection.tsx +134 -130
- package/src/common-components/item-wizard/ItemFormWizard.tsx +118 -60
- package/src/common-components/pickers/PartyPicker.tsx +28 -401
- package/src/common-components/pickers/QuickPartyAddModal.tsx +414 -0
- package/src/common-components/pickers/UomPicker.tsx +20 -20
- package/src/common-components/pickers/modals/BatchSelectionModal.tsx +167 -114
- package/src/common-components/transaction/ProductSelectionPanel.tsx +6 -4
- package/src/common-components/transaction/ProductTransactionScreen.tsx +3 -3
- package/src/common-components/transaction/types.ts +2 -0
- package/src/components/shared/ImageUploadComponent.tsx +3 -0
- package/src/finance-components/DirectTransactionDetailModal.tsx +9 -1
- package/src/index.tsx +2 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useRef, useEffect } from "react";
|
|
4
|
+
import { Modal, ModalFooter } from "../../base-components/Modal";
|
|
5
|
+
import { Input } from "../../base-components/Input";
|
|
6
|
+
import { Button } from "../../base-components/Button";
|
|
7
|
+
import { AsyncSearchableSelect } from "../../base-components/SearchableSelect";
|
|
8
|
+
import { createParty, sendRequest } from "@apptimate/core-lib";
|
|
9
|
+
import toast from "react-hot-toast";
|
|
10
|
+
|
|
11
|
+
interface QuickPartyAddModalProps {
|
|
12
|
+
isOpen: boolean;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
onSuccess?: (party: any) => void;
|
|
15
|
+
partyType?: "customer" | "supplier" | "all" | "artisan" | "other" | "employee" | "salesperson";
|
|
16
|
+
createAsType?: "customer" | "supplier" | "artisan" | "other" | "employee" | "salesperson";
|
|
17
|
+
label?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function QuickPartyAddModal({
|
|
21
|
+
isOpen,
|
|
22
|
+
onClose,
|
|
23
|
+
onSuccess,
|
|
24
|
+
partyType = "all",
|
|
25
|
+
createAsType,
|
|
26
|
+
label = "Party"
|
|
27
|
+
}: QuickPartyAddModalProps) {
|
|
28
|
+
const [firstName, setFirstName] = useState("");
|
|
29
|
+
const [lastName, setLastName] = useState("");
|
|
30
|
+
const [email, setEmail] = useState("");
|
|
31
|
+
const [phone, setPhone] = useState("");
|
|
32
|
+
const [secondaryContact, setSecondaryContact] = useState("");
|
|
33
|
+
const [dob, setDob] = useState("");
|
|
34
|
+
const [identityType, setIdentityType] = useState("");
|
|
35
|
+
const [identity, setIdentity] = useState("");
|
|
36
|
+
|
|
37
|
+
const [addressLine1, setAddressLine1] = useState("");
|
|
38
|
+
const [addressLine2, setAddressLine2] = useState("");
|
|
39
|
+
const [city, setCity] = useState("");
|
|
40
|
+
const [state, setState] = useState("");
|
|
41
|
+
const [postalCode, setPostalCode] = useState("");
|
|
42
|
+
const [country, setCountry] = useState("");
|
|
43
|
+
const [showDetailedAddress, setShowDetailedAddress] = useState(false);
|
|
44
|
+
|
|
45
|
+
const [currencyId, setCurrencyId] = useState<string | number | undefined>(undefined);
|
|
46
|
+
const [currencyDisplay, setCurrencyDisplay] = useState<any>(null);
|
|
47
|
+
const [errors, setErrors] = useState<Record<string, string[]>>({});
|
|
48
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
49
|
+
|
|
50
|
+
const [fieldConfigs, setFieldConfigs] = useState<any[]>([]);
|
|
51
|
+
|
|
52
|
+
const effectiveType = createAsType || (partyType === "all" ? "customer" : partyType);
|
|
53
|
+
|
|
54
|
+
const mergedConfig = React.useMemo(() => {
|
|
55
|
+
const configMap: Record<string, { active: boolean; mandatory: boolean }> = {};
|
|
56
|
+
const relevantConfigs = fieldConfigs.filter(c => c.entity === effectiveType);
|
|
57
|
+
for (const config of relevantConfigs) {
|
|
58
|
+
if (!configMap[config.field]) {
|
|
59
|
+
configMap[config.field] = { active: false, mandatory: false };
|
|
60
|
+
}
|
|
61
|
+
if (config.status === "active") configMap[config.field].active = true;
|
|
62
|
+
if (config.is_mandatory) configMap[config.field].mandatory = true;
|
|
63
|
+
}
|
|
64
|
+
return configMap;
|
|
65
|
+
}, [fieldConfigs, effectiveType]);
|
|
66
|
+
|
|
67
|
+
const hasInitializedCurrency = useRef(false);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (isOpen) {
|
|
71
|
+
if (!hasInitializedCurrency.current) {
|
|
72
|
+
hasInitializedCurrency.current = true;
|
|
73
|
+
sendRequest({ url: `${process.env.NEXT_PUBLIC_API_URL}/api/settings`, method: 'GET' })
|
|
74
|
+
.then((res: any) => {
|
|
75
|
+
if (res.responseData?.is_success && res.responseData?.result) {
|
|
76
|
+
const configs = Array.isArray(res.responseData.result) ? res.responseData.result : res.responseData.result.data;
|
|
77
|
+
const baseConfig = configs?.find((c: any) => c.key === 'base_currency');
|
|
78
|
+
if (baseConfig?.resolved_item) {
|
|
79
|
+
const currency = baseConfig.resolved_item;
|
|
80
|
+
setCurrencyId(currency.id);
|
|
81
|
+
setCurrencyDisplay({ id: currency.id, code: currency.code, displayLabel: `${currency.code} — ${currency.name}` });
|
|
82
|
+
} else if (baseConfig?.value) {
|
|
83
|
+
const baseId = baseConfig.value;
|
|
84
|
+
sendRequest({ url: `${process.env.NEXT_PUBLIC_API_URL}/api/currencies`, method: 'GET', params: { search: '' } })
|
|
85
|
+
.then((cRes: any) => {
|
|
86
|
+
if (cRes.responseData?.is_success && cRes.responseData?.result?.data) {
|
|
87
|
+
const cData = Array.isArray(cRes.responseData.result.data) ? cRes.responseData.result.data : cRes.responseData.result;
|
|
88
|
+
const baseCurrency = cData.find((c: any) => String(c.id) === String(baseId));
|
|
89
|
+
if (baseCurrency) {
|
|
90
|
+
setCurrencyId(baseCurrency.id);
|
|
91
|
+
setCurrencyDisplay({
|
|
92
|
+
id: baseCurrency.id,
|
|
93
|
+
code: baseCurrency.code,
|
|
94
|
+
name: baseCurrency.name || "",
|
|
95
|
+
symbol: baseCurrency.symbol || "",
|
|
96
|
+
displayLabel: `${baseCurrency.code}${baseCurrency.name ? ` — ${baseCurrency.name}` : ''}`
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (fieldConfigs.length === 0) {
|
|
107
|
+
sendRequest({ url: `${process.env.NEXT_PUBLIC_API_URL}/api/field-configs`, method: 'GET' })
|
|
108
|
+
.then((res: any) => {
|
|
109
|
+
if (res.responseData?.is_success && res.responseData?.result) {
|
|
110
|
+
setFieldConfigs(res.responseData.result);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}, [isOpen, fieldConfigs.length]);
|
|
116
|
+
|
|
117
|
+
const handleQuickAdd = async () => {
|
|
118
|
+
if (!firstName.trim()) return;
|
|
119
|
+
setIsCreating(true);
|
|
120
|
+
setErrors({});
|
|
121
|
+
try {
|
|
122
|
+
const payload = {
|
|
123
|
+
first_name: firstName.trim(),
|
|
124
|
+
last_name: lastName.trim() || undefined,
|
|
125
|
+
email: email.trim() || undefined,
|
|
126
|
+
primary_contact: phone.trim() || undefined,
|
|
127
|
+
secondary_contact: secondaryContact.trim() || undefined,
|
|
128
|
+
date_of_birth: dob || undefined,
|
|
129
|
+
identity_type: identityType || undefined,
|
|
130
|
+
identity: identity.trim() || undefined,
|
|
131
|
+
default_currency_id: currencyId || undefined,
|
|
132
|
+
type: [createAsType || (partyType === "all" || !partyType ? "customer" : partyType)],
|
|
133
|
+
status: "active",
|
|
134
|
+
address: addressLine1.trim() ? {
|
|
135
|
+
address_line_1: addressLine1.trim(),
|
|
136
|
+
address_line_2: addressLine2.trim() || undefined,
|
|
137
|
+
city: city.trim() || undefined,
|
|
138
|
+
state: state.trim() || undefined,
|
|
139
|
+
postal_code: postalCode.trim() || undefined,
|
|
140
|
+
country: country.trim() || undefined,
|
|
141
|
+
} : undefined
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const res = await createParty(payload);
|
|
145
|
+
if (res.is_success && res.result) {
|
|
146
|
+
toast.success("Party created");
|
|
147
|
+
const party = res.result;
|
|
148
|
+
|
|
149
|
+
// Reset form
|
|
150
|
+
setFirstName("");
|
|
151
|
+
setLastName("");
|
|
152
|
+
setEmail("");
|
|
153
|
+
setPhone("");
|
|
154
|
+
setSecondaryContact("");
|
|
155
|
+
setDob("");
|
|
156
|
+
setIdentityType("");
|
|
157
|
+
setIdentity("");
|
|
158
|
+
setAddressLine1("");
|
|
159
|
+
setAddressLine2("");
|
|
160
|
+
setCity("");
|
|
161
|
+
setState("");
|
|
162
|
+
setPostalCode("");
|
|
163
|
+
setCountry("");
|
|
164
|
+
setShowDetailedAddress(false);
|
|
165
|
+
setErrors({});
|
|
166
|
+
|
|
167
|
+
if (onSuccess) {
|
|
168
|
+
onSuccess({ ...party, currencyDisplay });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
onClose();
|
|
172
|
+
} else if ((res as any).errors) {
|
|
173
|
+
setErrors((res as any).errors);
|
|
174
|
+
} else {
|
|
175
|
+
toast.error(res.message || "Failed to create");
|
|
176
|
+
}
|
|
177
|
+
} catch (e: any) {
|
|
178
|
+
toast.error(e.message || "Error creating party");
|
|
179
|
+
}
|
|
180
|
+
setIsCreating(false);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<Modal
|
|
185
|
+
isOpen={isOpen}
|
|
186
|
+
onClose={onClose}
|
|
187
|
+
title={`Quick Add ${label}`}
|
|
188
|
+
size="md"
|
|
189
|
+
zIndex={300}
|
|
190
|
+
backdrop="blur"
|
|
191
|
+
>
|
|
192
|
+
<div className="space-y-4">
|
|
193
|
+
<div className="grid grid-cols-2 gap-3">
|
|
194
|
+
{mergedConfig.first_name?.active !== false && (
|
|
195
|
+
<Input
|
|
196
|
+
label="First Name"
|
|
197
|
+
isRequired={mergedConfig.first_name?.mandatory ?? true}
|
|
198
|
+
placeholder={`Enter ${label.toLowerCase()} first name`}
|
|
199
|
+
value={firstName}
|
|
200
|
+
onChange={(e) => setFirstName(e.target.value)}
|
|
201
|
+
error={errors.first_name?.[0]}
|
|
202
|
+
/>
|
|
203
|
+
)}
|
|
204
|
+
{mergedConfig.last_name?.active !== false && (
|
|
205
|
+
<Input
|
|
206
|
+
label="Last Name"
|
|
207
|
+
isRequired={mergedConfig.last_name?.mandatory}
|
|
208
|
+
placeholder={`Enter ${label.toLowerCase()} last name`}
|
|
209
|
+
value={lastName}
|
|
210
|
+
onChange={(e) => setLastName(e.target.value)}
|
|
211
|
+
error={errors.last_name?.[0]}
|
|
212
|
+
/>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
<div className="grid grid-cols-2 gap-3">
|
|
216
|
+
{mergedConfig.email?.active !== false && (
|
|
217
|
+
<Input
|
|
218
|
+
label="Email"
|
|
219
|
+
type="email"
|
|
220
|
+
isRequired={mergedConfig.email?.mandatory}
|
|
221
|
+
placeholder="Email address"
|
|
222
|
+
value={email}
|
|
223
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
224
|
+
error={errors.email?.[0]}
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
{mergedConfig.primary_contact?.active !== false && (
|
|
228
|
+
<Input
|
|
229
|
+
label="Primary Contact"
|
|
230
|
+
isRequired={mergedConfig.primary_contact?.mandatory}
|
|
231
|
+
placeholder="Primary phone number"
|
|
232
|
+
value={phone}
|
|
233
|
+
onChange={(e) => setPhone(e.target.value)}
|
|
234
|
+
error={errors.primary_contact?.[0]}
|
|
235
|
+
/>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
<div className="grid grid-cols-2 gap-3">
|
|
239
|
+
{mergedConfig.secondary_contact?.active !== false && (
|
|
240
|
+
<Input
|
|
241
|
+
label="Secondary Contact"
|
|
242
|
+
isRequired={mergedConfig.secondary_contact?.mandatory}
|
|
243
|
+
placeholder="Alternative phone (optional)"
|
|
244
|
+
value={secondaryContact}
|
|
245
|
+
onChange={(e) => setSecondaryContact(e.target.value)}
|
|
246
|
+
error={errors.secondary_contact?.[0]}
|
|
247
|
+
/>
|
|
248
|
+
)}
|
|
249
|
+
{mergedConfig.date_of_birth?.active !== false && (
|
|
250
|
+
<Input
|
|
251
|
+
label="Date of Birth"
|
|
252
|
+
type="date"
|
|
253
|
+
isRequired={mergedConfig.date_of_birth?.mandatory}
|
|
254
|
+
value={dob}
|
|
255
|
+
onChange={(e) => setDob(e.target.value)}
|
|
256
|
+
error={errors.date_of_birth?.[0]}
|
|
257
|
+
/>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
{mergedConfig.identity?.active !== false && (
|
|
261
|
+
<div className="space-y-1.5 w-full">
|
|
262
|
+
<label className="text-[13px] font-medium text-[#4A5568]">
|
|
263
|
+
Identity Details {mergedConfig.identity?.mandatory && <span className="text-red-500">*</span>}
|
|
264
|
+
</label>
|
|
265
|
+
<div className="flex relative">
|
|
266
|
+
<select
|
|
267
|
+
value={identityType}
|
|
268
|
+
onChange={(e) => setIdentityType(e.target.value)}
|
|
269
|
+
className={`w-1/3 bg-surface-0 border-[1.5px] ${errors.identity_type ? 'border-danger-alt' : 'border-border-subtle'} rounded-l-[10px] px-3.5 py-2.5 text-[13.5px] text-foreground-1 outline-none transition-all hover:border-gray-300 focus:border-gray-300 focus:bg-surface-1 border-r-0 focus:z-10 appearance-none`}
|
|
270
|
+
style={{ backgroundImage: 'url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23131313%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right .7rem top 50%', backgroundSize: '.65rem auto' }}
|
|
271
|
+
>
|
|
272
|
+
<option value="">Select type</option>
|
|
273
|
+
<option value="nic">NIC</option>
|
|
274
|
+
<option value="passport">Passport</option>
|
|
275
|
+
<option value="driving_license">Driving License</option>
|
|
276
|
+
<option value="business_registration">Business Registration</option>
|
|
277
|
+
<option value="other">Other</option>
|
|
278
|
+
</select>
|
|
279
|
+
<input
|
|
280
|
+
type="text"
|
|
281
|
+
placeholder="Enter identity number"
|
|
282
|
+
value={identity}
|
|
283
|
+
onChange={(e) => setIdentity(e.target.value)}
|
|
284
|
+
className={`flex-1 bg-surface-0 border-[1.5px] ${errors.identity ? 'border-danger-alt' : 'border-border-subtle'} rounded-r-[10px] px-3.5 py-2.5 text-[13.5px] text-foreground-1 outline-none transition-all hover:border-gray-300 focus:border-gray-300 focus:bg-surface-1 placeholder:text-foreground-disabled z-0 focus:z-10 relative`}
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
{(errors.identity_type?.[0] || errors.identity?.[0]) && (
|
|
288
|
+
<span className="text-[11px] text-danger-alt font-medium">
|
|
289
|
+
{errors.identity_type?.[0] || errors.identity?.[0]}
|
|
290
|
+
</span>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
{mergedConfig.address?.active !== false && (
|
|
295
|
+
<div className="space-y-3">
|
|
296
|
+
<div className="flex justify-between items-center">
|
|
297
|
+
<label className="text-[13px] font-medium text-[#4A5568]">
|
|
298
|
+
Address {mergedConfig.address?.mandatory ? <span className="text-red-500">*</span> : "(Optional)"}
|
|
299
|
+
</label>
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
onClick={() => setShowDetailedAddress(!showDetailedAddress)}
|
|
303
|
+
className="text-xs font-semibold text-primary-600 hover:text-primary-700 transition-colors"
|
|
304
|
+
>
|
|
305
|
+
{showDetailedAddress ? "Quick Add Address" : "Add Detailed Address"}
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
{!showDetailedAddress ? (
|
|
309
|
+
<Input
|
|
310
|
+
placeholder="Enter complete primary address..."
|
|
311
|
+
value={addressLine1}
|
|
312
|
+
onChange={(e) => setAddressLine1(e.target.value)}
|
|
313
|
+
error={errors.address?.[0] || errors['address.address_line_1']?.[0]}
|
|
314
|
+
/>
|
|
315
|
+
) : (
|
|
316
|
+
<div className="p-3 bg-gray-50 border border-gray-100 rounded-xl space-y-3">
|
|
317
|
+
<Input
|
|
318
|
+
label="Address Line 1"
|
|
319
|
+
placeholder="Street address, P.O. box, etc."
|
|
320
|
+
value={addressLine1}
|
|
321
|
+
onChange={(e) => setAddressLine1(e.target.value)}
|
|
322
|
+
error={errors.address?.[0] || errors['address.address_line_1']?.[0]}
|
|
323
|
+
/>
|
|
324
|
+
<Input
|
|
325
|
+
label="Address Line 2"
|
|
326
|
+
placeholder="Apartment, suite, unit, etc. (optional)"
|
|
327
|
+
value={addressLine2}
|
|
328
|
+
onChange={(e) => setAddressLine2(e.target.value)}
|
|
329
|
+
error={errors['address.address_line_2']?.[0]}
|
|
330
|
+
/>
|
|
331
|
+
<div className="grid grid-cols-2 gap-3">
|
|
332
|
+
<Input
|
|
333
|
+
label="City"
|
|
334
|
+
placeholder="City"
|
|
335
|
+
value={city}
|
|
336
|
+
onChange={(e) => setCity(e.target.value)}
|
|
337
|
+
error={errors['address.city']?.[0]}
|
|
338
|
+
/>
|
|
339
|
+
<Input
|
|
340
|
+
label="State / Province"
|
|
341
|
+
placeholder="State or Province"
|
|
342
|
+
value={state}
|
|
343
|
+
onChange={(e) => setState(e.target.value)}
|
|
344
|
+
error={errors['address.state']?.[0]}
|
|
345
|
+
/>
|
|
346
|
+
</div>
|
|
347
|
+
<div className="grid grid-cols-2 gap-3">
|
|
348
|
+
<Input
|
|
349
|
+
label="Postal Code"
|
|
350
|
+
placeholder="ZIP or Postal Code"
|
|
351
|
+
value={postalCode}
|
|
352
|
+
onChange={(e) => setPostalCode(e.target.value)}
|
|
353
|
+
error={errors['address.postal_code']?.[0]}
|
|
354
|
+
/>
|
|
355
|
+
<Input
|
|
356
|
+
label="Country"
|
|
357
|
+
placeholder="Country"
|
|
358
|
+
value={country}
|
|
359
|
+
onChange={(e) => setCountry(e.target.value)}
|
|
360
|
+
error={errors['address.country']?.[0]}
|
|
361
|
+
/>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
)}
|
|
367
|
+
<div className="grid grid-cols-2 gap-3">
|
|
368
|
+
{(partyType === "all" || partyType === "customer" || partyType === "supplier" || partyType === "other") && (
|
|
369
|
+
<AsyncSearchableSelect
|
|
370
|
+
label="Default Currency"
|
|
371
|
+
placeholder="Search currency..."
|
|
372
|
+
loadOptions={async (search, page) => {
|
|
373
|
+
try {
|
|
374
|
+
const res: any = await sendRequest({
|
|
375
|
+
url: `${process.env.NEXT_PUBLIC_API_URL}/api/currencies`,
|
|
376
|
+
method: 'GET',
|
|
377
|
+
params: { search, page, per_page: 20 }
|
|
378
|
+
});
|
|
379
|
+
if (res.responseData?.is_success && res.responseData?.result?.data) {
|
|
380
|
+
return res.responseData.result.data.map((c: any) => ({
|
|
381
|
+
...c,
|
|
382
|
+
displayLabel: `${c.code} — ${c.name}`
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
return [];
|
|
386
|
+
} catch { return []; }
|
|
387
|
+
}}
|
|
388
|
+
option={{ label: "displayLabel", value: "id", keysToSearch: ["name", "code"] }}
|
|
389
|
+
onChange={(val, selectedObj: any) => {
|
|
390
|
+
setCurrencyId(val as string | number);
|
|
391
|
+
setCurrencyDisplay(selectedObj);
|
|
392
|
+
}}
|
|
393
|
+
key={currencyDisplay ? currencyDisplay.code : 'empty'}
|
|
394
|
+
defaultValue={currencyDisplay || undefined}
|
|
395
|
+
error={errors.default_currency_id?.[0]}
|
|
396
|
+
/>
|
|
397
|
+
)}
|
|
398
|
+
</div>
|
|
399
|
+
<ModalFooter>
|
|
400
|
+
<Button
|
|
401
|
+
variant="flat"
|
|
402
|
+
color="default"
|
|
403
|
+
onClick={onClose}
|
|
404
|
+
>
|
|
405
|
+
Cancel
|
|
406
|
+
</Button>
|
|
407
|
+
<Button onClick={handleQuickAdd} isDisabled={isCreating || !firstName.trim()}>
|
|
408
|
+
{isCreating ? "Creating…" : "Create & Select"}
|
|
409
|
+
</Button>
|
|
410
|
+
</ModalFooter>
|
|
411
|
+
</div>
|
|
412
|
+
</Modal>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
@@ -28,6 +28,7 @@ interface UomPickerProps {
|
|
|
28
28
|
uomGroupId?: number | null;
|
|
29
29
|
pickerZIndex?: number;
|
|
30
30
|
baseUnitsOnly?: boolean;
|
|
31
|
+
isDisabled?: boolean;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export function UomPicker({
|
|
@@ -41,6 +42,7 @@ export function UomPicker({
|
|
|
41
42
|
uomGroupId,
|
|
42
43
|
pickerZIndex,
|
|
43
44
|
baseUnitsOnly = false,
|
|
45
|
+
isDisabled = false,
|
|
44
46
|
}: UomPickerProps) {
|
|
45
47
|
const [isOpen, setIsOpen] = useState(false);
|
|
46
48
|
const [search, setSearch] = useState("");
|
|
@@ -90,9 +92,11 @@ export function UomPicker({
|
|
|
90
92
|
}
|
|
91
93
|
setIsCreating(true);
|
|
92
94
|
try {
|
|
93
|
-
// 1. Create Group
|
|
95
|
+
// 1. Create Group (which automatically creates the base UOM on the backend)
|
|
94
96
|
const groupRes = await createUomGroup({
|
|
95
|
-
name: quickAddData.new_group_name.trim()
|
|
97
|
+
name: quickAddData.new_group_name.trim(),
|
|
98
|
+
base_uom_name: quickAddData.name.trim(),
|
|
99
|
+
base_uom_code: quickAddData.abbreviation.trim()
|
|
96
100
|
});
|
|
97
101
|
|
|
98
102
|
if (!groupRes.is_success || !groupRes.result) {
|
|
@@ -101,26 +105,21 @@ export function UomPicker({
|
|
|
101
105
|
return;
|
|
102
106
|
}
|
|
103
107
|
|
|
108
|
+
toast.success("UOM created");
|
|
109
|
+
|
|
104
110
|
const newGroupId = groupRes.result.id;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
uom_group_id: newGroupId
|
|
112
|
-
base_uom_id: null,
|
|
113
|
-
status: quickAddData.status,
|
|
111
|
+
const baseUomId = groupRes.result.base_uom?.id || -1;
|
|
112
|
+
|
|
113
|
+
onChange({
|
|
114
|
+
id: baseUomId,
|
|
115
|
+
name: quickAddData.name.trim(),
|
|
116
|
+
abbreviation: quickAddData.abbreviation.trim(),
|
|
117
|
+
uom_group_id: newGroupId
|
|
114
118
|
});
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
setQuickAddData({ name: "", abbreviation: "", new_group_name: "", status: "active" });
|
|
120
|
-
setIsOpen(false);
|
|
121
|
-
} else {
|
|
122
|
-
toast.error(res.message || "Failed to create");
|
|
123
|
-
}
|
|
119
|
+
|
|
120
|
+
setIsQuickAddOpen(false);
|
|
121
|
+
setQuickAddData({ name: "", abbreviation: "", new_group_name: "", status: "active" });
|
|
122
|
+
setIsOpen(false);
|
|
124
123
|
} catch (e: any) {
|
|
125
124
|
toast.error(e.message || "Error creating UOM");
|
|
126
125
|
}
|
|
@@ -149,6 +148,7 @@ export function UomPicker({
|
|
|
149
148
|
value={displayValue || null}
|
|
150
149
|
placeholder={placeholder}
|
|
151
150
|
isRequired={isRequired}
|
|
151
|
+
disabled={isDisabled}
|
|
152
152
|
onClick={handleOpen}
|
|
153
153
|
onClear={value ? handleClear : undefined}
|
|
154
154
|
/>
|