feat: add Zod validation schemas for all domain routes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-03-23 08:57:38 +01:00
parent a4303b0188
commit d2b22e9399
32 changed files with 819 additions and 140 deletions

View File

@@ -5,6 +5,18 @@ import { requireAuth, requirePermission } from '../../middleware/auth';
import { logAudit } from '../../services/audit';
import { success, error, parseId } from '../../utils/response';
import { parsePagination, buildPaginationMeta } from '../../utils/pagination';
import { parseBody } from '../../schemas/common';
import {
AttendanceNotesSchema,
AttendanceUpdateAddressSchema,
AttendanceSwitchProjectSchema,
AttendanceBalancesSchema,
AttendanceBulkSchema,
AttendanceLeaveSchema,
AttendancePunchSchema,
CreateAttendanceSchema,
UpdateAttendanceSchema,
} from '../../schemas/attendance.schema';
const VALID_LEAVE_TYPES = ['work', 'vacation', 'sick', 'holiday', 'unpaid'] as const;
@@ -175,7 +187,9 @@ export default async function attendanceRoutes(fastify: FastifyInstance): Promis
// POST /api/admin/attendance/notes — save shift notes
fastify.post('/notes', { preHandler: requireAuth }, async (request, reply) => {
const authData = request.authData!;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(AttendanceNotesSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const ongoing = await prisma.attendance.findFirst({
where: { user_id: authData.userId, departure_time: null, arrival_time: { not: null } },
@@ -194,9 +208,11 @@ export default async function attendanceRoutes(fastify: FastifyInstance): Promis
// POST /api/admin/attendance/update-address — update GPS address after punch
fastify.post('/update-address', { preHandler: requireAuth }, async (request, reply) => {
const authData = request.authData!;
const body = request.body as Record<string, unknown>;
const addr = body.address ? String(body.address).substring(0, 500) : null;
const action = String(body.punch_action || 'arrival');
const parsed = parseBody(AttendanceUpdateAddressSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const addr = body.address ?? null;
const action = body.punch_action;
const latest = await prisma.attendance.findFirst({
where: { user_id: authData.userId },
@@ -218,7 +234,9 @@ export default async function attendanceRoutes(fastify: FastifyInstance): Promis
// POST /api/admin/attendance/switch-project — switch active project on current shift
fastify.post('/switch-project', { preHandler: requireAuth }, async (request, reply) => {
const authData = request.authData!;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(AttendanceSwitchProjectSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const ongoing = await prisma.attendance.findFirst({
where: { user_id: authData.userId, departure_time: null, arrival_time: { not: null } },
@@ -686,7 +704,7 @@ export default async function attendanceRoutes(fastify: FastifyInstance): Promis
// POST /api/admin/attendance
fastify.post('/', { preHandler: requireAuth }, async (request, reply) => {
const body = request.body as Record<string, unknown>;
const rawBody = request.body as Record<string, unknown>;
const authData = request.authData!;
const postQuery = request.query as Record<string, unknown>;
@@ -696,25 +714,29 @@ export default async function attendanceRoutes(fastify: FastifyInstance): Promis
return error(reply, 'Nedostatečná oprávnění', 403);
}
const userId = Number(body.user_id);
const yr = Number(body.year) || new Date().getFullYear();
const actionType = String(body.action_type);
const balParsed = parseBody(AttendanceBalancesSchema, rawBody);
if ('error' in balParsed) return error(reply, balParsed.error, 400);
const balBody = balParsed.data;
const userId = balBody.user_id;
const yr = balBody.year || new Date().getFullYear();
const actionType = balBody.action_type;
if (actionType === 'edit') {
await prisma.leave_balances.upsert({
where: { user_id_year: { user_id: userId, year: yr } },
update: {
vacation_total: body.vacation_total != null ? Number(body.vacation_total) : undefined,
vacation_used: body.vacation_used != null ? Number(body.vacation_used) : undefined,
sick_used: body.sick_used != null ? Number(body.sick_used) : undefined,
vacation_total: balBody.vacation_total != null ? Number(balBody.vacation_total) : undefined,
vacation_used: balBody.vacation_used != null ? Number(balBody.vacation_used) : undefined,
sick_used: balBody.sick_used != null ? Number(balBody.sick_used) : undefined,
updated_at: new Date(),
},
create: {
user_id: userId,
year: yr,
vacation_total: Number(body.vacation_total) || 160,
vacation_used: Number(body.vacation_used) || 0,
sick_used: Number(body.sick_used) || 0,
vacation_total: Number(balBody.vacation_total) || 160,
vacation_used: Number(balBody.vacation_used) || 0,
sick_used: Number(balBody.sick_used) || 0,
},
});
@@ -748,19 +770,16 @@ export default async function attendanceRoutes(fastify: FastifyInstance): Promis
return error(reply, 'Nedostatečná oprávnění', 403);
}
const monthStr = String(body.month || '');
const userIds = (body.user_ids as number[]) || [];
const arrivalTime = String(body.arrival_time || '08:00');
const departureTime = String(body.departure_time || '16:30');
const breakStartTime = String(body.break_start_time || '12:00');
const breakEndTime = String(body.break_end_time || '12:30');
const bulkParsed = parseBody(AttendanceBulkSchema, rawBody);
if ('error' in bulkParsed) return error(reply, bulkParsed.error, 400);
const bulkBody = bulkParsed.data;
if (!monthStr || !/^\d{4}-\d{2}$/.test(monthStr)) {
return error(reply, 'Měsíc je povinný (formát YYYY-MM)', 400);
}
if (!userIds.length) {
return error(reply, 'Vyberte alespoň jednoho zaměstnance', 400);
}
const monthStr = bulkBody.month;
const userIds = bulkBody.user_ids;
const arrivalTime = bulkBody.arrival_time;
const departureTime = bulkBody.departure_time;
const breakStartTime = bulkBody.break_start_time;
const breakEndTime = bulkBody.break_end_time;
const [yrStr, moStr] = monthStr.split('-');
const yr = Number(yrStr);
@@ -821,10 +840,13 @@ export default async function attendanceRoutes(fastify: FastifyInstance): Promis
// --- action=leave: add leave record directly ---
if (postQuery.action === 'leave') {
const userId = body.user_id ? Number(body.user_id) : authData.userId;
const dateFrom = String(body.date_from || '');
const dateTo = String(body.date_to || dateFrom);
const leaveTypeStr = String(body.leave_type || 'vacation');
const leaveParsed = parseBody(AttendanceLeaveSchema, rawBody);
if ('error' in leaveParsed) return error(reply, leaveParsed.error, 400);
const leaveBody = leaveParsed.data;
const userId = leaveBody.user_id ?? authData.userId;
const dateFrom = leaveBody.date_from;
const dateTo = leaveBody.date_to || dateFrom;
const leaveTypeStr = leaveBody.leave_type;
if (!VALID_LEAVE_TYPES.includes(leaveTypeStr as typeof VALID_LEAVE_TYPES[number])) {
return error(reply, 'Neplatný typ nepřítomnosti', 400);
}
@@ -847,8 +869,8 @@ export default async function attendanceRoutes(fastify: FastifyInstance): Promis
user_id: userId,
shift_date: shiftDate,
leave_type: leaveType,
leave_hours: body.leave_hours ? Number(body.leave_hours) : 8,
notes: body.notes ? String(body.notes) : null,
leave_hours: leaveBody.leave_hours ? Number(leaveBody.leave_hours) : 8,
notes: leaveBody.notes ? String(leaveBody.notes) : null,
},
});
created++;
@@ -857,7 +879,7 @@ export default async function attendanceRoutes(fastify: FastifyInstance): Promis
}
// Update leave balance for vacation/sick (matching PHP updateLeaveBalance)
const totalLeaveHours = created * (body.leave_hours ? Number(body.leave_hours) : 8);
const totalLeaveHours = created * (leaveBody.leave_hours ? Number(leaveBody.leave_hours) : 8);
if ((leaveType === 'vacation' || leaveType === 'sick') && totalLeaveHours > 0) {
const year = new Date(dateFrom).getFullYear();
const existingBalance = await prisma.leave_balances.findFirst({
@@ -886,17 +908,20 @@ export default async function attendanceRoutes(fastify: FastifyInstance): Promis
}
// Punch action (arrival / departure / break_start) from Dashboard or Attendance page
if (body.punch_action) {
const action = String(body.punch_action);
if (rawBody.punch_action) {
const punchParsed = parseBody(AttendancePunchSchema, rawBody);
if ('error' in punchParsed) return error(reply, punchParsed.error, 400);
const punchBody = punchParsed.data;
const action = punchBody.punch_action;
const now = new Date();
// Use noon UTC to avoid timezone shift issues with Prisma/MySQL DATE columns
const y = now.getFullYear(), m = now.getMonth(), d = now.getDate();
const today = new Date(Date.UTC(y, m, d, 12, 0, 0));
const gpsLat = body.latitude != null && body.latitude !== '' ? Number(body.latitude) : null;
const gpsLng = body.longitude != null && body.longitude !== '' ? Number(body.longitude) : null;
const gpsAcc = body.accuracy != null && body.accuracy !== '' ? Number(body.accuracy) : null;
const gpsAddr = body.address ? String(body.address).substring(0, 500) : null;
const gpsLat = punchBody.latitude != null && punchBody.latitude !== '' ? Number(punchBody.latitude) : null;
const gpsLng = punchBody.longitude != null && punchBody.longitude !== '' ? Number(punchBody.longitude) : null;
const gpsAcc = punchBody.accuracy != null && punchBody.accuracy !== '' ? Number(punchBody.accuracy) : null;
const gpsAddr = punchBody.address ?? null;
// Round arrival UP to nearest 15 min, departure DOWN
const roundUp15 = (d: Date) => {
@@ -1012,30 +1037,34 @@ export default async function attendanceRoutes(fastify: FastifyInstance): Promis
}
// Standard attendance record creation (from admin forms)
const stdParsed = parseBody(CreateAttendanceSchema, rawBody);
if ('error' in stdParsed) return error(reply, stdParsed.error, 400);
const body = stdParsed.data;
const record = await prisma.attendance.create({
data: {
user_id: body.user_id ? Number(body.user_id) : authData.userId,
shift_date: new Date(String(body.shift_date)),
arrival_time: body.arrival_time ? new Date(String(body.arrival_time)) : null,
arrival_lat: body.arrival_lat ? Number(body.arrival_lat) : null,
arrival_lng: body.arrival_lng ? Number(body.arrival_lng) : null,
arrival_accuracy: body.arrival_accuracy ? Number(body.arrival_accuracy) : null,
arrival_address: body.arrival_address ? String(body.arrival_address) : null,
departure_time: body.departure_time ? new Date(String(body.departure_time)) : null,
departure_lat: body.departure_lat ? Number(body.departure_lat) : null,
departure_lng: body.departure_lng ? Number(body.departure_lng) : null,
departure_accuracy: body.departure_accuracy ? Number(body.departure_accuracy) : null,
departure_address: body.departure_address ? String(body.departure_address) : null,
notes: body.notes ? String(body.notes) : null,
project_id: body.project_id ? Number(body.project_id) : null,
leave_type: (body.leave_type ? String(body.leave_type) : 'work') as attendance_leave_type,
leave_hours: body.leave_hours ? Number(body.leave_hours) : null,
user_id: body.user_id ?? authData.userId,
shift_date: new Date(body.shift_date),
arrival_time: body.arrival_time ? new Date(body.arrival_time) : null,
arrival_lat: body.arrival_lat ?? null,
arrival_lng: body.arrival_lng ?? null,
arrival_accuracy: body.arrival_accuracy ?? null,
arrival_address: body.arrival_address ?? null,
departure_time: body.departure_time ? new Date(body.departure_time) : null,
departure_lat: body.departure_lat ?? null,
departure_lng: body.departure_lng ?? null,
departure_accuracy: body.departure_accuracy ?? null,
departure_address: body.departure_address ?? null,
notes: body.notes ?? null,
project_id: body.project_id ?? null,
leave_type: body.leave_type as attendance_leave_type,
leave_hours: body.leave_hours ?? null,
},
});
// Save project logs if provided
if (Array.isArray(body.project_logs)) {
const logs = (body.project_logs as Array<Record<string, unknown>>)
const logs = body.project_logs
.filter(l => l.project_id && (Number(l.hours) > 0 || Number(l.minutes) > 0));
if (logs.length > 0) {
await prisma.attendance_project_logs.createMany({
@@ -1065,7 +1094,9 @@ export default async function attendanceRoutes(fastify: FastifyInstance): Promis
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requireAuth }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(UpdateAttendanceSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const existing = await prisma.attendance.findUnique({ where: { id } });
if (!existing) return error(reply, 'Záznam nenalezen', 404);
@@ -1096,7 +1127,7 @@ export default async function attendanceRoutes(fastify: FastifyInstance): Promis
// Delete existing logs for this record
await prisma.attendance_project_logs.deleteMany({ where: { attendance_id: id } });
// Insert new ones (skip entries with no project_id or zero time)
const logs = (body.project_logs as Array<Record<string, unknown>>)
const logs = body.project_logs
.filter(l => l.project_id && (Number(l.hours) > 0 || Number(l.minutes) > 0));
if (logs.length > 0) {
await prisma.attendance_project_logs.createMany({

View File

@@ -3,6 +3,8 @@ import prisma from '../../config/database';
import { requirePermission } from '../../middleware/auth';
import { logAudit } from '../../services/audit';
import { success, error, parseId } from '../../utils/response';
import { parseBody } from '../../schemas/common';
import { CreateBankAccountSchema, UpdateBankAccountSchema } from '../../schemas/bank-accounts.schema';
export default async function bankAccountsRoutes(fastify: FastifyInstance): Promise<void> {
fastify.get('/', { preHandler: requirePermission('offers.settings') }, async (_request, reply) => {
@@ -11,7 +13,9 @@ export default async function bankAccountsRoutes(fastify: FastifyInstance): Prom
});
fastify.post('/', { preHandler: requirePermission('offers.settings') }, async (request, reply) => {
const body = request.body as Record<string, unknown>;
const parsed = parseBody(CreateBankAccountSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const account = await prisma.bank_accounts.create({
data: {
account_name: body.account_name ? String(body.account_name) : null,
@@ -32,7 +36,9 @@ export default async function bankAccountsRoutes(fastify: FastifyInstance): Prom
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('offers.settings') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(UpdateBankAccountSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const existing = await prisma.bank_accounts.findUnique({ where: { id } });
if (!existing) return error(reply, 'Účet nenalezen', 404);

View File

@@ -4,6 +4,8 @@ import { requireAuth, requirePermission } from '../../middleware/auth';
import { logAudit } from '../../services/audit';
import { success, error } from '../../utils/response';
import multipart from '@fastify/multipart';
import { parseBody } from '../../schemas/common';
import { UpdateCompanySettingsSchema } from '../../schemas/company-settings.schema';
/** Encode custom_fields + supplier_field_order into a single JSON blob (matching PHP format) */
function encodeCustomFields(fields: unknown, fieldOrder: unknown): string | null {
@@ -142,7 +144,9 @@ export default async function companySettingsRoutes(fastify: FastifyInstance): P
});
fastify.put('/', { preHandler: requirePermission('offers.settings') }, async (request, reply) => {
const body = request.body as Record<string, unknown>;
const parsed = parseBody(UpdateCompanySettingsSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const existing = await prisma.company_settings.findFirst();
if (!existing) return error(reply, 'Nastavení nenalezeno', 404);

View File

@@ -4,6 +4,8 @@ import { requireAuth, requirePermission } from '../../middleware/auth';
import { logAudit } from '../../services/audit';
import { success, error, parseId } from '../../utils/response';
import { parsePagination, buildPaginationMeta } from '../../utils/pagination';
import { parseBody } from '../../schemas/common';
import { CreateCustomerSchema, UpdateCustomerSchema } from '../../schemas/customers.schema';
const ALLOWED_SORT_FIELDS = ['id', 'name', 'company_id', 'city', 'country'];
@@ -69,10 +71,11 @@ export default async function customersRoutes(fastify: FastifyInstance): Promise
});
fastify.post('/', { preHandler: requirePermission('customers.manage') }, async (request, reply) => {
const body = request.body as Record<string, unknown>;
const parsed = parseBody(CreateCustomerSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const name = body.name ? String(body.name).trim() : '';
if (!name) return error(reply, 'Název zákazníka je povinný', 400);
const name = body.name;
const customer = await prisma.customers.create({
data: {
@@ -94,7 +97,9 @@ export default async function customersRoutes(fastify: FastifyInstance): Promise
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('customers.manage') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(UpdateCustomerSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const existing = await prisma.customers.findUnique({ where: { id } });
if (!existing) return error(reply, 'Zákazník nenalezen', 404);

View File

@@ -5,6 +5,8 @@ import { logAudit } from '../../services/audit';
import { success, error, parseId } from '../../utils/response';
import { parsePagination, buildPaginationMeta } from '../../utils/pagination';
import { getNextNumber } from '../../utils/sequence';
import { parseBody } from '../../schemas/common';
import { CreateInvoiceSchema, UpdateInvoiceSchema } from '../../schemas/invoices.schema';
// Status transition rules matching PHP
const VALID_TRANSITIONS: Record<string, string[]> = {
@@ -236,7 +238,9 @@ export default async function invoicesRoutes(fastify: FastifyInstance): Promise<
// POST /api/admin/invoices
fastify.post('/', { preHandler: requirePermission('invoices.create') }, async (request, reply) => {
const body = request.body as Record<string, unknown>;
const parsed = parseBody(CreateInvoiceSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const invoice = await prisma.invoices.create({
data: {
@@ -285,7 +289,9 @@ export default async function invoicesRoutes(fastify: FastifyInstance): Promise<
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('invoices.edit') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(UpdateInvoiceSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const existing = await prisma.invoices.findUnique({ where: { id } });
if (!existing) return error(reply, 'Faktura nenalezena', 404);

View File

@@ -5,6 +5,8 @@ import { requireAuth, requirePermission } from '../../middleware/auth';
import { logAudit } from '../../services/audit';
import { success, error, parseId } from '../../utils/response';
import { parsePagination, buildPaginationMeta } from '../../utils/pagination';
import { parseBody } from '../../schemas/common';
import { CreateLeaveRequestSchema, ReviewLeaveRequestSchema } from '../../schemas/leave-requests.schema';
const VALID_LEAVE_TYPES = ['vacation', 'sick', 'unpaid'] as const;
const VALID_REVIEW_STATUSES = ['approved', 'rejected'] as const;
@@ -36,20 +38,18 @@ export default async function leaveRequestsRoutes(fastify: FastifyInstance): Pro
});
fastify.post('/', { preHandler: requireAuth }, async (request, reply) => {
const body = request.body as Record<string, unknown>;
const parsed = parseBody(CreateLeaveRequestSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const authData = request.authData!;
const leaveType = String(body.leave_type || '');
const leaveType = body.leave_type;
if (!VALID_LEAVE_TYPES.includes(leaveType as typeof VALID_LEAVE_TYPES[number])) {
return error(reply, 'Neplatný typ nepřítomnosti', 400);
}
if (!body.date_from || !body.date_to) {
return error(reply, 'Datum od a do je povinné', 400);
}
const dateFrom = new Date(String(body.date_from));
const dateTo = new Date(String(body.date_to));
const dateFrom = new Date(body.date_from);
const dateTo = new Date(body.date_to);
if (isNaN(dateFrom.getTime()) || isNaN(dateTo.getTime())) {
return error(reply, 'Neplatné datum', 400);
@@ -92,10 +92,12 @@ export default async function leaveRequestsRoutes(fastify: FastifyInstance): Pro
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('attendance.approve') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(ReviewLeaveRequestSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const authData = request.authData!;
const status = String(body.status || '');
const status = body.status;
if (!VALID_REVIEW_STATUSES.includes(status as typeof VALID_REVIEW_STATUSES[number])) {
return error(reply, 'Neplatný stav', 400);
}

View File

@@ -4,6 +4,8 @@ import { requirePermission } from '../../middleware/auth';
import { logAudit } from '../../services/audit';
import { success, error, parseId } from '../../utils/response';
import { parsePagination, buildPaginationMeta } from '../../utils/pagination';
import { parseBody } from '../../schemas/common';
import { CreateOrderFromQuotationSchema, CreateOrderSchema, UpdateOrderSchema } from '../../schemas/orders.schema';
import multipart from '@fastify/multipart';
@@ -275,12 +277,14 @@ export default async function ordersRoutes(fastify: FastifyInstance): Promise<vo
}
// === JSON body — either from-quotation (no attachment) or manual order ===
const body = request.body as Record<string, unknown>;
const rawBody = request.body as Record<string, unknown>;
// From-quotation flow via JSON (no attachment)
if (body.quotationId) {
const quotationId = Number(body.quotationId);
const customerOrderNumber = body.customerOrderNumber ? String(body.customerOrderNumber) : '';
if (rawBody.quotationId) {
const fromQuotParsed = parseBody(CreateOrderFromQuotationSchema, rawBody);
if ('error' in fromQuotParsed) return error(reply, fromQuotParsed.error, 400);
const quotationId = fromQuotParsed.data.quotationId;
const customerOrderNumber = fromQuotParsed.data.customerOrderNumber;
if (!quotationId || isNaN(quotationId)) {
return error(reply, 'Chybí ID nabídky', 400);
@@ -369,21 +373,25 @@ export default async function ordersRoutes(fastify: FastifyInstance): Promise<vo
}
// Manual order creation
const manualParsed = parseBody(CreateOrderSchema, rawBody);
if ('error' in manualParsed) return error(reply, manualParsed.error, 400);
const body = manualParsed.data;
const order = await prisma.orders.create({
data: {
order_number: body.order_number ? String(body.order_number) : null,
customer_order_number: body.customer_order_number ? String(body.customer_order_number) : null,
quotation_id: body.quotation_id ? Number(body.quotation_id) : null,
customer_id: body.customer_id ? Number(body.customer_id) : null,
status: body.status ? String(body.status) : 'prijata',
currency: body.currency ? String(body.currency) : 'CZK',
language: body.language ? String(body.language) : 'cs',
vat_rate: body.vat_rate ? Number(body.vat_rate) : 21.0,
order_number: body.order_number ?? null,
customer_order_number: body.customer_order_number ?? null,
quotation_id: body.quotation_id ?? null,
customer_id: body.customer_id ?? null,
status: body.status,
currency: body.currency,
language: body.language,
vat_rate: body.vat_rate,
apply_vat: body.apply_vat !== false,
exchange_rate: body.exchange_rate ? Number(body.exchange_rate) : 1.0,
scope_title: body.scope_title ? String(body.scope_title) : null,
scope_description: body.scope_description ? String(body.scope_description) : null,
notes: body.notes ? String(body.notes) : null,
exchange_rate: body.exchange_rate,
scope_title: body.scope_title ?? null,
scope_description: body.scope_description ?? null,
notes: body.notes ?? null,
},
});
@@ -421,7 +429,9 @@ export default async function ordersRoutes(fastify: FastifyInstance): Promise<vo
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('orders.edit') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(UpdateOrderSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const existing = await prisma.orders.findUnique({ where: { id } });
if (!existing) return error(reply, 'Objednávka nenalezena', 404);

View File

@@ -5,6 +5,8 @@ import { success, error } from '../../utils/response';
import bcrypt from 'bcryptjs';
import { config } from '../../config/env';
import { logAudit } from '../../services/audit';
import { parseBody } from '../../schemas/common';
import { UpdateProfileSchema } from '../../schemas/profile.schema';
export default async function profileRoutes(fastify: FastifyInstance): Promise<void> {
fastify.get('/', { preHandler: requireAuth }, async (request, reply) => {
@@ -21,7 +23,9 @@ export default async function profileRoutes(fastify: FastifyInstance): Promise<v
});
fastify.put('/', { preHandler: requireAuth }, async (request, reply) => {
const body = request.body as Record<string, unknown>;
const parsed = parseBody(UpdateProfileSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const userId = request.authData!.userId;
const data: Record<string, unknown> = {};

View File

@@ -4,6 +4,8 @@ import { requirePermission } from '../../middleware/auth';
import { logAudit } from '../../services/audit';
import { success, error, parseId } from '../../utils/response';
import { parsePagination, buildPaginationMeta } from '../../utils/pagination';
import { parseBody } from '../../schemas/common';
import { CreateProjectSchema, UpdateProjectSchema, CreateProjectNoteSchema } from '../../schemas/projects.schema';
const PROJECT_ALLOWED_SORT_FIELDS = ['id', 'project_number', 'name', 'status', 'created_at'];
@@ -52,7 +54,9 @@ export default async function projectsRoutes(fastify: FastifyInstance): Promise<
});
fastify.post('/', { preHandler: requirePermission('projects.create') }, async (request, reply) => {
const body = request.body as Record<string, unknown>;
const parsed = parseBody(CreateProjectSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const project = await prisma.projects.create({
data: {
@@ -76,7 +80,9 @@ export default async function projectsRoutes(fastify: FastifyInstance): Promise<
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('projects.edit') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(UpdateProjectSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const existing = await prisma.projects.findUnique({ where: { id } });
if (!existing) return error(reply, 'Projekt nenalezen', 404);
@@ -100,7 +106,9 @@ export default async function projectsRoutes(fastify: FastifyInstance): Promise<
fastify.post<{ Params: { id: string } }>('/:id/notes', { preHandler: requirePermission('projects.edit') }, async (request, reply) => {
const projectId = parseId(request.params.id, reply);
if (projectId === null) return;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(CreateProjectNoteSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const authData = request.authData!;
const note = await prisma.project_notes.create({

View File

@@ -4,6 +4,8 @@ import { requirePermission } from '../../middleware/auth';
import { logAudit } from '../../services/audit';
import { success, error, parseId } from '../../utils/response';
import { parsePagination, buildPaginationMeta } from '../../utils/pagination';
import { parseBody } from '../../schemas/common';
import { CreateQuotationSchema, UpdateQuotationSchema } from '../../schemas/offers.schema';
interface QuotationItemInput { description?: string; item_description?: string; quantity?: number; unit?: string; unit_price?: number; is_included_in_total?: boolean; position?: number }
@@ -199,7 +201,9 @@ export default async function quotationsRoutes(fastify: FastifyInstance): Promis
});
fastify.post('/', { preHandler: requirePermission('offers.create') }, async (request, reply) => {
const body = request.body as Record<string, unknown>;
const parsed = parseBody(CreateQuotationSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const quotation = await prisma.quotations.create({
data: {
@@ -252,7 +256,9 @@ export default async function quotationsRoutes(fastify: FastifyInstance): Promis
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('offers.edit') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(UpdateQuotationSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const existing = await prisma.quotations.findUnique({ where: { id } });
if (!existing) return error(reply, 'Nabídka nenalezena', 404);

View File

@@ -6,6 +6,8 @@ import { requirePermission } from '../../middleware/auth';
import { logAudit } from '../../services/audit';
import { success, error, parseId } from '../../utils/response';
import { parsePagination, buildPaginationMeta } from '../../utils/pagination';
import { parseBody } from '../../schemas/common';
import { CreateReceivedInvoiceSchema, UpdateReceivedInvoiceSchema } from '../../schemas/received-invoices.schema';
const VALID_STATUSES = ['unpaid', 'paid'] as const;
const ALLOWED_SORT_FIELDS = ['id', 'supplier_name', 'amount', 'issue_date', 'due_date', 'status', 'created_at'];
@@ -173,16 +175,16 @@ export default async function receivedInvoicesRoutes(fastify: FastifyInstance):
}
// JSON body: single invoice creation (no file)
const body = request.body as Record<string, unknown>;
const status = body.status ? String(body.status) : 'unpaid';
const parsed = parseBody(CreateReceivedInvoiceSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const status = body.status;
if (!VALID_STATUSES.includes(status as typeof VALID_STATUSES[number])) {
return error(reply, 'Neplatný stav', 400);
}
const amount = Number(body.amount ?? 0);
const vatRate = Number(body.vat_rate ?? 21);
if (!body.supplier_name) return error(reply, 'Název dodavatele je povinný', 400);
const amount = body.amount;
const vatRate = body.vat_rate;
const invoice = await prisma.received_invoices.create({
data: {
month: Number(body.month),
@@ -209,7 +211,9 @@ export default async function receivedInvoicesRoutes(fastify: FastifyInstance):
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('invoices.edit') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(UpdateReceivedInvoiceSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const existing = await prisma.received_invoices.findUnique({ where: { id } });
if (!existing) return error(reply, 'Přijatá faktura nenalezena', 404);

View File

@@ -3,6 +3,8 @@ import prisma from '../../config/database';
import { requirePermission } from '../../middleware/auth';
import { logAudit } from '../../services/audit';
import { success, error, parseId } from '../../utils/response';
import { parseBody } from '../../schemas/common';
import { CreateRoleSchema, UpdateRoleSchema } from '../../schemas/roles.schema';
export default async function rolesRoutes(fastify: FastifyInstance): Promise<void> {
// GET /api/admin/roles
@@ -32,7 +34,9 @@ export default async function rolesRoutes(fastify: FastifyInstance): Promise<voi
// POST /api/admin/roles
fastify.post('/', { preHandler: requirePermission('settings.roles') }, async (request, reply) => {
const body = request.body as Record<string, unknown>;
const parsed = parseBody(CreateRoleSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const role = await prisma.roles.create({
data: {
@@ -67,7 +71,9 @@ export default async function rolesRoutes(fastify: FastifyInstance): Promise<voi
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('settings.roles') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(UpdateRoleSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const existing = await prisma.roles.findUnique({ where: { id } });
if (!existing) return error(reply, 'Role nenalezena', 404);

View File

@@ -2,6 +2,8 @@ import { FastifyInstance } from 'fastify';
import prisma from '../../config/database';
import { requirePermission } from '../../middleware/auth';
import { success, error, parseId } from '../../utils/response';
import { parseBody } from '../../schemas/common';
import { CreateScopeTemplateSchema, CreateItemTemplateSchema, UpdateScopeTemplateSchema } from '../../schemas/scope-templates.schema';
interface ScopeSectionInput { title?: string; title_cz?: string; content?: string; position?: number }
@@ -32,9 +34,11 @@ export default async function scopeTemplatesRoutes(fastify: FastifyInstance): Pr
// Item template CRUD via ?action=item
fastify.post('/', { preHandler: requirePermission('offers.settings') }, async (request, reply) => {
const query = request.query as Record<string, unknown>;
const body = request.body as Record<string, unknown>;
if (String(query.action) === 'item') {
const itemParsed = parseBody(CreateItemTemplateSchema, request.body);
if ('error' in itemParsed) return error(reply, itemParsed.error, 400);
const body = itemParsed.data;
const itemData = {
name: body.name ? String(body.name) : null,
description: body.description ? String(body.description) : null,
@@ -58,6 +62,10 @@ export default async function scopeTemplatesRoutes(fastify: FastifyInstance): Pr
}
// Scope template create (original logic below)
const scopeParsed = parseBody(CreateScopeTemplateSchema, request.body);
if ('error' in scopeParsed) return error(reply, scopeParsed.error, 400);
const body = scopeParsed.data;
const template = await prisma.scope_templates.create({
data: {
name: body.name ? String(body.name) : null,
@@ -108,7 +116,9 @@ export default async function scopeTemplatesRoutes(fastify: FastifyInstance): Pr
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('offers.settings') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(UpdateScopeTemplateSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const existing = await prisma.scope_templates.findUnique({ where: { id } });
if (!existing) return error(reply, 'Šablona nenalezena', 404);

View File

@@ -4,6 +4,8 @@ import { requireAuth, requirePermission } from '../../middleware/auth';
import { logAudit } from '../../services/audit';
import { success, error } from '../../utils/response';
import { parsePagination, buildPaginationMeta } from '../../utils/pagination';
import { parseBody } from '../../schemas/common';
import { CreateTripSchema, UpdateTripSchema } from '../../schemas/trips.schema';
export default async function tripsRoutes(fastify: FastifyInstance): Promise<void> {
fastify.get('/', { preHandler: requireAuth }, async (request, reply) => {
@@ -118,7 +120,9 @@ export default async function tripsRoutes(fastify: FastifyInstance): Promise<voi
});
fastify.post('/', { preHandler: requireAuth }, async (request, reply) => {
const body = request.body as Record<string, unknown>;
const parsed = parseBody(CreateTripSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const authData = request.authData!;
const trip = await prisma.trips.create({
@@ -148,7 +152,9 @@ export default async function tripsRoutes(fastify: FastifyInstance): Promise<voi
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requireAuth }, async (request, reply) => {
const id = parseInt(request.params.id, 10);
if (isNaN(id)) return error(reply, 'Neplatné ID', 400);
const body = request.body as Record<string, unknown>;
const parsed = parseBody(UpdateTripSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const authData = request.authData!;
const existing = await prisma.trips.findUnique({ where: { id } });

View File

@@ -6,6 +6,8 @@ import { success, error, parseId } from '../../utils/response';
import { parsePagination, buildPaginationMeta } from '../../utils/pagination';
import bcrypt from 'bcryptjs';
import { config } from '../../config/env';
import { parseBody } from '../../schemas/common';
import { CreateUserSchema, UpdateUserSchema } from '../../schemas/users.schema';
const ALLOWED_SORT_FIELDS = ['id', 'username', 'email', 'first_name', 'last_name', 'created_at'];
@@ -69,25 +71,17 @@ export default async function usersRoutes(fastify: FastifyInstance): Promise<voi
// POST /api/admin/users
fastify.post('/', { preHandler: requirePermission('users.create') }, async (request, reply) => {
const body = request.body as Record<string, unknown>;
const parsed = parseBody(CreateUserSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const username = body.username ? String(body.username).trim() : '';
const email = body.email ? String(body.email).trim() : '';
const password = body.password ? String(body.password) : '';
const firstName = body.first_name ? String(body.first_name).trim() : '';
const lastName = body.last_name ? String(body.last_name).trim() : '';
const username = body.username.trim();
const email = body.email.trim();
const password = body.password;
const firstName = body.first_name.trim();
const lastName = body.last_name.trim();
const roleId = body.role_id;
// Required fields
if (!username || !email || !password || !firstName || !lastName || !roleId) {
return error(reply, 'Všechna pole jsou povinná', 400);
}
// Password length
if (password.length < 8) {
return error(reply, 'Heslo musí mít alespoň 8 znaků', 400);
}
// Email format
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return error(reply, 'Neplatný formát e-mailu', 400);
@@ -135,7 +129,9 @@ export default async function usersRoutes(fastify: FastifyInstance): Promise<voi
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('users.edit') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(UpdateUserSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const existing = await prisma.users.findUnique({ where: { id } });
if (!existing) return error(reply, 'Uživatel nenalezen', 404);

View File

@@ -3,6 +3,8 @@ import prisma from '../../config/database';
import { requirePermission } from '../../middleware/auth';
import { logAudit } from '../../services/audit';
import { success, error, parseId } from '../../utils/response';
import { parseBody } from '../../schemas/common';
import { CreateVehicleSchema, UpdateVehicleSchema } from '../../schemas/vehicles.schema';
export default async function vehiclesRoutes(fastify: FastifyInstance): Promise<void> {
fastify.get('/', { preHandler: requirePermission('trips.vehicles') }, async (_request, reply) => {
@@ -29,16 +31,17 @@ export default async function vehiclesRoutes(fastify: FastifyInstance): Promise<
});
fastify.post('/', { preHandler: requirePermission('trips.vehicles') }, async (request, reply) => {
const body = request.body as Record<string, unknown>;
if (!body.spz || !body.name) return error(reply, 'SPZ a název jsou povinné', 400);
const parsed = parseBody(CreateVehicleSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const vehicle = await prisma.vehicles.create({
data: {
spz: String(body.spz),
name: String(body.name),
brand: body.brand ? String(body.brand) : null,
model: body.model ? String(body.model) : null,
initial_km: body.initial_km ? Number(body.initial_km) : 0,
actual_km: body.actual_km ? Number(body.actual_km) : 0,
spz: body.spz,
name: body.name,
brand: body.brand ?? null,
model: body.model ?? null,
initial_km: body.initial_km,
actual_km: body.actual_km,
is_active: body.is_active !== false,
},
});
@@ -50,7 +53,9 @@ export default async function vehiclesRoutes(fastify: FastifyInstance): Promise<
fastify.put<{ Params: { id: string } }>('/:id', { preHandler: requirePermission('trips.vehicles') }, async (request, reply) => {
const id = parseId(request.params.id, reply);
if (id === null) return;
const body = request.body as Record<string, unknown>;
const parsed = parseBody(UpdateVehicleSchema, request.body);
if ('error' in parsed) return error(reply, parsed.error, 400);
const body = parsed.data;
const existing = await prisma.vehicles.findUnique({ where: { id } });
if (!existing) return error(reply, 'Vozidlo nenalezeno', 404);

View File

@@ -0,0 +1,97 @@
import { z } from 'zod';
export const AttendanceNotesSchema = z.object({
notes: z.string().nullish(),
});
export const AttendanceUpdateAddressSchema = z.object({
address: z.string().max(500).nullish(),
punch_action: z.string().optional().default('arrival'),
});
export const AttendanceSwitchProjectSchema = z.object({
project_id: z.union([z.number(), z.string()]).transform(v => Number(v)).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(),
action_type: z.string(),
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(),
});
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.number()).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'),
});
export const AttendanceLeaveSchema = z.object({
user_id: z.union([z.number(), z.string()]).transform(v => Number(v)).optional(),
date_from: z.string().min(1, 'Datum je povinné'),
date_to: z.string().optional(),
leave_type: z.string().optional().default('vacation'),
leave_hours: z.union([z.number(), z.string()]).transform(v => Number(v)).optional(),
notes: z.string().nullish(),
});
const ProjectLogSchema = z.object({
project_id: z.union([z.number(), z.string()]).transform(v => Number(v)),
hours: z.number().optional().default(0),
minutes: z.number().optional().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(),
address: z.string().max(500).nullish(),
});
export const CreateAttendanceSchema = z.object({
user_id: z.union([z.number(), z.string()]).transform(v => Number(v)).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_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_address: z.string().nullish(),
notes: z.string().nullish(),
project_id: z.union([z.number(), z.string()]).transform(v => Number(v)).nullish(),
leave_type: z.string().optional().default('work'),
leave_hours: z.union([z.number(), z.string()]).transform(v => Number(v)).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(),
notes: z.string().nullish(),
project_id: z.union([z.number(), z.string(), z.null()]).optional(),
leave_type: z.string().optional(),
leave_hours: z.union([z.number(), z.string(), z.null()]).optional(),
project_logs: z.array(ProjectLogSchema).optional(),
});
export type AttendanceNotesInput = z.infer<typeof AttendanceNotesSchema>;
export type AttendanceUpdateAddressInput = z.infer<typeof AttendanceUpdateAddressSchema>;
export type AttendanceSwitchProjectInput = z.infer<typeof AttendanceSwitchProjectSchema>;
export type AttendanceBalancesInput = z.infer<typeof AttendanceBalancesSchema>;
export type AttendanceBulkInput = z.infer<typeof AttendanceBulkSchema>;
export type AttendanceLeaveInput = z.infer<typeof AttendanceLeaveSchema>;
export type AttendancePunchInput = z.infer<typeof AttendancePunchSchema>;
export type CreateAttendanceInput = z.infer<typeof CreateAttendanceSchema>;
export type UpdateAttendanceInput = z.infer<typeof UpdateAttendanceSchema>;

View File

@@ -0,0 +1,26 @@
import { z } from 'zod';
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.any().optional().default(false),
position: z.union([z.number(), z.string()]).transform(v => Number(v)).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.any().optional(),
position: z.union([z.number(), z.string()]).transform(v => Number(v)).optional(),
});
export type CreateBankAccountInput = z.infer<typeof CreateBankAccountSchema>;
export type UpdateBankAccountInput = z.infer<typeof UpdateBankAccountSchema>;

View File

@@ -0,0 +1,21 @@
import { z } from 'zod';
export const UpdateCompanySettingsSchema = z.object({
company_name: z.string().nullish(),
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(),
quotation_prefix: z.string().nullish(),
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(),
require_2fa: z.any().optional(),
custom_fields: z.array(z.any()).optional(),
supplier_field_order: z.array(z.any()).optional(),
});
export type UpdateCompanySettingsInput = z.infer<typeof UpdateCompanySettingsSchema>;

View File

@@ -0,0 +1,28 @@
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(),
customer_field_order: z.array(z.string()).optional(),
});
export const UpdateCustomerSchema = z.object({
name: z.string().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(),
customer_field_order: z.array(z.string()).optional(),
});
export type CreateCustomerInput = z.infer<typeof CreateCustomerSchema>;
export type UpdateCustomerInput = z.infer<typeof UpdateCustomerSchema>;

View File

@@ -0,0 +1,58 @@
import { z } from 'zod';
const InvoiceItemSchema = z.object({
description: z.string().nullish(),
quantity: z.number().optional().default(1),
unit: z.string().nullish(),
unit_price: z.number().optional().default(0),
vat_rate: z.number().optional().default(21.0),
position: z.number().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.any().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(),
notes: z.string().nullish(),
internal_notes: z.string().nullish(),
items: z.array(InvoiceItemSchema).optional(),
});
export const UpdateInvoiceSchema = z.object({
status: z.string().optional(),
currency: 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()]).optional(),
vat_rate: z.union([z.number(), z.string()]).transform(v => Number(v)).optional(),
apply_vat: z.any().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(),
items: z.array(InvoiceItemSchema).optional(),
});
export type CreateInvoiceInput = z.infer<typeof CreateInvoiceSchema>;
export type UpdateInvoiceInput = z.infer<typeof UpdateInvoiceSchema>;

View File

@@ -0,0 +1,16 @@
import { z } from 'zod';
export const CreateLeaveRequestSchema = z.object({
leave_type: z.string().min(1, 'Typ nepřítomnosti je povinný'),
date_from: z.string().min(1, 'Datum od je povinné'),
date_to: z.string().min(1, 'Datum do je povinné'),
notes: z.string().nullish(),
});
export const ReviewLeaveRequestSchema = z.object({
status: z.string().min(1, 'Stav je povinný'),
reviewer_note: z.string().nullish(),
});
export type CreateLeaveRequestInput = z.infer<typeof CreateLeaveRequestSchema>;
export type ReviewLeaveRequestInput = z.infer<typeof ReviewLeaveRequestSchema>;

View File

@@ -0,0 +1,55 @@
import { z } from 'zod';
const QuotationItemSchema = z.object({
description: z.string().nullish(),
item_description: z.string().nullish(),
quantity: z.number().optional().default(1),
unit: z.string().nullish(),
unit_price: z.number().optional().default(0),
is_included_in_total: z.any().optional().default(true),
position: z.number().optional(),
});
const ScopeSectionSchema = z.object({
title: z.string().nullish(),
title_cz: z.string().nullish(),
content: z.string().nullish(),
position: z.number().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)).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)).optional().default(21.0),
apply_vat: z.any().optional().default(true),
exchange_rate: z.union([z.number(), z.string()]).transform(v => Number(v)).optional().default(1.0),
status: z.string().optional().default('active'),
scope_title: z.string().nullish(),
scope_description: z.string().nullish(),
items: z.array(QuotationItemSchema).optional(),
sections: z.array(ScopeSectionSchema).optional(),
});
export const UpdateQuotationSchema = z.object({
quotation_number: z.string().optional(),
project_code: z.string().nullish(),
customer_id: z.union([z.number(), z.string()]).transform(v => Number(v)).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)).optional(),
apply_vat: z.any().optional(),
exchange_rate: z.union([z.number(), z.string()]).transform(v => Number(v)).optional(),
status: z.string().optional(),
scope_title: z.string().nullish(),
scope_description: z.string().nullish(),
items: z.array(QuotationItemSchema).optional(),
sections: z.array(ScopeSectionSchema).optional(),
});
export type CreateQuotationInput = z.infer<typeof CreateQuotationSchema>;
export type UpdateQuotationInput = z.infer<typeof UpdateQuotationSchema>;

View File

@@ -0,0 +1,61 @@
import { z } from 'zod';
const OrderItemSchema = z.object({
description: z.string().nullish(),
item_description: z.string().nullish(),
quantity: z.number().optional().default(1),
unit: z.string().nullish(),
unit_price: z.number().optional().default(0),
is_included_in_total: z.any().optional().default(true),
position: z.number().optional(),
});
const OrderSectionSchema = z.object({
title: z.string().nullish(),
title_cz: z.string().nullish(),
content: z.string().nullish(),
position: z.number().optional(),
});
export const CreateOrderFromQuotationSchema = z.object({
quotationId: z.union([z.number(), z.string()]).transform(v => Number(v)),
customerOrderNumber: z.string().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)).nullish(),
customer_id: z.union([z.number(), z.string()]).transform(v => Number(v)).nullish(),
status: z.string().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)).optional().default(21.0),
apply_vat: z.any().optional().default(true),
exchange_rate: z.union([z.number(), z.string()]).transform(v => Number(v)).optional().default(1.0),
scope_title: z.string().nullish(),
scope_description: z.string().nullish(),
notes: z.string().nullish(),
items: z.array(OrderItemSchema).optional(),
sections: z.array(OrderSectionSchema).optional(),
});
export const UpdateOrderSchema = z.object({
order_number: z.string().nullish(),
customer_order_number: z.string().nullish(),
status: z.string().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()]).optional(),
vat_rate: z.union([z.number(), z.string()]).transform(v => Number(v)).optional(),
apply_vat: z.any().optional(),
items: z.array(OrderItemSchema).optional(),
sections: z.array(OrderSectionSchema).optional(),
});
export type CreateOrderFromQuotationInput = z.infer<typeof CreateOrderFromQuotationSchema>;
export type CreateOrderInput = z.infer<typeof CreateOrderSchema>;
export type UpdateOrderInput = z.infer<typeof UpdateOrderSchema>;

View File

@@ -0,0 +1,11 @@
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().optional(),
});
export type UpdateProfileInput = z.infer<typeof UpdateProfileSchema>;

View File

@@ -0,0 +1,35 @@
import { z } from 'zod';
export const CreateProjectSchema = z.object({
project_number: z.string().nullish(),
name: z.string().nullish(),
customer_id: z.union([z.number(), z.string()]).transform(v => Number(v)).nullish(),
responsible_user_id: z.union([z.number(), z.string()]).transform(v => Number(v)).nullish(),
quotation_id: z.union([z.number(), z.string()]).transform(v => Number(v)).nullish(),
order_id: z.union([z.number(), z.string()]).transform(v => Number(v)).nullish(),
status: z.string().optional().default('aktivni'),
start_date: z.string().nullish(),
end_date: z.string().nullish(),
notes: z.string().nullish(),
});
export const UpdateProjectSchema = z.object({
project_number: z.string().nullish(),
name: z.string().nullish(),
status: z.string().optional(),
notes: z.string().nullish(),
customer_id: z.union([z.number(), z.string(), z.null()]).optional(),
responsible_user_id: z.union([z.number(), z.string(), z.null()]).optional(),
quotation_id: z.union([z.number(), z.string(), z.null()]).optional(),
order_id: z.union([z.number(), z.string(), z.null()]).optional(),
start_date: z.union([z.string(), z.null()]).optional(),
end_date: z.union([z.string(), z.null()]).optional(),
});
export const CreateProjectNoteSchema = z.object({
content: z.string().nullish(),
});
export type CreateProjectInput = z.infer<typeof CreateProjectSchema>;
export type UpdateProjectInput = z.infer<typeof UpdateProjectSchema>;
export type CreateProjectNoteInput = z.infer<typeof CreateProjectNoteSchema>;

View File

@@ -0,0 +1,37 @@
import { z } from 'zod';
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(),
status: z.string().optional().default('unpaid'),
notes: z.string().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(),
status: z.string().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(),
});
export type CreateReceivedInvoiceInput = z.infer<typeof CreateReceivedInvoiceSchema>;
export type UpdateReceivedInvoiceInput = z.infer<typeof UpdateReceivedInvoiceSchema>;

View File

@@ -0,0 +1,17 @@
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(),
});
export const UpdateRoleSchema = z.object({
display_name: z.string().optional(),
description: z.string().optional(),
permission_ids: z.array(z.number()).optional(),
});
export type CreateRoleInput = z.infer<typeof CreateRoleSchema>;
export type UpdateRoleInput = z.infer<typeof UpdateRoleSchema>;

View File

@@ -0,0 +1,34 @@
import { z } from 'zod';
const ScopeSectionSchema = z.object({
title: z.string().nullish(),
title_cz: z.string().nullish(),
content: z.string().nullish(),
position: z.number().optional(),
});
export const CreateScopeTemplateSchema = z.object({
name: z.string().nullish(),
title: z.string().nullish(),
description: z.string().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(),
});
export const UpdateScopeTemplateSchema = z.object({
name: z.string().optional(),
title: z.string().optional(),
description: z.string().optional(),
sections: z.array(ScopeSectionSchema).optional(),
});
export type CreateScopeTemplateInput = z.infer<typeof CreateScopeTemplateSchema>;
export type CreateItemTemplateInput = z.infer<typeof CreateItemTemplateSchema>;
export type UpdateScopeTemplateInput = z.infer<typeof UpdateScopeTemplateSchema>;

View File

@@ -0,0 +1,26 @@
import { z } from 'zod';
export const CreateTripSchema = z.object({
vehicle_id: z.union([z.number(), z.string()]).transform(v => Number(v)),
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.any().optional().default(false),
notes: z.string().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.any().optional(),
notes: z.string().nullish(),
});
export type CreateTripInput = z.infer<typeof CreateTripSchema>;
export type UpdateTripInput = z.infer<typeof UpdateTripSchema>;

View File

@@ -0,0 +1,24 @@
import { z } from 'zod';
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)),
is_active: z.any().optional().default(true),
});
export const UpdateUserSchema = z.object({
username: z.string().optional(),
email: z.string().email('Neplatný formát e-mailu').optional(),
password: z.string().min(8, 'Heslo musí mít alespoň 8 znaků').optional(),
first_name: z.string().optional(),
last_name: z.string().optional(),
role_id: z.union([z.number(), z.string(), z.null()]).optional(),
is_active: z.any().optional(),
});
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
export type UpdateUserInput = z.infer<typeof UpdateUserSchema>;

View File

@@ -0,0 +1,24 @@
import { z } from 'zod';
export const CreateVehicleSchema = z.object({
spz: z.string().min(1, 'SPZ je povinná'),
name: z.string().min(1, 'Název je povinný'),
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),
is_active: z.any().optional().default(true),
});
export const UpdateVehicleSchema = z.object({
spz: z.string().optional(),
name: z.string().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(),
is_active: z.any().optional(),
});
export type CreateVehicleInput = z.infer<typeof CreateVehicleSchema>;
export type UpdateVehicleInput = z.infer<typeof UpdateVehicleSchema>;