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:
BOHA
2026-06-09 06:45:26 +02:00
parent c454d1a3fc
commit 519edce373
179 changed files with 7179 additions and 2844 deletions

View File

@@ -1,4 +1,5 @@
import { z } from "zod";
import { numberInRange } from "./common";
export const AiChatSchema = z.object({
messages: z
@@ -33,8 +34,5 @@ export const AiRenameConversationSchema = z.object({
});
export const AiBudgetSchema = z.object({
budget_usd: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((n) => Number.isFinite(n) && n >= 0, "Neplatný rozpočet"),
budget_usd: numberInRange(0, 1000000, "Neplatný rozpočet"),
});

View File

@@ -1,4 +1,11 @@
import { z } from "zod";
import {
intIdFromForm,
nullableIntIdFromForm,
numberFromForm,
numberInRange,
timeString,
} from "./common";
export const AttendanceNotesSchema = z.object({
notes: z.string().nullish(),
@@ -10,138 +17,87 @@ export const AttendanceUpdateAddressSchema = z.object({
});
export const AttendanceSwitchProjectSchema = z.object({
project_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.nullish(),
project_id: nullableIntIdFromForm.nullish(),
});
export const AttendanceBalancesSchema = z.object({
user_id: z.union([z.number(), z.string()]).transform((v) => Number(v)),
year: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
user_id: intIdFromForm,
year: numberFromForm.optional(),
action_type: z.enum(["edit", "reset"]),
vacation_total: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
vacation_used: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
sick_used: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
vacation_total: numberFromForm.optional(),
vacation_used: numberFromForm.optional(),
sick_used: numberFromForm.optional(),
});
export const AttendanceBulkSchema = z.object({
month: z.string().regex(/^\d{4}-\d{2}$/, "Měsíc je povinný (formát YYYY-MM)"),
user_ids: z
.array(z.union([z.number(), z.string()]).transform((v) => Number(v)))
.array(intIdFromForm)
.min(1, "Vyberte alespoň jednoho zaměstnance"),
arrival_time: z.string().optional().default("08:00"),
departure_time: z.string().optional().default("16:30"),
break_start_time: z.string().optional().default("12:00"),
break_end_time: z.string().optional().default("12:30"),
arrival_time: timeString.optional().default("08:00"),
departure_time: timeString.optional().default("16:30"),
break_start_time: timeString.optional().default("12:00"),
break_end_time: timeString.optional().default("12:30"),
});
export const AttendanceLeaveSchema = z.object({
user_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
user_id: intIdFromForm.optional(),
date_from: z.string().min(1, "Datum je povinné"),
date_to: z.string().optional(),
leave_type: z
.enum(["vacation", "sick", "unpaid"])
.optional()
.default("vacation"),
leave_hours: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
leave_hours: numberFromForm.optional(),
notes: z.string().nullish(),
});
const ProjectLogSchema = z.object({
project_id: z.union([z.number(), z.string()]).transform((v) => Number(v)),
project_id: intIdFromForm,
hours: z.coerce.number().min(0).default(0),
minutes: z.coerce.number().min(0).default(0),
minutes: numberInRange(0, 59).default(0),
});
export const AttendancePunchSchema = z.object({
punch_action: z.string(),
latitude: z.union([z.number(), z.string(), z.null()]).optional(),
longitude: z.union([z.number(), z.string(), z.null()]).optional(),
accuracy: z.union([z.number(), z.string(), z.null()]).optional(),
latitude: numberFromForm.nullish(),
longitude: numberFromForm.nullish(),
accuracy: numberFromForm.nullish(),
address: z.string().max(500).nullish(),
});
export const CreateAttendanceSchema = z.object({
user_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
user_id: intIdFromForm.optional(),
shift_date: z.string(),
arrival_time: z.string().nullish(),
arrival_lat: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.nullish(),
arrival_lng: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.nullish(),
arrival_accuracy: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.nullish(),
arrival_time: timeString.nullish(),
arrival_lat: numberFromForm.nullish(),
arrival_lng: numberFromForm.nullish(),
arrival_accuracy: numberFromForm.nullish(),
arrival_address: z.string().nullish(),
departure_time: z.string().nullish(),
departure_lat: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.nullish(),
departure_lng: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.nullish(),
departure_accuracy: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.nullish(),
departure_time: timeString.nullish(),
departure_lat: numberFromForm.nullish(),
departure_lng: numberFromForm.nullish(),
departure_accuracy: numberFromForm.nullish(),
departure_address: z.string().nullish(),
notes: z.string().nullish(),
project_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.nullish(),
project_id: nullableIntIdFromForm.nullish(),
leave_type: z
.enum(["work", "vacation", "sick", "unpaid"])
.optional()
.default("work"),
leave_hours: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.nullish(),
leave_hours: numberFromForm.nullish(),
project_logs: z.array(ProjectLogSchema).optional(),
});
export const UpdateAttendanceSchema = z.object({
arrival_time: z.union([z.string(), z.null()]).optional(),
departure_time: z.union([z.string(), z.null()]).optional(),
break_start: z.union([z.string(), z.null()]).optional(),
break_end: z.union([z.string(), z.null()]).optional(),
arrival_time: timeString.nullish(),
departure_time: timeString.nullish(),
break_start: timeString.nullish(),
break_end: timeString.nullish(),
notes: z.string().nullish(),
project_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.optional(),
project_id: nullableIntIdFromForm.optional(),
leave_type: z.enum(["work", "vacation", "sick", "unpaid"]).optional(),
leave_hours: z.union([z.number(), z.string(), z.null()]).optional(),
leave_hours: numberFromForm.nullish(),
project_logs: z.array(ProjectLogSchema).optional(),
});

View File

@@ -1,14 +1,17 @@
import { z } from "zod";
export const LoginSchema = z.object({
username: z.string().min(1, "Uživatelské jméno je povinné"),
password: z.string().min(1, "Heslo je povinné"),
username: z.string().min(1, "Uživatelské jméno je povinné").max(255),
password: z.string().min(1, "Heslo je povinné").max(200),
remember_me: z.boolean().optional().default(false),
});
export const TotpVerifySchema = z.object({
login_token: z.string().min(1, "Token je povinný"),
totp_code: z.string().length(6, "Kód musí mít 6 číslic"),
totp_code: z
.string()
.length(6, "Kód musí mít 6 číslic")
.regex(/^\d{6}$/, "Kód musí mít 6 číslic"),
remember_me: z.boolean().optional().default(false),
});
@@ -23,15 +26,15 @@ export const TotpBackupSchema = z.object({
});
export const TotpEnableSchema = z.object({
secret: z.string().min(1),
code: z.string().min(1),
password: z.string().optional(),
current_code: z.string().optional(),
secret: z.string().min(1).max(255),
code: z.string().min(1).max(64),
password: z.string().max(200).optional(),
current_code: z.string().max(64).optional(),
});
export const TotpDisableSchema = z.object({
code: z.string().min(1),
password: z.string().optional(),
code: z.string().min(1).max(64),
password: z.string().max(200).optional(),
});
export const TotpRequiredSchema = z.object({

View File

@@ -1,37 +1,26 @@
import { z } from "zod";
import { booleanFromForm, numberFromForm } from "./common";
export const CreateBankAccountSchema = z.object({
account_name: z.string().nullish(),
bank_name: z.string().nullish(),
account_number: z.string().nullish(),
iban: z.string().nullish(),
bic: z.string().nullish(),
currency: z.string().optional().default("CZK"),
is_default: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional()
.default(false),
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional()
.default(0),
account_name: z.string().max(255).nullish(),
bank_name: z.string().max(255).nullish(),
account_number: z.string().max(255).nullish(),
iban: z.string().max(255).nullish(),
bic: z.string().max(255).nullish(),
currency: z.string().max(255).optional().default("CZK"),
is_default: booleanFromForm.optional().default(false),
position: numberFromForm.optional().default(0),
});
export const UpdateBankAccountSchema = z.object({
account_name: z.string().nullish(),
bank_name: z.string().nullish(),
account_number: z.string().nullish(),
iban: z.string().nullish(),
bic: z.string().nullish(),
currency: z.string().optional(),
is_default: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional(),
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
account_name: z.string().max(255).nullish(),
bank_name: z.string().max(255).nullish(),
account_number: z.string().max(255).nullish(),
iban: z.string().max(255).nullish(),
bic: z.string().max(255).nullish(),
currency: z.string().max(255).optional(),
is_default: booleanFromForm.optional(),
position: numberFromForm.optional(),
});
export type CreateBankAccountInput = z.infer<typeof CreateBankAccountSchema>;

View File

@@ -1,7 +1,7 @@
import { ZodSchema, z } from "zod";
import { z } from "zod";
export function parseBody<T>(
schema: ZodSchema<T>,
schema: z.ZodType<T>,
body: unknown,
): { data: T } | { error: string } {
try {
@@ -15,3 +15,108 @@ export function parseBody<T>(
return { error: "Neplatný požadavek" };
}
}
// ── Shared form-coercion helpers ──────────────────────────────────────────
// HTTP bodies arrive as strings (multipart) or numbers (JSON). These helpers
// coerce number|string → number with a NaN/Infinity guard, replacing the
// `z.union([z.number(), z.string()]).transform(Number)` idiom that was
// duplicated ~150× across schemas and silently produced NaN on bad input
// (which then flowed into Prisma / business math). User-facing messages Czech.
const coerceNum = (v: unknown): number =>
typeof v === "number"
? v
: typeof v === "string" && v.trim() !== ""
? Number(v)
: NaN;
/** number|string → number; rejects NaN/Infinity/empty. */
export const numberFromForm = z
.union([z.number(), z.string()])
.transform(coerceNum)
.refine((n) => Number.isFinite(n), { message: "Neplatné číslo" });
/** number|string → number within [min,max]; rejects NaN/out-of-range. */
export const numberInRange = (min: number, max: number, message?: string) =>
z
.union([z.number(), z.string()])
.transform(coerceNum)
.refine((n) => Number.isFinite(n) && n >= min && n <= max, {
message: message ?? `Číslo musí být mezi ${min} a ${max}`,
});
/** number|string → non-negative number (e.g. quantities, prices, odometers). */
export const nonNegativeNumberFromForm = z
.union([z.number(), z.string()])
.transform(coerceNum)
.refine((n) => Number.isFinite(n) && n >= 0, {
message: "Číslo nesmí být záporné",
});
/** number|string → positive number ( > 0 ). */
export const positiveNumberFromForm = z
.union([z.number(), z.string()])
.transform(coerceNum)
.refine((n) => Number.isFinite(n) && n > 0, {
message: "Číslo musí být kladné",
});
/** number|string → positive integer id; rejects NaN/≤0/non-integer. */
export const intIdFromForm = z
.union([z.number(), z.string()])
.transform(coerceNum)
.refine((n) => Number.isInteger(n) && n > 0, { message: "Neplatné ID" });
/** number|string|null → number|null; rejects NaN when a value is present. */
export const nullableNumberFromForm = z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null || v === "" ? null : coerceNum(v)))
.refine((n) => n === null || Number.isFinite(n), {
message: "Neplatné číslo",
});
/** number|string|null → positive-int id | null; rejects NaN/≤0 when present. */
export const nullableIntIdFromForm = z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null || v === "" ? null : coerceNum(v)))
.refine((n) => n === null || (Number.isInteger(n) && n > 0), {
message: "Neplatné ID",
});
/** Truthy form flag → boolean: true | 1 | "1" | "true" → true. */
export const booleanFromForm = z.preprocess(
(v) => v === true || v === 1 || v === "1" || v === "true",
z.boolean(),
);
/**
* YYYY-MM-DD date string. Tolerant of a trailing time component: an edit form
* round-trips a `@db.Date` that the `Date.toJSON` override serialises as
* "YYYY-MM-DDT00:00:00", so we strip anything after the date before validating
* (and the validated output is always the date-only `YYYY-MM-DD`).
*/
export const isoDateString = z.preprocess(
(v) => (typeof v === "string" ? v.slice(0, 10) : v),
z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, "Datum musí být ve formátu YYYY-MM-DD"),
);
/** HH:MM (24h) time string. Tolerant of a trailing ":ss" seconds component. */
export const timeString = z.preprocess(
(v) =>
typeof v === "string" && /^\d{2}:\d{2}:\d{2}/.test(v) ? v.slice(0, 5) : v,
z
.string()
.regex(/^([01]\d|2[0-3]):[0-5]\d$/, "Čas musí být ve formátu HH:MM"),
);
/**
* An email field that also accepts an empty string (meaning "not set"). Many
* settings/supplier forms submit "" for un-filled optional emails; a bare
* `.email()` would 400 the whole form. Pair with `.nullish()` at the call site.
*/
export const emailOrEmpty = z.union([
z.literal(""),
z.string().max(255).email("Neplatný email"),
]);

View File

@@ -1,26 +1,26 @@
import { z } from "zod";
export const CreateCustomerSchema = z.object({
name: z.string().min(1, "Název zákazníka je povinný"),
street: z.string().nullish(),
city: z.string().nullish(),
postal_code: z.string().nullish(),
country: z.string().nullish(),
company_id: z.string().nullish(),
vat_id: z.string().nullish(),
custom_fields: z.array(z.any()).optional(),
name: z.string().min(1, "Název zákazníka je povinný").max(255),
street: z.string().max(255).nullish(),
city: z.string().max(255).nullish(),
postal_code: z.string().max(255).nullish(),
country: z.string().max(255).nullish(),
company_id: z.string().max(255).nullish(),
vat_id: z.string().max(255).nullish(),
custom_fields: z.array(z.unknown()).max(100).optional(),
customer_field_order: z.array(z.string()).optional(),
});
export const UpdateCustomerSchema = z.object({
name: z.string().min(1, "Název zákazníka je povinný").optional(),
street: z.string().nullish(),
city: z.string().nullish(),
postal_code: z.string().nullish(),
country: z.string().nullish(),
company_id: z.string().nullish(),
vat_id: z.string().nullish(),
custom_fields: z.array(z.any()).optional(),
name: z.string().min(1, "Název zákazníka je povinný").max(255).optional(),
street: z.string().max(255).nullish(),
city: z.string().max(255).nullish(),
postal_code: z.string().max(255).nullish(),
country: z.string().max(255).nullish(),
company_id: z.string().max(255).nullish(),
vat_id: z.string().max(255).nullish(),
custom_fields: z.array(z.unknown()).max(100).optional(),
customer_field_order: z.array(z.string()).optional(),
});

View File

@@ -1,104 +1,68 @@
import { z } from "zod";
import {
numberFromForm,
numberInRange,
nonNegativeNumberFromForm,
positiveNumberFromForm,
nullableIntIdFromForm,
booleanFromForm,
} from "./common";
const InvoiceItemSchema = z.object({
description: z.string().nullish(),
item_description: z.string().nullish(),
quantity: z
.union([z.number(), z.string()])
.transform((v) => {
const n = Number(v);
if (isNaN(n) || n <= 0) throw new Error("Invalid quantity");
return n;
})
.optional()
.default(1),
unit: z.string().nullish(),
unit_price: z
.union([z.number(), z.string()])
.transform((v) => {
const n = Number(v);
if (isNaN(n)) throw new Error("Invalid unit_price");
return n;
})
.optional()
.default(0),
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional()
.default(21.0),
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
description: z.string().max(8000).nullish(),
item_description: z.string().max(8000).nullish(),
quantity: positiveNumberFromForm.default(1),
unit: z.string().max(255).nullish(),
unit_price: numberFromForm.default(0),
vat_rate: numberInRange(0, 100).optional().default(21.0),
position: nonNegativeNumberFromForm.optional(),
});
export const CreateInvoiceSchema = z.object({
invoice_number: z.string().nullish(),
order_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.nullish(),
customer_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.nullish(),
status: z.string().optional().default("issued"),
currency: z.string().optional().default("CZK"),
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional()
.default(21.0),
apply_vat: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional()
.default(true),
payment_method: z.string().nullish(),
constant_symbol: z.string().nullish(),
bank_name: z.string().nullish(),
bank_swift: z.string().nullish(),
bank_iban: z.string().nullish(),
bank_account: z.string().nullish(),
issue_date: z.string().nullish(),
due_date: z.string().nullish(),
tax_date: z.string().nullish(),
issued_by: z.string().nullish(),
billing_text: z.string().nullish(),
language: z.string().optional().default("cs"),
notes: z.string().nullish(),
internal_notes: z.string().nullish(),
invoice_number: z.string().max(255).nullish(),
order_id: nullableIntIdFromForm.nullish(),
customer_id: nullableIntIdFromForm.nullish(),
status: z.string().max(20).optional().default("issued"),
currency: z.string().max(20).optional().default("CZK"),
vat_rate: numberInRange(0, 100).optional().default(21.0),
apply_vat: booleanFromForm.optional().default(true),
payment_method: z.string().max(255).nullish(),
constant_symbol: z.string().max(255).nullish(),
bank_name: z.string().max(255).nullish(),
bank_swift: z.string().max(255).nullish(),
bank_iban: z.string().max(255).nullish(),
bank_account: z.string().max(255).nullish(),
issue_date: z.string().max(255).nullish(),
due_date: z.string().max(255).nullish(),
tax_date: z.string().max(255).nullish(),
issued_by: z.string().max(255).nullish(),
billing_text: z.string().max(8000).nullish(),
language: z.string().max(20).optional().default("cs"),
notes: z.string().max(8000).nullish(),
internal_notes: z.string().max(8000).nullish(),
items: z.array(InvoiceItemSchema).optional(),
});
export const UpdateInvoiceSchema = z.object({
status: z.string().optional(),
currency: z.string().optional(),
language: z.string().optional(),
payment_method: z.string().nullish(),
constant_symbol: z.string().nullish(),
bank_name: z.string().nullish(),
bank_swift: z.string().nullish(),
bank_iban: z.string().nullish(),
bank_account: z.string().nullish(),
issued_by: z.string().nullish(),
customer_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.optional(),
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
apply_vat: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional(),
issue_date: z.union([z.string(), z.null()]).optional(),
due_date: z.union([z.string(), z.null()]).optional(),
tax_date: z.union([z.string(), z.null()]).optional(),
notes: z.string().nullish(),
internal_notes: z.string().nullish(),
paid_date: z.union([z.string(), z.null()]).optional(),
status: z.string().max(20).optional(),
currency: z.string().max(20).optional(),
language: z.string().max(20).optional(),
payment_method: z.string().max(255).nullish(),
constant_symbol: z.string().max(255).nullish(),
bank_name: z.string().max(255).nullish(),
bank_swift: z.string().max(255).nullish(),
bank_iban: z.string().max(255).nullish(),
bank_account: z.string().max(255).nullish(),
issued_by: z.string().max(255).nullish(),
customer_id: nullableIntIdFromForm.optional(),
vat_rate: numberInRange(0, 100).optional(),
apply_vat: booleanFromForm.optional(),
issue_date: z.union([z.string().max(255), z.null()]).optional(),
due_date: z.union([z.string().max(255), z.null()]).optional(),
tax_date: z.union([z.string().max(255), z.null()]).optional(),
notes: z.string().max(8000).nullish(),
internal_notes: z.string().max(8000).nullish(),
paid_date: z.union([z.string().max(255), z.null()]).optional(),
items: z.array(InvoiceItemSchema).optional(),
});

View File

@@ -1,17 +1,18 @@
import { z } from "zod";
import { isoDateString } from "./common";
export const CreateLeaveRequestSchema = z.object({
leave_type: z.enum(["vacation", "sick", "unpaid"]).default("vacation"),
date_from: z.string().min(1, "Datum od je povinné"),
date_to: z.string().min(1, "Datum do je povinné"),
notes: z.string().nullish(),
date_from: isoDateString,
date_to: isoDateString,
notes: z.string().max(2000).nullish(),
});
export const ReviewLeaveRequestSchema = z.object({
status: z
.enum(["pending", "approved", "rejected", "cancelled"])
.default("pending"),
reviewer_note: z.string().nullish(),
reviewer_note: z.string().max(2000).nullish(),
});
export type CreateLeaveRequestInput = z.infer<typeof CreateLeaveRequestSchema>;

View File

@@ -1,97 +1,62 @@
import { z } from "zod";
import {
numberFromForm,
numberInRange,
nonNegativeNumberFromForm,
positiveNumberFromForm,
nullableIntIdFromForm,
booleanFromForm,
} from "./common";
const QuotationItemSchema = z.object({
description: z.string().nullish(),
item_description: z.string().nullish(),
quantity: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.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: "Musí být platné číslo" })
.optional()
.default(0),
is_included_in_total: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional()
.default(true),
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional(),
description: z.string().max(8000).nullish(),
item_description: z.string().max(8000).nullish(),
quantity: positiveNumberFromForm.default(1),
unit: z.string().max(255).nullish(),
unit_price: numberFromForm.default(0),
is_included_in_total: booleanFromForm.optional().default(true),
position: nonNegativeNumberFromForm.optional(),
});
const ScopeSectionSchema = z.object({
title: z.string().nullish(),
title_cz: z.string().nullish(),
content: z.string().nullish(),
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional(),
title: z.string().max(255).nullish(),
title_cz: z.string().max(255).nullish(),
content: z.string().max(8000).nullish(),
position: nonNegativeNumberFromForm.optional(),
});
export const CreateQuotationSchema = z.object({
quotation_number: z.string().nullish(),
project_code: z.string().nullish(),
customer_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.nullish(),
created_at: z.string().nullish(),
valid_until: z.string().nullish(),
currency: z.string().optional().default("CZK"),
language: z.string().optional().default("cs"),
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional()
.default(21.0),
apply_vat: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional()
.default(true),
quotation_number: z.string().max(255).nullish(),
project_code: z.string().max(255).nullish(),
customer_id: nullableIntIdFromForm.nullish(),
created_at: z.string().max(255).nullish(),
valid_until: z.string().max(255).nullish(),
currency: z.string().max(20).optional().default("CZK"),
language: z.string().max(20).optional().default("cs"),
vat_rate: numberInRange(0, 100).optional().default(21.0),
apply_vat: booleanFromForm.optional().default(true),
status: z
.enum(["active", "ordered", "invalidated"])
.optional()
.default("active"),
scope_title: z.string().nullish(),
scope_description: z.string().nullish(),
scope_title: z.string().max(255).nullish(),
scope_description: z.string().max(8000).nullish(),
items: z.array(QuotationItemSchema).optional(),
sections: z.array(ScopeSectionSchema).optional(),
});
export const UpdateQuotationSchema = z.object({
project_code: z.string().nullish(),
customer_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.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(),
currency: z.string().optional(),
language: z.string().optional(),
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.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())
.optional(),
project_code: z.string().max(255).nullish(),
customer_id: nullableIntIdFromForm.optional(),
created_at: z.union([z.string().max(255), z.null()]).optional(),
valid_until: z.union([z.string().max(255), z.null()]).optional(),
currency: z.string().max(20).optional(),
language: z.string().max(20).optional(),
vat_rate: numberInRange(0, 100).optional(),
apply_vat: booleanFromForm.optional(),
status: z.enum(["active", "ordered", "invalidated"]).optional(),
scope_title: z.string().nullish(),
scope_description: z.string().nullish(),
scope_title: z.string().max(255).nullish(),
scope_description: z.string().max(8000).nullish(),
items: z.array(QuotationItemSchema).optional(),
sections: z.array(ScopeSectionSchema).optional(),
});

