@carlonicora/nextjs-jsonapi 1.65.1 → 1.67.0

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 (93) hide show
  1. package/dist/{AuthComponent-B4rNZRYE.d.ts → AuthComponent-DL1D3y7f.d.ts} +1 -1
  2. package/dist/{AuthComponent-nzabiz68.d.mts → AuthComponent-NwQ_ZXsv.d.mts} +1 -1
  3. package/dist/{BlockNoteEditor-QGNV6E4X.js → BlockNoteEditor-QHWPE3BJ.js} +14 -14
  4. package/dist/{BlockNoteEditor-QGNV6E4X.js.map → BlockNoteEditor-QHWPE3BJ.js.map} +1 -1
  5. package/dist/{BlockNoteEditor-CZOW7J5K.mjs → BlockNoteEditor-TIX3GDVZ.mjs} +4 -4
  6. package/dist/{auth.interface-C1WjZ0fM.d.ts → auth.interface-BX_1qZZJ.d.ts} +1 -1
  7. package/dist/{auth.interface-fBFqIrw4.d.mts → auth.interface-yeLelxdI.d.mts} +1 -1
  8. package/dist/billing/index.js +346 -346
  9. package/dist/billing/index.mjs +3 -3
  10. package/dist/{chunk-CDCGQFIA.js → chunk-3BWYWS3A.js} +2118 -1659
  11. package/dist/chunk-3BWYWS3A.js.map +1 -0
  12. package/dist/{chunk-LRXJT656.js → chunk-CJY63D6U.js} +72 -5
  13. package/dist/chunk-CJY63D6U.js.map +1 -0
  14. package/dist/{chunk-RA4RYKYB.js → chunk-KFIQTY4O.js} +11 -11
  15. package/dist/{chunk-RA4RYKYB.js.map → chunk-KFIQTY4O.js.map} +1 -1
  16. package/dist/{chunk-G7PGWMFO.mjs → chunk-RIG2BEXJ.mjs} +72 -5
  17. package/dist/{chunk-G7PGWMFO.mjs.map → chunk-RIG2BEXJ.mjs.map} +1 -1
  18. package/dist/{chunk-ESGUCYJS.mjs → chunk-WWP32QYC.mjs} +3534 -3075
  19. package/dist/chunk-WWP32QYC.mjs.map +1 -0
  20. package/dist/{chunk-5KMKI23S.mjs → chunk-ZYAAJMZZ.mjs} +2 -2
  21. package/dist/client/index.d.mts +6 -6
  22. package/dist/client/index.d.ts +6 -6
  23. package/dist/client/index.js +4 -4
  24. package/dist/client/index.mjs +3 -3
  25. package/dist/components/index.d.mts +69 -12
  26. package/dist/components/index.d.ts +69 -12
  27. package/dist/components/index.js +18 -4
  28. package/dist/components/index.js.map +1 -1
  29. package/dist/components/index.mjs +19 -5
  30. package/dist/{config-DZWAFB7H.d.ts → config-CyCAWW-d.d.ts} +1 -1
  31. package/dist/{config-ndRJIQsP.d.mts → config-D-mqttuF.d.mts} +1 -1
  32. package/dist/{content.interface-B5ySfiOE.d.mts → content.interface-8T5-G84c.d.mts} +1 -1
  33. package/dist/{content.interface-mmz0uMwm.d.ts → content.interface-D-xdYxjt.d.ts} +1 -1
  34. package/dist/contexts/index.d.mts +3 -3
  35. package/dist/contexts/index.d.ts +3 -3
  36. package/dist/contexts/index.js +4 -4
  37. package/dist/contexts/index.mjs +3 -3
  38. package/dist/core/index.d.mts +29 -9
  39. package/dist/core/index.d.ts +29 -9
  40. package/dist/core/index.js +2 -2
  41. package/dist/core/index.mjs +1 -1
  42. package/dist/index.d.mts +8 -8
  43. package/dist/index.d.ts +8 -8
  44. package/dist/index.js +3 -3
  45. package/dist/index.mjs +2 -2
  46. package/dist/{notification.interface-DG7cq9oG.d.mts → notification.interface-C6UcmJqu.d.mts} +20 -0
  47. package/dist/{notification.interface-COKHDQeE.d.ts → notification.interface-ItBxq2au.d.ts} +20 -0
  48. package/dist/{s3.service-ppn9iGJU.d.ts → s3.service-Cg5TmbU_.d.ts} +6 -3
  49. package/dist/{s3.service-BoRPFx82.d.mts → s3.service-DLf_a0xS.d.mts} +6 -3
  50. package/dist/server/index.d.mts +4 -4
  51. package/dist/server/index.d.ts +4 -4
  52. package/dist/server/index.js +3 -3
  53. package/dist/server/index.mjs +1 -1
  54. package/dist/{useRbacState-DhuYYr0S.d.mts → useRbacState-Btk1gkQg.d.mts} +1 -1
  55. package/dist/{useRbacState-NnzNL2ED.d.ts → useRbacState-CUj0hp8t.d.ts} +1 -1
  56. package/dist/{useSocket-bsV-K4qR.d.ts → useSocket-BSUN9s3p.d.ts} +1 -1
  57. package/dist/{useSocket-CtfuR5wD.d.mts → useSocket-DKI92Fbg.d.mts} +1 -1
  58. package/package.json +2 -1
  59. package/src/components/EditableAvatar.tsx +175 -0
  60. package/src/components/containers/RoundPageContainer.tsx +1 -1
  61. package/src/components/fiscal/FiscalDataDisplay.tsx +26 -0
  62. package/src/components/fiscal/ItalianFiscalData.tsx +120 -0
  63. package/src/components/fiscal/ItalianFiscalDataDisplay.tsx +24 -0
  64. package/src/components/fiscal/index.ts +4 -0
  65. package/src/components/index.ts +3 -0
  66. package/src/components/navigations/RecentPagesNavigator.tsx +3 -3
  67. package/src/features/company/components/details/CompanyContent.tsx +105 -0
  68. package/src/features/company/components/details/CompanyDetails.tsx +2 -19
  69. package/src/features/company/components/details/index.ts +1 -0
  70. package/src/features/company/components/forms/CompanyConfigurationEditor.tsx +38 -70
  71. package/src/features/company/components/forms/CompanyEditor.tsx +212 -172
  72. package/src/features/company/data/company.interface.ts +20 -0
  73. package/src/features/company/data/company.ts +73 -0
  74. package/src/features/role/components/forms/FormRoles.tsx +5 -4
  75. package/src/features/user/components/containers/AllUsersListContainer.tsx +36 -0
  76. package/src/features/user/components/containers/UserContainer.tsx +10 -13
  77. package/src/features/user/components/containers/UsersListContainer.tsx +15 -24
  78. package/src/features/user/components/containers/index.ts +1 -0
  79. package/src/features/user/components/details/UserContent.tsx +92 -0
  80. package/src/features/user/components/details/index.ts +1 -1
  81. package/src/features/user/components/forms/UserEditor.tsx +233 -233
  82. package/src/features/user/components/lists/CompanyUsersList.tsx +3 -1
  83. package/src/features/user/contexts/UserContext.tsx +1 -6
  84. package/src/features/user/data/user.service.ts +9 -0
  85. package/src/features/user/data/user.ts +3 -4
  86. package/src/utils/fiscal-utils.ts +7 -0
  87. package/src/utils/italian-validators.ts +79 -0
  88. package/dist/chunk-CDCGQFIA.js.map +0 -1
  89. package/dist/chunk-ESGUCYJS.mjs.map +0 -1
  90. package/dist/chunk-LRXJT656.js.map +0 -1
  91. package/src/features/user/components/details/UserDetails.tsx +0 -74
  92. /package/dist/{BlockNoteEditor-CZOW7J5K.mjs.map → BlockNoteEditor-TIX3GDVZ.mjs.map} +0 -0
  93. /package/dist/{chunk-5KMKI23S.mjs.map → chunk-ZYAAJMZZ.mjs.map} +0 -0
