feat: implement comprehensive notification engine with policies, preferences, and automated dispatching services
This commit is contained in:
+101
@@ -0,0 +1,101 @@
|
||||
.container {
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: min(100%, 900px);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.channels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1fr) minmax(360px, 1fr);
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.channelTitle {
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.channelDescription {
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.optionButton {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.optionButtonActive {
|
||||
border-color: var(--primary-color);
|
||||
background-color: rgba(25, 118, 210, 0.12);
|
||||
color: var(--primary-color);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
align-self: flex-end;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.card {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.options {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.saveButton {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Saveable from "@components/Saveable/Saveable";
|
||||
|
||||
export default function NotificationsPreferencesLayout({ children }: { children: React.ReactNode }) {
|
||||
return <Saveable>{children}</Saveable>;
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import Location from "@components/Location/Location";
|
||||
import { useLocationStore } from "@store/Location.Store";
|
||||
import { useNotificationPreferencesStore } from "@store/NotificationPreferences.Store";
|
||||
import { useSessionStore } from "@store/Sesion.Store";
|
||||
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
|
||||
import { useAlert } from "@store/Alert.Store";
|
||||
import { getClientPreferences, upsertClientPreferences } from "@services/Notifications.Service";
|
||||
import { NotificationChannel } from "@models/NotificationPreferences.model";
|
||||
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
|
||||
import NotificationsIcon from "@mui/icons-material/NotificationsOutlined";
|
||||
import MaterialAlert from "@mui/material/Alert";
|
||||
import styles from "./NotificationsPreferencesPage.module.css";
|
||||
|
||||
const CHANNELS: { key: NotificationChannel; label: string }[] = [
|
||||
{ key: "whatsapp", label: "WhatsApp" },
|
||||
{ key: "email", label: "Email" },
|
||||
{ key: "system", label: "Sistema" }
|
||||
];
|
||||
|
||||
type ChannelPreference = "default" | "preferred" | "muted";
|
||||
|
||||
export default function NotificationsPreferencesPage() {
|
||||
const location = useLocationStore();
|
||||
const store = useNotificationPreferencesStore();
|
||||
const SessionInfo = useSessionStore();
|
||||
const eventHandler = useEventHandlerStore();
|
||||
const alert = useAlert();
|
||||
|
||||
useEffect(() => {
|
||||
location.clear();
|
||||
|
||||
location.add({
|
||||
url: "/user/profile",
|
||||
title: "Perfil de usuario",
|
||||
icon: <AccountCircleOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />
|
||||
});
|
||||
|
||||
location.add({
|
||||
url: "",
|
||||
title: "Preferencias de notificación",
|
||||
icon: <NotificationsIcon sx={{ fontSize: "32px", mr: "10px" }} />
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!SessionInfo.userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.setClientId(SessionInfo.userId);
|
||||
store.setMode("client-preferences");
|
||||
|
||||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||
getClientPreferences(SessionInfo.userId, SessionInfo.userId)
|
||||
.then((preferences) => {
|
||||
store.setClientPreferences(preferences);
|
||||
store.updateFormData({
|
||||
preferredChannels: preferences.preferredChannels || [],
|
||||
mutedChannels: preferences.mutedChannels || []
|
||||
});
|
||||
store.setDirty(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
alert.showError(error.format());
|
||||
})
|
||||
.finally(() => {
|
||||
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||
});
|
||||
}, [SessionInfo.userId]);
|
||||
|
||||
const getChannelPreference = (channel: NotificationChannel): ChannelPreference => {
|
||||
if (store.formData.mutedChannels.includes(channel)) {
|
||||
return "muted";
|
||||
}
|
||||
|
||||
if (store.formData.preferredChannels.includes(channel)) {
|
||||
return "preferred";
|
||||
}
|
||||
|
||||
return "default";
|
||||
};
|
||||
|
||||
const selectPreference = (channel: NotificationChannel, preference: ChannelPreference) => {
|
||||
store.updateFormData({
|
||||
preferredChannels:
|
||||
preference === "preferred"
|
||||
? [...store.formData.preferredChannels.filter((c) => c !== channel), channel]
|
||||
: store.formData.preferredChannels.filter((c) => c !== channel),
|
||||
mutedChannels:
|
||||
preference === "muted"
|
||||
? [...store.formData.mutedChannels.filter((c) => c !== channel), channel]
|
||||
: store.formData.mutedChannels.filter((c) => c !== channel)
|
||||
});
|
||||
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||
upsertClientPreferences(SessionInfo.userId, SessionInfo.userId, {
|
||||
preferredChannels: store.formData.preferredChannels as NotificationChannel[],
|
||||
mutedChannels: store.formData.mutedChannels as NotificationChannel[]
|
||||
})
|
||||
.then((preferences) => {
|
||||
store.setClientPreferences(preferences);
|
||||
store.setDirty(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
alert.showError(error.format());
|
||||
})
|
||||
.finally(() => {
|
||||
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
|
||||
handleSave();
|
||||
}
|
||||
}, [eventHandler.eventType]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Location />
|
||||
|
||||
<div className={styles.container}>
|
||||
<MaterialAlert severity="info" sx={{ marginBottom: "0" }}>
|
||||
<b>Preferencias globales:</b> Configure cómo prefiere recibir notificaciones. Si deja un canal en estándar, cada organización podrá usar su configuración general.
|
||||
</MaterialAlert>
|
||||
|
||||
<div className={styles.channels}>
|
||||
{CHANNELS.map((channel) => {
|
||||
const selectedPreference = getChannelPreference(channel.key);
|
||||
return (
|
||||
<div key={channel.key} className={styles.card}>
|
||||
<div>
|
||||
<div className={styles.channelTitle}>{channel.label}</div>
|
||||
<div className={styles.channelDescription}>
|
||||
{selectedPreference === "preferred" && "Priorizar este canal para tus notificaciones."}
|
||||
{selectedPreference === "muted" && "No recibir notificaciones por este canal."}
|
||||
{selectedPreference === "default" && "Usar la configuración estándar de cada organización."}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.options}>
|
||||
{[
|
||||
{ key: "default" as ChannelPreference, label: "Estándar" },
|
||||
{ key: "preferred" as ChannelPreference, label: "Preferido" },
|
||||
{ key: "muted" as ChannelPreference, label: "Silenciado" }
|
||||
].map((option) => {
|
||||
const isSelected = selectedPreference === option.key;
|
||||
return (
|
||||
<button
|
||||
key={option.key}
|
||||
type="button"
|
||||
onClick={() => selectPreference(channel.key, option.key)}
|
||||
className={`${styles.optionButton} ${isSelected ? styles.optionButtonActive : ""}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{store.isDirty && (
|
||||
<button onClick={handleSave} className={styles.saveButton}>
|
||||
Guardar preferencias
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: "40px" }}> </div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import Location from "@components/Location/Location";
|
||||
import { useLocationStore } from "@store/Location.Store";
|
||||
import { useEffect } from "react";
|
||||
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
|
||||
import NotificationsIcon from "@mui/icons-material/Notifications";
|
||||
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
|
||||
|
||||
export default function Profile() {
|
||||
@@ -52,6 +53,11 @@ export default function Profile() {
|
||||
icon="/notifications-icon.svg"
|
||||
onClick={() => goTo("/user/profile/notifications")}
|
||||
/>
|
||||
<ButtonOption
|
||||
text="Preferencias de notificación"
|
||||
icon="/notifications-icon.svg"
|
||||
onClick={() => goTo("/user/profile/notifications-preferences")}
|
||||
/>
|
||||
<ButtonOption
|
||||
text="Avanzado"
|
||||
icon="/services-icon.svg"
|
||||
|
||||
Reference in New Issue
Block a user