@atlashub/smartstack-cli 3.7.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/templates/skills/business-analyse/html/ba-interactive.html +3058 -2252
- package/templates/skills/business-analyse/html/build-html.js +77 -0
- package/templates/skills/business-analyse/html/src/scripts/01-data-init.js +129 -0
- package/templates/skills/business-analyse/html/src/scripts/02-navigation.js +22 -0
- package/templates/skills/business-analyse/html/src/scripts/03-render-cadrage.js +208 -0
- package/templates/skills/business-analyse/html/src/scripts/04-render-modules.js +211 -0
- package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +542 -0
- package/templates/skills/business-analyse/html/src/scripts/06-render-consolidation.js +105 -0
- package/templates/skills/business-analyse/html/src/scripts/07-render-handoff.js +90 -0
- package/templates/skills/business-analyse/html/src/scripts/08-editing.js +45 -0
- package/templates/skills/business-analyse/html/src/scripts/09-export.js +65 -0
- package/templates/skills/business-analyse/html/src/scripts/10-comments.js +165 -0
- package/templates/skills/business-analyse/html/src/scripts/11-review-panel.js +139 -0
- package/templates/skills/business-analyse/html/src/styles/01-variables.css +38 -0
- package/templates/skills/business-analyse/html/src/styles/02-layout.css +101 -0
- package/templates/skills/business-analyse/html/src/styles/03-navigation.css +62 -0
- package/templates/skills/business-analyse/html/src/styles/04-cards.css +196 -0
- package/templates/skills/business-analyse/html/src/styles/05-modules.css +325 -0
- package/templates/skills/business-analyse/html/src/styles/06-wireframes.css +230 -0
- package/templates/skills/business-analyse/html/src/styles/07-comments.css +184 -0
- package/templates/skills/business-analyse/html/src/styles/08-review-panel.css +229 -0
- package/templates/skills/business-analyse/html/src/template.html +622 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build script for ba-interactive.html
|
|
5
|
+
*
|
|
6
|
+
* Assembles the HTML template from split source files:
|
|
7
|
+
* - src/template.html (shell with placeholders)
|
|
8
|
+
* - src/styles/*.css (concatenated in alphabetical order)
|
|
9
|
+
* - src/scripts/*.js (concatenated in alphabetical order)
|
|
10
|
+
*
|
|
11
|
+
* Output: ba-interactive.html (single file, compatible with file:// protocol)
|
|
12
|
+
*
|
|
13
|
+
* Preserves template placeholders: {{FEATURE_DATA}}, {{EMBEDDED_ARTIFACTS}}, {{APPLICATION_NAME}}, {{APPLICATION_ID}}
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const SRC_DIR = path.join(__dirname, 'src');
|
|
20
|
+
const STYLES_DIR = path.join(SRC_DIR, 'styles');
|
|
21
|
+
const SCRIPTS_DIR = path.join(SRC_DIR, 'scripts');
|
|
22
|
+
const TEMPLATE_FILE = path.join(SRC_DIR, 'template.html');
|
|
23
|
+
const OUTPUT_FILE = path.join(__dirname, 'ba-interactive.html');
|
|
24
|
+
|
|
25
|
+
function readSortedFiles(dir, extension) {
|
|
26
|
+
const files = fs.readdirSync(dir)
|
|
27
|
+
.filter(f => f.endsWith(extension))
|
|
28
|
+
.sort();
|
|
29
|
+
|
|
30
|
+
return files.map(f => {
|
|
31
|
+
const content = fs.readFileSync(path.join(dir, f), 'utf8');
|
|
32
|
+
return { name: f, content };
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function build() {
|
|
37
|
+
console.log('[build-html] Starting build...');
|
|
38
|
+
|
|
39
|
+
// Read template
|
|
40
|
+
if (!fs.existsSync(TEMPLATE_FILE)) {
|
|
41
|
+
console.error('[build-html] ERROR: Template not found at', TEMPLATE_FILE);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
let template = fs.readFileSync(TEMPLATE_FILE, 'utf8');
|
|
45
|
+
|
|
46
|
+
// Read and concatenate CSS files
|
|
47
|
+
const cssFiles = readSortedFiles(STYLES_DIR, '.css');
|
|
48
|
+
console.log(`[build-html] CSS files: ${cssFiles.map(f => f.name).join(', ')}`);
|
|
49
|
+
const css = cssFiles.map(f => `/* --- ${f.name} --- */\n${f.content}`).join('\n\n');
|
|
50
|
+
|
|
51
|
+
// Read and concatenate JS files
|
|
52
|
+
const jsFiles = readSortedFiles(SCRIPTS_DIR, '.js');
|
|
53
|
+
console.log(`[build-html] JS files: ${jsFiles.map(f => f.name).join(', ')}`);
|
|
54
|
+
const js = jsFiles.map(f => `/* --- ${f.name} --- */\n${f.content}`).join('\n\n');
|
|
55
|
+
|
|
56
|
+
// Inject into template
|
|
57
|
+
template = template.replace('<!-- CSS_PLACEHOLDER -->', css);
|
|
58
|
+
template = template.replace('<!-- JS_PLACEHOLDER -->', js);
|
|
59
|
+
|
|
60
|
+
// Verify placeholders are preserved
|
|
61
|
+
const requiredPlaceholders = ['{{FEATURE_DATA}}', '{{EMBEDDED_ARTIFACTS}}', '{{APPLICATION_NAME}}'];
|
|
62
|
+
const missing = requiredPlaceholders.filter(p => !template.includes(p));
|
|
63
|
+
if (missing.length > 0) {
|
|
64
|
+
console.error('[build-html] ERROR: Missing placeholders:', missing.join(', '));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Write output
|
|
69
|
+
fs.writeFileSync(OUTPUT_FILE, template, 'utf8');
|
|
70
|
+
|
|
71
|
+
const sizeKB = Math.round(fs.statSync(OUTPUT_FILE).size / 1024);
|
|
72
|
+
console.log(`[build-html] Output: ${OUTPUT_FILE} (${sizeKB} KB)`);
|
|
73
|
+
console.log(`[build-html] CSS: ${cssFiles.length} files, JS: ${jsFiles.length} files`);
|
|
74
|
+
console.log('[build-html] Build complete.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
build();
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
DATA STORE
|
|
3
|
+
============================================ */
|
|
4
|
+
const APP_KEY = 'ba-{{APPLICATION_ID}}';
|
|
5
|
+
let data = {{FEATURE_DATA}};
|
|
6
|
+
const EMBEDDED_ARTIFACTS = {{EMBEDDED_ARTIFACTS}};
|
|
7
|
+
|
|
8
|
+
// Initialize optional data structures
|
|
9
|
+
data.wireframeComments = data.wireframeComments || {};
|
|
10
|
+
data.specComments = data.specComments || {};
|
|
11
|
+
data.customRoles = data.customRoles || [];
|
|
12
|
+
data.customActions = data.customActions || [];
|
|
13
|
+
data.cadrage.criteria = data.cadrage.criteria || [];
|
|
14
|
+
data.cadrage.risks = data.cadrage.risks || [];
|
|
15
|
+
data.consolidation = data.consolidation || {};
|
|
16
|
+
data.consolidation.e2eFlows = data.consolidation.e2eFlows || [];
|
|
17
|
+
|
|
18
|
+
// Initialize comments array
|
|
19
|
+
data.comments = data.comments || [];
|
|
20
|
+
|
|
21
|
+
// Defensive mapping: globalScope (feature.json format) -> scope (HTML format)
|
|
22
|
+
// Handles cases where FEATURE_DATA contains raw globalScope instead of pre-mapped scope
|
|
23
|
+
if (!data.cadrage.scope || (!data.cadrage.scope.vital?.length && data.cadrage.globalScope)) {
|
|
24
|
+
const gs = data.cadrage.globalScope || {};
|
|
25
|
+
data.cadrage.scope = {
|
|
26
|
+
vital: (gs.mustHave || []).map(s => typeof s === 'string' ? { name: s, description: '' } : s),
|
|
27
|
+
important: (gs.shouldHave || []).map(s => typeof s === 'string' ? { name: s, description: '' } : s),
|
|
28
|
+
optional: (gs.couldHave || []).map(s => typeof s === 'string' ? { name: s, description: '' } : s),
|
|
29
|
+
excluded: (gs.outOfScope || []).map(s => typeof s === 'string' ? { name: s, description: '' } : s)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
data.cadrage.scope = data.cadrage.scope || { vital: [], important: [], optional: [], excluded: [] };
|
|
33
|
+
|
|
34
|
+
// Defensive init: moduleSpecs (may be missing if LLM didn't follow mapping)
|
|
35
|
+
data.moduleSpecs = data.moduleSpecs || {};
|
|
36
|
+
|
|
37
|
+
// Vibe coding mode: hide non-relevant sections
|
|
38
|
+
const isVibeCoding = data.metadata?.vibeCoding === true;
|
|
39
|
+
if (isVibeCoding) {
|
|
40
|
+
document.querySelectorAll('[data-vibe-hide]').forEach(el => el.style.display = 'none');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* ============================================
|
|
44
|
+
UTILITIES
|
|
45
|
+
============================================ */
|
|
46
|
+
function showNotification(message) {
|
|
47
|
+
const el = document.getElementById('notification');
|
|
48
|
+
el.textContent = message;
|
|
49
|
+
el.classList.add('visible');
|
|
50
|
+
setTimeout(() => el.classList.remove('visible'), 2500);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toggleForm(formId) {
|
|
54
|
+
document.getElementById(formId).classList.toggle('visible');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function clearForm(formId) {
|
|
58
|
+
document.querySelectorAll('#' + formId + ' input, #' + formId + ' textarea').forEach(el => el.value = '');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getNestedValue(obj, path) {
|
|
62
|
+
return path.split('.').reduce((o, k) => (o || {})[k], obj);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function setNestedValue(obj, path, value) {
|
|
66
|
+
const keys = path.split('.');
|
|
67
|
+
let current = obj;
|
|
68
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
69
|
+
if (!current[keys[i]]) current[keys[i]] = {};
|
|
70
|
+
current = current[keys[i]];
|
|
71
|
+
}
|
|
72
|
+
current[keys[keys.length - 1]] = value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function updateCounts() {
|
|
76
|
+
document.getElementById('stakeholderCount').textContent = data.cadrage.stakeholders.length;
|
|
77
|
+
document.getElementById('moduleCount').textContent = data.modules.length;
|
|
78
|
+
updateModulesNav();
|
|
79
|
+
updateDepSelects();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatFrequency(f) {
|
|
83
|
+
return { daily: 'Quotidien', weekly: 'Hebdomadaire', monthly: 'Mensuel', occasional: 'Occasionnel' }[f] || f;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatAccess(a) {
|
|
87
|
+
return { admin: 'Administration', manager: 'Supervision', contributor: 'Contribution', viewer: 'Consultation' }[a] || a;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatPriority(p) {
|
|
91
|
+
return { vital: 'Indispensable', important: 'Important', optional: 'Optionnel', excluded: 'Hors perimetre' }[p] || p;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatLevel(l) {
|
|
95
|
+
return { high: 'Fort', medium: 'Moyen', low: 'Faible' }[l] || l;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatModuleType(t) {
|
|
99
|
+
return { 'data-centric': 'Donnees', 'workflow': 'Processus', 'reporting': 'Rapports', 'integration': 'Integration', 'full-module': 'Complet' }[t] || t;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function formatModulePriority(p) {
|
|
103
|
+
return { must: 'Indispensable', should: 'Important', could: 'Optionnel' }[p] || p;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ============================================
|
|
107
|
+
INITIALIZATION
|
|
108
|
+
============================================ */
|
|
109
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
110
|
+
loadFromLocalStorage();
|
|
111
|
+
initEditableFields();
|
|
112
|
+
// Re-apply vibe hiding after DOM init (in case loadFromLocalStorage changed data)
|
|
113
|
+
if (isVibeCoding) {
|
|
114
|
+
document.querySelectorAll('[data-vibe-hide]').forEach(el => el.style.display = 'none');
|
|
115
|
+
}
|
|
116
|
+
renderStakeholders();
|
|
117
|
+
renderScope();
|
|
118
|
+
renderRisks();
|
|
119
|
+
renderCriteria();
|
|
120
|
+
renderModules();
|
|
121
|
+
renderDependencies();
|
|
122
|
+
renderAllModuleSpecs();
|
|
123
|
+
renderConsolidation();
|
|
124
|
+
renderHandoff();
|
|
125
|
+
renderE2EFlows();
|
|
126
|
+
updateCounts();
|
|
127
|
+
initInlineComments();
|
|
128
|
+
renderReviewPanel();
|
|
129
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
NAVIGATION
|
|
3
|
+
============================================ */
|
|
4
|
+
let currentSectionId = 'cadrage-problem';
|
|
5
|
+
|
|
6
|
+
function showSection(sectionId) {
|
|
7
|
+
currentSectionId = sectionId;
|
|
8
|
+
document.querySelectorAll('.section').forEach(s => s.style.display = 'none');
|
|
9
|
+
const section = document.getElementById(sectionId);
|
|
10
|
+
if (section) section.style.display = 'block';
|
|
11
|
+
|
|
12
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
13
|
+
const navItem = document.querySelector('[data-section="' + sectionId + '"]');
|
|
14
|
+
if (navItem) navItem.classList.add('active');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function restoreCurrentSection() {
|
|
18
|
+
if (currentSectionId) {
|
|
19
|
+
const section = document.getElementById(currentSectionId);
|
|
20
|
+
if (section) section.style.display = 'block';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
EDITABLE FIELDS
|
|
3
|
+
============================================ */
|
|
4
|
+
function initEditableFields() {
|
|
5
|
+
document.querySelectorAll('.editable[data-field]').forEach(el => {
|
|
6
|
+
const field = el.dataset.field;
|
|
7
|
+
const value = getNestedValue(data, 'cadrage.' + field);
|
|
8
|
+
if (value) el.textContent = value;
|
|
9
|
+
|
|
10
|
+
el.addEventListener('blur', function() {
|
|
11
|
+
setNestedValue(data, 'cadrage.' + field, this.textContent.trim());
|
|
12
|
+
autoSave();
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* ============================================
|
|
18
|
+
STAKEHOLDERS
|
|
19
|
+
============================================ */
|
|
20
|
+
function addStakeholder() {
|
|
21
|
+
const role = document.getElementById('sh-role').value.trim();
|
|
22
|
+
if (!role) return;
|
|
23
|
+
|
|
24
|
+
data.cadrage.stakeholders.push({
|
|
25
|
+
role: role,
|
|
26
|
+
function: document.getElementById('sh-function').value.trim(),
|
|
27
|
+
tasks: document.getElementById('sh-tasks').value.split('\n').filter(t => t.trim()),
|
|
28
|
+
frequency: document.getElementById('sh-frequency').value,
|
|
29
|
+
access: document.getElementById('sh-access').value,
|
|
30
|
+
frustrations: document.getElementById('sh-frustrations').value.trim()
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
renderStakeholders();
|
|
34
|
+
toggleForm('addStakeholderForm');
|
|
35
|
+
clearForm('addStakeholderForm');
|
|
36
|
+
updateCounts();
|
|
37
|
+
autoSave();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderStakeholders() {
|
|
41
|
+
const grid = document.getElementById('stakeholderGrid');
|
|
42
|
+
grid.innerHTML = data.cadrage.stakeholders.map((s, i) => `
|
|
43
|
+
<div class="stakeholder-card">
|
|
44
|
+
<div style="display:flex;justify-content:space-between;align-items:start;">
|
|
45
|
+
<div class="stakeholder-role">${s.role}</div>
|
|
46
|
+
<button class="btn btn-sm" onclick="removeStakeholder(${i})" style="opacity:0.5;font-size:0.7rem;">Supprimer</button>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="stakeholder-function">${s.function || ''}</div>
|
|
49
|
+
<ul class="stakeholder-tasks">
|
|
50
|
+
${(s.tasks || []).map(t => '<li>' + t + '</li>').join('')}
|
|
51
|
+
</ul>
|
|
52
|
+
<div class="stakeholder-meta">
|
|
53
|
+
<span>${formatFrequency(s.frequency)}</span>
|
|
54
|
+
<span>${formatAccess(s.access)}</span>
|
|
55
|
+
</div>
|
|
56
|
+
${s.frustrations ? '<div style="font-size:0.8rem;color:var(--warning);margin-top:0.5rem;font-style:italic;">' + s.frustrations + '</div>' : ''}
|
|
57
|
+
</div>
|
|
58
|
+
`).join('');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function removeStakeholder(index) {
|
|
62
|
+
data.cadrage.stakeholders.splice(index, 1);
|
|
63
|
+
renderStakeholders();
|
|
64
|
+
updateCounts();
|
|
65
|
+
autoSave();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ============================================
|
|
69
|
+
SCOPE ITEMS
|
|
70
|
+
============================================ */
|
|
71
|
+
function addScopeItem(priority) {
|
|
72
|
+
const name = document.getElementById('scope-name-' + priority).value.trim();
|
|
73
|
+
if (!name) return;
|
|
74
|
+
const description = document.getElementById('scope-desc-' + priority).value.trim();
|
|
75
|
+
|
|
76
|
+
data.cadrage.scope[priority].push({ name, description });
|
|
77
|
+
renderScope();
|
|
78
|
+
toggleForm('addScopeForm-' + priority);
|
|
79
|
+
document.getElementById('scope-name-' + priority).value = '';
|
|
80
|
+
document.getElementById('scope-desc-' + priority).value = '';
|
|
81
|
+
autoSave();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function renderScope() {
|
|
85
|
+
['vital', 'important', 'optional', 'excluded'].forEach(p => {
|
|
86
|
+
const container = document.getElementById('scope' + p.charAt(0).toUpperCase() + p.slice(1));
|
|
87
|
+
container.innerHTML = data.cadrage.scope[p].map((item, i) => `
|
|
88
|
+
<div class="uc-item">
|
|
89
|
+
<div class="uc-header">
|
|
90
|
+
<span class="priority priority-${p}">${formatPriority(p)}</span>
|
|
91
|
+
<span class="uc-title">${item.name}</span>
|
|
92
|
+
<div class="uc-actions">
|
|
93
|
+
<button class="btn btn-sm" onclick="removeScopeItem('${p}',${i})">Supprimer</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
${item.description ? '<div class="uc-detail">' + item.description + '</div>' : ''}
|
|
97
|
+
</div>
|
|
98
|
+
`).join('');
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function removeScopeItem(priority, index) {
|
|
103
|
+
data.cadrage.scope[priority].splice(index, 1);
|
|
104
|
+
renderScope();
|
|
105
|
+
autoSave();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* ============================================
|
|
109
|
+
ACCEPTANCE CRITERIA
|
|
110
|
+
============================================ */
|
|
111
|
+
function addCriterion() {
|
|
112
|
+
const text = document.getElementById('criterion-text').value.trim();
|
|
113
|
+
if (!text) return;
|
|
114
|
+
if (!data.cadrage.criteria) data.cadrage.criteria = [];
|
|
115
|
+
data.cadrage.criteria.push({ text, validated: false });
|
|
116
|
+
renderCriteria();
|
|
117
|
+
toggleForm('addCriterionForm');
|
|
118
|
+
document.getElementById('criterion-text').value = '';
|
|
119
|
+
autoSave();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function removeCriterion(index) {
|
|
123
|
+
data.cadrage.criteria.splice(index, 1);
|
|
124
|
+
renderCriteria();
|
|
125
|
+
autoSave();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toggleCriterion(index) {
|
|
129
|
+
data.cadrage.criteria[index].validated = !data.cadrage.criteria[index].validated;
|
|
130
|
+
renderCriteria();
|
|
131
|
+
autoSave();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderCriteria() {
|
|
135
|
+
const container = document.getElementById('criteriaList');
|
|
136
|
+
if (!container) return;
|
|
137
|
+
const criteria = data.cadrage.criteria || [];
|
|
138
|
+
container.innerHTML = criteria.map((c, i) => `
|
|
139
|
+
<div class="uc-item" style="display:flex;align-items:center;gap:0.75rem;">
|
|
140
|
+
<input type="checkbox" ${c.validated ? 'checked' : ''} onchange="toggleCriterion(${i})" style="cursor:pointer;width:18px;height:18px;flex-shrink:0;">
|
|
141
|
+
<span style="flex:1;${c.validated ? 'text-decoration:line-through;color:var(--text-muted);' : ''}">${c.text}</span>
|
|
142
|
+
<button class="btn btn-sm" onclick="removeCriterion(${i})" style="opacity:0.5;flex-shrink:0;">✕</button>
|
|
143
|
+
</div>
|
|
144
|
+
`).join('');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ============================================
|
|
148
|
+
RISKS
|
|
149
|
+
============================================ */
|
|
150
|
+
function addRisk() {
|
|
151
|
+
const desc = document.getElementById('risk-desc').value.trim();
|
|
152
|
+
if (!desc) return;
|
|
153
|
+
|
|
154
|
+
data.cadrage.risks.push({
|
|
155
|
+
description: desc,
|
|
156
|
+
probability: document.getElementById('risk-probability').value,
|
|
157
|
+
impact: document.getElementById('risk-impact').value,
|
|
158
|
+
mitigation: document.getElementById('risk-mitigation').value.trim()
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
renderRisks();
|
|
162
|
+
toggleForm('addRiskForm');
|
|
163
|
+
clearForm('addRiskForm');
|
|
164
|
+
autoSave();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderRisks() {
|
|
168
|
+
const list = document.getElementById('risksList');
|
|
169
|
+
list.innerHTML = data.cadrage.risks.map((r, i) => {
|
|
170
|
+
const level = (r.probability === 'high' && r.impact === 'high') ? 'critical'
|
|
171
|
+
: (r.probability === 'low' && r.impact === 'low') ? 'low' : 'medium';
|
|
172
|
+
return `
|
|
173
|
+
<div class="risk-item">
|
|
174
|
+
<div class="risk-level risk-${level}"></div>
|
|
175
|
+
<div>
|
|
176
|
+
<div class="risk-text">${r.description}</div>
|
|
177
|
+
${r.mitigation ? '<div style="font-size:0.75rem;color:var(--text-muted);margin-top:0.25rem;">Prevention : ' + r.mitigation + '</div>' : ''}
|
|
178
|
+
</div>
|
|
179
|
+
<div class="risk-probability">${formatLevel(r.probability)}</div>
|
|
180
|
+
<div class="risk-impact">${formatLevel(r.impact)}</div>
|
|
181
|
+
</div>
|
|
182
|
+
`;
|
|
183
|
+
}).join('');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* ============================================
|
|
187
|
+
PROCESS STEPS
|
|
188
|
+
============================================ */
|
|
189
|
+
function addProcessStep() {
|
|
190
|
+
const label = prompt('Nom de l\'etape :');
|
|
191
|
+
if (!label) return;
|
|
192
|
+
if (!data.cadrage.current.steps) data.cadrage.current.steps = [];
|
|
193
|
+
data.cadrage.current.steps.push(label);
|
|
194
|
+
renderProcessFlow();
|
|
195
|
+
autoSave();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function renderProcessFlow() {
|
|
199
|
+
const container = document.getElementById('processFlow');
|
|
200
|
+
const steps = data.cadrage.current.steps || [];
|
|
201
|
+
container.innerHTML = steps.map((step, i) => `
|
|
202
|
+
<div class="process-step">
|
|
203
|
+
<div class="process-step-number">Etape ${i + 1}</div>
|
|
204
|
+
<div class="process-step-label">${step}</div>
|
|
205
|
+
</div>
|
|
206
|
+
${i < steps.length - 1 ? '<div class="process-arrow">→</div>' : ''}
|
|
207
|
+
`).join('');
|
|
208
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
MODULE MANAGEMENT
|
|
3
|
+
============================================ */
|
|
4
|
+
function addModule() {
|
|
5
|
+
const name = document.getElementById('mod-name').value.trim();
|
|
6
|
+
if (!name) return;
|
|
7
|
+
const code = name.replace(/[^a-zA-Z0-9]/g, '');
|
|
8
|
+
|
|
9
|
+
data.modules.push({
|
|
10
|
+
code: code,
|
|
11
|
+
name: name,
|
|
12
|
+
description: document.getElementById('mod-desc').value.trim(),
|
|
13
|
+
featureType: document.getElementById('mod-type').value,
|
|
14
|
+
priority: document.getElementById('mod-priority').value,
|
|
15
|
+
entities: document.getElementById('mod-entities').value.split('\n').filter(e => e.trim()),
|
|
16
|
+
status: 'pending'
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Initialize module spec
|
|
20
|
+
if (!data.moduleSpecs[code]) {
|
|
21
|
+
data.moduleSpecs[code] = {
|
|
22
|
+
useCases: [],
|
|
23
|
+
businessRules: [],
|
|
24
|
+
entities: [],
|
|
25
|
+
permissions: [],
|
|
26
|
+
notes: ''
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
renderModules();
|
|
31
|
+
renderDependencies();
|
|
32
|
+
renderAllModuleSpecs();
|
|
33
|
+
toggleForm('addModuleForm');
|
|
34
|
+
clearForm('addModuleForm');
|
|
35
|
+
updateCounts();
|
|
36
|
+
autoSave();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function removeModule(index) {
|
|
40
|
+
if (!confirm('Supprimer ce domaine fonctionnel ?')) return;
|
|
41
|
+
const code = data.modules[index].code;
|
|
42
|
+
data.modules.splice(index, 1);
|
|
43
|
+
delete data.moduleSpecs[code];
|
|
44
|
+
data.dependencies = data.dependencies.filter(d => d.from !== code && d.to !== code);
|
|
45
|
+
renderModules();
|
|
46
|
+
renderDependencies();
|
|
47
|
+
renderAllModuleSpecs();
|
|
48
|
+
updateCounts();
|
|
49
|
+
autoSave();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderModules() {
|
|
53
|
+
const grid = document.getElementById('moduleGrid');
|
|
54
|
+
grid.innerHTML = data.modules.map((m, i) => `
|
|
55
|
+
<div class="module-card" onclick="showSection('module-spec-${m.code}')">
|
|
56
|
+
<button class="module-card-remove" onclick="event.stopPropagation();removeModule(${i})">✕</button>
|
|
57
|
+
<div class="module-card-header">
|
|
58
|
+
<span class="module-card-code">${m.name}</span>
|
|
59
|
+
<span class="module-card-type">${formatModuleType(m.featureType)}</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="module-card-desc">${m.description || ''}</div>
|
|
62
|
+
<div class="module-card-meta">
|
|
63
|
+
<span class="priority priority-${m.priority === 'must' ? 'vital' : m.priority === 'should' ? 'important' : 'optional'}">${formatModulePriority(m.priority)}</span>
|
|
64
|
+
<span>${(m.entities || []).length} donnees</span>
|
|
65
|
+
<span>${(data.moduleSpecs[m.code]?.useCases || []).length} cas d'utilisation</span>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
`).join('') || '<p style="color:var(--text-muted);text-align:center;padding:2rem;grid-column:1/-1;">Aucun domaine fonctionnel defini. Cliquez sur le bouton ci-dessous pour commencer.</p>';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function updateModulesNav() {
|
|
72
|
+
const nav = document.getElementById('modulesNav');
|
|
73
|
+
const navItems = data.modules.map(m => `
|
|
74
|
+
<a class="nav-item" onclick="showSection('module-spec-${m.code}')" data-section="module-spec-${m.code}">
|
|
75
|
+
<span class="nav-icon">●</span> ${m.name}
|
|
76
|
+
<span class="nav-badge">${(data.moduleSpecs[m.code]?.useCases || []).length}</span>
|
|
77
|
+
</a>
|
|
78
|
+
`).join('');
|
|
79
|
+
nav.innerHTML = '<div class="nav-group-title">3. Specification</div>' + (navItems || '<div style="padding:0.3rem 1rem;font-size:0.8rem;color:var(--text-muted);font-style:italic;">Aucun domaine</div>');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ============================================
|
|
83
|
+
DEPENDENCY MANAGEMENT
|
|
84
|
+
============================================ */
|
|
85
|
+
function updateDepSelects() {
|
|
86
|
+
const fromSel = document.getElementById('dep-from');
|
|
87
|
+
const toSel = document.getElementById('dep-to');
|
|
88
|
+
if (!fromSel || !toSel) return;
|
|
89
|
+
const opts = data.modules.map(m => `<option value="${m.code}">${m.name}</option>`).join('');
|
|
90
|
+
fromSel.innerHTML = opts;
|
|
91
|
+
toSel.innerHTML = opts;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function addDependency() {
|
|
95
|
+
const from = document.getElementById('dep-from').value;
|
|
96
|
+
const to = document.getElementById('dep-to').value;
|
|
97
|
+
if (!from || !to || from === to) return;
|
|
98
|
+
if (data.dependencies.some(d => d.from === from && d.to === to)) return;
|
|
99
|
+
|
|
100
|
+
data.dependencies.push({
|
|
101
|
+
from: from,
|
|
102
|
+
to: to,
|
|
103
|
+
description: document.getElementById('dep-desc').value.trim()
|
|
104
|
+
});
|
|
105
|
+
document.getElementById('dep-desc').value = '';
|
|
106
|
+
|
|
107
|
+
renderDependencies();
|
|
108
|
+
autoSave();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function removeDependency(index) {
|
|
112
|
+
data.dependencies.splice(index, 1);
|
|
113
|
+
renderDependencies();
|
|
114
|
+
autoSave();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderDependencies() {
|
|
118
|
+
// Render dependency list
|
|
119
|
+
const depList = document.getElementById('depList');
|
|
120
|
+
if (depList) {
|
|
121
|
+
depList.innerHTML = data.dependencies.map((d, i) => {
|
|
122
|
+
const fromName = data.modules.find(m => m.code === d.from)?.name || d.from;
|
|
123
|
+
const toName = data.modules.find(m => m.code === d.to)?.name || d.to;
|
|
124
|
+
return `
|
|
125
|
+
<div class="interaction-item">
|
|
126
|
+
<span style="font-weight:600;color:var(--text-bright);">${fromName}</span>
|
|
127
|
+
<span class="interaction-arrow">→</span>
|
|
128
|
+
<span style="font-weight:600;color:var(--text-bright);">${toName}</span>
|
|
129
|
+
<span style="flex:1;font-size:0.8rem;color:var(--text-muted);">${d.description || ''}</span>
|
|
130
|
+
<button class="btn btn-sm" onclick="removeDependency(${i})" style="opacity:0.5;">Supprimer</button>
|
|
131
|
+
</div>
|
|
132
|
+
`;
|
|
133
|
+
}).join('');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Render graph
|
|
137
|
+
renderDepGraph();
|
|
138
|
+
// Render processing order
|
|
139
|
+
renderProcessingOrder();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderDepGraph() {
|
|
143
|
+
const graph = document.getElementById('depGraph');
|
|
144
|
+
if (!graph || data.modules.length === 0) return;
|
|
145
|
+
|
|
146
|
+
const layers = computeTopologicalLayers();
|
|
147
|
+
if (!layers) {
|
|
148
|
+
graph.innerHTML = '<p style="color:var(--error);text-align:center;padding:1rem;">Dependance circulaire detectee ! Corrigez les dependances.</p>';
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
graph.innerHTML = layers.map((layer, i) => `
|
|
153
|
+
<div class="dep-layer">
|
|
154
|
+
<div class="dep-layer-label">Couche ${i + 1}</div>
|
|
155
|
+
<div class="dep-layer-modules">
|
|
156
|
+
${layer.map(code => {
|
|
157
|
+
const m = data.modules.find(mod => mod.code === code);
|
|
158
|
+
return `<div class="dep-module">${m ? (m.name || m.code) : code}</div>`;
|
|
159
|
+
}).join('')}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
${i < layers.length - 1 ? '<div class="dep-arrow">↓</div>' : ''}
|
|
163
|
+
`).join('');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function computeTopologicalLayers() {
|
|
167
|
+
const codes = data.modules.map(m => m.code);
|
|
168
|
+
const inDeg = {};
|
|
169
|
+
const adj = {};
|
|
170
|
+
codes.forEach(c => { inDeg[c] = 0; adj[c] = []; });
|
|
171
|
+
data.dependencies.forEach(d => {
|
|
172
|
+
if (inDeg[d.from] !== undefined && adj[d.to] !== undefined) {
|
|
173
|
+
adj[d.to].push(d.from);
|
|
174
|
+
inDeg[d.from]++;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const layers = [];
|
|
179
|
+
let remaining = new Set(codes);
|
|
180
|
+
while (remaining.size > 0) {
|
|
181
|
+
const layer = [];
|
|
182
|
+
remaining.forEach(c => {
|
|
183
|
+
if (inDeg[c] === 0) layer.push(c);
|
|
184
|
+
});
|
|
185
|
+
if (layer.length === 0) return null; // cycle
|
|
186
|
+
layers.push(layer);
|
|
187
|
+
layer.forEach(c => {
|
|
188
|
+
remaining.delete(c);
|
|
189
|
+
(adj[c] || []).forEach(dep => { inDeg[dep]--; });
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return layers;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function renderProcessingOrder() {
|
|
196
|
+
const container = document.getElementById('processingOrder');
|
|
197
|
+
if (!container) return;
|
|
198
|
+
const layers = computeTopologicalLayers();
|
|
199
|
+
if (!layers) { container.innerHTML = ''; return; }
|
|
200
|
+
const order = layers.flat();
|
|
201
|
+
container.innerHTML = order.map((code, i) => {
|
|
202
|
+
const m = data.modules.find(mod => mod.code === code);
|
|
203
|
+
return `
|
|
204
|
+
<div class="process-step">
|
|
205
|
+
<div class="process-step-number">Etape ${i + 1}</div>
|
|
206
|
+
<div class="process-step-label">${m ? (m.name || m.code) : code}</div>
|
|
207
|
+
</div>
|
|
208
|
+
${i < order.length - 1 ? '<div class="process-arrow">→</div>' : ''}
|
|
209
|
+
`;
|
|
210
|
+
}).join('');
|
|
211
|
+
}
|