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:
@@ -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 →
|
||||
</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 →
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user