@@ -3,22 +3,14 @@
3
3
  import { zodResolver } from "@hookform/resolvers/zod";
4
4
  import { Settings2Icon } from "lucide-react";
5
5
  import { useTranslations } from "next-intl";
6
- import { ReactNode, useEffect, useState } from "react";
7
- import { SubmitHandler, UseFormReturn, useForm } from "react-hook-form";
6
+ import { ReactNode } from "react";
7
+ import { UseFormReturn, useForm } from "react-hook-form";
8
8
  import { showToast } from "../../../../utils/toast";
9
9
  import z from "zod";
10
- import { CommonEditorButtons, errorToast } from "../../../../components";
10
+ import { EditorSheet } from "../../../../components";
11
+ import { Modules } from "../../../../core";
11
12
  import { getRoleId } from "../../../../roles";
12
- import {
13
- Button,
14
- Dialog,
15
- DialogContent,
16
- DialogDescription,
17
- DialogHeader,
18
- DialogTitle,
19
- DialogTrigger,
20
- Form,
21
- } from "../../../../shadcnui";
13
+ import { Button } from "../../../../shadcnui";
22
14
  import { UserInterface } from "../../../user";
23
15
  import { useCurrentUserContext } from "../../../user/contexts";
24
16
  import { UserService } from "../../../user/data/user.service";
