/home/techb158/trellopowerup.abdallabala.com/src
Edit: /home/techb158/trellopowerup.abdallabala.com/src/assessmentAnalytics.js (7144B)
const MS_PER_DAY = 24 * 60 * 60 * 1000;
export function scoreValue(assessment) {
return Number(assessment?.score?.score || 0);
}
export function assessmentAgeDays(assessment, now = new Date()) {
const created = new Date(assessment?.createdAt).getTime();
if (!Number.isFinite(created)) return null;
return Math.max(0, Math.floor((now.getTime() - created) / MS_PER_DAY));
}
export function withTrend(assessment, previousAssessment) {
if (!assessment) return null;
const previousScore = previousAssessment ? scoreValue(previousAssessment) : null;
const scoreDelta = previousAssessment ? scoreValue(assessment) - previousScore : null;
return {
...assessment,
previousScore,
scoreDelta,
trend: scoreDelta === null ? 'new' : scoreDelta > 0 ? 'worse' : scoreDelta < 0 ? 'improved' : 'unchanged'
};
}
export function withReviewSignal(assessment, options = {}) {
if (!assessment) return null;
const now = options.now || new Date();
const reviewStaleDays = options.reviewStaleDays || 30;
const reasons = [];
const actions = [];
const ageDays = assessmentAgeDays(assessment, now);
const stale = ageDays !== null && ageDays >= reviewStaleDays;
if (assessment.score?.gate?.deploymentReady === false) {
reasons.push('Deployment gate blocked');
actions.push('Resolve deployment gate blockers before release.');
}
if (assessment.trend === 'worse') {
reasons.push('Risk score worsened');
actions.push('Review changed assumptions and update the mitigation plan.');
}
if (stale) {
reasons.push(`Assessment older than ${reviewStaleDays} days`);
actions.push('Refresh the assessment and confirm current controls.');
}
return {
...assessment,
review: {
needed: reasons.length > 0,
reasons,
actions,
stale,
ageDays,
staleAfterDays: reviewStaleDays
}
};
}
export function decorateAssessment(assessment, previousAssessment, options = {}) {
return withReviewSignal(withTrend(assessment, previousAssessment), options);
}
export function withCardHistoryTrends(assessments, options = {}) {
return assessments.map((assessment, index) => decorateAssessment(assessment, assessments[index + 1], options));
}
export function latestAssessmentsByCard(assessments, options = {}) {
const grouped = new Map();
for (const assessment of assessments) {
if (!grouped.has(assessment.cardId)) grouped.set(assessment.cardId, []);
grouped.get(assessment.cardId).push(assessment);
}
return [...grouped.values()].map((cardAssessments) => decorateAssessment(cardAssessments[0], cardAssessments[1], options));
}
export function firstRiskTitle(assessment) {
return assessment?.score?.highestRisk?.title || assessment?.input?.risks?.[0]?.title || '';
}
export function buildBoardSummaryFromAssessments(boardId, assessments, options = {}) {
const reviewStaleDays = options.reviewStaleDays || 30;
const latest = latestAssessmentsByCard(assessments, options);
const statusCounts = latest.reduce((acc, item) => {
const status = item.score?.status || 'unknown';
acc[status] = (acc[status] || 0) + 1;
return acc;
}, {});
const averageScore = latest.length
? Math.round(latest.reduce((sum, item) => sum + (item.score?.score || 0), 0) / latest.length)
: 0;
return {
boardId,
generatedAt: new Date().toISOString(),
cardCount: latest.length,
averageScore,
statusCounts,
deploymentBlocked: latest.filter((item) => item.score?.gate?.deploymentReady === false).length,
improved: latest.filter((item) => item.trend === 'improved').length,
worsened: latest.filter((item) => item.trend === 'worse').length,
unchanged: latest.filter((item) => item.trend === 'unchanged').length,
reviewNeeded: latest.filter((item) => item.review?.needed).length,
staleAssessments: latest.filter((item) => item.review?.stale).length,
reviewStaleDays,
latestAssessments: latest
};
}
function reviewPriority(item) {
const reasons = item.review?.reasons || [];
if (item.score?.gate?.deploymentReady === false) return 0;
if (item.trend === 'worse') return 1;
if (item.review?.stale) return 2;
if (reasons.length) return 3;
return 4;
}
export function buildBoardReviewQueueFromSummary(summary) {
const items = [...(summary.latestAssessments || [])]
.filter((item) => item.review?.needed)
.sort((a, b) => {
const priorityDelta = reviewPriority(a) - reviewPriority(b);
const scoreDelta = (b.score?.score || 0) - (a.score?.score || 0);
const ageDelta = (b.review?.ageDays || 0) - (a.review?.ageDays || 0);
return priorityDelta || scoreDelta || ageDelta;
});
return {
boardId: summary.boardId,
generatedAt: summary.generatedAt,
reviewStaleDays: summary.reviewStaleDays,
count: items.length,
items
};
}
function csvCell(value) {
const text = value === undefined || value === null ? '' : String(value);
return `"${text.replaceAll('"', '""')}"`;
}
export function buildBoardCsv(summary) {
const rows = [[
'board_id',
'card_id',
'assessment_id',
'created_at',
'risk_title',
'score',
'previous_score',
'score_delta',
'trend',
'status',
'deployment_ready',
'review_needed',
'review_reasons',
'recommended_actions',
'assessment_age_days',
'stale_assessment',
'blocking_reasons',
'project_typology',
'lifecycle_phase',
'measurement_maturity'
]];
for (const item of summary.latestAssessments) {
rows.push([
summary.boardId,
item.cardId,
item.id,
item.createdAt,
firstRiskTitle(item),
item.score?.score,
item.previousScore,
item.scoreDelta,
item.trend,
item.score?.status,
item.score?.gate?.deploymentReady,
item.review?.needed,
(item.review?.reasons || []).join('; '),
(item.review?.actions || []).join('; '),
item.review?.ageDays,
item.review?.stale,
(item.score?.gate?.blockingReasons || []).join('; '),
item.input?.projectTypology,
item.input?.lifecyclePhase,
item.input?.measurementMaturity
]);
}
return `${rows.map((row) => row.map(csvCell).join(',')).join('\n')}\n`;
}
export function buildReviewQueueCsv(queue) {
const rows = [[
'board_id',
'card_id',
'assessment_id',
'created_at',
'risk_title',
'score',
'trend',
'status',
'deployment_ready',
'review_reasons',
'recommended_actions',
'assessment_age_days',
'stale_assessment',
'blocking_reasons'
]];
for (const item of queue.items || []) {
rows.push([
queue.boardId,
item.cardId,
item.id,
item.createdAt,
firstRiskTitle(item),
item.score?.score,
item.trend,
item.score?.status,
item.score?.gate?.deploymentReady,
(item.review?.reasons || []).join('; '),
(item.review?.actions || []).join('; '),
item.review?.ageDays,
item.review?.stale,
(item.score?.gate?.blockingReasons || []).join('; ')
]);
}
return `${rows.map((row) => row.map(csvCell).join(',')).join('\n')}\n`;
}