feat: implement comprehensive notification engine with policies, preferences, and automated dispatching services

This commit is contained in:
2026-07-21 18:53:11 -03:00
parent 40090cdec5
commit 10ca449f88
82 changed files with 10249 additions and 171 deletions
@@ -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" }}>&nbsp;</div>
</>
);
}