v1.6.5: fix attendance unique constraint, schema sync, seed script

- Change attendance idx_attendance_user_date from unique to index (allow multiple shifts per day)
- Reset migrations to single baseline init migration
- Add seed script with admin user (admin/admin)
- Update CLAUDE.md with migration workflow documentation
- Various frontend fixes (queries, pages, hooks)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-05-17 16:21:34 +02:00
parent 1db5060c42
commit cd6d3daf43
75 changed files with 1748 additions and 1169 deletions

View File

@@ -14,7 +14,12 @@ export default async function tripsRoutes(
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 hasTripsAccess =
authData.permissions.includes("trips.manage") ||
authData.permissions.includes("trips.record") ||
authData.permissions.includes("trips.history");
if (!hasTripsAccess) return error(reply, "Nedostatečná oprávnění", 403);
const isAdmin = authData.permissions.includes("trips.manage");
const where: Record<string, unknown> = {};
if (!isAdmin) where.user_id = authData.userId;
@@ -107,7 +112,7 @@ export default async function tripsRoutes(
// GET /api/admin/trips/print — print data for trip report
fastify.get(
"/print",
{ preHandler: requirePermission("trips.admin") },
{ preHandler: requirePermission("trips.manage") },
async (request, reply) => {
const query = request.query as Record<string, unknown>;
const filterUserId = query.user_id ? Number(query.user_id) : null;
@@ -182,7 +187,7 @@ export default async function tripsRoutes(
// Matches PHP: COALESCE(MAX(end_km), vehicle.initial_km, 0)
fastify.get<{ Params: { vehicleId: string } }>(
"/last-km/:vehicleId",
{ preHandler: requirePermission("trips.view") },
{ preHandler: requirePermission("trips.manage") },
async (request, reply) => {
const vehicleId = parseInt(request.params.vehicleId, 10);
if (isNaN(vehicleId)) return error(reply, "Neplatné ID vozidla", 400);
@@ -213,6 +218,11 @@ export default async function tripsRoutes(
const body = parsed.data;
const authData = request.authData!;
const canRecord =
authData.permissions.includes("trips.manage") ||
authData.permissions.includes("trips.record");
if (!canRecord) return error(reply, "Nedostatečná oprávnění", 403);
if (body.end_km < body.start_km) {
return error(reply, "Konečný stav km nesmí být menší než počáteční", 400);
}
@@ -274,7 +284,7 @@ export default async function tripsRoutes(
if (!existing) return error(reply, "Jízda nenalezena", 404);
// Ownership check — same as DELETE handler
const isAdmin = authData.permissions.includes("trips.admin");
const isAdmin = authData.permissions.includes("trips.manage");
if (existing.user_id !== authData.userId && !isAdmin) {
return error(reply, "Nemáte oprávnění upravit tuto jízdu", 403);
}
@@ -332,7 +342,7 @@ export default async function tripsRoutes(
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");
const isAdmin = authData.permissions.includes("trips.manage");
if (existing.user_id !== authData.userId && !isAdmin) {
return error(reply, "Nemáte oprávnění smazat tuto jízdu", 403);
}