R19/R20: 项目主域进 OA 并打通报销差旅周报待办
这个提交包含在:
@@ -6,6 +6,7 @@ const { FEATURE_FLAG_KEYS, evaluateFeatureFlag } = require("../services/featureF
|
||||
const { currentDateString, parseDateOnlyString } = require("../utils/date");
|
||||
|
||||
const DATE_SCHEMA = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
||||
const UUID_SCHEMA = z.string().uuid();
|
||||
const QUERY_SCHEMA = z.object({ weekStart: DATE_SCHEMA.optional() });
|
||||
const WRITE_SCHEMA = z
|
||||
.object({
|
||||
@@ -14,6 +15,7 @@ const WRITE_SCHEMA = z
|
||||
blockersText: z.string().max(5000).optional(),
|
||||
nextWeekText: z.string().max(5000).optional(),
|
||||
needsHelp: z.boolean().optional(),
|
||||
projectLinks: z.array(z.object({ projectId: UUID_SCHEMA, progressText: z.string().max(4000).optional(), riskText: z.string().max(4000).optional(), nextStepText: z.string().max(4000).optional() }).strict()).max(20).optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine((value) => Object.keys(value).length > 0, { message: "empty_patch" });
|
||||
@@ -68,9 +70,14 @@ function mapReport(row) {
|
||||
submittedAt: row.submitted_at?.toISOString?.() ?? null,
|
||||
createdAt: row.created_at?.toISOString?.() ?? null,
|
||||
updatedAt: row.updated_at?.toISOString?.() ?? null,
|
||||
projectLinks: Array.isArray(row.project_links) ? row.project_links.map((item) => ({ id: item.id, projectId: item.project_id, progressText: item.progress_text, riskText: item.risk_text, nextStepText: item.next_step_text, project: item.project ? { id: item.project.id, projectNo: item.project.project_no, name: item.project.name } : null })) : [],
|
||||
};
|
||||
}
|
||||
|
||||
async function loadReportWithLinks(prisma, reportId) {
|
||||
return prisma.weeklyReport.findUnique({ where: { id: reportId }, include: { project_links: { include: { project: { select: { id: true, project_no: true, name: true } } }, orderBy: { updated_at: "desc" } } } });
|
||||
}
|
||||
|
||||
async function ensureReportDraft(prisma, employeeId, weekStart) {
|
||||
const existing = await prisma.weeklyReport.findUnique({
|
||||
where: {
|
||||
@@ -105,7 +112,7 @@ async function weeklyReportsRoutes(fastify) {
|
||||
}
|
||||
const weekStart = resolveWeekStart(parsed.data.weekStart, fastify.config.appTimezone);
|
||||
const report = await ensureReportDraft(fastify.prisma, request.employee.id, weekStart);
|
||||
return sendData(reply, request, { item: mapReport(report) });
|
||||
return sendData(reply, request, { item: mapReport(await loadReportWithLinks(fastify.prisma, report.id)) });
|
||||
});
|
||||
|
||||
fastify.put("/weekly-reports/me", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
@@ -116,16 +123,37 @@ async function weeklyReportsRoutes(fastify) {
|
||||
if (current.status === "submitted") {
|
||||
throw buildValidationError({ status: "submitted_readonly" }, 409);
|
||||
}
|
||||
const updated = await fastify.prisma.weeklyReport.update({
|
||||
where: { id: current.id },
|
||||
data: {
|
||||
...(body.highlightsText !== undefined ? { highlights_text: body.highlightsText } : {}),
|
||||
...(body.blockersText !== undefined ? { blockers_text: body.blockersText } : {}),
|
||||
...(body.nextWeekText !== undefined ? { next_week_text: body.nextWeekText } : {}),
|
||||
...(body.needsHelp !== undefined ? { needs_help: body.needsHelp } : {}),
|
||||
},
|
||||
const updated = await fastify.prisma.$transaction(async (tx) => {
|
||||
const saved = await tx.weeklyReport.update({
|
||||
where: { id: current.id },
|
||||
data: {
|
||||
...(body.highlightsText !== undefined ? { highlights_text: body.highlightsText } : {}),
|
||||
...(body.blockersText !== undefined ? { blockers_text: body.blockersText } : {}),
|
||||
...(body.nextWeekText !== undefined ? { next_week_text: body.nextWeekText } : {}),
|
||||
...(body.needsHelp !== undefined ? { needs_help: body.needsHelp } : {}),
|
||||
},
|
||||
});
|
||||
if (body.projectLinks !== undefined) {
|
||||
const linkGate = await evaluateFeatureFlag({ prisma: tx, config: fastify.config, flagKey: FEATURE_FLAG_KEYS.projectLinking, departmentId: request.employee.department_id ?? null, strictDepartment: false });
|
||||
if (!linkGate.enabled) {
|
||||
throw buildValidationError({ featureFlag: "project_linking_v1_disabled", reason: linkGate.reason }, 409);
|
||||
}
|
||||
await tx.projectLinkOnWeeklyReport.deleteMany({ where: { weekly_report_id: current.id } });
|
||||
if (body.projectLinks.length) {
|
||||
await tx.projectLinkOnWeeklyReport.createMany({
|
||||
data: body.projectLinks.map((item) => ({
|
||||
weekly_report_id: current.id,
|
||||
project_id: item.projectId,
|
||||
progress_text: item.progressText ?? "",
|
||||
risk_text: item.riskText ?? "",
|
||||
next_step_text: item.nextStepText ?? "",
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
return saved;
|
||||
});
|
||||
return sendData(reply, request, { item: mapReport(updated) });
|
||||
return sendData(reply, request, { item: mapReport(await loadReportWithLinks(fastify.prisma, updated.id)) });
|
||||
});
|
||||
|
||||
fastify.post("/weekly-reports/me/submit", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
@@ -140,7 +168,7 @@ async function weeklyReportsRoutes(fastify) {
|
||||
submitted_at: new Date(),
|
||||
},
|
||||
});
|
||||
return sendData(reply, request, { item: mapReport(updated) });
|
||||
return sendData(reply, request, { item: mapReport(await loadReportWithLinks(fastify.prisma, updated.id)) });
|
||||
});
|
||||
|
||||
fastify.get("/weekly-reports/team", { preHandler: fastify.authenticate }, async (request, reply) => {
|
||||
|
||||
在新工单中引用
屏蔽一个用户