metis-desk/modules/companies/src/api/infrastructure/http/process.routes.ts
David dffb4b2794 feat: add process management features including feedback, help, and templates
- Implemented ProcessFeedback component for error handling in process forms.
- Created ProcessHelp component to provide contextual assistance for users.
- Added internationalization support for process-related strings in Spanish and English.
- Developed ProcessStartForm for initiating new processes with person and template selection.
- Introduced ProcessTemplateForm for creating and editing process templates.
- Built ProcessTemplatesPage to display and manage process templates.
- Enhanced ProcessesPage to list current processes and allow for creation and detail viewing.
- Added unit tests for various components to ensure functionality and correctness.
2026-10-07 09:34:18 +02:00

184 lines
6.2 KiB
TypeScript

import { type Application, type RequestHandler, type Response, Router } from "express";
import { z } from "zod/v4";
import {
ProcessTemplateInputSchema,
StartProcessInputSchema,
UpdateProcessInputSchema,
} from "../../../common/processes";
import type { ProcessUseCases } from "../../application/process.use-cases";
import { buildProcess, buildProcessTemplate } from "../../application/process-response.builder";
const companyParams = z.strictObject({ companyId: z.uuid().transform((v) => v.toLowerCase()) });
const itemParams = companyParams.extend({ id: z.uuid().transform((v) => v.toLowerCase()) });
const templateParams = z.strictObject({ id: z.uuid().transform((v) => v.toLowerCase()) });
function fail(response: Response, code: string) {
response
.status(code === "INVALID_REQUEST" ? 400 : code.endsWith("NOT_FOUND") ? 404 : 409)
.json({ code, requestId: response.getHeader("x-request-id") });
}
function actorOf(request: unknown) {
return z.object({ session: z.object({ userId: z.uuid() }) }).safeParse(request);
}
export function registerProcessRoutes(
app: Application,
useCases: ProcessUseCases,
authenticate: RequestHandler
) {
const router = Router();
router.use((request, response, next) => {
if (!z.strictObject({}).safeParse(request.query).success) {
fail(response, "INVALID_REQUEST");
return;
}
next();
});
router.get("/templates", (_request, response, next) => {
useCases
.catalog()
.then((result) => {
if (!result.isSuccess) {
fail(response, result.error.code);
return;
}
response.json({
templates: result.data.templates.map((t) => buildProcessTemplate(t, result.data)),
});
})
.catch(next);
});
for (const path of ["/templates", "/templates/:id/update"]) {
router.post(path, (request, response, next) => {
const body = z.strictObject({ values: ProcessTemplateInputSchema }).safeParse(request.body);
const params = path.endsWith("update")
? templateParams.safeParse(request.params)
: z.strictObject({}).safeParse(request.params);
const actor = actorOf(request);
if (!(body.success && params.success && actor.success)) {
fail(response, "INVALID_REQUEST");
return;
}
const id =
"id" in params.data && typeof params.data.id === "string" ? params.data.id : undefined;
useCases
.saveTemplate(body.data.values, actor.data.session.userId, id)
.then((result) => {
if (!result.isSuccess) {
fail(response, result.error.code);
return;
}
response
.status(id ? 200 : 201)
.json({ template: buildProcessTemplate(result.data.template, result.data.context) });
})
.catch(next);
});
}
router.get("/:companyId", (request, response, next) => {
const params = companyParams.safeParse(request.params);
if (!params.success) {
fail(response, "INVALID_REQUEST");
return;
}
useCases
.get(params.data.companyId)
.then((result) => {
if (!result.isSuccess) {
fail(response, result.error.code);
return;
}
response.json({
processes: result.data.processes.map((p) => buildProcess(p, result.data)),
});
})
.catch(next);
});
router.get("/:companyId/people/:id/review", (request, response, next) => {
const params = itemParams.safeParse(request.params);
if (!params.success) {
fail(response, "INVALID_REQUEST");
return;
}
useCases
.get(params.data.companyId)
.then((result) => {
if (!result.isSuccess) {
fail(response, result.error.code);
return;
}
const review = result.data.reviews.get(params.data.id);
if (!review) {
fail(response, "PROCESS_PERSON_NOT_FOUND");
return;
}
response.json({ review });
})
.catch(next);
});
router.post("/:companyId", (request, response, next) => {
const params = companyParams.safeParse(request.params);
const body = z.strictObject({ values: StartProcessInputSchema }).safeParse(request.body);
const actor = actorOf(request);
if (!(params.success && body.success && actor.success)) {
fail(response, "INVALID_REQUEST");
return;
}
useCases
.start(params.data.companyId, body.data.values, actor.data.session.userId)
.then((result) => {
if (!result.isSuccess) {
fail(response, result.error.code);
return;
}
response
.status(201)
.json({ process: buildProcess(result.data.process, result.data.context) });
})
.catch(next);
});
router.post("/:companyId/:id/update", (request, response, next) => {
const params = itemParams.safeParse(request.params);
const body = z.strictObject({ values: UpdateProcessInputSchema }).safeParse(request.body);
const actor = actorOf(request);
if (!(params.success && body.success && actor.success)) {
fail(response, "INVALID_REQUEST");
return;
}
useCases
.update(params.data.companyId, params.data.id, body.data.values, actor.data.session.userId)
.then((result) => {
if (!result.isSuccess) {
fail(response, result.error.code);
return;
}
response.json({ process: buildProcess(result.data.process, result.data.context) });
})
.catch(next);
});
router.post("/:companyId/:id/refresh", (request, response, next) => {
const params = itemParams.safeParse(request.params);
const body = z.strictObject({ revisionId: z.uuid() }).safeParse(request.body);
const actor = actorOf(request);
if (!(params.success && body.success && actor.success)) {
fail(response, "INVALID_REQUEST");
return;
}
useCases
.refresh(
params.data.companyId,
params.data.id,
body.data.revisionId,
actor.data.session.userId
)
.then((result) => {
if (!result.isSuccess) {
fail(response, result.error.code);
return;
}
response.json({ process: buildProcess(result.data.process, result.data.context) });
})
.catch(next);
});
app.use("/api/processes", authenticate, router);
}