View File

@@ -1,117 +1,69 @@
import { z } from "zod";
import {
numberFromForm,
numberInRange,
nonNegativeNumberFromForm,
positiveNumberFromForm,
intIdFromForm,
nullableIntIdFromForm,
booleanFromForm,
} from "./common";
const OrderItemSchema = z.object({
description: z.string().nullish(),
item_description: z.string().nullish(),
quantity: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.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: "Musí být platné číslo" })
.optional()
.default(0),
is_included_in_total: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional()
.default(true),
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional(),
description: z.string().max(8000).nullish(),
item_description: z.string().max(8000).nullish(),
quantity: positiveNumberFromForm.default(1),
unit: z.string().max(255).nullish(),
unit_price: numberFromForm.default(0),
is_included_in_total: booleanFromForm.optional().default(true),
position: nonNegativeNumberFromForm.optional(),
});
const OrderSectionSchema = z.object({
title: z.string().nullish(),
title_cz: z.string().nullish(),
content: z.string().nullish(),
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional(),
title: z.string().max(255).nullish(),
title_cz: z.string().max(255).nullish(),
content: z.string().max(8000).nullish(),
position: nonNegativeNumberFromForm.optional(),
});
export const CreateOrderFromQuotationSchema = z.object({
quotationId: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" }),
customerOrderNumber: z.string().optional().default(""),
quotationId: intIdFromForm,
customerOrderNumber: z.string().max(255).optional().default(""),
});
export const CreateOrderSchema = z.object({
order_number: z.string().nullish(),
customer_order_number: z.string().nullish(),
quotation_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.nullish(),
customer_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.nullish(),
order_number: z.string().max(255).nullish(),
customer_order_number: z.string().max(255).nullish(),
quotation_id: nullableIntIdFromForm.nullish(),
customer_id: nullableIntIdFromForm.nullish(),
status: z
.enum(["prijata", "v_realizaci", "dokoncena", "zrusena"])
.optional()
.default("prijata"),
currency: z.string().optional().default("CZK"),
language: z.string().optional().default("cs"),
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional()
.default(21.0),
apply_vat: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional()
.default(true),
exchange_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((v) => !Number.isNaN(v), { message: "Musí být platné číslo" })
.optional()
.default(1.0),
scope_title: z.string().nullish(),
scope_description: z.string().nullish(),
notes: z.string().nullish(),
currency: z.string().max(20).optional().default("CZK"),
language: z.string().max(20).optional().default("cs"),
vat_rate: numberInRange(0, 100).optional().default(21.0),
apply_vat: booleanFromForm.optional().default(true),
exchange_rate: positiveNumberFromForm.optional().default(1.0),
scope_title: z.string().max(255).nullish(),
scope_description: z.string().max(8000).nullish(),
notes: z.string().max(8000).nullish(),
items: z.array(OrderItemSchema).optional(),
sections: z.array(OrderSectionSchema).optional(),
create_project: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional()
.default(true),
create_project: booleanFromForm.optional().default(true),
});
export const UpdateOrderSchema = z.object({
customer_order_number: z.string().nullish(),
customer_order_number: z.string().max(255).nullish(),
status: z.enum(["prijata", "v_realizaci", "dokoncena", "zrusena"]).optional(),
currency: z.string().optional(),
language: z.string().optional(),
scope_title: z.string().nullish(),
scope_description: z.string().nullish(),
notes: z.string().nullish(),
customer_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.optional(),
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.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())
.optional(),
currency: z.string().max(20).optional(),
language: z.string().max(20).optional(),
scope_title: z.string().max(255).nullish(),
scope_description: z.string().max(8000).nullish(),
notes: z.string().max(8000).nullish(),
customer_id: nullableIntIdFromForm.optional(),
vat_rate: numberInRange(0, 100).optional(),
apply_vat: booleanFromForm.optional(),
items: z.array(OrderItemSchema).optional(),
sections: z.array(OrderSectionSchema).optional(),
});

