@atlashub/smartstack-cli 3.20.0 → 3.22.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/dist/index.js +70 -6
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +69 -3
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/project/api.ts.template +8 -29
- package/templates/project/appsettings.json.template +1 -0
- package/templates/skills/application/references/application-roles-template.md +2 -2
- package/templates/skills/application/steps/step-05-frontend.md +40 -35
- package/templates/skills/application/templates-frontend.md +64 -36
- package/templates/skills/business-analyse/html/ba-interactive.html +642 -156
- package/templates/skills/business-analyse/html/src/scripts/01-data-init.js +11 -6
- package/templates/skills/business-analyse/html/src/scripts/02-navigation.js +209 -4
- package/templates/skills/business-analyse/html/src/scripts/04-render-modules.js +2 -8
- package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +95 -8
- package/templates/skills/business-analyse/html/src/scripts/07-render-handoff.js +3 -1
- package/templates/skills/business-analyse/html/src/scripts/08-editing.js +112 -22
- package/templates/skills/business-analyse/html/src/scripts/11-review-panel.js +7 -0
- package/templates/skills/business-analyse/html/src/styles/02-layout.css +1 -1
- package/templates/skills/business-analyse/html/src/styles/03-navigation.css +89 -31
- package/templates/skills/business-analyse/html/src/styles/05-modules.css +64 -0
- package/templates/skills/business-analyse/html/src/styles/06-wireframes.css +42 -0
- package/templates/skills/business-analyse/html/src/template.html +8 -76
- package/templates/skills/business-analyse/references/acceptance-criteria.md +169 -0
- package/templates/skills/business-analyse/references/deploy-data-build.md +13 -9
- package/templates/skills/business-analyse/references/handoff-file-templates.md +2 -1
- package/templates/skills/business-analyse/references/html-data-mapping.md +20 -28
- package/templates/skills/business-analyse/references/naming-conventions.md +245 -0
- package/templates/skills/business-analyse/references/validate-incremental-html.md +28 -5
- package/templates/skills/business-analyse/references/validation-checklist.md +31 -11
- package/templates/skills/business-analyse/references/wireframe-svg-style-guide.md +335 -0
- package/templates/skills/business-analyse/steps/step-03b-ui.md +59 -0
- package/templates/skills/business-analyse/steps/step-03c-compile.md +169 -2
- package/templates/skills/business-analyse/steps/step-03d-validate.md +217 -28
- package/templates/skills/business-analyse/steps/step-05a-handoff.md +189 -3
- package/templates/skills/business-analyse/steps/step-05b-deploy.md +55 -0
- package/templates/skills/ralph-loop/references/category-rules.md +5 -2
- package/templates/skills/ralph-loop/references/compact-loop.md +52 -1
- package/templates/skills/ralph-loop/references/core-seed-data.md +232 -21
- package/templates/skills/ralph-loop/steps/step-01-task.md +36 -4
- package/templates/skills/ralph-loop/steps/step-02-execute.md +81 -0
|
@@ -47,6 +47,14 @@ data.cadrage.scope = data.cadrage.scope || { vital: [], important: [], optional:
|
|
|
47
47
|
|
|
48
48
|
// Defensive init: moduleSpecs (may be missing if LLM didn't follow mapping)
|
|
49
49
|
data.moduleSpecs = data.moduleSpecs || {};
|
|
50
|
+
// Ensure ALL modules have a moduleSpecs entry (prevents missing schemas)
|
|
51
|
+
(data.modules || []).forEach(function(m) {
|
|
52
|
+
if (!data.moduleSpecs[m.code]) {
|
|
53
|
+
data.moduleSpecs[m.code] = { useCases: [], businessRules: [], entities: [], permissions: [], notes: '' };
|
|
54
|
+
}
|
|
55
|
+
// Ensure anticipatedSections array exists
|
|
56
|
+
m.anticipatedSections = m.anticipatedSections || [];
|
|
57
|
+
});
|
|
50
58
|
|
|
51
59
|
// Vibe coding mode: hide non-relevant sections
|
|
52
60
|
const isVibeCoding = data.metadata?.vibeCoding === true;
|
|
@@ -87,11 +95,7 @@ function setNestedValue(obj, path, value) {
|
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
function updateCounts() {
|
|
90
|
-
|
|
91
|
-
document.getElementById('moduleCount').textContent = data.modules.length;
|
|
92
|
-
const totalEntities = data.modules.reduce((sum, m) => sum + (data.moduleSpecs[m.code]?.entities || []).length, 0);
|
|
93
|
-
document.getElementById('entityCount').textContent = totalEntities;
|
|
94
|
-
updateModulesNav();
|
|
98
|
+
buildNavTree();
|
|
95
99
|
updateDepSelects();
|
|
96
100
|
}
|
|
97
101
|
|
|
@@ -139,7 +143,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
139
143
|
renderConsolidation();
|
|
140
144
|
renderHandoff();
|
|
141
145
|
renderE2EFlows();
|
|
142
|
-
|
|
146
|
+
buildNavTree();
|
|
147
|
+
updateDepSelects();
|
|
143
148
|
initInlineComments();
|
|
144
149
|
renderReviewPanel();
|
|
145
150
|
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/* ============================================
|
|
2
|
-
NAVIGATION
|
|
2
|
+
NAVIGATION - Hierarchical Tree
|
|
3
3
|
============================================ */
|
|
4
4
|
let currentSectionId = 'cadrage-context';
|
|
5
|
+
let navCollapseState = {};
|
|
6
|
+
|
|
7
|
+
/* ---------- Core ---------- */
|
|
5
8
|
|
|
6
9
|
function showSection(sectionId) {
|
|
7
10
|
currentSectionId = sectionId;
|
|
@@ -9,9 +12,30 @@ function showSection(sectionId) {
|
|
|
9
12
|
const section = document.getElementById(sectionId);
|
|
10
13
|
if (section) section.style.display = 'block';
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
// Highlight active nav item
|
|
16
|
+
document.querySelectorAll('#sidebarNav .nav-item').forEach(n => n.classList.remove('active'));
|
|
17
|
+
const navItem = document.querySelector('#sidebarNav [data-section="' + sectionId + '"]');
|
|
18
|
+
if (navItem) {
|
|
19
|
+
navItem.classList.add('active');
|
|
20
|
+
// Auto-expand parent groups to reveal active item
|
|
21
|
+
let parent = navItem.parentElement;
|
|
22
|
+
while (parent && parent.id !== 'sidebarNav') {
|
|
23
|
+
if (parent.classList.contains('nav-children') && parent.style.display === 'none') {
|
|
24
|
+
parent.style.display = '';
|
|
25
|
+
const groupEl = parent.parentElement;
|
|
26
|
+
if (groupEl) {
|
|
27
|
+
const groupId = groupEl.dataset.groupId;
|
|
28
|
+
if (groupId) {
|
|
29
|
+
navCollapseState[groupId] = false;
|
|
30
|
+
const chevron = groupEl.querySelector(':scope > .nav-group-title .nav-chevron, :scope > .nav-item.nav-module-header .nav-chevron');
|
|
31
|
+
if (chevron) chevron.classList.add('expanded');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
parent = parent.parentElement;
|
|
36
|
+
}
|
|
37
|
+
saveNavState();
|
|
38
|
+
}
|
|
15
39
|
}
|
|
16
40
|
|
|
17
41
|
function restoreCurrentSection() {
|
|
@@ -20,3 +44,184 @@ function restoreCurrentSection() {
|
|
|
20
44
|
if (section) section.style.display = 'block';
|
|
21
45
|
}
|
|
22
46
|
}
|
|
47
|
+
|
|
48
|
+
/* ---------- Tree Builder ---------- */
|
|
49
|
+
|
|
50
|
+
function buildNavTree() {
|
|
51
|
+
const nav = document.getElementById('sidebarNav');
|
|
52
|
+
if (!nav) return;
|
|
53
|
+
|
|
54
|
+
nav.innerHTML =
|
|
55
|
+
renderNavGroup('cadrage', 'Cadrage', buildCadrageItems()) +
|
|
56
|
+
renderNavGroup('modules', 'Modules (' + data.modules.length + ')', buildModuleItems()) +
|
|
57
|
+
renderNavGroup('consolidation', 'Consolidation', buildConsolidationItems()) +
|
|
58
|
+
renderNavGroup('synthese', 'Synthese', buildSyntheseItems());
|
|
59
|
+
|
|
60
|
+
restoreNavState();
|
|
61
|
+
highlightActiveNavItem();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function renderNavGroup(id, title, itemsHtml) {
|
|
65
|
+
const collapsed = navCollapseState[id] === true;
|
|
66
|
+
return '<div class="nav-group" data-group-id="' + id + '">' +
|
|
67
|
+
'<div class="nav-group-title" onclick="toggleNavGroup(\'' + id + '\')">' +
|
|
68
|
+
'<span class="nav-chevron ' + (collapsed ? '' : 'expanded') + '">▸</span> ' +
|
|
69
|
+
title +
|
|
70
|
+
'</div>' +
|
|
71
|
+
'<div class="nav-children"' + (collapsed ? ' style="display:none;"' : '') + '>' +
|
|
72
|
+
itemsHtml +
|
|
73
|
+
'</div>' +
|
|
74
|
+
'</div>';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderNavItem(sectionId, label, badge) {
|
|
78
|
+
return '<a class="nav-item" onclick="showSection(\'' + sectionId + '\')" data-section="' + sectionId + '">' +
|
|
79
|
+
'<span class="nav-icon">●</span> ' + label +
|
|
80
|
+
(badge !== undefined ? ' <span class="nav-badge">' + badge + '</span>' : '') +
|
|
81
|
+
'</a>';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ---------- Group Builders ---------- */
|
|
85
|
+
|
|
86
|
+
function buildCadrageItems() {
|
|
87
|
+
return renderNavItem('cadrage-context', 'Contexte') +
|
|
88
|
+
renderNavItem('cadrage-stakeholders', 'Parties prenantes', data.cadrage.stakeholders.length) +
|
|
89
|
+
renderNavItem('cadrage-scope', 'Perimetre fonctionnel') +
|
|
90
|
+
renderNavItem('cadrage-risks', 'Risques et hypotheses', data.cadrage.risks.length) +
|
|
91
|
+
renderNavItem('cadrage-success', 'Criteres de reussite');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildModuleItems() {
|
|
95
|
+
let html = '';
|
|
96
|
+
data.modules.forEach(function(mod) {
|
|
97
|
+
html += renderModuleNavItem(mod);
|
|
98
|
+
});
|
|
99
|
+
// Global module views at bottom
|
|
100
|
+
html += renderNavItem('decomp-modules', 'Vue d\'ensemble', data.modules.length);
|
|
101
|
+
html += renderNavItem('decomp-dependencies', 'Dependances', data.dependencies.length);
|
|
102
|
+
return html;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildConsolidationItems() {
|
|
106
|
+
var totalEntities = data.modules.reduce(function(sum, m) {
|
|
107
|
+
return sum + (data.moduleSpecs[m.code]?.entities || []).length;
|
|
108
|
+
}, 0);
|
|
109
|
+
return renderNavItem('consol-datamodel', 'Modele de donnees', totalEntities) +
|
|
110
|
+
renderNavItem('consol-interactions', 'Interactions') +
|
|
111
|
+
renderNavItem('consol-permissions', 'Coherence des acces') +
|
|
112
|
+
renderNavItem('consol-flows', 'Parcours bout en bout', data.consolidation.e2eFlows.length);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildSyntheseItems() {
|
|
116
|
+
return renderNavItem('handoff-summary', 'Vue d\'ensemble');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ---------- Module Sub-Tree ---------- */
|
|
120
|
+
|
|
121
|
+
function renderModuleNavItem(mod) {
|
|
122
|
+
var code = mod.code;
|
|
123
|
+
var spec = data.moduleSpecs[code] || {};
|
|
124
|
+
var ucCount = (spec.useCases || []).length;
|
|
125
|
+
var brCount = (spec.businessRules || []).length;
|
|
126
|
+
var entCount = (spec.entities || []).length;
|
|
127
|
+
var sections = mod.anticipatedSections || [];
|
|
128
|
+
var groupId = 'mod-' + code;
|
|
129
|
+
var collapsed = navCollapseState[groupId] === true;
|
|
130
|
+
|
|
131
|
+
var html = '<div class="nav-module" data-group-id="' + groupId + '">';
|
|
132
|
+
|
|
133
|
+
// Module header (clickable to expand + navigate to module spec)
|
|
134
|
+
html += '<a class="nav-item nav-module-header" onclick="toggleNavGroup(\'' + groupId + '\');showSection(\'module-spec-' + code + '\')" data-section="module-spec-' + code + '">';
|
|
135
|
+
html += '<span class="nav-chevron ' + (collapsed ? '' : 'expanded') + '">▸</span> ';
|
|
136
|
+
html += (mod.name || mod.code);
|
|
137
|
+
html += '</a>';
|
|
138
|
+
|
|
139
|
+
// Children: tabs
|
|
140
|
+
html += '<div class="nav-children"' + (collapsed ? ' style="display:none;"' : '') + '>';
|
|
141
|
+
html += renderModuleTabNavItem(code, 'uc', 'Cas d\'utilisation', ucCount);
|
|
142
|
+
html += renderModuleTabNavItem(code, 'br', 'Regles metier', brCount);
|
|
143
|
+
html += renderModuleTabNavItem(code, 'ent', 'Donnees', entCount);
|
|
144
|
+
html += renderModuleTabNavItem(code, 'perm', 'Droits d\'acces');
|
|
145
|
+
html += renderModuleTabNavItem(code, 'mock', 'Maquettes');
|
|
146
|
+
html += renderModuleTabNavItem(code, 'struct', 'Structure', sections.length);
|
|
147
|
+
|
|
148
|
+
// Children: sections/resources (deeper tree)
|
|
149
|
+
if (sections.length > 0) {
|
|
150
|
+
sections.forEach(function(section) {
|
|
151
|
+
var resources = section.resources || [];
|
|
152
|
+
html += '<div class="nav-section-item">';
|
|
153
|
+
html += '<a class="nav-item nav-section-link" onclick="showSection(\'module-spec-' + code + '\');switchTab(\'' + code + '\',\'struct\')" data-section="module-struct-' + code + '-' + section.code + '">';
|
|
154
|
+
html += '<span class="nav-icon nav-icon-section">▷</span> ' + section.code;
|
|
155
|
+
if (resources.length > 0) html += ' <span class="nav-badge">' + resources.length + '</span>';
|
|
156
|
+
html += '</a>';
|
|
157
|
+
if (resources.length > 0) {
|
|
158
|
+
html += '<div class="nav-children nav-resources">';
|
|
159
|
+
resources.forEach(function(res) {
|
|
160
|
+
var resName = typeof res === 'string' ? res : (res.code || res.name || '');
|
|
161
|
+
html += '<a class="nav-item nav-resource-link">';
|
|
162
|
+
html += '<span class="nav-icon nav-icon-resource">•</span> ' + resName;
|
|
163
|
+
html += '</a>';
|
|
164
|
+
});
|
|
165
|
+
html += '</div>';
|
|
166
|
+
}
|
|
167
|
+
html += '</div>';
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
html += '</div>'; // nav-children
|
|
172
|
+
html += '</div>'; // nav-module
|
|
173
|
+
return html;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderModuleTabNavItem(code, tabId, label, badge) {
|
|
177
|
+
var sectionId = 'module-spec-' + code;
|
|
178
|
+
return '<a class="nav-item" onclick="showSection(\'' + sectionId + '\');switchTab(\'' + code + '\',\'' + tabId + '\')" data-section="' + sectionId + '-' + tabId + '">' +
|
|
179
|
+
'<span class="nav-icon">●</span> ' + label +
|
|
180
|
+
(badge !== undefined ? ' <span class="nav-badge">' + badge + '</span>' : '') +
|
|
181
|
+
'</a>';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ---------- Collapse/Expand ---------- */
|
|
185
|
+
|
|
186
|
+
function toggleNavGroup(groupId) {
|
|
187
|
+
navCollapseState[groupId] = !navCollapseState[groupId];
|
|
188
|
+
var groupEl = document.querySelector('[data-group-id="' + groupId + '"]');
|
|
189
|
+
if (groupEl) {
|
|
190
|
+
var children = groupEl.querySelector(':scope > .nav-children');
|
|
191
|
+
var chevron = groupEl.querySelector(':scope > .nav-group-title .nav-chevron, :scope > .nav-item.nav-module-header .nav-chevron');
|
|
192
|
+
if (children) children.style.display = navCollapseState[groupId] ? 'none' : '';
|
|
193
|
+
if (chevron) chevron.classList.toggle('expanded', !navCollapseState[groupId]);
|
|
194
|
+
}
|
|
195
|
+
saveNavState();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function saveNavState() {
|
|
199
|
+
try { localStorage.setItem(APP_KEY + '-nav', JSON.stringify(navCollapseState)); } catch(e) {}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function restoreNavState() {
|
|
203
|
+
try {
|
|
204
|
+
var saved = localStorage.getItem(APP_KEY + '-nav');
|
|
205
|
+
if (saved) {
|
|
206
|
+
navCollapseState = JSON.parse(saved);
|
|
207
|
+
// Apply collapse state to all groups
|
|
208
|
+
Object.keys(navCollapseState).forEach(function(groupId) {
|
|
209
|
+
if (navCollapseState[groupId]) {
|
|
210
|
+
var groupEl = document.querySelector('[data-group-id="' + groupId + '"]');
|
|
211
|
+
if (groupEl) {
|
|
212
|
+
var children = groupEl.querySelector(':scope > .nav-children');
|
|
213
|
+
var chevron = groupEl.querySelector(':scope > .nav-group-title .nav-chevron, :scope > .nav-item.nav-module-header .nav-chevron');
|
|
214
|
+
if (children) children.style.display = 'none';
|
|
215
|
+
if (chevron) chevron.classList.remove('expanded');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
} catch(e) {}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function highlightActiveNavItem() {
|
|
224
|
+
if (!currentSectionId) return;
|
|
225
|
+
var navItem = document.querySelector('#sidebarNav [data-section="' + currentSectionId + '"]');
|
|
226
|
+
if (navItem) navItem.classList.add('active');
|
|
227
|
+
}
|
|
@@ -13,6 +13,7 @@ function addModule() {
|
|
|
13
13
|
featureType: document.getElementById('mod-type').value,
|
|
14
14
|
priority: document.getElementById('mod-priority').value,
|
|
15
15
|
entities: document.getElementById('mod-entities').value.split('\n').filter(e => e.trim()),
|
|
16
|
+
anticipatedSections: [],
|
|
16
17
|
status: 'pending'
|
|
17
18
|
});
|
|
18
19
|
|
|
@@ -69,14 +70,7 @@ function renderModules() {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
function updateModulesNav() {
|
|
72
|
-
|
|
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>');
|
|
73
|
+
buildNavTree();
|
|
80
74
|
}
|
|
81
75
|
|
|
82
76
|
/* ============================================
|
|
@@ -37,6 +37,7 @@ function renderModuleSpecSection(mod) {
|
|
|
37
37
|
<button class="tab-btn" onclick="switchTab('${code}', 'perm')">Droits d'acces</button>
|
|
38
38
|
<button class="tab-btn" onclick="switchTab('${code}', 'mock')">Maquettes</button>
|
|
39
39
|
<button class="tab-btn" onclick="switchTab('${code}', 'notes')">Notes</button>
|
|
40
|
+
<button class="tab-btn" onclick="switchTab('${code}', 'struct')">Structure</button>
|
|
40
41
|
</div>
|
|
41
42
|
|
|
42
43
|
<!-- TAB: Cas d'utilisation -->
|
|
@@ -113,7 +114,12 @@ function renderModuleSpecSection(mod) {
|
|
|
113
114
|
<div class="tab-panel" id="tab-${code}-ent">
|
|
114
115
|
<p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:1rem;">Les types de donnees que ce domaine gere. Decrivez les informations importantes a enregistrer pour chaque type.</p>
|
|
115
116
|
<div id="entList-${code}">
|
|
116
|
-
${spec.entities.
|
|
117
|
+
${spec.entities.length > 0
|
|
118
|
+
? spec.entities.map((ent, i) => renderEntity(code, ent, i)).join('')
|
|
119
|
+
: (mod.entities || []).length > 0
|
|
120
|
+
? '<div class="card" style="color:var(--text-muted);padding:1.5rem;"><p style="margin-bottom:0.75rem;">Les schemas detailles ne sont pas encore disponibles. Entites identifiees :</p><ul style="list-style:disc;padding-left:1.5rem;">' + mod.entities.map(e => '<li>' + e + '</li>').join('') + '</ul><p style="font-size:0.8rem;margin-top:0.75rem;font-style:italic;">Utilisez le bouton ci-dessous pour ajouter les schemas detailles.</p></div>'
|
|
121
|
+
: ''
|
|
122
|
+
}
|
|
117
123
|
</div>
|
|
118
124
|
<button class="add-btn" onclick="toggleForm('addEntForm-${code}')">+ Ajouter un type de donnees</button>
|
|
119
125
|
<div class="inline-form" id="addEntForm-${code}">
|
|
@@ -190,6 +196,14 @@ function renderModuleSpecSection(mod) {
|
|
|
190
196
|
<div class="editable" contenteditable="true" data-module-code="${code}" data-module-field="notes" data-placeholder="Notez ici tout ce qui concerne ce domaine : questions, precisions, contraintes particulieres...">${spec.notes || ''}</div>
|
|
191
197
|
</div>
|
|
192
198
|
</div>
|
|
199
|
+
|
|
200
|
+
<!-- TAB: Structure (sections/resources) -->
|
|
201
|
+
<div class="tab-panel" id="tab-${code}-struct">
|
|
202
|
+
<p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:1rem;">Organisation des ecrans et ressources de ce domaine. Structure hierarchique : sections regroupant des ressources.</p>
|
|
203
|
+
<div id="structContainer-${code}">
|
|
204
|
+
${renderModuleStructure(code)}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
193
207
|
</div>`;
|
|
194
208
|
}
|
|
195
209
|
|
|
@@ -373,18 +387,30 @@ function renderModuleMockups(code) {
|
|
|
373
387
|
</div>`;
|
|
374
388
|
}
|
|
375
389
|
|
|
376
|
-
return wireframes.map((wf, i) =>
|
|
390
|
+
return wireframes.map((wf, i) => {
|
|
391
|
+
const hasSvg = !!wf.svgContent;
|
|
392
|
+
const wireframeId = `wf-${code}-${i}`;
|
|
393
|
+
|
|
394
|
+
return `
|
|
377
395
|
<div class="mockup-frame" style="${i > 0 ? 'margin-top:1.5rem;' : ''}">
|
|
378
396
|
<div class="mockup-toolbar">
|
|
379
397
|
<div class="mockup-dot mockup-dot-red"></div>
|
|
380
398
|
<div class="mockup-dot mockup-dot-yellow"></div>
|
|
381
399
|
<div class="mockup-dot mockup-dot-green"></div>
|
|
382
400
|
<span class="mockup-title">${wf.screen || wf.section}</span>
|
|
401
|
+
${hasSvg ? `
|
|
402
|
+
<div class="wireframe-toggle">
|
|
403
|
+
<button class="wireframe-toggle-btn active" data-target="${wireframeId}" data-view="svg" onclick="toggleWireframeView('${wireframeId}', 'svg')">SVG</button>
|
|
404
|
+
<button class="wireframe-toggle-btn" data-target="${wireframeId}" data-view="ascii" onclick="toggleWireframeView('${wireframeId}', 'ascii')">ASCII</button>
|
|
405
|
+
</div>` : ''}
|
|
383
406
|
</div>
|
|
384
|
-
<div class="mockup-content">
|
|
385
|
-
${
|
|
386
|
-
? `<
|
|
387
|
-
|
|
407
|
+
<div class="mockup-content" id="${wireframeId}">
|
|
408
|
+
${hasSvg
|
|
409
|
+
? `<div class="svg-wireframe wireframe-view active" data-view="svg">${wf.svgContent}</div>
|
|
410
|
+
<pre class="ascii-wireframe wireframe-view" data-view="ascii" style="display:none;">${wf.content || ''}</pre>`
|
|
411
|
+
: (wf.format === 'ascii'
|
|
412
|
+
? `<pre class="ascii-wireframe">${wf.content || ''}</pre>`
|
|
413
|
+
: `<div class="svg-wireframe">${wf.content || ''}</div>`)}
|
|
388
414
|
</div>
|
|
389
415
|
${wf.description ? `
|
|
390
416
|
<div class="wireframe-description">
|
|
@@ -423,7 +449,27 @@ function renderModuleMockups(code) {
|
|
|
423
449
|
>${getWireframeComment(code, wf.screen)}</textarea>
|
|
424
450
|
</div>
|
|
425
451
|
</div>
|
|
426
|
-
`).join('');
|
|
452
|
+
`}).join('');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function toggleWireframeView(wireframeId, view) {
|
|
456
|
+
const container = document.getElementById(wireframeId);
|
|
457
|
+
if (!container) return;
|
|
458
|
+
|
|
459
|
+
// Toggle visibility of wireframe views
|
|
460
|
+
container.querySelectorAll('.wireframe-view').forEach(el => {
|
|
461
|
+
const isTarget = el.dataset.view === view;
|
|
462
|
+
el.style.display = isTarget ? '' : 'none';
|
|
463
|
+
el.classList.toggle('active', isTarget);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// Toggle button active states
|
|
467
|
+
const toolbar = container.closest('.mockup-frame').querySelector('.wireframe-toggle');
|
|
468
|
+
if (toolbar) {
|
|
469
|
+
toolbar.querySelectorAll('.wireframe-toggle-btn').forEach(btn => {
|
|
470
|
+
btn.classList.toggle('active', btn.dataset.view === view);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
427
473
|
}
|
|
428
474
|
|
|
429
475
|
function getPermRoles() {
|
|
@@ -547,6 +593,47 @@ function refreshAllPermGrids() {
|
|
|
547
593
|
});
|
|
548
594
|
}
|
|
549
595
|
|
|
596
|
+
function renderModuleStructure(code) {
|
|
597
|
+
const mod = data.modules.find(m => m.code === code);
|
|
598
|
+
const sections = mod ? (mod.anticipatedSections || []) : [];
|
|
599
|
+
|
|
600
|
+
if (sections.length === 0) {
|
|
601
|
+
return '<div class="card" style="text-align:center;padding:2rem;color:var(--text-muted);">' +
|
|
602
|
+
'<p>Aucune section definie pour ce module.</p>' +
|
|
603
|
+
'<p style="font-size:0.85rem;margin-top:0.5rem;">Les sections et ressources seront identifiees lors de l\'analyse approfondie.</p>' +
|
|
604
|
+
'</div>';
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return sections.map(function(section) {
|
|
608
|
+
var resources = section.resources || [];
|
|
609
|
+
var html = '<div class="struct-section">';
|
|
610
|
+
html += '<div class="struct-section-header">';
|
|
611
|
+
html += '<span class="struct-section-code">' + (section.code || section.name || '') + '</span>';
|
|
612
|
+
if (section.description) {
|
|
613
|
+
html += '<span class="struct-section-desc">' + section.description + '</span>';
|
|
614
|
+
}
|
|
615
|
+
html += '<span class="struct-section-badge">' + resources.length + ' ressource' + (resources.length !== 1 ? 's' : '') + '</span>';
|
|
616
|
+
html += '</div>';
|
|
617
|
+
|
|
618
|
+
if (resources.length > 0) {
|
|
619
|
+
html += '<div class="struct-resources">';
|
|
620
|
+
resources.forEach(function(res) {
|
|
621
|
+
var resName = typeof res === 'string' ? res : (res.code || res.name || '');
|
|
622
|
+
var resDesc = typeof res === 'object' ? (res.description || '') : '';
|
|
623
|
+
html += '<div class="struct-resource">';
|
|
624
|
+
html += '<span class="struct-resource-icon">•</span>';
|
|
625
|
+
html += '<span class="struct-resource-name">' + resName + '</span>';
|
|
626
|
+
if (resDesc) html += '<span class="struct-resource-desc">' + resDesc + '</span>';
|
|
627
|
+
html += '</div>';
|
|
628
|
+
});
|
|
629
|
+
html += '</div>';
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
html += '</div>';
|
|
633
|
+
return html;
|
|
634
|
+
}).join('');
|
|
635
|
+
}
|
|
636
|
+
|
|
550
637
|
function switchTab(code, tabId) {
|
|
551
638
|
const section = document.getElementById('module-spec-' + code);
|
|
552
639
|
if (!section) return;
|
|
@@ -555,6 +642,6 @@ function switchTab(code, tabId) {
|
|
|
555
642
|
const targetPanel = document.getElementById('tab-' + code + '-' + tabId);
|
|
556
643
|
if (targetPanel) targetPanel.classList.add('active');
|
|
557
644
|
const buttons = section.querySelectorAll('.tab-btn');
|
|
558
|
-
const tabIndex = { uc: 0, br: 1, ent: 2, perm: 3, mock: 4, notes: 5 }[tabId];
|
|
645
|
+
const tabIndex = { uc: 0, br: 1, ent: 2, perm: 3, mock: 4, notes: 5, struct: 6 }[tabId];
|
|
559
646
|
if (buttons[tabIndex]) buttons[tabIndex].classList.add('active');
|
|
560
647
|
}
|
|
@@ -76,7 +76,9 @@ function renderCoverageMatrix() {
|
|
|
76
76
|
<thead><tr><th>Besoin</th><th>Priorite</th><th>Domaine</th><th>Couvert</th></tr></thead>
|
|
77
77
|
<tbody>
|
|
78
78
|
${allScope.map(item => {
|
|
79
|
-
const moduleName =
|
|
79
|
+
const moduleName = item.module
|
|
80
|
+
? (data.modules.find(m => m.code === item.module)?.name || item.module)
|
|
81
|
+
: (data.modules.length > 0 ? data.modules.map(m => m.name).join(', ') : 'A definir');
|
|
80
82
|
return `
|
|
81
83
|
<tr>
|
|
82
84
|
<td>${item.name}</td>
|
|
@@ -11,35 +11,125 @@ function saveToLocalStorage() {
|
|
|
11
11
|
showNotification('Modifications sauvegardees');
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function resetToEmbedded() {
|
|
15
|
+
if (!confirm('Reinitialiser toutes les donnees depuis la version d\'origine ? Vos modifications locales (commentaires, notes) seront perdues.')) return;
|
|
16
|
+
localStorage.removeItem(APP_KEY);
|
|
17
|
+
// Restore data from ORIGINAL_DATA
|
|
18
|
+
Object.keys(data).forEach(k => delete data[k]);
|
|
19
|
+
Object.assign(data, JSON.parse(JSON.stringify(ORIGINAL_DATA)));
|
|
20
|
+
// Re-render everything
|
|
21
|
+
initEditableFields();
|
|
22
|
+
renderStakeholders();
|
|
23
|
+
renderScope();
|
|
24
|
+
renderRisks();
|
|
25
|
+
renderCriteria();
|
|
26
|
+
renderModules();
|
|
27
|
+
renderDependencies();
|
|
28
|
+
renderAllModuleSpecs();
|
|
29
|
+
renderConsolidation();
|
|
30
|
+
renderHandoff();
|
|
31
|
+
renderE2EFlows();
|
|
32
|
+
updateCounts();
|
|
33
|
+
renderReviewPanel();
|
|
34
|
+
showNotification('Donnees reinitialisees depuis la version d\'origine');
|
|
35
|
+
}
|
|
36
|
+
|
|
14
37
|
function loadFromLocalStorage() {
|
|
15
38
|
const saved = localStorage.getItem(APP_KEY);
|
|
16
|
-
if (saved)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
if (!saved) return;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(saved);
|
|
43
|
+
|
|
44
|
+
// Build fingerprint of embedded structural data to detect HTML regeneration
|
|
45
|
+
const embeddedFingerprint = ORIGINAL_DATA.modules.map(m => m.code).sort().join(',')
|
|
46
|
+
+ '|' + (ORIGINAL_DATA.metadata?.createdAt || '')
|
|
47
|
+
+ '|' + ORIGINAL_DATA.modules.length;
|
|
48
|
+
const cachedFingerprint = parsed._structureFingerprint || '';
|
|
49
|
+
|
|
50
|
+
const structureChanged = embeddedFingerprint !== cachedFingerprint;
|
|
51
|
+
const embeddedModuleCount = ORIGINAL_DATA.modules?.length || 0;
|
|
52
|
+
const cachedModuleCount = (parsed.modules || []).length;
|
|
53
|
+
const hasNewModules = embeddedModuleCount > cachedModuleCount;
|
|
54
|
+
const embeddedModuleCodes = new Set(ORIGINAL_DATA.modules.map(m => m.code));
|
|
55
|
+
const cachedModuleCodes = new Set((parsed.modules || []).map(m => m.code));
|
|
56
|
+
const missingModules = [...embeddedModuleCodes].filter(c => !cachedModuleCodes.has(c));
|
|
57
|
+
|
|
58
|
+
if (structureChanged || hasNewModules || missingModules.length > 0) {
|
|
59
|
+
// HTML was regenerated or has new modules — keep embedded structural data
|
|
60
|
+
// Only restore user-specific edits (comments, custom roles, notes)
|
|
61
|
+
data.wireframeComments = parsed.wireframeComments || {};
|
|
62
|
+
data.specComments = parsed.specComments || {};
|
|
63
|
+
data.customRoles = parsed.customRoles || [];
|
|
64
|
+
data.customActions = parsed.customActions || [];
|
|
65
|
+
data.comments = parsed.comments || [];
|
|
66
|
+
|
|
67
|
+
// Merge user-added notes per module (preserve existing module notes)
|
|
68
|
+
for (const code of Object.keys(parsed.moduleSpecs || {})) {
|
|
69
|
+
if (data.moduleSpecs[code] && parsed.moduleSpecs[code]?.notes) {
|
|
70
|
+
data.moduleSpecs[code].notes = parsed.moduleSpecs[code].notes;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Save fresh embedded data with fingerprint
|
|
75
|
+
data._structureFingerprint = embeddedFingerprint;
|
|
76
|
+
autoSave();
|
|
77
|
+
} else {
|
|
78
|
+
// Cache matches current structure — safe to restore user edits on cadrage/notes
|
|
79
|
+
// IMPORTANT: Always keep embedded modules and moduleSpecs as structural source of truth
|
|
80
|
+
// Only merge cadrage user-editable fields and notes
|
|
81
|
+
if (parsed.cadrage) {
|
|
82
|
+
// Merge cadrage context (user may have edited text fields)
|
|
83
|
+
if (parsed.cadrage.context) {
|
|
84
|
+
data.cadrage.context = { ...data.cadrage.context, ...parsed.cadrage.context };
|
|
85
|
+
}
|
|
86
|
+
// Merge scope only if user added items interactively
|
|
87
|
+
if (parsed.cadrage.scope) {
|
|
88
|
+
data.cadrage.scope = { ...data.cadrage.scope, ...parsed.cadrage.scope };
|
|
89
|
+
}
|
|
90
|
+
// Merge stakeholders and risks if user edited them
|
|
91
|
+
if (parsed.cadrage.stakeholders) data.cadrage.stakeholders = parsed.cadrage.stakeholders;
|
|
92
|
+
if (parsed.cadrage.risks) data.cadrage.risks = parsed.cadrage.risks;
|
|
93
|
+
}
|
|
94
|
+
data.dependencies = parsed.dependencies || data.dependencies;
|
|
29
95
|
data.consolidation = { ...data.consolidation, ...(parsed.consolidation || {}) };
|
|
30
|
-
|
|
96
|
+
|
|
97
|
+
// Merge moduleSpecs: keep embedded structure, overlay user-editable fields (notes, custom permissions)
|
|
98
|
+
for (const code of Object.keys(data.moduleSpecs)) {
|
|
99
|
+
const cached = parsed.moduleSpecs?.[code];
|
|
100
|
+
if (cached) {
|
|
101
|
+
// Preserve user notes
|
|
102
|
+
if (cached.notes) data.moduleSpecs[code].notes = cached.notes;
|
|
103
|
+
// Preserve user-added use cases/rules/entities (merged with embedded)
|
|
104
|
+
// Only add items that don't exist in embedded (by name match)
|
|
105
|
+
const embeddedUcNames = new Set((data.moduleSpecs[code].useCases || []).map(uc => uc.name));
|
|
106
|
+
(cached.useCases || []).forEach(uc => {
|
|
107
|
+
if (!embeddedUcNames.has(uc.name)) data.moduleSpecs[code].useCases.push(uc);
|
|
108
|
+
});
|
|
109
|
+
const embeddedBrNames = new Set((data.moduleSpecs[code].businessRules || []).map(br => br.name));
|
|
110
|
+
(cached.businessRules || []).forEach(br => {
|
|
111
|
+
if (!embeddedBrNames.has(br.name)) data.moduleSpecs[code].businessRules.push(br);
|
|
112
|
+
});
|
|
113
|
+
// Preserve user permissions edits
|
|
114
|
+
if (cached.permissions) data.moduleSpecs[code].permissions = cached.permissions;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
31
118
|
data.wireframeComments = parsed.wireframeComments || {};
|
|
32
119
|
data.specComments = parsed.specComments || {};
|
|
33
120
|
data.customRoles = parsed.customRoles || [];
|
|
34
121
|
data.customActions = parsed.customActions || [];
|
|
35
122
|
data.comments = parsed.comments || [];
|
|
36
123
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
124
|
+
// Update fingerprint
|
|
125
|
+
data._structureFingerprint = embeddedFingerprint;
|
|
126
|
+
autoSave();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Restore editable fields
|
|
130
|
+
document.querySelectorAll('.editable[data-field]').forEach(el => {
|
|
131
|
+
const value = getNestedValue(data, 'cadrage.' + el.dataset.field);
|
|
132
|
+
if (value) el.textContent = value;
|
|
133
|
+
});
|
|
134
|
+
} catch (e) { console.error('Error loading saved data:', e); }
|
|
45
135
|
}
|
|
@@ -118,6 +118,13 @@ function getSectionLabel(sectionId) {
|
|
|
118
118
|
const mod = data.modules.find(m => m.code === code);
|
|
119
119
|
return mod ? mod.name : code;
|
|
120
120
|
}
|
|
121
|
+
// Handle module structure sections (module-struct-{code}-{section})
|
|
122
|
+
const structMatch = sectionId.match(/^module-struct-(.+?)-(.+)$/);
|
|
123
|
+
if (structMatch) {
|
|
124
|
+
const mod = data.modules.find(m => m.code === structMatch[1]);
|
|
125
|
+
const modName = mod ? mod.name : structMatch[1];
|
|
126
|
+
return modName + ' > Structure > ' + structMatch[2];
|
|
127
|
+
}
|
|
121
128
|
// Handle list-based sectionIds (ucList-*, brList-*, entList-*)
|
|
122
129
|
const listMatch = sectionId.match(/^(uc|br|ent)List-(.+)$/);
|
|
123
130
|
if (listMatch) {
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
PRINT
|
|
91
91
|
============================================ */
|
|
92
92
|
@media print {
|
|
93
|
-
.sidebar, .header-actions, .add-btn, .uc-actions, .inline-form, .module-card-remove
|
|
93
|
+
.sidebar, .header-actions, .add-btn, .uc-actions, .inline-form, .module-card-remove { display: none !important; }
|
|
94
94
|
.main { max-width: 100%; padding: 0; }
|
|
95
95
|
.section { display: block !important; page-break-inside: avoid; }
|
|
96
96
|
body { background: #fff; color: #1a1a1a; }
|