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:
@@ -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"),
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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"),
|
||||
]);
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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(""),
|
||||
})
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user