@accounter/client 0.0.8-alpha-20251023122652-765c7f951395441461726b3b4345eebd020632d7 → 0.0.8-alpha-20251023122900-7a815283add88dac9904c7a135ddeb225f89589c

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 +19 -16
  2. package/dist/assets/{Checkbox-CpANMiXr.js → Checkbox-Bu7lbyW3.js} +2 -2
  3. package/dist/assets/{Progress-BEKiyNRK.js → Progress-Cq1kNJW5.js} +1 -1
  4. package/dist/assets/{Typography-C0c5augK.js → Typography-qy9vYtPs.js} +1 -1
  5. package/dist/assets/{accordion-ZFVTCyDy.js → accordion-DOoNLp8s.js} +1 -1
  6. package/dist/assets/accountant-approvals-Be5zF2_c.js +1 -0
  7. package/dist/assets/{all-charges-BTyaAlp7.js → all-charges-CkXCeGgB.js} +1 -1
  8. package/dist/assets/{arrow-up-down-PQdrJdei.js → arrow-up-down-DvV6Dq8U.js} +1 -1
  9. package/dist/assets/building-2-B-rX_xIy.js +6 -0
  10. package/dist/assets/business-CNkwr3_Z.js +32 -0
  11. package/dist/assets/business-header-Drp6R3A0.js +1 -0
  12. package/dist/assets/{business-transactions-single-DJqvqlMO.js → business-transactions-single-BSpUxu3D.js} +1 -1
  13. package/dist/assets/{business-trip-BXAjI1mO.js → business-trip-Bv1KXZPV.js} +1 -1
  14. package/dist/assets/charges-filters-CpNNXLPU.js +1 -0
  15. package/dist/assets/{charges-ledger-validation-CFc_zJFO.js → charges-ledger-validation-BcO2gYIK.js} +1 -1
  16. package/dist/assets/{chart-C5LuxRSv.js → chart-Cg9PgOtK.js} +1 -1
  17. package/dist/assets/{data-table-pagination-g2cF8_uB.js → data-table-pagination-aFECnXu9.js} +1 -1
  18. package/dist/assets/{editable-business-trip-DyQsUJjl.js → editable-business-trip-qxr12Yt0.js} +2 -2
  19. package/dist/assets/index--C4Az49P.js +1 -0
  20. package/dist/assets/index-A1xHtOLI.js +1 -0
  21. package/dist/assets/{index-Bpef3vuz.js → index-B2_BlI07.js} +2 -2
  22. package/dist/assets/{index-C0OGGVXE.js → index-BDsbL_Kg.js} +2 -2
  23. package/dist/assets/{index-BSg8ocop.js → index-BFERto_M.js} +2 -2
  24. package/dist/assets/{index-gBZ-7Z07.js → index-BNXVR9UW.js} +1 -1
  25. package/dist/assets/{index-Is4LJW4y.js → index-BYyokQ1_.js} +2 -2
  26. package/dist/assets/index-BrsmDzry.js +1 -0
  27. package/dist/assets/index-C3AdAlvt.js +9 -0
  28. package/dist/assets/index-CD53YLxm.js +1 -0
  29. package/dist/assets/index-CY2CmEvR.js +1 -0
  30. package/dist/assets/index-Crn32InT.js +1 -0
  31. package/dist/assets/{index-6-J73JPC.js → index-CuIQrA5k.js} +1 -1
  32. package/dist/assets/index-CyI9xpFz.js +1 -0
  33. package/dist/assets/{index-CN2818Wt.js → index-DOrujuWI.js} +233 -223
  34. package/dist/assets/index-DStQk1jH.js +1 -0
  35. package/dist/assets/{index-C99l-a0m.js → index-Dh7go2PN.js} +1 -1
  36. package/dist/assets/index-DtE5Y1ZB.css +1 -0
  37. package/dist/assets/index-DwzG6-LK.js +1 -0
  38. package/dist/assets/{index-CJdrnxxy.js → index-GwuBtmZO.js} +2 -2
  39. package/dist/assets/{index-o_W5PWRq.js → index-M3bu66aM.js} +6 -6
  40. package/dist/assets/{index-S6eSocQH.js → index-aW2k8oov.js} +7 -7
  41. package/dist/assets/index-bPa4yB-5.js +1 -0
  42. package/dist/assets/index-vHwCjYSy.js +1 -0
  43. package/dist/assets/{index.es-DZgQ4YcV.js → index.es-Bg5AVQ7i.js} +1 -1
  44. package/dist/assets/{issue-document-D-JoFiMZ.js → issue-document-B4ZlDg1e.js} +1 -1
  45. package/dist/assets/login-page-v4kHU0le.js +1 -0
  46. package/dist/assets/{missing-info-charges-CPnjfjfW.js → missing-info-charges-Ck_n6s6i.js} +1 -1
  47. package/dist/assets/page-not-found-TWjTuISB.js +1 -0
  48. package/dist/assets/{pencil-BUwX_eHK.js → pencil-CpRI4b2y.js} +1 -1
  49. package/dist/assets/report-commentary-row-BNIW-6Qg.js +1 -0
  50. package/dist/assets/save-CjLvssI1.js +6 -0
  51. package/dist/assets/{sub-C2zifPY5.js → sub-Daf_8shG.js} +1 -1
  52. package/dist/assets/subMonths-DIZWyi5T.js +1 -0
  53. package/dist/index.html +2 -2
  54. package/package.json +1 -1
  55. package/src/components/business/business-header.tsx +21 -2
  56. package/src/components/businesses/index.tsx +56 -39
  57. package/src/components/common/forms/index.ts +0 -1
  58. package/src/components/common/modals/insert-business.tsx +659 -51
  59. package/src/gql/gql.ts +6 -30
  60. package/src/gql/graphql.ts +7 -30
  61. package/src/providers/urql-client.ts +3 -2
  62. package/src/providers/urql.tsx +0 -5
  63. package/src/router/loaders/business-loader.ts +4 -2
  64. package/src/router/loaders/charge-loader.ts +4 -2
  65. package/dist/assets/accountant-approvals-CpzuSBgK.js +0 -1
  66. package/dist/assets/business-DiQFqEHI.js +0 -42
  67. package/dist/assets/charges-filters-r7FjeZLc.js +0 -1
  68. package/dist/assets/index-2g0J28D2.js +0 -1
  69. package/dist/assets/index-B5j8Fmt8.js +0 -1
  70. package/dist/assets/index-BSBV6pv2.js +0 -9
  71. package/dist/assets/index-BziuNiXZ.js +0 -1
  72. package/dist/assets/index-C7KKktQ5.js +0 -1
  73. package/dist/assets/index-CE3kLG2a.js +0 -1
  74. package/dist/assets/index-CjDRKTTf.js +0 -1
  75. package/dist/assets/index-CzzfC-dD.css +0 -1
  76. package/dist/assets/index-D2xdQJAx.js +0 -1
  77. package/dist/assets/index-DGvh10a7.js +0 -1
  78. package/dist/assets/index-DLI6Z9VU.js +0 -1
  79. package/dist/assets/index-DXL1qPt9.js +0 -1
  80. package/dist/assets/index-T-JiUNDA.js +0 -1
  81. package/dist/assets/login-page-DsHy5amR.js +0 -1
  82. package/dist/assets/page-not-found-CYmXF0n2.js +0 -1
  83. package/dist/assets/report-commentary-row-Bpvvtwpd.js +0 -1
  84. package/dist/assets/save-Ck68rh6u.js +0 -11
  85. package/dist/assets/similar-charges-by-business-modal-bUtiFfAw.js +0 -1
  86. package/dist/assets/subMonths-uUfFviD2.js +0 -1
  87. package/src/components/businesses/all-businesses-row.tsx +0 -87
  88. package/src/components/businesses/cells/hebrew-name.tsx +0 -31
  89. package/src/components/businesses/cells/index.ts +0 -2
  90. package/src/components/businesses/cells/name.tsx +0 -31
  91. package/src/components/common/forms/business-card.tsx +0 -234
  92. package/src/providers/urql-error-handler.ts +0 -27
