@accounter/client 0.0.8-alpha-20251023121008-8c51a83a479af2fcbc3aa02c876285dc4e21f02b → 0.0.8-alpha-20251023122652-765c7f951395441461726b3b4345eebd020632d7

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.
Files changed (92) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/dist/assets/{Checkbox-Bu7lbyW3.js → Checkbox-CpANMiXr.js} +2 -2
  3. package/dist/assets/{Progress-Cq1kNJW5.js → Progress-BEKiyNRK.js} +1 -1
  4. package/dist/assets/{Typography-qy9vYtPs.js → Typography-C0c5augK.js} +1 -1
  5. package/dist/assets/{accordion-DOoNLp8s.js → accordion-ZFVTCyDy.js} +1 -1
  6. package/dist/assets/accountant-approvals-CpzuSBgK.js +1 -0
  7. package/dist/assets/{all-charges-CkXCeGgB.js → all-charges-BTyaAlp7.js} +1 -1
  8. package/dist/assets/{arrow-up-down-DvV6Dq8U.js → arrow-up-down-PQdrJdei.js} +1 -1
  9. package/dist/assets/business-DiQFqEHI.js +42 -0
  10. package/dist/assets/{business-transactions-single-BSpUxu3D.js → business-transactions-single-DJqvqlMO.js} +1 -1
  11. package/dist/assets/{business-trip-Bv1KXZPV.js → business-trip-BXAjI1mO.js} +1 -1
  12. package/dist/assets/charges-filters-r7FjeZLc.js +1 -0
  13. package/dist/assets/{charges-ledger-validation-BcO2gYIK.js → charges-ledger-validation-CFc_zJFO.js} +1 -1
  14. package/dist/assets/{chart-Cg9PgOtK.js → chart-C5LuxRSv.js} +1 -1
  15. package/dist/assets/{data-table-pagination-aFECnXu9.js → data-table-pagination-g2cF8_uB.js} +1 -1
  16. package/dist/assets/{editable-business-trip-qxr12Yt0.js → editable-business-trip-DyQsUJjl.js} +2 -2
  17. package/dist/assets/index-2g0J28D2.js +1 -0
  18. package/dist/assets/{index-CuIQrA5k.js → index-6-J73JPC.js} +1 -1
  19. package/dist/assets/index-B5j8Fmt8.js +1 -0
  20. package/dist/assets/index-BSBV6pv2.js +9 -0
  21. package/dist/assets/{index-BFERto_M.js → index-BSg8ocop.js} +2 -2
  22. package/dist/assets/{index-B2_BlI07.js → index-Bpef3vuz.js} +2 -2
  23. package/dist/assets/index-BziuNiXZ.js +1 -0
  24. package/dist/assets/{index-BDsbL_Kg.js → index-C0OGGVXE.js} +2 -2
  25. package/dist/assets/index-C7KKktQ5.js +1 -0
  26. package/dist/assets/{index-Dh7go2PN.js → index-C99l-a0m.js} +1 -1
  27. package/dist/assets/index-CE3kLG2a.js +1 -0
  28. package/dist/assets/{index-GwuBtmZO.js → index-CJdrnxxy.js} +2 -2
  29. package/dist/assets/{index-DOrujuWI.js → index-CN2818Wt.js} +223 -233
  30. package/dist/assets/index-CjDRKTTf.js +1 -0
  31. package/dist/assets/index-CzzfC-dD.css +1 -0
  32. package/dist/assets/index-D2xdQJAx.js +1 -0
  33. package/dist/assets/index-DGvh10a7.js +1 -0
  34. package/dist/assets/index-DLI6Z9VU.js +1 -0
  35. package/dist/assets/index-DXL1qPt9.js +1 -0
  36. package/dist/assets/{index-BYyokQ1_.js → index-Is4LJW4y.js} +2 -2
  37. package/dist/assets/{index-aW2k8oov.js → index-S6eSocQH.js} +7 -7
  38. package/dist/assets/index-T-JiUNDA.js +1 -0
  39. package/dist/assets/{index-BNXVR9UW.js → index-gBZ-7Z07.js} +1 -1
  40. package/dist/assets/{index-M3bu66aM.js → index-o_W5PWRq.js} +6 -6
  41. package/dist/assets/{index.es-Bg5AVQ7i.js → index.es-DZgQ4YcV.js} +1 -1
  42. package/dist/assets/{issue-document-B4ZlDg1e.js → issue-document-D-JoFiMZ.js} +1 -1
  43. package/dist/assets/login-page-DsHy5amR.js +1 -0
  44. package/dist/assets/{missing-info-charges-Ck_n6s6i.js → missing-info-charges-CPnjfjfW.js} +1 -1
  45. package/dist/assets/page-not-found-CYmXF0n2.js +1 -0
  46. package/dist/assets/{pencil-CpRI4b2y.js → pencil-BUwX_eHK.js} +1 -1
  47. package/dist/assets/report-commentary-row-Bpvvtwpd.js +1 -0
  48. package/dist/assets/save-Ck68rh6u.js +11 -0
  49. package/dist/assets/similar-charges-by-business-modal-bUtiFfAw.js +1 -0
  50. package/dist/assets/{sub-Daf_8shG.js → sub-C2zifPY5.js} +1 -1
  51. package/dist/assets/subMonths-uUfFviD2.js +1 -0
  52. package/dist/index.html +2 -2
  53. package/package.json +1 -1
  54. package/src/components/business/business-header.tsx +2 -21
  55. package/src/components/businesses/all-businesses-row.tsx +87 -0
  56. package/src/components/businesses/cells/hebrew-name.tsx +31 -0
  57. package/src/components/businesses/cells/index.ts +2 -0
  58. package/src/components/businesses/cells/name.tsx +31 -0
  59. package/src/components/businesses/index.tsx +39 -56
  60. package/src/components/common/forms/business-card.tsx +234 -0
  61. package/src/components/common/forms/index.ts +1 -0
  62. package/src/components/common/modals/insert-business.tsx +51 -659
  63. package/src/gql/gql.ts +30 -6
  64. package/src/gql/graphql.ts +30 -7
  65. package/src/providers/urql-client.ts +2 -3
  66. package/src/providers/urql-error-handler.ts +27 -0
  67. package/src/providers/urql.tsx +5 -0
  68. package/src/router/loaders/business-loader.ts +2 -4
  69. package/src/router/loaders/charge-loader.ts +2 -4
  70. package/dist/assets/accountant-approvals-Be5zF2_c.js +0 -1
  71. package/dist/assets/building-2-B-rX_xIy.js +0 -6
  72. package/dist/assets/business-CNkwr3_Z.js +0 -32
  73. package/dist/assets/business-header-Drp6R3A0.js +0 -1
  74. package/dist/assets/charges-filters-CpNNXLPU.js +0 -1
  75. package/dist/assets/index--C4Az49P.js +0 -1
  76. package/dist/assets/index-A1xHtOLI.js +0 -1
  77. package/dist/assets/index-BrsmDzry.js +0 -1
  78. package/dist/assets/index-C3AdAlvt.js +0 -9
  79. package/dist/assets/index-CD53YLxm.js +0 -1
  80. package/dist/assets/index-CY2CmEvR.js +0 -1
  81. package/dist/assets/index-Crn32InT.js +0 -1
  82. package/dist/assets/index-CyI9xpFz.js +0 -1
  83. package/dist/assets/index-DStQk1jH.js +0 -1
  84. package/dist/assets/index-DtE5Y1ZB.css +0 -1
  85. package/dist/assets/index-DwzG6-LK.js +0 -1
  86. package/dist/assets/index-bPa4yB-5.js +0 -1
  87. package/dist/assets/index-vHwCjYSy.js +0 -1
  88. package/dist/assets/login-page-v4kHU0le.js +0 -1
  89. package/dist/assets/page-not-found-TWjTuISB.js +0 -1
  90. package/dist/assets/report-commentary-row-BNIW-6Qg.js +0 -1
  91. package/dist/assets/save-CjLvssI1.js +0 -6
  92. package/dist/assets/subMonths-DIZWyi5T.js +0 -1
