fix: 2026-06-09 full-codebase audit hardening
Validation: shared NaN-guarded Zod coercion helpers in schemas/common.ts replace the raw number|string transform idiom across every schema (the root-cause NaN bug class); emailOrEmpty + lenient isoDateString/timeString. Security: roles privilege-escalation closed; refresh-token family revocation on reuse; TOTP uses config params; read endpoints permission-guarded; received-invoices gross VAT on all paths; orders-pdf custom-items authz. Concurrency: $queryRaw SELECT...FOR UPDATE locks in ascending-id order (warehouse confirm/cancel, attendance lockUserRow); uniqueness checks moved into create transactions (TOCTOU -> 409); deterministic id tiebreak on second-precision timestamp ordering (plan resolveCell/resolveGrid, warehouse FIFO). Frontend: Rules-of-Hooks fixed across ~14 pages + PlanCellModal; UTC-date persisted fields; dashboard invalidation gaps; stale-closure confirm bugs. Tooling/tests: ESLint flat config (react-hooks/rules-of-hooks = error) + Prettier; tsconfig.test.json so tsc -b type-checks the tests; removed 3 dead deps; npm audit fix (8 -> 3). Suite 195 -> 247 (happy-path auth, FIFO oldest-first, flakiness fixes), isolated on app_test via .env.test with a hard-throw setup guard. Gates: tsc 0 | build 0 | vitest 247/247 | eslint 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,11 +8,14 @@ import {
|
||||
warehouseStockStatusOptions,
|
||||
warehouseBelowMinimumOptions,
|
||||
warehouseCategoryListOptions,
|
||||
warehouseMovementLogOptions,
|
||||
type WarehouseItem,
|
||||
type MovementLogRow,
|
||||
} from "../lib/queries/warehouse";
|
||||
import apiFetch from "../utils/api";
|
||||
import { formatCurrency } from "../utils/formatters";
|
||||
import { jsonQuery } from "../lib/apiAdapter";
|
||||
import { formatCurrency, formatDate } from "../utils/formatters";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
DataTable,
|
||||
@@ -43,16 +46,6 @@ interface ProjectConsumptionRow {
|
||||
total_value: number;
|
||||
}
|
||||
|
||||
interface MovementLogRow {
|
||||
date: string;
|
||||
type: string;
|
||||
document_number: string;
|
||||
item_name: string;
|
||||
quantity: number;
|
||||
unit_price: number;
|
||||
supplier_or_project: string;
|
||||
}
|
||||
|
||||
const TABS: TabDef[] = [
|
||||
{ value: "stock-status", label: "Stav zásob" },
|
||||
{ value: "project-consumption", label: "Spotřeba projektů" },
|
||||
@@ -260,34 +253,36 @@ function ProjectConsumptionTab({
|
||||
name: string;
|
||||
}[]) ?? [];
|
||||
|
||||
const [data, setData] = useState<ProjectConsumptionRow[] | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fetched, setFetched] = useState(false);
|
||||
// Filters applied on "Zobrazit"; the query key carries them so results land in
|
||||
// the ["warehouse"] cache and refresh on warehouse mutations.
|
||||
const [applied, setApplied] = useState<{
|
||||
projectId: number | "";
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
} | null>(null);
|
||||
|
||||
const handleFetch = async () => {
|
||||
setLoading(true);
|
||||
setFetched(true);
|
||||
try {
|
||||
const {
|
||||
data,
|
||||
isFetching: loading,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ["warehouse", "project-consumption", applied],
|
||||
enabled: applied !== null,
|
||||
queryFn: () => {
|
||||
const params = new URLSearchParams();
|
||||
if (projectId) params.set("project_id", String(projectId));
|
||||
if (dateFrom) params.set("date_from", dateFrom);
|
||||
if (dateTo) params.set("date_to", dateTo);
|
||||
if (applied?.projectId)
|
||||
params.set("project_id", String(applied.projectId));
|
||||
if (applied?.dateFrom) params.set("date_from", applied.dateFrom);
|
||||
if (applied?.dateTo) params.set("date_to", applied.dateTo);
|
||||
const qs = params.toString();
|
||||
const response = await apiFetch(
|
||||
return jsonQuery<ProjectConsumptionRow[]>(
|
||||
`/api/admin/warehouse/reports/project-consumption${qs ? `?${qs}` : ""}`,
|
||||
);
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
setData(result.data ?? []);
|
||||
} else {
|
||||
setData([]);
|
||||
}
|
||||
} catch {
|
||||
setData([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const fetched = applied !== null;
|
||||
const handleFetch = () => setApplied({ projectId, dateFrom, dateTo });
|
||||
|
||||
const columns: DataColumn<ProjectConsumptionRow & { _idx: number }>[] = [
|
||||
{
|
||||
@@ -352,7 +347,15 @@ function ProjectConsumptionTab({
|
||||
|
||||
{fetched && loading && <LoadingState />}
|
||||
|
||||
{fetched && !loading && data && (
|
||||
{fetched && !loading && error && (
|
||||
<Alert severity="error">
|
||||
{error instanceof Error
|
||||
? error.message
|
||||
: "Nepodařilo se načíst spotřebu projektů"}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{fetched && !loading && !error && data && (
|
||||
<Card>
|
||||
<DataTable<ProjectConsumptionRow & { _idx: number }>
|
||||
columns={columns}
|
||||
@@ -383,34 +386,29 @@ function MovementLogTab({
|
||||
dateTo: string;
|
||||
setDateTo: (v: string) => void;
|
||||
}) {
|
||||
const [data, setData] = useState<MovementLogRow[] | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fetched, setFetched] = useState(false);
|
||||
// Filters applied on "Zobrazit"; uses the shared movement-log query option so
|
||||
// results land in the ["warehouse"] cache and refresh on warehouse mutations.
|
||||
const [applied, setApplied] = useState<{
|
||||
type: string;
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
} | null>(null);
|
||||
|
||||
const handleFetch = async () => {
|
||||
setLoading(true);
|
||||
setFetched(true);
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (type) params.set("type", type);
|
||||
if (dateFrom) params.set("date_from", dateFrom);
|
||||
if (dateTo) params.set("date_to", dateTo);
|
||||
const qs = params.toString();
|
||||
const response = await apiFetch(
|
||||
`/api/admin/warehouse/reports/movement-log${qs ? `?${qs}` : ""}`,
|
||||
);
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
setData(result.data ?? []);
|
||||
} else {
|
||||
setData([]);
|
||||
}
|
||||
} catch {
|
||||
setData([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const {
|
||||
data,
|
||||
isFetching: loading,
|
||||
error,
|
||||
} = useQuery({
|
||||
...warehouseMovementLogOptions({
|
||||
type: applied?.type || undefined,
|
||||
date_from: applied?.dateFrom || undefined,
|
||||
date_to: applied?.dateTo || undefined,
|
||||
}),
|
||||
enabled: applied !== null,
|
||||
});
|
||||
|
||||
const fetched = applied !== null;
|
||||
const handleFetch = () => setApplied({ type, dateFrom, dateTo });
|
||||
|
||||
const TYPE_LABEL: Record<string, string> = {
|
||||
receipt: "Příjem",
|
||||
@@ -423,7 +421,7 @@ function MovementLogTab({
|
||||
header: "Datum",
|
||||
width: "12%",
|
||||
mono: true,
|
||||
render: (row) => row.date,
|
||||
render: (row) => formatDate(row.date),
|
||||
},
|
||||
{
|
||||
key: "type",
|
||||
@@ -501,7 +499,15 @@ function MovementLogTab({
|
||||
|
||||
{fetched && loading && <LoadingState />}
|
||||
|
||||
{fetched && !loading && data && (
|
||||
{fetched && !loading && error && (
|
||||
<Alert severity="error">
|
||||
{error instanceof Error
|
||||
? error.message
|
||||
: "Nepodařilo se načíst pohyby"}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{fetched && !loading && !error && data && (
|
||||
<Card>
|
||||
<DataTable<MovementLogRow & { _idx: number }>
|
||||
columns={columns}
|
||||
|
||||
Reference in New Issue
Block a user