@@ -1,84 +1,692 @@
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';
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';
7
7
  import {
8
8
  Dialog,
9
9
  DialogContent,
10
+ DialogDescription,
10
11
  DialogHeader,
11
12
  DialogTitle,
12
13
  DialogTrigger,
13
- } from '../../ui/dialog.js';
14
- import { Form } from '../../ui/form.js';
15
- import { InsertBusinessFields } from '../index.js';
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
+ }
16
111
 
17
112
  export function InsertBusiness({
18
113
  description,
19
114
  onAdd,
20
115
  }: {
21
- description: string;
116
+ description?: string;
22
117
  onAdd?: (businessId: string) => void;
23
- }): ReactElement {
24
- const [open, setOpen] = useState(false);
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
+ };
25
142
 
26
143
  return (
27
- <Dialog open={open} onOpenChange={setOpen}>
144
+ <Dialog open={isNewBusinessOpen} onOpenChange={setIsNewBusinessOpen}>
28
145
  <DialogTrigger asChild>
29
- <Button>Add Business</Button>
146
+ <Button>
147
+ <Plus className="size-4 mr-2" />
148
+ New Business
149
+ </Button>
30
150
  </DialogTrigger>
31
- <DialogContent className="w-[90vw] max-w-screen-md">
151
+ <DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
32
152
  <DialogHeader>
33
- <DialogTitle>Add New Business</DialogTitle>
153
+ <DialogTitle>Create New Business</DialogTitle>
154
+ <DialogDescription>Add a new business to Accounter.</DialogDescription>
34
155
  </DialogHeader>
35
- <CreateBusinessForm description={description} close={() => setOpen(false)} onAdd={onAdd} />
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>
36
174
  </DialogContent>
37
175
  </Dialog>
38
176
  );