@@ -1,692 +1,84 @@
1
- import { useCallback, useContext, useState } from 'react';
2
- import { Globe, Mail, MapPin, Phone, Plus, X } from 'lucide-react';
3
- import { useForm } from 'react-hook-form';
4
- import { z } from 'zod';
5
- import { Badge } from '@/components/ui/badge.js';
6
- import { Button } from '@/components/ui/button.js';
1
+ import { useContext, useState, type ReactElement } from 'react';
2
+ import { useForm, type SubmitHandler } from 'react-hook-form';
3
+ import { UserContext } from '@/providers/user-provider.js';
4
+ import type { InsertNewBusinessInput } from '../../../gql/graphql.js';
5
+ import { useInsertBusiness } from '../../../hooks/use-insert-business.js';
6
+ import { Button } from '../../ui/button.js';
7
7
  import {
8
8
  Dialog,
9
9
  DialogContent,
10
- DialogDescription,
11
10
  DialogHeader,
12
11
  DialogTitle,
13
12
  DialogTrigger,
14
- } from '@/components/ui/dialog.js';
15
- import {
16
- Form,
17
- FormControl,
18
- FormField,
19
- FormItem,
20
- FormLabel,
21
- FormMessage,
22
- } from '@/components/ui/form.js';
23
- import { Input } from '@/components/ui/input.js';
24
- import {
25
- Select,
26
- SelectContent,
27
- SelectItem,
28
- SelectTrigger,
29
- SelectValue,
30
- } from '@/components/ui/select.js';
31
- import { Textarea } from '@/components/ui/textarea.js';
32
- import { pcn874RecordEnum } from '@/helpers/index.js';
33
- import { useAllCountries } from '@/hooks/use-get-countries.js';
34
- import { useGetSortCodes } from '@/hooks/use-get-sort-codes.js';
35
- import { useGetTags } from '@/hooks/use-get-tags.js';
36
- import { useGetTaxCategories } from '@/hooks/use-get-tax-categories.js';
37
- import { UserContext } from '@/providers/user-provider.js';
38
- import { zodResolver } from '@hookform/resolvers/zod';
39
- import type { InsertNewBusinessInput, Pcn874RecordType } from '../../../gql/graphql.js';
40
- import { useInsertBusiness } from '../../../hooks/use-insert-business.js';
41
- import { ComboBox, MultiSelect, NumberInput } from '../index.js';
42
-
43
- // Zod schema for the business form
44
- const businessFormSchema = z
45
- .object({
46
- businessName: z.string().min(1, 'Business name is required'),
47
- locality: z.string().min(1, 'Locality is required'),
48
- localName: z.string().optional(),
49
- govId: z.string().optional(),
50
- address: z.string().optional(),
51
- generalContacts: z.array(z.string().email()),
52
- website: z.string().url().optional().or(z.literal('')),
53
- phone: z.string().optional(),
54
- taxCategory: z.string().optional(),
55
- sortCode: z.string().optional(),
56
- pcn874RecordType: z.string().optional(),
57
- irsCode: z.int().optional(),
58
- defaultDescription: z.string().optional(),
59
- defaultTags: z.array(z.string()),
60
- transactionPhrases: z.array(z.string()),
61
- emailAddresses: z.array(z.string().email()),
62
- })
63
- .refine(
64
- data => {
65
- if (data.locality === 'Israel') {
66
- return !!data.localName && !!data.govId;
67
- }
68
- return true;
69
- },
70
- {
71
- message: 'Local name and company number are required for Israeli businesses',
72
- path: ['localName'],
73
- },
74
- );
75
-
76
- type BusinessFormValues = z.infer<typeof businessFormSchema>;
77
-
78
- function convertFormDataToInsertNewBusinessInput(
79
- formData: BusinessFormValues,
80
- ): InsertNewBusinessInput {
81
- const suggestionsDataExists =
82
- formData.defaultDescription ||
83
- formData.defaultTags?.length ||
84
- formData.transactionPhrases?.length ||
85
- formData.emailAddresses?.length;
86
- const suggestions: InsertNewBusinessInput['suggestions'] | undefined = suggestionsDataExists
87
- ? {
88
- description: formData.defaultDescription,
89
- tags: formData.defaultTags?.map(id => ({ id })),
90
- phrases: formData.transactionPhrases,
91
- emails: formData.emailAddresses,
92
- }
93
- : undefined;
94
-
95
- return {
96
- name: formData.businessName,
97
- country: formData.locality,
98
- hebrewName: formData.localName,
99
- governmentId: formData.govId,
100
- address: formData.address,
101
- phoneNumber: formData.phone,
102
- website: formData.website,
103
- email: formData.generalContacts?.join(', '),
104
- sortCode: formData.sortCode ? parseInt(formData.sortCode) : undefined,
105
- taxCategory: formData.taxCategory,
106
- pcn874RecordType: formData.pcn874RecordType as Pcn874RecordType,
107
- irsCode: formData.irsCode,
108
- suggestions,
109
- };
110
- }
13
+ } from '../../ui/dialog.js';
14
+ import { Form } from '../../ui/form.js';
15
+ import { InsertBusinessFields } from '../index.js';
111
16
 
