feat(plan): REST routes for plan endpoints
This commit is contained in:
312
src/routes/admin/plan.ts
Normal file
312
src/routes/admin/plan.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
||||
import {
|
||||
requireAuth,
|
||||
requirePermission,
|
||||
requireAnyPermission,
|
||||
} from "../../middleware/auth";
|
||||
import { success, error, parseId } from "../../utils/response";
|
||||
import { parseBody } from "../../schemas/common";
|
||||
import { logAudit } from "../../services/audit";
|
||||
import {
|
||||
CreatePlanEntrySchema,
|
||||
UpdatePlanEntrySchema,
|
||||
CreatePlanOverrideSchema,
|
||||
UpdatePlanOverrideSchema,
|
||||
PlanRangeQuerySchema,
|
||||
PlanGridQuerySchema,
|
||||
} from "../../schemas/plan.schema";
|
||||
import {
|
||||
resolveGrid,
|
||||
listPlanUsers,
|
||||
createEntry,
|
||||
updateEntry,
|
||||
deleteEntry,
|
||||
createOverride,
|
||||
updateOverride,
|
||||
deleteOverride,
|
||||
listEntries,
|
||||
listOverrides,
|
||||
} from "../../services/plan.service";
|
||||
|
||||
const MAX_RANGE_DAYS = 92;
|
||||
|
||||
function isAdminLike(authData: any): boolean {
|
||||
return authData?.role === "admin";
|
||||
}
|
||||
|
||||
function forceFromQuery(query: unknown): boolean {
|
||||
if (!query || typeof query !== "object") return false;
|
||||
return (query as { force?: unknown }).force === "1";
|
||||
}
|
||||
|
||||
function daysBetween(from: string, to: string): number {
|
||||
const a = new Date(from + "T00:00:00.000Z").getTime();
|
||||
const b = new Date(to + "T00:00:00.000Z").getTime();
|
||||
return Math.round((b - a) / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
export default async function planRoutes(app: FastifyInstance) {
|
||||
app.addHook("preHandler", requireAuth);
|
||||
|
||||
// --- GET /plan/users ---
|
||||
app.get(
|
||||
"/users",
|
||||
{
|
||||
preHandler: requireAnyPermission(
|
||||
"attendance.manage",
|
||||
"attendance.record",
|
||||
),
|
||||
},
|
||||
async (_request: FastifyRequest, reply: FastifyReply) => {
|
||||
const users = await listPlanUsers();
|
||||
return success(reply, users);
|
||||
},
|
||||
);
|
||||
|
||||
// --- GET /plan/grid ---
|
||||
app.get(
|
||||
"/grid",
|
||||
{
|
||||
preHandler: requireAnyPermission(
|
||||
"attendance.manage",
|
||||
"attendance.record",
|
||||
),
|
||||
},
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const parsed = PlanGridQuerySchema.safeParse(request.query);
|
||||
if (!parsed.success) {
|
||||
return error(reply, parsed.error.issues[0].message, 400);
|
||||
}
|
||||
const { date_from, date_to, view } = parsed.data;
|
||||
if (daysBetween(date_from, date_to) > MAX_RANGE_DAYS) {
|
||||
return error(reply, "Maximální rozsah je 92 dní", 400);
|
||||
}
|
||||
const users = await listPlanUsers();
|
||||
const cells = await resolveGrid(
|
||||
users.map((u) => u.id),
|
||||
date_from,
|
||||
date_to,
|
||||
);
|
||||
return success(reply, { view, date_from, date_to, users, cells });
|
||||
},
|
||||
);
|
||||
|
||||
// --- GET /plan/entries ---
|
||||
app.get(
|
||||
"/entries",
|
||||
{
|
||||
preHandler: requireAnyPermission(
|
||||
"attendance.manage",
|
||||
"attendance.record",
|
||||
),
|
||||
},
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const parsed = PlanRangeQuerySchema.safeParse(request.query);
|
||||
if (!parsed.success)
|
||||
return error(reply, parsed.error.issues[0].message, 400);
|
||||
const rows = await listEntries(
|
||||
parsed.data,
|
||||
request.authData!.userId,
|
||||
isAdminLike(request.authData),
|
||||
);
|
||||
return success(reply, rows);
|
||||
},
|
||||
);
|
||||
|
||||
// --- GET /plan/overrides ---
|
||||
app.get(
|
||||
"/overrides",
|
||||
{
|
||||
preHandler: requireAnyPermission(
|
||||
"attendance.manage",
|
||||
"attendance.record",
|
||||
),
|
||||
},
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const parsed = PlanRangeQuerySchema.safeParse(request.query);
|
||||
if (!parsed.success)
|
||||
return error(reply, parsed.error.issues[0].message, 400);
|
||||
const rows = await listOverrides(
|
||||
parsed.data,
|
||||
request.authData!.userId,
|
||||
isAdminLike(request.authData),
|
||||
);
|
||||
return success(reply, rows);
|
||||
},
|
||||
);
|
||||
|
||||
// --- POST /plan/entries ---
|
||||
app.post(
|
||||
"/entries",
|
||||
{ preHandler: requirePermission("attendance.manage") },
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const body = parseBody(CreatePlanEntrySchema, request.body);
|
||||
if ("error" in body) return error(reply, body.error, 400);
|
||||
const force = forceFromQuery(request.query);
|
||||
const result = await createEntry(
|
||||
body.data!,
|
||||
request.authData!.userId,
|
||||
force,
|
||||
);
|
||||
if ("error" in result) return error(reply, result.error, result.status);
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
action: "create",
|
||||
entityType: "work_plan_entry",
|
||||
entityId: (result.data as any).id,
|
||||
newValues: result.data,
|
||||
description: force ? "force-edit past date" : undefined,
|
||||
});
|
||||
return success(reply, result.data, 201, "Plán vytvořen");
|
||||
},
|
||||
);
|
||||
|
||||
// --- PATCH /plan/entries/:id ---
|
||||
app.patch(
|
||||
"/entries/:id",
|
||||
{ preHandler: requirePermission("attendance.manage") },
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const id = parseId((request.params as any).id, reply);
|
||||
if (id === null) return;
|
||||
const body = parseBody(UpdatePlanEntrySchema, request.body);
|
||||
if ("error" in body) return error(reply, body.error, 400);
|
||||
const force = forceFromQuery(request.query);
|
||||
const result = await updateEntry(
|
||||
id,
|
||||
body.data!,
|
||||
request.authData!.userId,
|
||||
force,
|
||||
);
|
||||
if ("error" in result) return error(reply, result.error, result.status);
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
action: "update",
|
||||
entityType: "work_plan_entry",
|
||||
entityId: id,
|
||||
oldValues: (result.oldData as any) ?? undefined,
|
||||
newValues: result.data as Record<string, unknown>,
|
||||
description: force ? "force-edit past date" : undefined,
|
||||
});
|
||||
return success(reply, result.data, 200, "Plán aktualizován");
|
||||
},
|
||||
);
|
||||
|
||||
// --- DELETE /plan/entries/:id ---
|
||||
app.delete(
|
||||
"/entries/:id",
|
||||
{ preHandler: requirePermission("attendance.manage") },
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const id = parseId((request.params as any).id, reply);
|
||||
if (id === null) return;
|
||||
const force = forceFromQuery(request.query);
|
||||
const result = await deleteEntry(id, request.authData!.userId, force);
|
||||
if ("error" in result) return error(reply, result.error, result.status);
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
action: "delete",
|
||||
entityType: "work_plan_entry",
|
||||
entityId: id,
|
||||
oldValues: (result.oldData as any) ?? undefined,
|
||||
description: force ? "force-edit past date" : undefined,
|
||||
});
|
||||
return success(reply, { ok: true }, 200, "Plán smazán");
|
||||
},
|
||||
);
|
||||
|
||||
// --- POST /plan/overrides ---
|
||||
app.post(
|
||||
"/overrides",
|
||||
{ preHandler: requirePermission("attendance.manage") },
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const body = parseBody(CreatePlanOverrideSchema, request.body);
|
||||
if ("error" in body) return error(reply, body.error, 400);
|
||||
const force = forceFromQuery(request.query);
|
||||
const result = await createOverride(
|
||||
body.data!,
|
||||
request.authData!.userId,
|
||||
force,
|
||||
);
|
||||
if ("error" in result) return error(reply, result.error, result.status);
|
||||
|
||||
// If the create replaced an existing active override, log the soft-delete first.
|
||||
if (result.replacedData) {
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
action: "delete",
|
||||
entityType: "work_plan_override",
|
||||
entityId: (result.replacedData as any).id,
|
||||
oldValues: result.replacedData as Record<string, unknown>,
|
||||
description: "auto-soft-delete before replace",
|
||||
});
|
||||
}
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
action: "create",
|
||||
entityType: "work_plan_override",
|
||||
entityId: (result.data as any).id,
|
||||
newValues: result.data,
|
||||
description: force ? "force-edit past date" : undefined,
|
||||
});
|
||||
return success(reply, result.data, 201, "Přepsání dne vytvořeno");
|
||||
},
|
||||
);
|
||||
|
||||
// --- PATCH /plan/overrides/:id ---
|
||||
app.patch(
|
||||
"/overrides/:id",
|
||||
{ preHandler: requirePermission("attendance.manage") },
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const id = parseId((request.params as any).id, reply);
|
||||
if (id === null) return;
|
||||
const body = parseBody(UpdatePlanOverrideSchema, request.body);
|
||||
if ("error" in body) return error(reply, body.error, 400);
|
||||
const force = forceFromQuery(request.query);
|
||||
const result = await updateOverride(
|
||||
id,
|
||||
body.data!,
|
||||
request.authData!.userId,
|
||||
force,
|
||||
);
|
||||
if ("error" in result) return error(reply, result.error, result.status);
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
action: "update",
|
||||
entityType: "work_plan_override",
|
||||
entityId: id,
|
||||
oldValues: (result.oldData as any) ?? undefined,
|
||||
newValues: result.data,
|
||||
description: force ? "force-edit past date" : undefined,
|
||||
});
|
||||
return success(reply, result.data, 200, "Přepsání aktualizováno");
|
||||
},
|
||||
);
|
||||
|
||||
// --- DELETE /plan/overrides/:id ---
|
||||
app.delete(
|
||||
"/overrides/:id",
|
||||
{ preHandler: requirePermission("attendance.manage") },
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const id = parseId((request.params as any).id, reply);
|
||||
if (id === null) return;
|
||||
const force = forceFromQuery(request.query);
|
||||
const result = await deleteOverride(id, request.authData!.userId, force);
|
||||
if ("error" in result) return error(reply, result.error, result.status);
|
||||
await logAudit({
|
||||
request,
|
||||
authData: request.authData,
|
||||
action: "delete",
|
||||
entityType: "work_plan_override",
|
||||
entityId: id,
|
||||
oldValues: (result.oldData as any) ?? undefined,
|
||||
description: force ? "force-edit past date" : undefined,
|
||||
});
|
||||
return success(reply, { ok: true }, 200, "Přepsání smazáno");
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user