Add alerts ssytems to the frontend

This commit is contained in:
Urtzi Alfaro
2025-09-21 17:35:36 +02:00
parent 57fd2f22f0
commit f08667150d
17 changed files with 2086 additions and 786 deletions

View File

@@ -3,11 +3,13 @@ import { clsx } from 'clsx';
import { useNavigate } from 'react-router-dom';
import { useAuthUser, useIsAuthenticated, useAuthActions } from '../../../stores';
import { useTheme } from '../../../contexts/ThemeContext';
import { useNotifications } from '../../../hooks/useNotifications';
import { Button } from '../../ui';
import { Avatar } from '../../ui';
import { Badge } from '../../ui';
import { Modal } from '../../ui';
import { TenantSwitcher } from '../../ui/TenantSwitcher';
import { NotificationPanel } from '../../ui/NotificationPanel/NotificationPanel';
import {
Menu,
Search,
@@ -101,11 +103,21 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
const isAuthenticated = useIsAuthenticated();
const { logout } = useAuthActions();
const { theme, resolvedTheme, setTheme } = useTheme();
const {
notifications,
unreadCount,
isConnected,
markAsRead,
markAllAsRead,
removeNotification,
clearAll
} = useNotifications();
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const [isSearchFocused, setIsSearchFocused] = useState(false);
const [searchValue, setSearchValue] = useState('');
const [isThemeMenuOpen, setIsThemeMenuOpen] = useState(false);
const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false);
const searchInputRef = React.useRef<HTMLInputElement>(null);
@@ -168,6 +180,7 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
if (e.key === 'Escape') {
setIsUserMenuOpen(false);
setIsThemeMenuOpen(false);
setIsNotificationPanelOpen(false);
if (isSearchFocused) {
searchInputRef.current?.blur();
}
@@ -188,6 +201,9 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
if (!target.closest('[data-theme-menu]')) {
setIsThemeMenuOpen(false);
}
if (!target.closest('[data-notification-panel]')) {
setIsNotificationPanelOpen(false);
}
};
document.addEventListener('click', handleClickOutside);
@@ -379,25 +395,45 @@ export const Header = forwardRef<HeaderRef, HeaderProps>(({
{/* Notifications */}
{showNotifications && (
<div className="relative">
<div className="relative" data-notification-panel>
<Button
variant="ghost"
size="sm"
onClick={onNotificationClick}
className="w-10 h-10 p-0 flex items-center justify-center relative"
aria-label={`Notificaciones${notificationCount > 0 ? ` (${notificationCount})` : ''}`}
onClick={() => setIsNotificationPanelOpen(!isNotificationPanelOpen)}
className={clsx(
"w-10 h-10 p-0 flex items-center justify-center relative",
!isConnected && "opacity-50",
isNotificationPanelOpen && "bg-[var(--bg-secondary)]"
)}
aria-label={`Notificaciones${unreadCount > 0 ? ` (${unreadCount})` : ''}${!isConnected ? ' - Desconectado' : ''}`}
title={!isConnected ? 'Sin conexión en tiempo real' : undefined}
aria-expanded={isNotificationPanelOpen}
aria-haspopup="true"
>
<Bell className="h-5 w-5" />
{notificationCount > 0 && (
<Bell className={clsx(
"h-5 w-5 transition-colors",
unreadCount > 0 && "text-[var(--color-warning)]"
)} />
{unreadCount > 0 && (
<Badge
variant="error"
size="sm"
className="absolute -top-1 -right-1 min-w-[18px] h-[18px] text-xs flex items-center justify-center"
>
{notificationCount > 99 ? '99+' : notificationCount}
{unreadCount > 99 ? '99+' : unreadCount}
</Badge>
)}
</Button>
<NotificationPanel
notifications={notifications}
isOpen={isNotificationPanelOpen}
onClose={() => setIsNotificationPanelOpen(false)}
onMarkAsRead={markAsRead}
onMarkAllAsRead={markAllAsRead}
onRemoveNotification={removeNotification}
onClearAll={clearAll}
/>
</div>
)}