@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.
- package/dist/{AuthComponent-B4rNZRYE.d.ts → AuthComponent-DL1D3y7f.d.ts} +1 -1
- package/dist/{AuthComponent-nzabiz68.d.mts → AuthComponent-NwQ_ZXsv.d.mts} +1 -1
- package/dist/{BlockNoteEditor-QGNV6E4X.js → BlockNoteEditor-QHWPE3BJ.js} +14 -14
- package/dist/{BlockNoteEditor-QGNV6E4X.js.map → BlockNoteEditor-QHWPE3BJ.js.map} +1 -1
- package/dist/{BlockNoteEditor-CZOW7J5K.mjs → BlockNoteEditor-TIX3GDVZ.mjs} +4 -4
- package/dist/{auth.interface-C1WjZ0fM.d.ts → auth.interface-BX_1qZZJ.d.ts} +1 -1
- package/dist/{auth.interface-fBFqIrw4.d.mts → auth.interface-yeLelxdI.d.mts} +1 -1
- package/dist/billing/index.js +346 -346
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-CDCGQFIA.js → chunk-3BWYWS3A.js} +2118 -1659
- package/dist/chunk-3BWYWS3A.js.map +1 -0
- package/dist/{chunk-LRXJT656.js → chunk-CJY63D6U.js} +72 -5
- package/dist/chunk-CJY63D6U.js.map +1 -0
- package/dist/{chunk-RA4RYKYB.js → chunk-KFIQTY4O.js} +11 -11
- package/dist/{chunk-RA4RYKYB.js.map → chunk-KFIQTY4O.js.map} +1 -1
- package/dist/{chunk-G7PGWMFO.mjs → chunk-RIG2BEXJ.mjs} +72 -5
- package/dist/{chunk-G7PGWMFO.mjs.map → chunk-RIG2BEXJ.mjs.map} +1 -1
- package/dist/{chunk-ESGUCYJS.mjs → chunk-WWP32QYC.mjs} +3534 -3075
- package/dist/chunk-WWP32QYC.mjs.map +1 -0
- package/dist/{chunk-5KMKI23S.mjs → chunk-ZYAAJMZZ.mjs} +2 -2
- package/dist/client/index.d.mts +6 -6
- package/dist/client/index.d.ts +6 -6
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +69 -12
- package/dist/components/index.d.ts +69 -12
- package/dist/components/index.js +18 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +19 -5
- package/dist/{config-DZWAFB7H.d.ts → config-CyCAWW-d.d.ts} +1 -1
- package/dist/{config-ndRJIQsP.d.mts → config-D-mqttuF.d.mts} +1 -1
- package/dist/{content.interface-B5ySfiOE.d.mts → content.interface-8T5-G84c.d.mts} +1 -1
- package/dist/{content.interface-mmz0uMwm.d.ts → content.interface-D-xdYxjt.d.ts} +1 -1
- package/dist/contexts/index.d.mts +3 -3
- package/dist/contexts/index.d.ts +3 -3
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +29 -9
- package/dist/core/index.d.ts +29 -9
- package/dist/core/index.js +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/index.d.mts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +3 -3
- package/dist/index.mjs +2 -2
- package/dist/{notification.interface-DG7cq9oG.d.mts → notification.interface-C6UcmJqu.d.mts} +20 -0
- package/dist/{notification.interface-COKHDQeE.d.ts → notification.interface-ItBxq2au.d.ts} +20 -0
- package/dist/{s3.service-ppn9iGJU.d.ts → s3.service-Cg5TmbU_.d.ts} +6 -3
- package/dist/{s3.service-BoRPFx82.d.mts → s3.service-DLf_a0xS.d.mts} +6 -3
- package/dist/server/index.d.mts +4 -4
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/{useRbacState-DhuYYr0S.d.mts → useRbacState-Btk1gkQg.d.mts} +1 -1
- package/dist/{useRbacState-NnzNL2ED.d.ts → useRbacState-CUj0hp8t.d.ts} +1 -1
- package/dist/{useSocket-bsV-K4qR.d.ts → useSocket-BSUN9s3p.d.ts} +1 -1
- package/dist/{useSocket-CtfuR5wD.d.mts → useSocket-DKI92Fbg.d.mts} +1 -1
- package/package.json +2 -1
- package/src/components/EditableAvatar.tsx +175 -0
- package/src/components/containers/RoundPageContainer.tsx +1 -1
- package/src/components/fiscal/FiscalDataDisplay.tsx +26 -0
- package/src/components/fiscal/ItalianFiscalData.tsx +120 -0
- package/src/components/fiscal/ItalianFiscalDataDisplay.tsx +24 -0
- package/src/components/fiscal/index.ts +4 -0
- package/src/components/index.ts +3 -0
- package/src/components/navigations/RecentPagesNavigator.tsx +3 -3
- package/src/features/company/components/details/CompanyContent.tsx +105 -0
- package/src/features/company/components/details/CompanyDetails.tsx +2 -19
- package/src/features/company/components/details/index.ts +1 -0
- package/src/features/company/components/forms/CompanyConfigurationEditor.tsx +38 -70
- package/src/features/company/components/forms/CompanyEditor.tsx +212 -172
- package/src/features/company/data/company.interface.ts +20 -0
- package/src/features/company/data/company.ts +73 -0
- package/src/features/role/components/forms/FormRoles.tsx +5 -4
- package/src/features/user/components/containers/AllUsersListContainer.tsx +36 -0
- package/src/features/user/components/containers/UserContainer.tsx +10 -13
- package/src/features/user/components/containers/UsersListContainer.tsx +15 -24
- package/src/features/user/components/containers/index.ts +1 -0
- package/src/features/user/components/details/UserContent.tsx +92 -0
- package/src/features/user/components/details/index.ts +1 -1
- package/src/features/user/components/forms/UserEditor.tsx +233 -233
- package/src/features/user/components/lists/CompanyUsersList.tsx +3 -1
- package/src/features/user/contexts/UserContext.tsx +1 -6
- package/src/features/user/data/user.service.ts +9 -0
- package/src/features/user/data/user.ts +3 -4
- package/src/utils/fiscal-utils.ts +7 -0
- package/src/utils/italian-validators.ts +79 -0
- package/dist/chunk-CDCGQFIA.js.map +0 -1
- package/dist/chunk-ESGUCYJS.mjs.map +0 -1
- package/dist/chunk-LRXJT656.js.map +0 -1
- package/src/features/user/components/details/UserDetails.tsx +0 -74
- /package/dist/{BlockNoteEditor-CZOW7J5K.mjs.map → BlockNoteEditor-TIX3GDVZ.mjs.map} +0 -0
- /package/dist/{chunk-5KMKI23S.mjs.map → chunk-ZYAAJMZZ.mjs.map} +0 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../shadcnui";
|
|
4
|
+
import { useCurrentUserContext } from "../contexts";
|
|
5
|
+
import { S3Interface } from "../features/s3/data/s3.interface";
|
|
6
|
+
import { S3Service } from "../features/s3/data/s3.service";
|
|
7
|
+
import { ModuleWithPermissions } from "../permissions";
|
|
8
|
+
import { errorToast } from "./errors/errorToast";
|
|
9
|
+
import { PencilIcon, Trash2Icon } from "lucide-react";
|
|
10
|
+
import { useTranslations } from "next-intl";
|
|
11
|
+
import { useCallback, useRef, useState } from "react";
|
|
12
|
+
import { cn } from "../utils/cn";
|
|
13
|
+
|
|
14
|
+
type EditableAvatarProps = {
|
|
15
|
+
entityId: string;
|
|
16
|
+
module: ModuleWithPermissions;
|
|
17
|
+
image?: string;
|
|
18
|
+
fallback: string;
|
|
19
|
+
alt: string;
|
|
20
|
+
patchImage: (imageKey: string) => Promise<void>;
|
|
21
|
+
className?: string;
|
|
22
|
+
fallbackClassName?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function EditableAvatar({
|
|
26
|
+
entityId,
|
|
27
|
+
module,
|
|
28
|
+
image,
|
|
29
|
+
fallback,
|
|
30
|
+
alt,
|
|
31
|
+
patchImage,
|
|
32
|
+
className,
|
|
33
|
+
fallbackClassName,
|
|
34
|
+
}: EditableAvatarProps) {
|
|
35
|
+
const { company } = useCurrentUserContext();
|
|
36
|
+
const t = useTranslations();
|
|
37
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
38
|
+
|
|
39
|
+
// Optimistic state: null means "use the prop", string means "override"
|
|
40
|
+
const [optimisticImage, setOptimisticImage] = useState<string | null>(null);
|
|
41
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
42
|
+
|
|
43
|
+
const displayImage = optimisticImage ?? image;
|
|
44
|
+
|
|
45
|
+
const generateS3Key = useCallback(
|
|
46
|
+
(file: File) => {
|
|
47
|
+
const ext = file.type.split("/").pop() ?? "";
|
|
48
|
+
const ts = new Date().toISOString().replace(/[-:T]/g, "").split(".")[0];
|
|
49
|
+
return `companies/${company!.id}/${module.name}/${entityId}/${entityId}.${ts}.${ext}`;
|
|
50
|
+
},
|
|
51
|
+
[company, module.name, entityId],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const handleFile = useCallback(
|
|
55
|
+
async (file: File) => {
|
|
56
|
+
if (isUploading) return;
|
|
57
|
+
if (!company) return;
|
|
58
|
+
|
|
59
|
+
const previousImage = image;
|
|
60
|
+
const previewUrl = URL.createObjectURL(file);
|
|
61
|
+
setOptimisticImage(previewUrl);
|
|
62
|
+
setIsUploading(true);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const s3Key = generateS3Key(file);
|
|
66
|
+
|
|
67
|
+
const s3: S3Interface = await S3Service.getPreSignedUrl({
|
|
68
|
+
key: s3Key,
|
|
69
|
+
contentType: file.type,
|
|
70
|
+
isPublic: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const uploadResponse = await fetch(s3.url, {
|
|
74
|
+
method: "PUT",
|
|
75
|
+
headers: s3.headers,
|
|
76
|
+
body: file,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!uploadResponse.ok) {
|
|
80
|
+
throw new Error(`S3 upload failed: ${uploadResponse.status}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await patchImage(s3Key);
|
|
84
|
+
setOptimisticImage(null);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
setOptimisticImage(previousImage ?? null);
|
|
87
|
+
errorToast({ title: t("generic.errors.update"), error });
|
|
88
|
+
} finally {
|
|
89
|
+
URL.revokeObjectURL(previewUrl);
|
|
90
|
+
setIsUploading(false);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
[company, generateS3Key, image, isUploading, patchImage, t],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const handleRemove = useCallback(async () => {
|
|
97
|
+
if (isUploading) return;
|
|
98
|
+
|
|
99
|
+
const previousImage = image;
|
|
100
|
+
setOptimisticImage("");
|
|
101
|
+
setIsUploading(true);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
await patchImage("");
|
|
105
|
+
setOptimisticImage(null);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
setOptimisticImage(previousImage ?? null);
|
|
108
|
+
errorToast({ title: t("generic.errors.update"), error });
|
|
109
|
+
} finally {
|
|
110
|
+
setIsUploading(false);
|
|
111
|
+
}
|
|
112
|
+
}, [image, isUploading, patchImage, t]);
|
|
113
|
+
|
|
114
|
+
const handleFileInputChange = useCallback(
|
|
115
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
116
|
+
const file = e.target.files?.[0];
|
|
117
|
+
if (file) handleFile(file);
|
|
118
|
+
e.target.value = "";
|
|
119
|
+
},
|
|
120
|
+
[handleFile],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const handleDrop = useCallback(
|
|
124
|
+
(e: React.DragEvent) => {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
const file = e.dataTransfer.files?.[0];
|
|
127
|
+
if (file && file.type.startsWith("image/")) {
|
|
128
|
+
handleFile(file);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
[handleFile],
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className={cn("group relative", className)} onDrop={handleDrop} onDragOver={handleDragOver}>
|
|
140
|
+
<Avatar className="h-full w-full">
|
|
141
|
+
{displayImage ? <AvatarImage src={displayImage} alt={alt} /> : null}
|
|
142
|
+
<AvatarFallback className={fallbackClassName}>{fallback}</AvatarFallback>
|
|
143
|
+
</Avatar>
|
|
144
|
+
|
|
145
|
+
{/* Hover overlay */}
|
|
146
|
+
<div
|
|
147
|
+
className={cn(
|
|
148
|
+
"absolute inset-0 flex items-center justify-center gap-x-2 rounded-full bg-black/50 opacity-0 transition-opacity group-hover:opacity-100",
|
|
149
|
+
isUploading && "opacity-100",
|
|
150
|
+
)}
|
|
151
|
+
>
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
onClick={() => fileInputRef.current?.click()}
|
|
155
|
+
disabled={isUploading}
|
|
156
|
+
className="rounded-full p-2 text-white hover:bg-white/20 disabled:opacity-50"
|
|
157
|
+
>
|
|
158
|
+
<PencilIcon className="h-4 w-4" />
|
|
159
|
+
</button>
|
|
160
|
+
{displayImage && (
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
onClick={handleRemove}
|
|
164
|
+
disabled={isUploading}
|
|
165
|
+
className="rounded-full p-2 text-white hover:bg-white/20 disabled:opacity-50"
|
|
166
|
+
>
|
|
167
|
+
<Trash2Icon className="h-4 w-4" />
|
|
168
|
+
</button>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<input ref={fileInputRef} type="file" accept="image/*" className="hidden" onChange={handleFileInputChange} />
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -62,7 +62,7 @@ export function RoundPageContainer({
|
|
|
62
62
|
)}
|
|
63
63
|
<div className="flex h-full w-full overflow-hidden">
|
|
64
64
|
<div className={cn(`grow overflow-y-auto p-4`, fullWidth && `p-0`)}>
|
|
65
|
-
<div className={cn(`mx-auto max-w-6xl space-y-12 p-8`, fullWidth && `max-w-full w-full p-0`)}>
|
|
65
|
+
<div className={cn(`mx-auto max-w-6xl space-y-12 p-8`, fullWidth && `max-w-full w-full p-0 h-full`)}>
|
|
66
66
|
{tabs ? (
|
|
67
67
|
<Tabs
|
|
68
68
|
value={activeTab}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { parseFiscalData } from "../../utils/fiscal-utils";
|
|
4
|
+
import { ItalianFiscalDataDisplay } from "./ItalianFiscalDataDisplay";
|
|
5
|
+
|
|
6
|
+
type FiscalDataDisplayProps = {
|
|
7
|
+
fiscalData: string;
|
|
8
|
+
country?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function hasNonEmptyValues(data: Record<string, string>): boolean {
|
|
12
|
+
return Object.values(data).some((v) => v && v.trim() !== "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function FiscalDataDisplay({ fiscalData, country = "it" }: FiscalDataDisplayProps) {
|
|
16
|
+
const data = parseFiscalData(fiscalData);
|
|
17
|
+
|
|
18
|
+
if (!hasNonEmptyValues(data)) return null;
|
|
19
|
+
|
|
20
|
+
switch (country) {
|
|
21
|
+
case "it":
|
|
22
|
+
return <ItalianFiscalDataDisplay data={data} />;
|
|
23
|
+
default:
|
|
24
|
+
return <ItalianFiscalDataDisplay data={data} />;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { validateItalianTaxCode, validatePartitaIva } from "../../utils/italian-validators";
|
|
4
|
+
import { Field, FieldError, FieldLabel, Input } from "../../shadcnui";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import { forwardRef, useCallback, useImperativeHandle, useRef, useState } from "react";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
export interface FiscalDataHandle {
|
|
10
|
+
validate: () => boolean;
|
|
11
|
+
getData: () => Record<string, string>;
|
|
12
|
+
isDirty: () => boolean;
|
|
13
|
+
reset: (initialData: Record<string, string>) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ItalianFiscalDataProps {
|
|
17
|
+
initialData: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ItalianFiscalData = forwardRef<FiscalDataHandle, ItalianFiscalDataProps>(function ItalianFiscalData(
|
|
21
|
+
{ initialData },
|
|
22
|
+
ref,
|
|
23
|
+
) {
|
|
24
|
+
const t = useTranslations();
|
|
25
|
+
const initialRef = useRef<Record<string, string>>(initialData);
|
|
26
|
+
const [fiscalData, setFiscalData] = useState<Record<string, string>>(initialData);
|
|
27
|
+
const fiscalDataRef = useRef<Record<string, string>>(initialData);
|
|
28
|
+
const [fiscalErrors, setFiscalErrors] = useState<Record<string, string>>({});
|
|
29
|
+
|
|
30
|
+
const updateFiscalField = useCallback((key: string, value: string) => {
|
|
31
|
+
setFiscalData((prev) => {
|
|
32
|
+
const next = { ...prev, [key]: value };
|
|
33
|
+
fiscalDataRef.current = next;
|
|
34
|
+
return next;
|
|
35
|
+
});
|
|
36
|
+
setFiscalErrors((prev) => {
|
|
37
|
+
const next = { ...prev };
|
|
38
|
+
delete next[key];
|
|
39
|
+
return next;
|
|
40
|
+
});
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
useImperativeHandle(ref, () => ({
|
|
44
|
+
validate: (): boolean => {
|
|
45
|
+
const data = fiscalDataRef.current;
|
|
46
|
+
const errors: Record<string, string> = {};
|
|
47
|
+
|
|
48
|
+
if (data.codice_fiscale && !validateItalianTaxCode(data.codice_fiscale, "codiceFiscale")) {
|
|
49
|
+
errors.codice_fiscale = t("common.fields.codice_fiscale.error");
|
|
50
|
+
}
|
|
51
|
+
if (data.partita_iva && !validatePartitaIva(data.partita_iva)) {
|
|
52
|
+
errors.partita_iva = t("common.fields.partita_iva.error");
|
|
53
|
+
}
|
|
54
|
+
if (data.pec && !z.string().email().safeParse(data.pec).success) {
|
|
55
|
+
errors.pec = t("common.fields.pec.error");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setFiscalErrors(errors);
|
|
59
|
+
return Object.keys(errors).length === 0;
|
|
60
|
+
},
|
|
61
|
+
getData: (): Record<string, string> => fiscalDataRef.current,
|
|
62
|
+
isDirty: (): boolean => JSON.stringify(fiscalDataRef.current) !== JSON.stringify(initialRef.current),
|
|
63
|
+
reset: (newInitialData: Record<string, string>) => {
|
|
64
|
+
initialRef.current = newInitialData;
|
|
65
|
+
fiscalDataRef.current = newInitialData;
|
|
66
|
+
setFiscalData(newInitialData);
|
|
67
|
+
setFiscalErrors({});
|
|
68
|
+
},
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
73
|
+
<Field data-invalid={!!fiscalErrors.codice_fiscale}>
|
|
74
|
+
<FieldLabel>{t("common.fields.codice_fiscale.label")}</FieldLabel>
|
|
75
|
+
<Input
|
|
76
|
+
value={fiscalData.codice_fiscale || ""}
|
|
77
|
+
onChange={(e) => updateFiscalField("codice_fiscale", e.target.value)}
|
|
78
|
+
placeholder={t("common.fields.codice_fiscale.placeholder")}
|
|
79
|
+
/>
|
|
80
|
+
{fiscalErrors.codice_fiscale && <FieldError>{fiscalErrors.codice_fiscale}</FieldError>}
|
|
81
|
+
</Field>
|
|
82
|
+
<Field data-invalid={!!fiscalErrors.partita_iva}>
|
|
83
|
+
<FieldLabel>{t("common.fields.partita_iva.label")}</FieldLabel>
|
|
84
|
+
<Input
|
|
85
|
+
value={fiscalData.partita_iva || ""}
|
|
86
|
+
onChange={(e) => updateFiscalField("partita_iva", e.target.value)}
|
|
87
|
+
placeholder={t("common.fields.partita_iva.placeholder")}
|
|
88
|
+
/>
|
|
89
|
+
{fiscalErrors.partita_iva && <FieldError>{fiscalErrors.partita_iva}</FieldError>}
|
|
90
|
+
</Field>
|
|
91
|
+
<Field>
|
|
92
|
+
<FieldLabel>{t("common.fields.sdi.label")}</FieldLabel>
|
|
93
|
+
<Input
|
|
94
|
+
value={fiscalData.sdi || ""}
|
|
95
|
+
onChange={(e) => updateFiscalField("sdi", e.target.value)}
|
|
96
|
+
placeholder={t("common.fields.sdi.placeholder")}
|
|
97
|
+
/>
|
|
98
|
+
</Field>
|
|
99
|
+
<Field>
|
|
100
|
+
<FieldLabel>{t("common.fields.rea.label")}</FieldLabel>
|
|
101
|
+
<Input
|
|
102
|
+
value={fiscalData.rea || ""}
|
|
103
|
+
onChange={(e) => updateFiscalField("rea", e.target.value)}
|
|
104
|
+
placeholder={t("common.fields.rea.placeholder")}
|
|
105
|
+
/>
|
|
106
|
+
</Field>
|
|
107
|
+
<Field data-invalid={!!fiscalErrors.pec}>
|
|
108
|
+
<FieldLabel>{t("common.fields.pec.label")}</FieldLabel>
|
|
109
|
+
<Input
|
|
110
|
+
value={fiscalData.pec || ""}
|
|
111
|
+
onChange={(e) => updateFiscalField("pec", e.target.value)}
|
|
112
|
+
placeholder={t("common.fields.pec.placeholder")}
|
|
113
|
+
/>
|
|
114
|
+
{fiscalErrors.pec && <FieldError>{fiscalErrors.pec}</FieldError>}
|
|
115
|
+
</Field>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export default ItalianFiscalData;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AttributeElement } from "../contents";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
|
|
6
|
+
type ItalianFiscalDataDisplayProps = {
|
|
7
|
+
data: Record<string, string>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function ItalianFiscalDataDisplay({ data }: ItalianFiscalDataDisplayProps) {
|
|
11
|
+
const t = useTranslations();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
15
|
+
{data.codice_fiscale && (
|
|
16
|
+
<AttributeElement title={t("common.fields.codice_fiscale.label")} value={data.codice_fiscale} />
|
|
17
|
+
)}
|
|
18
|
+
{data.partita_iva && <AttributeElement title={t("common.fields.partita_iva.label")} value={data.partita_iva} />}
|
|
19
|
+
{data.sdi && <AttributeElement title={t("common.fields.sdi.label")} value={data.sdi} />}
|
|
20
|
+
{data.rea && <AttributeElement title={t("common.fields.rea.label")} value={data.rea} />}
|
|
21
|
+
{data.pec && <AttributeElement title={t("common.fields.pec.label")} value={data.pec} />}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as ItalianFiscalData } from "./ItalianFiscalData";
|
|
2
|
+
export type { FiscalDataHandle, ItalianFiscalDataProps } from "./ItalianFiscalData";
|
|
3
|
+
export { ItalianFiscalDataDisplay } from "./ItalianFiscalDataDisplay";
|
|
4
|
+
export { FiscalDataDisplay } from "./FiscalDataDisplay";
|
package/src/components/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { EntityAvatar } from "./EntityAvatar";
|
|
2
|
+
export { EditableAvatar } from "./EditableAvatar";
|
|
2
3
|
export { TableCellAvatar } from "./TableCellAvatar";
|
|
3
4
|
export { getInitials } from "../utils/getInitials";
|
|
4
5
|
export * from "./containers";
|
|
@@ -10,6 +11,8 @@ export * from "./forms";
|
|
|
10
11
|
export * from "./navigations";
|
|
11
12
|
export * from "./pages";
|
|
12
13
|
export * from "./tables";
|
|
14
|
+
export * from "./fiscal";
|
|
15
|
+
export { parseFiscalData } from "../utils/fiscal-utils";
|
|
13
16
|
|
|
14
17
|
// Feature components
|
|
15
18
|
export * from "../features/auth/components";
|
|
@@ -26,10 +26,10 @@ export function RecentPagesNavigator() {
|
|
|
26
26
|
|
|
27
27
|
return (
|
|
28
28
|
<DropdownMenu>
|
|
29
|
-
<DropdownMenuTrigger>
|
|
30
|
-
<
|
|
29
|
+
<DropdownMenuTrigger render={<span />} nativeButton={false}>
|
|
30
|
+
<span className="flex w-full cursor-pointer items-center gap-2">
|
|
31
31
|
{state === "collapsed" ? <HistoryIcon className="h-4 w-4" /> : <span>{t(`common.recent_pages`)}</span>}
|
|
32
|
-
</
|
|
32
|
+
</span>
|
|
33
33
|
</DropdownMenuTrigger>
|
|
34
34
|
<DropdownMenuContent align="start" className="w-96">
|
|
35
35
|
<DropdownMenuLabel>{t(`common.recent_pages`)}</DropdownMenuLabel>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { MapPinIcon } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import Image from "next/image";
|
|
6
|
+
import { ReactNode } from "react";
|
|
7
|
+
import { AttributeElement } from "../../../../components";
|
|
8
|
+
import { FiscalDataDisplay } from "../../../../components/fiscal/FiscalDataDisplay";
|
|
9
|
+
import { CompanyInterface } from "../../data";
|
|
10
|
+
|
|
11
|
+
type CompanyContentProps = {
|
|
12
|
+
company?: CompanyInterface;
|
|
13
|
+
actions?: ReactNode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function hasAddressDetails(company: CompanyInterface): boolean {
|
|
17
|
+
return !!(
|
|
18
|
+
company.street ||
|
|
19
|
+
company.street_number ||
|
|
20
|
+
company.city ||
|
|
21
|
+
company.province ||
|
|
22
|
+
company.region ||
|
|
23
|
+
company.postcode ||
|
|
24
|
+
company.country
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function CompanyContent({ company, actions }: CompanyContentProps) {
|
|
29
|
+
const t = useTranslations();
|
|
30
|
+
|
|
31
|
+
if (!company) return null;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="flex flex-col gap-y-8">
|
|
35
|
+
{/* Title Row */}
|
|
36
|
+
<div className="flex w-full items-center justify-between">
|
|
37
|
+
<h2 className="text-lg font-semibold">{company.name}</h2>
|
|
38
|
+
{actions && <div className="flex items-center gap-x-2">{actions}</div>}
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
{/* Hero Section */}
|
|
42
|
+
<div className="flex items-start gap-x-6">
|
|
43
|
+
{company.logo && (
|
|
44
|
+
<Image src={company.logo} alt={company.name} width={150} height={150} className="rounded-md" />
|
|
45
|
+
)}
|
|
46
|
+
<div className="flex flex-col gap-y-2">
|
|
47
|
+
{company.legal_address && (
|
|
48
|
+
<div className="text-muted-foreground flex items-center gap-x-2 text-sm">
|
|
49
|
+
<MapPinIcon className="h-4 w-4 shrink-0" />
|
|
50
|
+
{company.legal_address}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
<div className="flex flex-col gap-y-1">
|
|
54
|
+
{company.configurations?.country && (
|
|
55
|
+
<div className="text-muted-foreground text-sm">
|
|
56
|
+
<span className="font-medium">{t("features.configuration.country")}:</span>{" "}
|
|
57
|
+
{company.configurations.country}
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
{company.configurations?.currency && (
|
|
61
|
+
<div className="text-muted-foreground text-sm">
|
|
62
|
+
<span className="font-medium">{t("features.configuration.currency")}:</span>{" "}
|
|
63
|
+
{company.configurations.currency}
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* Fiscal Data Section */}
|
|
71
|
+
{company.fiscal_data && (
|
|
72
|
+
<div className="flex flex-col gap-y-3">
|
|
73
|
+
<h3 className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
|
|
74
|
+
{t("company.sections.fiscal_data")}
|
|
75
|
+
</h3>
|
|
76
|
+
<FiscalDataDisplay fiscalData={company.fiscal_data} />
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{/* Address Details Section */}
|
|
81
|
+
{hasAddressDetails(company) && (
|
|
82
|
+
<div className="flex flex-col gap-y-3">
|
|
83
|
+
<h3 className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
|
|
84
|
+
{t("company.sections.address_details")}
|
|
85
|
+
</h3>
|
|
86
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
87
|
+
{company.street && <AttributeElement title={t("company.fields.street.label")} value={company.street} />}
|
|
88
|
+
{company.street_number && (
|
|
89
|
+
<AttributeElement title={t("company.fields.street_number.label")} value={company.street_number} />
|
|
90
|
+
)}
|
|
91
|
+
{company.city && <AttributeElement title={t("company.fields.city.label")} value={company.city} />}
|
|
92
|
+
{company.province && (
|
|
93
|
+
<AttributeElement title={t("company.fields.province.label")} value={company.province} />
|
|
94
|
+
)}
|
|
95
|
+
{company.region && <AttributeElement title={t("company.fields.region.label")} value={company.region} />}
|
|
96
|
+
{company.postcode && (
|
|
97
|
+
<AttributeElement title={t("company.fields.postcode.label")} value={company.postcode} />
|
|
98
|
+
)}
|
|
99
|
+
{company.country && <AttributeElement title={t("company.fields.country.label")} value={company.country} />}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useTranslations } from "next-intl";
|
|
4
|
-
import Image from "next/image";
|
|
5
3
|
import { Modules } from "../../../../";
|
|
6
4
|
import { ContentTitle } from "../../../../components";
|
|
7
5
|
import { useSharedContext } from "../../../../contexts";
|
|
8
6
|
import { usePageUrlGenerator } from "../../../../hooks";
|
|
9
7
|
import { useCompanyContext } from "../../contexts/CompanyContext";
|
|
8
|
+
import { CompanyContent } from "./CompanyContent";
|
|
10
9
|
import { TokenStatusIndicator } from "./TokenStatusIndicator";
|
|
11
10
|
|
|
12
11
|
export function CompanyDetails() {
|
|
13
|
-
const t = useTranslations();
|
|
14
12
|
const { title } = useSharedContext();
|
|
15
13
|
const _generateUrl = usePageUrlGenerator();
|
|
16
14
|
|
|
@@ -20,23 +18,8 @@ export function CompanyDetails() {
|
|
|
20
18
|
return (
|
|
21
19
|
<div className="flex w-full flex-col gap-y-2">
|
|
22
20
|
<ContentTitle module={Modules.Company} type={title.type} element={title.element} functions={title.functions} />
|
|
23
|
-
{company.logo && (
|
|
24
|
-
<Image src={company.logo} alt={company.name} width={150} height={150} className="mb-4 rounded-md" />
|
|
25
|
-
)}
|
|
26
21
|
<TokenStatusIndicator size="md" />
|
|
27
|
-
<
|
|
28
|
-
{company.configurations?.country && (
|
|
29
|
-
<div className="text-muted-foreground text-sm">
|
|
30
|
-
<span className="font-medium">{t("features.configuration.country")}:</span> {company.configurations.country}
|
|
31
|
-
</div>
|
|
32
|
-
)}
|
|
33
|
-
{company.configurations?.currency && (
|
|
34
|
-
<div className="text-muted-foreground text-sm">
|
|
35
|
-
<span className="font-medium">{t("features.configuration.currency")}:</span>{" "}
|
|
36
|
-
{company.configurations.currency}
|
|
37
|
-
</div>
|
|
38
|
-
)}
|
|
39
|
-
</div>
|
|
22
|
+
<CompanyContent company={company} />
|
|
40
23
|
</div>
|
|
41
24
|
);
|
|
42
25
|
}
|