refactor: route/schema consistency (parseId, broad invalidation, Czech messages, Zod 4 strictObject)

- OfferDetail: invalidate broad ["offers"] not ["offers","list"] (CLAUDE.md convention)
- trips.ts/sessions.ts: use parseId helper (validated 400) instead of raw parseInt for route ids
- audit-log.ts: z.object().strict() -> z.strictObject() (Zod 4 idiom)
- attendance.ts, orders/offers schemas: English user-facing strings -> Czech
- project-files.ts: unify 'not found' phrasing to the codebase-dominant 'nenalezen' form

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-06 02:29:04 +02:00
parent 522afefc80
commit 9ae49c0d6a
8 changed files with 43 additions and 45 deletions

View File

@@ -678,7 +678,7 @@ export default function OfferDetail() {
}
}
if (!isEdit && result.data?.id) {
queryClient.invalidateQueries({ queryKey: ["offers", "list"] });
queryClient.invalidateQueries({ queryKey: ["offers"] });
queryClient.invalidateQueries({ queryKey: ["orders"] });
queryClient.invalidateQueries({ queryKey: ["projects"] });
queryClient.invalidateQueries({ queryKey: ["invoices"] });
@@ -690,7 +690,7 @@ export default function OfferDetail() {
items,
sections,
});
queryClient.invalidateQueries({ queryKey: ["offers", "list"] });
queryClient.invalidateQueries({ queryKey: ["offers"] });
queryClient.invalidateQueries({ queryKey: ["orders"] });
queryClient.invalidateQueries({ queryKey: ["projects"] });
queryClient.invalidateQueries({ queryKey: ["invoices"] });
@@ -736,7 +736,7 @@ export default function OfferDetail() {
if (result.success) {
setShowOrderModal(false);
alert.success(result.message || "Objednávka byla vytvořena");
await queryClient.invalidateQueries({ queryKey: ["offers", "list"] });
await queryClient.invalidateQueries({ queryKey: ["offers"] });
await queryClient.invalidateQueries({ queryKey: ["orders"] });
await queryClient.invalidateQueries({ queryKey: ["projects"] });
await queryClient.invalidateQueries({ queryKey: ["invoices"] });
@@ -762,7 +762,7 @@ export default function OfferDetail() {
setInvalidateConfirm(false);
setOfferStatus("invalidated");
alert.success(result.message || "Nabídka byla zneplatněna");
queryClient.invalidateQueries({ queryKey: ["offers", "list"] });
queryClient.invalidateQueries({ queryKey: ["offers"] });
queryClient.invalidateQueries({ queryKey: ["orders"] });
queryClient.invalidateQueries({ queryKey: ["projects"] });
queryClient.invalidateQueries({ queryKey: ["invoices"] });
@@ -789,7 +789,7 @@ export default function OfferDetail() {
deletingRef.current = true;
queryClient.removeQueries({ queryKey: ["offers", id] });
alert.success(result.message || "Nabídka byla smazána");
await queryClient.invalidateQueries({ queryKey: ["offers", "list"] });
await queryClient.invalidateQueries({ queryKey: ["offers"] });
await queryClient.invalidateQueries({ queryKey: ["orders"] });
await queryClient.invalidateQueries({ queryKey: ["projects"] });
await queryClient.invalidateQueries({ queryKey: ["invoices"] });

View File

@@ -185,7 +185,7 @@ export default async function attendanceRoutes(
return error(reply, "Nedostatečná oprávnění", 403);
}
const attendanceId = Number(query.attendance_id);
if (!attendanceId) return error(reply, "Missing attendance_id", 400);
if (!attendanceId) return error(reply, "Chybí attendance_id", 400);
const data = await attendanceService.getProjectLogs(attendanceId);
return reply.send({ success: true, data });
}
@@ -193,7 +193,7 @@ export default async function attendanceRoutes(
// --- action=location: single record with GPS data ---
if (action === "location") {
const id = Number(query.id);
if (!id) return error(reply, "Missing id", 400);
if (!id) return error(reply, "Chybí id záznamu", 400);
const record = await attendanceService.getLocationRecord(id);
if (!record) return error(reply, "Záznam nenalezen", 404);
const isAdmin = authData.permissions.includes("attendance.manage");

View File

@@ -12,12 +12,10 @@ import { parseBody } from "../../schemas/common";
// forensic history.
const DELETE_ALL_CONFIRM = "DELETE_ALL_AUDIT" as const;
const AuditCleanupSchema = z
.object({
days: z.number().int().nonnegative().optional(),
confirm: z.string().optional(),
})
.strict();
const AuditCleanupSchema = z.strictObject({
days: z.number().int().nonnegative().optional(),
confirm: z.string().optional(),
});
export default async function auditLogRoutes(
fastify: FastifyInstance,

View File

@@ -58,7 +58,7 @@ export default async function projectFilesRoutes(
const { project_id: projectIdStr, path: subPath = "" } = parsedQuery.data;
const projectId = Number(projectIdStr);
const project = await getProjectForFiles(projectId);
if (!project) return error(reply, "Projekt nebyl nalezen", 404);
if (!project) return error(reply, "Projekt nenalezen", 404);
if (!fm.isConfigured()) {
return error(reply, "Souborový systém není nakonfigurován", 500);
@@ -70,7 +70,7 @@ export default async function projectFilesRoutes(
return error(reply, "Projekt nemá číslo projektu");
const result = fm.downloadFile(project.project_number, subPath);
if (!result) return error(reply, "Soubor nebyl nalezen", 404);
if (!result) return error(reply, "Soubor nenalezen", 404);
const stream = fs.createReadStream(result.filePath);
const encodedName = encodeURIComponent(result.fileName);
@@ -89,7 +89,7 @@ export default async function projectFilesRoutes(
const result = fm.listFiles(project.project_number, subPath);
if (result === null) {
return error(reply, "Složka nebyla nalezena", 404);
return error(reply, "Složka nenalezena", 404);
}
return success(reply, {
@@ -109,7 +109,7 @@ export default async function projectFilesRoutes(
if ("error" in parsedQuery) return error(reply, parsedQuery.error, 400);
const projectId = Number(parsedQuery.data.project_id);
const project = await getProjectForFiles(projectId);
if (!project) return error(reply, "Projekt nebyl nalezen", 404);
if (!project) return error(reply, "Projekt nenalezen", 404);
if (!project.project_number)
return error(reply, "Projekt nemá číslo projektu");
@@ -162,7 +162,7 @@ export default async function projectFilesRoutes(
if ("error" in parsedQuery) return error(reply, parsedQuery.error, 400);
const projectId = Number(parsedQuery.data.project_id);
const project = await getProjectForFiles(projectId);
if (!project) return error(reply, "Projekt nebyl nalezen", 404);
if (!project) return error(reply, "Projekt nenalezen", 404);
if (!project.project_number)
return error(reply, "Projekt nemá číslo projektu");
@@ -225,7 +225,7 @@ export default async function projectFilesRoutes(
if ("error" in parsedQuery) return error(reply, parsedQuery.error, 400);
const projectId = Number(parsedQuery.data.project_id);
const project = await getProjectForFiles(projectId);
if (!project) return error(reply, "Projekt nebyl nalezen", 404);
if (!project) return error(reply, "Projekt nenalezen", 404);
if (!project.project_number)
return error(reply, "Projekt nemá číslo projektu");
@@ -267,7 +267,7 @@ export default async function projectFilesRoutes(
if ("error" in parsedQuery) return error(reply, parsedQuery.error, 400);
const projectId = Number(parsedQuery.data.project_id);
const project = await getProjectForFiles(projectId);
if (!project) return error(reply, "Projekt nebyl nalezen", 404);
if (!project) return error(reply, "Projekt nenalezen", 404);
if (!project.project_number)
return error(reply, "Projekt nemá číslo projektu");

View File

@@ -1,7 +1,7 @@
import { FastifyInstance } from "fastify";
import prisma from "../../config/database";
import { requireAuth } from "../../middleware/auth";
import { success, error } from "../../utils/response";
import { success, error, parseId } from "../../utils/response";
import { hashToken } from "../../services/auth";
/** Parse user-agent string into browser, OS, and device icon */
@@ -81,8 +81,8 @@ export default async function sessionsRoutes(
"/:id",
{ preHandler: requireAuth },
async (request, reply) => {
const id = parseInt(request.params.id, 10);
if (Number.isNaN(id)) return error(reply, "Neplatné ID relace", 400);
const id = parseId((request.params as any).id, reply);
if (id === null) return;
const authData = request.authData!;
const session = await prisma.refresh_tokens.findFirst({

View File

@@ -2,7 +2,7 @@ import { FastifyInstance } from "fastify";
import prisma from "../../config/database";
import { requireAuth, requirePermission } from "../../middleware/auth";
import { logAudit } from "../../services/audit";
import { success, error } from "../../utils/response";
import { success, error, parseId } from "../../utils/response";
import { parsePagination, buildPaginationMeta } from "../../utils/pagination";
import { parseBody } from "../../schemas/common";
import { CreateTripSchema, UpdateTripSchema } from "../../schemas/trips.schema";
@@ -189,8 +189,8 @@ export default async function tripsRoutes(
"/last-km/:vehicleId",
{ preHandler: requirePermission("trips.manage") },
async (request, reply) => {
const vehicleId = parseInt(request.params.vehicleId, 10);
if (isNaN(vehicleId)) return error(reply, "Neplatné ID vozidla", 400);
const vehicleId = parseId((request.params as any).vehicleId, reply);
if (vehicleId === null) return;
const [lastTrip, vehicle] = await Promise.all([
prisma.trips.findFirst({
@@ -261,8 +261,8 @@ export default async function tripsRoutes(
"/:id",
{ preHandler: requireAuth },
async (request, reply) => {
const id = parseInt(request.params.id, 10);
if (isNaN(id)) return error(reply, "Neplatné ID", 400);
const id = parseId((request.params as any).id, reply);
if (id === null) return;
const parsed = parseBody(UpdateTripSchema, request.body);
if ("error" in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
@@ -335,8 +335,8 @@ export default async function tripsRoutes(
"/:id",
{ preHandler: requireAuth },
async (request, reply) => {
const id = parseInt(request.params.id, 10);
if (isNaN(id)) return error(reply, "Neplatné ID", 400);
const id = parseId((request.params as any).id, reply);
if (id === null) return;
const authData = request.authData!;
const existing = await prisma.trips.findUnique({ where: { id } });
if (!existing) return error(reply, "Jízda nenalezena", 404);

View File

@@ -6,14 +6,14 @@ const QuotationItemSchema = z.object({
quantity: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional()
.default(1),
unit: z.string().nullish(),
unit_price: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional()
.default(0),
is_included_in_total: z
@@ -23,7 +23,7 @@ const QuotationItemSchema = z.object({
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional(),
});
@@ -34,7 +34,7 @@ const ScopeSectionSchema = z.object({
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional(),
});
@@ -53,7 +53,7 @@ export const CreateQuotationSchema = z.object({
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional()
.default(21.0),
apply_vat: z
@@ -75,7 +75,7 @@ export const UpdateQuotationSchema = z.object({
customer_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional(),
created_at: z.union([z.string(), z.null()]).optional(),
valid_until: z.union([z.string(), z.null()]).optional(),
@@ -84,7 +84,7 @@ export const UpdateQuotationSchema = z.object({
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional(),
apply_vat: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())

View File

@@ -6,14 +6,14 @@ const OrderItemSchema = z.object({
quantity: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional()
.default(1),
unit: z.string().nullish(),
unit_price: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional()
.default(0),
is_included_in_total: z
@@ -23,7 +23,7 @@ const OrderItemSchema = z.object({
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional(),
});
@@ -34,7 +34,7 @@ const OrderSectionSchema = z.object({
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional(),
});
@@ -42,7 +42,7 @@ export const CreateOrderFromQuotationSchema = z.object({
quotationId: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" }),
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" }),
customerOrderNumber: z.string().optional().default(""),
});
@@ -68,7 +68,7 @@ export const CreateOrderSchema = z.object({
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional()
.default(21.0),
apply_vat: z
@@ -78,7 +78,7 @@ export const CreateOrderSchema = z.object({
exchange_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional()
.default(1.0),
scope_title: z.string().nullish(),
@@ -103,7 +103,7 @@ export const UpdateOrderSchema = z.object({
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Must be a valid number" })
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional(),
apply_vat: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())