@carlonicora/nextjs-jsonapi 1.66.0 → 1.68.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/{BlockNoteEditor-KCJMA6LW.mjs → BlockNoteEditor-6FDECIS2.mjs} +4 -4
- package/dist/{BlockNoteEditor-GQM2TZG2.js → BlockNoteEditor-DXHROT4C.js} +14 -14
- package/dist/{BlockNoteEditor-GQM2TZG2.js.map → BlockNoteEditor-DXHROT4C.js.map} +1 -1
- package/dist/billing/index.js +346 -346
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-NVXYOQFW.js → chunk-37KYO2UD.js} +20 -5
- package/dist/chunk-37KYO2UD.js.map +1 -0
- package/dist/{chunk-QIFM4G7T.js → chunk-ELTHSXBI.js} +1476 -1298
- package/dist/chunk-ELTHSXBI.js.map +1 -0
- package/dist/{chunk-4E74ZTRT.mjs → chunk-H4ZS3R76.mjs} +2606 -2428
- package/dist/chunk-H4ZS3R76.mjs.map +1 -0
- package/dist/{chunk-35GWVOYZ.mjs → chunk-IOMDNRX5.mjs} +20 -5
- package/dist/{chunk-35GWVOYZ.mjs.map → chunk-IOMDNRX5.mjs.map} +1 -1
- package/dist/{chunk-OQRBY22T.js → chunk-WOJIRXIP.js} +11 -11
- package/dist/{chunk-OQRBY22T.js.map → chunk-WOJIRXIP.js.map} +1 -1
- package/dist/{chunk-UXGPZZ6V.mjs → chunk-WVTBEVAL.mjs} +2 -2
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +29 -7
- package/dist/components/index.d.ts +29 -7
- package/dist/components/index.js +8 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +9 -5
- package/dist/contexts/index.d.mts +1 -1
- package/dist/contexts/index.d.ts +1 -1
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/index.mjs +2 -2
- package/dist/{s3.service-XchHd3ii.d.mts → s3.service-CHOTwfWA.d.mts} +7 -0
- package/dist/{s3.service-DIR6Su9B.d.ts → s3.service-N1g0piXD.d.ts} +7 -0
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -1
- package/src/components/EditableAvatar.tsx +175 -0
- package/src/components/index.ts +1 -0
- package/src/features/company/components/forms/CompanyEditor.tsx +1 -3
- 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 +18 -0
- package/src/features/user/data/user.ts +3 -4
- package/dist/chunk-4E74ZTRT.mjs.map +0 -1
- package/dist/chunk-NVXYOQFW.js.map +0 -1
- package/dist/chunk-QIFM4G7T.js.map +0 -1
- package/src/features/user/components/details/UserDetails.tsx +0 -74
- /package/dist/{BlockNoteEditor-KCJMA6LW.mjs.map → BlockNoteEditor-6FDECIS2.mjs.map} +0 -0
- /package/dist/{chunk-UXGPZZ6V.mjs.map → chunk-WVTBEVAL.mjs.map} +0 -0
package/dist/server/index.js
CHANGED
|
@@ -15,7 +15,7 @@ var _chunk3ZPK4QOBjs = require('../chunk-3ZPK4QOB.js');
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
var
|
|
18
|
+
var _chunk37KYO2UDjs = require('../chunk-37KYO2UD.js');
|
|
19
19
|
require('../chunk-LXKSUWAV.js');
|
|
20
20
|
require('../chunk-IBS6NI7D.js');
|
|
21
21
|
|
|
@@ -86,7 +86,7 @@ var ServerSession = class {
|
|
|
86
86
|
if (!rawModules) return false;
|
|
87
87
|
const modules = JSON.parse(_pako2.default.ungzip(Buffer.from(rawModules, "base64"), { to: "string" }));
|
|
88
88
|
const selectedModule = modules.find((module) => module.id === params.module.moduleId);
|
|
89
|
-
return
|
|
89
|
+
return _chunk37KYO2UDjs.checkPermissionsFromServer.call(void 0, {
|
|
90
90
|
module: params.module,
|
|
91
91
|
action: params.action,
|
|
92
92
|
data: params.data,
|
|
@@ -296,5 +296,5 @@ _chunk7QVYU63Ejs.__name.call(void 0, ServerJsonApiDelete, "ServerJsonApiDelete")
|
|
|
296
296
|
|
|
297
297
|
|
|
298
298
|
|
|
299
|
-
exports.ServerAuthService =
|
|
299
|
+
exports.ServerAuthService = _chunk37KYO2UDjs.AuthService; exports.ServerCompanyService = _chunk37KYO2UDjs.CompanyService; exports.ServerContentService = _chunk37KYO2UDjs.ContentService; exports.ServerFeatureService = _chunk37KYO2UDjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunk37KYO2UDjs.NotificationService; exports.ServerPushService = _chunk37KYO2UDjs.PushService; exports.ServerRoleService = _chunk37KYO2UDjs.RoleService; exports.ServerS3Service = _chunk37KYO2UDjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunk37KYO2UDjs.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
|
|
300
300
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -195,9 +195,7 @@ function CompanyEditorInternal({
|
|
|
195
195
|
fiscal_data: fiscalRef.current ? JSON.stringify(fiscalRef.current.getData()) : undefined,
|
|
196
196
|
};
|
|
197
197
|
|
|
198
|
-
const updatedCompany = company
|
|
199
|
-
? await CompanyService.update(payload)
|
|
200
|
-
: await CompanyService.create(payload);
|
|
198
|
+
const updatedCompany = company ? await CompanyService.update(payload) : await CompanyService.create(payload);
|
|
201
199
|
|
|
202
200
|
// Refresh user context after company changes
|
|
203
201
|
const fullUser = await UserService.findFullUser();
|
|
@@ -22,7 +22,7 @@ export function FormRoles({ form, id, name, roles }: FormRolesProps) {
|
|
|
22
22
|
<div className="flex w-full flex-col">
|
|
23
23
|
<FormFieldWrapper form={form} name={id} label={name}>
|
|
24
24
|
{(field) => (
|
|
25
|
-
<div>
|
|
25
|
+
<div className="flex w-full flex-col gap-y-1">
|
|
26
26
|
{roles
|
|
27
27
|
.filter((role: RoleInterface) => role.isSelectable)
|
|
28
28
|
.sort((a: RoleInterface, b: RoleInterface) => a.name.localeCompare(b.name))
|
|
@@ -30,9 +30,10 @@ export function FormRoles({ form, id, name, roles }: FormRolesProps) {
|
|
|
30
30
|
if (role.requiredFeature && !hasAccesToFeature(role.requiredFeature.id)) return null;
|
|
31
31
|
|
|
32
32
|
return (
|
|
33
|
-
<div key={role.id}>
|
|
33
|
+
<div key={role.id} className="flex w-full items-center">
|
|
34
34
|
<Checkbox
|
|
35
|
-
|
|
35
|
+
id={`role-${role.id}`}
|
|
36
|
+
checked={(field.value as string[]).some((roleId: string) => roleId === role.id)}
|
|
36
37
|
onCheckedChange={(checked) => {
|
|
37
38
|
if (checked) {
|
|
38
39
|
form.setValue(id, [...(field.value as string[]), role.id]);
|
|
@@ -46,7 +47,7 @@ export function FormRoles({ form, id, name, roles }: FormRolesProps) {
|
|
|
46
47
|
/>
|
|
47
48
|
<Tooltip>
|
|
48
49
|
<TooltipTrigger>
|
|
49
|
-
<FieldLabel className="ml-3 font-normal">
|
|
50
|
+
<FieldLabel htmlFor={`role-${role.id}`} className="ml-3 cursor-pointer font-normal">
|
|
50
51
|
{t(`role.roles`, { role: role.id.replaceAll(`-`, ``) })}
|
|
51
52
|
</FieldLabel>
|
|
52
53
|
</TooltipTrigger>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { CompanyUsersList, Tab, TabsContainer } from "../../../../components";
|
|
5
|
+
import { Modules } from "../../../../core";
|
|
6
|
+
import { Action } from "../../../../permissions";
|
|
7
|
+
import { useCurrentUserContext } from "../../contexts";
|
|
8
|
+
import { UserInterface } from "../../data";
|
|
9
|
+
|
|
10
|
+
function AllUsersListContainerInternal() {
|
|
11
|
+
const { hasPermissionToModule } = useCurrentUserContext<UserInterface>();
|
|
12
|
+
const t = useTranslations();
|
|
13
|
+
|
|
14
|
+
if (!hasPermissionToModule({ module: Modules.User, action: Action.Delete })) return <CompanyUsersList />;
|
|
15
|
+
|
|
16
|
+
const tabs: Tab[] = [
|
|
17
|
+
{
|
|
18
|
+
label: t(`entities.users`, { count: 2 }),
|
|
19
|
+
content: <CompanyUsersList />,
|
|
20
|
+
modules: [Modules.Company],
|
|
21
|
+
action: Action.Read,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: t(`user.deleted`),
|
|
25
|
+
content: <CompanyUsersList isDeleted={true} />,
|
|
26
|
+
modules: [Modules.Company],
|
|
27
|
+
action: Action.Update,
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
return <TabsContainer tabs={tabs} />;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function AllUsersListContainer() {
|
|
35
|
+
return <AllUsersListContainerInternal />;
|
|
36
|
+
}
|
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { RoundPageContainer, Tab } from "@/components";
|
|
4
|
+
import { Modules } from "@/core";
|
|
4
5
|
import { useUserContext } from "../../contexts";
|
|
5
|
-
import {
|
|
6
|
+
import { UserContent } from "../details";
|
|
6
7
|
|
|
7
8
|
export function UserContainer() {
|
|
8
9
|
const { user } = useUserContext();
|
|
9
10
|
if (!user) return null;
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
+
const tabs: Tab[] = [
|
|
13
|
+
{
|
|
14
|
+
label: "Details",
|
|
15
|
+
content: <UserContent user={user} />,
|
|
16
|
+
},
|
|
17
|
+
];
|
|
12
18
|
|
|
13
|
-
return
|
|
14
|
-
<div className="flex w-full gap-x-4">
|
|
15
|
-
<div className="w-2xl flex h-[calc(100vh-theme(spacing.20))] flex-col justify-between border-r pr-4">
|
|
16
|
-
<div className="flex h-full overflow-y-auto">
|
|
17
|
-
<UserDetails user={user} />
|
|
18
|
-
</div>
|
|
19
|
-
</div>
|
|
20
|
-
<div className="flex w-full flex-col gap-y-4"></div>
|
|
21
|
-
</div>
|
|
22
|
-
);
|
|
19
|
+
return <RoundPageContainer module={Modules.User} tabs={tabs} />;
|
|
23
20
|
}
|
|
@@ -1,36 +1,27 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { CompanyUsersList, Tab, TabsContainer } from "../../../../components";
|
|
3
|
+
import { CompanyUsersList, RoundPageContainer } from "../../../../components";
|
|
5
4
|
import { Modules } from "../../../../core";
|
|
6
5
|
import { Action } from "../../../../permissions";
|
|
7
6
|
import { useCurrentUserContext } from "../../contexts";
|
|
8
7
|
import { UserInterface } from "../../data";
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
type UsersListContainerProps = {
|
|
10
|
+
fullWidth?: boolean;
|
|
11
|
+
};
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
function UsersListContainerInternal({ fullWidth }: UsersListContainerProps) {
|
|
14
|
+
return (
|
|
15
|
+
<RoundPageContainer module={Modules.User} fullWidth={fullWidth}>
|
|
16
|
+
<CompanyUsersList fullWidth={fullWidth} />
|
|
17
|
+
</RoundPageContainer>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
label: t(`entities.users`, { count: 2 }),
|
|
19
|
-
content: <CompanyUsersList />,
|
|
20
|
-
modules: [Modules.Company],
|
|
21
|
-
action: Action.Read,
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
label: t(`user.deleted`),
|
|
25
|
-
content: <CompanyUsersList isDeleted={true} />,
|
|
26
|
-
modules: [Modules.Company],
|
|
27
|
-
action: Action.Update,
|
|
28
|
-
},
|
|
29
|
-
];
|
|
21
|
+
export function UsersListContainer({ fullWidth }: UsersListContainerProps) {
|
|
22
|
+
const { hasPermissionToModule } = useCurrentUserContext<UserInterface>();
|
|
30
23
|
|
|
31
|
-
|
|
32
|
-
}
|
|
24
|
+
if (!hasPermissionToModule({ module: Modules.User, action: Action.Read })) return null;
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
return <UsersListContainerInternal />;
|
|
26
|
+
return <UsersListContainerInternal fullWidth={fullWidth} />;
|
|
36
27
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { AttributeElement } from "../../../../components/contents";
|
|
5
|
+
import { EditableAvatar } from "../../../../components/EditableAvatar";
|
|
6
|
+
import { getInitials } from "../../../../utils/getInitials";
|
|
7
|
+
import { Modules } from "../../../../core";
|
|
8
|
+
import { usePageUrlGenerator } from "../../../../hooks";
|
|
9
|
+
import { Badge, Link } from "../../../../shadcnui";
|
|
10
|
+
import { RoleInterface } from "../../../role";
|
|
11
|
+
import { UserInterface } from "../../data";
|
|
12
|
+
import { UserService } from "../../data/user.service";
|
|
13
|
+
import { useUserContext } from "../../contexts";
|
|
14
|
+
import { BriefcaseIcon, MailIcon, PhoneIcon } from "lucide-react";
|
|
15
|
+
|
|
16
|
+
type UserContentProps = {
|
|
17
|
+
user: UserInterface;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function UserContent({ user }: UserContentProps) {
|
|
21
|
+
const t = useTranslations();
|
|
22
|
+
const generateUrl = usePageUrlGenerator();
|
|
23
|
+
const { setUser } = useUserContext();
|
|
24
|
+
|
|
25
|
+
const hasBio = !!user.bio;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="flex flex-col gap-y-8">
|
|
29
|
+
{/* Hero Section */}
|
|
30
|
+
<div className="flex items-start gap-x-6">
|
|
31
|
+
<EditableAvatar
|
|
32
|
+
entityId={user.id}
|
|
33
|
+
module={Modules.User}
|
|
34
|
+
image={user.avatar}
|
|
35
|
+
fallback={getInitials(user.name)}
|
|
36
|
+
alt={user.name}
|
|
37
|
+
patchImage={async (imageKey) => {
|
|
38
|
+
const updated = await UserService.patchAvatar({ id: user.id, avatar: imageKey });
|
|
39
|
+
setUser(updated);
|
|
40
|
+
}}
|
|
41
|
+
className="h-24 w-24"
|
|
42
|
+
fallbackClassName="text-2xl"
|
|
43
|
+
/>
|
|
44
|
+
<div className="flex flex-col gap-y-2">
|
|
45
|
+
{user.roles && user.roles.length > 0 && (
|
|
46
|
+
<div className="flex flex-wrap gap-2">
|
|
47
|
+
{user.roles.map((role: RoleInterface) => (
|
|
48
|
+
<Link key={role.id} href={generateUrl({ page: Modules.Role, id: role.id })}>
|
|
49
|
+
<Badge variant="default">{t(`role.roles`, { role: role.id.replaceAll(`-`, ``) })}</Badge>
|
|
50
|
+
</Link>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
{user.isDeleted ? (
|
|
55
|
+
<div>
|
|
56
|
+
<Badge variant="destructive">{t(`user.errors.deleted`)}</Badge>
|
|
57
|
+
</div>
|
|
58
|
+
) : (
|
|
59
|
+
<>
|
|
60
|
+
{!user.isActivated && (
|
|
61
|
+
<div>
|
|
62
|
+
<Badge variant="destructive">{t(`user.errors.inactive`)}</Badge>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</>
|
|
66
|
+
)}
|
|
67
|
+
{user.title && (
|
|
68
|
+
<div className="text-muted-foreground flex items-center gap-x-2 text-sm">
|
|
69
|
+
<BriefcaseIcon className="h-4 w-4 shrink-0" />
|
|
70
|
+
{user.title}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
{user.email && (
|
|
74
|
+
<div className="text-muted-foreground flex items-center gap-x-2 text-sm">
|
|
75
|
+
<MailIcon className="h-4 w-4 shrink-0" />
|
|
76
|
+
{user.email}
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
{user.phone && (
|
|
80
|
+
<div className="text-muted-foreground flex items-center gap-x-2 text-sm">
|
|
81
|
+
<PhoneIcon className="h-4 w-4 shrink-0" />
|
|
82
|
+
{user.phone}
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Bio Section */}
|
|
89
|
+
{hasBio && <AttributeElement title={t(`user.fields.bio.label`)} value={user.bio} />}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|