feat(mui): migrate Dashboard (Přehled) onto MUI kit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-07 01:19:49 +02:00
parent 5ca03bd662
commit 5459d8c325
7 changed files with 1205 additions and 968 deletions

View File

@@ -1,15 +1,18 @@
import { useState, useCallback } from "react";
import { Link } from "react-router-dom";
import { Link as RouterLink } from "react-router-dom";
import { motion } from "framer-motion";
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 useModalLock from "../hooks/useModalLock";
import apiFetch from "../utils/api";
import { dashboardOptions } from "../lib/queries/dashboard";
import { require2FAOptions } from "../lib/queries/settings";
import { getCzechDate } from "../utils/dashboardHelpers";
import { useApiMutation } from "../lib/queries/mutations";
import { Card, Button, StatusChip } from "../ui";
import DashKpiCards from "../components/dashboard/DashKpiCards";
import DashQuickActions from "../components/dashboard/DashQuickActions";
import DashActivityFeed from "../components/dashboard/DashActivityFeed";
@@ -102,9 +105,6 @@ export default function Dashboard() {
const [backupCodes, setBackupCodes] = useState<string[] | null>(null);
const [disableCode, setDisableCode] = useState("");
useModalLock(show2FASetup);
useModalLock(show2FADisable);
// Punch (prichod/odchod) primo z dashboardu
const handleQuickPunch = useCallback(() => {
const action = dashData?.my_shift?.has_ongoing ? "departure" : "arrival";
@@ -221,99 +221,101 @@ export default function Dashboard() {
};
return (
<div className="dash">
<Box>
{/* Header */}
<motion.div
className="admin-page-header"
<Box
component={motion.div}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25 }}
sx={{ mb: 3 }}
>
<div>
<h1 className="admin-page-title">
Vítejte zpět, {user?.fullName || user?.username}
</h1>
<p className="admin-page-subtitle">{getCzechDate()}</p>
</div>
</motion.div>
<Typography variant="h4">
Vítejte zpět, {user?.fullName || user?.username}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
{getCzechDate()}
</Typography>
</Box>
{/* 2FA Required Banner */}
{user?.require2FA && !user?.totpEnabled && (
<motion.div
className="admin-card"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.06 }}
style={{
border: "2px solid var(--danger)",
background: "var(--danger-light)",
}}
>
<div
className="admin-card-body"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "1rem",
flexWrap: "wrap",
<Card
sx={{
mb: 3,
border: 2,
borderColor: "error.main",
bgcolor: "error.light",
}}
>
<div className="flex-row-gap">
<div
style={{
width: 40,
height: 40,
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "var(--danger-light)",
color: "var(--danger)",
flexShrink: 0,
}}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
</div>
<div>
<div className="fw-600">Dvoufaktorové ověření je povinné</div>
<div
className="text-secondary"
style={{ fontSize: "0.875rem" }}
>
Administrátor vyžaduje aktivaci 2FA. Dokud ji neaktivujete,
nemáte přístup k ostatním sekcím systému.
</div>
</div>
</div>
<button
onClick={handleStart2FASetup}
disabled={totpSubmitting}
className="admin-btn admin-btn-primary"
style={{ flexShrink: 0 }}
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 2,
flexWrap: "wrap",
}}
>
{totpSubmitting ? "Generuji..." : "Aktivovat 2FA nyní"}
</button>
</div>
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
<Box
sx={{
width: 40,
height: 40,
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
bgcolor: "error.light",
color: "error.main",
flexShrink: 0,
}}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
</Box>
<Box>
<Typography sx={{ fontWeight: 600 }}>
Dvoufaktorové ověření je povinné
</Typography>
<Typography variant="body2" color="text.secondary">
Administrátor vyžaduje aktivaci 2FA. Dokud ji neaktivujete,
nemáte přístup k ostatním sekcím systému.
</Typography>
</Box>
</Box>
<Button
onClick={handleStart2FASetup}
disabled={totpSubmitting}
sx={{ flexShrink: 0 }}
>
{totpSubmitting ? "Generuji..." : "Aktivovat 2FA nyní"}
</Button>
</Box>
</Card>
</motion.div>
)}
{/* Loading spinner */}
{dashLoading && (
<div className="admin-loading">
<div className="admin-spinner" />
</div>
<Box sx={{ display: "flex", justifyContent: "center", py: 6 }}>
<CircularProgress />
</Box>
)}
{/* KPI cards — only show if user has any admin-level permissions */}
@@ -336,11 +338,18 @@ export default function Dashboard() {
{/* Main content grid */}
{!dashLoading && (
<motion.div
className="dash-main-grid"
<Box
component={motion.div}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.25, delay: 0.12 }}
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", lg: "1fr 1fr 1fr" },
gap: 3,
alignItems: "start",
mb: 3,
}}
>
{hasPermission("settings.audit") && (
<DashActivityFeed activities={dashData?.recent_activity ?? null} />
@@ -351,86 +360,146 @@ export default function Dashboard() {
)}
{/* Pravy sloupec: projekty + nabidky */}
<div className="dash-right-col">
<Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
{dashData?.projects && (
<div className="admin-card">
<div className="admin-card-header flex-between">
<h2 className="admin-card-title">Aktivní projekty</h2>
<Link
<Card sx={{ display: "flex", flexDirection: "column" }}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
mb: 2,
}}
>
<Typography variant="h6">Aktivní projekty</Typography>
<Button
component={RouterLink}
to="/projects"
className="admin-btn admin-btn-primary admin-btn-sm"
size="small"
sx={{ flexShrink: 0 }}
>
Vše &rarr;
</Link>
</div>
<div className="admin-card-body" style={{ padding: 0 }}>
{dashData.projects.active_projects.length === 0 && (
<div className="dash-empty-row">Žádné aktivní projekty</div>
)}
{dashData.projects.active_projects.map(
(p: {
id: number;
name: string;
customer_name: string | null;
}) => (
<Link
key={p.id}
to={`/projects/${p.id}`}
className="dash-project-row"
>
<div className="dash-project-name">{p.name}</div>
{p.customer_name && (
<div className="dash-project-customer">
{p.customer_name}
</div>
)}
</Link>
),
)}
</div>
</div>
</Button>
</Box>
{dashData.projects.active_projects.length === 0 && (
<Typography
variant="body2"
color="text.secondary"
sx={{ py: 1 }}
>
Žádné aktivní projekty
</Typography>
)}
{dashData.projects.active_projects.map((p) => (
<Box
key={p.id}
component={RouterLink}
to={`/projects/${p.id}`}
sx={{
display: "block",
py: 1.25,
textDecoration: "none",
color: "inherit",
borderBottom: 1,
borderColor: "divider",
"&:last-of-type": { borderBottom: 0 },
"&:hover": { color: "primary.main" },
}}
>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{p.name}
</Typography>
{p.customer_name && (
<Typography variant="caption" color="text.secondary">
{p.customer_name}
</Typography>
)}
</Box>
))}
</Card>
)}
{dashData?.offers && (
<div className="admin-card">
<div className="admin-card-header flex-between">
<h2 className="admin-card-title">Nabídky</h2>
<Link
<Card sx={{ display: "flex", flexDirection: "column" }}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
mb: 2,
}}
>
<Typography variant="h6">Nabídky</Typography>
<Button
component={RouterLink}
to="/offers"
className="admin-btn admin-btn-primary admin-btn-sm"
size="small"
sx={{ flexShrink: 0 }}
>
Zobrazit &rarr;
</Link>
</div>
<div className="admin-card-body" style={{ padding: 0 }}>
<div className="dash-stat-row">
<span>Otevřené</span>
<span className="admin-badge admin-badge-info">
{dashData.offers.open_count}
</span>
</div>
<div className="dash-stat-row">
<span>Převedené na objednávku</span>
<span className="admin-badge admin-badge-success">
{dashData.offers.converted_count}
</span>
</div>
<div className="dash-stat-row">
<span>Zneplatněné</span>
<span className="admin-badge admin-badge-warning">
{dashData.offers.expired_count}
</span>
</div>
</div>
</div>
</Button>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
py: 1,
borderBottom: 1,
borderColor: "divider",
}}
>
<Typography variant="body2">Otevřené</Typography>
<StatusChip color="info" label={dashData.offers.open_count} />
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
py: 1,
borderBottom: 1,
borderColor: "divider",
}}
>
<Typography variant="body2">
Převedené na objednávku
</Typography>
<StatusChip
color="success"
label={dashData.offers.converted_count}
/>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
py: 1,
}}
>
<Typography variant="body2">Zneplatněné</Typography>
<StatusChip
color="warning"
label={dashData.offers.expired_count}
/>
</Box>
</Card>
)}
</div>
</motion.div>
</Box>
</Box>
)}
{/* Profile + Sessions */}
{!dashLoading && (
<div className="dash-bottom">
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", lg: "1fr 1fr" },
gap: 3,
alignItems: "start",
}}
>
<DashProfile
totpEnabled={totpEnabled}
totpLoading={totpLoading}
@@ -452,8 +521,8 @@ export default function Dashboard() {
setDisableCode={setDisableCode}
/>
<DashSessions />
</div>
</Box>
)}
</div>
</Box>
);
}