import { useState, useCallback } from "react"; import { Link as RouterLink } from "react-router-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import CircularProgress from "@mui/material/CircularProgress"; import { useAuth } from "../context/AuthContext"; import { useAlert } from "../context/AlertContext"; import apiFetch from "../utils/api"; import { dashboardOptions } from "../lib/queries/dashboard"; import { totpStatusOptions } from "../lib/queries/settings"; import { getCzechDate } from "../utils/dashboardHelpers"; import { useApiMutation } from "../lib/queries/mutations"; import { Card, Button, StatusChip, PageEnter } from "../ui"; import { iconBadgeSx } from "../theme"; import DashKpiCards from "../components/dashboard/DashKpiCards"; import DashQuickActions from "../components/dashboard/DashQuickActions"; import DashActivityFeed from "../components/dashboard/DashActivityFeed"; import DashAttendanceToday from "../components/dashboard/DashAttendanceToday"; import DashProfile from "../components/dashboard/DashProfile"; import DashSessions from "../components/dashboard/DashSessions"; import DashTodayPlan, { type TodayPlan, } from "../components/dashboard/DashTodayPlan"; const API_BASE = "/api/admin"; interface DashData { my_shift?: { has_ongoing: boolean }; attendance?: { present_today: number; total_active: number; on_leave: number; users: Array<{ user_id: number | string; name: string; initials?: string; status: string; leave_type?: string; arrived_at?: string; }>; }; offers?: { open_count: number; converted_count: number; expired_count: number; created_this_month: number; }; projects?: { active_projects: Array<{ id: number; name: string; customer_name: string | null; }>; }; invoices?: { revenue_this_month: Array<{ amount: number; currency: string }>; unpaid_count: number; revenue_czk: number | null; }; leave_pending?: { count: number }; recent_activity?: Array<{ id: number | string; action: string; entity_type: string; description: string; username?: string; created_at: string; }>; users_count?: number; active_projects?: number; pending_orders?: number; unpaid_invoices?: number; pending_leave_requests?: number; today_plan?: TodayPlan[]; [key: string]: unknown; } export default function Dashboard() { const { user, updateUser, hasPermission } = useAuth(); const alert = useAlert(); const [punching, setPunching] = useState(false); const queryClient = useQueryClient(); const { data: dashDataRaw, isPending: dashLoading } = useQuery(dashboardOptions()); const dashData = dashDataRaw as DashData | undefined; // Personal 2FA enrollment (users.totp_enabled) — NOT the company-wide // require_2fa policy flag (the banner below uses user.require2FA for that). const { data: totpData, isPending: totpLoading } = useQuery(totpStatusOptions()); const totpEnabled = totpData?.totp_enabled ?? !!user?.totpEnabled; const punchMutation = useApiMutation< Record, { message?: string } >({ url: () => `${API_BASE}/attendance`, method: () => "POST", invalidate: ["attendance", "dashboard"], }); // 2FA state - sdileny mezi profilem a bannerem const [show2FASetup, setShow2FASetup] = useState(false); const [show2FADisable, setShow2FADisable] = useState(false); const [totpSecret, setTotpSecret] = useState(null); const [totpQrUri, setTotpQrUri] = useState(null); const [totpCode, setTotpCode] = useState(""); const [totpSubmitting, setTotpSubmitting] = useState(false); const [backupCodes, setBackupCodes] = useState(null); const [disableCode, setDisableCode] = useState(""); // Punch (prichod/odchod) primo z dashboardu const handleQuickPunch = useCallback(() => { const action = dashData?.my_shift?.has_ongoing ? "departure" : "arrival"; setPunching(true); const submitPunch = async (gpsData: Record = {}) => { try { const result = await punchMutation.mutateAsync({ punch_action: action, ...gpsData, }); alert.success(result?.message || "Docházka zaznamenána"); } catch (e) { alert.error(e instanceof Error ? e.message : "Chyba připojení"); } finally { setPunching(false); } }; if (!navigator.geolocation) { submitPunch({}); return; } navigator.geolocation.getCurrentPosition( (pos) => { const { latitude, longitude, accuracy } = pos.coords; submitPunch({ latitude, longitude, accuracy, address: "" }); }, () => submitPunch({}), { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }, ); }, [dashData, alert, punchMutation]); // 2FA handlery const handleStart2FASetup = async () => { setTotpSubmitting(true); try { const response = await apiFetch(`${API_BASE}/totp/setup`); const data = await response.json(); if (data.success) { setTotpSecret(data.data.secret); setTotpQrUri(data.data.uri || data.data.qr_uri); setTotpCode(""); setBackupCodes(null); setShow2FASetup(true); } else { alert.error(data.error || "Nepodařilo se vygenerovat 2FA klíč"); } } catch { alert.error("Chyba připojení"); } finally { setTotpSubmitting(false); } }; const handleConfirm2FA = async () => { if (!totpCode.trim()) return; setTotpSubmitting(true); try { const response = await apiFetch(`${API_BASE}/totp/enable`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ secret: totpSecret, code: totpCode.trim() }), }); const data = await response.json(); if (data.success) { queryClient.invalidateQueries({ queryKey: ["totp"] }); queryClient.invalidateQueries({ queryKey: ["settings"] }); queryClient.invalidateQueries({ queryKey: ["dashboard"] }); queryClient.invalidateQueries({ queryKey: ["users"] }); setBackupCodes(data.data?.backup_codes || null); setTotpSecret(null); setTotpQrUri(null); updateUser({ totpEnabled: true }); alert.success("2FA bylo aktivováno"); } else { alert.error(data.error || "Neplatný kód"); setTotpCode(""); } } catch { alert.error("Chyba připojení"); } finally { setTotpSubmitting(false); } }; const handleDisable2FA = async () => { if (!disableCode.trim()) return; setTotpSubmitting(true); try { const response = await apiFetch(`${API_BASE}/totp/disable`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code: disableCode.trim() }), }); const data = await response.json(); if (data.success) { queryClient.invalidateQueries({ queryKey: ["totp"] }); queryClient.invalidateQueries({ queryKey: ["settings"] }); queryClient.invalidateQueries({ queryKey: ["dashboard"] }); queryClient.invalidateQueries({ queryKey: ["users"] }); setShow2FADisable(false); setDisableCode(""); updateUser({ totpEnabled: false }); alert.success("2FA bylo deaktivováno"); } else { alert.error(data.error || "Neplatný kód"); setDisableCode(""); } } catch { alert.error("Chyba připojení"); } finally { setTotpSubmitting(false); } }; return ( {/* Header */} Vítejte zpět, {user?.fullName || user?.username} {getCzechDate()} {/* 2FA Required Banner */} {user?.require2FA && !user?.totpEnabled && ( Dvoufaktorové ověření je povinné Administrátor vyžaduje aktivaci 2FA. Dokud ji neaktivujete, nemáte přístup k ostatním sekcím systému. )} {/* Loading spinner */} {dashLoading && ( )} {/* KPI cards — only show if user has any admin-level permissions */} {!dashLoading && (hasPermission("offers.view") || hasPermission("invoices.view") || hasPermission("projects.view") || hasPermission("orders.view")) && ( )} {/* My work plan for today — paired with the clock-in below */} {!dashLoading && (hasPermission("attendance.record") || hasPermission("attendance.manage")) && ( )} {/* Quick actions */} {!dashLoading && ( )} {/* Main content grid */} {!dashLoading && ( {hasPermission("settings.audit") && ( )} {hasPermission("attendance.manage") && ( )} {/* Pravy sloupec: projekty + nabidky — both grow so the stacked cards fill the column to match the neighbouring cards' height. */} {dashData?.projects && ( Aktivní projekty {dashData.projects.active_projects.length === 0 && ( Žádné aktivní projekty )} {dashData.projects.active_projects.map((p) => ( {p.name} {p.customer_name && ( {p.customer_name} )} ))} )} {dashData?.offers && ( Nabídky Otevřené Převedené na objednávku Zneplatněné )} )} {/* Profile + Sessions */} {!dashLoading && ( )} ); }