701 lines
29 KiB
PHP
Executable File
701 lines
29 KiB
PHP
Executable File
@extends('admin.layouts.netgescon')
|
|
|
|
@section('page-title', 'Controllo Qualità Codice NetGescon')
|
|
|
|
@section('content')
|
|
<div class="space-y-8 netgescon-fade-in">
|
|
<!-- Header -->
|
|
<div class="netgescon-card">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h2 class="netgescon-title">
|
|
<i class="fas fa-code text-green-500 mr-3"></i>
|
|
Controllo Qualità Codice NetGescon
|
|
</h2>
|
|
<p class="netgescon-text mt-2">
|
|
Sistema automatico di controllo standard di codifica e componenti NetGescon
|
|
</p>
|
|
</div>
|
|
<div class="hidden md:block">
|
|
<button onclick="startScan()" class="netgescon-btn netgescon-btn-primary">
|
|
<i class="fas fa-search mr-2"></i>
|
|
Avvia Scansione
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filtri di Scansione -->
|
|
<div class="netgescon-card">
|
|
<h3 class="netgescon-subtitle mb-4">
|
|
<i class="fas fa-filter mr-2"></i>
|
|
Configurazione Scansione
|
|
</h3>
|
|
|
|
<form id="scanForm" class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div>
|
|
<label class="netgescon-label">Percorsi da Scansionare</label>
|
|
<input type="text" name="path" value="{{ $scanPath ?? 'app,resources/views' }}"
|
|
class="netgescon-input" placeholder="app,resources/views,routes">
|
|
<p class="netgescon-text-small mt-1">Separa più percorsi con virgole</p>
|
|
<div class="flex flex-wrap gap-2 mt-2">
|
|
<button type="button" class="netgescon-badge netgescon-badge-secondary" onclick="appendPath('app')">app</button>
|
|
<button type="button" class="netgescon-badge netgescon-badge-secondary" onclick="appendPath('resources/views')">resources/views</button>
|
|
<button type="button" class="netgescon-badge netgescon-badge-secondary" onclick="appendPath('routes')">routes</button>
|
|
<button type="button" class="netgescon-badge netgescon-badge-secondary" onclick="appendPath('database/migrations')">database/migrations</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="netgescon-label">Filtro Severità</label>
|
|
<select name="severity" class="netgescon-input">
|
|
<option value="all" {{ ($severityFilter ?? 'all') === 'all' ? 'selected' : '' }}>Tutti</option>
|
|
<option value="critical" {{ ($severityFilter ?? '') === 'critical' ? 'selected' : '' }}>🔴 Critical</option>
|
|
<option value="error" {{ ($severityFilter ?? '') === 'error' ? 'selected' : '' }}>🟠 Error</option>
|
|
<option value="warning" {{ ($severityFilter ?? '') === 'warning' ? 'selected' : '' }}>🟡 Warning</option>
|
|
<option value="info" {{ ($severityFilter ?? '') === 'info' ? 'selected' : '' }}>🔵 Info</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="netgescon-label">Modulo</label>
|
|
<select name="module" class="netgescon-input">
|
|
<option value="all" {{ ($moduleFilter ?? 'all') === 'all' ? 'selected' : '' }}>Tutti</option>
|
|
<option value="gescon-import" {{ ($moduleFilter ?? '') === 'gescon-import' ? 'selected' : '' }}>Gescon Import</option>
|
|
<option value="stabili" {{ ($moduleFilter ?? '') === 'stabili' ? 'selected' : '' }}>Stabili</option>
|
|
<option value="contabilita-gescon" {{ ($moduleFilter ?? '') === 'contabilita-gescon' ? 'selected' : '' }}>Contabilità Gescon</option>
|
|
<option value="supporto" {{ ($moduleFilter ?? '') === 'supporto' ? 'selected' : '' }}>Supporto</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="netgescon-label">Menù</label>
|
|
<select name="menu" class="netgescon-input">
|
|
<option value="all" {{ ($menuFilter ?? 'all') === 'all' ? 'selected' : '' }}>Tutti</option>
|
|
<option value="stabili" {{ ($menuFilter ?? '') === 'stabili' ? 'selected' : '' }}>Stabili</option>
|
|
<option value="condomini" {{ ($menuFilter ?? '') === 'condomini' ? 'selected' : '' }}>Condomini</option>
|
|
<option value="amministrazione" {{ ($menuFilter ?? '') === 'amministrazione' ? 'selected' : '' }}>Amministrazione</option>
|
|
<option value="contabilita" {{ ($menuFilter ?? '') === 'contabilita' ? 'selected' : '' }}>Contabilità</option>
|
|
<option value="gestione-economica" {{ ($menuFilter ?? '') === 'gestione-economica' ? 'selected' : '' }}>Gestione Economica</option>
|
|
<option value="moduli" {{ ($menuFilter ?? '') === 'moduli' ? 'selected' : '' }}>Moduli</option>
|
|
<option value="gescon-import" {{ ($menuFilter ?? '') === 'gescon-import' ? 'selected' : '' }}>Gescon Import</option>
|
|
<option value="supporto" {{ ($menuFilter ?? '') === 'supporto' ? 'selected' : '' }}>Supporto</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="flex items-end">
|
|
<button type="button" onclick="startScan()" class="netgescon-btn netgescon-btn-secondary w-full">
|
|
<i class="fas fa-play mr-2"></i>
|
|
Avvia Scansione
|
|
</button>
|
|
</div>
|
|
|
|
<div class="md:col-span-3">
|
|
<div class="rounded-md border border-blue-200 bg-blue-50 px-3 py-2">
|
|
<input type="hidden" name="include_data_checks" value="0">
|
|
<label class="inline-flex items-center gap-2 text-sm text-blue-900">
|
|
<input type="checkbox" name="include_data_checks" value="1" {{ ($includeDataChecks ?? true) ? 'checked' : '' }}>
|
|
<span>Controlli integrità archivi (millesimali, legacy individuali, correlazioni DB)</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
<div class="mt-4 flex flex-wrap gap-2">
|
|
<button type="button" class="netgescon-btn netgescon-btn-primary" onclick="quickScanGesconImport()">
|
|
<i class="fas fa-bolt mr-2"></i> Scansione Rapida: Gescon Import
|
|
</button>
|
|
<button type="button" class="netgescon-btn" onclick="exportResults('csv')">
|
|
<i class="fas fa-file-csv mr-2"></i> Esporta CSV
|
|
</button>
|
|
<button type="button" class="netgescon-btn" onclick="exportResults('json')">
|
|
<i class="fas fa-file-code mr-2"></i> Esporta JSON
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Strumenti Rapidi (Lint & Editor) -->
|
|
<div class="netgescon-card">
|
|
<h3 class="netgescon-subtitle mb-4 flex items-center gap-2">
|
|
<i class="fas fa-tools text-gray-500"></i>
|
|
Strumenti Rapidi Qualità
|
|
</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div class="md:col-span-3">
|
|
<label class="netgescon-label">File relativo (app/, resources/views/, routes/)</label>
|
|
<input type="text" id="quickToolsFile" class="netgescon-input" placeholder="es. resources/views/admin/gescon-import/tabs/mapping.blade.php">
|
|
<p class="netgescon-text-small mt-1">Percorso relativo dalla root del progetto. Usa la scansione per copiare rapidamente il path.</p>
|
|
</div>
|
|
<div class="flex items-end gap-2">
|
|
<button type="button" class="netgescon-btn netgescon-btn-secondary flex-1" onclick="lintFileFromInput()">
|
|
<i class="fas fa-stethoscope mr-2"></i>Controlla Sintassi
|
|
</button>
|
|
<button type="button" class="netgescon-btn netgescon-btn-outline-primary flex-1" onclick="openQuickEditFromInput()">
|
|
<i class="fas fa-edit mr-2"></i>Editor Rapido
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<pre id="quickToolsOutput" class="mt-4 bg-gray-900 text-green-200 text-xs p-3 rounded hidden overflow-x-auto"></pre>
|
|
</div>
|
|
|
|
<!-- Loading -->
|
|
<div id="scanLoading" class="netgescon-card hidden">
|
|
<div class="text-center py-8">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
|
<p class="netgescon-text">Scansione in corso...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Risultati Summary -->
|
|
<div id="scanSummary" class="hidden">
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
|
<div class="netgescon-card">
|
|
<div class="flex items-center">
|
|
<div class="p-3 rounded-full bg-blue-100">
|
|
<i class="fas fa-file-code text-blue-600 text-xl"></i>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="netgescon-text-small">File Analizzati</p>
|
|
<p class="netgescon-number" id="totalFiles">{{ $totalScanned ?? 0 }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="netgescon-card">
|
|
<div class="flex items-center">
|
|
<div class="p-3 rounded-full bg-red-100">
|
|
<i class="fas fa-exclamation-triangle text-red-600 text-xl"></i>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="netgescon-text-small">Problemi Totali</p>
|
|
<p class="netgescon-number" id="totalIssues">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="netgescon-card">
|
|
<div class="flex items-center">
|
|
<div class="p-3 rounded-full bg-red-100">
|
|
<i class="fas fa-ban text-red-600 text-xl"></i>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="netgescon-text-small">Critical</p>
|
|
<p class="netgescon-number text-red-600" id="criticalCount">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="netgescon-card">
|
|
<div class="flex items-center">
|
|
<div class="p-3 rounded-full bg-orange-100">
|
|
<i class="fas fa-times-circle text-orange-600 text-xl"></i>
|
|
</div>
|
|
<div class="ml-4">
|
|
<p class="netgescon-text-small">Errori</p>
|
|
<p class="netgescon-number text-orange-600" id="errorCount">0</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="ruleSummary" class="netgescon-card"></div>
|
|
<div id="moduleMenuSummary" class="netgescon-card mt-4"></div>
|
|
</div>
|
|
|
|
<!-- Risultati Dettagliati -->
|
|
<div id="scanResults" class="space-y-6 hidden">
|
|
<!-- I risultati saranno popolati dinamicamente -->
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Modal semplice per mostrare il contenuto proposto dall'Autofix
|
|
let autofixModal;
|
|
function ensureAutofixModal() {
|
|
if (autofixModal) return autofixModal;
|
|
const modal = document.createElement('div');
|
|
modal.id = 'autofixModal';
|
|
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden';
|
|
modal.innerHTML = `
|
|
<div class="bg-white rounded-lg shadow-xl w-11/12 md:w-3/4 lg:w-1/2 max-h-[80vh] overflow-hidden">
|
|
<div class="px-6 py-4 border-b flex items-center justify-between">
|
|
<h3 class="font-semibold text-gray-800">Anteprima Fix Automatico</h3>
|
|
<button class="text-gray-500 hover:text-gray-700" onclick="closeAutofixModal()">✕</button>
|
|
</div>
|
|
<div class="p-4 space-y-3">
|
|
<div class="text-sm text-gray-600" id="autofixFile"></div>
|
|
<textarea id="autofixContent" class="w-full h-64 border rounded p-2 font-mono text-sm"></textarea>
|
|
<div id="autofixHints" class="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded p-2 hidden"></div>
|
|
</div>
|
|
<div class="px-6 py-3 bg-gray-50 border-t flex items-center justify-end gap-2">
|
|
<button class="netgescon-btn" onclick="copyAutofixContent()"><i class="fas fa-copy mr-1"></i>Copia</button>
|
|
<button class="netgescon-btn netgescon-btn-primary hidden" id="autofixSaveBtn" onclick="saveQuickEdit()"><i class="fas fa-save mr-1"></i>Salva</button>
|
|
<button class="netgescon-btn netgescon-btn-secondary" onclick="closeAutofixModal()">Chiudi</button>
|
|
</div>
|
|
</div>`;
|
|
document.body.appendChild(modal);
|
|
autofixModal = modal;
|
|
return modal;
|
|
}
|
|
function openAutofixModal(file, content, hints = [], mode = 'autofix') {
|
|
const modal = ensureAutofixModal();
|
|
modal.dataset.mode = mode;
|
|
modal.dataset.file = file || '';
|
|
const fileLabel = modal.querySelector('#autofixFile');
|
|
fileLabel.textContent = 'File: ' + (file || '');
|
|
fileLabel.dataset.file = file || '';
|
|
const textarea = modal.querySelector('#autofixContent');
|
|
textarea.value = content || '';
|
|
textarea.readOnly = mode !== 'edit';
|
|
const saveBtn = modal.querySelector('#autofixSaveBtn');
|
|
if (mode === 'edit') {
|
|
saveBtn.classList.remove('hidden');
|
|
} else {
|
|
saveBtn.classList.add('hidden');
|
|
}
|
|
const hintsBox = modal.querySelector('#autofixHints');
|
|
if (hints && hints.length) {
|
|
hintsBox.innerHTML = '<ul class="list-disc pl-5">' + hints.map(h => `<li>${h}</li>`).join('') + '</ul>';
|
|
hintsBox.classList.remove('hidden');
|
|
} else {
|
|
hintsBox.classList.add('hidden');
|
|
hintsBox.innerHTML = '';
|
|
}
|
|
modal.classList.remove('hidden');
|
|
}
|
|
function closeAutofixModal() { if (autofixModal) autofixModal.classList.add('hidden'); }
|
|
function copyAutofixContent() {
|
|
const ta = document.getElementById('autofixContent');
|
|
ta.select();
|
|
document.execCommand('copy');
|
|
}
|
|
function saveQuickEdit() {
|
|
const modal = ensureAutofixModal();
|
|
const file = modal.dataset.file || modal.querySelector('#autofixFile')?.dataset.file;
|
|
if (!file) {
|
|
alert('Nessun file selezionato.');
|
|
return;
|
|
}
|
|
const content = modal.querySelector('#autofixContent').value;
|
|
fetch('{{ route('admin.code-quality.file') }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json'
|
|
},
|
|
body: JSON.stringify({ file, action: 'save', content })
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (!data.success) {
|
|
alert(data.message || 'Errore salvataggio file');
|
|
return;
|
|
}
|
|
showQualityOutput('File salvato: ' + file, 'success');
|
|
closeAutofixModal();
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
alert('Errore salvataggio: ' + err);
|
|
});
|
|
}
|
|
|
|
function lintFile(file, options = {}) {
|
|
if (!file) {
|
|
alert('Specificare un file da controllare');
|
|
return;
|
|
}
|
|
fetch('{{ route('admin.code-quality.lint') }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json'
|
|
},
|
|
body: JSON.stringify({ file })
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
const output = (data.output || []).join('\n');
|
|
const baseMsg = data.success ? '✅ Sintassi ok' : '⚠️ Sintassi da correggere';
|
|
const message = baseMsg + (output ? '\n' + output : '');
|
|
if (options.target) {
|
|
showQualityOutput(message, data.success ? 'success' : 'error', options.target);
|
|
} else {
|
|
alert(message);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error(error);
|
|
if (options.target) {
|
|
showQualityOutput('Errore lint: ' + error, 'error', options.target);
|
|
} else {
|
|
alert('Errore durante il lint: ' + error);
|
|
}
|
|
});
|
|
}
|
|
|
|
function lintFileFromInput() {
|
|
const file = document.getElementById('quickToolsFile').value.trim();
|
|
if (!file) {
|
|
alert('Inserisci un percorso file relativo.');
|
|
return;
|
|
}
|
|
lintFile(file, { target: 'quickToolsOutput' });
|
|
}
|
|
|
|
function openQuickEdit(file) {
|
|
if (!file) {
|
|
alert('File non disponibile');
|
|
return;
|
|
}
|
|
fetch('{{ route('admin.code-quality.file') }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json'
|
|
},
|
|
body: JSON.stringify({ file, action: 'read' })
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (!data.success) {
|
|
alert(data.message || 'Impossibile aprire il file');
|
|
return;
|
|
}
|
|
const hints = [];
|
|
let mode = 'edit';
|
|
if (!data.writable) {
|
|
mode = 'preview';
|
|
hints.push('Il file non è scrivibile dall\'applicazione: aggiorna i permessi oppure modifica manualmente.');
|
|
}
|
|
openAutofixModal(data.file, data.content, hints, mode);
|
|
})
|
|
.catch(error => {
|
|
console.error(error);
|
|
alert('Errore apertura file: ' + error);
|
|
});
|
|
}
|
|
|
|
function openQuickEditFromInput() {
|
|
const file = document.getElementById('quickToolsFile').value.trim();
|
|
if (!file) {
|
|
alert('Inserisci un percorso file relativo.');
|
|
return;
|
|
}
|
|
openQuickEdit(file);
|
|
}
|
|
|
|
function showQualityOutput(message, variant = 'info', targetId = 'quickToolsOutput') {
|
|
const box = document.getElementById(targetId);
|
|
if (!box) {
|
|
alert(message);
|
|
return;
|
|
}
|
|
const colors = {
|
|
success: '#bbf7d0',
|
|
error: '#fecaca',
|
|
warning: '#fef3c7',
|
|
info: '#bfdbfe'
|
|
};
|
|
box.textContent = message;
|
|
box.classList.remove('hidden');
|
|
box.style.color = colors[variant] || colors.info;
|
|
}
|
|
function appendPath(p) {
|
|
const input = document.querySelector('#scanForm input[name="path"]');
|
|
const parts = input.value.split(',').map(s => s.trim()).filter(Boolean);
|
|
if (!parts.includes(p)) parts.push(p);
|
|
input.value = parts.join(',');
|
|
}
|
|
|
|
function startScan() {
|
|
const form = document.getElementById('scanForm');
|
|
const formData = new FormData(form);
|
|
|
|
// Mostra loading
|
|
document.getElementById('scanLoading').classList.remove('hidden');
|
|
document.getElementById('scanSummary').classList.add('hidden');
|
|
document.getElementById('scanResults').classList.add('hidden');
|
|
|
|
// Richiesta AJAX
|
|
fetch('{{ route('admin.code-quality.scan') }}', {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
document.getElementById('scanLoading').classList.add('hidden');
|
|
|
|
if (data.success) {
|
|
displayResults(data.results, data.summary);
|
|
} else {
|
|
alert('Errore durante la scansione: ' + data.message);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
document.getElementById('scanLoading').classList.add('hidden');
|
|
console.error('Error:', error);
|
|
alert('Errore durante la scansione');
|
|
});
|
|
}
|
|
|
|
function quickScanGesconImport() {
|
|
const form = document.getElementById('scanForm');
|
|
form.querySelector('select[name="module"]').value = 'gescon-import';
|
|
form.querySelector('select[name="menu"]').value = 'gescon-import';
|
|
startScan();
|
|
}
|
|
|
|
function displayResults(results, summary) {
|
|
// Aggiorna summary
|
|
document.getElementById('totalFiles').textContent = summary.total_scanned_files ?? summary.total_files;
|
|
document.getElementById('totalIssues').textContent = summary.total_issues;
|
|
document.getElementById('criticalCount').textContent = summary.by_severity.critical;
|
|
document.getElementById('errorCount').textContent = summary.by_severity.error;
|
|
|
|
document.getElementById('scanSummary').classList.remove('hidden');
|
|
renderRuleSummary(summary);
|
|
renderModuleMenuSummary(summary);
|
|
|
|
// Genera risultati dettagliati
|
|
const resultsContainer = document.getElementById('scanResults');
|
|
resultsContainer.innerHTML = '';
|
|
|
|
if (results.length === 0) {
|
|
resultsContainer.innerHTML = `
|
|
<div class="netgescon-card text-center">
|
|
<i class="fas fa-check-circle text-green-500 text-4xl mb-4"></i>
|
|
<h3 class="netgescon-subtitle text-green-600">Nessun Problema Rilevato!</h3>
|
|
<p class="netgescon-text">Il codice rispetta tutti gli standard NetGescon.</p>
|
|
</div>
|
|
`;
|
|
} else {
|
|
results.forEach(fileResult => {
|
|
const fileCard = createFileResultCard(fileResult);
|
|
resultsContainer.appendChild(fileCard);
|
|
});
|
|
}
|
|
|
|
document.getElementById('scanResults').classList.remove('hidden');
|
|
}
|
|
|
|
function createFileResultCard(fileResult) {
|
|
const card = document.createElement('div');
|
|
card.className = 'netgescon-card';
|
|
|
|
const severityIcons = {
|
|
critical: '🔴',
|
|
error: '🟠',
|
|
warning: '🟡',
|
|
info: '🔵'
|
|
};
|
|
const fileName = fileResult.file || '';
|
|
const safeFile = fileName.replace(/'/g, "\\'");
|
|
const lintable = /\.php$/i.test(fileName) || /\.blade\.php$/i.test(fileName);
|
|
|
|
const fixableRules = new Set(['layout_required','layout_forbidden_app','bootstrap_classes','deprecated_helpers']);
|
|
let issuesHtml = '';
|
|
fileResult.issues.forEach(issue => {
|
|
const canFix = fixableRules.has(issue.rule);
|
|
issuesHtml += `
|
|
<div class="border-l-4 border-${getSeverityColor(issue.severity)}-500 pl-4 py-3 mb-4 bg-gray-50">
|
|
<div class="flex items-center justify-between">
|
|
<h4 class="font-medium text-gray-800">
|
|
${severityIcons[issue.severity]} ${issue.name}
|
|
</h4>
|
|
<div class="flex items-center space-x-2">
|
|
<span class="text-sm text-gray-500">Riga ${issue.line}</span>
|
|
${canFix ? `<button onclick="applyAutoFix('${safeFile}', '${issue.rule}')" class="netgescon-btn-small netgescon-btn-secondary"><i class="fas fa-magic"></i> Fix Auto</button>` : ''}
|
|
</div>
|
|
</div>
|
|
<p class="text-sm text-gray-600 mt-1">${issue.description}</p>
|
|
<div class="mt-2">
|
|
<p class="text-xs text-gray-500 mb-1">Codice trovato:</p>
|
|
<code class="text-xs bg-red-100 text-red-800 px-2 py-1 rounded">${issue.match}</code>
|
|
</div>
|
|
<div class="mt-2">
|
|
<p class="text-xs text-gray-500 mb-1">Suggerimento:</p>
|
|
<code class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">${issue.suggestion}</code>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
card.innerHTML = `
|
|
<div class="flex items-center justify-between mb-4 flex-wrap gap-2">
|
|
<h3 class="netgescon-subtitle flex items-center gap-2">
|
|
<i class="fas fa-file-code"></i>
|
|
${fileName}
|
|
</h3>
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<span class="netgescon-badge netgescon-badge-warning">
|
|
${fileResult.issues.length} problemi
|
|
</span>
|
|
${lintable ? `<button type="button" class="netgescon-btn netgescon-btn-xs netgescon-btn-outline-secondary" onclick="lintFile('${safeFile}')"><i class="fas fa-stethoscope mr-1"></i>Sintassi</button>` : ''}
|
|
<button type="button" class="netgescon-btn netgescon-btn-xs" onclick="openQuickEdit('${safeFile}')"><i class="fas fa-edit mr-1"></i>Editor</button>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-4">
|
|
${issuesHtml}
|
|
</div>
|
|
`;
|
|
|
|
return card;
|
|
}
|
|
|
|
function getSeverityColor(severity) {
|
|
const colors = {
|
|
critical: 'red',
|
|
error: 'orange',
|
|
warning: 'yellow',
|
|
info: 'blue'
|
|
};
|
|
return colors[severity] || 'gray';
|
|
}
|
|
|
|
function applyAutoFix(file, rule) {
|
|
fetch('{{ route('admin.code-quality.autofix') }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
file: file,
|
|
rule: rule
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
alert('Fix applicato con successo!');
|
|
startScan(); // Riavvia scansione
|
|
return;
|
|
}
|
|
// Se il server restituisce un contenuto proposto (file non scrivibile o anteprima), mostralo
|
|
if (data.proposed) {
|
|
openAutofixModal(file, data.proposed, data.permissionHints || [], 'preview');
|
|
return;
|
|
}
|
|
alert('Errore: ' + (data.message || 'Nessun fix disponibile'));
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('Errore durante il fix automatico');
|
|
});
|
|
}
|
|
|
|
function exportResults(format) {
|
|
const form = document.getElementById('scanForm');
|
|
const formData = new FormData(form);
|
|
formData.append('format', format);
|
|
fetch('{{ route('admin.code-quality.export') }}', {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' }
|
|
}).then(async res => {
|
|
const blob = await res.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = format === 'json' ? 'code-quality-report.json' : 'code-quality-report.csv';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
window.URL.revokeObjectURL(url);
|
|
}).catch(err => {
|
|
console.error(err);
|
|
alert('Errore durante l\'export');
|
|
});
|
|
}
|
|
|
|
function renderRuleSummary(summary) {
|
|
const container = document.getElementById('ruleSummary');
|
|
if (!summary.by_rule || Object.keys(summary.by_rule).length === 0) {
|
|
container.classList.add('hidden');
|
|
return;
|
|
}
|
|
container.classList.remove('hidden');
|
|
let html = '<div class="flex flex-wrap gap-2">';
|
|
Object.entries(summary.by_rule).forEach(([key, data]) => {
|
|
html += `<span class="netgescon-badge netgescon-badge-info" title="${data.name}">${data.name}: ${data.count}</span>`;
|
|
});
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function renderModuleMenuSummary(summary) {
|
|
const container = document.getElementById('moduleMenuSummary');
|
|
let html = '<div class="flex flex-wrap gap-3">';
|
|
if (summary.by_module) {
|
|
html += '<div><div class="font-medium mb-1">Per Modulo</div>';
|
|
Object.entries(summary.by_module).forEach(([k, v]) => {
|
|
html += `<span class="netgescon-badge netgescon-badge-secondary">${k}: ${v}</span> `;
|
|
});
|
|
html += '</div>';
|
|
}
|
|
if (summary.by_menu) {
|
|
html += '<div><div class="font-medium mb-1">Per Menù</div>';
|
|
Object.entries(summary.by_menu).forEach(([k, v]) => {
|
|
html += `<span class="netgescon-badge netgescon-badge-secondary">${k}: ${v}</span> `;
|
|
});
|
|
html += '</div>';
|
|
}
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
// Avvia scansione automatica se ci sono già risultati
|
|
@if(isset($results))
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const results = @json($results ?? []);
|
|
// Generiamo summary manualmente dal frontend se ci sono risultati
|
|
if (results.length > 0) {
|
|
const summary = generateSummaryFromResults(results);
|
|
displayResults(results, summary);
|
|
}
|
|
});
|
|
|
|
function generateSummaryFromResults(results) {
|
|
const summary = {
|
|
total_files: results.length,
|
|
total_scanned_files: {{ $totalScanned ?? 'null' }},
|
|
total_issues: 0,
|
|
by_severity: { critical: 0, error: 0, warning: 0, info: 0 },
|
|
by_rule: {},
|
|
by_module: {},
|
|
by_menu: {}
|
|
};
|
|
|
|
results.forEach(fileResult => {
|
|
fileResult.issues.forEach(issue => {
|
|
summary.total_issues++;
|
|
summary.by_severity[issue.severity]++;
|
|
|
|
if (!summary.by_rule[issue.rule]) {
|
|
summary.by_rule[issue.rule] = {
|
|
name: issue.name,
|
|
count: 0
|
|
};
|
|
}
|
|
summary.by_rule[issue.rule].count++;
|
|
|
|
const module = fileResult.module || 'unknown';
|
|
const menu = fileResult.menu || 'unknown';
|
|
summary.by_module[module] = (summary.by_module[module] || 0) + 1;
|
|
summary.by_menu[menu] = (summary.by_menu[menu] || 0) + 1;
|
|
});
|
|
});
|
|
|
|
return summary;
|
|
}
|
|
@endif
|
|
</script>
|
|
@endsection
|