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:
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> = {};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
97
src/schemas/attendance.schema.ts
Normal file
97
src/schemas/attendance.schema.ts
Normal 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>;
|
||||
26
src/schemas/bank-accounts.schema.ts
Normal file
26
src/schemas/bank-accounts.schema.ts
Normal 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>;
|
||||
21
src/schemas/company-settings.schema.ts
Normal file
21
src/schemas/company-settings.schema.ts
Normal 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>;
|
||||
28
src/schemas/customers.schema.ts
Normal file
28
src/schemas/customers.schema.ts
Normal 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>;
|
||||
58
src/schemas/invoices.schema.ts
Normal file
58
src/schemas/invoices.schema.ts
Normal 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>;
|
||||
16
src/schemas/leave-requests.schema.ts
Normal file
16
src/schemas/leave-requests.schema.ts
Normal 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>;
|
||||
55
src/schemas/offers.schema.ts
Normal file
55
src/schemas/offers.schema.ts
Normal 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>;
|
||||
61
src/schemas/orders.schema.ts
Normal file
61
src/schemas/orders.schema.ts
Normal 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>;
|
||||
11
src/schemas/profile.schema.ts
Normal file
11
src/schemas/profile.schema.ts
Normal 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>;
|
||||
35
src/schemas/projects.schema.ts
Normal file
35
src/schemas/projects.schema.ts
Normal 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>;
|
||||
37
src/schemas/received-invoices.schema.ts
Normal file
37
src/schemas/received-invoices.schema.ts
Normal 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>;
|
||||
17
src/schemas/roles.schema.ts
Normal file
17
src/schemas/roles.schema.ts
Normal 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>;
|
||||
34
src/schemas/scope-templates.schema.ts
Normal file
34
src/schemas/scope-templates.schema.ts
Normal 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>;
|
||||
26
src/schemas/trips.schema.ts
Normal file
26
src/schemas/trips.schema.ts
Normal 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>;
|
||||
24
src/schemas/users.schema.ts
Normal file
24
src/schemas/users.schema.ts
Normal 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>;
|
||||
24
src/schemas/vehicles.schema.ts
Normal file
24
src/schemas/vehicles.schema.ts
Normal 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>;
|
||||
Reference in New Issue
Block a user