style: run prettier on entire codebase
This commit is contained in:
@@ -1,203 +1,203 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { motion } from 'framer-motion'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useAlert } from '../context/AlertContext'
|
||||
import useModalLock from '../hooks/useModalLock'
|
||||
import apiFetch from '../utils/api'
|
||||
import { getCzechDate } from '../utils/dashboardHelpers'
|
||||
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 { useState, useEffect, useCallback } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { useAlert } from "../context/AlertContext";
|
||||
import useModalLock from "../hooks/useModalLock";
|
||||
import apiFetch from "../utils/api";
|
||||
import { getCzechDate } from "../utils/dashboardHelpers";
|
||||
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";
|
||||
|
||||
const API_BASE = '/api/admin'
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type DashData = Record<string, any>
|
||||
type DashData = Record<string, any>;
|
||||
|
||||
export default function Dashboard() {
|
||||
const { user, updateUser, hasPermission } = useAuth()
|
||||
const alert = useAlert()
|
||||
const { user, updateUser, hasPermission } = useAuth();
|
||||
const alert = useAlert();
|
||||
|
||||
const [dashData, setDashData] = useState<DashData | null>(null)
|
||||
const [dashLoading, setDashLoading] = useState(true)
|
||||
const [punching, setPunching] = useState(false)
|
||||
const [dashData, setDashData] = useState<DashData | null>(null);
|
||||
const [dashLoading, setDashLoading] = useState(true);
|
||||
const [punching, setPunching] = useState(false);
|
||||
|
||||
// 2FA state - sdileny mezi profilem a bannerem
|
||||
const [totpEnabled, setTotpEnabled] = useState(false)
|
||||
const [totpLoading, setTotpLoading] = useState(true)
|
||||
const [show2FASetup, setShow2FASetup] = useState(false)
|
||||
const [show2FADisable, setShow2FADisable] = useState(false)
|
||||
const [totpSecret, setTotpSecret] = useState<string | null>(null)
|
||||
const [totpQrUri, setTotpQrUri] = useState<string | null>(null)
|
||||
const [totpCode, setTotpCode] = useState('')
|
||||
const [totpSubmitting, setTotpSubmitting] = useState(false)
|
||||
const [backupCodes, setBackupCodes] = useState<string[] | null>(null)
|
||||
const [disableCode, setDisableCode] = useState('')
|
||||
const [totpEnabled, setTotpEnabled] = useState(false);
|
||||
const [totpLoading, setTotpLoading] = useState(true);
|
||||
const [show2FASetup, setShow2FASetup] = useState(false);
|
||||
const [show2FADisable, setShow2FADisable] = useState(false);
|
||||
const [totpSecret, setTotpSecret] = useState<string | null>(null);
|
||||
const [totpQrUri, setTotpQrUri] = useState<string | null>(null);
|
||||
const [totpCode, setTotpCode] = useState("");
|
||||
const [totpSubmitting, setTotpSubmitting] = useState(false);
|
||||
const [backupCodes, setBackupCodes] = useState<string[] | null>(null);
|
||||
const [disableCode, setDisableCode] = useState("");
|
||||
|
||||
useModalLock(show2FASetup)
|
||||
useModalLock(show2FADisable)
|
||||
useModalLock(show2FASetup);
|
||||
useModalLock(show2FADisable);
|
||||
|
||||
const fetchDashboard = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/dashboard`)
|
||||
const data = await response.json()
|
||||
const response = await apiFetch(`${API_BASE}/dashboard`);
|
||||
const data = await response.json();
|
||||
if (data.success !== false) {
|
||||
setDashData(data.data || data)
|
||||
setDashData(data.data || data);
|
||||
}
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('Dashboard fetch error:', err)
|
||||
console.error("Dashboard fetch error:", err);
|
||||
}
|
||||
} finally {
|
||||
setDashLoading(false)
|
||||
setDashLoading(false);
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboard()
|
||||
}, [fetchDashboard])
|
||||
fetchDashboard();
|
||||
}, [fetchDashboard]);
|
||||
|
||||
// 2FA status fetch
|
||||
const fetch2FAStatus = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/totp/setup`)
|
||||
const data = await response.json()
|
||||
const response = await apiFetch(`${API_BASE}/totp/setup`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setTotpEnabled(!!user?.totpEnabled)
|
||||
setTotpEnabled(!!user?.totpEnabled);
|
||||
}
|
||||
} catch {
|
||||
// 2FA status fetch failed silently
|
||||
setTotpEnabled(!!user?.totpEnabled)
|
||||
setTotpEnabled(!!user?.totpEnabled);
|
||||
} finally {
|
||||
setTotpLoading(false)
|
||||
setTotpLoading(false);
|
||||
}
|
||||
}, [user?.totpEnabled])
|
||||
}, [user?.totpEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch2FAStatus()
|
||||
}, [fetch2FAStatus])
|
||||
fetch2FAStatus();
|
||||
}, [fetch2FAStatus]);
|
||||
|
||||
// Punch (prichod/odchod) primo z dashboardu
|
||||
const handleQuickPunch = () => {
|
||||
const action = dashData?.my_shift?.has_ongoing ? 'departure' : 'arrival'
|
||||
setPunching(true)
|
||||
const action = dashData?.my_shift?.has_ongoing ? "departure" : "arrival";
|
||||
setPunching(true);
|
||||
|
||||
const submitPunch = async (gpsData: Record<string, unknown> = {}) => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/attendance`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ punch_action: action, ...gpsData })
|
||||
})
|
||||
const result = await response.json()
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ punch_action: action, ...gpsData }),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert.success(result.data?.message || 'Docházka zaznamenána')
|
||||
fetchDashboard()
|
||||
alert.success(result.data?.message || "Docházka zaznamenána");
|
||||
fetchDashboard();
|
||||
} else {
|
||||
alert.error(result.error || 'Chyba při záznamu docházky')
|
||||
alert.error(result.error || "Chyba při záznamu docházky");
|
||||
}
|
||||
} catch {
|
||||
alert.error('Chyba pripojeni')
|
||||
alert.error("Chyba pripojeni");
|
||||
} finally {
|
||||
setPunching(false)
|
||||
setPunching(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!navigator.geolocation) {
|
||||
submitPunch({})
|
||||
return
|
||||
submitPunch({});
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => {
|
||||
const { latitude, longitude, accuracy } = pos.coords
|
||||
submitPunch({ latitude, longitude, accuracy, address: '' })
|
||||
const { latitude, longitude, accuracy } = pos.coords;
|
||||
submitPunch({ latitude, longitude, accuracy, address: "" });
|
||||
},
|
||||
() => submitPunch({}),
|
||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }
|
||||
)
|
||||
}
|
||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 },
|
||||
);
|
||||
};
|
||||
|
||||
// 2FA handlery
|
||||
const handleStart2FASetup = async () => {
|
||||
setTotpSubmitting(true)
|
||||
setTotpSubmitting(true);
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/totp/setup`)
|
||||
const data = await response.json()
|
||||
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)
|
||||
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íč')
|
||||
alert.error(data.error || "Nepodařilo se vygenerovat 2FA klíč");
|
||||
}
|
||||
} catch {
|
||||
alert.error('Chyba připojení')
|
||||
alert.error("Chyba připojení");
|
||||
} finally {
|
||||
setTotpSubmitting(false)
|
||||
setTotpSubmitting(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm2FA = async () => {
|
||||
if (!totpCode.trim()) return
|
||||
setTotpSubmitting(true)
|
||||
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()
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ secret: totpSecret, code: totpCode.trim() }),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setTotpEnabled(true)
|
||||
setBackupCodes(data.data?.backup_codes || null)
|
||||
setTotpSecret(null)
|
||||
setTotpQrUri(null)
|
||||
updateUser({ totpEnabled: true })
|
||||
alert.success('2FA bylo aktivováno')
|
||||
setTotpEnabled(true);
|
||||
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('')
|
||||
alert.error(data.error || "Neplatný kód");
|
||||
setTotpCode("");
|
||||
}
|
||||
} catch {
|
||||
alert.error('Chyba připojení')
|
||||
alert.error("Chyba připojení");
|
||||
} finally {
|
||||
setTotpSubmitting(false)
|
||||
setTotpSubmitting(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisable2FA = async () => {
|
||||
if (!disableCode.trim()) return
|
||||
setTotpSubmitting(true)
|
||||
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()
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ code: disableCode.trim() }),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setTotpEnabled(false)
|
||||
setShow2FADisable(false)
|
||||
setDisableCode('')
|
||||
updateUser({ totpEnabled: false })
|
||||
alert.success('2FA bylo deaktivováno')
|
||||
setTotpEnabled(false);
|
||||
setShow2FADisable(false);
|
||||
setDisableCode("");
|
||||
updateUser({ totpEnabled: false });
|
||||
alert.success("2FA bylo deaktivováno");
|
||||
} else {
|
||||
alert.error(data.error || 'Neplatný kód')
|
||||
setDisableCode('')
|
||||
alert.error(data.error || "Neplatný kód");
|
||||
setDisableCode("");
|
||||
}
|
||||
} catch {
|
||||
alert.error('Chyba připojení')
|
||||
alert.error("Chyba připojení");
|
||||
} finally {
|
||||
setTotpSubmitting(false)
|
||||
setTotpSubmitting(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dash">
|
||||
@@ -223,29 +223,66 @@ export default function Dashboard() {
|
||||
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)' }}
|
||||
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' }}>
|
||||
<div
|
||||
className="admin-card-body"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: "1rem",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
<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" />
|
||||
<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
|
||||
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 }}>
|
||||
{totpSubmitting ? 'Generuji...' : 'Aktivovat 2FA nyní'}
|
||||
<button
|
||||
onClick={handleStart2FASetup}
|
||||
disabled={totpSubmitting}
|
||||
className="admin-btn admin-btn-primary"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{totpSubmitting ? "Generuji..." : "Aktivovat 2FA nyní"}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
@@ -253,36 +290,70 @@ export default function Dashboard() {
|
||||
|
||||
{/* Skeleton loading */}
|
||||
{dashLoading && (
|
||||
<div className="admin-skeleton" style={{ padding: 0, gap: '1.25rem' }}>
|
||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.25rem" }}>
|
||||
<div className="dash-kpi-grid dash-kpi-4">
|
||||
{[0, 1, 2, 3].map(i => (
|
||||
<div key={i} className="admin-skeleton-line h-24" style={{ borderRadius: '10px' }} />
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="admin-skeleton-line h-24"
|
||||
style={{ borderRadius: "10px" }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="dash-quick-actions">
|
||||
{[0, 1, 2, 3].map(i => (
|
||||
<div key={i} className="admin-skeleton-line" style={{ height: '52px', borderRadius: '10px' }} />
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="admin-skeleton-line"
|
||||
style={{ height: "52px", borderRadius: "10px" }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="dash-main-grid">
|
||||
<div className="admin-skeleton-line" style={{ height: '320px', borderRadius: '10px' }} />
|
||||
<div className="admin-skeleton-line" style={{ height: '320px', borderRadius: '10px' }} />
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||
<div className="admin-skeleton-line" style={{ height: '150px', borderRadius: '10px' }} />
|
||||
<div className="admin-skeleton-line" style={{ height: '150px', borderRadius: '10px' }} />
|
||||
<div
|
||||
className="admin-skeleton-line"
|
||||
style={{ height: "320px", borderRadius: "10px" }}
|
||||
/>
|
||||
<div
|
||||
className="admin-skeleton-line"
|
||||
style={{ height: "320px", borderRadius: "10px" }}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1.25rem",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="admin-skeleton-line"
|
||||
style={{ height: "150px", borderRadius: "10px" }}
|
||||
/>
|
||||
<div
|
||||
className="admin-skeleton-line"
|
||||
style={{ height: "150px", borderRadius: "10px" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dash-bottom">
|
||||
<div className="admin-skeleton-line" style={{ height: '200px', borderRadius: '10px' }} />
|
||||
<div className="admin-skeleton-line" style={{ height: '200px', borderRadius: '10px' }} />
|
||||
<div
|
||||
className="admin-skeleton-line"
|
||||
style={{ height: "200px", borderRadius: "10px" }}
|
||||
/>
|
||||
<div
|
||||
className="admin-skeleton-line"
|
||||
style={{ height: "200px", borderRadius: "10px" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* KPI cards — only show if user has any admin-level permissions */}
|
||||
{!dashLoading && (hasPermission('offers.view') || hasPermission('invoices.view') || hasPermission('projects.view') || hasPermission('orders.view')) && (
|
||||
<DashKpiCards dashData={dashData} />
|
||||
)}
|
||||
{!dashLoading &&
|
||||
(hasPermission("offers.view") ||
|
||||
hasPermission("invoices.view") ||
|
||||
hasPermission("projects.view") ||
|
||||
hasPermission("orders.view")) && <DashKpiCards dashData={dashData} />}
|
||||
|
||||
{/* Quick actions */}
|
||||
{!dashLoading && (
|
||||
@@ -294,87 +365,125 @@ export default function Dashboard() {
|
||||
)}
|
||||
|
||||
{/* Main content grid */}
|
||||
{!dashLoading && <motion.div
|
||||
className="dash-main-grid"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.12 }}
|
||||
>
|
||||
{hasPermission('settings.audit') && <DashActivityFeed activities={dashData?.recent_activity} />}
|
||||
{!dashLoading && (
|
||||
<motion.div
|
||||
className="dash-main-grid"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.25, delay: 0.12 }}
|
||||
>
|
||||
{hasPermission("settings.audit") && (
|
||||
<DashActivityFeed activities={dashData?.recent_activity} />
|
||||
)}
|
||||
|
||||
{hasPermission('attendance.admin') && <DashAttendanceToday attendance={dashData?.attendance} />}
|
||||
{hasPermission("attendance.admin") && (
|
||||
<DashAttendanceToday attendance={dashData?.attendance} />
|
||||
)}
|
||||
|
||||
{/* Pravy sloupec: projekty + nabidky */}
|
||||
<div className="dash-right-col">
|
||||
{dashData?.projects && (
|
||||
<div className="admin-card">
|
||||
<div className="admin-card-header flex-between">
|
||||
<h2 className="admin-card-title">Aktivní projekty</h2>
|
||||
<Link to="/projects" className="admin-btn admin-btn-primary admin-btn-sm">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>}
|
||||
{/* Pravy sloupec: projekty + nabidky */}
|
||||
<div className="dash-right-col">
|
||||
{dashData?.projects && (
|
||||
<div className="admin-card">
|
||||
<div className="admin-card-header flex-between">
|
||||
<h2 className="admin-card-title">Aktivní projekty</h2>
|
||||
<Link
|
||||
to="/projects"
|
||||
className="admin-btn admin-btn-primary admin-btn-sm"
|
||||
>
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{dashData?.offers && (
|
||||
<div className="admin-card">
|
||||
<div className="admin-card-header flex-between">
|
||||
<h2 className="admin-card-title">Nabídky</h2>
|
||||
<Link to="/offers" className="admin-btn admin-btn-primary admin-btn-sm">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>
|
||||
{dashData?.offers && (
|
||||
<div className="admin-card">
|
||||
<div className="admin-card-header flex-between">
|
||||
<h2 className="admin-card-title">Nabídky</h2>
|
||||
<Link
|
||||
to="/offers"
|
||||
className="admin-btn admin-btn-primary admin-btn-sm"
|
||||
>
|
||||
Zobrazit →
|
||||
</Link>
|
||||
</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>Prošlé</span>
|
||||
<span className="admin-badge admin-badge-warning">{dashData.offers.expired_count}</span>
|
||||
<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>Prošlé</span>
|
||||
<span className="admin-badge admin-badge-warning">
|
||||
{dashData.offers.expired_count}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>}
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Profile + Sessions */}
|
||||
{!dashLoading && <div className="dash-bottom">
|
||||
<DashProfile
|
||||
totpEnabled={totpEnabled}
|
||||
totpLoading={totpLoading}
|
||||
totpSubmitting={totpSubmitting}
|
||||
onStart2FASetup={handleStart2FASetup}
|
||||
onConfirm2FA={handleConfirm2FA}
|
||||
onDisable2FA={handleDisable2FA}
|
||||
totpSecret={totpSecret}
|
||||
totpQrUri={totpQrUri}
|
||||
totpCode={totpCode}
|
||||
setTotpCode={setTotpCode}
|
||||
backupCodes={backupCodes}
|
||||
setBackupCodes={setBackupCodes}
|
||||
show2FASetup={show2FASetup}
|
||||
setShow2FASetup={setShow2FASetup}
|
||||
show2FADisable={show2FADisable}
|
||||
setShow2FADisable={setShow2FADisable}
|
||||
disableCode={disableCode}
|
||||
setDisableCode={setDisableCode}
|
||||
/>
|
||||
<DashSessions />
|
||||
</div>}
|
||||
{!dashLoading && (
|
||||
<div className="dash-bottom">
|
||||
<DashProfile
|
||||
totpEnabled={totpEnabled}
|
||||
totpLoading={totpLoading}
|
||||
totpSubmitting={totpSubmitting}
|
||||
onStart2FASetup={handleStart2FASetup}
|
||||
onConfirm2FA={handleConfirm2FA}
|
||||
onDisable2FA={handleDisable2FA}
|
||||
totpSecret={totpSecret}
|
||||
totpQrUri={totpQrUri}
|
||||
totpCode={totpCode}
|
||||
setTotpCode={setTotpCode}
|
||||
backupCodes={backupCodes}
|
||||
setBackupCodes={setBackupCodes}
|
||||
show2FASetup={show2FASetup}
|
||||
setShow2FASetup={setShow2FASetup}
|
||||
show2FADisable={show2FADisable}
|
||||
setShow2FADisable={setShow2FADisable}
|
||||
disableCode={disableCode}
|
||||
setDisableCode={setDisableCode}
|
||||
/>
|
||||
<DashSessions />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user