View File

@@ -1,13 +1,9 @@
import { z } from "zod";
import { intIdFromForm, isoDateString, nullableNumberFromForm } from "./common";
const isoDate = z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, "Datum musí být ve formátu YYYY-MM-DD");
const isoDate = isoDateString;
const intFromForm = z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.refine((n) => Number.isInteger(n) && n > 0, "Neplatné ID");
const intFromForm = intIdFromForm;
// Category is now a dynamic key (validated against plan_categories in the
// service). Keep it a non-empty string here.
@@ -18,10 +14,7 @@ export const CreatePlanEntrySchema = z
user_id: intFromForm,
date_from: isoDate,
date_to: isoDate,
project_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.nullish(),
project_id: nullableNumberFromForm.nullish(),
category: planCategoryEnum.default("work"),
note: z.string().max(500, "Maximálně 500 znaků"),
})
@@ -34,10 +27,7 @@ export const UpdatePlanEntrySchema = z
.object({
date_from: isoDate.optional(),
date_to: isoDate.optional(),
project_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.nullish(),
project_id: nullableNumberFromForm.nullish(),
category: planCategoryEnum.optional(),
note: z.string().max(500).optional(),
})
@@ -55,19 +45,13 @@ export const UpdatePlanEntrySchema = z
export const CreatePlanOverrideSchema = z.object({
user_id: intFromForm,
shift_date: isoDate,
project_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.nullish(),
project_id: nullableNumberFromForm.nullish(),
category: planCategoryEnum,
note: z.string().max(500, "Maximálně 500 znaků"),
});
export const UpdatePlanOverrideSchema = z.object({
project_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.nullish(),
project_id: nullableNumberFromForm.nullish(),
category: planCategoryEnum.optional(),
note: z.string().max(500).optional(),
});
@@ -96,10 +80,7 @@ export const BulkPlanEntrySchema = z
.union([z.boolean(), z.string()])
.transform((v) => v === true || v === "true" || v === "1")
.default(false),
project_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.nullish(),
project_id: nullableNumberFromForm.nullish(),
category: planCategoryEnum.default("work"),
note: z.string().max(500, "Maximálně 500 znaků").default(""),
})

