v1.5.6: boneyard-js skeleton migration, TanStack Query refactor, rate-limit config
- Replace hand-coded skeleton CSS/JSX with boneyard-js auto-generated bones - Remove skeleton.css and @keyframes shimmer from base.css - Add <Skeleton> wrappers with fixtures to all 25+ page components - Generate 20 bone captures via boneyard CLI (CDP auth-gated capture) - Refactor data fetching from useEffect+useState to TanStack Query - Extract query hooks into src/admin/lib/queries/ and apiAdapter - Add usePaginatedQuery hook replacing useApiCall/useListData - Fix parseFloat || 0 anti-pattern in OfferDetail and OffersTemplates inputs - Fix customer_id mandatory validation on offer creation - Fix leave-requests comma-separated status filter (Prisma enum in: []) - Add cross-entity cache invalidation for orders/offers/invoices/projects - Make rate limits configurable via env vars (RATE_LIMIT_MAX, RATE_LIMIT_REFRESH, etc.) - Add boneyard.config.json with routes and breakpoints Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
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 DashKpiCards from "../components/dashboard/DashKpiCards";
|
||||
import DashQuickActions from "../components/dashboard/DashQuickActions";
|
||||
@@ -12,6 +15,8 @@ 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 { Skeleton } from "boneyard-js/react";
|
||||
import DashboardFixture from "../fixtures/DashboardFixture";
|
||||
|
||||
const API_BASE = "/api/admin";
|
||||
|
||||
@@ -69,13 +74,17 @@ export default function Dashboard() {
|
||||
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 queryClient = useQueryClient();
|
||||
const { data: dashDataRaw, isPending: dashLoading } =
|
||||
useQuery(dashboardOptions());
|
||||
const dashData = dashDataRaw as DashData | undefined;
|
||||
const { data: totpData, isPending: totpLoading } =
|
||||
useQuery(require2FAOptions());
|
||||
const totpEnabled = totpData?.require_2fa ?? !!user?.totpEnabled;
|
||||
|
||||
// 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);
|
||||
@@ -88,46 +97,6 @@ export default function Dashboard() {
|
||||
useModalLock(show2FASetup);
|
||||
useModalLock(show2FADisable);
|
||||
|
||||
const fetchDashboard = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/dashboard`);
|
||||
const data = await response.json();
|
||||
if (data.success !== false) {
|
||||
setDashData(data.data || data);
|
||||
}
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.error("Dashboard fetch error:", err);
|
||||
}
|
||||
} finally {
|
||||
setDashLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboard();
|
||||
}, [fetchDashboard]);
|
||||
|
||||
// 2FA status fetch
|
||||
const fetch2FAStatus = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE}/totp/setup`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setTotpEnabled(!!user?.totpEnabled);
|
||||
}
|
||||
} catch {
|
||||
// 2FA status fetch failed silently
|
||||
setTotpEnabled(!!user?.totpEnabled);
|
||||
} finally {
|
||||
setTotpLoading(false);
|
||||
}
|
||||
}, [user?.totpEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch2FAStatus();
|
||||
}, [fetch2FAStatus]);
|
||||
|
||||
// Punch (prichod/odchod) primo z dashboardu
|
||||
const handleQuickPunch = useCallback(() => {
|
||||
const action = dashData?.my_shift?.has_ongoing ? "departure" : "arrival";
|
||||
@@ -143,7 +112,7 @@ export default function Dashboard() {
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert.success(result.data?.message || "Docházka zaznamenána");
|
||||
fetchDashboard();
|
||||
queryClient.invalidateQueries({ queryKey: ["dashboard"] });
|
||||
} else {
|
||||
alert.error(result.error || "Chyba při záznamu docházky");
|
||||
}
|
||||
@@ -167,7 +136,7 @@ export default function Dashboard() {
|
||||
() => submitPunch({}),
|
||||
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 },
|
||||
);
|
||||
}, [dashData, alert, fetchDashboard]);
|
||||
}, [dashData, alert, queryClient]);
|
||||
|
||||
// 2FA handlery
|
||||
const handleStart2FASetup = async () => {
|
||||
@@ -202,7 +171,7 @@ export default function Dashboard() {
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setTotpEnabled(true);
|
||||
queryClient.invalidateQueries({ queryKey: ["settings", "2fa"] });
|
||||
setBackupCodes(data.data?.backup_codes || null);
|
||||
setTotpSecret(null);
|
||||
setTotpQrUri(null);
|
||||
@@ -230,7 +199,7 @@ export default function Dashboard() {
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setTotpEnabled(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["settings", "2fa"] });
|
||||
setShow2FADisable(false);
|
||||
setDisableCode("");
|
||||
updateUser({ totpEnabled: false });
|
||||
@@ -337,62 +306,13 @@ export default function Dashboard() {
|
||||
|
||||
{/* Skeleton loading */}
|
||||
{dashLoading && (
|
||||
<div className="admin-skeleton" style={{ padding: 0, gap: "1.25rem" }}>
|
||||
<div className="admin-kpi-grid admin-kpi-4">
|
||||
{[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" }}
|
||||
/>
|
||||
))}
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
<Skeleton
|
||||
name="dashboard"
|
||||
loading={dashLoading}
|
||||
fixture={<DashboardFixture />}
|
||||
>
|
||||
<div />
|
||||
</Skeleton>
|
||||
)}
|
||||
|
||||
{/* KPI cards — only show if user has any admin-level permissions */}
|
||||
|
||||
Reference in New Issue
Block a user