@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.
@@ -14,6 +14,7 @@ import {
14
14
  PickerTrigger,
15
15
  } from "./EntityPickerModal";
16
16
  import { lookupParties, createParty, sendRequest } from "@apptimate/core-lib";
17
+ import { QuickPartyAddModal } from "./QuickPartyAddModal";
17
18
  import toast from "react-hot-toast";
18
19
 
19
20
  interface PartyPickerProps {
@@ -24,7 +25,8 @@ interface PartyPickerProps {
24
25
  placeholder?: string;
25
26
  isRequired?: boolean;
26
27
  disabled?: boolean;
27
- partyType?: "customer" | "supplier" | "all" | "artisan";
28
+ partyType?: "customer" | "supplier" | "all" | "artisan" | "other" | "employee" | "salesperson";
29
+ createAsType?: "customer" | "supplier" | "artisan" | "other" | "employee" | "salesperson";
28
30
  customTrigger?: (onClick: () => void) => React.ReactNode;
29
31
  }
30
32
 
@@ -37,6 +39,7 @@ export function PartyPicker({
37
39
  isRequired = false,
38
40
  disabled = false,
39
41
  partyType = "all",
42
+ createAsType,
40
43
  customTrigger,
41
44
  }: PartyPickerProps) {
42
45
  const [isOpen, setIsOpen] = useState(false);
@@ -46,88 +49,6 @@ export function PartyPicker({
46
49
 
47
50
  // Quick add
48
51
  const [isQuickAddOpen, setIsQuickAddOpen] = useState(false);
49
- const [quickAddFirstName, setQuickAddFirstName] = useState("");
50
- const [quickAddLastName, setQuickAddLastName] = useState("");
51
- const [quickAddEmail, setQuickAddEmail] = useState("");
52
- const [quickAddPhone, setQuickAddPhone] = useState("");
53
- const [quickAddSecondaryContact, setQuickAddSecondaryContact] = useState("");
54
- const [quickAddDOB, setQuickAddDOB] = useState("");
55
- const [quickAddIdentityType, setQuickAddIdentityType] = useState("");
56
- const [quickAddIdentity, setQuickAddIdentity] = useState("");
57
-
58
- const [quickAddAddressLine1, setQuickAddAddressLine1] = useState("");
59
- const [quickAddAddressLine2, setQuickAddAddressLine2] = useState("");
60
- const [quickAddCity, setQuickAddCity] = useState("");
61
- const [quickAddState, setQuickAddState] = useState("");
62
- const [quickAddPostalCode, setQuickAddPostalCode] = useState("");
63
- const [quickAddCountry, setQuickAddCountry] = useState("");
64
- const [showQuickAddDetailedAddress, setShowQuickAddDetailedAddress] = useState(false);
65
-
66
- const [quickAddCurrencyId, setQuickAddCurrencyId] = useState<string | number | undefined>(undefined);
67
- const [quickAddCurrencyDisplay, setQuickAddCurrencyDisplay] = useState<any>(null);
68
- const [errors, setErrors] = useState<Record<string, string[]>>({});
69
- const [isCreating, setIsCreating] = useState(false);
70
-
71
- const [fieldConfigs, setFieldConfigs] = useState<any[]>([]);
72
-
73
- const effectiveType = partyType === "all" ? "customer" : partyType;
74
- const mergedConfig = React.useMemo(() => {
75
- const configMap: Record<string, { active: boolean; mandatory: boolean }> = {};
76
- const relevantConfigs = fieldConfigs.filter(c => c.entity === effectiveType);
77
- for (const config of relevantConfigs) {
78
- if (!configMap[config.field]) {
79
- configMap[config.field] = { active: false, mandatory: false };
80
- }
81
- if (config.status === "active") configMap[config.field].active = true;
82
- if (config.is_mandatory) configMap[config.field].mandatory = true;
83
- }
84
- return configMap;
85
- }, [fieldConfigs, effectiveType]);
86
-
87
- const hasInitializedQuickAddCurrency = useRef(false);
88
-
89
- useEffect(() => {
90
- if (isQuickAddOpen) {
91
- if (!hasInitializedQuickAddCurrency.current) {
92
- hasInitializedQuickAddCurrency.current = true;
93
- sendRequest({ url: `${process.env.NEXT_PUBLIC_API_URL}/api/settings`, method: 'GET' })
94
- .then((res: any) => {
95
- if (res.responseData?.is_success && res.responseData?.result) {
96
- const configs = Array.isArray(res.responseData.result) ? res.responseData.result : res.responseData.result.data;
97
- const baseConfig = configs?.find((c: any) => c.key === 'base_currency');
98
- if (baseConfig?.resolved_item) {
99
- const currency = baseConfig.resolved_item;
100
- setQuickAddCurrencyId(currency.id);
101
- setQuickAddCurrencyDisplay({ id: currency.id, code: currency.code, displayLabel: `${currency.code} — ${currency.name}` });
102
- } else if (baseConfig?.value) {
103
- // Fallback just in case
104
- const baseId = baseConfig.value;
105
- sendRequest({ url: `${process.env.NEXT_PUBLIC_API_URL}/api/currencies`, method: 'GET', params: { search: '' } })
106
- .then((cRes: any) => {
107
- if (cRes.responseData?.is_success && cRes.responseData?.result?.data) {
108
- const cData = Array.isArray(cRes.responseData.result.data) ? cRes.responseData.result.data : cRes.responseData.result;
109
- const baseCurrency = cData.find((c: any) => String(c.id) === String(baseId));
110
- if (baseCurrency) {
111
- setQuickAddCurrencyId(baseCurrency.id);
112
- setQuickAddCurrencyDisplay({ id: baseCurrency.id, code: baseCurrency.code, displayLabel: `${baseCurrency.code} — ${baseCurrency.name}` });
113
- }
114
- }
115
- });
116
- }
117
- }
118
- });
119
- }
120
-
121
- if (fieldConfigs.length === 0) {
122
- sendRequest({ url: `${process.env.NEXT_PUBLIC_API_URL}/api/field-configs`, method: 'GET' })
123
- .then((res: any) => {
124
- if (res.responseData?.is_success && res.responseData?.result) {
125
- setFieldConfigs(res.responseData.result);
126
- }
127
- });
128
- }
129
- }
130
- }, [isQuickAddOpen, fieldConfigs.length]);
131
52
 
132
53
  const fetchData = useCallback(async (query: string = "") => {
133
54
  setIsLoading(true);
@@ -177,78 +98,7 @@ export function PartyPicker({
177
98
  onChange(null);
178
99
  };
179
100
 
180
- const handleQuickAdd = async () => {
181
- if (!quickAddFirstName.trim()) return;
182
- setIsCreating(true);
183
- setErrors({});
184
- try {
185
- const payload = {
186
- first_name: quickAddFirstName.trim(),
187
- last_name: quickAddLastName.trim() || undefined,
188
- email: quickAddEmail.trim() || undefined,
189
- primary_contact: quickAddPhone.trim() || undefined,
190
- secondary_contact: quickAddSecondaryContact.trim() || undefined,
191
- date_of_birth: quickAddDOB || undefined,
192
- identity_type: quickAddIdentityType || undefined,
193
- identity: quickAddIdentity.trim() || undefined,
194
- default_currency_id: quickAddCurrencyId || undefined,
195
- type: [partyType === "all" || !partyType ? "customer" : partyType],
196
- status: "active",
197
- address: quickAddAddressLine1.trim() ? {
198
- address_line_1: quickAddAddressLine1.trim(),
199
- address_line_2: quickAddAddressLine2.trim() || undefined,
200
- city: quickAddCity.trim() || undefined,
201
- state: quickAddState.trim() || undefined,
202
- postal_code: quickAddPostalCode.trim() || undefined,
203
- country: quickAddCountry.trim() || undefined,
204
- } : undefined
205
- };
206
- const res = await createParty(payload);
207
- if (res.is_success && res.result) {
208
- toast.success("Party created");
209
- const party = res.result;
210
- const partyName = party.name || party.full_name || [party.first_name, party.last_name].filter(Boolean).join(" ");
211
- onChange({
212
- id: Number(party.id),
213
- name: String(partyName),
214
- code: party.code ? String(party.code) : undefined,
215
- type: Array.isArray(party.type) ? party.type[0] : (party.type ? String(party.type) : undefined),
216
- default_currency_id: quickAddCurrencyDisplay?.id ? Number(quickAddCurrencyDisplay.id) : undefined,
217
- default_currency: quickAddCurrencyDisplay ? {
218
- id: Number(quickAddCurrencyDisplay.id),
219
- code: String(quickAddCurrencyDisplay.code),
220
- name: String(quickAddCurrencyDisplay.name),
221
- symbol: String(quickAddCurrencyDisplay.symbol)
222
- } : undefined
223
- });
224
- setIsQuickAddOpen(false);
225
- setQuickAddFirstName("");
226
- setQuickAddLastName("");
227
- setQuickAddEmail("");
228
- setQuickAddPhone("");
229
- setQuickAddSecondaryContact("");
230
- setQuickAddDOB("");
231
- setQuickAddIdentityType("");
232
- setQuickAddIdentity("");
233
- setQuickAddAddressLine1("");
234
- setQuickAddAddressLine2("");
235
- setQuickAddCity("");
236
- setQuickAddState("");
237
- setQuickAddPostalCode("");
238
- setQuickAddCountry("");
239
- setShowQuickAddDetailedAddress(false);
240
- setErrors({});
241
- setIsOpen(false);
242
- } else if ((res as any).errors) {
243
- setErrors((res as any).errors);
244
- } else {
245
- toast.error(res.message || "Failed to create");
246
- }
247
- } catch (e: any) {
248
- toast.error(e.message || "Error creating party");
249
- }
250
- setIsCreating(false);
251
- };
101
+
252
102
 
253
103
  // Server-side filtering used
254
104
 
@@ -282,23 +132,7 @@ export function PartyPicker({
282
132
  <button
283
133
  type="button"
284
134
  onClick={() => {
285
- setIsQuickAddOpen(true);
286
- setQuickAddFirstName("");
287
- setQuickAddLastName("");
288
- setQuickAddEmail("");
289
- setQuickAddPhone("");
290
- setQuickAddSecondaryContact("");
291
- setQuickAddDOB("");
292
- setQuickAddIdentityType("");
293
- setQuickAddIdentity("");
294
- setQuickAddAddressLine1("");
295
- setQuickAddAddressLine2("");
296
- setQuickAddCity("");
297
- setQuickAddState("");
298
- setQuickAddPostalCode("");
299
- setQuickAddCountry("");
300
- setShowQuickAddDetailedAddress(false);
301
- setErrors({});
135
+ setIsQuickAddOpen(true);
302
136
  }}
303
137
  className="p-1.5 rounded-lg bg-primary-50 text-primary-600 hover:bg-primary-100 transition-colors flex-shrink-0"
304
138
  title={`Quick Add ${label}`}
@@ -336,237 +170,30 @@ export function PartyPicker({
336
170
  )}
337
171
  </EntityPickerModal>
338
172
 
339
- {/* Quick Add Modal */}
340
- <Modal
173
+ <QuickPartyAddModal
341
174
  isOpen={isQuickAddOpen}
342
175
  onClose={() => setIsQuickAddOpen(false)}
343
- title={`Quick Add ${label}`}
344
- size="md"
345
- zIndex={300}
346
- backdrop="blur"
347
- >
348
- <div className="space-y-4">
349
- <div className="grid grid-cols-2 gap-3">
350
- {mergedConfig.first_name?.active !== false && (
351
- <Input
352
- label="First Name"
353
- isRequired={mergedConfig.first_name?.mandatory ?? true}
354
- placeholder={`Enter ${label.toLowerCase()} first name`}
355
- value={quickAddFirstName}
356
- onChange={(e) => setQuickAddFirstName(e.target.value)}
357
- error={errors.first_name?.[0]}
358
- />
359
- )}
360
- {mergedConfig.last_name?.active !== false && (
361
- <Input
362
- label="Last Name"
363
- isRequired={mergedConfig.last_name?.mandatory}
364
- placeholder={`Enter ${label.toLowerCase()} last name`}
365
- value={quickAddLastName}
366
- onChange={(e) => setQuickAddLastName(e.target.value)}
367
- error={errors.last_name?.[0]}
368
- />
369
- )}
370
- </div>
371
- <div className="grid grid-cols-2 gap-3">
372
- {mergedConfig.email?.active !== false && (
373
- <Input
374
- label="Email"
375
- type="email"
376
- isRequired={mergedConfig.email?.mandatory}
377
- placeholder="Email address"
378
- value={quickAddEmail}
379
- onChange={(e) => setQuickAddEmail(e.target.value)}
380
- error={errors.email?.[0]}
381
- />
382
- )}
383
- {mergedConfig.primary_contact?.active !== false && (
384
- <Input
385
- label="Primary Contact"
386
- isRequired={mergedConfig.primary_contact?.mandatory}
387
- placeholder="Primary phone number"
388
- value={quickAddPhone}
389
- onChange={(e) => setQuickAddPhone(e.target.value)}
390
- error={errors.primary_contact?.[0]}
391
- />
392
- )}
393
- </div>
394
- <div className="grid grid-cols-2 gap-3">
395
- {mergedConfig.secondary_contact?.active !== false && (
396
- <Input
397
- label="Secondary Contact"
398
- isRequired={mergedConfig.secondary_contact?.mandatory}
399
- placeholder="Alternative phone (optional)"
400
- value={quickAddSecondaryContact}
401
- onChange={(e) => setQuickAddSecondaryContact(e.target.value)}
402
- error={errors.secondary_contact?.[0]}
403
- />
404
- )}
405
- {mergedConfig.date_of_birth?.active !== false && (
406
- <Input
407
- label="Date of Birth"
408
- type="date"
409
- isRequired={mergedConfig.date_of_birth?.mandatory}
410
- value={quickAddDOB}
411
- onChange={(e) => setQuickAddDOB(e.target.value)}
412
- error={errors.date_of_birth?.[0]}
413
- />
414
- )}
415
- </div>
416
- {mergedConfig.identity?.active !== false && (
417
- <div className="space-y-1.5 w-full">
418
- <label className="text-[13px] font-medium text-[#4A5568]">
419
- Identity Details {mergedConfig.identity?.mandatory && <span className="text-red-500">*</span>}
420
- </label>
421
- <div className="flex relative">
422
- <select
423
- value={quickAddIdentityType}
424
- onChange={(e) => setQuickAddIdentityType(e.target.value)}
425
- 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`}
426
- 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' }}
427
- >
428
- <option value="">Select type</option>
429
- <option value="nic">NIC</option>
430
- <option value="passport">Passport</option>
431
- <option value="driving_license">Driving License</option>
432
- <option value="business_registration">Business Registration</option>
433
- <option value="other">Other</option>
434
- </select>
435
- <input
436
- type="text"
437
- placeholder="Enter identity number"
438
- value={quickAddIdentity}
439
- onChange={(e) => setQuickAddIdentity(e.target.value)}
440
- 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`}
441
- />
442
- </div>
443
- {(errors.identity_type?.[0] || errors.identity?.[0]) && (
444
- <span className="text-[11px] text-danger-alt font-medium">
445
- {errors.identity_type?.[0] || errors.identity?.[0]}
446
- </span>
447
- )}
448
- </div>
449
- )}
450
- {mergedConfig.address?.active !== false && (
451
- <div className="space-y-3">
452
- <div className="flex justify-between items-center">
453
- <label className="text-[13px] font-medium text-[#4A5568]">
454
- Address {mergedConfig.address?.mandatory ? <span className="text-red-500">*</span> : "(Optional)"}
455
- </label>
456
- <button
457
- type="button"
458
- onClick={() => setShowQuickAddDetailedAddress(!showQuickAddDetailedAddress)}
459
- className="text-xs font-semibold text-primary-600 hover:text-primary-700 transition-colors"
460
- >
461
- {showQuickAddDetailedAddress ? "Quick Add Address" : "Add Detailed Address"}
462
- </button>
463
- </div>
464
-
465
- {!showQuickAddDetailedAddress ? (
466
- <Input
467
- placeholder="Enter complete primary address..."
468
- value={quickAddAddressLine1}
469
- onChange={(e) => setQuickAddAddressLine1(e.target.value)}
470
- error={errors.address?.[0] || errors['address.address_line_1']?.[0]}
471
- />
472
- ) : (
473
- <div className="p-3 bg-gray-50 border border-gray-100 rounded-xl space-y-3">
474
- <Input
475
- label="Address Line 1"
476
- placeholder="Street address, P.O. box, etc."
477
- value={quickAddAddressLine1}
478
- onChange={(e) => setQuickAddAddressLine1(e.target.value)}
479
- error={errors.address?.[0] || errors['address.address_line_1']?.[0]}
480
- />
481
- <Input
482
- label="Address Line 2"
483
- placeholder="Apartment, suite, unit, etc. (optional)"
484
- value={quickAddAddressLine2}
485
- onChange={(e) => setQuickAddAddressLine2(e.target.value)}
486
- error={errors['address.address_line_2']?.[0]}
487
- />
488
- <div className="grid grid-cols-2 gap-3">
489
- <Input
490
- label="City"
491
- placeholder="City"
492
- value={quickAddCity}
493
- onChange={(e) => setQuickAddCity(e.target.value)}
494
- error={errors['address.city']?.[0]}
495
- />
496
- <Input
497
- label="State / Province"
498
- placeholder="State or Province"
499
- value={quickAddState}
500
- onChange={(e) => setQuickAddState(e.target.value)}
501
- error={errors['address.state']?.[0]}
502
- />
503
- </div>
504
- <div className="grid grid-cols-2 gap-3">
505
- <Input
506
- label="Postal Code"
507
- placeholder="ZIP or Postal Code"
508
- value={quickAddPostalCode}
509
- onChange={(e) => setQuickAddPostalCode(e.target.value)}
510
- error={errors['address.postal_code']?.[0]}
511
- />
512
- <Input
513
- label="Country"
514
- placeholder="Country"
515
- value={quickAddCountry}
516
- onChange={(e) => setQuickAddCountry(e.target.value)}
517
- error={errors['address.country']?.[0]}
518
- />
519
- </div>
520
- </div>
521
- )}
522
- </div>
523
- )}
524
- <div className="grid grid-cols-2 gap-3">
525
- {(partyType === "all" || partyType === "customer" || partyType === "supplier") && (
526
- <AsyncSearchableSelect
527
- label="Default Currency"
528
- placeholder="Search currency..."
529
- loadOptions={async (search, page) => {
530
- try {
531
- const res: any = await sendRequest({
532
- url: `${process.env.NEXT_PUBLIC_API_URL}/api/currencies`,
533
- method: 'GET',
534
- params: { search, page, per_page: 20 }
535
- });
536
- if (res.responseData?.is_success && res.responseData?.result?.data) {
537
- return res.responseData.result.data.map((c: any) => ({
538
- ...c,
539
- displayLabel: `${c.code} — ${c.name}`
540
- }));
541
- }
542
- return [];
543
- } catch { return []; }
544
- }}
545
- option={{ label: "displayLabel", value: "id", keysToSearch: ["name", "code"] }}
546
- onChange={(val, selectedObj: any) => {
547
- setQuickAddCurrencyId(val as string | number);
548
- setQuickAddCurrencyDisplay(selectedObj);
549
- }}
550
- key={quickAddCurrencyDisplay ? quickAddCurrencyDisplay.code : 'empty'}
551
- defaultValue={quickAddCurrencyDisplay || undefined}
552
- error={errors.default_currency_id?.[0]}
553
- />
554
- )}
555
- </div>
556
- <ModalFooter>
557
- <Button
558
- variant="flat"
559
- color="default"
560
- onClick={() => setIsQuickAddOpen(false)}
561
- >
562
- Cancel
563
- </Button>
564
- <Button onClick={handleQuickAdd} isDisabled={isCreating || !quickAddFirstName.trim()}>
565
- {isCreating ? "Creating…" : "Create & Select"}
566
- </Button>
567
- </ModalFooter>
568
- </div>
569
- </Modal>
176
+ label={label}
177
+ partyType={partyType}
178
+ createAsType={createAsType}
179
+ onSuccess={(party) => {
180
+ const partyName = party.name || party.full_name || [party.first_name, party.last_name].filter(Boolean).join(" ");
181
+ onChange({
182
+ id: Number(party.id),
183
+ name: String(partyName),
184
+ code: party.code ? String(party.code) : undefined,
185
+ type: Array.isArray(party.type) ? party.type[0] : (party.type ? String(party.type) : undefined),
186
+ default_currency_id: party.currencyDisplay?.id ? Number(party.currencyDisplay.id) : undefined,
187
+ default_currency: party.currencyDisplay ? {
188
+ id: Number(party.currencyDisplay.id),
189
+ code: String(party.currencyDisplay.code),
190
+ name: party.currencyDisplay.name ? String(party.currencyDisplay.name) : "",
191
+ symbol: party.currencyDisplay.symbol ? String(party.currencyDisplay.symbol) : ""
192
+ } : undefined
193
+ });
194
+ setIsOpen(false);
195
+ }}
196
+ />
570
197
  </>
571
198
  );
572
199
  }