diff --git a/client/components/editor/editor-modal-maturitymatrix.vue b/client/components/editor/editor-modal-maturitymatrix.vue index 620c015e..e7615a70 100644 --- a/client/components/editor/editor-modal-maturitymatrix.vue +++ b/client/components/editor/editor-modal-maturitymatrix.vue @@ -2,7 +2,7 @@ v-card.editor-modal-maturitymatrix.animated.fadeIn(flat, tile) iframe( ref='matrix' - src='/_assets/maturity-matrix/index.html' + :src='iframeSrc' frameborder='0' ) @@ -12,7 +12,10 @@ import { sync } from 'vuex-pathify' export default { computed: { - activeModal: sync('editor/activeModal') + activeModal: sync('editor/activeModal'), + iframeSrc () { + return `/_assets/maturity-matrix/index.html?v=${Date.now()}` + } }, methods: { close () { diff --git a/client/static/maturity-matrix/index.html b/client/static/maturity-matrix/index.html index 887ffd6c..5950ee12 100644 --- a/client/static/maturity-matrix/index.html +++ b/client/static/maturity-matrix/index.html @@ -210,6 +210,7 @@ select.cell-input { cursor: pointer; }
Use Case Maturity Assessment Matrix
+
@@ -255,9 +256,6 @@ select.cell-input { cursor: pointer; } -
-
-
@@ -389,7 +387,9 @@ let state = { ] }; /* __STATE_END__ */ -let nextId = 300; +const INITIAL_STATE_JSON = JSON.stringify(state); +const INITIAL_NEXT_ID = 300; +let nextId = INITIAL_NEXT_ID; let modalTargetSection = null; let sectionCharts = {}; @@ -471,6 +471,12 @@ function buildSectionEl(section) {
+
+
${section.name} KPIs
+
+ +
+
@@ -774,27 +780,20 @@ function radarOptions() { } function initCharts() { - const container = document.getElementById('charts-container'); - container.innerHTML = ''; sectionCharts = {}; state.sections.forEach(section => { - // Size the canvas based on KPI count so labels always have room + const canvas = document.getElementById(`chart-${section.id}`); + if (!canvas) return; + const kpiCount = section.kpis.filter(k => k.applicable === 'y').length; const SIZE = Math.max(700, kpiCount * 62); + canvas.width = SIZE; + canvas.height = SIZE; + canvas.style.width = SIZE + 'px'; + canvas.style.height = SIZE + 'px'; - const card = document.createElement('div'); - card.className = 'chart-card'; - // Wrapper keeps the fixed-size canvas centred inside the full-width card - card.innerHTML = ` -
${section.name} KPIs
-
- -
- `; - container.appendChild(card); - - const ctx = document.getElementById(`chart-${section.id}`).getContext('2d'); + const ctx = canvas.getContext('2d'); sectionCharts[section.id] = new Chart(ctx, { type: 'radar', data: { @@ -882,6 +881,19 @@ function exitToWiki() { } } +function resetToInitial() { + if (!confirm('Reset to the initial template? All current edits will be discarded.')) return; + state = JSON.parse(INITIAL_STATE_JSON); + nextId = INITIAL_NEXT_ID; + const cn = document.getElementById('customerName'); + const uc = document.getElementById('useCaseName'); + if (cn) cn.value = state.customer; + if (uc) uc.value = state.useCase; + render(); + initCharts(); + showToast('Reset to initial template', 'success'); +} + // ═══════════════════════════════════════════════════ // EXPORT MARKDOWN w/ VEGA RADAR CHARTS // ═══════════════════════════════════════════════════ @@ -1166,21 +1178,42 @@ function buildMarkdown() { md += `**Generated:** ${ts}\n\n`; md += `---\n\n`; md += `## Summary Scores\n\n`; - md += `| Section | Score | Status |\n`; - md += `|---------|-------|--------|\n`; + md += `\`\`\`infographic height=300\n`; + md += `infographic list-grid-progress-card\n`; + md += `data\n`; + md += ` lists\n`; + + const SECTION_ICONS = { + 'Performance': 'speedometer', + 'Availability': 'shield check', + 'Excellence': 'star' + }; + const palette = []; + let totalFinal = 0, totalMax = 0; state.sections.forEach(section => { const { finalScore, maxScore, ratio } = calcSection(section); + totalFinal += finalScore; + totalMax += maxScore; const pct = (ratio * 100).toFixed(1); - const status = statusForRatio(ratio); - const statusEmoji = status === 'red' ? '🔴' : status === 'amber' ? '🟡' : '🟢'; - md += `| ${section.name} | ${pct}% (${finalScore}/${maxScore}) | ${statusEmoji} **${status.toUpperCase()}** |\n`; + const icon = SECTION_ICONS[section.name] || 'chart line'; + md += ` - label ${section.name}\n`; + md += ` value ${pct}\n`; + md += ` desc ${finalScore} / ${maxScore} pts\n`; + md += ` icon ${icon}\n`; + palette.push(section.color || '#6B7280'); }); const overall = calcOverall(); - const overallStatus = statusForRatio(overall); - const overallEmoji = overallStatus === 'red' ? '🔴' : overallStatus === 'amber' ? '🟡' : '🟢'; - md += `| **Overall** | **${(overall*100).toFixed(1)}%** | ${overallEmoji} **${overallStatus.toUpperCase()}** |\n\n`; + md += ` - label Overall\n`; + md += ` value ${(overall * 100).toFixed(1)}\n`; + md += ` desc ${totalFinal} / ${totalMax} pts\n`; + md += ` icon trophy\n`; + palette.push('#C0392B'); + + md += `theme\n`; + md += ` palette ${palette.join(' ')}\n`; + md += `\`\`\`\n\n`; state.sections.forEach(section => { const { finalScore, maxScore, ratio } = calcSection(section); @@ -1194,12 +1227,24 @@ function buildMarkdown() { md += `| KPI | Score | Weight | Final | Max | Applicable | Notes | Action Plan |\n`; md += `|-----|-------|--------|-------|-----|------------|-------|-------------|\n`; + const SCORE_COLOR = ['red', 'orange', 'green']; section.kpis.forEach(k => { - const fs = k.applicable==='y' ? k.score*k.weight : 0; - const ms = k.applicable==='y' ? 2*k.weight : 0; + const applicable = k.applicable === 'y'; + const fs = applicable ? k.score * k.weight : 0; + const ms = applicable ? 2 * k.weight : 0; const sl = ['Not Started','In Progress','Complete'][k.score]; const wl = ['','Low','Medium','High'][k.weight]; - md += `| ${k.kpi} | ${k.score} (${sl}) | ${k.weight} (${wl}) | ${fs} | ${ms} | ${k.applicable.toUpperCase()} | ${k.notes||'—'} | ${k.action||'—'} |\n`; + const color = SCORE_COLOR[k.score]; + const wrap = (s) => applicable ? s : `~~${s}~~`; + const scoreCell = wrap(`${k.score} (${sl})`); + const weightCell = `${k.weight} (${wl})`; + const finalCell = wrap(`${fs}`); + const maxCell = wrap(`${ms}`); + const kpiCell = wrap(`**${k.kpi}**`); + const notesCell = wrap(k.notes || '—'); + const actionCell = wrap(k.action || '—'); + const appCell = applicable ? '✅' : '❌'; + md += `| ${kpiCell} | ${scoreCell} | ${weightCell} | ${finalCell} | ${maxCell} | ${appCell} | ${notesCell} | ${actionCell} |\n`; }); md += `\n`; }); @@ -1227,9 +1272,23 @@ function parseMarkdown(markdownString) { let maxKpiId = 0; function parseScoreCell(cell) { - const m = cell.match(/^\s*(\d+)/); + // Cell may be prefixed with emoji (🔴/🟡/🟢/⚪/⬇️/➡️/⬆️) or wrapped in markdown emphasis. + const m = cell.match(/(\d+)/); return m ? parseInt(m[1], 10) : 0; } + function stripEmphasis(cell) { + return cell + .replace(/^~~(.+)~~$/, '$1') + .replace(/^\*\*(.+)\*\*$/, '$1') + .replace(/^_(.+)_$/, '$1') + .trim(); + } + function isApplicable(cell) { + const t = cell.trim(); + if (t.includes('✅')) return true; + if (t.includes('❌')) return false; + return t.toLowerCase().startsWith('y'); + } for (let i = 0; i < lines.length; i++) { const line = lines[i]; @@ -1292,14 +1351,16 @@ function parseMarkdown(markdownString) { const [kpiName, scoreCell, weightCell, finalCell, maxCell, applicableCell, notesCell, actionCell] = cells; const id = ++nextId; if (id > maxKpiId) maxKpiId = id; + const notesStripped = stripEmphasis(notesCell); + const actionStripped = stripEmphasis(actionCell); const kpi = { id, - kpi: kpiName, + kpi: stripEmphasis(kpiName), score: parseScoreCell(scoreCell), weight: parseScoreCell(weightCell), - applicable: applicableCell.toLowerCase().startsWith('y') ? 'y' : 'n', - notes: notesCell === '—' ? '' : notesCell, - action: actionCell === '—' ? '' : actionCell + applicable: isApplicable(applicableCell) ? 'y' : 'n', + notes: notesStripped === '—' ? '' : notesStripped, + action: actionStripped === '—' ? '' : actionStripped }; currentSection.kpis.push(kpi); }