@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/dist/assets/index-B2UYAO1O.css +1 -0
  3. package/dist/assets/index-BexxGuN6.js +1224 -0
  4. package/dist/assets/{index.es-DHwHzag1.js → index.es-CWwhWGxX.js} +1 -1
  5. package/dist/index.html +2 -2
  6. package/package.json +6 -5
  7. package/src/app.tsx +35 -25
  8. package/src/components/business/business-header.tsx +68 -0
  9. package/src/components/business/charges-section.tsx +82 -0
  10. package/src/components/business/charts-section.tsx +115 -0
  11. package/src/components/business/configurations-section.tsx +885 -0
  12. package/src/components/business/contact-info-section.tsx +536 -0
  13. package/src/components/business/contracts-section.tsx +196 -0
  14. package/src/components/business/documents-section.tsx +26 -0
  15. package/src/components/business/index.tsx +171 -0
  16. package/src/components/business/integrations-section.tsx +477 -0
  17. package/src/components/business/transactions-section.tsx +26 -0
  18. package/src/components/business-transactions/business-extended-info.tsx +11 -15
  19. package/src/components/business-transactions/business-transactions-single.tsx +1 -1
  20. package/src/components/business-transactions/index.tsx +1 -1
  21. package/src/components/charges/charge-extended-info-menu.tsx +27 -21
  22. package/src/components/charges/charges-row.tsx +12 -10
  23. package/src/components/charges/charges-table.tsx +15 -9
  24. package/src/components/clients/contracts/modify-contract-dialog.tsx +464 -0
  25. package/src/components/clients/modify-client-dialog.tsx +276 -0
  26. package/src/components/common/documents/issue-document/index.tsx +3 -3
  27. package/src/components/common/documents/issue-document/{recent-client-docs.tsx → recent-business-docs.tsx} +19 -13
  28. package/src/components/common/forms/business-card.tsx +1 -0
  29. package/src/components/common/forms/modify-business-fields.tsx +2 -19
  30. package/src/components/common/inputs/combo-box.tsx +1 -1
  31. package/src/components/layout/sidelinks.tsx +3 -3
  32. package/src/components/reports/trial-balance-report/trial-balance-report-group.tsx +4 -6
  33. package/src/components/reports/trial-balance-report/trial-balance-report-sort-code.tsx +8 -11
  34. package/src/components/screens/businesses/business.tsx +44 -0
  35. package/src/components/screens/documents/issue-documents/edit-issue-document-modal.tsx +4 -4
  36. package/src/components/ui/progress.tsx +25 -0
  37. package/src/components/ui/skeleton.tsx +12 -0
  38. package/src/gql/gql.ts +93 -9
  39. package/src/gql/graphql.ts +289 -9
  40. package/src/helpers/contracts.ts +22 -0
  41. package/src/helpers/currency.ts +5 -0
  42. package/src/helpers/index.ts +2 -0
  43. package/src/helpers/pcn874.ts +17 -0
  44. package/src/hooks/use-add-sort-code.ts +1 -1
  45. package/src/hooks/use-add-tag.ts +1 -1
  46. package/src/hooks/use-create-contract.ts +62 -0
  47. package/src/hooks/use-delete-contract.ts +64 -0
  48. package/src/hooks/use-delete-tag.ts +1 -1
  49. package/src/hooks/use-get-all-contracts.ts +0 -1
  50. package/src/hooks/use-insert-client.ts +80 -0
  51. package/src/hooks/use-merge-businesses.ts +1 -1
  52. package/src/hooks/use-merge-charges.ts +1 -1
  53. package/src/hooks/use-update-client.ts +75 -0
  54. package/src/hooks/use-update-contract.ts +69 -0
  55. package/dist/assets/index-0eCf1BcD.css +0 -1
  56. package/dist/assets/index-DHTbHvtz.js +0 -1188