39
177
  }
40
178
 
41
- type CreateBusinessFormProps = {
42
- close: () => void;
43
- onAdd?: (businessId: string) => void;
44
- description: string;
45
- };
179
+ interface SectionProps {
180
+ form: ReturnType<typeof useForm<BusinessFormValues>>;
181
+ }
182
+
183
+ function ContactInformationSection({ form }: SectionProps) {
184
+ const [newContact, setNewContact] = useState('');
46
185
 
47
- function CreateBusinessForm({ description, close, onAdd }: CreateBusinessFormProps): ReactElement {
48
186
  const { userContext } = useContext(UserContext);
49
- const formManager = useForm<InsertNewBusinessInput>({
50
- defaultValues: {
51
- name: description,
52
- country: userContext?.context.locality || 'ISR',
53
- suggestions: { phrases: [description] },
54
- },
55
- });
56
- const { handleSubmit } = formManager;
57
- const [fetching, setFetching] = useState(false);
187
+ const { countries, fetching: fetchingCountries } = useAllCountries();
58
188
 
59
- const { insertBusiness, fetching: addingInProcess } = useInsertBusiness();
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
+ };
60
201
 
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();
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
+ }
67
411
  }
68
- });
412
+ },
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
+ };
575
+
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
+ };
584
+
585
+ const removeEmail = (index: number) => {
586
+ const currentEmails = getValues('emailAddresses');
587
+ setValue(
588
+ 'emailAddresses',
589
+ currentEmails.filter((_, i) => i !== index),
590
+ { shouldDirty: true },
591
+ );
69
592
  };
70
593
 
71
594
  return (
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>
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>
83
691
  );
84
692
  }