@accounter/client 0.0.7-alpha-20251021062721-ab43b5f013b9852ff5216bc141852da9dcfe0df7 → 0.0.8-alpha-20251021150501-2328478f6fd03d3eb9b501fe1840430e6983940d

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 (48) hide show
  1. package/CHANGELOG.md +109 -105
  2. package/dist/assets/index-CqaS5jWM.css +1 -0
  3. package/dist/assets/index-IlfO2QvT.js +1188 -0
  4. package/dist/assets/{index.es-NOVMdy_X.js → index.es-BaJmdn-u.js} +1 -1
  5. package/dist/index.html +2 -2
  6. package/package.json +5 -6
  7. package/src/app.tsx +4 -8
  8. package/src/components/business-transactions/business-extended-info.tsx +13 -9
  9. package/src/components/charges/charge-extended-info-menu.tsx +21 -27
  10. package/src/components/charges/charges-row.tsx +10 -12
  11. package/src/components/charges/charges-table.tsx +9 -15
  12. package/src/components/common/documents/issue-document/index.tsx +3 -3
  13. package/src/components/common/documents/issue-document/{recent-business-docs.tsx → recent-client-docs.tsx} +13 -19
  14. package/src/components/common/forms/business-card.tsx +0 -1
  15. package/src/components/common/forms/modify-business-fields.tsx +19 -2
  16. package/src/components/common/inputs/combo-box.tsx +1 -1
  17. package/src/components/reports/trial-balance-report/trial-balance-report-group.tsx +6 -4
  18. package/src/components/reports/trial-balance-report/trial-balance-report-sort-code.tsx +11 -8
  19. package/src/components/screens/documents/issue-documents/edit-issue-document-modal.tsx +4 -4
  20. package/src/gql/gql.ts +9 -93
  21. package/src/gql/graphql.ts +9 -285
  22. package/src/helpers/currency.ts +0 -5
  23. package/src/helpers/index.ts +0 -2
  24. package/src/hooks/use-get-all-contracts.ts +1 -0
  25. package/dist/assets/index-CJB5mMpd.js +0 -1224
  26. package/dist/assets/index-CzRRUK04.css +0 -1
  27. package/src/components/business/business-header.tsx +0 -65
  28. package/src/components/business/charges-section.tsx +0 -82
  29. package/src/components/business/charts-section.tsx +0 -115
  30. package/src/components/business/configurations-section.tsx +0 -835
  31. package/src/components/business/contact-info-section.tsx +0 -544
  32. package/src/components/business/contracts-section.tsx +0 -195
  33. package/src/components/business/documents-section.tsx +0 -26
  34. package/src/components/business/index.tsx +0 -171
  35. package/src/components/business/integrations-section.tsx +0 -479
  36. package/src/components/business/transactions-section.tsx +0 -26
  37. package/src/components/clients/contracts/modify-contract-dialog.tsx +0 -466
  38. package/src/components/clients/modify-client-dialog.tsx +0 -276
  39. package/src/components/screens/businesses/business.tsx +0 -50
  40. package/src/components/ui/progress.tsx +0 -25
  41. package/src/components/ui/skeleton.tsx +0 -12
  42. package/src/helpers/contracts.ts +0 -22
  43. package/src/helpers/pcn874.ts +0 -17
  44. package/src/hooks/use-create-contract.ts +0 -62
  45. package/src/hooks/use-delete-contract.ts +0 -64
  46. package/src/hooks/use-insert-client.ts +0 -80
  47. package/src/hooks/use-update-client.ts +0 -75
  48. package/src/hooks/use-update-contract.ts +0 -69
