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