@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
@@ -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";
@@ -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
- <div className="flex w-full cursor-pointer items-center gap-2">
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
- </div>
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
- <div className="flex flex-col gap-y-1">
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
  }
@@ -1,2 +1,3 @@
1
+ export * from "./CompanyContent";
1
2
  export * from "./CompanyDetails";
2
3
  export * from "./TokenStatusIndicator";