@@ -1,835 +0,0 @@
1
- import { useCallback, useContext, useEffect, useState } from 'react';
2
- import { Plus, Save, X } from 'lucide-react';
3
- import { useForm } from 'react-hook-form';
4
- import { Badge } from '@/components/ui/badge.js';
5
- import { Button } from '@/components/ui/button.js';
6
- import {
7
- Card,
8
- CardContent,
9
- CardDescription,
10
- CardFooter,
11
- CardHeader,
12
- CardTitle,
13
- } from '@/components/ui/card.js';
14
- import {
15
- Form,
16
- FormControl,
17
- FormDescription,
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 { Separator } from '@/components/ui/separator.js';
32
- import { Switch } from '@/components/ui/switch.js';
33
- import {
34
- BusinessConfigurationSectionFragmentDoc,
35
- EmailAttachmentType,
36
- Pcn874RecordType,
37
- type BusinessConfigurationSectionFragment,
38
- type UpdateBusinessInput,
39
- } from '@/gql/graphql.js';
40
- import { getFragmentData, type FragmentType } from '@/gql/index.js';
41
- import {
42
- dirtyFieldMarker,
43
- pcn874RecordEnum,
44
- relevantDataPicker,
45
- type MakeBoolean,
46
- } from '@/helpers/index.js';
47
- import { useGetSortCodes } from '@/hooks/use-get-sort-codes.js';
48
- import { useGetTags } from '@/hooks/use-get-tags.js';
49
- import { useGetTaxCategories } from '@/hooks/use-get-tax-categories.js';
50
- import { useUpdateBusiness } from '@/hooks/use-update-business.js';
51
- import { UserContext } from '@/providers/user-provider.js';
52
- import { ModifyClientDialog } from '../clients/modify-client-dialog.js';
53
- import {
54
- ComboBox,
55
- MultiSelect,
56
- NumberInput,
57
- SimilarChargesByBusinessModal,
58
- } from '../common/index.js';
59
-
60
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions -- used by codegen
61
- /* GraphQL */ `
62
- fragment BusinessConfigurationSection on Business {
63
- __typename
64
- id
65
- pcn874RecordType
66
- irsCode
67
- ... on LtdFinancialEntity {
68
- optionalVAT
69
- exemptDealer
70
- sortCode {
71
- id
72
- key
73
- defaultIrsCode
74
- }
75
- taxCategory {
76
- id
77
- }
78
- suggestions {
79
- phrases
80
- emails
81
- tags {
82
- id
83
- }
84
- description
85
- emailListener {
86
- internalEmailLinks
87
- emailBody
88
- attachments
89
- }
90
- }
91
- clientInfo {
92
- id
93
- }
94
- }
95
- }
96
- `;
97
-
98
- interface ConfigurationFormValues {
99
- isClient: boolean;
100
- // TODO: activate these fields later. requires additional backend support
101
- // isActive: boolean;
102
- // isReceiptEnough: boolean;
103
- // noDocsRequired: boolean;
104
- isVatOptional: boolean;
105
- isExemptDealer: boolean;
106
- sortCode: string;
107
- taxCategory: string;
108
- pcn874RecordType: Pcn874RecordType;
109
- irsCode: number | null;
110
- description: string;
111
- tags: string[];
112
- phrases: string[];
113
- emails: string[];
114
- internalLinks: string[];
115
- attachmentTypes: EmailAttachmentType[];
116
- useMessageBody: boolean;
117
- }
118
-
119
- function ConfigurationsSectionFragmentToFormValues(
120
- business?: BusinessConfigurationSectionFragment,
121
- ): Partial<ConfigurationFormValues> {
122
- if (!business || business.__typename !== 'LtdFinancialEntity') {
123
- return {} as ConfigurationFormValues;
124
- }
125
-
126
- return {
127
- isClient: !!business.clientInfo?.id,
128
- // TODO: activate these fields later. requires additional backend support
129
- // isActive: true,
130
- // isReceiptEnough: false,
131
- // noDocsRequired: false,
132
- isVatOptional: business.optionalVAT ?? undefined,
133
- isExemptDealer: business.exemptDealer ?? undefined,
134
- sortCode: business.sortCode?.key?.toString() ?? undefined,
135
- taxCategory: business.taxCategory?.id ?? undefined,
136
- pcn874RecordType: business?.pcn874RecordType ?? undefined,
137
- irsCode: business?.irsCode ?? undefined,
138
- description: business.suggestions?.description ?? undefined,
139
- tags: business.suggestions?.tags?.map(tag => tag.id) ?? undefined,
140
- phrases: business.suggestions?.phrases,
141
- emails: business.suggestions?.emails ?? undefined,
142
- internalLinks: business.suggestions?.emailListener?.internalEmailLinks ?? undefined,
143
- attachmentTypes: business.suggestions?.emailListener?.attachments ?? undefined,
144
- useMessageBody: business.suggestions?.emailListener?.emailBody ?? undefined,
145
- };
146
- }
147
-
148
- function convertFormDataToUpdateBusinessInput(
149
- formData: Partial<ConfigurationFormValues>,
150
- ): UpdateBusinessInput {
151
- const emailListenerDataExists =
152
- formData.internalLinks || formData.useMessageBody || formData.attachmentTypes;
153
- const emailListener:
154
- | NonNullable<UpdateBusinessInput['suggestions']>['emailListener']
155
- | undefined = emailListenerDataExists
156
- ? {
157
- internalEmailLinks: formData.internalLinks,
158
- emailBody: formData.useMessageBody,
159
- attachments: formData.attachmentTypes,
160
- }
161
- : undefined;
162
-
163
- const suggestionsDataExists =
164
- formData.description || formData.tags || formData.phrases || formData.emails || emailListener;
165
- const suggestions: UpdateBusinessInput['suggestions'] | undefined = suggestionsDataExists
166
- ? {
167
- description: formData.description,
168
- tags: formData.tags?.map(id => ({ id })),
169
- phrases: formData.phrases,
170
- emails: formData.emails,
171
- emailListener,
172
- }
173
- : undefined;
174
-
175
- return {
176
- optionalVAT: formData.isVatOptional,
177
- exemptDealer: formData.isExemptDealer,
178
- sortCode: formData.sortCode ? parseInt(formData.sortCode) : undefined,
179
- taxCategory: formData.taxCategory,
180
- pcn874RecordType: formData.pcn874RecordType,
181
- irsCode: formData.irsCode,
182
- suggestions,
183
- };
184
- }
185
-
186
- interface Props {
187
- data?: FragmentType<typeof BusinessConfigurationSectionFragmentDoc>;
188
- refetchBusiness?: () => Promise<void>;
189
- }
190
-
191
- export function ConfigurationsSection({ data, refetchBusiness }: Props) {
192
- const business = getFragmentData(BusinessConfigurationSectionFragmentDoc, data);
193
- const [defaultFormValues, setDefaultFormValues] = useState(
194
- ConfigurationsSectionFragmentToFormValues(business),
195
- );
196
- const { userContext } = useContext(UserContext);
197
-
198
- const { updateBusiness: updateDbBusiness, fetching: isBusinessUpdating } = useUpdateBusiness();
199
-
200
- const [similarChargesOpen, setSimilarChargesOpen] = useState(false);
201
- const [similarChargesData, setSimilarChargesData] = useState<
202
- | {
203
- tagIds?: { id: string }[];
204
- description?: string;
205
- }
206
- | undefined
207
- >(undefined);
208
- const [newPhrase, setNewPhrase] = useState('');
209
- const [newEmail, setNewEmail] = useState('');
210
- const [newLink, setNewLink] = useState('');
211
-
212
- const form = useForm<ConfigurationFormValues>({
213
- defaultValues: defaultFormValues,
214
- });
215
-
216
- // Sort codes array handle
217
- const { selectableSortCodes, fetching: fetchingSortCodes, sortCodes } = useGetSortCodes();
218
-
219
- // Tax categories array handle
220
- const { selectableTaxCategories, fetching: fetchingTaxCategories } = useGetTaxCategories();
221
-
222
- // Tags handle
223
- const { selectableTags, fetching: fetchingTags } = useGetTags();
224
-
225
- const availableAttachmentTypes = Object.values(EmailAttachmentType);
226
-
227
- const addPhrase = () => {
228
- if (newPhrase.trim()) {
229
- const currentPhrases = form.getValues('phrases');
230
- form.setValue('phrases', [...currentPhrases, newPhrase.trim()], { shouldDirty: true });
231
- setNewPhrase('');
232
- }
233
- };
234
-
235
- const addEmail = () => {
236
- if (newEmail.trim()) {
237
- const currentEmails = form.getValues('emails');
238
- form.setValue('emails', [...currentEmails, newEmail.trim()], { shouldDirty: true });
239
- setNewEmail('');
240
- }
241
- };
242
-
243
- const addLink = () => {
244
- if (newLink.trim()) {
245
- const currentLinks = form.getValues('internalLinks');
246
- form.setValue('internalLinks', [...currentLinks, newLink.trim()], { shouldDirty: true });
247
- setNewLink('');
248
- }
249
- };
250
-
251
- const removePhrase = (index: number) => {
252
- const currentPhrases = form.getValues('phrases');
253
- form.setValue(
254
- 'phrases',
255
- currentPhrases.filter((_, i) => i !== index),
256
- { shouldDirty: true },
257
- );
258
- };
259
-
260
- const removeEmail = (index: number) => {
261
- const currentEmails = form.getValues('emails');
262
- form.setValue(
263
- 'emails',
264
- currentEmails.filter((_, i) => i !== index),
265
- { shouldDirty: true },
266
- );
267
- };
268
-
269
- const removeLink = (index: number) => {
270
- const currentLinks = form.getValues('internalLinks');
271
- form.setValue(
272
- 'internalLinks',
273
- currentLinks.filter((_, i) => i !== index),
274
- { shouldDirty: true },
275
- );
276
- };
277
-
278
- const toggleAttachmentType = (type: EmailAttachmentType) => {
279
- const currentTypes = form.getValues('attachmentTypes');
280
- form.setValue(
281
- 'attachmentTypes',
282
- currentTypes?.includes(type) ? currentTypes.filter(t => t !== type) : [...currentTypes, type],
283
- { shouldDirty: true },
284
- );
285
- };
286
-
287
- const onSubmit = async (values: Partial<ConfigurationFormValues>) => {
288
- if (!business || !userContext?.context.adminBusinessId) {
289
- return;
290
- }
291
-
292
- const dataToUpdate = relevantDataPicker(
293
- values,
294
- form.formState.dirtyFields as MakeBoolean<typeof values>,
295
- );
296
-
297
- if (!dataToUpdate) return;
298
-
299
- const updateBusinessInput = convertFormDataToUpdateBusinessInput(dataToUpdate);
300
-
301
- await updateDbBusiness({
302
- businessId: business.id,
303
- ownerId: userContext.context.adminBusinessId,
304
- fields: updateBusinessInput,
305
- });
306
-
307
- if (dataToUpdate.tags?.length || dataToUpdate.description) {
308
- // Show similar charges modal if tags or description were updated
309
- setSimilarChargesData({
310
- tagIds: dataToUpdate.tags?.map(id => ({ id })),
311
- description: dataToUpdate.description,
312
- });
313
- setSimilarChargesOpen(true);
314
- } else {
315
- // Otherwise, just refetch business data
316
- refetchBusiness?.();
317
- }
318
- };
319
-
320
- // When sort code changes, update IRS code if sort code has a default IRS code
321
- const onSortCodeChangeUpdateIrsCode = useCallback(
322
- (sortCode: string | null) => {
323
- if (sortCode) {
324
- const sortCodeObj = sortCodes.find(sc => Number(sc.key) === Number(sortCode));
325
-
326
- if (sortCodeObj) {
327
- if (sortCodeObj.defaultIrsCode) {
328
- form.setValue('irsCode', sortCodeObj.defaultIrsCode, { shouldDirty: true });
329
- } else {
330
- form.setValue('irsCode', null, { shouldDirty: true });
331
- }
332
- }
333
- }
334
- },
335
- [form, sortCodes],
336
- );
337
-
338
- useEffect(() => {
339
- if (business) {
340
- const formValues = ConfigurationsSectionFragmentToFormValues(business);
341
- setDefaultFormValues(formValues);
342
- form.reset(formValues);
343
- }
344
- }, [business, form]);
345
-
346
- if (!business) {
347
- return <div />;
348
- }
349
-
350
- const isClient = form.watch('isClient');
351
-
352
- return (
353
- <Card>
354
- <CardHeader>
355
- <div className="flex items-center justify-between">
356
- <div>
357
- <CardTitle>Configurations</CardTitle>
358
- <CardDescription>
359
- Business status, tax settings, automation rules, and integration preferences
360
- </CardDescription>
361
- </div>
362
- {!isClient && <ModifyClientDialog businessId={business.id} onDone={refetchBusiness} />}
363
- </div>
364
- </CardHeader>
365
- <Form {...form}>
366
- <form onSubmit={form.handleSubmit(onSubmit)}>
367
- <CardContent className="space-y-6">
368
- <div className="space-y-4">
369
- <h3 className="text-sm font-semibold text-foreground">Business Status & Behavior</h3>
370
- <div className="space-y-4">
371
- <FormField
372
- control={form.control}
373
- name="isClient"
374
- render={({ field }) => (
375
- <FormItem className="flex items-center justify-between">
376
- <div className="space-y-0.5">
377
- <FormLabel>Is Client</FormLabel>
378
- <FormDescription>Mark this business as a client</FormDescription>
379
- </div>
380
- <FormControl>
381
- <Switch disabled checked={field.value} onCheckedChange={field.onChange} />
382
- </FormControl>
383
- </FormItem>
384
- )}
385
- />
386
-
387
- {/* TODO: activate these fields later. requires additional backend support */}
388
- {/* <FormField
389
- control={form.control}
390
- name="isActive"
391
- render={({ field }) => (
392
- <FormItem className="flex items-center justify-between">
393
- <div className="space-y-0.5">
394
- <FormLabel>Is Active</FormLabel>
395
- <FormDescription>Business is currently active</FormDescription>
396
- </div>
397
- <FormControl>
398
- <Switch checked={field.value} onCheckedChange={field.onChange} />
399
- </FormControl>
400
- </FormItem>
401
- )}
402
- /> */}
403
-
404
- {/* <FormField
405
- control={form.control}
406
- name="isReceiptEnough"
407
- render={({ field }) => (
408
- <FormItem className="flex items-center justify-between">
409
- <div className="space-y-0.5">
410
- <FormLabel>Is Receipt Enough</FormLabel>
411
- <FormDescription>
412
- Generate ledger for receipt documents if no invoice available
413
- </FormDescription>
414
- </div>
415
- <FormControl>
416
- <Switch checked={field.value} onCheckedChange={field.onChange} />
417
- </FormControl>
418
- </FormItem>
419
- )}
420
- /> */}
421
-
422
- {/* <FormField
423
- control={form.control}
424
- name="noDocsRequired"
425
- render={({ field }) => (
426
- <FormItem className="flex items-center justify-between">
427
- <div className="space-y-0.5">
428
- <FormLabel>No Docs Required</FormLabel>
429
- <FormDescription>
430
- Skip document validation for common charges
431
- </FormDescription>
432
- </div>
433
- <FormControl>
434
- <Switch checked={field.value} onCheckedChange={field.onChange} />
435
- </FormControl>
436
- </FormItem>
437
- )}
438
- /> */}
439
-
440
- <FormField
441
- control={form.control}
442
- name="isVatOptional"
443
- render={({ field }) => (
444
- <FormItem className="flex items-center justify-between">
445
- <div className="space-y-0.5">
446
- <FormLabel>Is VAT Optional</FormLabel>
447
- <FormDescription>Mute missing VAT indicator</FormDescription>
448
- </div>
449
- <FormControl>
450
- <Switch checked={field.value} onCheckedChange={field.onChange} />
451
- </FormControl>
452
- </FormItem>
453
- )}
454
- />
455
-
456
- <FormField
457
- control={form.control}
458
- name="isExemptDealer"
459
- render={({ field }) => (
460
- <FormItem className="flex items-center justify-between">
461
- <div className="space-y-0.5">
462
- <FormLabel>Is Exempt Dealer</FormLabel>
463
- <FormDescription>Business is exempt from VAT requirements</FormDescription>
464
- </div>
465
- <FormControl>
466
- <Switch checked={field.value} onCheckedChange={field.onChange} />
467
- </FormControl>
468
- </FormItem>
469
- )}
470
- />
471
- </div>
472
- </div>
473
-
474
- <Separator />
475
-
476
- <div className="space-y-4">
477
- <h3 className="text-sm font-semibold text-foreground">Default Settings</h3>
478
- <div className="grid gap-4 md:grid-cols-2">
479
- <FormField
480
- control={form.control}
481
- name="sortCode"
482
- render={({ field, fieldState }) => (
483
- <FormItem>
484
- <FormLabel>Sort Code</FormLabel>
485
- <FormControl>
486
- <ComboBox
487
- onChange={sortCode => {
488
- onSortCodeChangeUpdateIrsCode(sortCode);
489
- field.onChange(sortCode);
490
- }}
491
- data={selectableSortCodes}
492
- value={field.value}
493
- disabled={fetchingSortCodes}
494
- placeholder="Scroll to see all options"
495
- formPart
496
- triggerProps={{
497
- className: dirtyFieldMarker(fieldState),
498
- }}
499
- />
500
- </FormControl>
501
- <FormMessage />
502
- </FormItem>
503
- )}
504
- />
505
-
506
- <FormField
507
- control={form.control}
508
- name="taxCategory"
509
- render={({ field, fieldState }) => (
510
- <FormItem>
511
- <FormLabel>Tax Category</FormLabel>
512
- <FormControl>
513
- <ComboBox
514
- data={selectableTaxCategories}
515
- disabled={fetchingTaxCategories}
516
- value={field.value}
517
- onChange={field.onChange}
518
- placeholder="Scroll to see all options"
519
- formPart
520
- triggerProps={{
521
- className: dirtyFieldMarker(fieldState),
522
- }}
523
- />
524
- </FormControl>
525
- <FormMessage />
526
- </FormItem>
527
- )}
528
- />
529
-
530
- <FormField
531
- control={form.control}
532
- name="pcn874RecordType"
533
- render={({ field, fieldState }) => (
534
- <FormItem>
535
- <FormLabel>PCN874 Record Type</FormLabel>
536
- <Select onValueChange={field.onChange} value={field.value}>
537
- <FormControl>
538
- <SelectTrigger className={dirtyFieldMarker(fieldState)}>
539
- <SelectValue />
540
- </SelectTrigger>
541
- </FormControl>
542
- <SelectContent>
543
- {Object.entries(pcn874RecordEnum).map(([value, label]) => (
544
- <SelectItem key={value} value={value}>
545
- {`${label} (${value})`}
546
- </SelectItem>
547
- ))}
548
- </SelectContent>
549
- </Select>
550
- <FormMessage />
551
- </FormItem>
552
- )}
553
- />
554
-
555
- <FormField
556
- control={form.control}
557
- name="irsCode"
558
- render={({ field, fieldState }) => (
559
- <FormItem>
560
- <FormLabel>IRS Code</FormLabel>
561
- <FormControl>
562
- <NumberInput
563
- value={field.value ?? undefined}
564
- onValueChange={value => field.onChange(value ?? null)}
565
- hideControls
566
- decimalScale={0}
567
- className={dirtyFieldMarker(fieldState)}
568
- />
569
- </FormControl>
570
- <FormMessage />
571
- </FormItem>
572
- )}
573
- />
574
-
575
- <FormField
576
- control={form.control}
577
- name="description"
578
- render={({ field, fieldState }) => (
579
- <FormItem>
580
- <FormLabel>Charge Description</FormLabel>
581
- <FormControl>
582
- <Input
583
- placeholder="Enter default charge description"
584
- {...field}
585
- className={dirtyFieldMarker(fieldState)}
586
- />
587
- </FormControl>
588
- <FormMessage />
589
- </FormItem>
590
- )}
591
- />
592
-
593
- <FormField
594
- control={form.control}
595
- name="tags"
596
- render={({ field, fieldState }) => (
597
- <FormItem>
598
- <FormLabel>Tags</FormLabel>
599
- <FormControl>
600
- <MultiSelect
601
- options={Object.values(selectableTags).map(({ value, label }) => ({
602
- label,
603
- value,
604
- }))}
605
- onValueChange={field.onChange}
606
- defaultValue={field.value}
607
- value={field.value}
608
- placeholder="Select Default Tags"
609
- variant="default"
610
- disabled={fetchingTags}
611
- className={dirtyFieldMarker(fieldState)}
612
- />
613
- </FormControl>
614
- <FormMessage />
615
- </FormItem>
616
- )}
617
- />
618
- </div>
619
- </div>
620
-
621
- <Separator />
622
-
623
- <div className="space-y-4">
624
- <h3 className="text-sm font-semibold text-foreground">Auto-matching Configuration</h3>
625
- <p className="text-sm text-muted-foreground">
626
- Configure patterns for automatic matching of bank transactions and documents
627
- </p>
628
-
629
- <FormField
630
- control={form.control}
631
- name="phrases"
632
- render={({ field, fieldState }) => (
633
- <FormItem>
634
- <FormLabel>Phrases</FormLabel>
635
- <div className="flex gap-2">
636
- <Input
637
- placeholder="Add phrase..."
638
- value={newPhrase}
639
- onChange={e => setNewPhrase(e.target.value)}
640
- onKeyDown={e => {
641
- if (e.key === 'Enter') {
642
- e.preventDefault();
643
- addPhrase();
644
- }
645
- }}
646
- className={dirtyFieldMarker(fieldState)}
647
- />
648
- <Button type="button" size="sm" onClick={addPhrase}>
649
- <Plus className="h-4 w-4" />
650
- </Button>
651
- </div>
652
- <div className="flex flex-wrap gap-2">
653
- {field.value?.map((phrase, index) => (
654
- <Badge key={index} variant="secondary" className="gap-1">
655
- {phrase}
656
- <Button
657
- variant="ghost"
658
- size="icon"
659
- className="p-0 size-3"
660
- onClick={() => removePhrase(index)}
661
- >
662
- <X
663
- className="size-3 cursor-pointer"
664
- onClick={() => removePhrase(index)}
665
- />
666
- </Button>
667
- </Badge>
668
- ))}
669
- </div>
670
- <FormMessage />
671
- </FormItem>
672
- )}
673
- />
674
-
675
- <FormField
676
- control={form.control}
677
- name="emails"
678
- render={({ field, fieldState }) => (
679
- <FormItem>
680
- <FormLabel>Email Addresses</FormLabel>
681
- <div className="flex gap-2">
682
- <Input
683
- type="email"
684
- placeholder="Add email..."
685
- value={newEmail}
686
- onChange={e => setNewEmail(e.target.value)}
687
- onKeyDown={e => {
688
- if (e.key === 'Enter') {
689
- e.preventDefault();
690
- addEmail();
691
- }
692
- }}
693
- className={dirtyFieldMarker(fieldState)}
694
- />
695
- <Button type="button" size="sm" onClick={addEmail}>
696
- <Plus className="h-4 w-4" />
697
- </Button>
698
- </div>
699
- <div className="flex flex-wrap gap-2">
700
- {field.value?.map((email, index) => (
701
- <Badge key={index} variant="secondary" className="gap-1">
702
- {email}
703
- <Button
704
- variant="ghost"
705
- size="icon"
706
- className="p-0 size-3"
707
- onClick={() => removeEmail(index)}
708
- >
709
- <X className="size-3 cursor-pointer" />
710
- </Button>
711
- </Badge>
712
- ))}
713
- </div>
714
- <FormMessage />
715
- </FormItem>
716
- )}
717
- />
718
- </div>
719
-
720
- <Separator />
721
-
722
- <div className="space-y-4">
723
- <h3 className="text-sm font-semibold text-foreground">Gmail Feature Configuration</h3>
724
- <p className="text-sm text-muted-foreground">
725
- Configure Gmail integration settings for document processing
726
- </p>
727
-
728
- <FormField
729
- control={form.control}
730
- name="attachmentTypes"
731
- render={({ field, fieldState }) => (
732
- <FormItem>
733
- <FormLabel>Attachment Types</FormLabel>
734
- <div
735
- className={dirtyFieldMarker(fieldState) + ' rounded-md flex flex-wrap gap-2'}
736
- >
737
- {availableAttachmentTypes.map(type => (
738
- <Badge
739
- key={type}
740
- variant={field.value?.includes(type) ? 'default' : 'outline'}
741
- className="cursor-pointer"
742
- onClick={() => toggleAttachmentType(type)}
743
- >
744
- {type}
745
- </Badge>
746
- ))}
747
- </div>
748
- <FormMessage />
749
- </FormItem>
750
- )}
751
- />
752
-
753
- <FormField
754
- control={form.control}
755
- name="internalLinks"
756
- render={({ field }) => (
757
- <FormItem>
758
- <FormLabel>Internal Links</FormLabel>
759
- <div className="flex gap-2">
760
- <Input
761
- type="url"
762
- placeholder="Add internal link..."
763
- value={newLink}
764
- onChange={e => setNewLink(e.target.value)}
765
- onKeyDown={e => {
766
- if (e.key === 'Enter') {
767
- e.preventDefault();
768
- addLink();
769
- }
770
- }}
771
- />
772
- <Button type="button" size="sm" onClick={addLink}>
773
- <Plus className="h-4 w-4" />
774
- </Button>
775
- </div>
776
- <div className="flex flex-wrap gap-2">
777
- {field.value?.map((link, index) => (
778
- <Badge key={index} variant="secondary" className="gap-1 max-w-xs truncate">
779
- {link}
780
- <Button
781
- variant="ghost"
782
- size="icon"
783
- className="p-0 size-3"
784
- onClick={() => removeLink(index)}
785
- >
786
- <X className="size-3 cursor-pointer flex-shrink-0" />
787
- </Button>
788
- </Badge>
789
- ))}
790
- </div>
791
- <FormMessage />
792
- </FormItem>
793
- )}
794
- />
795
-
796
- <FormField
797
- control={form.control}
798
- name="useMessageBody"
799
- render={({ field }) => (
800
- <FormItem className="flex items-center justify-between">
801
- <div className="space-y-0.5">
802
- <FormLabel>Should Use Message Body</FormLabel>
803
- <FormDescription>Extract information from email message body</FormDescription>
804
- </div>
805
- <FormControl>
806
- <Switch checked={field.value} onCheckedChange={field.onChange} />
807
- </FormControl>
808
- </FormItem>
809
- )}
810
- />
811
- </div>
812
- </CardContent>
813
- <CardFooter className="flex justify-end border-t pt-6">
814
- <Button
815
- type="submit"
816
- disabled={isBusinessUpdating || Object.keys(form.formState.dirtyFields).length === 0}
817
- >
818
- <Save className="h-4 w-4 mr-2" />
819
- Save Changes
820
- </Button>
821
- </CardFooter>
822
- </form>
823
- </Form>
824
-
825
- <SimilarChargesByBusinessModal
826
- businessId={business.id}
827
- tagIds={similarChargesData?.tagIds}
828
- description={similarChargesData?.description}
829
- open={similarChargesOpen}
830
- onOpenChange={setSimilarChargesOpen}
831
- onClose={refetchBusiness}
832
- />
833
- </Card>
834
- );
835
- }