@@ -40,75 +32,51 @@ function CompanyConfigurationEditorInternal({
40
32
  buildPayload,
41
33
  children,
42
34
  }: CompanyConfigurationEditorProps) {
43
- const [open, setOpen] = useState<boolean>(false);
44
35
  const t = useTranslations();
45
36
  const { setUser } = useCurrentUserContext<UserInterface>();
46
37
 
47
- const close = () => {
48
- setOpen(false);
49
- form.reset();
50
- };
51
-
52
38
  const form = useForm<z.infer<typeof formSchema>>({
53
39
  resolver: zodResolver(formSchema),
54
40
  defaultValues: defaultValues,
55
41
  shouldUnregister: false,
56
42
  });
57
43
 
58
- useEffect(() => {
59
- if (open) {
60
- form.reset(defaultValues);
61
- }
62
- }, [company, open]);
63
-
64
- const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = async (values) => {
65
- const payload: CompanyInput = {
66
- id: company.id,
67
- configurations: buildPayload(values),
68
- };
69
-
70
- try {
71
- await CompanyService.updateConfigurations(payload);
72
-
73
- const fullUser = await UserService.findFullUser();
74
- if (fullUser) {
75
- setUser(fullUser);
76
- }
77
-
78
- showToast(t("features.configuration.updated_title"), {
79
- description: t("features.configuration.updated_description"),
80
- });
81
- close();
82
- } catch (error) {
83
- errorToast({
84
- title: t(`common.errors.update`),
85
- error,
86
- });
87
- }
88
- };
89
-
90
44
  return (
91
- <Dialog open={open} onOpenChange={setOpen}>
92
- <DialogTrigger>
93
- <Button render={<div />} nativeButton={false} size="sm" variant={`ghost`} className="cursor-pointer">
45
+ <EditorSheet
46
+ form={form}
47
+ entityType={t(`entities.configurations`, { count: 2 })}
48
+ entityName={company.name}
49
+ isEdit={true}
50
+ module={Modules.Company}
51
+ size="sm"
52
+ trigger={
53
+ <Button render={<div />} nativeButton={false} size="sm" variant="ghost" className="cursor-pointer">
94
54
  <Settings2Icon className="h-3.5 w-3.5" />
95
55
  </Button>
96
- </DialogTrigger>
97
- <DialogContent className={`flex max-h-[70vh] max-w-4xl flex-col overflow-y-auto`}>
98
- <DialogHeader>
99
- <DialogTitle>{t(`entities.configurations`, { count: 2 })}</DialogTitle>
100
- <DialogDescription className="text-destructive">
101
- {t(`features.configuration.warning_description`)}
102
- </DialogDescription>
103
- </DialogHeader>
104
- <Form {...form}>
105
- <form onSubmit={form.handleSubmit(onSubmit)} className={`flex w-full flex-col gap-y-4`}>
106
- {children(form)}
107
- <CommonEditorButtons form={form} setOpen={setOpen} isEdit={true} />
108
- </form>
109
- </Form>
110
- </DialogContent>
111
- </Dialog>
56
+ }
57
+ onSubmit={async (values) => {
58
+ const payload: CompanyInput = {
59
+ id: company.id,
60
+ configurations: buildPayload(values),
61
+ };
62
+
63
+ await CompanyService.updateConfigurations(payload);
64
+
65
+ const fullUser = await UserService.findFullUser();
66
+ if (fullUser) {
67
+ setUser(fullUser);
68
+ }
69
+ }}
70
+ onSuccess={async () => {
71
+ showToast(t("features.configuration.updated_title"), {
72
+ description: t("features.configuration.updated_description"),
73
+ });
74
+ }}
75
+ onReset={() => defaultValues}
76
+ >
77
+ <p className="text-destructive text-sm font-medium">{t(`features.configuration.warning_description`)}</p>
78
+ {children(form)}
79
+ </EditorSheet>
112
80
  );
113
81
  }
