initial commit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-03-23 08:46:51 +01:00
commit 4608494a3f
130 changed files with 40361 additions and 0 deletions

222
src/routes/admin/trips.ts Normal file
View File

@@ -0,0 +1,222 @@
import { FastifyInstance } from 'fastify';
import prisma from '../../config/database';
import { requireAuth, requirePermission } from '../../middleware/auth';
import { logAudit } from '../../services/audit';
import { success, error } from '../../utils/response';
import { parsePagination, buildPaginationMeta } from '../../utils/pagination';
export default async function tripsRoutes(fastify: FastifyInstance): Promise<void> {
fastify.get('/', { preHandler: requireAuth }, async (request, reply) => {
const query = request.query as Record<string, unknown>;
const { page, limit, skip, order } = parsePagination(query);
const authData = request.authData!;
const isAdmin = authData.permissions.includes('trips.admin');
const where: Record<string, unknown> = {};
if (!isAdmin) where.user_id = authData.userId;
else if (query.user_id) where.user_id = Number(query.user_id);
if (query.vehicle_id) where.vehicle_id = Number(query.vehicle_id);
// Support both "month=3&year=2026" (TripsAdmin) and "month=2026-03" (TripsHistory) formats
if (query.month) {
const monthStr = String(query.month);
let yr: number, mo: number;
if (monthStr.includes('-')) {
// Combined YYYY-MM format
const [yStr, mStr] = monthStr.split('-');
yr = Number(yStr);
mo = Number(mStr);
} else if (query.year) {
yr = Number(query.year);
mo = Number(query.month);
} else {
yr = NaN;
mo = NaN;
}
if (!isNaN(yr) && !isNaN(mo) && mo >= 1 && mo <= 12) {
where.trip_date = {
gte: new Date(yr, mo - 1, 1),
lt: new Date(yr, mo, 1),
};
}
}
const [trips, total] = await Promise.all([
prisma.trips.findMany({
where, skip, take: limit, orderBy: { trip_date: order },
include: {
users: { select: { id: true, first_name: true, last_name: true } },
vehicles: { select: { id: true, name: true, spz: true } },
},
}),
prisma.trips.count({ where }),
]);
return reply.send({ success: true, data: trips, pagination: buildPaginationMeta(total, page, limit) });
});
// GET /api/admin/trips/print — print data for trip report
fastify.get('/print', { preHandler: requirePermission('trips.admin') }, async (request, reply) => {
const query = request.query as Record<string, unknown>;
const filterUserId = query.user_id ? Number(query.user_id) : null;
const filterVehicleId = query.vehicle_id ? Number(query.vehicle_id) : null;
const where: Record<string, unknown> = {};
if (filterUserId) where.user_id = filterUserId;
if (filterVehicleId) where.vehicle_id = filterVehicleId;
if (query.month && query.year) {
where.trip_date = {
gte: new Date(Number(query.year), Number(query.month) - 1, 1),
lt: new Date(Number(query.year), Number(query.month), 1),
};
}
const trips = await prisma.trips.findMany({
where,
include: {
users: { select: { id: true, first_name: true, last_name: true } },
vehicles: { select: { id: true, name: true, spz: true } },
},
orderBy: { trip_date: 'asc' },
});
const vehicles = await prisma.vehicles.findMany({ orderBy: { name: 'asc' } });
const users = await prisma.users.findMany({
where: { is_active: true },
select: { id: true, first_name: true, last_name: true },
orderBy: { last_name: 'asc' },
});
let totalKm = 0;
let businessKm = 0;
let privateKm = 0;
for (const t of trips) {
const dist = Number(t.end_km) - Number(t.start_km);
totalKm += dist;
if (t.is_business) businessKm += dist;
else privateKm += dist;
}
return success(reply, {
trips,
vehicles,
users: users.map(u => ({ id: u.id, name: `${u.first_name} ${u.last_name}`.trim() })),
totals: { total_km: totalKm, business_km: businessKm, private_km: privateKm, count: trips.length },
});
});
// GET /api/admin/trips/last-km/:vehicleId
fastify.get<{ Params: { vehicleId: string } }>('/last-km/:vehicleId', { preHandler: requireAuth }, async (request, reply) => {
const vehicleId = parseInt(request.params.vehicleId, 10);
if (isNaN(vehicleId)) return error(reply, 'Neplatné ID vozidla', 400);
const lastTrip = await prisma.trips.findFirst({
where: { vehicle_id: vehicleId },
orderBy: { id: 'desc' },
select: { end_km: true },
});
return success(reply, { last_km: lastTrip ? Number(lastTrip.end_km) : 0 });
});
fastify.post('/', { preHandler: requireAuth }, async (request, reply) => {
const body = request.body as Record<string, unknown>;
const authData = request.authData!;
const trip = await prisma.trips.create({
data: {
vehicle_id: Number(body.vehicle_id),
user_id: body.user_id ? Number(body.user_id) : authData.userId,
trip_date: new Date(String(body.trip_date)),
start_km: Number(body.start_km),
end_km: Number(body.end_km),
route_from: String(body.route_from),
route_to: String(body.route_to),
is_business: body.is_business === true || body.is_business === 1 || body.is_business === '1',
notes: body.notes ? String(body.notes) : null,
},
});
// Update vehicle actual_km
await prisma.vehicles.update({
where: { id: Number(body.vehicle_id) },
data: { actual_km: Number(body.end_km) },
});
await logAudit({ request, authData, action: 'create', entityType: 'trip', entityId: trip.id, description: `Vytvořena jízda` });
return success(reply, { id: trip.id }, 201, 'Jízda byla zaznamenána');
});
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 authData = request.authData!;
const existing = await prisma.trips.findUnique({ where: { id } });
if (!existing) return error(reply, 'Jízda nenalezena', 404);
// Ownership check — same as DELETE handler
const isAdmin = authData.permissions.includes('trips.admin');
if (existing.user_id !== authData.userId && !isAdmin) {
return error(reply, 'Nemáte oprávnění upravit tuto jízdu', 403);
}
const data: Record<string, unknown> = {};
if (body.trip_date !== undefined) data.trip_date = new Date(String(body.trip_date));
if (body.start_km !== undefined) data.start_km = Number(body.start_km);
if (body.end_km !== undefined) data.end_km = Number(body.end_km);
if (body.route_from !== undefined) data.route_from = String(body.route_from);
if (body.route_to !== undefined) data.route_to = String(body.route_to);
if (body.is_business !== undefined) data.is_business = body.is_business === true || body.is_business === 1 || body.is_business === '1';
if (body.notes !== undefined) data.notes = body.notes ? String(body.notes) : null;
await prisma.trips.update({ where: { id }, data });
// Update vehicle actual_km if end_km changed
if (body.end_km !== undefined) {
const vehicleId = existing.vehicle_id;
const maxTrip = await prisma.trips.findFirst({
where: { vehicle_id: vehicleId },
orderBy: { end_km: 'desc' },
select: { end_km: true },
});
if (maxTrip) {
await prisma.vehicles.update({ where: { id: vehicleId }, data: { actual_km: Number(maxTrip.end_km) } });
}
}
await logAudit({ request, authData, action: 'update', entityType: 'trip', entityId: id, description: `Upravena jízda` });
return success(reply, { id }, 200, 'Záznam byl aktualizován');
});
fastify.delete<{ 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 authData = request.authData!;
const existing = await prisma.trips.findUnique({ where: { id } });
if (!existing) return error(reply, 'Jízda nenalezena', 404);
// Allow users to delete their own trips, admins can delete any
const isAdmin = authData.permissions.includes('trips.admin');
if (existing.user_id !== authData.userId && !isAdmin) {
return error(reply, 'Nemáte oprávnění smazat tuto jízdu', 403);
}
const vehicleId = existing.vehicle_id;
await prisma.trips.delete({ where: { id } });
// Recalculate vehicle actual_km after deletion
const maxTrip = await prisma.trips.findFirst({
where: { vehicle_id: vehicleId },
orderBy: { end_km: 'desc' },
select: { end_km: true },
});
const vehicle = await prisma.vehicles.findUnique({ where: { id: vehicleId }, select: { initial_km: true } });
await prisma.vehicles.update({
where: { id: vehicleId },
data: { actual_km: maxTrip ? Number(maxTrip.end_km) : (vehicle?.initial_km ?? 0) },
});
await logAudit({ request, authData, action: 'delete', entityType: 'trip', entityId: id, description: `Smazána jízda` });
return success(reply, { id }, 200, 'Záznam byl smazán');
});
}