Feat: dashboard con gráficos, estructura CBS/Portafolios, semáforo de proyectos y cartera de especialistas
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
@@ -20,9 +21,9 @@
|
||||
<span class="kpi-title">EJECUCIÓN HISTÓRICA / MES</span>
|
||||
<div class="kpi-icon"><i data-lucide="credit-card"></i></div>
|
||||
</div>
|
||||
<div class="kpi-value" id="kpi-spend">${{ total_all|floatformat:"0" }} CLP</div>
|
||||
<div class="kpi-value" id="kpi-spend">${{ total_all|floatformat:"0"|intcomma }} CLP</div>
|
||||
<div class="kpi-subtext">
|
||||
<span>Mes actual: ${{ total_month|floatformat:"0" }} CLP</span>
|
||||
<span>Mes actual: ${{ total_month|floatformat:"0"|intcomma }} CLP</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,9 +32,9 @@
|
||||
<span class="kpi-title">PRESUPUESTO GENERAL I+D+i</span>
|
||||
<div class="kpi-icon"><i data-lucide="wallet"></i></div>
|
||||
</div>
|
||||
<div class="kpi-value" id="kpi-remaining">${{ global_budget|floatformat:"0" }} CLP</div>
|
||||
<div class="kpi-value" id="kpi-remaining">${{ global_budget|floatformat:"0"|intcomma }} CLP</div>
|
||||
<div class="kpi-subtext">
|
||||
<span>Saldo Restante: ${{ remaining_budget|floatformat:"0" }} CLP</span>
|
||||
<span>Saldo Restante: ${{ remaining_budget|floatformat:"0"|intcomma }} CLP</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,6 +50,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gráficos de Inteligencia de Negocios (BI) -->
|
||||
<div class="charts-grid" style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem; margin-bottom: 2rem;">
|
||||
<!-- Gráfico 1: Categorías -->
|
||||
<div class="data-card" style="padding: 1.5rem;">
|
||||
<h3 style="color: var(--efintel-petroleum); font-size: 1rem; margin-bottom: 1rem; border-bottom: 1px solid var(--efintel-light); padding-bottom: 0.5rem;">Distribución por Categoría</h3>
|
||||
<div style="position: relative; height: 250px;">
|
||||
<canvas id="categoryChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gráfico 2: Proyectos -->
|
||||
<div class="data-card" style="padding: 1.5rem;">
|
||||
<h3 style="color: var(--efintel-petroleum); font-size: 1rem; margin-bottom: 1rem; border-bottom: 1px solid var(--efintel-light); padding-bottom: 0.5rem;">Ejecución por Proyectos / Centros</h3>
|
||||
<div style="position: relative; height: 250px;">
|
||||
<canvas id="projectChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card">
|
||||
<div class="data-card-header">
|
||||
<div style="font-weight:700; font-size:0.95rem; color:var(--efintel-dark);">Últimas Erogaciones Registradas (Flujo ERP)</div>
|
||||
@@ -79,8 +99,8 @@
|
||||
<span style="font-size:0.65rem; color:var(--efintel-gray);">{{ expense.responsible_person.username|default:expense.user.username }}</span>
|
||||
</td>
|
||||
<td class="amount-cell">
|
||||
<div>${{ expense.amount_clp|floatformat:"0" }} CLP</div>
|
||||
<div style="font-size:0.68rem; color:var(--efintel-gray); font-family:var(--font-mono);">${{ expense.amount_usd|floatformat:"2" }} USD</div>
|
||||
<div>${{ expense.amount_clp|floatformat:"0"|intcomma }} CLP</div>
|
||||
<div style="font-size:0.68rem; color:var(--efintel-gray); font-family:var(--font-mono);">${{ expense.amount_usd|floatformat:"2"|intcomma }} USD</div>
|
||||
</td>
|
||||
<td>
|
||||
{% if expense.approval_status == 'EN_REVISION_DOCUMENTAL' %}
|
||||
@@ -120,3 +140,112 @@
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Colores corporativos EFINTEL
|
||||
const colors = [
|
||||
'#053B50', // petroleum
|
||||
'#176B87', // blue
|
||||
'#64CCC5', // turquoise
|
||||
'#107c41', // emerald
|
||||
'#d97706', // amber
|
||||
'#475569', // slate
|
||||
'#0f172a' // dark
|
||||
];
|
||||
|
||||
// Data inyectada desde el backend (JSON)
|
||||
const catLabels = JSON.parse('{{ category_labels_json|safe }}');
|
||||
const catData = JSON.parse('{{ category_data_json|safe }}');
|
||||
|
||||
const projLabels = JSON.parse('{{ project_labels_json|safe }}');
|
||||
const projData = JSON.parse('{{ project_data_json|safe }}');
|
||||
|
||||
// 1. Gráfico de Dona: Categorías
|
||||
if (document.getElementById('categoryChart') && catData.length > 0) {
|
||||
new Chart(document.getElementById('categoryChart'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: catLabels,
|
||||
datasets: [{
|
||||
data: catData,
|
||||
backgroundColor: colors,
|
||||
borderWidth: 0,
|
||||
hoverOffset: 4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: 'right', labels: { boxWidth: 12, font: { family: 'Inter', size: 11 } } },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let value = context.raw || 0;
|
||||
return ' $' + value.toLocaleString('es-CL') + ' CLP';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
cutout: '70%'
|
||||
}
|
||||
});
|
||||
} else if(document.getElementById('categoryChart')) {
|
||||
document.getElementById('categoryChart').parentElement.innerHTML = '<div style="display:flex; height:100%; align-items:center; justify-content:center; color:#94a3b8;">Sin datos suficientes</div>';
|
||||
}
|
||||
|
||||
// 2. Gráfico de Barras: Proyectos
|
||||
if (document.getElementById('projectChart') && projData.length > 0) {
|
||||
new Chart(document.getElementById('projectChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: projLabels,
|
||||
datasets: [{
|
||||
label: 'Gasto Acumulado (CLP)',
|
||||
data: projData,
|
||||
backgroundColor: '#176B87',
|
||||
borderRadius: 4,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let value = context.raw || 0;
|
||||
return ' $' + value.toLocaleString('es-CL') + ' CLP';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: '#f1f5f9', drawBorder: false },
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
if (value >= 1000000) return '$' + (value / 1000000).toFixed(1) + 'M';
|
||||
if (value >= 1000) return '$' + (value / 1000).toFixed(0) + 'k';
|
||||
return '$' + value;
|
||||
},
|
||||
font: { family: 'Inter', size: 10 }
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { font: { family: 'Inter', size: 11 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if(document.getElementById('projectChart')) {
|
||||
document.getElementById('projectChart').parentElement.innerHTML = '<div style="display:flex; height:100%; align-items:center; justify-content:center; color:#94a3b8;">Sin datos suficientes</div>';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user