diff --git a/controllers/project.js b/controllers/project.js index 7dab5fb..daad591 100644 --- a/controllers/project.js +++ b/controllers/project.js @@ -193,6 +193,7 @@ async function addRiskScoreToProject(project) { unintendedConsequence.riskScore = null; } } + return project; } @@ -241,4 +242,4 @@ async function getCompletionState(projectId, schema) { } } -module.exports = { getUserProjects, getCompletionState }; \ No newline at end of file +module.exports = { getUserProjects, getCompletionState, getUserProjectMetrics, addRiskScoreToProject }; \ No newline at end of file diff --git a/package.json b/package.json index 6dc1b90..e2a78ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "care.theodi.org", - "version": "2.2.0", + "version": "2.3.0", "description": "The ODI Care tool (AI enabled)", "main": "index.js", "scripts": { diff --git a/public/css/style.css b/public/css/style.css index 989d1f1..8f1dd57 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -133,7 +133,10 @@ li { overflow-x: hidden; height: 175px; } -.top-risks tbody tr:nth-child(2n) { +.top-risks tr:nth-child(2n) { + color: white; +} +#unintendedConsequences tr:nth-child(2n) { color: white; } .top-risks tbody tr:nth-child(2n) a { @@ -286,7 +289,10 @@ fieldset .form-group { overflow-y: auto; /* Scrollable contents if sidebar is long */ z-index: 1000; /* Ensures sidebar is above other content */ } -.sidebar li { +.result ul { + line-height: 2rem; +} +.sidebar li, .result li { margin: 0; padding: 4px 0 6px 21px; list-style: none; @@ -295,6 +301,9 @@ fieldset .form-group { background-position: left center; background-size: 25px; } +.result li { + padding-left: 2rem; +} .sidebar ul li a { display: block; padding: 10px; diff --git a/public/data/forms/actionPlanning.json b/public/data/forms/actionPlanning.json index 602c3f4..e32463b 100644 --- a/public/data/forms/actionPlanning.json +++ b/public/data/forms/actionPlanning.json @@ -11,18 +11,16 @@ "items": [ { "key": "unintendedConsequences[].consequence", - "fieldHtmlClass": "input inputText", + "fieldHtmlClass": "input inputText disabled", "placeholder": "Unintended consequence", "prepend": "

Unintended consequence

", - "notitle": true, - "disabled": true + "notitle": true }, { "key": "unintendedConsequences[].outcome", - "fieldHtmlClass": "input", + "fieldHtmlClass": "input disabled", "prepend": "

Outcome

", - "notitle": true, - "disabled": true + "notitle": true }, { "type": "fieldset", @@ -30,17 +28,15 @@ "items": [ { "key": "unintendedConsequences[].impact", - "fieldHtmlClass": "input", + "fieldHtmlClass": "input disabled", "prepend": "

Impact

", - "notitle": true, - "disabled": true + "notitle": true }, { "key": "unintendedConsequences[].likelihood", - "fieldHtmlClass": "input", + "fieldHtmlClass": "input disabled", "prepend": "

Likelihood

", - "notitle": true, - "disabled": true + "notitle": true } ] }, @@ -88,7 +84,7 @@ "type": "submit", "title": "View risk register", "htmlClass": "submitButton", - "next": "riskRegister", - "quickNav": "true" + "next": "", + "quickNav": "false" } ] \ No newline at end of file diff --git a/public/data/forms/riskEvaluation.json b/public/data/forms/riskEvaluation.json index d3143d8..5a1222c 100644 --- a/public/data/forms/riskEvaluation.json +++ b/public/data/forms/riskEvaluation.json @@ -11,18 +11,16 @@ "items": [ { "key": "unintendedConsequences[].consequence", - "fieldHtmlClass": "input inputText", + "fieldHtmlClass": "input inputText disabled", "placeholder": "Unintended consequence", "prepend": "

Unintended consequence

", - "notitle": true, - "disabled": true + "notitle": true }, { "key": "unintendedConsequences[].outcome", - "fieldHtmlClass": "input", + "fieldHtmlClass": "input disabled", "prepend": "

Outcome

", - "notitle": true, - "disabled": true + "notitle": true }, { "type": "fieldset", diff --git a/public/js/dashboard.js b/public/js/dashboard.js new file mode 100644 index 0000000..7283dfd --- /dev/null +++ b/public/js/dashboard.js @@ -0,0 +1,136 @@ +function addRiskDonut(riskCounts) { + const ctx = document.getElementById('riskChart').getContext('2d'); + new Chart(ctx, { + type: 'doughnut', + data: { + labels: Object.keys(riskCounts), + datasets: [{ + label: 'Risk count', + data: Object.values(riskCounts), + backgroundColor: [ + 'rgba(200, 200, 200, 0.5)', + 'rgba(255, 99, 132, 0.5)', + 'rgba(255, 206, 86, 0.5)', + 'rgba(54, 162, 235, 0.5)' + + ], + borderColor: [ + 'rgba(200, 200, 200, 1)', + 'rgba(255, 99, 132, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(54, 162, 235, 1)' + ], + borderWidth: 1 + }] + }, + options: { + aspectRatio: 2, + responsive: true, + plugins: { + legend: { + position: 'right', + }, + title: { + display: false, + text: 'Risk Counts' + } + } + } + }); +} +function addAverages(averageScores) { + const likelihoodBar = document.getElementById('likelihood-bar'); + likelihoodBar.style.width = (averageScores.likelihood / 3 * 100) + '%'; + likelihoodBar.innerText = averageScores.likelihood; + likelihoodBar.style.backgroundColor = getColorForScore(averageScores.likelihood); + + const impactBar = document.getElementById('impact-bar'); + impactBar.style.width = (averageScores.impact / 3 * 100) + '%'; + impactBar.innerText = averageScores.impact; + impactBar.style.backgroundColor = getColorForScore(averageScores.impact); + + const riskBar = document.getElementById('risk-bar'); + riskBar.style.width = (averageScores.riskScore / 9 * 100) + '%'; + riskBar.innerText = averageScores.riskScore; + riskBar.style.backgroundColor = getColorForScore(averageScores.riskScore / 3); +} + +function addTopRisks(topRisks) { + const tableBody = document.getElementById('topRisksTableBody'); + tableBody.innerHTML = ''; // Clear existing rows + + topRisks.forEach(risk => { + const row = tableBody.insertRow(); + const scoreText = getScoreText(risk.score/3); + const scoreColor = getColorForScore(risk.score/3); + + row.innerHTML = ` + ${risk.consequence} + ${scoreText} + View + `; + }); +} + +function getScoreText(score) { + if (score < 1) { + return 'Low'; + } else if (score < 2) { + return 'Medium'; + } else { + return 'High'; + } +} + +function getColorForScore(score) { + if (score > 2) { + return 'rgba(255, 99, 132, 0.75)'; + } else if (score >= 1 && score <= 2) { + return 'rgba(255, 206, 86, 0.75)'; + } else { + return 'rgba(54, 162, 235, 0.75)'; + } +} + +async function addRiskScoreToProject(project) { + for (const unintendedConsequence of project.unintendedConsequences) { + // Check if both impact and likelihood are defined + if (unintendedConsequence.likelihood && unintendedConsequence.impact) { + // Calculate risk score for the unintended consequence + let riskScore = 1; // Default risk score + switch (unintendedConsequence.likelihood) { + case 'High': + riskScore *= 3; + break; + case 'Medium': + riskScore *= 2; + break; + case 'Low': + riskScore *= 1; + break; + default: + // Handle unknown likelihood + break; + } + switch (unintendedConsequence.impact) { + case 'High': + riskScore *= 3; + break; + case 'Medium': + riskScore *= 2; + break; + case 'Low': + riskScore *= 1; + break; + default: + // Handle unknown impact + break; + } + // Add risk score to the unintended consequence + unintendedConsequence.riskScore = riskScore; + } else { + unintendedConsequence.riskScore = null; + } + } + return project; +} \ No newline at end of file diff --git a/routes/project.js b/routes/project.js index 37c92e8..52e00c9 100644 --- a/routes/project.js +++ b/routes/project.js @@ -33,9 +33,25 @@ router.get('/:id/completeAssessment', ensureAuthenticated, checkProjectAccess, l next(error); // Pass error to error handling middleware } }); +// GET route to retrieve a project by ID +router.get('/:id/riskSummary', ensureAuthenticated, checkProjectAccess, loadProject, async (req, res, next) => { + try { + // Find the project by ID + let project = res.locals.project; + project = await projectController.addRiskScoreToProject(project); + const userProjects = []; + userProjects.push(project); + let metrics = await projectController.getUserProjectMetrics(userProjects); + + return res.json(metrics); + + } catch (error) { + next(error); // Pass error to error handling middleware + } +}); // GET route to retrieve a project by ID -router.get('/:id/:page?', ensureAuthenticated, checkProjectAccess, loadProject, async (req, res, next) => { +router.get('/:id/:page', ensureAuthenticated, checkProjectAccess, loadProject, async (req, res, next) => { try { // Find the project by ID const project = res.locals.project; @@ -44,14 +60,9 @@ router.get('/:id/:page?', ensureAuthenticated, checkProjectAccess, loadProject, const acceptHeader = req.get('Accept'); if (acceptHeader === 'application/json') { - // Respond with JSON + // Respond with JSON (filter it according to the schema?) return res.json(project); } else { - // Respond with HTML (rendering scan.ejs) - let page = { - link: "projectDetails", - title: "Project details" - }; // Check if the page parameter is provided const pages = require('../pages.json'); const pageParam = req.params.page; @@ -71,6 +82,31 @@ router.get('/:id/:page?', ensureAuthenticated, checkProjectAccess, loadProject, } }); +// GET route to retrieve a project by ID +router.get('/:id', ensureAuthenticated, checkProjectAccess, loadProject, async (req, res, next) => { + try { + // Find the project by ID + const project = res.locals.project; + + // Content negotiation based on request Accept header + const acceptHeader = req.get('Accept'); + + if (acceptHeader === 'application/json') { + // Respond with JSON (filter it according to the schema?) + return res.json(project); + } else { + let page = { + link: "/finalReport", + title: "Project evaluation" + }; + res.locals.page = page; + res.render('pages/project', { project: project }); + } + } catch (error) { + next(error); // Pass error to error handling middleware + } +}); + // POST route to create a new project router.post('/', ensureAuthenticated, async (req, res) => { try { diff --git a/views/pages/project.ejs b/views/pages/project.ejs new file mode 100644 index 0000000..6772d35 --- /dev/null +++ b/views/pages/project.ejs @@ -0,0 +1,201 @@ +<%- include('../partials/header') %> + + + +
+
+
+

+

Objectives

+

+

Data used

+

+

Stakeholders

+ +

Intended conseuqneces

+ +

Risk dashboard

+
+ +
+

Risks

+
+
+ +
+
+ +
+

Stats (average)

+
+
+

Likelihood

+
+
+
+

Impact

+
+
+
+

Risk

+
+
+
+
+
+ +
+

Priority (top 5)

+
+
+ + + + + + + + + + + +
RiskLevelAction
+
+
+
+

Risk management table

+ + + + + + + + + + + + + + +
ConsequenceRisk categoryLikelihoodImpactRoleActionTimescaleAssigneeKPI
+
+
+ +
+ +<%- include('../partials/footer') %> diff --git a/views/pages/projects.ejs b/views/pages/projects.ejs index 20b35f4..bf2fbfe 100644 --- a/views/pages/projects.ejs +++ b/views/pages/projects.ejs @@ -8,6 +8,7 @@ +
@@ -85,7 +86,6 @@ $(document).ready(function () { }) .then(response => response.json()) .then(data => { - console.log(data); renderMyProjects(data.ownedProjects.projects); renderSharedProjects(data.sharedProjects); addRiskDonut(data.ownedProjects.riskCounts); @@ -105,7 +105,7 @@ function renderMyProjects(data) { width: '12%', render: function(data, type, row) { if (data == "done") { - return "Complete"; + return 'Complete
'; } if (data == "inProgress") { return "In Progress"; @@ -152,7 +152,12 @@ function renderMyProjects(data) { $('#myProjectsTable').on('click', '.editBtn', function () { var id = $(this).data('id'); - window.location.href = '/project/' + id; + window.location.href = '/project/' + id + "/projectDetails"; + }); + + $('#myProjectsTable').on('click', '.viewOutput', function () { + var id = $(this).data('id'); + window.location.href = '/project/' + id + "/"; }); $('#myProjectsTable').on('click', '.deleteBtn', function () { @@ -234,102 +239,8 @@ function renderSharedProjects(data) { $('#sharedProjectsTable').on('click', '.editBtn', function () { var id = $(this).data('id'); - window.location.href = '/project/' + id; - }); -} - -function addRiskDonut(riskCounts) { - const ctx = document.getElementById('riskChart').getContext('2d'); - new Chart(ctx, { - type: 'doughnut', - data: { - labels: Object.keys(riskCounts), - datasets: [{ - label: 'Risk count', - data: Object.values(riskCounts), - backgroundColor: [ - 'rgba(200, 200, 200, 0.5)', - 'rgba(255, 99, 132, 0.5)', - 'rgba(255, 206, 86, 0.5)', - 'rgba(54, 162, 235, 0.5)' - - ], - borderColor: [ - 'rgba(200, 200, 200, 1)', - 'rgba(255, 99, 132, 1)', - 'rgba(255, 206, 86, 1)', - 'rgba(54, 162, 235, 1)' - ], - borderWidth: 1 - }] - }, - options: { - aspectRatio: 2, - responsive: true, - plugins: { - legend: { - position: 'right', - }, - title: { - display: false, - text: 'Risk Counts' - } - } - } + window.location.href = '/project/' + id + "/projectDetails"; }); } -function addAverages(averageScores) { - const likelihoodBar = document.getElementById('likelihood-bar'); - likelihoodBar.style.width = (averageScores.likelihood / 3 * 100) + '%'; - likelihoodBar.innerText = averageScores.likelihood; - likelihoodBar.style.backgroundColor = getColorForScore(averageScores.likelihood); - - const impactBar = document.getElementById('impact-bar'); - impactBar.style.width = (averageScores.impact / 3 * 100) + '%'; - impactBar.innerText = averageScores.impact; - impactBar.style.backgroundColor = getColorForScore(averageScores.impact); - - const riskBar = document.getElementById('risk-bar'); - riskBar.style.width = (averageScores.riskScore / 9 * 100) + '%'; - riskBar.innerText = averageScores.riskScore; - riskBar.style.backgroundColor = getColorForScore(averageScores.riskScore / 3); -} - -function addTopRisks(topRisks) { - const tableBody = document.getElementById('topRisksTableBody'); - tableBody.innerHTML = ''; // Clear existing rows - - topRisks.forEach(risk => { - const row = tableBody.insertRow(); - const scoreText = getScoreText(risk.score/3); - const scoreColor = getColorForScore(risk.score/3); - - row.innerHTML = ` - ${risk.consequence} - ${scoreText} - View - `; - }); -} - -function getScoreText(score) { - if (score < 1) { - return 'Low'; - } else if (score < 2) { - return 'Medium'; - } else { - return 'High'; - } - } - -function getColorForScore(score) { - if (score > 2) { - return 'rgba(255, 99, 132, 0.75)'; - } else if (score >= 1 && score <= 2) { - return 'rgba(255, 206, 86, 0.75)'; - } else { - return 'rgba(54, 162, 235, 0.75)'; - } -} <%- include('../partials/footer') %> \ No newline at end of file diff --git a/views/pages/scan.ejs b/views/pages/scan.ejs index c6a2739..f7e746c 100644 --- a/views/pages/scan.ejs +++ b/views/pages/scan.ejs @@ -213,6 +213,7 @@ function moveSubmitButton() { } async function sendDataToServer(inputObject) { + console.log(inputObject); let url; let method; const form = document.getElementById("dataForm"); @@ -249,7 +250,9 @@ async function sendDataToServer(inputObject) { if (quickNavAttributeValue === "true" && nextAttributeValue) { quickNav(nextAttributeValue); } else if (projectId && nextAttributeValue) { - window.location.href = `/project/${projectId}/${nextAttributeValue}`; + window.location.href = `/project/${projectId}/${nextAttributeValue}`; + } else { + window.location.href = `/project/${projectId}/`; } } else { console.error("Request failed with status:", response.status); @@ -267,7 +270,7 @@ function extractDisabledFieldValues(formId) { return; } - const disabledFields = form.querySelectorAll('input[disabled], select[disabled], textarea[disabled]'); + const disabledFields = form.querySelectorAll('input.disabled, select.disabled, textarea.disabled'); if (disabledFields.length === 0) { return; @@ -275,10 +278,16 @@ function extractDisabledFieldValues(formId) { disabledFields.forEach(field => { const fieldValue = field.value; - // Replace input field with a paragraph element containing the value + + // Hide the original field + field.style.display = 'none'; + + // Create a new paragraph element to display the field value const paragraphElement = document.createElement('p'); - paragraphElement.textContent = `${fieldValue}`; - field.replaceWith(paragraphElement); + paragraphElement.textContent = fieldValue; + + // Insert the new paragraph element after the original field + field.parentNode.insertBefore(paragraphElement, field.nextSibling); }); } diff --git a/views/partials/header.ejs b/views/partials/header.ejs index d13ea4d..1208736 100644 --- a/views/partials/header.ejs +++ b/views/partials/header.ejs @@ -54,6 +54,7 @@ <% }); %> +
  • Final report
  • <% } %>