feat: implement notification dropdown in header with centralized navigation logic
This commit is contained in:
@@ -40,3 +40,10 @@
|
||||
.userProfileOptionsContainer a {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.userProfileOptionsContainer {
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { ISystemNotification, NotificationType } from "@core/Models/SystemNotifications.model";
|
||||
import API from "@services/Api.Service";
|
||||
|
||||
type ResolveNotificationRouteParams = {
|
||||
notification: ISystemNotification;
|
||||
sessionUser: string;
|
||||
organizationSubscriptions?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const NOTIFICATIONS_ROUTE = "/user/profile/notifications";
|
||||
|
||||
export const resolveNotificationRoute = async ({
|
||||
notification,
|
||||
sessionUser,
|
||||
organizationSubscriptions,
|
||||
}: ResolveNotificationRouteParams): Promise<string> => {
|
||||
if (!notification.type || !notification.code) {
|
||||
return NOTIFICATIONS_ROUTE;
|
||||
}
|
||||
|
||||
let companyId = notification.companyId;
|
||||
|
||||
switch (notification.type) {
|
||||
case NotificationType.APPOINTMENT: {
|
||||
const aptRes = await API.post<any>("appointments/get-event", {
|
||||
appointmentId: notification.code,
|
||||
sessionUser,
|
||||
});
|
||||
|
||||
companyId = companyId || aptRes?.companyId;
|
||||
if (aptRes?.clientId && companyId) {
|
||||
return `/admin/org/${companyId}/client/${aptRes.clientId}/appointments/${notification.code}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NotificationType.COMPANY:
|
||||
return `/admin/org/profile/${notification.code}`;
|
||||
case NotificationType.EMPLOYEE:
|
||||
case NotificationType.SCHEDULE:
|
||||
case NotificationType.SCHEDULE_EXCEPTION:
|
||||
case NotificationType.SCHEDULE_RESTRICTION:
|
||||
if (!companyId) {
|
||||
const empRes = await API.post<any>("employees/get-by-id", {
|
||||
id: notification.code,
|
||||
sessionUser,
|
||||
});
|
||||
companyId = empRes?.companyId;
|
||||
}
|
||||
|
||||
if (companyId) {
|
||||
if (
|
||||
notification.type === NotificationType.EMPLOYEE ||
|
||||
notification.type === NotificationType.SCHEDULE
|
||||
) {
|
||||
return `/admin/org/profile/${companyId}/collaborators/${notification.code}`;
|
||||
}
|
||||
|
||||
if (notification.type === NotificationType.SCHEDULE_EXCEPTION) {
|
||||
return `/admin/org/profile/${companyId}/collaborators/${notification.code}/exceptions`;
|
||||
}
|
||||
|
||||
return `/admin/org/profile/${companyId}/collaborators/${notification.code}/restrictions`;
|
||||
}
|
||||
break;
|
||||
case NotificationType.BILLING:
|
||||
companyId = companyId || Object.keys(organizationSubscriptions || {})[0];
|
||||
if (companyId) {
|
||||
return `/admin/org/profile/${companyId}/dashboard`;
|
||||
}
|
||||
break;
|
||||
case NotificationType.MESSAGE:
|
||||
if (notification.conversationId) {
|
||||
return `/messages?conversation=${notification.conversationId}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return NOTIFICATIONS_ROUTE;
|
||||
};
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
UpdateSystemNotificationParams,
|
||||
NotificationType,
|
||||
} from "@core/Models/SystemNotifications.model";
|
||||
import API from "@services/Api.Service";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
deleteNotificationsByDate,
|
||||
@@ -23,18 +22,19 @@ import {
|
||||
setStateNotificationByUser,
|
||||
updateSystemNotification,
|
||||
} from "./Notifications.Service";
|
||||
import { resolveNotificationRoute } from "./Notifications.Navigation";
|
||||
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
|
||||
import MarkReadIcon from "@mui/icons-material/MarkChatReadOutlined";
|
||||
import MarkUnReadIcon from "@mui/icons-material/MarkUnreadChatAltOutlined";
|
||||
import ReloadIcon from "@mui/icons-material/Autorenew";
|
||||
import DoneAllIcon from "@mui/icons-material/DoneAll";
|
||||
import NotificationsActiveIcon from "@mui/icons-material/NotificationsActive";
|
||||
import InboxIcon from "@mui/icons-material/Inbox";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Pagination from "@mui/material/Pagination";
|
||||
import { useConfirmStore } from "@core/Store/Confirm.Store";
|
||||
import HeaderConfProvider from "@core/app/components/HeaderConfProvider/HeaderConfProvider";
|
||||
import { useMenuStore } from "@core/Store/Menu.Store";
|
||||
import useWindowSize from "@hooks/WindowSize";
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/es";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
@@ -49,6 +49,7 @@ export default function EditProfile() {
|
||||
const location = useLocationStore();
|
||||
const menu = useMenuStore();
|
||||
const router = useRouter();
|
||||
const [windowWidth] = useWindowSize();
|
||||
const [filterType, setFilterType] = useState<string>("ALL");
|
||||
const [pageSize] = useState<number>(
|
||||
parseInt(
|
||||
@@ -129,66 +130,22 @@ export default function EditProfile() {
|
||||
};
|
||||
|
||||
const handleNotificationClick = async (notif: SystemNotificationRow) => {
|
||||
if (!notif.readed) {
|
||||
handleMarkRead(notif);
|
||||
}
|
||||
|
||||
if (!notif.type || !notif.code) return;
|
||||
|
||||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||
try {
|
||||
let companyId = notif.companyId;
|
||||
|
||||
switch (notif.type) {
|
||||
case NotificationType.APPOINTMENT:
|
||||
// Fetch appointment to get clientId and companyId
|
||||
const aptRes = await API.post<any>("appointments/get-event", {
|
||||
appointmentId: notif.code,
|
||||
sessionUser: SessionInfo.userId,
|
||||
});
|
||||
if (aptRes && aptRes.clientId) {
|
||||
companyId = companyId || aptRes.companyId;
|
||||
if (companyId) {
|
||||
router.push(`/admin/org/${companyId}/client/${aptRes.clientId}/appointments/${notif.code}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NotificationType.COMPANY:
|
||||
router.push(`/admin/org/profile/${notif.code}`);
|
||||
break;
|
||||
case NotificationType.EMPLOYEE:
|
||||
case NotificationType.SCHEDULE:
|
||||
case NotificationType.SCHEDULE_EXCEPTION:
|
||||
case NotificationType.SCHEDULE_RESTRICTION:
|
||||
if (!companyId) {
|
||||
const empRes = await API.post<any>("employees/get-by-id", {
|
||||
id: notif.code,
|
||||
sessionUser: SessionInfo.userId,
|
||||
});
|
||||
companyId = companyId || empRes?.companyId;
|
||||
}
|
||||
if (companyId) {
|
||||
if (notif.type === NotificationType.EMPLOYEE || notif.type === NotificationType.SCHEDULE) {
|
||||
router.push(`/admin/org/profile/${companyId}/collaborators/${notif.code}`);
|
||||
} else if (notif.type === NotificationType.SCHEDULE_EXCEPTION) {
|
||||
router.push(`/admin/org/profile/${companyId}/collaborators/${notif.code}/exceptions`);
|
||||
} else {
|
||||
router.push(`/admin/org/profile/${companyId}/collaborators/${notif.code}/restrictions`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NotificationType.BILLING:
|
||||
companyId = companyId || Object.keys(SessionInfo.organizationSubscriptions || {})[0];
|
||||
if (companyId) {
|
||||
router.push(`/admin/org/profile/${companyId}/dashboard`);
|
||||
}
|
||||
break;
|
||||
case NotificationType.MESSAGE:
|
||||
if (notif.conversationId) {
|
||||
router.push(`/messages?conversation=${notif.conversationId}`);
|
||||
}
|
||||
break;
|
||||
if (!notif.readed) {
|
||||
await updateSystemNotification({
|
||||
id: notif._id,
|
||||
readed: true,
|
||||
sessionUser: SessionInfo.userId,
|
||||
});
|
||||
}
|
||||
|
||||
const route = await resolveNotificationRoute({
|
||||
notification: notif,
|
||||
sessionUser: SessionInfo.userId,
|
||||
organizationSubscriptions: SessionInfo.organizationSubscriptions,
|
||||
});
|
||||
router.push(route);
|
||||
} catch (e: any) {
|
||||
console.error("Error navigating from notification", e);
|
||||
alert.showError(e.format ? e.format() : "Error al navegar: " + (e.message || "Desconocido"));
|
||||
@@ -332,7 +289,7 @@ export default function EditProfile() {
|
||||
});
|
||||
|
||||
location.add({
|
||||
url: "/user/profile/edit",
|
||||
url: "/user/profile/notifications",
|
||||
title: "Notificaciones",
|
||||
icon: <AccountCircleOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
|
||||
});
|
||||
@@ -395,27 +352,55 @@ export default function EditProfile() {
|
||||
|
||||
const unreadCount = allDataList.filter(n => !n.readed).length;
|
||||
const isInboxZero = allDataList.length > 0 && unreadCount === 0;
|
||||
const isMobile = windowWidth < 640;
|
||||
|
||||
return (
|
||||
<>
|
||||
<style jsx global>{`
|
||||
.notifications-filter-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
<Location />
|
||||
<HeaderConfProvider />
|
||||
|
||||
<div style={{ padding: "20px 40px", maxWidth: "900px", margin: "0 auto", paddingBottom: "100px" }}>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
boxSizing: "border-box",
|
||||
padding: isMobile ? "18px 10px 90px" : "20px 40px 100px",
|
||||
maxWidth: "900px",
|
||||
margin: "0 auto",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
|
||||
{/* Header and Controls */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "30px", borderBottom: "1px solid #eaeaea", paddingBottom: "20px" }}>
|
||||
<div>
|
||||
<h1 style={{ margin: 0, fontSize: "28px", color: "var(--black-light)", display: "flex", alignItems: "center", gap: "10px" }}>
|
||||
<NotificationsActiveIcon sx={{ color: "var(--wine-dark)" }} />
|
||||
Bandeja de Entrada
|
||||
</h1>
|
||||
<p style={{ margin: "5px 0 0 0", color: "#666", fontSize: "14px" }}>
|
||||
{unreadCount > 0 ? `Tienes ${unreadCount} notificación(es) sin leer.` : "No tienes notificaciones pendientes."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: "10px" }}>
|
||||
{/* Controls */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
gap: "10px",
|
||||
marginBottom: isMobile ? 0 : "24px",
|
||||
borderBottom: isMobile ? "none" : "1px solid #eaeaea",
|
||||
paddingBottom: isMobile ? 0 : "20px",
|
||||
position: isMobile ? "absolute" : "static",
|
||||
top: isMobile ? "-44px" : "auto",
|
||||
right: isMobile ? "10px" : "auto",
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
width: "auto",
|
||||
justifyContent: "flex-end",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{unreadCount > 0 && (
|
||||
<button
|
||||
onClick={() => setNotiState(true)}
|
||||
@@ -423,7 +408,7 @@ export default function EditProfile() {
|
||||
backgroundColor: "var(--wine-lighterX2)",
|
||||
color: "var(--wine-darkest)",
|
||||
border: "none",
|
||||
padding: "8px 16px",
|
||||
padding: isMobile ? "8px 14px" : "8px 16px",
|
||||
borderRadius: "20px",
|
||||
cursor: "pointer",
|
||||
fontWeight: "bold",
|
||||
@@ -441,7 +426,7 @@ export default function EditProfile() {
|
||||
<IconButton
|
||||
onClick={() => loadNotifications(notifications?.page, pageSize)}
|
||||
color="secondary"
|
||||
style={{ backgroundColor: "#f5f5f5" }}
|
||||
style={{ backgroundColor: "#f5f5f5", boxShadow: isMobile ? "0 8px 20px rgba(0, 0, 0, 0.08)" : "none" }}
|
||||
>
|
||||
<ReloadIcon sx={{ color: "var(--black)" }} />
|
||||
</IconButton>
|
||||
@@ -450,7 +435,20 @@ export default function EditProfile() {
|
||||
|
||||
{/* Filters */}
|
||||
{allDataList.length > 0 && (
|
||||
<div style={{ display: "flex", gap: "10px", marginBottom: "25px", flexWrap: "wrap" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: isMobile ? "8px" : "10px",
|
||||
marginBottom: isMobile ? "18px" : "25px",
|
||||
flexWrap: isMobile ? "nowrap" : "wrap",
|
||||
overflowX: isMobile ? "auto" : "visible",
|
||||
paddingBottom: isMobile ? "8px" : 0,
|
||||
WebkitOverflowScrolling: "touch",
|
||||
scrollbarWidth: "none",
|
||||
msOverflowStyle: "none",
|
||||
}}
|
||||
className="notifications-filter-scroll"
|
||||
>
|
||||
{[
|
||||
{ id: "ALL", label: "Todas" },
|
||||
{ id: "APPOINTMENT", label: "Turnos" },
|
||||
@@ -463,7 +461,7 @@ export default function EditProfile() {
|
||||
key={f.id}
|
||||
onClick={() => setFilterType(f.id)}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
padding: isMobile ? "9px 14px" : "8px 16px",
|
||||
borderRadius: "20px",
|
||||
border: "1px solid",
|
||||
borderColor: filterType === f.id ? "var(--wine-dark)" : "#eaeaea",
|
||||
@@ -471,7 +469,8 @@ export default function EditProfile() {
|
||||
color: filterType === f.id ? "#fff" : "#666",
|
||||
cursor: "pointer",
|
||||
fontWeight: filterType === f.id ? "bold" : "normal",
|
||||
transition: "all 0.2s"
|
||||
transition: "all 0.2s",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{f.label}
|
||||
@@ -485,7 +484,7 @@ export default function EditProfile() {
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
style={{ textAlign: "center", padding: "40px 20px", backgroundColor: "#f9fafc", borderRadius: "15px", marginBottom: "30px" }}
|
||||
style={{ textAlign: "center", padding: isMobile ? "34px 18px" : "40px 20px", backgroundColor: "#f9fafc", borderRadius: "15px", marginBottom: "30px" }}
|
||||
>
|
||||
<InboxIcon sx={{ fontSize: "64px", color: "var(--wine-light)", marginBottom: "15px" }} />
|
||||
<h2 style={{ margin: 0, color: "var(--wine-darkest)" }}>¡Estás al día! 🌟</h2>
|
||||
@@ -514,11 +513,13 @@ export default function EditProfile() {
|
||||
style={{
|
||||
backgroundColor: notif.readed ? "#ffffff" : "var(--wine-lighterX2)",
|
||||
border: notif.readed ? "1px solid #eaeaea" : "1px solid var(--wine-light)",
|
||||
borderRadius: "12px",
|
||||
padding: "16px 20px",
|
||||
borderRadius: isMobile ? "18px" : "12px",
|
||||
padding: isMobile ? "14px" : "16px 20px",
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
alignItems: isMobile ? "stretch" : "flex-start",
|
||||
justifyContent: "space-between",
|
||||
flexDirection: isMobile ? "column" : "row",
|
||||
gap: isMobile ? "12px" : "8px",
|
||||
boxShadow: notif.readed ? "none" : "0 4px 12px rgba(127, 42, 255, 0.1)",
|
||||
transition: "all 0.2s ease",
|
||||
cursor: notif.type && notif.code ? "pointer" : "default"
|
||||
@@ -527,25 +528,35 @@ export default function EditProfile() {
|
||||
onMouseOver={(e) => { e.currentTarget.style.transform = "translateY(-2px)"; e.currentTarget.style.boxShadow = "0 6px 16px rgba(0,0,0,0.08)"; }}
|
||||
onMouseOut={(e) => { e.currentTarget.style.transform = "none"; e.currentTarget.style.boxShadow = notif.readed ? "none" : "0 4px 12px rgba(127, 42, 255, 0.1)"; }}
|
||||
>
|
||||
<div style={{ display: "flex", gap: "15px", alignItems: "flex-start", flex: 1 }}>
|
||||
<div style={{ marginTop: "4px" }}>
|
||||
<div style={{ display: "flex", gap: isMobile ? "12px" : "15px", alignItems: "flex-start", flex: 1, minWidth: 0 }}>
|
||||
<div style={{ marginTop: "4px", flexShrink: 0 }}>
|
||||
{notif.readed ?
|
||||
<MarkReadIcon sx={{ color: "#ccc" }} /> :
|
||||
<MarkUnReadIcon sx={{ color: "var(--wine-dark)" }} />
|
||||
}
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "4px" }}>
|
||||
<h3 style={{ margin: 0, fontSize: "16px", fontWeight: notif.readed ? "normal" : "bold", color: notif.readed ? "#444" : "var(--wine-darkest)" }}>
|
||||
<h3 style={{ margin: 0, fontSize: isMobile ? "15px" : "16px", lineHeight: "1.35", fontWeight: notif.readed ? "normal" : "bold", color: notif.readed ? "#444" : "var(--wine-darkest)", overflowWrap: "anywhere" }}>
|
||||
{notif.subject}
|
||||
</h3>
|
||||
</div>
|
||||
<p style={{ margin: 0, fontSize: "14px", color: notif.readed ? "#777" : "#333", lineHeight: "1.5" }}>
|
||||
<p style={{ margin: 0, fontSize: isMobile ? "13px" : "14px", color: notif.readed ? "#777" : "#333", lineHeight: "1.5", overflowWrap: "anywhere" }}>
|
||||
{notif.message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "5px", opacity: 0.8 }} onClick={(e) => e.stopPropagation()}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "5px",
|
||||
opacity: 0.8,
|
||||
justifyContent: isMobile ? "flex-end" : "flex-start",
|
||||
borderTop: isMobile ? "1px solid #f0f0f0" : "none",
|
||||
paddingTop: isMobile ? "8px" : 0,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<IconButton size="small" onClick={() => handleMarkRead(notif)} title={notif.readed ? "Marcar como no leída" : "Marcar como leída"}>
|
||||
{notif.readed ? <MarkUnReadIcon fontSize="small" /> : <MarkReadIcon fontSize="small" color="primary" />}
|
||||
</IconButton>
|
||||
|
||||
Reference in New Issue
Block a user