114
82
 
@@ -1,36 +1,36 @@
1
1
  "use client";
2
2
 
3
3
  import { zodResolver } from "@hookform/resolvers/zod";
4
- import { setCookie } from "cookies-next";
5
4
  import { UploadIcon } from "lucide-react";
6
5
  import { useTranslations } from "next-intl";
7
6
  import Image from "next/image";
8
- import { useEffect, useState } from "react";
7
+ import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
9
8
  import { DropzoneOptions } from "react-dropzone";
10
- import { SubmitHandler, useForm } from "react-hook-form";
9
+ import { useForm } from "react-hook-form";
11
10
  import { v4 } from "uuid";
12
11
  import { z } from "zod";
13
12
  import {
14
- CommonEditorButtons,
15
- CommonEditorHeader,
16
- CommonEditorTrigger,
17
- errorToast,
13
+ EditorSheet,
18
14
  FileInput,
19
15
  FileUploader,
20
16
  FormFeatures,
21
17
  FormInput,
18
+ FormPlaceAutocomplete,
19
+ ItalianFiscalData,
20
+ parseFiscalData,
22
21
  } from "../../../../components";
22
+ import type { FiscalDataHandle } from "../../../../components";
23
23
  import { Modules } from "../../../../core";
24
- import { usePageUrlGenerator } from "../../../../hooks";
25
24
  import { useI18nRouter } from "../../../../i18n";
26
25
  import { getRoleId } from "../../../../roles";
27
- import { Dialog, DialogContent, Form, ScrollArea } from "../../../../shadcnui";
26
+ import { ScrollArea } from "../../../../shadcnui";
28
27
  import { FeatureInterface } from "../../../feature";
29
28
  import { FeatureService } from "../../../feature/data/feature.service";
30
29
  import { S3Interface } from "../../../s3";
31
30
  import { S3Service } from "../../../s3/data/s3.service";
32
31
  import { UserInterface } from "../../../user";
33
32
  import { useCurrentUserContext } from "../../../user/contexts";
33
+ import { UserService } from "../../../user/data/user.service";
34
34
  import { CompanyInput, CompanyInterface } from "../../data";
35
35
  import { CompanyService } from "../../data/company.service";
36
36
 
@@ -38,202 +38,242 @@ type CompanyEditorProps = {
38
38
  company?: CompanyInterface;
39
39
  propagateChanges?: (company: CompanyInterface) => void;
40
40
  onRevalidate?: (path: string) => Promise<void>;
41
+ trigger?: ReactNode;
42
+ forceShow?: boolean;
43
+ onClose?: () => void;
44
+ dialogOpen?: boolean;
45
+ onDialogOpenChange?: (open: boolean) => void;
41
46
  };
42
47
 
