@serhii.olishevskyi/docs 1.0.1 → 1.0.3

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.
@@ -0,0 +1,399 @@
1
+ import {
2
+ CopyClipboardButtonComponent,
3
+ TranslatableTooltipComponent,
4
+ TranslateText,
5
+ UserRoleChipComponent,
6
+ UserStatusChipComponent,
7
+ getUserLastSeenAt,
8
+ useAuth,
9
+ useLocalization,
10
+ useSupabase
11
+ } from "./chunk-CT3G3YSD.mjs";
12
+
13
+ // src/UI/components/data.display/UserPopoverComponent.tsx
14
+ import { useEffect as useEffect2, useState as useState2 } from "react";
15
+ import {
16
+ Popover,
17
+ Box,
18
+ Avatar,
19
+ Typography,
20
+ Divider,
21
+ IconButton,
22
+ CircularProgress,
23
+ Stack,
24
+ Badge
25
+ } from "@mui/material";
26
+ import {
27
+ NotificationsNone as NotificationIcon,
28
+ AssignmentOutlined as TaskIcon,
29
+ ChatBubble as ChatIcon,
30
+ Work as WorkIcon,
31
+ CalendarToday as CalendarIcon,
32
+ Phone as PhoneIcon,
33
+ Close as CloseIcon,
34
+ Email as EmailIcon,
35
+ AccessTime as AccessTimeIcon,
36
+ Cake as CakeIcon
37
+ } from "@mui/icons-material";
38
+
39
+ // src/context/UserSettingsProvider.tsx
40
+ import { createContext, useContext, useEffect, useState, useCallback } from "react";
41
+ import { jsx } from "react/jsx-runtime";
42
+ var UserSettingsContext = createContext(void 0);
43
+ var useUserSettings = () => {
44
+ const context = useContext(UserSettingsContext);
45
+ if (!context) {
46
+ throw new Error("useUserSettings should be used within the UserSettingsProvider");
47
+ }
48
+ return context;
49
+ };
50
+
51
+ // src/UI/components/data.display/UserPopoverComponent.tsx
52
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
53
+ var useUserStatus = (supabase, userId, open) => {
54
+ const [lastSeenAt, setLastSeenAt] = useState2(null);
55
+ const [loading, setLoading] = useState2(true);
56
+ useEffect2(() => {
57
+ if (!userId || !open) {
58
+ setLastSeenAt(null);
59
+ setLoading(false);
60
+ return;
61
+ }
62
+ let isMounted = true;
63
+ const loadStatus = async () => {
64
+ setLoading(true);
65
+ const date = await getUserLastSeenAt(supabase, userId);
66
+ if (isMounted) {
67
+ setLastSeenAt(date);
68
+ setLoading(false);
69
+ }
70
+ };
71
+ loadStatus();
72
+ const channel = supabase.channel(`user_status_popover_${userId}`).on(
73
+ "postgres_changes",
74
+ {
75
+ event: "*",
76
+ schema: "public",
77
+ table: "user_sessions",
78
+ filter: `user_id=eq.${userId}`
79
+ },
80
+ () => {
81
+ loadStatus();
82
+ }
83
+ ).subscribe();
84
+ return () => {
85
+ isMounted = false;
86
+ supabase.removeChannel(channel);
87
+ };
88
+ }, [userId, open]);
89
+ const isOnline = Boolean(
90
+ lastSeenAt && (/* @__PURE__ */ new Date()).getTime() - new Date(lastSeenAt).getTime() < 3 * 60 * 1e3
91
+ );
92
+ return { lastSeenAt, isOnline, loading };
93
+ };
94
+ var UserPopoverComponent = ({
95
+ overrideNamespace,
96
+ userId,
97
+ anchorEl,
98
+ open,
99
+ onClose
100
+ }) => {
101
+ const supabase = useSupabase();
102
+ const { activeCompanyId, membership: currentMembership } = useAuth();
103
+ const { openChatWithUser } = useUserSettings();
104
+ const { formatDate, formatRelativeTime } = useLocalization();
105
+ const [profileData, setProfileData] = useState2(null);
106
+ const [membershipData, setMembershipData] = useState2(null);
107
+ const [avatarUrl, setAvatarUrl] = useState2(null);
108
+ const [coverUrl, setCoverUrl] = useState2(null);
109
+ const [loading, setLoading] = useState2(true);
110
+ const { lastSeenAt, isOnline } = useUserStatus(supabase, userId, open);
111
+ useEffect2(() => {
112
+ if (!open || !userId || !activeCompanyId) return;
113
+ let isMounted = true;
114
+ setLoading(true);
115
+ const fetchUserData = async () => {
116
+ try {
117
+ const { data, error } = await supabase.rpc("get_user_avatar_and_profile", {
118
+ p_company_id: activeCompanyId,
119
+ //currentMembership?.id || null,
120
+ p_user_id: userId
121
+ }).maybeSingle();
122
+ if (error) throw error;
123
+ if (isMounted && data) {
124
+ const rpcData = data;
125
+ const profile = rpcData.profile;
126
+ const membership = rpcData.membership;
127
+ setProfileData(profile);
128
+ setMembershipData(membership);
129
+ const avatarPath = profile?.avatar_path || profile?.avatar_url;
130
+ if (avatarPath && !avatarPath.startsWith("http")) {
131
+ const { data: avatarSigned } = await supabase.storage.from("user-media").createSignedUrl(avatarPath, 3600);
132
+ if (isMounted) setAvatarUrl(avatarSigned?.signedUrl || null);
133
+ } else {
134
+ if (isMounted) setAvatarUrl(avatarPath || null);
135
+ }
136
+ const coverPath = profile?.cover_image_path || profile?.cover_image_url;
137
+ if (coverPath && !coverPath.startsWith("http")) {
138
+ const { data: coverSigned } = await supabase.storage.from("user-media").createSignedUrl(coverPath, 3600);
139
+ if (isMounted) setCoverUrl(coverSigned?.signedUrl || null);
140
+ } else {
141
+ if (isMounted) setCoverUrl(coverPath || null);
142
+ }
143
+ }
144
+ } catch (err) {
145
+ console.error("Error loading user popover data:", err);
146
+ } finally {
147
+ if (isMounted) setLoading(false);
148
+ }
149
+ };
150
+ fetchUserData();
151
+ return () => {
152
+ isMounted = false;
153
+ };
154
+ }, [open, userId, currentMembership?.id, activeCompanyId]);
155
+ const getStatusChip = () => {
156
+ if (!profileData) return null;
157
+ return /* @__PURE__ */ jsx2(UserStatusChipComponent, { state: { profile_archived: profileData.archived, profile_locked: profileData.locked, excluded: membershipData?.excluded || false, locked: membershipData?.locked || false, locked_reason: membershipData?.locked_reason || "" } });
158
+ };
159
+ const getStatusTypographyColor = (isOnline2, lastSeenAt2) => {
160
+ if (isOnline2) return "success";
161
+ if (!lastSeenAt2) return "textSecondary";
162
+ const diffInMinutes = (Date.now() - new Date(lastSeenAt2).getTime()) / (1e3 * 60);
163
+ if (diffInMinutes <= 15) {
164
+ return "warning";
165
+ }
166
+ return "textSecondary";
167
+ };
168
+ const getStatusIconColor = (isOnline2, lastSeenAt2) => {
169
+ if (isOnline2) return "success";
170
+ if (!lastSeenAt2) return "inherit";
171
+ const diffInMinutes = (Date.now() - new Date(lastSeenAt2).getTime()) / (1e3 * 60);
172
+ if (diffInMinutes <= 15) {
173
+ return "warning";
174
+ }
175
+ return "inherit";
176
+ };
177
+ const canChat = currentMembership && membershipData && currentMembership.id !== membershipData.id;
178
+ return /* @__PURE__ */ jsx2(
179
+ Popover,
180
+ {
181
+ open,
182
+ anchorEl,
183
+ onClose,
184
+ anchorOrigin: { vertical: "bottom", horizontal: "left" },
185
+ transformOrigin: { vertical: "top", horizontal: "left" },
186
+ slotProps: {
187
+ paper: {
188
+ sx: {
189
+ width: 350,
190
+ p: 0,
191
+ overflow: "hidden",
192
+ borderRadius: 0,
193
+ boxShadow: 4
194
+ }
195
+ }
196
+ },
197
+ children: loading ? /* @__PURE__ */ jsx2(Box, { sx: { display: "flex", justifyContent: "center", py: 4 }, children: /* @__PURE__ */ jsx2(CircularProgress, { size: 28 }) }) : profileData ? /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", flexDirection: "column" }, children: [
198
+ /* @__PURE__ */ jsxs(
199
+ Box,
200
+ {
201
+ sx: {
202
+ position: "relative",
203
+ width: "100%",
204
+ height: 96,
205
+ bgcolor: "action.selected"
206
+ },
207
+ children: [
208
+ coverUrl && /* @__PURE__ */ jsx2(
209
+ Box,
210
+ {
211
+ component: "img",
212
+ src: coverUrl,
213
+ alt: "Profile background",
214
+ sx: {
215
+ width: "100%",
216
+ height: "100%",
217
+ objectFit: "cover",
218
+ display: "block",
219
+ m: 0,
220
+ p: 0
221
+ }
222
+ }
223
+ ),
224
+ /* @__PURE__ */ jsx2(
225
+ IconButton,
226
+ {
227
+ size: "small",
228
+ onClick: onClose,
229
+ sx: {
230
+ position: "absolute",
231
+ top: 8,
232
+ right: 8,
233
+ color: "common.white",
234
+ bgcolor: "rgba(0, 0, 0, 0.4)",
235
+ backdropFilter: "blur(4px)",
236
+ transition: "all 0.2s ease",
237
+ "&:hover": {
238
+ bgcolor: "rgba(0, 0, 0, 0.8)",
239
+ transform: "scale(1.1)"
240
+ }
241
+ },
242
+ children: /* @__PURE__ */ jsx2(CloseIcon, { fontSize: "small" })
243
+ }
244
+ )
245
+ ]
246
+ }
247
+ ),
248
+ /* @__PURE__ */ jsxs(Box, { sx: { p: 2, pt: 0 }, children: [
249
+ /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1.5, sx: { alignItems: "flex-end", mt: "-18px", mb: 1 }, children: [
250
+ /* @__PURE__ */ jsx2(
251
+ Badge,
252
+ {
253
+ overlap: "circular",
254
+ anchorOrigin: { vertical: "bottom", horizontal: "right" },
255
+ variant: "dot",
256
+ sx: {
257
+ "& .MuiBadge-badge": {
258
+ backgroundColor: isOnline ? "#44b700" : "#bdbdbd",
259
+ color: isOnline ? "#44b700" : "#bdbdbd",
260
+ boxShadow: `0 0 0 2px #fff`,
261
+ width: 12,
262
+ height: 12,
263
+ borderRadius: "50%",
264
+ "&::after": isOnline ? {
265
+ position: "absolute",
266
+ top: 0,
267
+ left: 0,
268
+ width: "100%",
269
+ height: "100%",
270
+ borderRadius: "50%",
271
+ animation: "ripple 1.2s infinite ease-in-out",
272
+ border: "1px solid currentColor",
273
+ content: '""'
274
+ } : {}
275
+ },
276
+ "@keyframes ripple": {
277
+ "0%": {
278
+ transform: "scale(.8)",
279
+ opacity: 1
280
+ },
281
+ "100%": {
282
+ transform: "scale(2.4)",
283
+ opacity: 0
284
+ }
285
+ }
286
+ },
287
+ children: /* @__PURE__ */ jsx2(
288
+ Avatar,
289
+ {
290
+ src: avatarUrl || coverUrl || void 0,
291
+ sx: {
292
+ width: 65,
293
+ height: 65,
294
+ border: "3px solid",
295
+ borderColor: "background.paper",
296
+ boxShadow: 2,
297
+ bgcolor: "primary.main",
298
+ fontSize: 22,
299
+ fontWeight: 600
300
+ },
301
+ children: profileData.display_name?.[0]?.toUpperCase() || "?"
302
+ }
303
+ )
304
+ }
305
+ ),
306
+ /* @__PURE__ */ jsxs(Box, { sx: { overflow: "hidden", pb: 0.5 }, children: [
307
+ /* @__PURE__ */ jsx2(Typography, { variant: "subtitle1", sx: { fontWeight: 600, lineHeight: 1.2 }, noWrap: true, children: profileData.display_name || `${profileData.first_name || ""} ${profileData.last_name || ""}` }),
308
+ membershipData?.employee_post && /* @__PURE__ */ jsx2(Typography, { variant: "body2", color: "text.secondary", noWrap: true, children: membershipData.employee_post })
309
+ ] })
310
+ ] }),
311
+ /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, sx: { my: 1, flexWrap: "wrap", gap: 0.5 }, children: [
312
+ getStatusChip(),
313
+ /* @__PURE__ */ jsx2(UserRoleChipComponent, { role: membershipData?.role || "user" })
314
+ ] }),
315
+ /* @__PURE__ */ jsx2(Divider, { sx: { my: 1.5 } }),
316
+ /* @__PURE__ */ jsxs(Stack, { spacing: 1, sx: { fontSize: "0.875rem", mb: 1.5 }, children: [
317
+ /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, sx: { alignItems: "center" }, color: "text.secondary", children: [
318
+ /* @__PURE__ */ jsx2(AccessTimeIcon, { fontSize: "small", color: getStatusIconColor(isOnline, lastSeenAt) }),
319
+ /* @__PURE__ */ jsx2(Typography, { variant: "body2", color: getStatusTypographyColor(isOnline, lastSeenAt), children: isOnline ? /* @__PURE__ */ jsx2(TranslateText, { overrideNamespace: overrideNamespace || "form_components", children: "Online" }) : lastSeenAt ? /* @__PURE__ */ jsxs(Fragment, { children: [
320
+ /* @__PURE__ */ jsx2(TranslateText, { overrideNamespace: overrideNamespace || "form_components", children: "Last seen" }),
321
+ ":",
322
+ " ",
323
+ /* @__PURE__ */ jsx2("strong", { children: formatRelativeTime(lastSeenAt) })
324
+ ] }) : /* @__PURE__ */ jsx2(TranslateText, { overrideNamespace: overrideNamespace || "form_components", children: "Offline" }) })
325
+ ] }),
326
+ profileData.email && /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, sx: { alignItems: "center" }, color: "text.secondary", children: [
327
+ /* @__PURE__ */ jsx2(EmailIcon, { fontSize: "small" }),
328
+ /* @__PURE__ */ jsx2(Typography, { variant: "body2", color: "text.primary", noWrap: true, sx: { flexGrow: 1 }, children: profileData.email }),
329
+ /* @__PURE__ */ jsx2(CopyClipboardButtonComponent, { textToCopy: profileData.email })
330
+ ] }),
331
+ membershipData?.employee_post && /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, sx: { alignItems: "center" }, color: "text.secondary", children: [
332
+ /* @__PURE__ */ jsx2(WorkIcon, { fontSize: "small" }),
333
+ /* @__PURE__ */ jsx2(Typography, { variant: "body2", color: "text.primary", noWrap: true, sx: { flexGrow: 1 }, children: membershipData.employee_post }),
334
+ /* @__PURE__ */ jsx2(CopyClipboardButtonComponent, { textToCopy: membershipData.employee_post })
335
+ ] }),
336
+ profileData.phone && /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, sx: { alignItems: "center" }, color: "text.secondary", children: [
337
+ /* @__PURE__ */ jsx2(PhoneIcon, { fontSize: "small" }),
338
+ /* @__PURE__ */ jsx2(Typography, { variant: "body2", color: "text.primary", children: profileData.phone })
339
+ ] }),
340
+ profileData.birthday && /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, sx: { alignItems: "center" }, color: "text.secondary", children: [
341
+ /* @__PURE__ */ jsx2(CakeIcon, { fontSize: "small" }),
342
+ /* @__PURE__ */ jsx2(Typography, { variant: "body2", color: "text.primary", children: formatDate(profileData.birthday) })
343
+ ] }),
344
+ membershipData?.created_at && /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, sx: { alignItems: "center" }, color: "text.secondary", children: [
345
+ /* @__PURE__ */ jsx2(CalendarIcon, { fontSize: "small" }),
346
+ /* @__PURE__ */ jsxs(Typography, { variant: "body2", color: "text.primary", children: [
347
+ /* @__PURE__ */ jsx2(TranslateText, { overrideNamespace: overrideNamespace || "form_components", children: "In company from" }),
348
+ ": ",
349
+ formatDate(membershipData.created_at)
350
+ ] })
351
+ ] }),
352
+ profileData.description && /* @__PURE__ */ jsx2(
353
+ Typography,
354
+ {
355
+ variant: "caption",
356
+ color: "text.secondary",
357
+ sx: {
358
+ display: "-webkit-box",
359
+ WebkitLineClamp: 2,
360
+ WebkitBoxOrient: "vertical",
361
+ overflow: "hidden",
362
+ mt: 0.5
363
+ },
364
+ children: profileData.description
365
+ }
366
+ )
367
+ ] }),
368
+ /* @__PURE__ */ jsx2(Divider, { sx: { my: 1 } }),
369
+ /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, sx: { justifyContent: "space-around", pt: 0.5 }, children: [
370
+ /* @__PURE__ */ jsx2(TranslatableTooltipComponent, { title: /* @__PURE__ */ jsx2(TranslateText, { overrideNamespace: overrideNamespace || "form_components", children: "Send a notification" }), children: /* @__PURE__ */ jsx2(IconButton, { size: "small", color: "primary", children: /* @__PURE__ */ jsx2(NotificationIcon, { fontSize: "small" }) }) }),
371
+ /* @__PURE__ */ jsx2(TranslatableTooltipComponent, { title: /* @__PURE__ */ jsx2(TranslateText, { overrideNamespace: overrideNamespace || "form_components", children: "Set a task" }), children: /* @__PURE__ */ jsx2(IconButton, { size: "small", color: "primary", children: /* @__PURE__ */ jsx2(TaskIcon, { fontSize: "small" }) }) }),
372
+ /* @__PURE__ */ jsx2(TranslatableTooltipComponent, { title: /* @__PURE__ */ jsx2(TranslateText, { overrideNamespace: overrideNamespace || "form_components", children: "Start a chat" }), children: /* @__PURE__ */ jsx2(
373
+ IconButton,
374
+ {
375
+ size: "small",
376
+ color: "primary",
377
+ disabled: !canChat,
378
+ onClick: () => membershipData && openChatWithUser(membershipData.id),
379
+ children: /* @__PURE__ */ jsx2(ChatIcon, { fontSize: "small" })
380
+ }
381
+ ) })
382
+ ] })
383
+ ] })
384
+ ] }) : /* @__PURE__ */ jsx2(Box, { sx: { p: 2 }, children: /* @__PURE__ */ jsx2(
385
+ Typography,
386
+ {
387
+ color: "error",
388
+ variant: "body2",
389
+ children: /* @__PURE__ */ jsx2(TranslateText, { overrideNamespace: overrideNamespace || "form_components", children: "Failed to load user data" })
390
+ }
391
+ ) })
392
+ }
393
+ );
394
+ };
395
+ var UserPopoverComponent_default = UserPopoverComponent;
396
+ export {
397
+ UserPopoverComponent,
398
+ UserPopoverComponent_default as default
399
+ };