View File

@@ -2,10 +2,14 @@ import { z } from "zod";
export const UpdateProfileSchema = z.object({
email: z.string().email("Neplatný formát e-mailu").optional(),
first_name: z.string().optional(),
last_name: z.string().optional(),
current_password: z.string().optional(),
new_password: z.string().min(8, "Heslo musí mít alespoň 8 znaků").optional(),
first_name: z.string().min(1).max(100).optional(),
last_name: z.string().min(1).max(100).optional(),
current_password: z.string().max(200).optional(),
new_password: z
.string()
.min(8, "Heslo musí mít alespoň 8 znaků")
.max(200)
.optional(),
});
export type UpdateProfileInput = z.infer<typeof UpdateProfileSchema>;

View File

@@ -1,24 +1,13 @@
import { z } from "zod";
import { isoDateString, nullableIntIdFromForm } from "./common";
export const CreateProjectSchema = z.object({
name: z.string().min(1, "Zadejte název projektu"),
customer_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null || v === "" ? null : Number(v)))
.refine((v) => v === null || !Number.isNaN(v), {
message: "Neplatný zákazník",
})
.optional(),
responsible_user_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null || v === "" ? null : Number(v)))
.refine((v) => v === null || !Number.isNaN(v), {
message: "Neplatná zodpovědná osoba",
})
.optional(),
customer_id: nullableIntIdFromForm.optional(),
responsible_user_id: nullableIntIdFromForm.optional(),
status: z.string().optional().default("aktivni"),
start_date: z.union([z.string(), z.null()]).optional(),
end_date: z.union([z.string(), z.null()]).optional(),
start_date: isoDateString.nullish(),
end_date: isoDateString.nullish(),
notes: z.string().nullish(),
});
@@ -26,24 +15,12 @@ export const UpdateProjectSchema = z.object({
name: z.string().nullish(),
status: z.string().optional(),
notes: z.string().nullish(),
customer_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.optional(),
responsible_user_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.optional(),
quotation_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.optional(),
order_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.optional(),
start_date: z.union([z.string(), z.null()]).optional(),
end_date: z.union([z.string(), z.null()]).optional(),
customer_id: nullableIntIdFromForm.optional(),
responsible_user_id: nullableIntIdFromForm.optional(),
quotation_id: nullableIntIdFromForm.optional(),
order_id: nullableIntIdFromForm.optional(),
start_date: isoDateString.nullish(),
end_date: isoDateString.nullish(),
});
export const CreateProjectNoteSchema = z.object({

View File

@@ -1,63 +1,37 @@
import { z } from "zod";
import { numberInRange, nonNegativeNumberFromForm } from "./common";
export const CreateReceivedInvoiceSchema = z.object({
supplier_name: z.string().min(1, "Název dodavatele je povinný"),
month: z.union([z.number(), z.string()]).transform((v) => Number(v)),
year: z.union([z.number(), z.string()]).transform((v) => Number(v)),
invoice_number: z.string().nullish(),
description: z.string().nullish(),
amount: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional()
.default(0),
currency: z.string().optional().default("CZK"),
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional()
.default(21),
vat_amount: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional()
.default(0),
issue_date: z.string().nullish(),
due_date: z.string().nullish(),
supplier_name: z.string().min(1, "Název dodavatele je povinný").max(255),
month: numberInRange(1, 12),
year: numberInRange(2000, 2100),
invoice_number: z.string().max(255).nullish(),
description: z.string().max(8000).nullish(),
amount: nonNegativeNumberFromForm.optional().default(0),
currency: z.string().max(20).optional().default("CZK"),
vat_rate: numberInRange(0, 100).optional().default(21),
vat_amount: nonNegativeNumberFromForm.optional().default(0),
issue_date: z.string().max(255).nullish(),
due_date: z.string().max(255).nullish(),
status: z.enum(["unpaid", "paid"]).optional().default("unpaid"),
notes: z.string().nullish(),
notes: z.string().max(8000).nullish(),
});
export const UpdateReceivedInvoiceSchema = z.object({
supplier_name: z.string().optional(),
invoice_number: z.string().nullish(),
description: z.string().nullish(),
amount: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
currency: z.string().optional(),
vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
vat_amount: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
issue_date: z.union([z.string(), z.null()]).optional(),
due_date: z.union([z.string(), z.null()]).optional(),
paid_date: z.union([z.string(), z.null()]).optional(),
supplier_name: z.string().max(255).optional(),
invoice_number: z.string().max(255).nullish(),
description: z.string().max(8000).nullish(),
amount: nonNegativeNumberFromForm.optional(),
currency: z.string().max(20).optional(),
vat_rate: numberInRange(0, 100).optional(),
vat_amount: nonNegativeNumberFromForm.optional(),
issue_date: z.union([z.string().max(255), z.null()]).optional(),
due_date: z.union([z.string().max(255), z.null()]).optional(),
paid_date: z.union([z.string().max(255), z.null()]).optional(),
status: z.enum(["unpaid", "paid"]).optional(),
notes: z.string().nullish(),
month: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
year: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
notes: z.string().max(8000).nullish(),
month: numberInRange(1, 12).optional(),
year: numberInRange(2000, 2100).optional(),
});
export type CreateReceivedInvoiceInput = z.infer<

View File

@@ -1,16 +1,16 @@
import { z } from "zod";
export const CreateRoleSchema = z.object({
name: z.string().min(1, "Název role je povinný"),
display_name: z.string().min(1, "Zobrazovaný název je povinný"),
description: z.string().nullish(),
permission_ids: z.array(z.number()).optional(),
name: z.string().min(1, "Název role je povinný").max(100),
display_name: z.string().min(1, "Zobrazovaný název je povinný").max(100),
description: z.string().max(255).nullish(),
permission_ids: z.array(z.number().int().positive()).max(500).optional(),
});
export const UpdateRoleSchema = z.object({
display_name: z.string().optional(),
description: z.string().optional(),
permission_ids: z.array(z.number()).optional(),
display_name: z.string().max(100).optional(),
description: z.string().max(255).optional(),
permission_ids: z.array(z.number().int().positive()).max(500).optional(),
});
export type CreateRoleInput = z.infer<typeof CreateRoleSchema>;

View File

@@ -1,41 +1,36 @@
import { z } from "zod";
import {
numberFromForm,
nonNegativeNumberFromForm,
intIdFromForm,
} from "./common";
const ScopeSectionSchema = z.object({
title: z.string().nullish(),
title_cz: z.string().nullish(),
content: z.string().nullish(),
position: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
title: z.string().max(255).nullish(),
title_cz: z.string().max(255).nullish(),
content: z.string().max(8000).nullish(),
position: nonNegativeNumberFromForm.optional(),
});
export const CreateScopeTemplateSchema = z.object({
name: z.string().nullish(),
title: z.string().nullish(),
description: z.string().nullish(),
name: z.string().max(255).nullish(),
title: z.string().max(255).nullish(),
description: z.string().max(8000).nullish(),
sections: z.array(ScopeSectionSchema).optional(),
});
export const CreateItemTemplateSchema = z.object({
id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
name: z.string().nullish(),
description: z.string().nullish(),
default_price: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional()
.default(0),
category: z.string().nullish(),
id: intIdFromForm.optional(),
name: z.string().max(255).nullish(),
description: z.string().max(8000).nullish(),
default_price: numberFromForm.optional().default(0),
category: z.string().max(255).nullish(),
});
export const UpdateScopeTemplateSchema = z.object({
name: z.string().optional(),
title: z.string().optional(),
description: z.string().optional(),
name: z.string().max(255).optional(),
title: z.string().max(255).optional(),
description: z.string().max(8000).optional(),
sections: z.array(ScopeSectionSchema).optional(),
});

View File

@@ -1,4 +1,9 @@
import { z } from "zod";
import {
nonNegativeNumberFromForm,
numberInRange,
emailOrEmpty,
} from "./common";
export const UpdateCompanySettingsSchema = z.object({
company_name: z.string().nullish(),
@@ -12,32 +17,17 @@ export const UpdateCompanySettingsSchema = z.object({
default_currency: z.string().nullish(),
order_type_code: z.string().nullish(),
invoice_type_code: z.string().nullish(),
default_vat_rate: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
default_vat_rate: numberInRange(0, 100).optional(),
require_2fa: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional(),
break_threshold_hours: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
break_duration_short: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
break_duration_long: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
clock_rounding_minutes: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
invoice_alert_email: z.string().nullish(),
leave_notify_email: z.string().nullish(),
smtp_from: z.string().nullish(),
break_threshold_hours: nonNegativeNumberFromForm.optional(),
break_duration_short: nonNegativeNumberFromForm.optional(),
break_duration_long: nonNegativeNumberFromForm.optional(),
clock_rounding_minutes: numberInRange(0, 1440).optional(),
invoice_alert_email: emailOrEmpty.nullish(),
leave_notify_email: emailOrEmpty.nullish(),
smtp_from: emailOrEmpty.nullish(),
smtp_from_name: z.string().nullish(),
offer_number_pattern: z.string().nullish(),
order_number_pattern: z.string().nullish(),
@@ -48,22 +38,13 @@ export const UpdateCompanySettingsSchema = z.object({
warehouse_issue_number_pattern: z.string().nullish(),
warehouse_inventory_prefix: z.string().nullish(),
warehouse_inventory_number_pattern: z.string().nullish(),
max_login_attempts: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
lockout_minutes: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
max_requests_per_minute: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
available_vat_rates: z.array(z.number()).optional(),
max_login_attempts: numberInRange(1, 100).optional(),
lockout_minutes: numberInRange(1, 1440).optional(),
max_requests_per_minute: numberInRange(1, 100000).optional(),
available_vat_rates: z.array(numberInRange(0, 100)).max(50).optional(),
available_currencies: z.array(z.string()).optional(),
custom_fields: z.array(z.any()).optional(),
supplier_field_order: z.array(z.any()).optional(),
custom_fields: z.array(z.unknown()).max(200).optional(),
supplier_field_order: z.array(z.unknown()).max(200).optional(),
});
export type UpdateCompanySettingsInput = z.infer<

View File

@@ -1,41 +1,33 @@
import { z } from "zod";
import {
intIdFromForm,
nonNegativeNumberFromForm,
booleanFromForm,
isoDateString,
} from "./common";
export const CreateTripSchema = z.object({
vehicle_id: z.union([z.number(), z.string()]).transform((v) => Number(v)),
vehicle_id: intIdFromForm,
// user_id is optional here because the route injects it from authData.userId
// when the client doesn't provide it (see POST /trips handler)
user_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
trip_date: z.string(),
start_km: z.union([z.number(), z.string()]).transform((v) => Number(v)),
end_km: z.union([z.number(), z.string()]).transform((v) => Number(v)),
route_from: z.string(),
route_to: z.string(),
is_business: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional()
.default(false),
notes: z.string().nullish(),
user_id: intIdFromForm.optional(),
trip_date: isoDateString,
start_km: nonNegativeNumberFromForm,
end_km: nonNegativeNumberFromForm,
route_from: z.string().min(1, "Vyplňte odkud").max(255),
route_to: z.string().min(1, "Vyplňte kam").max(255),
is_business: booleanFromForm.optional().default(false),
notes: z.string().max(2000).nullish(),
});
export const UpdateTripSchema = z.object({
trip_date: z.string().optional(),
start_km: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
end_km: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
route_from: z.string().optional(),
route_to: z.string().optional(),
is_business: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional(),
notes: z.string().nullish(),
trip_date: isoDateString.optional(),
start_km: nonNegativeNumberFromForm.optional(),
end_km: nonNegativeNumberFromForm.optional(),
route_from: z.string().min(1, "Vyplňte odkud").max(255).optional(),
route_to: z.string().min(1, "Vyplňte kam").max(255).optional(),
is_business: booleanFromForm.optional(),
notes: z.string().max(2000).nullish(),
});
export type CreateTripInput = z.infer<typeof CreateTripSchema>;

View File

@@ -1,15 +1,13 @@
import { z } from "zod";
import { intIdFromForm, nullableIntIdFromForm } from "./common";
export const CreateUserSchema = z.object({
username: z.string().min(1, "Uživatelské jméno je povinné"),
email: z.string().email("Neplatný formát e-mailu"),
password: z.string().min(8, "Heslo musí mít alespoň 8 znaků"),
first_name: z.string().min(1, "Jméno je povinné"),
last_name: z.string().min(1, "Příjmení je povinné"),
role_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
password: z.string().min(8, "Heslo musí mít alespoň 8 znaků").max(200),
first_name: z.string().min(1, "Jméno je povinné").max(100),
last_name: z.string().min(1, "Příjmení je povinné").max(100),
role_id: intIdFromForm.optional(),
is_active: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional()
@@ -21,14 +19,11 @@ export const UpdateUserSchema = z.object({
email: z.string().email("Neplatný formát e-mailu").optional(),
password: z.preprocess(
(v) => (v === "" ? undefined : v),
z.string().min(8, "Heslo musí mít alespoň 8 znaků").optional(),
z.string().min(8, "Heslo musí mít alespoň 8 znaků").max(200).optional(),
),
first_name: z.string().optional(),
last_name: z.string().optional(),
role_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.optional(),
first_name: z.string().max(100).optional(),
last_name: z.string().max(100).optional(),
role_id: nullableIntIdFromForm.optional(),
is_active: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional(),

View File

@@ -1,20 +1,13 @@
import { z } from "zod";
import { nonNegativeNumberFromForm } from "./common";
export const CreateVehicleSchema = z.object({
spz: z.string().min(1, "SPZ je povinná"),
name: z.string().min(1, "Název je povinný"),
spz: z.string().min(1, "SPZ je povinná").max(50),
name: z.string().min(1, "Název je povinný").max(255),
brand: z.string().nullish(),
model: z.string().nullish(),
initial_km: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional()
.default(0),
actual_km: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional()
.default(0),
initial_km: nonNegativeNumberFromForm.optional().default(0),
actual_km: nonNegativeNumberFromForm.optional().default(0),
is_active: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional()
@@ -22,18 +15,12 @@ export const CreateVehicleSchema = z.object({
});
export const UpdateVehicleSchema = z.object({
spz: z.string().optional(),
name: z.string().optional(),
spz: z.string().max(50).optional(),
name: z.string().max(255).optional(),
brand: z.string().nullish(),
model: z.string().nullish(),
initial_km: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
actual_km: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
initial_km: nonNegativeNumberFromForm.optional(),
actual_km: nonNegativeNumberFromForm.optional(),
is_active: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional(),

View File

@@ -1,74 +1,68 @@
import { z } from "zod";
import {
numberFromForm,
nonNegativeNumberFromForm,
positiveNumberFromForm,
intIdFromForm,
nullableNumberFromForm,
nullableIntIdFromForm,
booleanFromForm,
isoDateString,
emailOrEmpty,
} from "./common";
// === Categories ===
export const CreateCategorySchema = z.object({
name: z.string().min(1, "Název je povinný"),
description: z.string().nullish(),
sort_order: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional()
.default(0),
name: z.string().min(1, "Název je povinný").max(255),
description: z.string().max(5000).nullish(),
sort_order: numberFromForm.optional().default(0),
});
export const UpdateCategorySchema = z.object({
name: z.string().min(1, "Název je povinný").optional(),
description: z.string().nullish(),
sort_order: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
name: z.string().min(1, "Název je povinný").max(255).optional(),
description: z.string().max(5000).nullish(),
sort_order: numberFromForm.optional(),
});
export type CreateCategoryInput = z.infer<typeof CreateCategorySchema>;
export type UpdateCategoryInput = z.infer<typeof UpdateCategorySchema>;
// === Suppliers ===
export const CreateSupplierSchema = z.object({
name: z.string().min(1, "Název je povinný"),
ico: z.string().nullish(),
dic: z.string().nullish(),
contact_person: z.string().nullish(),
email: z.string().nullish(),
phone: z.string().nullish(),
address: z.string().nullish(),
notes: z.string().nullish(),
is_active: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional()
.default(true),
name: z.string().min(1, "Název je povinný").max(255),
ico: z.string().max(255).nullish(),
dic: z.string().max(255).nullish(),
contact_person: z.string().max(255).nullish(),
email: emailOrEmpty.nullish(),
phone: z.string().max(50).nullish(),
address: z.string().max(5000).nullish(),
notes: z.string().max(5000).nullish(),
is_active: booleanFromForm.optional().default(true),
});
export const UpdateSupplierSchema = z.object({
name: z.string().min(1, "Název je povinný").optional(),
ico: z.string().nullish(),
dic: z.string().nullish(),
contact_person: z.string().nullish(),
email: z.string().nullish(),
phone: z.string().nullish(),
address: z.string().nullish(),
notes: z.string().nullish(),
is_active: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional(),
name: z.string().min(1, "Název je povinný").max(255).optional(),
ico: z.string().max(255).nullish(),
dic: z.string().max(255).nullish(),
contact_person: z.string().max(255).nullish(),
email: emailOrEmpty.nullish(),
phone: z.string().max(50).nullish(),
address: z.string().max(5000).nullish(),
notes: z.string().max(5000).nullish(),
is_active: booleanFromForm.optional(),
});
export type CreateSupplierInput = z.infer<typeof CreateSupplierSchema>;
export type UpdateSupplierInput = z.infer<typeof UpdateSupplierSchema>;
// === Locations ===
export const CreateLocationSchema = z.object({
code: z.string().min(1, "Kód je povinný"),
name: z.string().min(1, "Název je povinný"),
description: z.string().nullish(),
is_active: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional()
.default(true),
code: z.string().min(1, "Kód je povinný").max(255),
name: z.string().min(1, "Název je povinný").max(255),
description: z.string().max(5000).nullish(),
is_active: booleanFromForm.optional().default(true),
});
export const UpdateLocationSchema = z.object({
code: z.string().min(1, "Kód je povinný").optional(),
name: z.string().min(1, "Název je povinný").optional(),
description: z.string().nullish(),
is_active: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional(),
code: z.string().min(1, "Kód je povinný").max(255).optional(),
name: z.string().min(1, "Název je povinný").max(255).optional(),
description: z.string().max(5000).nullish(),
is_active: booleanFromForm.optional(),
});
export type CreateLocationInput = z.infer<typeof CreateLocationSchema>;
export type UpdateLocationInput = z.infer<typeof UpdateLocationSchema>;
@@ -79,77 +73,51 @@ export const UnitEnum = z.enum(UNIT_LIST);
export type Unit = z.infer<typeof UnitEnum>;
export const CreateItemSchema = z.object({
item_number: z.string().nullish(),
name: z.string().min(1, "Název je povinný"),
description: z.string().nullish(),
category_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.nullish(),
item_number: z.string().max(255).nullish(),
name: z.string().min(1, "Název je povinný").max(255),
description: z.string().max(5000).nullish(),
category_id: nullableIntIdFromForm.nullish(),
unit: UnitEnum,
min_quantity: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.nullish(),
notes: z.string().nullish(),
is_active: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional()
.default(true),
min_quantity: nullableNumberFromForm.nullish(),
notes: z.string().max(5000).nullish(),
is_active: booleanFromForm.optional().default(true),
});
export const UpdateItemSchema = z.object({
item_number: z.string().nullish(),
name: z.string().min(1, "Název je povinný").optional(),
description: z.string().nullish(),
category_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.nullish(),
item_number: z.string().max(255).nullish(),
name: z.string().min(1, "Název je povinný").max(255).optional(),
description: z.string().max(5000).nullish(),
category_id: nullableIntIdFromForm.nullish(),
unit: UnitEnum.optional(),
min_quantity: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.nullish(),
notes: z.string().nullish(),
is_active: z
.preprocess((v) => v === true || v === 1 || v === "1", z.boolean())
.optional(),
min_quantity: nullableNumberFromForm.nullish(),
notes: z.string().max(5000).nullish(),
is_active: booleanFromForm.optional(),
});
export type CreateItemInput = z.infer<typeof CreateItemSchema>;
export type UpdateItemInput = z.infer<typeof UpdateItemSchema>;
// === Receipts ===
export const ReceiptItemSchema = z.object({
item_id: z.union([z.number(), z.string()]).transform((v) => Number(v)),
quantity: z.union([z.number(), z.string()]).transform((v) => Number(v)),
unit_price: z.union([z.number(), z.string()]).transform((v) => Number(v)),
location_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.nullish(),
notes: z.string().nullish(),
item_id: intIdFromForm,
quantity: positiveNumberFromForm,
unit_price: nonNegativeNumberFromForm,
location_id: nullableIntIdFromForm.nullish(),
notes: z.string().max(5000).nullish(),
});
export const CreateReceiptSchema = z.object({
supplier_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.nullish(),
delivery_note_number: z.string().nullish(),
delivery_note_date: z.union([z.string(), z.null()]).nullish(),
notes: z.string().nullish(),
supplier_id: nullableIntIdFromForm.nullish(),
delivery_note_number: z.string().max(255).nullish(),
delivery_note_date: isoDateString.nullish(),
notes: z.string().max(5000).nullish(),
items: z
.array(ReceiptItemSchema)
.min(1, "Příjem musí obsahovat alespoň jednu položku"),
});
export const UpdateReceiptSchema = z.object({
supplier_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.nullish(),
delivery_note_number: z.string().nullish(),
delivery_note_date: z.union([z.string(), z.null()]).nullish(),
notes: z.string().nullish(),
supplier_id: nullableIntIdFromForm.nullish(),
delivery_note_number: z.string().max(255).nullish(),
delivery_note_date: isoDateString.nullish(),
notes: z.string().max(5000).nullish(),
items: z
.array(ReceiptItemSchema)
.min(1, "Příjem musí obsahovat alespoň jednu položku")
@@ -161,36 +129,24 @@ export type ReceiptItemInput = z.infer<typeof ReceiptItemSchema>;
// === Issues ===
export const IssueItemSchema = z.object({
item_id: z.union([z.number(), z.string()]).transform((v) => Number(v)),
batch_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.nullish(),
quantity: z.union([z.number(), z.string()]).transform((v) => Number(v)),
location_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.nullish(),
reservation_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.nullish(),
notes: z.string().nullish(),
item_id: intIdFromForm,
batch_id: nullableIntIdFromForm.nullish(),
quantity: positiveNumberFromForm,
location_id: nullableIntIdFromForm.nullish(),
reservation_id: nullableIntIdFromForm.nullish(),
notes: z.string().max(5000).nullish(),
});
export const CreateIssueSchema = z.object({
project_id: z.union([z.number(), z.string()]).transform((v) => Number(v)),
notes: z.string().nullish(),
project_id: intIdFromForm,
notes: z.string().max(5000).nullish(),
items: z
.array(IssueItemSchema)
.min(1, "Výdej musí obsahovat alespoň jednu položku"),
});
export const UpdateIssueSchema = z.object({
project_id: z
.union([z.number(), z.string()])
.transform((v) => Number(v))
.optional(),
notes: z.string().nullish(),
project_id: intIdFromForm.optional(),
notes: z.string().max(5000).nullish(),
items: z
.array(IssueItemSchema)
.min(1, "Výdej musí obsahovat alespoň jednu položku")
@@ -202,32 +158,29 @@ export type IssueItemInput = z.infer<typeof IssueItemSchema>;
// === Reservations ===
export const CreateReservationSchema = z.object({
item_id: z.union([z.number(), z.string()]).transform((v) => Number(v)),
project_id: z.union([z.number(), z.string()]).transform((v) => Number(v)),
quantity: z.union([z.number(), z.string()]).transform((v) => Number(v)),
notes: z.string().nullish(),
item_id: intIdFromForm,
project_id: intIdFromForm,
quantity: positiveNumberFromForm,
notes: z.string().max(5000).nullish(),
});
export type CreateReservationInput = z.infer<typeof CreateReservationSchema>;
// === Inventory ===
export const InventoryItemSchema = z.object({
item_id: z.union([z.number(), z.string()]).transform((v) => Number(v)),
location_id: z
.union([z.number(), z.string(), z.null()])
.transform((v) => (v === null ? null : Number(v)))
.nullish(),
actual_qty: z.union([z.number(), z.string()]).transform((v) => Number(v)),
notes: z.string().nullish(),
item_id: intIdFromForm,
location_id: nullableIntIdFromForm.nullish(),
actual_qty: nonNegativeNumberFromForm,
notes: z.string().max(5000).nullish(),
});
export const CreateInventorySessionSchema = z.object({
notes: z.string().nullish(),
notes: z.string().max(5000).nullish(),
items: z
.array(InventoryItemSchema)
.min(1, "Inventura musí obsahovat alespoň jednu položku"),
});
export const UpdateInventorySessionSchema = z.object({
notes: z.string().nullish(),
notes: z.string().max(5000).nullish(),
items: z
.array(InventoryItemSchema)
.min(1, "Inventura musí obsahovat alespoň jednu položku")