112
17
  export function InsertBusiness({
113
18
  description,
114
19
  onAdd,
115
20
  }: {
116
- description?: string;
21
+ description: string;
117
22
  onAdd?: (businessId: string) => void;
118
- }) {
119
- const [isNewBusinessOpen, setIsNewBusinessOpen] = useState(false);
120
-
121
- const { insertBusiness, fetching: addingInProcess } = useInsertBusiness();
122
-
123
- const form = useForm<BusinessFormValues>({
124
- resolver: zodResolver(businessFormSchema),
125
- defaultValues: {
126
- businessName: description,
127
- generalContacts: [],
128
- defaultTags: [],
129
- transactionPhrases: description ? [description] : [],
130
- emailAddresses: [],
131
- },
132
- });
133
-
134
- const onSubmit = async (data: BusinessFormValues) => {
135
- const newBusiness = await insertBusiness({
136
- fields: convertFormDataToInsertNewBusinessInput(data),
137
- });
138
- setIsNewBusinessOpen(false);
139
- form.reset();
140
- onAdd?.(newBusiness?.id ?? '');
141
- };
23
+ }): ReactElement {
24
+ const [open, setOpen] = useState(false);
142
25
 
143
26
  return (
144
- <Dialog open={isNewBusinessOpen} onOpenChange={setIsNewBusinessOpen}>
27
+ <Dialog open={open} onOpenChange={setOpen}>
145
28
  <DialogTrigger asChild>
146
- <Button>
147
- <Plus className="size-4 mr-2" />
148
- New Business
149
- </Button>
29
+ <Button>Add Business</Button>
150
30
  </DialogTrigger>
151
- <DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
31
+ <DialogContent className="w-[90vw] max-w-screen-md">
152
32
  <DialogHeader>
153
- <DialogTitle>Create New Business</DialogTitle>
154
- <DialogDescription>Add a new business to Accounter.</DialogDescription>
33
+ <DialogTitle>Add New Business</DialogTitle>
155
34
  </DialogHeader>
156
- <Form {...form}>
157
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
158
- <ContactInformationSection form={form} />
159
-
160
- <DefaultsSection form={form} />
161
-
162
- <AutoMatchingSection form={form} />
163
-
164
- <div className="flex justify-end gap-3 pt-4 border-t">
165
- <Button type="button" variant="outline" onClick={() => setIsNewBusinessOpen(false)}>
166
- Cancel
167
- </Button>
168
- <Button type="submit" disabled={addingInProcess}>
169
- Create Business
170
- </Button>
171
- </div>
172
- </form>
173
- </Form>
35
+ <CreateBusinessForm description={description} close={() => setOpen(false)} onAdd={onAdd} />
174
36
  </DialogContent>
175
37
  </Dialog>
176
38
  );
177
39
  }
