chore(release): v2.4.35 — lint cleanup (68→0) + duplicate-Zavřít modal fix
UI fix:
- Close-only modals showing a redundant second close button now use
hideCancel: ReceivedInvoices paid-detail modal (was two identical "Zavřít"
buttons) and PlanCellModal "Den je součástí rozsahu".
- Add modal-duplicate-close test enforcing close-only modals set hideCancel.
Lint: cleared all 68 warnings → 0.
- preserve-caught-error: attach { cause } in ai.service / exchange-rates.
- no-require-imports: package.json version read via fs (APP_VERSION) instead
of require(), avoiding a rootDir-expanding static JSON import.
- react-hooks/exhaustive-deps (11): ref-in-cleanup copies, derived-value
useMemo wrapping, PlanGrid field extraction, stable nextKey useCallback,
AuthContext documented cycle-break.
- no-explicit-any (53): precise route param/Prisma types, generic enrich*()
preserving payload shape, minimal vite module type, frontend body/query-key
types, SystemInfo for Settings.
Refactor (test enablement): shift-form types moved to dependency-free
shiftFormTypes.ts so the print-HTML builders are unit-testable without the
component graph; characterization test pins their output.
Gates: 649 tests pass, tsc -b clean, lint 0. Verified touched flows live
via Playwright (PlanWork CRUD + optimistic cache, warehouse form keys,
Settings system info, invoice detail).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -210,7 +210,7 @@ export default function AttendanceHistory() {
|
||||
const { data, isPending } = useQuery(
|
||||
attendanceHistoryOptions({ month, userId: user?.id }),
|
||||
);
|
||||
const records = data ?? [];
|
||||
const records = useMemo(() => data ?? [], [data]);
|
||||
|
||||
const computed = useMemo(() => {
|
||||
const [yearStr, monthStr] = month.split("-");
|
||||
|
||||
@@ -621,7 +621,10 @@ export default function InvoiceDetail() {
|
||||
const customers = customersQuery.data ?? [];
|
||||
|
||||
const bankAccountsQuery = useQuery(bankAccountsOptions());
|
||||
const bankAccounts = bankAccountsQuery.data ?? [];
|
||||
const bankAccounts = useMemo(
|
||||
() => bankAccountsQuery.data ?? [],
|
||||
[bankAccountsQuery.data],
|
||||
);
|
||||
|
||||
const invoiceQuery = useQuery(invoiceDetailOptions(id));
|
||||
const invoice = invoiceQuery.data ?? null;
|
||||
@@ -656,8 +659,9 @@ export default function InvoiceDetail() {
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const blobTimeouts = blobTimeoutsRef.current;
|
||||
return () => {
|
||||
blobTimeoutsRef.current.forEach(clearTimeout);
|
||||
blobTimeouts.forEach(clearTimeout);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -121,8 +121,9 @@ export default function OrderDetail() {
|
||||
}, [orderQuery.error]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
useEffect(() => {
|
||||
const blobTimeouts = blobTimeoutsRef.current;
|
||||
return () => {
|
||||
blobTimeoutsRef.current.forEach(clearTimeout);
|
||||
blobTimeouts.forEach(clearTimeout);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
getCell,
|
||||
getCells,
|
||||
type ModalMode as HookModalMode,
|
||||
type PlanEntryBody,
|
||||
type PlanEntryPatchBody,
|
||||
type PlanOverrideBody,
|
||||
type PlanOverridePatchBody,
|
||||
} from "../hooks/usePlanWork";
|
||||
import type { PlanCellModalMode } from "../components/PlanCellModal";
|
||||
import PlanGrid from "../components/PlanGrid";
|
||||
@@ -182,7 +186,13 @@ export default function PlanWork() {
|
||||
if (bulkForm.user_ids.length === 0) return;
|
||||
setBulkSubmitting(true);
|
||||
try {
|
||||
const data: any = await bulkCreate.mutateAsync({
|
||||
// Counts can arrive either at the envelope root or nested under .data.
|
||||
type BulkSummary = {
|
||||
created_days?: number;
|
||||
skipped_days?: number;
|
||||
users?: number;
|
||||
};
|
||||
const data = (await bulkCreate.mutateAsync({
|
||||
user_ids: bulkForm.user_ids.map(Number),
|
||||
date_from: bulkForm.date_from,
|
||||
date_to: bulkForm.is_range ? bulkForm.date_to : bulkForm.date_from,
|
||||
@@ -190,12 +200,12 @@ export default function PlanWork() {
|
||||
project_id: bulkForm.project_id ? Number(bulkForm.project_id) : null,
|
||||
category: bulkForm.category,
|
||||
note: bulkForm.note,
|
||||
});
|
||||
})) as BulkSummary & { data?: BulkSummary };
|
||||
// apiCall returns the full envelope { success, data, message };
|
||||
// the summary counts are under .data
|
||||
const summary = data?.data ?? data;
|
||||
const summary: BulkSummary = data?.data ?? data;
|
||||
const skipped =
|
||||
summary?.skipped_days > 0
|
||||
(summary?.skipped_days ?? 0) > 0
|
||||
? ` ${summary.skipped_days} dní přeskočeno (limit 3 na den).`
|
||||
: "";
|
||||
alert.success(
|
||||
@@ -392,7 +402,7 @@ export default function PlanWork() {
|
||||
// optimistic cache patch the hook already applied.
|
||||
|
||||
const saveEntry = useCallback(
|
||||
async (body: any) => {
|
||||
async (body: PlanEntryBody) => {
|
||||
try {
|
||||
await createEntry.mutateAsync(body);
|
||||
// Pulse the first day of the new entry — for multi-day entries
|
||||
@@ -409,7 +419,7 @@ export default function PlanWork() {
|
||||
);
|
||||
|
||||
const updateEntryFn = useCallback(
|
||||
async (id: number, body: any) => {
|
||||
async (id: number, body: PlanEntryPatchBody) => {
|
||||
// Find the user for this entry so we know which cell to pulse /
|
||||
// which cache to roll back. The hook's optimistic patch already
|
||||
// discovered this; we read it from the current grid.
|
||||
@@ -473,7 +483,7 @@ export default function PlanWork() {
|
||||
);
|
||||
|
||||
const saveOverride = useCallback(
|
||||
async (body: any) => {
|
||||
async (body: PlanOverrideBody) => {
|
||||
try {
|
||||
await createOverride.mutateAsync(body);
|
||||
firePulse(body.user_id, body.shift_date);
|
||||
@@ -488,7 +498,7 @@ export default function PlanWork() {
|
||||
);
|
||||
|
||||
const updateOverrideFn = useCallback(
|
||||
async (id: number, body: any) => {
|
||||
async (id: number, body: PlanOverridePatchBody) => {
|
||||
// Find the user/date for the pulse + rollback.
|
||||
let userId: number | null = null;
|
||||
let date: string | null = null;
|
||||
|
||||
@@ -362,8 +362,9 @@ export default function ReceivedInvoices({
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const blobTimeouts = blobTimeoutsRef.current;
|
||||
return () => {
|
||||
blobTimeoutsRef.current.forEach(clearTimeout);
|
||||
blobTimeouts.forEach(clearTimeout);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -1149,11 +1150,9 @@ export default function ReceivedInvoices({
|
||||
? "Zavřít"
|
||||
: "Uložit"
|
||||
}
|
||||
cancelText={
|
||||
editInvoice && editInvoice._originalStatus === "paid"
|
||||
? "Zavřít"
|
||||
: "Zrušit"
|
||||
}
|
||||
// Read-only (paid) detail has only a close action — a single "Zavřít"
|
||||
// button, not a redundant cancel button beside it.
|
||||
hideCancel={editInvoice?._originalStatus === "paid"}
|
||||
loading={saving}
|
||||
>
|
||||
{editInvoice &&
|
||||
|
||||
@@ -715,7 +715,25 @@ export default function Settings() {
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
const si = systemInfo as Record<string, any>;
|
||||
type NasInfo = { configured?: boolean; path?: string | null };
|
||||
type SystemInfo = {
|
||||
app_version?: string;
|
||||
node_version?: string;
|
||||
platform?: string;
|
||||
uptime?: string;
|
||||
environment?: string;
|
||||
timezone?: string;
|
||||
memory?: {
|
||||
rss?: string;
|
||||
heap_used?: string;
|
||||
heap_total?: string;
|
||||
system_free?: string;
|
||||
system_total?: string;
|
||||
};
|
||||
database?: { status?: string; migrations_applied?: number | string };
|
||||
nas?: Record<string, NasInfo | undefined>;
|
||||
};
|
||||
const si = systemInfo as SystemInfo;
|
||||
const Row = ({
|
||||
label,
|
||||
children,
|
||||
@@ -800,7 +818,7 @@ export default function Settings() {
|
||||
["Faktury", si.nas?.invoices],
|
||||
["Objednávky", si.nas?.orders],
|
||||
["Nabídky", si.nas?.offers],
|
||||
] as [string, Record<string, any>][]
|
||||
] as [string, NasInfo | undefined][]
|
||||
).map(([label, info]) => (
|
||||
<Row key={label} label={label}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef, useId } from "react";
|
||||
import { useState, useEffect, useRef, useId, useCallback } from "react";
|
||||
import {
|
||||
useParams,
|
||||
useNavigate,
|
||||
@@ -174,7 +174,10 @@ export default function WarehouseIssueForm() {
|
||||
|
||||
const baseId = useId();
|
||||
const counterRef = useRef(0);
|
||||
const nextKey = () => `${baseId}-item-${counterRef.current++}`;
|
||||
const nextKey = useCallback(
|
||||
() => `${baseId}-item-${counterRef.current++}`,
|
||||
[baseId],
|
||||
);
|
||||
|
||||
// Pre-fill data from reservation ("Create výdej" button)
|
||||
const prefill = location.state as {
|
||||
@@ -262,7 +265,7 @@ export default function WarehouseIssueForm() {
|
||||
}
|
||||
formInitialized.current = true;
|
||||
}
|
||||
}, [isEdit, issue]);
|
||||
}, [isEdit, issue, nextKey]);
|
||||
|
||||
const saveMutation = useApiMutation<
|
||||
ReturnType<typeof buildPayload>,
|
||||
|
||||
@@ -126,7 +126,10 @@ export default function WarehouseReceiptForm() {
|
||||
|
||||
const baseId = useId();
|
||||
const counterRef = useRef(0);
|
||||
const nextKey = () => `${baseId}-item-${counterRef.current++}`;
|
||||
const nextKey = useCallback(
|
||||
() => `${baseId}-item-${counterRef.current++}`,
|
||||
[baseId],
|
||||
);
|
||||
|
||||
const [form, setForm] = useState<ReceiptForm>({
|
||||
supplier_id: null,
|
||||
@@ -198,7 +201,7 @@ export default function WarehouseReceiptForm() {
|
||||
}
|
||||
formInitialized.current = true;
|
||||
}
|
||||
}, [isEdit, receipt]);
|
||||
}, [isEdit, receipt, nextKey]);
|
||||
|
||||
const saveMutation = useApiMutation<
|
||||
ReturnType<typeof buildPayload>,
|
||||
|
||||
Reference in New Issue
Block a user