43
- function CompanyEditorInternal({ company, propagateChanges, onRevalidate }: CompanyEditorProps) {
44
- const { hasRole } = useCurrentUserContext<UserInterface>();
48
+ function CompanyEditorInternal({
49
+ company,
50
+ propagateChanges,
51
+ onRevalidate,
52
+ trigger,
53
+ forceShow,
54
+ onClose,
55
+ dialogOpen,
56
+ onDialogOpenChange,
57
+ }: CompanyEditorProps) {
58
+ const { hasRole, setUser } = useCurrentUserContext<UserInterface>();
45
59
  const router = useI18nRouter();
46
- const [open, setOpen] = useState<boolean>(false);
47
60
  const [features, setFeatures] = useState<FeatureInterface[]>([]);
48
61
  const [file, setFile] = useState<File | null>(null);
49
62
  const [files, setFiles] = useState<File[] | null>(null);
50
63
  const [contentType, setContentType] = useState<string | null>(null);
51
64
  const t = useTranslations();
52
- const generateUrl = usePageUrlGenerator();
53
-
54
- const formSchema = z.object({
55
- id: z.uuidv4(),
56
- name: z.string().min(1, {
57
- message: t(`company.fields.name.error`),
58
- }),
59
- featureIds: z.array(z.string()).optional(),
60
- moduleIds: z.array(z.string()).optional(),
61
- logo: z.string().optional(),
62
- });
63
-
64
- const form = useForm({
65
- resolver: zodResolver(formSchema),
66
- defaultValues: {
67
- id: company?.id || v4(),
68
- name: company?.name || "",
69
- featureIds: company?.features.map((feature) => feature.id) || [],
70
- moduleIds: company?.modules.map((module) => module.id) || [],
71
- logo: company?.logo || "",
72
- },
73
- });
65
+ const fiscalRef = useRef<FiscalDataHandle>(null);
66
+ const addressComponentsRef = useRef<Record<string, string>>({});
74
67
 
75
- const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = async (values: z.infer<typeof formSchema>) => {
76
- if (values.logo && contentType) {
77
- const s3: S3Interface = await S3Service.getPreSignedUrl({
78
- key: values.logo,
79
- contentType: contentType,
80
- isPublic: true,
81
- });
82
-
83
- await fetch(s3.url, {
84
- method: "PUT",
85
- headers: s3.headers,
86
- body: file,
87
- });
88
- }
89
-
90
- const payload: CompanyInput = {
91
- id: company?.id ?? v4(),
92
- name: values.name,
93
- logo: files && contentType ? values.logo : undefined,
94
- featureIds: values.featureIds,
95
- moduleIds: values.moduleIds,
96
- };
97
-
98
- try {
99
- const updatedCompany = company ? await CompanyService.update(payload) : await CompanyService.create(payload);
68
+ const canAccessFeatures =
69
+ hasRole(getRoleId().Administrator) ||
70
+ (hasRole(getRoleId().CompanyAdministrator) &&
71
+ process.env.NEXT_PUBLIC_PRIVATE_INSTALLATION?.toLowerCase() === "true");
100
72
 
101
- if (onRevalidate) {
102
- await onRevalidate(generateUrl({ page: Modules.Company, id: updatedCompany.id, language: `[locale]` }));
73
+ // Fetch features when sheet opens
74
+ const handleDialogOpenChange = useCallback(
75
+ (open: boolean) => {
76
+ if (open && features.length === 0 && canAccessFeatures) {
77
+ async function fetchFeatures() {
78
+ const allfeatures = await FeatureService.findMany({});
79
+ if (hasRole(getRoleId().Administrator)) {
80
+ setFeatures(allfeatures);
81
+ } else {
82
+ setFeatures(allfeatures.filter((feature) => feature.isCore));
83
+ }
84
+ }
85
+ fetchFeatures();
103
86
  }
104
- if (company && propagateChanges) {
105
- setCookie("reloadData", "true", { path: "/" });
106
- propagateChanges(updatedCompany);
107
- setOpen(false);
108
- } else {
109
- router.push(`/administration/companies/${updatedCompany.id}`);
110
- }
111
- } catch (error) {
112
- errorToast({
113
- title: company ? t(`common.errors.update`) : t(`common.errors.create`),
114
- error,
115
- });
116
- }
117
- };
87
+ onDialogOpenChange?.(open);
88
+ },
89
+ [features.length, canAccessFeatures, hasRole, onDialogOpenChange],
90
+ );
118
91
 
92
+ const dropzone = {
93
+ multiple: false,
94
+ maxSize: 100 * 1024 * 1024,
95
+ preventDropOnDocument: false,
96
+ accept: {
97
+ "application/images": [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"],
98
+ },
99
+ } satisfies DropzoneOptions;
100
+
101
+ // Handle file selection from dropzone
119
102
  useEffect(() => {
120
- async function fetchFeatures() {
121
- const allfeatures = await FeatureService.findMany({});
122
- if (hasRole(getRoleId().Administrator)) {
123
- setFeatures(allfeatures);
124
- } else {
125
- setFeatures(allfeatures.filter((feature) => feature.isCore));
126
- }
103
+ if (files && files.length > 0) {
104
+ setFile(files[0]);
127
105
  }
128
- if (
129
- open &&
130
- features.length === 0 &&
131
- (hasRole(getRoleId().Administrator) ||
132
- (hasRole(getRoleId().CompanyAdministrator) &&
133
- process.env.NEXT_PUBLIC_PRIVATE_INSTALLATION?.toLowerCase() === "true"))
134
- )
135
- fetchFeatures();
136
- }, [open, features]);
106
+ }, [files]);
137
107
 
108
+ // Generate S3 path when file is selected
138
109
  useEffect(() => {
139
110
  if (file && company) {
140
111
  const id = form.getValues("id");
141
112
  const fileType = file.type;
142
- let extension = "";
143
-
144
- switch (fileType) {
145
- default:
146
- extension = file.type.split("/").pop() ?? "";
147
- break;
148
- }
149
-
113
+ const extension = fileType.split("/").pop() ?? "";
150
114
  const timestamp = new Date().toISOString().replace(/[-:T]/g, "").split(".")[0];
151
-
152
115
  const fileUrl = `companies/${form.getValues("id")}/companies/${id}/${id}.${timestamp}.${extension}`;
153
116
  form.setValue("logo", fileUrl);
154
-
155
117
  setContentType(fileType);
156
118
  } else {
157
119
  setContentType(null);
158
120
  }
159
121
  }, [file]);
160
122
 
161
- useEffect(() => {
162
- if (files && files.length > 0) {
163
- setFile(files[0]);
164
- }
165
- }, [files]);
166
-
167
- const dropzone = {
168
- multiple: false,
169
- maxSize: 100 * 1024 * 1024,
170
- preventDropOnDocument: false,
171
- accept: {
172
- "application/images": [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"],
173
- },
174
- } satisfies DropzoneOptions;
123
+ const formSchema = useMemo(
124
+ () =>
125
+ z.object({
126
+ id: z.uuidv4(),
127
+ name: z.string().min(1, {
128
+ message: t(`company.fields.name.error`),
129
+ }),
130
+ featureIds: z.array(z.string()).optional(),
131
+ moduleIds: z.array(z.string()).optional(),
132
+ logo: z.string().optional(),
133
+ legal_address: z.string().optional(),
134
+ }),
135
+ [t],
136
+ );
175
137
 
176
- const canAccessFeatures =
177
- hasRole(getRoleId().Administrator) ||
178
- (hasRole(getRoleId().CompanyAdministrator) &&
179
- process.env.NEXT_PUBLIC_PRIVATE_INSTALLATION?.toLowerCase() === "true");
138
+ const getDefaultValues = useCallback(() => {
139
+ return {
140
+ id: company?.id || v4(),
141
+ name: company?.name || "",
142
+ featureIds: company?.features.map((feature) => feature.id) || [],
143
+ moduleIds: company?.modules.map((module) => module.id) || [],
144
+ logo: company?.logo || "",
145
+ legal_address: company?.legal_address || "",
146
+ };
147
+ }, [company]);
180
148
 
181
- const isAdministrator = hasRole(getRoleId().Administrator);
149
+ const form = useForm<z.infer<typeof formSchema>>({
150
+ resolver: zodResolver(formSchema),
151
+ defaultValues: getDefaultValues(),
152
+ });
182
153
 
183
154
  return (
184
- <Dialog open={open} onOpenChange={setOpen}>
185
- <CommonEditorTrigger isEdit={!!company} />
186
- <DialogContent
187
- className={`flex max-h-[70vh] w-full ${isAdministrator || canAccessFeatures ? `max-w-5xl` : `max-w-4xl`} flex-col overflow-y-auto`}
188
- >
189
- <CommonEditorHeader type={t(`entities.companies`, { count: 1 })} name={company?.name} />
190
- <Form {...form}>
191
- <form onSubmit={form.handleSubmit(onSubmit)} className={`flex w-full flex-col gap-y-4`}>
192
- <div className="flex w-full items-start justify-between gap-x-4">
193
- <div className={`flex w-96 flex-col justify-start gap-y-4`}>
194
- <FileUploader value={files} onValueChange={setFiles} dropzoneOptions={dropzone} className="w-full p-4">
195
- <FileInput className="text-neutral-300 outline-dashed">
196
- <div className="flex w-full flex-col items-center justify-center pt-3 pb-4">
197
- <div className="flex w-full flex-col items-center justify-center pt-3 pb-4">
198
- {file || company?.logo ? (
199
- <Image
200
- src={file ? URL.createObjectURL(file) : company?.logo || ""}
201
- alt="Company Logo"
202
- width={200}
203
- height={200}
204
- />
205
- ) : (
206
- <>
207
- <UploadIcon className="my-4 h-8 w-8" />
208
- <p className="mb-1 flex w-full text-center text-sm">{t(`company.click_drag_logo`)}</p>
209
- </>
210
- )}
211
- </div>
212
- </div>
213
- </FileInput>
214
- </FileUploader>
215
- </div>
216
- <div className={`flex w-full flex-col justify-start gap-y-4`}>
217
- <FormInput
218
- form={form}
219
- id="name"
220
- name={t(`company.fields.name.label`)}
221
- placeholder={t(`company.fields.name.placeholder`)}
222
- />
223
- </div>
224
- {canAccessFeatures && (
225
- <div className={`flex w-96 flex-col justify-start gap-y-4`}>
226
- <ScrollArea className="h-max">
227
- <FormFeatures form={form} name={t(`company.features_and_modules`)} features={features} />
228
- </ScrollArea>
155
+ <EditorSheet
156
+ form={form}
157
+ entityType={t(`entities.companies`, { count: 1 })}
158
+ entityName={company?.name}
159
+ isEdit={!!company}
160
+ module={Modules.Company}
161
+ propagateChanges={propagateChanges}
162
+ size="lg"
163
+ onSubmit={async (values) => {
164
+ // Upload logo to S3 if present
165
+ if (values.logo && contentType && file) {
166
+ const s3: S3Interface = await S3Service.getPreSignedUrl({
167
+ key: values.logo,
168
+ contentType: contentType,
169
+ isPublic: true,
170
+ });
171
+
172
+ const response = await fetch(s3.url, {
173
+ method: "PUT",
174
+ headers: s3.headers,
175
+ body: file,
176
+ });
177
+
178
+ if (!response.ok) {
179
+ throw new Error(`S3 upload failed: ${response.status}`);
180
+ }
181
+ }
182
+
183
+ if (fiscalRef.current && !fiscalRef.current.validate()) {
184
+ throw new Error("Fiscal data validation failed");
185
+ }
186
+
187
+ const payload: CompanyInput = {
188
+ id: company?.id ?? v4(),
189
+ name: values.name,
190
+ logo: files && contentType ? values.logo : undefined,
191
+ featureIds: values.featureIds,
192
+ moduleIds: values.moduleIds,
193
+ legal_address: values.legal_address,
194
+ ...addressComponentsRef.current,
195
+ fiscal_data: fiscalRef.current ? JSON.stringify(fiscalRef.current.getData()) : undefined,
196
+ };
197
+
198
+ const updatedCompany = company ? await CompanyService.update(payload) : await CompanyService.create(payload);
199
+
200
+ // Refresh user context after company changes
201
+ const fullUser = await UserService.findFullUser();
202
+ if (fullUser) {
203
+ setUser(fullUser);
204
+ }
205
+
206
+ return updatedCompany;
207
+ }}
208
+ onRevalidate={onRevalidate}
209
+ onNavigate={(url) => router.push(url)}
210
+ onReset={() => {
211
+ setFile(null);
212
+ setFiles(null);
213
+ setContentType(null);
214
+ addressComponentsRef.current = {};
215
+ return getDefaultValues();
216
+ }}
217
+ onClose={onClose}
218
+ trigger={trigger}
219
+ forceShow={forceShow}
220
+ dialogOpen={dialogOpen}
221
+ onDialogOpenChange={handleDialogOpenChange}
222
+ >
223
+ <div className="flex w-full items-start justify-between gap-x-4">
224
+ <div className="flex w-96 flex-col justify-start gap-y-4">
225
+ <FileUploader value={files} onValueChange={setFiles} dropzoneOptions={dropzone} className="w-full p-4">
226
+ <FileInput className="text-neutral-300 outline-dashed">
227
+ <div className="flex w-full flex-col items-center justify-center pt-3 pb-4">
228
+ <div className="flex w-full flex-col items-center justify-center pt-3 pb-4">
229
+ {file || company?.logo ? (
230
+ <Image
231
+ src={file ? URL.createObjectURL(file) : company?.logo || ""}
232
+ alt="Company Logo"
233
+ width={200}
234
+ height={200}
235
+ />
236
+ ) : (
237
+ <>
238
+ <UploadIcon className="my-4 h-8 w-8" />
239
+ <p className="mb-1 flex w-full text-center text-sm">{t(`company.click_drag_logo`)}</p>
240
+ </>
241
+ )}
229
242
  </div>
230
- )}
231
- </div>
232
- <CommonEditorButtons form={form} setOpen={setOpen} isEdit={!!company} />
233
- </form>
234
- </Form>
235
- </DialogContent>
236
- </Dialog>
243
+ </div>
244
+ </FileInput>
245
+ </FileUploader>
246
+ </div>
247
+ <div className="flex w-full flex-col justify-start gap-y-4">
248
+ <FormInput
249
+ form={form}
250
+ id="name"
251
+ name={t(`company.fields.name.label`)}
252
+ placeholder={t(`company.fields.name.placeholder`)}
253
+ />
254
+ <FormPlaceAutocomplete
255
+ form={form}
256
+ id="legal_address"
257
+ name={t(`company.fields.legal_address.label`)}
258
+ placeholder={t(`company.fields.legal_address.placeholder`)}
259
+ onPlaceSelect={(place) => {
260
+ if (place.addressComponents) {
261
+ addressComponentsRef.current = { ...place.addressComponents };
262
+ }
263
+ }}
264
+ />
265
+ <h3 className="mt-2 text-sm font-semibold">{t(`company.sections.fiscal_data`)}</h3>
266
+ <ItalianFiscalData ref={fiscalRef} initialData={parseFiscalData(company?.fiscal_data)} />
267
+ </div>
268
+ {canAccessFeatures && (
269
+ <div className="flex w-96 flex-col justify-start gap-y-4">
270
+ <ScrollArea className="h-max">
271
+ <FormFeatures form={form} name={t(`company.features_and_modules`)} features={features} />
272
+ </ScrollArea>
273
+ </div>
274
+ )}
275
+ </div>
276
+ </EditorSheet>
237
277
  );
238
278
  }
239
279
 
@@ -14,6 +14,16 @@ export type CompanyInput = {
14
14
 
15
15
  featureIds?: string[];
16
16
  moduleIds?: string[];
17
+ legal_address?: string;
18
+ street_number?: string;
19
+ street?: string;
20
+ city?: string;
21
+ province?: string;
22
+ region?: string;
23
+ postcode?: string;
24
+ country?: string;
25
+ country_code?: string;
26
+ fiscal_data?: string;
17
27
  };
18
28
 
19
29
  export interface CompanyInterface extends ApiDataInterface {
@@ -30,4 +40,14 @@ export interface CompanyInterface extends ApiDataInterface {
30
40
 
31
41
  get features(): FeatureInterface[];
32
42
  get modules(): ModuleInterface[];
43
+ get legal_address(): string | undefined;
44
+ get street_number(): string | undefined;
45
+ get street(): string | undefined;
46
+ get city(): string | undefined;
47
+ get province(): string | undefined;
48
+ get region(): string | undefined;
49
+ get postcode(): string | undefined;
50
+ get country(): string | undefined;
51
+ get country_code(): string | undefined;
52
+ get fiscal_data(): string | undefined;
33
53
  }