/home/techb158/balavpn.abdallabala.com/src/services
Edit: /home/techb158/balavpn.abdallabala.com/src/services/reporting-service.js (8644B)
const { prisma } = require("../lib/prisma");
const { getProjectDashboard } = require("./dashboard-service");
function csvEscape(value) {
const text = value === null || value === undefined ? "" : String(value);
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
}
function toCsv(headers, rows) {
return [headers.join(","), ...rows.map(row => headers.map(header => csvEscape(row[header])).join(","))].join("\n");
}
function riskScoreColor(score) {
if (score >= 15) return "#dc2626";
if (score >= 10) return "#f59e0b";
if (score >= 5) return "#eab308";
return "#16a34a";
}
async function getFullExportCsv(projectId) {
const project = await prisma.project.findUnique({
where: { id: projectId },
include: {
risks: {
include: { mitigations: { orderBy: { sequence: "asc" } } },
orderBy: { createdAt: "asc" }
},
lifecyclePhases: { orderBy: { sequence: "asc" } },
indicators: true
}
});
if (!project) {
const error = new Error(`Project not found: ${projectId}`);
error.status = 404;
throw error;
}
const riskHeaders = ["riskId", "title", "dimension", "status", "probability", "impact", "normalizedScore", "residualScore", "createdAt"];
const riskRows = project.risks.map(r => ({
riskId: r.id, title: r.title, dimension: r.dimension, status: r.status,
probability: r.probability, impact: r.impact,
normalizedScore: r.normalizedScore ?? "", residualScore: r.residualScore ?? "",
createdAt: r.createdAt?.toISOString?.() ?? r.createdAt
}));
const mitigationRows = [];
for (const risk of project.risks) {
for (const m of risk.mitigations) {
mitigationRows.push({
riskId: risk.id, riskTitle: risk.title,
mitigationId: m.id, title: m.title, type: m.type, status: m.status,
sequence: m.sequence, createdAt: m.createdAt?.toISOString?.() ?? m.createdAt
});
}
}
const mitHeaders = ["riskId", "riskTitle", "mitigationId", "title", "type", "status", "sequence", "createdAt"];
let csv = "=== RISKS ===\n";
csv += toCsv(riskHeaders, riskRows) + "\n\n";
csv += "=== MITIGATIONS ===\n";
csv += toCsv(mitHeaders, mitigationRows) + "\n\n";
if (project.lifecyclePhases.length > 0) {
const phaseHeaders = ["phaseId", "name", "status", "sequence"];
const phaseRows = project.lifecyclePhases.map(p => ({
phaseId: p.id, name: p.name, status: p.gateStatus ?? "", sequence: p.sequence
}));
csv += "=== PHASES ===\n";
csv += toCsv(phaseHeaders, phaseRows) + "\n\n";
}
if (project.indicators.length > 0) {
const indHeaders = ["indicatorId", "name", "category", "value", "target", "trend"];
const indRows = project.indicators.map(i => ({
indicatorId: i.id, name: i.name, category: i.category ?? "",
value: i.value ?? "", target: i.target ?? "", trend: i.trend ?? ""
}));
csv += "=== INDICATORS ===\n";
csv += toCsv(indHeaders, indRows) + "\n\n";
}
return { contentType: "text/csv; charset=utf-8", body: csv };
}
function buildExecutiveHtml(dashboard) {
const { project, summary, scoredRisks, indicators, lifecyclePhases, dimensionalScores } = dashboard;
const score = summary.overallScore;
const color = riskScoreColor(score);
const riskRows = scoredRisks.map(r => {
const rc = riskScoreColor(r.normalizedScore ?? 0);
return `
| ${csvEscape(r.title)} |
${r.dimension ?? ""} |
${r.status ?? ""} |
${r.normalizedScore ?? ""} |
${r.residualScore ?? ""} |
`;
}).join("\n");
const dimRows = (dimensionalScores ? Object.entries(dimensionalScores) : []).map(([dim, dimData]) => {
const ds = typeof dimData === "object" ? (dimData.score ?? dimData) : dimData;
const dc = riskScoreColor(ds);
return `
| ${csvEscape(dim)} | ${ds} |
`;
}).join("\n");
const phaseRows = (lifecyclePhases || []).map(p =>
`
| ${csvEscape(p.name)} | ${p.gateStatus ?? ""} | ${p.sequence ?? ""} |
`
).join("\n");
const indRows = (indicators || []).map(i =>
`
| ${csvEscape(i.name)} | ${i.category ?? ""} | ${i.value ?? ""} | ${i.target ?? ""} | ${i.trend ?? ""} |
`
).join("\n");
const barWidth = Math.min(score * 5, 100);
return `
Executive Report - ${csvEscape(project.name)}
${csvEscape(project.name)} — Executive Report
Generated: ${new Date().toISOString()} | ID: ${project.id}
Overall AI Risk Score: ${score}
| Metric | Value |
| Gate Status | ${summary.gateStatus ?? "N/A"} |
| Open Risks | ${summary.openRisks ?? 0} |
| Mitigated Risks | ${summary.mitigatedRisks ?? 0} |
| Total Risks | ${(scoredRisks || []).length} |
${dimRows ? `
Dimensional Scores
` : ""}
Risk Register
| Title | Dimension | Status | Score | Residual |
${riskRows}
${phaseRows ? `
Lifecycle Phases
| Phase | Status | Sequence |
${phaseRows}
` : ""}
${indRows ? `
Indicators
| Name | Category | Value | Target | Trend |
${indRows}
` : ""}
`;
}
async function getReport(projectId, reportType) {
if (reportType === "full-export.csv") {
return getFullExportCsv(projectId);
}
const dashboard = await getProjectDashboard(projectId);
if (reportType === "risk-register.csv") {
return {
contentType: "text/csv; charset=utf-8",
body: toCsv(["id", "title", "dimension", "status", "normalizedScore", "residualScore"], dashboard.scoredRisks)
};
}
if (reportType === "executive.html") {
return {
contentType: "text/html; charset=utf-8",
body: buildExecutiveHtml(dashboard)
};
}
if (reportType === "executive.json") {
return {
contentType: "application/json; charset=utf-8",
body: JSON.stringify({
project: { id: dashboard.project.id, name: dashboard.project.name },
summary: dashboard.summary,
dimensionalScores: dashboard.dimensionalScores,
scoredRisks: dashboard.scoredRisks.map(r => ({
id: r.id, title: r.title, dimension: r.dimension, status: r.status,
normalizedScore: r.normalizedScore, residualScore: r.residualScore
})),
generatedAt: new Date().toISOString()
}, null, 2)
};
}
return {
contentType: "application/json; charset=utf-8",
body: JSON.stringify({ reportType, dashboard }, null, 2)
};
}
module.exports = { getReport, buildExecutiveHtml };