178
40
 
179
- interface SectionProps {
180
- form: ReturnType<typeof useForm<BusinessFormValues>>;
181
- }
182
-
183
- function ContactInformationSection({ form }: SectionProps) {
184
- const [newContact, setNewContact] = useState('');
41
+ type CreateBusinessFormProps = {
42
+ close: () => void;
43
+ onAdd?: (businessId: string) => void;
44
+ description: string;
45
+ };
185
46
 
47
+ function CreateBusinessForm({ description, close, onAdd }: CreateBusinessFormProps): ReactElement {
186
48
  const { userContext } = useContext(UserContext);
187
- const { countries, fetching: fetchingCountries } = useAllCountries();
188
-
189
- const { watch, setValue, control } = form;
190
-
191
- const locality = watch('locality');
192
-
193
- const addGeneralContact = (currentContacts: string[]) => {
194
- if (newContact.trim()) {
195
- setValue('generalContacts', [...currentContacts, newContact.trim()], {
196
- shouldDirty: true,
197
- });
198
- setNewContact('');
199
- }
200
- };
201
-
202
- const removeGeneralContact = (currentContacts: string[], index: number) => {
203
- setValue(
204
- 'generalContacts',
205
- currentContacts.filter((_, i) => i !== index),
206
- { shouldDirty: true },
207
- );
208
- };
209
-
210
- const isLocalEntity = locality === userContext?.context.locality;
211
-
212
- return (
213
- <div className="space-y-4">
214
- <h3 className="text-sm font-semibold text-foreground">Contact Information</h3>
215
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
216
- <FormField
217
- control={control}
218
- name="businessName"
219
- render={({ field }) => (
220
- <FormItem>
221
- <FormLabel>Business Name *</FormLabel>
222
- <FormControl>
223
- <Input {...field} required />
224
- </FormControl>
225
- <FormMessage />
226
- </FormItem>
227
- )}
228
- />
229
-
230
- <FormField
231
- control={control}
232
- name="locality"
233
- render={({ field }) => (
234
- <FormItem>
235
- <FormLabel>Locality / Country *</FormLabel>
236
- <ComboBox
237
- onChange={field.onChange}
238
- data={countries.map(country => ({
239
- value: country.code,
240
- label: country.name,
241
- }))}
242
- value={field.value}
243
- disabled={fetchingCountries}
244
- placeholder="Scroll to see all options"
245
- formPart
246
- />
247
- <FormMessage />
248
- </FormItem>
249
- )}
250
- />
251
-
252
- {isLocalEntity && (
253
- <>
254
- <FormField
255
- control={control}
256
- name="localName"
257
- render={({ field }) => (
258
- <FormItem>
259
- <FormLabel>Local Name *</FormLabel>
260
- <FormControl>
261
- <Input {...field} placeholder="Business name in local language" />
262
- </FormControl>
263
- <FormMessage />
264
- </FormItem>
265
- )}
266
- />
267
-
268
- <FormField
269
- control={control}
270
- name="govId"
271
- render={({ field }) => (
272
- <FormItem>
273
- <FormLabel>Government ID *</FormLabel>
274
- <FormControl>
275
- <Input {...field} placeholder="Enter Government ID" />
276
- </FormControl>
277
- <FormMessage />
278
- </FormItem>
279
- )}
280
- />
281
- </>
282
- )}
283
-
284
- <FormField
285
- control={control}
286
- name="address"
287
- render={({ field }) => (
288
- <FormItem className="md:col-span-2">
289
- <FormLabel>
290
- <MapPin className="size-4" />
291
- Address
292
- </FormLabel>
293
- <FormControl>
294
- <Textarea {...field} rows={2} placeholder="Enter business address" />
295
- </FormControl>
296
- <FormMessage />
297
- </FormItem>
298
- )}
299
- />
300
-
301
- <FormField
302
- control={control}
303
- name="generalContacts"
304
- render={({ field }) => (
305
- <FormItem className="md:col-span-2">
306
- <FormLabel className="flex items-center gap-2">
307
- <Mail className="size-4" />
308
- General Contacts
309
- </FormLabel>
310
- <FormControl>
311
- <div className="space-y-2">
312
- {field.value?.map((contact, index) => (
313
- <Badge key={index} variant="secondary" className="gap-1 pr-1">
314
- {contact}
315
- <button
316
- type="button"
317
- onClick={() => removeGeneralContact(field.value ?? [], index)}
318
- className="ml-1 hover:bg-muted rounded-sm p-0.5"
319
- >
320
- <X className="size-3" />
321
- </button>
322
- </Badge>
323
- ))}
324
- <div className="flex gap-2">
325
- <Input
326
- value={newContact}
327
- onChange={e => setNewContact(e.target.value)}
328
- onKeyDown={e => {
329
- if (e.key === 'Enter') {
330
- e.preventDefault();
331
- addGeneralContact(field.value ?? []);
332
- }
333
- }}
334
- placeholder="Add contact email"
335
- type="email"
336
- />
337
- <Button
338
- type="button"
339
- variant="outline"
340
- size="icon"
341
- disabled={!newContact.trim()}
342
- onClick={() => addGeneralContact(field.value ?? [])}
343
- >
344
- <Plus className="size-4" />
345
- </Button>
346
- </div>
347
- </div>
348
- </FormControl>
349
- <FormMessage />
350
- </FormItem>
351
- )}
352
- />
353
-
354
- <FormField
355
- control={control}
356
- name="website"
357
- render={({ field }) => (
358
- <FormItem>
359
- <FormLabel>
360
- <Globe className="size-4" />
361
- Website
362
- </FormLabel>
363
- <FormControl>
364
- <Input {...field} type="url" placeholder="https://example.com" />
365
- </FormControl>
366
- <FormMessage />
367
- </FormItem>
368
- )}
369
- />
370
- <FormField
371
- control={control}
372
- name="phone"
373
- render={({ field }) => (
374
- <FormItem>
375
- <FormLabel>
376
- <Phone className="size-4" />
377
- Phone
378
- </FormLabel>
379
- <FormControl>
380
- <Input {...field} type="tel" />
381
- </FormControl>
382
- <FormMessage />
383
- </FormItem>
384
- )}
385
- />
386
- </div>
387
- </div>
388
- );
389
- }
390
-
391
- function DefaultsSection({ form }: SectionProps) {
392
- const { selectableSortCodes, fetching: fetchingSortCodes, sortCodes } = useGetSortCodes();
393
- const { selectableTaxCategories, fetching: fetchingTaxCategories } = useGetTaxCategories();
394
- const { selectableTags, fetching: fetchingTags } = useGetTags();
395
-
396
- const { setValue, control } = form;
397
-
398
- // When sort code changes, update IRS code if sort code has a default IRS code
399
- const onSortCodeChangeUpdateIrsCode = useCallback(
400
- (sortCode: string | null) => {
401
- if (sortCode) {
402
- const sortCodeObj = sortCodes.find(sc => Number(sc.key) === Number(sortCode));
403
-
404
- if (sortCodeObj) {
405
- if (sortCodeObj.defaultIrsCode) {
406
- setValue('irsCode', sortCodeObj.defaultIrsCode, { shouldDirty: true });
407
- } else {
408
- setValue('irsCode', undefined, { shouldDirty: true });
409
- }
410
- }
411
- }
49
+ const formManager = useForm<InsertNewBusinessInput>({
50
+ defaultValues: {
51
+ name: description,
52
+ country: userContext?.context.locality || 'ISR',
53
+ suggestions: { phrases: [description] },
412
54
  },
413
- [setValue, sortCodes],
414
- );
415
-
416
- return (
417
- <div className="space-y-4">
418
- <h3 className="text-sm font-semibold text-foreground">Defaults</h3>
419
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
420
- <FormField
421
- control={control}
422
- name="sortCode"
423
- render={({ field }) => (
424
- <FormItem>
425
- <FormLabel>Sort Code</FormLabel>
426
- <FormControl>
427
- <ComboBox
428
- onChange={sortCode => {
429
- onSortCodeChangeUpdateIrsCode(sortCode);
430
- field.onChange(sortCode);
431
- }}
432
- data={selectableSortCodes}
433
- value={field.value}
434
- disabled={fetchingSortCodes}
435
- placeholder="Scroll to see all options"
436
- formPart
437
- />
438
- </FormControl>
439
- <FormMessage />
440
- </FormItem>
441
- )}
442
- />
443
-
444
- <FormField
445
- control={control}
446
- name="taxCategory"
447
- render={({ field }) => (
448
- <FormItem>
449
- <FormLabel>Tax Category</FormLabel>
450
- <FormControl>
451
- <ComboBox
452
- data={selectableTaxCategories}
453
- disabled={fetchingTaxCategories}
454
- value={field.value}
455
- onChange={field.onChange}
456
- placeholder="Scroll to see all options"
457
- formPart
458
- />
459
- </FormControl>
460
- <FormMessage />
461
- </FormItem>
462
- )}
463
- />
464
-
465
- <FormField
466
- control={control}
467
- name="pcn874RecordType"
468
- render={({ field }) => (
469
- <FormItem>
470
- <FormLabel>PCN874 Record Type</FormLabel>
471
- <Select value={field.value} onValueChange={field.onChange}>
472
- <FormControl>
473
- <SelectTrigger>
474
- <SelectValue placeholder="Select record type" />
475
- </SelectTrigger>
476
- </FormControl>
477
- <SelectContent>
478
- {Object.entries(pcn874RecordEnum).map(([value, label]) => (
479
- <SelectItem key={value} value={value}>
480
- {`${label} (${value})`}
481
- </SelectItem>
482
- ))}
483
- </SelectContent>
484
- </Select>
485
- <FormMessage />
486
- </FormItem>
487
- )}
488
- />
489
-
490
- <FormField
491
- control={control}
492
- name="irsCode"
493
- render={({ field }) => (
494
- <FormItem>
495
- <FormLabel>IRS Code</FormLabel>
496
- <FormControl>
497
- <NumberInput
498
- value={field.value ?? undefined}
499
- onValueChange={value => field.onChange(value ?? null)}
500
- hideControls
501
- decimalScale={0}
502
- />
503
- </FormControl>
504
- <FormMessage />
505
- </FormItem>
506
- )}
507
- />
508
-
509
- <FormField
510
- control={control}
511
- name="defaultDescription"
512
- render={({ field }) => (
513
- <FormItem className="md:col-span-2">
514
- <FormLabel>Default Description</FormLabel>
515
- <FormControl>
516
- <Input placeholder="Enter default charge description" {...field} />
517
- </FormControl>
518
- <FormMessage />
519
- </FormItem>
520
- )}
521
- />
522
-
523
- <FormField
524
- control={control}
525
- name="defaultTags"
526
- render={({ field }) => (
527
- <FormItem>
528
- <FormLabel>Tags</FormLabel>
529
- <FormControl>
530
- <MultiSelect
531
- options={Object.values(selectableTags).map(({ value, label }) => ({
532
- label,
533
- value,
534
- }))}
535
- onValueChange={field.onChange}
536
- defaultValue={field.value}
537
- value={field.value}
538
- placeholder="Select Default Tags"
539
- variant="default"
540
- disabled={fetchingTags}
541
- />
542
- </FormControl>
543
- <FormMessage />
544
- </FormItem>
545
- )}
546
- />
547
- </div>
548
- </div>
549
- );
550
- }
551
-
552
- function AutoMatchingSection({ form }: SectionProps) {
553
- const [newPhrase, setNewPhrase] = useState('');
554
- const [newEmail, setNewEmail] = useState('');
555
-
556
- const { getValues, setValue, control } = form;
557
-
558
- const addPhrase = () => {
559
- if (newPhrase.trim()) {
560
- const currentPhrases = getValues('transactionPhrases');
561
- form.setValue('transactionPhrases', [...currentPhrases, newPhrase.trim()], {
562
- shouldDirty: true,
563
- });
564
- setNewPhrase('');
565
- }
566
- };
567
-
568
- const addEmail = () => {
569
- if (newEmail.trim()) {
570
- const currentEmails = getValues('emailAddresses');
571
- setValue('emailAddresses', [...currentEmails, newEmail.trim()], { shouldDirty: true });
572
- setNewEmail('');
573
- }
574
- };
55
+ });
56
+ const { handleSubmit } = formManager;
57
+ const [fetching, setFetching] = useState(false);
575
58
 
576
- const removePhrase = (index: number) => {
577
- const currentPhrases = getValues('transactionPhrases');
578
- setValue(
579
- 'transactionPhrases',
580
- currentPhrases.filter((_, i) => i !== index),
581
- { shouldDirty: true },
582
- );
583
- };
59
+ const { insertBusiness, fetching: addingInProcess } = useInsertBusiness();
584
60
 
585
- const removeEmail = (index: number) => {
586
- const currentEmails = getValues('emailAddresses');
587
- setValue(
588
- 'emailAddresses',
589
- currentEmails.filter((_, i) => i !== index),
590
- { shouldDirty: true },
591
- );
61
+ const onSubmit: SubmitHandler<InsertNewBusinessInput> = data => {
62
+ data.sortCode &&= parseInt(data.sortCode.toString());
63
+ insertBusiness({ fields: data }).then(res => {
64
+ if (res?.id) {
65
+ onAdd?.(res.id);
66
+ close();
67
+ }
68
+ });
592
69
  };
593
70
 
594
71
  return (
595
- <div className="space-y-4">
596
- <h3 className="text-sm font-semibold text-foreground">Auto Matching</h3>{' '}
597
- <p className="text-sm text-muted-foreground">
598
- Configure patterns for automatic matching of bank transactions and documents
599
- </p>
600
- <div className="grid grid-cols-1 gap-4">
601
- <FormField
602
- control={control}
603
- name="transactionPhrases"
604
- render={({ field }) => (
605
- <FormItem>
606
- <FormLabel>Transaction Phrases</FormLabel>
607
- {!!field.value?.length && (
608
- <div className="flex flex-wrap gap-2">
609
- {field.value.map((phrase, index) => (
610
- <Badge key={index} variant="secondary" className="gap-1">
611
- {phrase}
612
- <Button
613
- variant="ghost"
614
- size="icon"
615
- className="p-0 size-3"
616
- onClick={() => removePhrase(index)}
617
- >
618
- <X className="size-3 cursor-pointer" onClick={() => removePhrase(index)} />
619
- </Button>
620
- </Badge>
621
- ))}
622
- </div>
623
- )}
624
- <div className="flex gap-2">
625
- <Input
626
- placeholder="Add phrase..."
627
- value={newPhrase}
628
- onChange={e => setNewPhrase(e.target.value)}
629
- onKeyDown={e => {
630
- if (e.key === 'Enter') {
631
- e.preventDefault();
632
- addPhrase();
633
- }
634
- }}
635
- />
636
- <Button type="button" size="sm" onClick={addPhrase}>
637
- <Plus className="size-4" />
638
- </Button>
639
- </div>
640
- <FormMessage />
641
- </FormItem>
642
- )}
643
- />
644
-
645
- <FormField
646
- control={control}
647
- name="emailAddresses"
648
- render={({ field }) => (
649
- <FormItem>
650
- <FormLabel>Email Addresses</FormLabel>
651
- {!!field.value?.length && (
652
- <div className="flex flex-wrap gap-2">
653
- {field.value.map((email, index) => (
654
- <Badge key={index} variant="secondary" className="gap-1">
655
- {email}
656
- <Button
657
- variant="ghost"
658
- size="icon"
659
- className="p-0 size-3"
660
- onClick={() => removeEmail(index)}
661
- >
662
- <X className="size-3 cursor-pointer" />
663
- </Button>
664
- </Badge>
665
- ))}
666
- </div>
667
- )}
668
- <div className="flex gap-2">
669
- <Input
670
- type="email"
671
- placeholder="Add email..."
672
- value={newEmail}
673
- onChange={e => setNewEmail(e.target.value)}
674
- onKeyDown={e => {
675
- if (e.key === 'Enter') {
676
- e.preventDefault();
677
- addEmail();
678
- }
679
- }}
680
- />
681
- <Button type="button" size="sm" onClick={addEmail}>
682
- <Plus className="size-4" />
683
- </Button>
684
- </div>
685
- <FormMessage />
686
- </FormItem>
687
- )}
688
- />
689
- </div>
690
- </div>
72
+ <Form {...formManager}>
73
+ <form onSubmit={handleSubmit(onSubmit)}>
74
+ <InsertBusinessFields formManager={formManager} setFetching={setFetching} />
75
+
76
+ <div className="flex justify-center mt-4">
77
+ <Button type="submit" disabled={addingInProcess || fetching}>
78
+ Add
79
+ </Button>
80
+ </div>
81
+ </form>
82
+ </Form>
691
83
  );
692
84
  }