[FIX] E2E testing - fix router, EmailService, frontend data mapping
Critical fixes discovered during end-to-end testing:
Router (index.php):
- Rewrote route resolution engine to properly handle /{id}/subAction patterns
- All routes like GET /assessments/{id}/questions, POST /incidents/{id}/early-warning,
GET /organizations/{id}/members now resolve correctly
- Routes with kebab-case sub-actions (early-warning, ai-analyze) now convert to camelCase
- Controller methods receive correct arguments via spread operator
EmailService.php:
- Fix PHP parse error: ?? operator cannot be used inside string interpolation {}
- Extract incident_code to variable before interpolation (3 occurrences)
assessment.html:
- Fix data structure handling: API returns categories with nested questions array
- Fix field names: question_code (not question_id), response_value (not compliance_level)
- Fix answer enum values: not_implemented/partial/implemented (not Italian)
- Fix question text field: question_text (not text/question/title)
- Show NIS2 article and ISO 27001 control references
- Fix response restoration from existing answers
dashboard.html:
- Fix data mapping from overview API response structure
- risks.total instead of open_risks, policies array instead of approved_policies
- Calculate training completion percentage from training object
- Load deadlines/activity from dedicated endpoints (not included in overview)
onboarding.html:
- Fix field name mismatches: annual_turnover_eur, contact_email, contact_phone,
full_name, phone (matching OnboardingController expected params)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+28
-18
@@ -193,7 +193,16 @@
|
||||
try {
|
||||
const result = await api.getAssessmentQuestions(currentAssessmentId);
|
||||
if (result.success && result.data) {
|
||||
questions = result.data.questions || result.data;
|
||||
// API returns array of {category_id, category_title, questions: [...]}
|
||||
const data = result.data;
|
||||
if (Array.isArray(data) && data.length > 0 && data[0].questions) {
|
||||
questions = [];
|
||||
data.forEach(cat => {
|
||||
(cat.questions || []).forEach(q => questions.push(q));
|
||||
});
|
||||
} else {
|
||||
questions = data;
|
||||
}
|
||||
organizeByCategory();
|
||||
showWizard();
|
||||
renderCurrentQuestion();
|
||||
@@ -217,14 +226,14 @@
|
||||
questions: catMap[name]
|
||||
}));
|
||||
|
||||
// Ripristina risposte precedenti
|
||||
// Ripristina risposte precedenti (backend puts response_value directly on question)
|
||||
questions.forEach(q => {
|
||||
if (q.response) {
|
||||
if (q.response_value) {
|
||||
responses[q.id] = {
|
||||
answer: q.response.answer || q.response.compliance_level,
|
||||
maturity: q.response.maturity_level,
|
||||
notes: q.response.notes || '',
|
||||
evidence: q.response.evidence_description || ''
|
||||
answer: q.response_value,
|
||||
maturity: q.maturity_level ? parseInt(q.maturity_level) : 0,
|
||||
notes: q.notes || '',
|
||||
evidence: q.evidence_description || ''
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -277,19 +286,20 @@
|
||||
const r = responses[q.id] || {};
|
||||
|
||||
const answers = [
|
||||
{ value: 'non_implementato', label: 'Non Implementato', cls: 'danger' },
|
||||
{ value: 'parziale', label: 'Parziale', cls: 'warning' },
|
||||
{ value: 'implementato', label: 'Implementato', cls: 'success' },
|
||||
{ value: 'non_applicabile', label: 'Non Applicabile', cls: 'neutral' },
|
||||
{ value: 'not_implemented', label: 'Non Implementato', cls: 'danger' },
|
||||
{ value: 'partial', label: 'Parziale', cls: 'warning' },
|
||||
{ value: 'implemented', label: 'Implementato', cls: 'success' },
|
||||
{ value: 'not_applicable', label: 'Non Applicabile', cls: 'neutral' },
|
||||
];
|
||||
|
||||
let html = `
|
||||
<div style="margin-bottom:20px;">
|
||||
<p style="font-size:1rem; font-weight:600; color:var(--gray-900); margin-bottom:4px;">
|
||||
${escapeHtml(q.text || q.question || q.title || '')}
|
||||
${escapeHtml(q.question_text || q.text || q.title || '')}
|
||||
</p>
|
||||
${q.description ? `<p class="text-muted" style="font-size:0.8125rem;">${escapeHtml(q.description)}</p>` : ''}
|
||||
${q.reference ? `<span class="tag mt-8">Rif: ${escapeHtml(q.reference)}</span>` : ''}
|
||||
${q.guidance_it ? `<p class="text-muted" style="font-size:0.8125rem;">${escapeHtml(q.guidance_it)}</p>` : ''}
|
||||
${q.nis2_article ? `<span class="tag mt-8">Art. ${escapeHtml(q.nis2_article)}</span>` : ''}
|
||||
${q.iso27001_control ? `<span class="tag mt-8" style="margin-left:4px;">ISO ${escapeHtml(q.iso27001_control)}</span>` : ''}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -392,8 +402,8 @@
|
||||
|
||||
try {
|
||||
const result = await api.saveAssessmentResponse(currentAssessmentId, {
|
||||
question_id: q.id,
|
||||
compliance_level: r.answer,
|
||||
question_code: q.question_code,
|
||||
response_value: r.answer,
|
||||
maturity_level: r.maturity,
|
||||
notes: r.notes,
|
||||
evidence_description: r.evidence
|
||||
@@ -420,8 +430,8 @@
|
||||
if (r && r.answer) {
|
||||
// Salva in background
|
||||
api.saveAssessmentResponse(currentAssessmentId, {
|
||||
question_id: q.id,
|
||||
compliance_level: r.answer,
|
||||
question_code: q.question_code,
|
||||
response_value: r.answer,
|
||||
maturity_level: r.maturity,
|
||||
notes: r.notes,
|
||||
evidence_description: r.evidence
|
||||
|
||||
+18
-16
@@ -150,28 +150,30 @@
|
||||
const data = result.data;
|
||||
|
||||
// Compliance gauge
|
||||
const score = data.compliance_score != null ? data.compliance_score : 0;
|
||||
const score = data.compliance_score != null ? Math.round(data.compliance_score) : 0;
|
||||
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(score, 180);
|
||||
|
||||
// Stats
|
||||
document.getElementById('stat-risks').textContent = data.open_risks != null ? data.open_risks : 0;
|
||||
document.getElementById('stat-incidents').textContent = data.active_incidents != null ? data.active_incidents : 0;
|
||||
document.getElementById('stat-policies').textContent = data.approved_policies != null ? data.approved_policies : 0;
|
||||
document.getElementById('stat-training').textContent = (data.training_completion != null ? data.training_completion : 0) + '%';
|
||||
// Stats - map backend response structure to UI
|
||||
const openRisks = data.risks ? (parseInt(data.risks.total) || 0) : 0;
|
||||
const activeIncidents = data.active_incidents || 0;
|
||||
const approvedPolicies = Array.isArray(data.policies)
|
||||
? data.policies.reduce((sum, p) => p.status === 'approved' || p.status === 'published' ? sum + parseInt(p.count) : sum, 0)
|
||||
: 0;
|
||||
const trainingTotal = data.training ? (parseInt(data.training.total) || 0) : 0;
|
||||
const trainingCompleted = data.training ? (parseInt(data.training.completed) || 0) : 0;
|
||||
const trainingPct = trainingTotal > 0 ? Math.round((trainingCompleted / trainingTotal) * 100) : 0;
|
||||
|
||||
// Scadenze
|
||||
renderDeadlines(data.upcoming_deadlines || []);
|
||||
|
||||
// Attivita'
|
||||
renderActivity(data.recent_activity || []);
|
||||
} else {
|
||||
// Fallback: prova endpoint singoli
|
||||
loadIndividualData();
|
||||
document.getElementById('stat-risks').textContent = openRisks;
|
||||
document.getElementById('stat-incidents').textContent = activeIncidents;
|
||||
document.getElementById('stat-policies').textContent = approvedPolicies;
|
||||
document.getElementById('stat-training').textContent = trainingPct + '%';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Dashboard load error:', err);
|
||||
loadIndividualData();
|
||||
}
|
||||
|
||||
// Always load deadlines and activity from dedicated endpoints
|
||||
loadIndividualData();
|
||||
}
|
||||
|
||||
async function loadIndividualData() {
|
||||
@@ -179,7 +181,7 @@
|
||||
try {
|
||||
const scoreRes = await api.getComplianceScore();
|
||||
if (scoreRes.success && scoreRes.data) {
|
||||
const score = scoreRes.data.score || scoreRes.data.compliance_score || 0;
|
||||
const score = scoreRes.data.avg_implementation || scoreRes.data.score || 0;
|
||||
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(score, 180);
|
||||
} else {
|
||||
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(0, 180);
|
||||
|
||||
+64
-53
@@ -283,60 +283,79 @@ $actionMap = [
|
||||
|
||||
$actions = $actionMap[$controllerName] ?? [];
|
||||
$resolvedAction = null;
|
||||
$callArgs = [];
|
||||
|
||||
// Costruisci combinazioni di pattern da verificare (ordine di specificità)
|
||||
$patterns = [];
|
||||
// Helper: convert kebab-case to camelCase (same logic as $actionName conversion)
|
||||
$toCamel = function (string $s): string {
|
||||
return str_replace('-', '', lcfirst(ucwords($s, '-')));
|
||||
};
|
||||
|
||||
if ($subResourceId !== null && $subAction !== null) {
|
||||
// METHOD:action/{id}/subAction/{subId}
|
||||
$patterns[] = "{$method}:{$actionName}/{$subAction}/{subId}";
|
||||
// METHOD:{id}/subAction/{subId}
|
||||
$patterns[] = "{$method}:{id}/{$subAction}/{subId}";
|
||||
}
|
||||
// Costruisci candidati pattern → argomenti (ordine: più specifico prima)
|
||||
$candidates = [];
|
||||
|
||||
if ($subAction !== null && $resourceId !== null) {
|
||||
// METHOD:{id}/subAction
|
||||
$patterns[] = "{$method}:{id}/{$subAction}";
|
||||
// METHOD:action/{subId}
|
||||
$patterns[] = "{$method}:{$actionName}/{subId}";
|
||||
}
|
||||
if (is_numeric($actionName)) {
|
||||
// Il primo segmento è un ID numerico:
|
||||
// /controller/123 → METHOD:{id}
|
||||
// /controller/123/sub → METHOD:{id}/sub
|
||||
// /controller/123/sub/456 → METHOD:{id}/sub/{subId}
|
||||
$numericId = (int) $actionName;
|
||||
$sub = $resourceId !== null ? $toCamel($resourceId) : null;
|
||||
$subId = $subAction !== null && is_numeric($subAction) ? (int) $subAction : null;
|
||||
|
||||
if ($resourceId !== null && $subAction === null) {
|
||||
// METHOD:action/{id} (actionName è in realtà l'ID numerico)
|
||||
if (is_numeric($actionName)) {
|
||||
$patterns[] = "{$method}:{id}";
|
||||
$resourceId = (int) $actionName;
|
||||
} else {
|
||||
// METHOD:{id}
|
||||
$patterns[] = "{$method}:{id}";
|
||||
if ($sub !== null && $subId !== null) {
|
||||
$candidates[] = ['p' => "{$method}:{id}/{$sub}/{subId}", 'a' => [$numericId, $subId]];
|
||||
}
|
||||
if ($sub !== null) {
|
||||
$candidates[] = ['p' => "{$method}:{id}/{$sub}", 'a' => [$numericId]];
|
||||
}
|
||||
$candidates[] = ['p' => "{$method}:{id}", 'a' => [$numericId]];
|
||||
|
||||
} else {
|
||||
// Il primo segmento è un'azione nominale:
|
||||
// /controller/action → METHOD:action
|
||||
// /controller/action/123 → METHOD:action/{subId} oppure METHOD:{id}
|
||||
// /controller/action/sub → METHOD:action/sub
|
||||
// /controller/action/123/sub → METHOD:{id}/sub
|
||||
// /controller/action/123/sub/456 → METHOD:{id}/sub/{subId}
|
||||
|
||||
if ($subAction !== null && $resourceId !== null && is_numeric($resourceId)) {
|
||||
$rid = (int) $resourceId;
|
||||
$camelSub = $toCamel($subAction);
|
||||
|
||||
if ($subResourceId !== null && is_numeric($subResourceId)) {
|
||||
$sid = (int) $subResourceId;
|
||||
$candidates[] = ['p' => "{$method}:{id}/{$camelSub}/{subId}", 'a' => [$rid, $sid]];
|
||||
}
|
||||
$candidates[] = ['p' => "{$method}:{id}/{$camelSub}", 'a' => [$rid]];
|
||||
$candidates[] = ['p' => "{$method}:{$actionName}/{subId}", 'a' => [$rid]];
|
||||
}
|
||||
|
||||
if ($resourceId !== null && $subAction === null) {
|
||||
if (is_numeric($resourceId)) {
|
||||
// /controller/action/123 → potrebbe essere action/{subId} o {id}
|
||||
$rid = (int) $resourceId;
|
||||
$candidates[] = ['p' => "{$method}:{$actionName}/{subId}", 'a' => [$rid]];
|
||||
$candidates[] = ['p' => "{$method}:{id}", 'a' => [$rid]];
|
||||
} else {
|
||||
// /controller/action/subAction → nome composto (es: evidence/upload)
|
||||
$camelResource = $toCamel($resourceId);
|
||||
$candidates[] = ['p' => "{$method}:{$actionName}/{$camelResource}", 'a' => []];
|
||||
}
|
||||
}
|
||||
|
||||
// /controller/action
|
||||
$candidates[] = ['p' => "{$method}:{$actionName}", 'a' => []];
|
||||
}
|
||||
|
||||
// METHOD:action/subAction
|
||||
if ($resourceId !== null && !is_numeric($actionName)) {
|
||||
$patterns[] = "{$method}:{$actionName}/{$resourceId}";
|
||||
}
|
||||
|
||||
// METHOD:action
|
||||
$patterns[] = "{$method}:{$actionName}";
|
||||
|
||||
// Cerca match
|
||||
foreach ($patterns as $pattern) {
|
||||
if (isset($actions[$pattern])) {
|
||||
$resolvedAction = $actions[$pattern];
|
||||
// Cerca primo match
|
||||
foreach ($candidates as $candidate) {
|
||||
if (isset($actions[$candidate['p']])) {
|
||||
$resolvedAction = $actions[$candidate['p']];
|
||||
$callArgs = $candidate['a'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Se il primo segmento è numerico, l'azione è basata sull'ID
|
||||
if (!$resolvedAction && is_numeric($actionName)) {
|
||||
$resourceId = (int) $actionName;
|
||||
$pattern = "{$method}:{id}";
|
||||
if (isset($actions[$pattern])) {
|
||||
$resolvedAction = $actions[$pattern];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$resolvedAction) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: application/json');
|
||||
@@ -365,16 +384,8 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Chiama con gli argomenti appropriati
|
||||
if ($subResourceId !== null) {
|
||||
$controller->$resolvedAction((int) $resourceId, (int) $subResourceId);
|
||||
} elseif ($resourceId !== null && !is_numeric($actionName)) {
|
||||
$controller->$resolvedAction((int) $resourceId);
|
||||
} elseif (is_numeric($actionName)) {
|
||||
$controller->$resolvedAction((int) $resourceId);
|
||||
} else {
|
||||
$controller->$resolvedAction();
|
||||
}
|
||||
// Chiama con gli argomenti risolti
|
||||
$controller->$resolvedAction(...$callArgs);
|
||||
} catch (RuntimeException $e) {
|
||||
// Rate limit exceeded (429)
|
||||
if ($e->getCode() === 429) {
|
||||
|
||||
+8
-12
@@ -1600,27 +1600,23 @@
|
||||
const cl = wizardState.classification;
|
||||
|
||||
const payload = {
|
||||
// Company data
|
||||
// Company data (field names match backend OnboardingController)
|
||||
name: c.name,
|
||||
vat_number: c.vat_number,
|
||||
fiscal_code: c.fiscal_code,
|
||||
address: c.address,
|
||||
city: c.city,
|
||||
website: c.website,
|
||||
company_email: c.email,
|
||||
company_phone: c.phone,
|
||||
contact_email: c.email,
|
||||
contact_phone: c.phone,
|
||||
sector: c.sector,
|
||||
employee_count: parseInt(c.employee_count) || 0,
|
||||
annual_turnover: parseInt(c.annual_turnover) || 0,
|
||||
annual_turnover_eur: parseInt(c.annual_turnover) || 0,
|
||||
// Profile data
|
||||
user_full_name: p.full_name,
|
||||
user_role: p.role,
|
||||
user_phone: p.phone,
|
||||
// Classification
|
||||
classification: cl ? cl.classification : null,
|
||||
classification_label: cl ? cl.label : null,
|
||||
// Method used
|
||||
data_source: wizardState.method
|
||||
full_name: p.full_name,
|
||||
phone: p.phone,
|
||||
// Country
|
||||
country: 'IT',
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user