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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user