Správa používateľov + notifikačný systém

- Pridaná kompletná správa používateľov (CRUD, reset hesla, zmena roly) pre ROOT/ADMIN
- Backend: POST /users endpoint, createUser controller, validácia
- Frontend: UserManagement, UserForm, PasswordResetModal komponenty
- Settings prístupné pre ROOT aj ADMIN (AdminRoute)
- Notifikačný systém s snooze funkcionalitou
- Aktualizácia HELPDESK_INIT_V2.md dokumentácie

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 15:30:27 +01:00
parent cbdd952bc1
commit 2ca0c4f4d8
36 changed files with 3116 additions and 522 deletions

View File

@@ -0,0 +1,114 @@
import { create } from 'zustand';
import { notificationApi, type Notification } from '@/services/notification.api';
interface NotificationState {
notifications: Notification[];
unreadCount: number;
isLoading: boolean;
isOpen: boolean;
// Actions
fetchNotifications: () => Promise<void>;
fetchUnreadCount: () => Promise<void>;
markAsRead: (id: string) => Promise<void>;
markAllAsRead: () => Promise<void>;
snooze: (id: string, minutes: number) => Promise<void>;
deleteNotification: (id: string) => Promise<void>;
setIsOpen: (isOpen: boolean) => void;
addNotification: (notification: Notification) => void;
}
export const useNotificationStore = create<NotificationState>((set, get) => ({
notifications: [],
unreadCount: 0,
isLoading: false,
isOpen: false,
fetchNotifications: async () => {
set({ isLoading: true });
try {
const response = await notificationApi.getAll({ limit: 50 });
set({ notifications: response.data.notifications });
} catch (error) {
console.error('Error fetching notifications:', error);
} finally {
set({ isLoading: false });
}
},
fetchUnreadCount: async () => {
try {
const response = await notificationApi.getUnreadCount();
set({ unreadCount: response.data.count });
} catch (error) {
console.error('Error fetching unread count:', error);
}
},
markAsRead: async (id: string) => {
try {
await notificationApi.markAsRead(id);
set((state) => ({
notifications: state.notifications.map((n) =>
n.id === id ? { ...n, isRead: true, readAt: new Date().toISOString() } : n
),
unreadCount: Math.max(0, state.unreadCount - 1),
}));
} catch (error) {
console.error('Error marking notification as read:', error);
}
},
markAllAsRead: async () => {
try {
await notificationApi.markAllAsRead();
set((state) => ({
notifications: state.notifications.map((n) => ({
...n,
isRead: true,
readAt: new Date().toISOString(),
})),
unreadCount: 0,
}));
} catch (error) {
console.error('Error marking all as read:', error);
}
},
snooze: async (id: string, minutes: number) => {
try {
const response = await notificationApi.snooze(id, minutes);
set((state) => ({
notifications: state.notifications.filter((n) => n.id !== id),
unreadCount: Math.max(0, state.unreadCount - 1),
}));
return response;
} catch (error) {
console.error('Error snoozing notification:', error);
}
},
deleteNotification: async (id: string) => {
const notification = get().notifications.find((n) => n.id === id);
try {
await notificationApi.delete(id);
set((state) => ({
notifications: state.notifications.filter((n) => n.id !== id),
unreadCount: notification && !notification.isRead
? Math.max(0, state.unreadCount - 1)
: state.unreadCount,
}));
} catch (error) {
console.error('Error deleting notification:', error);
}
},
setIsOpen: (isOpen: boolean) => set({ isOpen }),
addNotification: (notification: Notification) => {
set((state) => ({
notifications: [notification, ...state.notifications],
unreadCount: notification.isRead ? state.unreadCount : state.unreadCount + 1,
}));
},
}));

View File

@@ -0,0 +1,15 @@
import { create } from 'zustand';
interface SidebarState {
isOpen: boolean;
toggle: () => void;
open: () => void;
close: () => void;
}
export const useSidebarStore = create<SidebarState>((set) => ({
isOpen: false,
toggle: () => set((state) => ({ isOpen: !state.isOpen })),
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
}));