v1.8.0: warehouse module (16 commits), docházka mzda model, leave_type=holiday removal, api.ts race fix

Highlights:
- Warehouse module: receipts, issues, reservations, inventory, reports, dashboard,
  master data (categories, suppliers, locations), FIFO service, integration tests
- Docházka: mzda PDF counting model (Odpracováno / Vč. svátků / Přesčas / Svátek /
  So/Ne / Noc) with Czech weekday names and decimal hours
- AttendanceAdmin/AttendanceHistory KPI cards unified to mzda formula with
  fund bar colored by delta, badges for Práce/Dov/Nem/Sv/Nep
- Remove leave_type=holiday entirely (auto-computed from Czech public holidays)
- Allow multiple work shifts per day (overlap detection only)
- Pre-flight refresh in api.ts eliminates spurious 401s on fresh page loads
- Prisma: company_settings gets 6 nullable columns for warehouse numbering
  (PRI/VYD/INV prefixes, default patterns); migration seeds defaults
This commit is contained in:
BOHA
2026-06-03 23:13:10 +02:00
parent dac45baaa8
commit 5233db2002
149 changed files with 11132 additions and 26950 deletions

View File

@@ -1,16 +1,16 @@
import { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";
import { motion } from "framer-motion";
import { useAuth } from "../context/AuthContext";
import { useAlert } from "../context/AlertContext";
import Forbidden from "../components/Forbidden";
import Pagination from "../components/Pagination";
import FormField from "../components/FormField";
import FormModal from "../components/FormModal";
import AdminDatePicker from "../components/AdminDatePicker";
import { czechPlural } from "../utils/formatters";
import apiFetch from "../utils/api";
import { Skeleton } from "boneyard-js/react";
import AuditLogFixture from "../fixtures/AuditLogFixture";
import { useApiMutation } from "../lib/queries/mutations";
const API_BASE = "/api/admin";
@@ -91,7 +91,6 @@ interface Filters {
export default function AuditLog() {
const { hasPermission } = useAuth();
const alert = useAlert();
const queryClient = useQueryClient();
const [filters, setFilters] = useState<Filters>({
search: "",
action: "",
@@ -103,7 +102,6 @@ export default function AuditLog() {
const [perPage, setPerPage] = useState(50);
const [showCleanup, setShowCleanup] = useState(false);
const [cleanupDays, setCleanupDays] = useState(90);
const [cleaning, setCleaning] = useState(false);
const { data: logsData, isPending } = useQuery({
queryKey: [
@@ -148,13 +146,26 @@ export default function AuditLog() {
},
});
const logs = logsData?.data ?? [];
const logs: AuditLogEntry[] = logsData?.data ?? [];
const pagination = logsData?.pagination ?? null;
if (!hasPermission("settings.audit")) {
return <Forbidden />;
}
const cleanupMutation = useApiMutation<
{ days: number; confirm?: string },
{ message?: string; error?: string }
>({
url: () => `${API_BASE}/audit-log/cleanup`,
method: () => "POST",
invalidate: ["audit-log"],
onSuccess: (data) => {
alert.success(data?.message || "Hotovo");
setShowCleanup(false);
},
});
const handleFilterChange = (key: keyof Filters, value: string) => {
setFilters((prev) => ({ ...prev, [key]: value }));
setPage(1);
@@ -170,25 +181,14 @@ export default function AuditLog() {
};
const handleCleanup = async () => {
setCleaning(true);
try {
const response = await apiFetch(`${API_BASE}/audit-log/cleanup`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ days: cleanupDays }),
// Backend requires this literal confirmation token when wiping everything.
await cleanupMutation.mutateAsync({
days: cleanupDays,
...(cleanupDays === 0 ? { confirm: "DELETE_ALL_AUDIT" } : {}),
});
const data = await response.json();
if (data.success) {
alert.success(data.message);
setShowCleanup(false);
queryClient.invalidateQueries({ queryKey: ["audit-log"] });
} else {
alert.error(data.error);
}
} catch {
alert.error("Chyba připojení");
} finally {
setCleaning(false);
} catch (e) {
alert.error(e instanceof Error ? e.message : "Chyba připojení");
}
};
@@ -199,13 +199,9 @@ export default function AuditLog() {
if (isPending && logs.length === 0) {
return (
<Skeleton
name="audit-log"
loading={isPending && logs.length === 0}
fixture={<AuditLogFixture />}
>
<div />
</Skeleton>
<div className="admin-loading">
<div className="admin-spinner" />
</div>
);
}
@@ -245,78 +241,36 @@ export default function AuditLog() {
</button>
</motion.div>
{showCleanup && (
<div className="admin-modal-overlay" style={{ opacity: 1 }}>
<div
className="admin-modal-backdrop"
onClick={() => !cleaning && setShowCleanup(false)}
/>
<motion.div
className="admin-modal admin-confirm-modal"
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ duration: 0.2 }}
<FormModal
isOpen={showCleanup}
onClose={() => !cleanupMutation.isPending && setShowCleanup(false)}
onSubmit={handleCleanup}
title="Vyčistit audit log"
submitLabel="Smazat"
loading={cleanupMutation.isPending}
>
<p className="admin-confirm-message">Smazat záznamy starší než:</p>
<div style={{ margin: "0.75rem 0", maxWidth: "200px" }}>
<select
className="admin-form-select"
value={cleanupDays}
onChange={(e) => setCleanupDays(parseInt(e.target.value))}
>
<div className="admin-modal-body admin-confirm-content">
<div className="admin-confirm-icon admin-confirm-icon-danger">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="3 6 5 6 21 6" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>
</div>
<h2 className="admin-confirm-title">Vyčistit audit log</h2>
<p className="admin-confirm-message">
Smazat záznamy starší než:
</p>
<div style={{ margin: "0.75rem auto", maxWidth: "200px" }}>
<select
className="admin-form-select"
value={cleanupDays}
onChange={(e) => setCleanupDays(parseInt(e.target.value))}
>
<option value={30}>30 dní</option>
<option value={60}>60 dní</option>
<option value={90}>90 dní</option>
<option value={180}>180 dní</option>
<option value={365}>1 rok</option>
<option value={0}>Vše</option>
</select>
</div>
<p
className="admin-confirm-message"
style={{ fontSize: "12px", opacity: 0.6 }}
>
Tato akce je nevratná.
</p>
</div>
<div className="admin-modal-footer">
<button
type="button"
onClick={() => setShowCleanup(false)}
className="admin-btn admin-btn-secondary"
disabled={cleaning}
>
Zrušit
</button>
<button
type="button"
onClick={handleCleanup}
className="admin-btn admin-btn-primary"
disabled={cleaning}
>
{cleaning ? "Mažu..." : "Smazat"}
</button>
</div>
</motion.div>
<option value={30}>30 dní</option>
<option value={60}>60 dní</option>
<option value={90}>90 dní</option>
<option value={180}>180 dní</option>
<option value={365}>1 rok</option>
<option value={0}>Vše</option>
</select>
</div>
)}
<p
className="admin-confirm-message"
style={{ fontSize: "12px", opacity: 0.6 }}
>
Tato akce je nevratná.
</p>
</FormModal>
<motion.div
className="admin-card mb-4"
@@ -391,90 +345,11 @@ export default function AuditLog() {
>
<div className="admin-card-body">
<div className="admin-table-responsive">
<Skeleton
name="audit-log-rows"
loading={isPending}
fixture={
<table className="admin-table">
<thead>
<tr>
<th>Čas</th>
<th>Uživatel</th>
<th>Akce</th>
<th>Typ entity</th>
<th>Popis</th>
<th>IP</th>
</tr>
</thead>
<tbody>
{Array.from({ length: 10 }, (_, i) => (
<tr key={i}>
<td>
<div
style={{
width: 110,
height: 14,
background: "var(--bg-tertiary)",
borderRadius: 4,
}}
/>
</td>
<td>
<div
style={{
width: 80,
height: 14,
background: "var(--bg-tertiary)",
borderRadius: 4,
}}
/>
</td>
<td>
<div
style={{
width: 70,
height: 22,
background: "var(--bg-tertiary)",
borderRadius: 10,
}}
/>
</td>
<td>
<div
style={{
width: 80,
height: 14,
background: "var(--bg-tertiary)",
borderRadius: 4,
}}
/>
</td>
<td>
<div
style={{
width: "100%",
height: 14,
background: "var(--bg-tertiary)",
borderRadius: 4,
}}
/>
</td>
<td>
<div
style={{
width: 90,
height: 14,
background: "var(--bg-tertiary)",
borderRadius: 4,
}}
/>
</td>
</tr>
))}
</tbody>
</table>
}
>
{isPending ? (
<div className="admin-loading">
<div className="admin-spinner" />
</div>
) : (
<table className="admin-table">
<thead>
<tr>
@@ -536,7 +411,7 @@ export default function AuditLog() {
))}
</tbody>
</table>
</Skeleton>
)}
</div>
<Pagination