+ {unreadNotifications.map((notification) => {
+ const actorName = notification.data?.actorName as string | undefined;
+
+ // Získať zmysluplný obsah správy
+ const getMessageContent = () => {
+ const msg = notification.message;
+
+ // Pre staré formáty zmeny stavu - extrahuj stavy
+ if (notification.type === 'TASK_STATUS_CHANGED' && msg.includes('zmenila stav')) {
+ const match = msg.match(/z "(.+?)" na "(.+?)"/);
+ if (match) {
+ return { message: `${match[1]} → ${match[2]}`, actor: actorName };
+ }
+ }
+
+ return { message: msg, actor: actorName };
+ };
+
+ const { message: displayMessage, actor: displayActor } = getMessageContent();
+
+ return (
+
{
+ if (notification.task) {
+ setTaskDetail({ taskId: notification.task.id, notificationId: notification.id });
+ }
+ }}
+ >
+
+ {/* Ikona */}
+
+
+ {getNotificationIcon(notification.type)}
+
+
+
+ {/* Obsah */}
+
+ {/* Hlavička - typ + čas */}
+
+
+ {getNotificationTypeLabel(notification.type)}
+
+ ·
+
+ {formatRelativeTime(notification.createdAt)}
+
+
+
+ {/* Názov úlohy + projekt */}
+ {notification.task && (
+
+
+ {notification.task.title}
+
+ {notification.task.project && (
+
+ • {notification.task.project.name}
+
+ )}
+
+ )}
+
+ {/* Detail zmeny + autor */}
+ {(displayMessage || displayActor) && (
+
+ {displayMessage && (
+
+ {displayMessage}
+
+ )}
+ {displayActor && (
+
+ {displayActor}
+
+ )}
+
+ )}
+
+
+ {/* Akcie */}
+
e.stopPropagation()}
+ >
+
+
+
+ {snoozeOpenFor === notification.id && (
+
+ {snoozeOptions.map((option) => (
+
+ ))}
+
+ )}
+
+
-
- {task.deadline && (
-
- {getDaysUntilDeadline(task.deadline)}
+ );
+ })}
+
+
+
+ )}
+
+ {/* Urgentné úlohy - po termíne + blížiaci sa termín */}
+ {(overdueTasks.length > 0 || urgentTasks.length > 0) && (
+
+
+
+
+ Vyžaduje pozornosť
+ {overdueTasks.length + urgentTasks.length}
+
+
+
+
+ {/* Po termíne */}
+ {overdueTasks.map((task) => (
+
setTaskDetail({ taskId: task.id })}
+ className="p-3 rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 cursor-pointer hover:border-red-400 transition-colors"
+ >
+
+
+
+
+ {getDaysUntilDeadline(task.deadline!)}
- )}
- {task.priority?.name}
+ {task.priority?.name}
+
+ {task.description && (
+
+ {task.description}
+
+ )}
+ {task.createdBy && (
+
+
+ Zadal: {task.createdBy.name}
+
+ )}
+
+ ))}
+ {/* Blížiaci sa termín */}
+ {urgentTasks.filter(t => !overdueTasks.includes(t)).map((task) => (
+
setTaskDetail({ taskId: task.id })}
+ className="p-3 rounded-lg bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 cursor-pointer hover:border-amber-400 transition-colors"
+ >
+
+
+
+
+ {getDaysUntilDeadline(task.deadline!)}
+
+ {task.priority?.name}
+
+
+ {task.description && (
+
+ {task.description}
+
+ )}
+ {task.createdBy && (
+
+
+ Zadal: {task.createdBy.name}
+
+ )}
))}
@@ -205,176 +472,181 @@ export function Dashboard() {
)}
-
- {/* Moje úlohy */}
-
-
-
-
- Moje úlohy
- {today?.myTasks && today.myTasks.length > 0 && (
-
- ({today.myTasks.length})
-
- )}
-
-
- Všetky
-
-
-
- {normalTasks.length > 0 ? (
-
- {normalTasks.slice(0, 5).map((task) => (
-
setDetailTaskId(task.id)}
- className="p-3 rounded-lg bg-muted/50 hover:bg-muted transition-colors cursor-pointer"
- >
-
-
-
{task.title}
- {task.description && (
-
- {task.description}
-
+ {/* Úlohy podľa stavov */}
+ {statuses.filter(s => !s.isFinal).map((status) => {
+ const tasks = tasksByStatus[status.id] || [];
+ const isCollapsed = collapsedStatuses.has(status.id);
+
+ if (tasks.length === 0) return null;
+
+ return (
+
+ toggleStatusCollapse(status.id)}
+ >
+
+
+ {isCollapsed ? (
+
+ ) : (
+
+ )}
+
+ {status.name}
+ {tasks.length}
+
+ e.stopPropagation()}
+ >
+ Zobraziť všetky
+
+
+
+ {!isCollapsed && (
+
+
+ {tasks.map((task) => (
+
setTaskDetail({ taskId: task.id })}
+ className="p-3 rounded-lg bg-muted/50 hover:bg-muted transition-colors cursor-pointer"
+ >
+
+
+
+ {task.priority?.name}
+
+
{task.title}
+
+ {task.deadline && (
+
+
+ {formatDate(task.deadline)}
+
)}
-
- {task.status?.name}
- {task.priority?.name}
-
-
-
- {task.project && (
-
-
- {task.project.name}
-
+ {task.description && (
+
+ {task.description}
+
)}
{task.createdBy && (
-
+
Zadal: {task.createdBy.name}
)}
- {task.deadline && (
-
-
- {formatDate(task.deadline)}
-
- )}
-
- ))}
- {normalTasks.length > 5 && (
-
- +{normalTasks.length - 5} ďalších úloh
-
- )}
-
- ) : today?.myTasks?.length === 0 ? (
-
-
-
Nemáte žiadne priradené úlohy
-
- Zobraziť všetky úlohy →
-
-
- ) : null}
-
-
-
- {/* Moje projekty */}
-
-
-
-
- Moje projekty
- {today?.myProjects && today.myProjects.length > 0 && (
-
- ({today.myProjects.length})
-
- )}
-
-
- Všetky
-
-
-
- {today?.myProjects && today.myProjects.length > 0 ? (
-
- {today.myProjects.map((project) => (
-
-
-
-
{project.name}
- {project.description && (
-
- {project.description}
-
- )}
-
-
{project.status?.name}
-
-
-
-
- {project._count?.tasks ?? 0} úloh
-
- {project.hardDeadline && (
-
-
- Termín: {formatDate(project.hardDeadline)}
-
- )}
-
-
- ))}
-
- ) : (
-
-
-
Nemáte žiadne aktívne projekty
-
- Zobraziť všetky projekty →
-
-
+ ))}
+
+
)}
-
-
-
+
+ );
+ })}
- {/* Upozornenie na revízie */}
- {(stats?.equipment.upcomingRevisions ?? 0) > 0 && (
-
-
-
-
-
Blížiace sa revízie
+ {/* Dokončené úlohy - defaultne zbalené */}
+ {statuses.filter(s => s.isFinal).map((status) => {
+ const tasks = tasksByStatus[status.id] || [];
+
+ if (tasks.length === 0) return null;
+
+ const isCollapsed = !collapsedStatuses.has(`done-${status.id}`);
+
+ return (
+
+ {
+ setCollapsedStatuses(prev => {
+ const next = new Set(prev);
+ const key = `done-${status.id}`;
+ if (next.has(key)) {
+ next.delete(key);
+ } else {
+ next.add(key);
+ }
+ return next;
+ });
+ }}
+ >
+
+ {isCollapsed ? (
+
+ ) : (
+
+ )}
+
+ {status.name}
+ {tasks.length}
+
+
+ {!isCollapsed && (
+
+
+ {tasks.slice(0, 5).map((task) => (
+
setTaskDetail({ taskId: task.id })}
+ className="flex items-center gap-4 p-3 rounded-lg bg-muted/50 hover:bg-muted transition-colors cursor-pointer"
+ >
+
+ {task.completedAt && (
+
+ {formatDate(task.completedAt)}
+
+ )}
+
+ ))}
+ {tasks.length > 5 && (
+
+ Zobraziť všetkých {tasks.length} úloh
+
+ )}
+
+
+ )}
+
+ );
+ })}
+
+ {/* Žiadne úlohy */}
+ {totalTasks === 0 && unreadNotifications.length === 0 && (
+
+
+
+
+
Všetko vybavené!
+
Nemáte žiadne priradené úlohy
+
+ Zobraziť všetky úlohy
+
-
-
-
- Máte {stats?.equipment.upcomingRevisions} zariadení s blížiacou sa revíziou v nasledujúcich 30 dňoch.
-
-
- Skontrolovať zariadenia →
-
)}
{/* Detail úlohy */}
- {detailTaskId && (
+ {taskDetail && (
{
- setDetailTaskId(null);
- // Refresh dashboard data po zatvorení
+ setTaskDetail(null);
queryClient.invalidateQueries({ queryKey: ['dashboard-today'] });
}}
/>
diff --git a/frontend/src/pages/projects/ProjectsList.tsx b/frontend/src/pages/projects/ProjectsList.tsx
index 9be1fde..a777131 100644
--- a/frontend/src/pages/projects/ProjectsList.tsx
+++ b/frontend/src/pages/projects/ProjectsList.tsx
@@ -1,7 +1,8 @@
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
-import { ExternalLink } from 'lucide-react';
+import { ExternalLink, AlertTriangle } from 'lucide-react';
import { zakazkyApi } from '@/services/zakazky.api';
+import { useAuthStore } from '@/store/authStore';
import {
Input,
Card,
@@ -23,9 +24,11 @@ import { formatDate } from '@/lib/utils';
export function ProjectsList() {
const [selectedYear, setSelectedYear] = useState(new Date().getFullYear());
const [search, setSearch] = useState('');
+ const { user } = useAuthStore();
+ const isAdmin = user?.role === 'ADMIN';
// Check if external DB is configured
- const { data: zakazkyStatus, isLoading: statusLoading } = useQuery({
+ const { data: zakazkyStatus, isLoading: statusLoading, error: statusError } = useQuery({
queryKey: ['zakazky-status'],
queryFn: () => zakazkyApi.checkStatus(),
});
@@ -38,12 +41,20 @@ export function ProjectsList() {
});
// Get zakazky for selected year
- const { data: zakazkyData, isLoading: zakazkyLoading } = useQuery({
+ const { data: zakazkyData, isLoading: zakazkyLoading, error: zakazkyError } = useQuery({
queryKey: ['zakazky', selectedYear, search],
queryFn: () => zakazkyApi.getAll(selectedYear, search || undefined),
enabled: !!zakazkyStatus?.data?.configured,
+ retry: 1,
});
+ // Extract error message
+ const getErrorMessage = (error: unknown): string => {
+ if (!error) return '';
+ const axiosError = error as { response?: { data?: { message?: string } }; message?: string };
+ return axiosError.response?.data?.message || axiosError.message || 'Neznáma chyba';
+ };
+
const isExternalDbConfigured = zakazkyStatus?.data?.configured;
const yearOptions = (yearsData?.data || []).map((year) => ({
value: String(year),
@@ -107,8 +118,25 @@ export function ProjectsList() {
+ {/* Error display for admins */}
+ {isAdmin && (statusError || zakazkyError) && (
+
+
+
+ Chyba pripojenia k externej databáze
+
+
+ {getErrorMessage(statusError || zakazkyError)}
+
+
+ )}
+
{zakazkyLoading ? (
+ ) : zakazkyError ? (
+
+ Nepodarilo sa načítať zákazky. Skúste obnoviť stránku.
+
) : (
diff --git a/frontend/src/pages/settings/PasswordResetModal.tsx b/frontend/src/pages/settings/PasswordResetModal.tsx
new file mode 100644
index 0000000..732ffd4
--- /dev/null
+++ b/frontend/src/pages/settings/PasswordResetModal.tsx
@@ -0,0 +1,91 @@
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { usersApi } from '@/services/users.api';
+import { Button, Input, ModalFooter } from '@/components/ui';
+import toast from 'react-hot-toast';
+
+const passwordResetSchema = z
+ .object({
+ password: z
+ .string()
+ .min(8, 'Heslo musí mať aspoň 8 znakov')
+ .regex(/[A-Z]/, 'Heslo musí obsahovať veľké písmeno')
+ .regex(/[0-9]/, 'Heslo musí obsahovať číslo'),
+ confirmPassword: z.string(),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ message: 'Heslá sa nezhodujú',
+ path: ['confirmPassword'],
+ });
+
+type PasswordResetFormData = z.infer;
+
+interface PasswordResetModalProps {
+ userId: string;
+ userName: string;
+ onClose: () => void;
+}
+
+export function PasswordResetModal({ userId, userName, onClose }: PasswordResetModalProps) {
+ const queryClient = useQueryClient();
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(passwordResetSchema),
+ defaultValues: {
+ password: '',
+ confirmPassword: '',
+ },
+ });
+
+ const mutation = useMutation({
+ mutationFn: (data: PasswordResetFormData) =>
+ usersApi.update(userId, { password: data.password }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['users'] });
+ toast.success(`Heslo pre ${userName} bolo zmenené`);
+ onClose();
+ },
+ onError: () => {
+ toast.error('Chyba pri zmene hesla');
+ },
+ });
+
+ return (
+
+ );
+}
diff --git a/frontend/src/pages/settings/SettingsDashboard.tsx b/frontend/src/pages/settings/SettingsDashboard.tsx
index dfe47c0..cf1eba6 100644
--- a/frontend/src/pages/settings/SettingsDashboard.tsx
+++ b/frontend/src/pages/settings/SettingsDashboard.tsx
@@ -21,8 +21,9 @@ import {
ModalFooter,
} from '@/components/ui';
import toast from 'react-hot-toast';
+import { UserManagement } from './UserManagement';
-type ConfigTab = 'taskStatuses' | 'priorities' | 'equipmentTypes' | 'revisionTypes' | 'rmaStatuses' | 'rmaSolutions' | 'userRoles';
+type ConfigTab = 'users' | 'taskStatuses' | 'priorities' | 'equipmentTypes' | 'revisionTypes' | 'rmaStatuses' | 'rmaSolutions' | 'userRoles' | 'systemSettings';
// Spoločný interface pre konfiguračné entity
interface ConfigItem {
@@ -33,7 +34,18 @@ interface ConfigItem {
order?: number;
}
+interface SystemSetting {
+ id: string;
+ key: string;
+ value: unknown;
+ category: string;
+ label: string;
+ description?: string | null;
+ dataType: string;
+}
+
const tabs: { key: ConfigTab; label: string }[] = [
+ { key: 'users', label: 'Používatelia' },
{ key: 'taskStatuses', label: 'Stavy úloh' },
{ key: 'priorities', label: 'Priority' },
{ key: 'equipmentTypes', label: 'Typy zariadení' },
@@ -41,11 +53,12 @@ const tabs: { key: ConfigTab; label: string }[] = [
{ key: 'rmaStatuses', label: 'RMA stavy' },
{ key: 'rmaSolutions', label: 'RMA riešenia' },
{ key: 'userRoles', label: 'Užívateľské role' },
+ { key: 'systemSettings', label: 'Systémové nastavenia' },
];
export function SettingsDashboard() {
const queryClient = useQueryClient();
- const [activeTab, setActiveTab] = useState('taskStatuses');
+ const [activeTab, setActiveTab] = useState('users');
const [editItem, setEditItem] = useState(null);
const [deleteItem, setDeleteItem] = useState<{ id: string; name: string } | null>(null);
@@ -91,6 +104,12 @@ export function SettingsDashboard() {
enabled: activeTab === 'userRoles',
});
+ const { data: systemSettings, isLoading: loadingSystemSettings } = useQuery({
+ queryKey: ['system-settings'],
+ queryFn: () => settingsApi.getSystemSettings(),
+ enabled: activeTab === 'systemSettings',
+ });
+
const deleteMutation = useMutation({
mutationFn: async ({ tab, id }: { tab: ConfigTab; id: string }) => {
switch (tab) {
@@ -114,7 +133,7 @@ export function SettingsDashboard() {
});
const isLoading = loadingStatuses || loadingPriorities || loadingEquipmentTypes ||
- loadingRevisionTypes || loadingRmaStatuses || loadingRmaSolutions || loadingUserRoles;
+ loadingRevisionTypes || loadingRmaStatuses || loadingRmaSolutions || loadingUserRoles || loadingSystemSettings;
const getCurrentData = (): ConfigItem[] => {
switch (activeTab) {
@@ -125,10 +144,12 @@ export function SettingsDashboard() {
case 'rmaStatuses': return (rmaStatuses?.data || []) as ConfigItem[];
case 'rmaSolutions': return (rmaSolutions?.data || []) as ConfigItem[];
case 'userRoles': return (userRoles?.data || []) as ConfigItem[];
+ case 'systemSettings': return []; // Systémové nastavenia majú iný formát
}
};
const data: ConfigItem[] = getCurrentData();
+ const settings: SystemSetting[] = (systemSettings?.data || []) as SystemSetting[];
return (
@@ -147,65 +168,71 @@ export function SettingsDashboard() {
))}
-
-
- {tabs.find(t => t.key === activeTab)?.label}
-
-
-
- {isLoading ? (
-
- ) : (
-
-
-
- Kód
- Názov
- Farba
- Poradie
- Akcie
-
-
-
- {data.map((item) => (
-
- {item.code}
- {item.name}
-
- {item.color && (
- {item.color}
- )}
-
- {item.order ?? 0}
-
-
-
-
-
- ))}
- {data.length === 0 && (
+ {activeTab === 'users' ? (
+
+ ) : activeTab === 'systemSettings' ? (
+
+ ) : (
+
+
+ {tabs.find(t => t.key === activeTab)?.label}
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+
-
- Žiadne položky
-
+ Kód
+ Názov
+ Farba
+ Poradie
+ Akcie
- )}
-
-
- )}
-
-
+
+
+ {data.map((item) => (
+
+ {item.code}
+ {item.name}
+
+ {item.color && (
+ {item.color}
+ )}
+
+ {item.order ?? 0}
+
+
+
+
+
+ ))}
+ {data.length === 0 && (
+
+
+ Žiadne položky
+
+
+ )}
+
+
+ )}
+
+
+ )}
);
}
+
+// Komponent pre systémové nastavenia
+interface SystemSettingsPanelProps {
+ settings: SystemSetting[];
+ isLoading: boolean;
+}
+
+function SystemSettingsPanel({ settings, isLoading }: SystemSettingsPanelProps) {
+ const queryClient = useQueryClient();
+ const [editingSetting, setEditingSetting] = useState(null);
+
+ const updateMutation = useMutation({
+ mutationFn: async ({ key, value }: { key: string; value: unknown }) => {
+ return settingsApi.updateSystemSetting(key, value);
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['system-settings'] });
+ toast.success('Nastavenie bolo aktualizované');
+ setEditingSetting(null);
+ },
+ onError: () => {
+ toast.error('Chyba pri aktualizácii nastavenia');
+ },
+ });
+
+ // Zoskupiť nastavenia podľa kategórie
+ const settingsByCategory = settings.reduce((acc, setting) => {
+ const category = setting.category || 'OTHER';
+ if (!acc[category]) acc[category] = [];
+ acc[category].push(setting);
+ return acc;
+ }, {} as Record);
+
+ const categoryLabels: Record = {
+ NOTIFICATIONS: 'Notifikácie',
+ GENERAL: 'Všeobecné',
+ EMAIL: 'Email',
+ OTHER: 'Ostatné',
+ };
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+ {Object.entries(settingsByCategory).map(([category, categorySettings]) => (
+
+
+ {categoryLabels[category] || category}
+
+
+ {categorySettings.map((setting) => (
+
+
+
+
{setting.label}
+ {setting.description && (
+
{setting.description}
+ )}
+
+ Kľúč: {setting.key}
+
+
+
+
+
+ {setting.dataType === 'json' ? (
+
{JSON.stringify(setting.value, null, 2)}
+ ) : (
+
{String(setting.value)}
+ )}
+
+
+ ))}
+
+
+ ))}
+
+ {settings.length === 0 && (
+
+
+ Žiadne systémové nastavenia
+
+
+ )}
+
+ {/* Modal pre editáciu */}
+
setEditingSetting(null)}
+ title={`Upraviť: ${editingSetting?.label}`}
+ >
+ {editingSetting && (
+ updateMutation.mutate({ key: editingSetting.key, value })}
+ onClose={() => setEditingSetting(null)}
+ isLoading={updateMutation.isPending}
+ />
+ )}
+
+
+ );
+}
+
+// Formulár pre úpravu systémového nastavenia
+interface SystemSettingFormProps {
+ setting: SystemSetting;
+ onSave: (value: unknown) => void;
+ onClose: () => void;
+ isLoading: boolean;
+}
+
+function SystemSettingForm({ setting, onSave, onClose, isLoading }: SystemSettingFormProps) {
+ const [value, setValue] = useState(() => {
+ if (setting.dataType === 'json') {
+ return JSON.stringify(setting.value, null, 2);
+ }
+ return String(setting.value);
+ });
+ const [error, setError] = useState(null);
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ setError(null);
+
+ try {
+ let parsedValue: unknown;
+ if (setting.dataType === 'json') {
+ parsedValue = JSON.parse(value);
+ } else if (setting.dataType === 'number') {
+ parsedValue = Number(value);
+ } else if (setting.dataType === 'boolean') {
+ parsedValue = value === 'true';
+ } else {
+ parsedValue = value;
+ }
+ onSave(parsedValue);
+ } catch {
+ setError('Neplatný formát hodnoty. Skontrolujte syntax JSON.');
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/src/pages/settings/UserForm.tsx b/frontend/src/pages/settings/UserForm.tsx
new file mode 100644
index 0000000..bf66a72
--- /dev/null
+++ b/frontend/src/pages/settings/UserForm.tsx
@@ -0,0 +1,176 @@
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { usersApi, type CreateUserData, type UpdateUserData } from '@/services/users.api';
+import { settingsApi } from '@/services/settings.api';
+import type { User } from '@/types';
+import { Button, Input, Select, ModalFooter, LoadingOverlay } from '@/components/ui';
+import toast from 'react-hot-toast';
+
+const userFormSchema = z.object({
+ name: z.string().min(2, 'Meno musí mať aspoň 2 znaky'),
+ email: z.string().email('Neplatný email'),
+ password: z.string().optional(),
+ roleId: z.string().min(1, 'Rola je povinná'),
+ active: z.boolean(),
+});
+
+type UserFormData = z.infer;
+
+interface UserFormProps {
+ user: User | null;
+ onClose: () => void;
+}
+
+export function UserForm({ user, onClose }: UserFormProps) {
+ const queryClient = useQueryClient();
+ const isEditing = !!user?.id;
+
+ const { data: rolesData, isLoading: rolesLoading } = useQuery({
+ queryKey: ['user-roles'],
+ queryFn: () => settingsApi.getUserRoles(),
+ });
+
+ const roles = rolesData?.data || [];
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(
+ isEditing
+ ? userFormSchema
+ : userFormSchema.extend({
+ password: z
+ .string()
+ .min(8, 'Heslo musí mať aspoň 8 znakov')
+ .regex(/[A-Z]/, 'Heslo musí obsahovať veľké písmeno')
+ .regex(/[0-9]/, 'Heslo musí obsahovať číslo'),
+ })
+ ),
+ defaultValues: {
+ name: user?.name || '',
+ email: user?.email || '',
+ password: '',
+ roleId: user?.role?.id || '',
+ active: user?.active ?? true,
+ },
+ });
+
+ const createMutation = useMutation({
+ mutationFn: (data: CreateUserData) => usersApi.create(data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['users'] });
+ toast.success('Používateľ bol vytvorený');
+ onClose();
+ },
+ onError: (error: unknown) => {
+ const msg = (error as { response?: { data?: { message?: string } } })?.response?.data?.message;
+ toast.error(msg || 'Chyba pri vytváraní používateľa');
+ },
+ });
+
+ const updateMutation = useMutation({
+ mutationFn: async (data: UserFormData) => {
+ const updateData: UpdateUserData = {
+ name: data.name,
+ email: data.email,
+ active: data.active,
+ };
+ await usersApi.update(user!.id, updateData);
+
+ if (data.roleId !== user!.role.id) {
+ await usersApi.updateRole(user!.id, data.roleId);
+ }
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['users'] });
+ toast.success('Používateľ bol aktualizovaný');
+ onClose();
+ },
+ onError: (error: unknown) => {
+ const msg = (error as { response?: { data?: { message?: string } } })?.response?.data?.message;
+ toast.error(msg || 'Chyba pri aktualizácii používateľa');
+ },
+ });
+
+ const onSubmit = handleSubmit((data) => {
+ if (isEditing) {
+ updateMutation.mutate(data);
+ } else {
+ createMutation.mutate({
+ name: data.name,
+ email: data.email,
+ password: data.password!,
+ roleId: data.roleId,
+ });
+ }
+ });
+
+ const isPending = createMutation.isPending || updateMutation.isPending;
+
+ const roleOptions = roles.map((role) => ({
+ value: role.id,
+ label: role.name,
+ }));
+
+ if (rolesLoading) {
+ return ;
+ }
+
+ return (
+
+ );
+}
diff --git a/frontend/src/pages/settings/UserManagement.tsx b/frontend/src/pages/settings/UserManagement.tsx
new file mode 100644
index 0000000..f489600
--- /dev/null
+++ b/frontend/src/pages/settings/UserManagement.tsx
@@ -0,0 +1,249 @@
+import { useState } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Plus, Pencil, Trash2, Search, KeyRound } from 'lucide-react';
+import { usersApi } from '@/services/users.api';
+import { useConfigStore } from '@/store/configStore';
+import { useAuthStore } from '@/store/authStore';
+import type { User } from '@/types';
+import {
+ Button,
+ Input,
+ Select,
+ Card,
+ CardHeader,
+ CardContent,
+ Table,
+ TableHeader,
+ TableBody,
+ TableRow,
+ TableHead,
+ TableCell,
+ Badge,
+ LoadingOverlay,
+ Modal,
+ ModalFooter,
+} from '@/components/ui';
+import { UserForm } from './UserForm';
+import { PasswordResetModal } from './PasswordResetModal';
+import { formatDate } from '@/lib/utils';
+import toast from 'react-hot-toast';
+
+export function UserManagement() {
+ const queryClient = useQueryClient();
+ const { userRoles } = useConfigStore();
+ const currentUser = useAuthStore((s) => s.user);
+
+ const [search, setSearch] = useState('');
+ const [roleFilter, setRoleFilter] = useState('');
+ const [activeFilter, setActiveFilter] = useState('');
+ const [isFormOpen, setIsFormOpen] = useState(false);
+ const [editingUser, setEditingUser] = useState(null);
+ const [passwordResetUser, setPasswordResetUser] = useState<{ id: string; name: string } | null>(null);
+ const [deleteConfirm, setDeleteConfirm] = useState(null);
+
+ const { data, isLoading } = useQuery({
+ queryKey: ['users', search, roleFilter, activeFilter],
+ queryFn: () =>
+ usersApi.getAll({
+ search: search || undefined,
+ roleId: roleFilter || undefined,
+ active: activeFilter || undefined,
+ limit: 100,
+ }),
+ });
+
+ const deleteMutation = useMutation({
+ mutationFn: (id: string) => usersApi.delete(id),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['users'] });
+ toast.success('Používateľ bol deaktivovaný');
+ setDeleteConfirm(null);
+ },
+ onError: (error: unknown) => {
+ const msg = (error as { response?: { data?: { message?: string } } })?.response?.data?.message;
+ toast.error(msg || 'Chyba pri deaktivácii používateľa');
+ },
+ });
+
+ const handleEdit = (user: User) => {
+ setEditingUser(user);
+ setIsFormOpen(true);
+ };
+
+ const handleCloseForm = () => {
+ setIsFormOpen(false);
+ setEditingUser(null);
+ };
+
+ const isSelf = (userId: string) => currentUser?.id === userId;
+
+ const roleOptions = [
+ { value: '', label: 'Všetky role' },
+ ...userRoles.map((role) => ({ value: role.id, label: role.name })),
+ ];
+
+ const activeOptions = [
+ { value: '', label: 'Všetci' },
+ { value: 'true', label: 'Aktívni' },
+ { value: 'false', label: 'Neaktívni' },
+ ];
+
+ const users = data?.data || [];
+
+ return (
+
+
+
Správa používateľov
+
+
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ className="pl-9"
+ />
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+
+
+ Meno
+ Email
+ Rola
+ Stav
+ Vytvorený
+ Akcie
+
+
+
+ {users.map((user) => (
+
+
+ {user.name}
+ {isSelf(user.id) && (
+ (vy)
+ )}
+
+ {user.email}
+
+ {user.role.name}
+
+
+
+ {user.active ? 'Aktívny' : 'Neaktívny'}
+
+
+ {formatDate(user.createdAt)}
+
+
+
+ {!isSelf(user.id) && (
+
+ )}
+
+
+ ))}
+ {users.length === 0 && (
+
+
+ Žiadni používatelia
+
+
+ )}
+
+
+ )}
+
+
+
+
+
+
+
+
setPasswordResetUser(null)}
+ title="Zmena hesla"
+ >
+ {passwordResetUser && (
+ setPasswordResetUser(null)}
+ />
+ )}
+
+
+
setDeleteConfirm(null)}
+ title="Potvrdiť deaktiváciu"
+ >
+
+ Naozaj chcete deaktivovať používateľa {deleteConfirm?.name}?
+
+
+ Používateľ sa nebude môcť prihlásiť, ale jeho dáta zostanú zachované.
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/tasks/TaskDetail.tsx b/frontend/src/pages/tasks/TaskDetail.tsx
index f067e84..a880a3c 100644
--- a/frontend/src/pages/tasks/TaskDetail.tsx
+++ b/frontend/src/pages/tasks/TaskDetail.tsx
@@ -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) {
+ {/* Tlačidlá pre notifikáciu */}
+ {notification && (
+ <>
+
+
+
+ {snoozeOpen && (
+
+ {snoozeOptions.map((option) => (
+
+ ))}
+
+ )}
+
+ >
+ )}
{canEdit && (