@@ -0,0 +1,536 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import { Globe, Mail, MapPin, Phone, Plus, Save, 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
+ import {
8
+ Card,
9
+ CardContent,
10
+ CardDescription,
11
+ CardFooter,
12
+ CardHeader,
13
+ CardTitle,
14
+ } from '@/components/ui/card.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 { Textarea } from '@/components/ui/textarea.js';
25
+ import {
26
+ BusinessContactSectionFragmentDoc,
27
+ type BusinessContactSectionFragment,
28
+ type UpdateBusinessInput,
29
+ } from '@/gql/graphql.js';
30
+ import { getFragmentData, type FragmentType } from '@/gql/index.js';
31
+ import { dirtyFieldMarker, relevantDataPicker, type MakeBoolean } from '@/helpers/index.js';
32
+ import { useAllCountries } from '@/hooks/use-get-countries.js';
33
+ import { useUpdateBusiness } from '@/hooks/use-update-business.js';
34
+ import { UserContext } from '@/providers/user-provider.js';
35
+ import { zodResolver } from '@hookform/resolvers/zod';
36
+ import { ComboBox } from '../common';
37
+
38
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions -- used by codegen
39
+ /* GraphQL */ `
40
+ fragment BusinessContactSection on Business {
41
+ __typename
42
+ id
43
+ ... on LtdFinancialEntity {
44
+ name
45
+ hebrewName
46
+ country
47
+ governmentId
48
+ address
49
+ email
50
+ # localAddress
51
+ phoneNumber
52
+ website
53
+ clientInfo {
54
+ id
55
+ emails
56
+ }
57
+ }
58
+ }
59
+ `;
60
+
61
+ const contactInfoSchema = z.object({
62
+ businessName: z.string().min(1, 'Business name is required'),
63
+ locality: z.string().min(1, 'Locality is required'),
64
+ localName: z.string().optional(),
65
+ govId: z.string().optional(),
66
+ address: z.string().optional(),
67
+ localAddress: z.string().optional(),
68
+ phone: z.string().optional(),
69
+ website: z.url('Invalid URL').optional().or(z.literal('')),
70
+ generalContacts: z.array(z.email()).optional(),
71
+ billingEmails: z.array(z.email()).optional(),
72
+ });
73
+
74
+ type ContactInfoFormValues = z.infer<typeof contactInfoSchema>;
75
+
76
+ function ContactsSectionFragmentToFormValues(
77
+ business?: BusinessContactSectionFragment,
78
+ ): ContactInfoFormValues {
79
+ if (!business || business.__typename !== 'LtdFinancialEntity') {
80
+ return {} as ContactInfoFormValues;
81
+ }
82
+
83
+ return {
84
+ businessName: business.name,
85
+ locality: business.country,
86
+ localName: business.hebrewName ?? undefined,
87
+ govId: business.governmentId ?? undefined,
88
+ address: business.address ?? undefined,
89
+ // TODO: activate this field later. requires additional backend support
90
+ // localAddress: ,
91
+ phone: business.phoneNumber ?? undefined,
92
+ website: business.website ?? undefined,
93
+ generalContacts: business.email?.split(',').map(email => email.trim()),
94
+ billingEmails: business.clientInfo?.emails,
95
+ };
96
+ }
97
+
98
+ function convertFormDataToUpdateBusinessInput(
99
+ formData: Partial<ContactInfoFormValues>,
100
+ ): UpdateBusinessInput {
101
+ return {
102
+ name: formData.businessName,
103
+ country: formData.locality,
104
+ hebrewName: formData.localName,
105
+ governmentId: formData.govId,
106
+ address: formData.address,
107
+ // localAddress: formData.localAddress,
108
+ phoneNumber: formData.phone,
109
+ website: formData.website,
110
+ email: formData.generalContacts?.join(', '),
111
+ };
112
+ }
113
+
114
+ interface Props {
115
+ data?: FragmentType<typeof BusinessContactSectionFragmentDoc>;
116
+ refetchBusiness?: () => Promise<void>;
117
+ }
118
+
119
+ export function ContactInfoSection({ data, refetchBusiness }: Props) {
120
+ const business = getFragmentData(BusinessContactSectionFragmentDoc, data);
121
+ const [defaultFormValues, setDefaultFormValues] = useState(
122
+ ContactsSectionFragmentToFormValues(business),
123
+ );
124
+ const { userContext } = useContext(UserContext);
125
+
126
+ const { updateBusiness: updateDbBusiness, fetching: isBusinessUpdating } = useUpdateBusiness();
127
+
128
+ const form = useForm<ContactInfoFormValues>({
129
+ resolver: zodResolver(contactInfoSchema),
130
+ defaultValues: defaultFormValues,
131
+ });
132
+
133
+ // handle countries
134
+ const { countries, fetching: fetchingCountries } = useAllCountries();
135
+
136
+ const [newContact, setNewContact] = useState('');
137
+ const [newBillingEmail, setNewBillingEmail] = useState('');
138
+
139
+ const locality = form.watch('locality');
140
+ const address = form.watch('address');
141
+ const localAddress = form.watch('localAddress');
142
+
143
+ useEffect(() => {
144
+ if (address !== defaultFormValues.address) {
145
+ form.setValue('address', address, { shouldDirty: true });
146
+ }
147
+ }, [address, defaultFormValues.address, form]);
148
+
149
+ useEffect(() => {
150
+ if (localAddress !== defaultFormValues.localAddress) {
151
+ form.setValue('localAddress', localAddress, { shouldDirty: true });
152
+ }
153
+ }, [localAddress, defaultFormValues.localAddress, form]);
154
+
155
+ const isClient = business && 'clientInfo' in business && !!business.clientInfo;
156
+ const isLocalEntity = locality === userContext?.context.locality;
157
+
158
+ const addGeneralContact = (currentContacts: string[]) => {
159
+ if (newContact.trim()) {
160
+ form.setValue('generalContacts', [...currentContacts, newContact.trim()], {
161
+ shouldDirty: true,
162
+ });
163
+ setNewContact('');
164
+ }
165
+ };
166
+
167
+ const removeGeneralContact = (currentContacts: string[], index: number) => {
168
+ form.setValue(
169
+ 'generalContacts',
170
+ currentContacts.filter((_, i) => i !== index),
171
+ { shouldDirty: true },
172
+ );
173
+ };
174
+
175
+ const addBillingEmail = (currentEmails: string[]) => {
176
+ if (newBillingEmail.trim()) {
177
+ form.setValue('billingEmails', [...currentEmails, newBillingEmail.trim()], {
178
+ shouldDirty: true,
179
+ });
180
+ setNewBillingEmail('');
181
+ }
182
+ };
183
+
184
+ const removeBillingEmail = (currentEmails: string[], index: number) => {
185
+ form.setValue(
186
+ 'billingEmails',
187
+ currentEmails.filter((_, i) => i !== index),
188
+ { shouldDirty: true },
189
+ );
190
+ };
191
+
192
+ const onSubmit = async (data: ContactInfoFormValues) => {
193
+ if (!business || !userContext?.context.adminBusinessId) {
194
+ return;
195
+ }
196
+
197
+ const dataToUpdate = relevantDataPicker(
198
+ data,
199
+ form.formState.dirtyFields as MakeBoolean<typeof data>,
200
+ );
201
+
202
+ if (!dataToUpdate) return;
203
+
204
+ const updateBusinessInput = convertFormDataToUpdateBusinessInput(dataToUpdate);
205
+
206
+ await updateDbBusiness({
207
+ businessId: business.id,
208
+ ownerId: userContext.context.adminBusinessId,
209
+ fields: updateBusinessInput,
210
+ });
211
+
212
+ refetchBusiness?.();
213
+ };
214
+
215
+ useEffect(() => {
216
+ if (business) {
217
+ const formValues = ContactsSectionFragmentToFormValues(business);
218
+ setDefaultFormValues(formValues);
219
+ form.reset(formValues);
220
+ }
221
+ }, [business, form]);
222
+
223
+ return (
224
+ <Card>
225
+ <CardHeader>
226
+ <CardTitle>Contact Information</CardTitle>
227
+ <CardDescription>Business contact details and address information</CardDescription>
228
+ </CardHeader>
229
+ <Form {...form}>
230
+ <form onSubmit={form.handleSubmit(onSubmit)}>
231
+ <CardContent>
232
+ <div className="grid gap-6 md:grid-cols-2">
233
+ <FormField
234
+ control={form.control}
235
+ name="businessName"
236
+ render={({ field, fieldState }) => (
237
+ <FormItem>
238
+ <FormLabel>Business Name</FormLabel>
239
+ <FormControl>
240
+ <Input {...field} className={dirtyFieldMarker(fieldState)} />
241
+ </FormControl>
242
+ <FormMessage />
243
+ </FormItem>
244
+ )}
245
+ />
246
+
247
+ <FormField
248
+ control={form.control}
249
+ name="locality"
250
+ render={({ field, fieldState }) => (
251
+ <FormItem>
252
+ <FormLabel>Locality / Country</FormLabel>
253
+ <ComboBox
254
+ onChange={field.onChange}
255
+ data={countries.map(country => ({
256
+ value: country.code,
257
+ label: country.name,
258
+ }))}
259
+ value={field.value}
260
+ disabled={fetchingCountries}
261
+ placeholder="Scroll to see all options"
262
+ formPart
263
+ triggerProps={{
264
+ className: dirtyFieldMarker(fieldState),
265
+ }}
266
+ />
267
+ <FormMessage />
268
+ </FormItem>
269
+ )}
270
+ />
271
+
272
+ {isLocalEntity && (
273
+ <FormField
274
+ control={form.control}
275
+ name="localName"
276
+ render={({ field, fieldState }) => (
277
+ <FormItem>
278
+ <FormLabel>Local Name</FormLabel>
279
+ <FormControl>
280
+ <Input
281
+ {...field}
282
+ placeholder="Business name in local language"
283
+ className={dirtyFieldMarker(fieldState)}
284
+ />
285
+ </FormControl>
286
+ <FormMessage />
287
+ </FormItem>
288
+ )}
289
+ />
290
+ )}
291
+
292
+ {isLocalEntity && (
293
+ <FormField
294
+ control={form.control}
295
+ name="govId"
296
+ render={({ field, fieldState }) => (
297
+ <FormItem>
298
+ <FormLabel>Government ID</FormLabel>
299
+ <FormControl>
300
+ <Input
301
+ {...field}
302
+ placeholder="Enter Government ID"
303
+ className={dirtyFieldMarker(fieldState)}
304
+ />
305
+ </FormControl>
306
+ <FormMessage />
307
+ </FormItem>
308
+ )}
309
+ />
310
+ )}
311
+
312
+ <FormField
313
+ control={form.control}
314
+ name="address"
315
+ render={({ field, fieldState }) => (
316
+ <FormItem className="md:col-span-2">
317
+ <FormLabel className="flex items-center gap-2">
318
+ <MapPin className="h-4 w-4" />
319
+ Address
320
+ </FormLabel>
321
+ <FormControl>
322
+ <Textarea
323
+ {...field}
324
+ rows={3}
325
+ placeholder="Enter business address"
326
+ className={dirtyFieldMarker(fieldState)}
327
+ />
328
+ </FormControl>
329
+ <FormMessage />
330
+ </FormItem>
331
+ )}
332
+ />
333
+
334
+ {isLocalEntity && (
335
+ <FormField
336
+ control={form.control}
337
+ name="localAddress"
338
+ render={({ field, fieldState }) => (
339
+ <FormItem className="md:col-span-2">
340
+ <FormLabel className="flex items-center gap-2">
341
+ <MapPin className="h-4 w-4" />
342
+ Local Address
343
+ </FormLabel>
344
+ <FormControl>
345
+ <Textarea
346
+ {...field}
347
+ rows={3}
348
+ placeholder="Enter address in local language"
349
+ className={dirtyFieldMarker(fieldState)}
350
+ />
351
+ </FormControl>
352
+ <FormMessage />
353
+ </FormItem>
354
+ )}
355
+ />
356
+ )}
357
+
358
+ <FormField
359
+ control={form.control}
360
+ name="phone"
361
+ render={({ field, fieldState }) => (
362
+ <FormItem>
363
+ <FormLabel className="flex items-center gap-2">
364
+ <Phone className="h-4 w-4" />
365
+ Phone
366
+ </FormLabel>
367
+ <FormControl>
368
+ <Input
369
+ {...field}
370
+ type="tel"
371
+ placeholder="+1 (555) 123-4567"
372
+ className={dirtyFieldMarker(fieldState)}
373
+ />
374
+ </FormControl>
375
+ <FormMessage />
376
+ </FormItem>
377
+ )}
378
+ />
379
+
380
+ <FormField
381
+ control={form.control}
382
+ name="website"
383
+ render={({ field, fieldState }) => (
384
+ <FormItem>
385
+ <FormLabel className="flex items-center gap-2">
386
+ <Globe className="h-4 w-4" />
387
+ Website
388
+ </FormLabel>
389
+ <FormControl>
390
+ <Input
391
+ {...field}
392
+ type="url"
393
+ placeholder="https://example.com"
394
+ className={dirtyFieldMarker(fieldState)}
395
+ />
396
+ </FormControl>
397
+ <FormMessage />
398
+ </FormItem>
399
+ )}
400
+ />
401
+
402
+ <FormField
403
+ control={form.control}
404
+ name="generalContacts"
405
+ render={({ field, fieldState }) => (
406
+ <FormItem className="md:col-span-2">
407
+ <FormLabel className="flex items-center gap-2">
408
+ <Mail className="h-4 w-4" />
409
+ General Contacts
410
+ </FormLabel>
411
+ <FormControl>
412
+ <div className="space-y-2">
413
+ <div
414
+ className={
415
+ dirtyFieldMarker(fieldState) + ' flex flex-wrap gap-2 mb-2 rounded-md'
416
+ }
417
+ >
418
+ {field.value?.map((contact, index) => (
419
+ <Badge key={index} variant="secondary" className="gap-1 pr-1">
420
+ {contact}
421
+ <button
422
+ type="button"
423
+ onClick={() => removeGeneralContact(field.value ?? [], index)}
424
+ className="ml-1 hover:bg-muted rounded-sm p-0.5"
425
+ >
426
+ <X className="h-3 w-3" />
427
+ </button>
428
+ </Badge>
429
+ ))}
430
+ </div>
431
+ <div className="flex gap-2">
432
+ <Input
433
+ value={newContact}
434
+ onChange={e => setNewContact(e.target.value)}
435
+ onKeyDown={e => {
436
+ if (e.key === 'Enter') {
437
+ e.preventDefault();
438
+ addGeneralContact(field.value ?? []);
439
+ }
440
+ }}
441
+ placeholder="Add contact email"
442
+ type="email"
443
+ />
444
+ <Button
445
+ type="button"
446
+ variant="outline"
447
+ size="icon"
448
+ disabled={!newContact.trim()}
449
+ onClick={() => addGeneralContact(field.value ?? [])}
450
+ >
451
+ <Plus className="h-4 w-4" />
452
+ </Button>
453
+ </div>
454
+ </div>
455
+ </FormControl>
456
+ <FormMessage />
457
+ </FormItem>
458
+ )}
459
+ />
460
+
461
+ {isClient && (
462
+ <FormField
463
+ control={form.control}
464
+ name="billingEmails"
465
+ render={({ field, fieldState }) => (
466
+ <FormItem className="md:col-span-2">
467
+ <FormLabel className="flex items-center gap-2">
468
+ <Mail className="h-4 w-4" />
469
+ Billing Emails
470
+ </FormLabel>
471
+ <FormControl>
472
+ <div className="space-y-2">
473
+ <div
474
+ className={
475
+ dirtyFieldMarker(fieldState) + ' flex flex-wrap gap-2 mb-2 rounded-md'
476
+ }
477
+ >
478
+ {field.value?.map((email, index) => (
479
+ <Badge key={index} variant="secondary" className="gap-1 pr-1">
480
+ {email}
481
+ <button
482
+ type="button"
483
+ onClick={() => removeBillingEmail(field.value ?? [], index)}
484
+ className="ml-1 hover:bg-muted rounded-sm p-0.5"
485
+ >
486
+ <X className="h-3 w-3" />
487
+ </button>
488
+ </Badge>
489
+ ))}
490
+ </div>
491
+ <div className="flex gap-2">
492
+ <Input
493
+ value={newBillingEmail}
494
+ onChange={e => setNewBillingEmail(e.target.value)}
495
+ onKeyDown={e => {
496
+ if (e.key === 'Enter') {
497
+ e.preventDefault();
498
+ addBillingEmail(field.value ?? []);
499
+ }
500
+ }}
501
+ placeholder="Add billing email"
502
+ type="email"
503
+ />
504
+ <Button
505
+ type="button"
506
+ variant="outline"
507
+ size="icon"
508
+ disabled={!newBillingEmail.trim()}
509
+ onClick={() => addBillingEmail(field.value ?? [])}
510
+ >
511
+ <Plus className="h-4 w-4" />
512
+ </Button>
513
+ </div>
514
+ </div>
515
+ </FormControl>
516
+ <FormMessage />
517
+ </FormItem>
518
+ )}
519
+ />
520
+ )}
521
+ </div>
522
+ </CardContent>
523
+ <CardFooter className="flex justify-end border-t mt-4 pt-6">
524
+ <Button
525
+ type="submit"
526
+ disabled={isBusinessUpdating || Object.keys(form.formState.dirtyFields).length === 0}
527
+ >
528
+ <Save className="h-4 w-4 mr-2" />
529
+ Save Changes
530
+ </Button>
531
+ </CardFooter>
532
+ </form>
533
+ </Form>
534
+ </Card>
535
+ );
536
+ }