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:
BOHA
2026-04-28 22:35:43 +02:00
parent 12289bdce3
commit ba95723b61
109 changed files with 26410 additions and 10159 deletions

View File

@@ -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 */}