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

@@ -1,9 +1,11 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { X, Send, Pencil, Calendar, User as UserIcon, Users, FolderOpen, CheckCircle2, ArrowLeft } from 'lucide-react';
import { X, Send, Pencil, Calendar, User as UserIcon, Users, FolderOpen, CheckCircle2, ArrowLeft, Check, Clock } from 'lucide-react';
import { tasksApi } from '@/services/tasks.api';
import { settingsApi } from '@/services/settings.api';
import { useAuthStore } from '@/store/authStore';
import { useNotificationStore } from '@/store/notificationStore';
import { useSnoozeOptions, calculateSnoozeMinutes } from '@/hooks/useSnoozeOptions';
import type { Task } from '@/types';
import { Button, Badge, Textarea, Select } from '@/components/ui';
import { TaskForm } from './TaskForm';
@@ -22,13 +24,24 @@ interface TaskDetailProps {
taskId: string;
onClose: () => void;
onEdit?: (task: Task) => void; // Optional - ak nie je, použije sa interný edit mód
notificationId?: string; // Ak je detail otvorený z notifikácie
}
export function TaskDetail({ taskId, onClose }: TaskDetailProps) {
export function TaskDetail({ taskId, onClose, notificationId }: TaskDetailProps) {
const queryClient = useQueryClient();
const { user } = useAuthStore();
const [newComment, setNewComment] = useState('');
const [isEditing, setIsEditing] = useState(false);
const [snoozeOpen, setSnoozeOpen] = useState(false);
// Notifikácie - len ak bol detail otvorený z notifikácie
const { notifications, markAsRead, snooze } = useNotificationStore();
const snoozeOptions = useSnoozeOptions();
// Konkrétna notifikácia ak existuje
const notification = notificationId
? notifications.find((n) => n.id === notificationId && !n.isRead)
: null;
const { data: taskData, isLoading } = useQuery({
queryKey: ['task', taskId],
@@ -56,6 +69,10 @@ export function TaskDetail({ taskId, onClose }: TaskDetailProps) {
queryClient.invalidateQueries({ queryKey: ['task-comments', taskId] });
setNewComment('');
toast.success('Komentár bol pridaný');
// Označiť notifikáciu ako prečítanú ak existuje
if (notificationId) {
markAsRead(notificationId);
}
},
onError: (error: unknown) => {
const axiosError = error as { response?: { data?: { message?: string } } };
@@ -70,6 +87,10 @@ export function TaskDetail({ taskId, onClose }: TaskDetailProps) {
queryClient.invalidateQueries({ queryKey: ['tasks'] });
queryClient.invalidateQueries({ queryKey: ['dashboard-today'] });
toast.success('Úloha bola aktualizovaná');
// Označiť notifikáciu ako prečítanú ak existuje
if (notificationId) {
markAsRead(notificationId);
}
},
onError: () => {
toast.error('Chyba pri aktualizácii úlohy');
@@ -191,6 +212,47 @@ export function TaskDetail({ taskId, onClose }: TaskDetailProps) {
</div>
</div>
<div className="flex items-center gap-2">
{/* Tlačidlá pre notifikáciu */}
{notification && (
<>
<Button
variant="outline"
size="sm"
onClick={() => markAsRead(notification.id)}
title="Označiť ako prečítané"
>
<Check className="h-4 w-4 mr-1" />
Prečítané
</Button>
<div className="relative">
<Button
variant="outline"
size="sm"
onClick={() => setSnoozeOpen(!snoozeOpen)}
title="Odložiť notifikáciu"
>
<Clock className="h-4 w-4 mr-1" />
Odložiť
</Button>
{snoozeOpen && (
<div className="absolute right-0 top-full mt-1 bg-popover border rounded-md shadow-lg z-20 min-w-[140px]">
{snoozeOptions.map((option) => (
<button
key={option.label}
onClick={() => {
snooze(notification.id, calculateSnoozeMinutes(option));
setSnoozeOpen(false);
}}
className="w-full px-3 py-1.5 text-left text-sm hover:bg-accent first:rounded-t-md last:rounded-b-md"
>
{option.label}
</button>
))}
</div>
)}
</div>
</>
)}
{canEdit && (
<Button variant="outline" size="sm" onClick={() => setIsEditing(true)}>
<Pencil className="h-4 w-4 mr-1" />