Feat: dashboard con gráficos, estructura CBS/Portafolios, semáforo de proyectos y cartera de especialistas

This commit is contained in:
Nachou-u
2026-08-20 18:54:58 -04:00
parent 8c0b435555
commit b692614857
25 changed files with 1717 additions and 159 deletions
+5
View File
@@ -55,6 +55,11 @@
<a href="{% url 'project_list' %}" class="nav-tab {% if request.resolver_match.url_name == 'project_list' %}active{% endif %}">
<i data-lucide="folder-kanban" style="width:16px;"></i> Control de Proyectos
</a>
{% if 'Administrador Finanzas' in request.user.groups.all.0.name or 'Administrador' in request.user.groups.all.0.name or request.user.is_superuser %}
<a href="{% url 'accounts_payable' %}" class="nav-tab {% if request.resolver_match.url_name == 'accounts_payable' %}active{% endif %}">
<i data-lucide="calculator" style="width:16px;"></i> Cuentas por Pagar (CXP)
</a>
{% endif %}
{% if 'Administrador' in request.user.groups.all.0.name or request.user.is_superuser %}
<a href="{% url 'gestion_panel' %}" class="nav-tab {% if request.resolver_match.url_name == 'gestion_panel' %}active{% endif %}" style="margin-left:auto; background:var(--efintel-petroleum); color:white;">
<i data-lucide="settings" style="width:16px;"></i> Gestión ERP
+63
View File
@@ -0,0 +1,63 @@
{% extends 'base.html' %}
{% load humanize %}
{% block content %}
<div class="view-header">
<div class="view-title">
<h2>Cuentas por Pagar (CXP)</h2>
<p>Consolidado de deudas por proveedor en estado Solicitado a Finanzas.</p>
</div>
</div>
<section class="main-content">
<div class="data-card">
<div class="data-table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>RUT</th>
<th>Razón Social</th>
<th>Datos de Pago</th>
<th>Cantidad de Facturas</th>
<th>Total Deuda (CLP)</th>
<th>Total Deuda (USD)</th>
<th>Acción Sugerida</th>
</tr>
</thead>
<tbody>
{% for debt in debts %}
<tr>
<td style="font-weight: 600; color: var(--efintel-blue);">{{ debt.vendor__rut }}</td>
<td style="font-weight: 600;">{{ debt.vendor__name }}</td>
<td>
{% if debt.vendor__bank %}
<div style="font-size: 0.85rem;">{{ debt.vendor__bank }}</div>
<div style="font-size: 0.75rem; color: var(--efintel-gray);">{{ debt.vendor__account_type }} - {{ debt.vendor__account_number }}</div>
{% else %}
<em style="color: var(--efintel-gray);">No registrados</em>
{% endif %}
</td>
<td style="text-align: center;"><span class="badge badge-amber">{{ debt.expense_count }} pendientes</span></td>
<td class="amount-cell" style="font-size: 1rem; color: var(--efintel-danger);">
${{ debt.total_clp|floatformat:"0"|intcomma }}
</td>
<td class="amount-cell" style="font-size: 0.85rem; color: var(--efintel-gray);">
${{ debt.total_usd|floatformat:"2"|intcomma }}
</td>
<td>
<a href="{% url 'expense_list' %}?vendor={{ debt.vendor__id }}" class="btn-primary" style="font-size: 0.75rem;">Ver Detalle</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" style="text-align: center; padding: 3rem; color: var(--efintel-gray);">
No hay cuentas por pagar pendientes en este momento.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</section>
{% endblock %}
+62
View File
@@ -0,0 +1,62 @@
{% extends 'base.html' %}
{% block content %}
<div class="form-container" style="max-width: 800px; margin: 0 auto;">
<div class="data-card" style="padding: 2.5rem; background: white; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);">
<h2 style="color: var(--efintel-petroleum); margin-bottom: 2rem; border-bottom: 2px solid var(--efintel-light); padding-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
<i data-lucide="{% if form.instance.pk %}edit-3{% else %}plus-circle{% endif %}"></i>
{% if form.instance.pk %}Editar Categoría Contable{% else %}Nueva Categoría Contable{% endif %}
</h2>
<form method="post">
{% csrf_token %}
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem; margin-bottom: 1.5rem;">
<div class="form-group">
<label for="{{ form.code.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Código</label>
{{ form.code }}
{% if form.code.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.code.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<label for="{{ form.name.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Nombre de Categoría</label>
{{ form.name }}
{% if form.name.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.name.errors }}</div>
{% endif %}
</div>
</div>
<div class="form-group" style="margin-bottom: 1.5rem;">
<label for="{{ form.parent.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Partida (Categoría Padre)</label>
<p style="font-size: 0.75rem; color: var(--text-secondary); margin-bottom: 0.25rem;">Dejar en blanco si esta es una Partida principal.</p>
{{ form.parent }}
{% if form.parent.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.parent.errors }}</div>
{% endif %}
</div>
<div class="form-group" style="margin-bottom: 2rem;">
<label for="{{ form.description.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Descripción (Opcional)</label>
{{ form.description }}
{% if form.description.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.description.errors }}</div>
{% endif %}
</div>
<div style="display: flex; gap: 1rem; padding-top: 1.5rem; border-top: 1px solid var(--efintel-light);">
<button type="submit" class="btn-primary" style="padding: 0.6rem 2rem; font-size: 0.9rem;">Guardar Categoría</button>
<a href="{% url 'gestion_panel' %}" class="btn-secondary" style="padding: 0.6rem 2rem; font-size: 0.9rem; text-decoration: none;">Cancelar</a>
</div>
</form>
</div>
</div>
<script>
document.querySelectorAll('input, select, textarea').forEach(el => {
el.classList.add('form-control');
});
</script>
{% endblock %}
+135 -6
View File
@@ -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 %}
+235 -5
View File
@@ -12,15 +12,27 @@
{% csrf_token %}
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
<div class="form-group">
<div class="form-group" style="position: relative;">
<label for="{{ form.project.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Centro de Costo / Proyecto</label>
{{ form.project }}
<div class="custom-select-wrapper" style="position: relative;">
{{ form.project }}
<input type="text" id="project-search-input" placeholder="Escriba código o nombre del proyecto..." autocomplete="off">
<div id="project-dropdown-list" class="custom-dropdown-list" style="display: none; position: absolute; background: white; border: 1px solid var(--efintel-border); border-radius: 4px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); width: 100%; z-index: 1000; max-height: 250px; overflow-y: auto; margin-top: 4px; box-sizing: border-box;">
<!-- Items dynamically populated -->
</div>
</div>
{% if form.project.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.project.errors }}</div>{% endif %}
</div>
<div class="form-group">
<div class="form-group" style="position: relative;">
<label for="{{ form.category.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Categoría Contable</label>
{{ form.category }}
<div class="custom-select-wrapper" style="position: relative;">
{{ form.category }}
<input type="text" id="category-search-input" placeholder="Escriba código o nombre para buscar..." autocomplete="off">
<div id="category-dropdown-list" class="custom-dropdown-list" style="display: none; position: absolute; background: white; border: 1px solid var(--efintel-border); border-radius: 4px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); width: 100%; z-index: 1000; max-height: 250px; overflow-y: auto; margin-top: 4px; box-sizing: border-box;">
<!-- Items dynamically populated -->
</div>
</div>
{% if form.category.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.category.errors }}</div>{% endif %}
</div>
</div>
@@ -29,10 +41,32 @@
<div class="form-group">
<label for="{{ form.amount_clp.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Monto Neto (CLP)</label>
{{ form.amount_clp }}
<small style="color: var(--efintel-gray); font-size: 0.7rem;">El equivalente en USD será calculado automáticamente por el sistema.</small>
{% if form.amount_clp.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.amount_clp.errors }}</div>{% endif %}
</div>
<div class="form-group">
<label for="{{ form.tax_type.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Tipo de Impuesto</label>
{{ form.tax_type }}
{% if form.tax_type.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.tax_type.errors }}</div>{% endif %}
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
<div class="form-group">
<label for="{{ form.tax_amount_clp.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Monto Impuesto (CLP)</label>
{{ form.tax_amount_clp }}
{% if form.tax_amount_clp.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.tax_amount_clp.errors }}</div>{% endif %}
</div>
<div class="form-group">
<label for="{{ form.total_amount_clp.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Monto Total (CLP)</label>
{{ form.total_amount_clp }}
<small style="color: var(--efintel-gray); font-size: 0.7rem;">El equivalente en USD será calculado automáticamente por el sistema.</small>
{% if form.total_amount_clp.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.total_amount_clp.errors }}</div>{% endif %}
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
<div class="form-group">
<label for="{{ form.currency.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Moneda Original</label>
{{ form.currency }}
@@ -110,5 +144,201 @@
el.style.fontSize = '0.9rem';
el.style.boxSizing = 'border-box';
});
const amountInput = document.getElementById('id_amount_clp');
const taxTypeSelect = document.getElementById('id_tax_type');
const taxAmountInput = document.getElementById('id_tax_amount_clp');
const totalAmountInput = document.getElementById('id_total_amount_clp');
function calculateTaxes() {
const amount = parseFloat(amountInput.value) || 0;
const taxType = taxTypeSelect.value;
let taxAmount = 0;
if (taxType === 'IVA_19') {
taxAmount = amount * 0.19;
} else if (taxType === 'HONORARIOS_13_75') {
taxAmount = amount * 0.1375;
}
taxAmountInput.value = taxAmount.toFixed(0);
totalAmountInput.value = (amount + taxAmount).toFixed(0);
}
amountInput.addEventListener('input', calculateTaxes);
taxTypeSelect.addEventListener('change', calculateTaxes);
// Initial calculation
calculateTaxes();
// Custom Searchable Dropdown for Category
const categorySelect = document.getElementById('id_category');
const searchInput = document.getElementById('category-search-input');
const dropdownList = document.getElementById('category-dropdown-list');
if (categorySelect && searchInput && dropdownList) {
// Hide original select (make it display none so form data is still submitted)
categorySelect.style.display = 'none';
// Extract options from the django-generated select
const options = Array.from(categorySelect.options)
.filter(opt => opt.value !== '')
.map(opt => ({
value: opt.value,
text: opt.textContent
}));
// Render option list dynamically
function renderList(filteredOptions) {
dropdownList.innerHTML = '';
if (filteredOptions.length === 0) {
dropdownList.innerHTML = '<div style="padding: 0.65rem; color: var(--efintel-gray); font-size: 0.85rem; text-align: center;">No se encontraron categorías</div>';
return;
}
filteredOptions.forEach(opt => {
const item = document.createElement('div');
item.className = 'dropdown-item';
item.style.padding = '0.65rem 1rem';
item.style.cursor = 'pointer';
item.style.fontSize = '0.85rem';
item.style.borderBottom = '1px solid #f1f5f9';
item.textContent = opt.text;
// Mark parent categories differently (they usually have a badge in representation or just end with standard code)
// Note: we can detect if it's a main partida (ends in '000: ' or contains 'Partida')
const isParent = opt.text.includes('1000: ') || opt.text.includes('2000: ') || opt.text.includes('3000: ') || opt.text.includes('4000: ');
if (isParent) {
item.style.fontWeight = '700';
item.style.background = '#f8fafc';
item.style.color = 'var(--efintel-petroleum)';
}
item.addEventListener('click', () => {
categorySelect.value = opt.value;
searchInput.value = opt.text;
dropdownList.style.display = 'none';
categorySelect.dispatchEvent(new Event('change'));
});
item.addEventListener('mouseenter', () => {
item.style.background = 'var(--efintel-light)';
item.style.color = 'var(--efintel-blue)';
});
item.addEventListener('mouseleave', () => {
item.style.background = isParent ? '#f8fafc' : 'white';
item.style.color = isParent ? 'var(--efintel-petroleum)' : 'inherit';
});
dropdownList.appendChild(item);
});
}
// Initialize input value if edit mode
const selectedOption = categorySelect.options[categorySelect.selectedIndex];
if (selectedOption && selectedOption.value !== '') {
searchInput.value = selectedOption.textContent;
}
// Show dropdown list on focus
searchInput.addEventListener('focus', () => {
renderList(options);
dropdownList.style.display = 'block';
});
// Dynamic filtering
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
const filtered = options.filter(opt => opt.text.toLowerCase().includes(query));
renderList(filtered);
});
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
if (!searchInput.contains(e.target) && !dropdownList.contains(e.target)) {
dropdownList.style.display = 'none';
}
});
}
// Custom Searchable Dropdown for Project
const projectSelect = document.getElementById('id_project');
const projectSearchInput = document.getElementById('project-search-input');
const projectDropdownList = document.getElementById('project-dropdown-list');
if (projectSelect && projectSearchInput && projectDropdownList) {
projectSelect.style.display = 'none';
const projectOptions = Array.from(projectSelect.options)
.filter(opt => opt.value !== '')
.map(opt => ({
value: opt.value,
text: opt.textContent
}));
function renderProjectList(filteredOptions) {
projectDropdownList.innerHTML = '';
if (filteredOptions.length === 0) {
projectDropdownList.innerHTML = '<div style="padding: 0.65rem; color: var(--efintel-gray); font-size: 0.85rem; text-align: center;">No se encontraron proyectos</div>';
return;
}
filteredOptions.forEach(opt => {
const item = document.createElement('div');
item.className = 'dropdown-item';
item.style.padding = '0.65rem 1rem';
item.style.cursor = 'pointer';
item.style.fontSize = '0.85rem';
item.style.borderBottom = '1px solid #f1f5f9';
item.textContent = opt.text;
const isPortfolio = opt.text.startsWith('PORT-');
if (isPortfolio) {
item.style.fontWeight = '700';
item.style.background = '#f8fafc';
item.style.color = 'var(--efintel-petroleum)';
}
item.addEventListener('click', () => {
projectSelect.value = opt.value;
projectSearchInput.value = opt.text;
projectDropdownList.style.display = 'none';
projectSelect.dispatchEvent(new Event('change'));
});
item.addEventListener('mouseenter', () => {
item.style.background = 'var(--efintel-light)';
item.style.color = 'var(--efintel-blue)';
});
item.addEventListener('mouseleave', () => {
item.style.background = isPortfolio ? '#f8fafc' : 'white';
item.style.color = isPortfolio ? 'var(--efintel-petroleum)' : 'inherit';
});
projectDropdownList.appendChild(item);
});
}
const selectedProjectOption = projectSelect.options[projectSelect.selectedIndex];
if (selectedProjectOption && selectedProjectOption.value !== '') {
projectSearchInput.value = selectedProjectOption.textContent;
}
projectSearchInput.addEventListener('focus', () => {
renderProjectList(projectOptions);
projectDropdownList.style.display = 'block';
});
projectSearchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
const filtered = projectOptions.filter(opt => opt.text.toLowerCase().includes(query));
renderProjectList(filtered);
});
document.addEventListener('click', (e) => {
if (!projectSearchInput.contains(e.target) && !projectDropdownList.contains(e.target)) {
projectDropdownList.style.display = 'none';
}
});
}
</script>
{% endblock %}
+16 -11
View File
@@ -1,5 +1,6 @@
{% extends 'base.html' %}
{% load static %}
{% load humanize %}
{% block content %}
<section class="page-view active">
@@ -71,8 +72,8 @@
{% endif %}
</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' %}
@@ -96,15 +97,19 @@
<a href="{% url 'approve_expense' expense.id 'rechazar' %}" class="btn-danger" style="padding:0.3rem; font-size:0.65rem;" title="Rechazar"><i data-lucide="x"></i></a>
{% endif %}
{% if request.user.groups.all.0.name == 'Administrador' or request.user.is_superuser or expense.user == request.user %}
<a href="/gastos/{{ expense.id }}/editar/" class="btn-secondary" style="padding: 0.3rem; background: var(--efintel-light); color: var(--efintel-petroleum);" title="Editar"><i data-lucide="pencil" style="width:14px;"></i></a>
{% endif %}
{% if request.user.groups.all.0.name == 'Administrador' or request.user.is_superuser %}
<form method="post" action="/gastos/{{ expense.id }}/eliminar/" style="display:inline;">
{% csrf_token %}
<button type="submit" class="btn-danger" style="padding: 0.3rem;" onclick="return confirm('¿Seguro que desea eliminar esta erogación? Esta acción quedará en el Log de Auditoría.');" title="Eliminar"><i data-lucide="trash-2" style="width:14px;"></i></button>
</form>
{% if expense.approval_status != 'SOLICITADO_FINANZAS' and expense.approval_status != 'LIBERADO_PAGADO' %}
{% if request.user.groups.all.0.name == 'Administrador' or request.user.is_superuser or expense.user == request.user %}
<a href="/gastos/{{ expense.id }}/editar/" class="btn-secondary" style="padding: 0.3rem; background: var(--efintel-light); color: var(--efintel-petroleum);" title="Editar"><i data-lucide="pencil" style="width:14px;"></i></a>
{% endif %}
{% if request.user.groups.all.0.name == 'Administrador' or request.user.is_superuser %}
<form method="post" action="/gastos/{{ expense.id }}/eliminar/" style="display:inline;">
{% csrf_token %}
<button type="submit" class="btn-danger" style="padding: 0.3rem;" onclick="return confirm('¿Seguro que desea eliminar esta erogación? Esta acción quedará en el Log de Auditoría.');" title="Eliminar"><i data-lucide="trash-2" style="width:14px;"></i></button>
</form>
{% endif %}
{% else %}
<span style="font-size:0.75rem; color:var(--efintel-gray); padding: 0.3rem;" title="Comprobante Bloqueado"><i data-lucide="lock" style="width:14px;"></i></span>
{% endif %}
</div>
</td>
+307 -47
View File
@@ -1,41 +1,149 @@
{% extends 'base.html' %}
{% load humanize %}
{% block content %}
<div class="header-section" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
<h1>Panel de Gestión Administrativa</h1>
</div>
<div class="glass-card tabs-container" style="padding: 2rem;">
<nav class="tabs-nav">
<button class="tab-btn active" onclick="switchTab(event, 'tab-proyectos')">📁 Proyectos</button>
<button class="tab-btn" onclick="switchTab(event, 'tab-responsables')">👥 Colaboradores / Responsables</button>
<button class="tab-btn" onclick="switchTab(event, 'tab-config')">⚙️ Configuración</button>
<button class="tab-btn" onclick="switchTab(event, 'tab-bitacora')">📜 Bitácora de Auditoría</button>
</nav>
<style>
.sidebar-btn {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
text-align: left;
padding: 0.8rem 1rem;
background: transparent;
border: none;
border-radius: 6px;
color: var(--efintel-dark);
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.sidebar-btn:hover {
background: rgba(0, 0, 0, 0.05);
color: var(--efintel-blue);
}
.sidebar-btn.active {
background: rgba(5, 59, 80, 0.08); /* efintel-petroleum with opacity */
color: var(--efintel-petroleum);
font-weight: 600;
border-left: 4px solid var(--efintel-petroleum);
}
.sidebar-btn i {
width: 18px;
height: 18px;
}
.tab-content {
display: none;
animation: fadeIn 0.3s ease;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
<div class="gestion-layout" style="display: grid; grid-template-columns: 260px 1fr; gap: 2rem; align-items: start;">
<!-- Sidebar Navigation -->
<div class="data-card" style="padding: 1rem; position: sticky; top: 2rem; background: white; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);">
<nav class="sidebar-nav" style="display: flex; flex-direction: column; gap: 0.25rem;">
<button class="sidebar-btn active" onclick="switchTab(event, 'tab-proyectos')">
<i data-lucide="folder-kanban"></i> Proyectos y Centros
</button>
<button class="sidebar-btn" onclick="switchTab(event, 'tab-categorias')">
<i data-lucide="tags"></i> Categorías Contables
</button>
<button class="sidebar-btn" onclick="switchTab(event, 'tab-proveedores')">
<i data-lucide="building-2"></i> Proveedores
</button>
<button class="sidebar-btn" onclick="switchTab(event, 'tab-responsables')">
<i data-lucide="users"></i> Responsables
</button>
<hr style="border: none; border-top: 1px solid var(--efintel-light); margin: 0.75rem 0;" />
<button class="sidebar-btn" onclick="switchTab(event, 'tab-config')">
<i data-lucide="settings"></i> Configuración General
</button>
<button class="sidebar-btn" onclick="switchTab(event, 'tab-bitacora')">
<i data-lucide="scroll-text"></i> Bitácora de Auditoría
</button>
</nav>
</div>
<!-- Main Content Area -->
<div class="gestion-content">
<!-- Pestaña Proyectos -->
<div id="tab-proyectos" class="tab-content active">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2>Proyectos del Sistema</h2>
<a href="{% url 'project_create' %}" class="btn btn-primary">➕ Nuevo Proyecto</a>
<h2>Proyectos y Centros de Costo</h2>
<a href="{% url 'project_create' %}" class="btn btn-primary">Nuevo Proyecto / Centro</a>
</div>
<div class="table-container">
<table>
<div class="data-card">
<div class="data-table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Código</th>
<th>Nombre</th>
<th>Descripción</th>
<th>Portafolio / Jerarquía</th>
<th>Categoría / Especialidad</th>
<th>Estado</th>
<th>Especialistas Habilitados</th>
<th>Encargado (Responsable)</th>
<th>Creado Por</th>
<th>Última Modificación</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{% for project in projects %}
<tr>
<td style="font-weight: 600; color: var(--accent-secondary);">{{ project.name }}</td>
<td>{{ project.description|default:"Sin descripción" }}</td>
<td style="font-weight: 600; color: var(--efintel-blue);">{{ project.code }}</td>
<td style="font-weight: 600; color: var(--accent-secondary);">
{% if project.parent %}<span style="color:var(--efintel-gray); margin-right:5px;">↳</span>{% endif %}
{{ project.name }}
</td>
<td>
{% if project.parent %}
<span style="font-size: 0.85rem; color: var(--efintel-blue); font-weight: 500;">{{ project.parent.code }} - {{ project.parent.name }}</span>
{% else %}
<span class="badge badge-emerald" style="font-size: 0.75rem;">Portafolio Principal</span>
{% endif %}
</td>
<td>
{% if project.category %}
<span style="font-size: 0.8rem; font-weight: 600; color: var(--efintel-petroleum);">{{ project.category.code }} - {{ project.category.name }}</span>
{% else %}
<span style="color: var(--efintel-gray); font-size: 0.75rem; font-style: italic;">Sin asignar</span>
{% endif %}
</td>
<td>
{% if project.status == 'ACTIVO' %}
<span class="badge badge-emerald"><i class="status-dot green"></i> Activo</span>
{% elif project.status == 'PAUSADA' %}
<span class="badge badge-amber"><i class="status-dot orange"></i> En Pausa</span>
{% else %}
<span class="badge badge-red"><i class="status-dot red"></i> Terminado</span>
{% endif %}
</td>
<td>
{% if project.category %}
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; max-width: 250px;">
{% for spec in project.category.specialists.all %}
<span class="badge badge-blue" style="font-size: 0.7rem; font-weight: 600; padding: 0.15rem 0.35rem; border-radius: 4px;">{{ spec.user.username }}</span>
{% empty %}
<span style="color: var(--efintel-gray); font-size: 0.75rem; font-style: italic;">Sin especialistas</span>
{% endfor %}
</div>
{% else %}
<span style="color: var(--efintel-gray); font-size: 0.75rem; font-style: italic;">Asigna una categoría para ver especialistas</span>
{% endif %}
</td>
<td>
{% if project.manager %}
<span style="font-weight: 500;">{{ project.manager.username }}</span>
@@ -43,44 +151,181 @@
<em style="color: var(--text-secondary);">No asignado</em>
{% endif %}
</td>
<td>{{ project.created_by.username|default:"Sistema" }}</td>
<td>{{ project.updated_at|date:"d/m/Y H:i" }}</td>
<td>
<div style="display: flex; gap: 0.5rem;">
<a href="{% url 'project_edit' project.id %}" class="btn btn-outline" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;">✏️ Editar</a>
<div style="display: flex; gap: 0.25rem;">
<a href="{% url 'project_edit' project.id %}" class="btn-icon" title="Editar" style="color: var(--efintel-blue); padding: 0.4rem; border-radius: 4px; transition: background 0.2s;">
<i data-lucide="edit-3" style="width: 16px;"></i>
</a>
<form action="{% url 'project_delete' project.id %}" method="POST" style="display: inline;" onsubmit="return confirm('¿Estás seguro de que deseas eliminar este proyecto? Se eliminarán todos los gastos asociados.');">
{% csrf_token %}
<button type="submit" class="btn btn-danger" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;">🗑️ Eliminar</button>
<button type="submit" class="btn-icon" title="Eliminar" style="color: var(--efintel-danger); padding: 0.4rem; border-radius: 4px; transition: background 0.2s; background: transparent; border: none; cursor: pointer;">
<i data-lucide="trash-2" style="width: 16px;"></i>
</button>
</form>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" style="text-align: center;">No hay proyectos registrados.</td>
<td colspan="8" style="text-align: center;">No hay proyectos registrados.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Pestaña Categorías -->
<div id="tab-categorias" class="tab-content">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<div>
<h2 style="color: var(--efintel-petroleum); margin-bottom: 0.25rem;">Categorías Contables</h2>
<p style="color: var(--efintel-gray); font-size: 0.9rem;">Estructura de imputación financiera.</p>
</div>
<a href="{% url 'category_create' %}" class="btn-primary" style="text-decoration: none;">
<i data-lucide="plus-circle" style="width: 16px;"></i> Nueva Categoría
</a>
</div>
<div class="data-card" style="padding: 0;">
<div class="data-table-wrapper" style="margin: 0; padding: 1.5rem;">
<table class="data-table">
<thead>
<tr>
<th>Código</th>
<th>Nombre</th>
<th>Partida / Jerarquía</th>
<th>Descripción</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{% for cat in categories %}
<tr>
<td style="font-weight: 600; color: var(--accent-primary);">{{ cat.code }}</td>
<td style="font-weight: 600; color: var(--text-primary);">
{% if cat.parent %}<span style="color:var(--efintel-gray); margin-right:5px;">↳</span>{% endif %}
{{ cat.name }}
</td>
<td>
{% if cat.parent %}
<span style="font-size: 0.85rem; color: var(--efintel-blue); font-weight: 500;">{{ cat.parent.code }} - {{ cat.parent.name }}</span>
{% else %}
<span class="badge badge-amber" style="font-size: 0.75rem;">Partida Principal</span>
{% endif %}
</td>
<td>{{ cat.description|default:"Sin descripción" }}</td>
<td>
<div style="display: flex; gap: 0.25rem;">
<a href="{% url 'category_edit' cat.id %}" class="btn-icon" title="Editar" style="color: var(--efintel-blue); padding: 0.4rem; border-radius: 4px; transition: background 0.2s;">
<i data-lucide="edit-3" style="width: 16px;"></i>
</a>
<form action="{% url 'category_delete' cat.id %}" method="POST" style="display: inline;" onsubmit="return confirm('¿Estás seguro de que deseas eliminar esta categoría? Se podrían perder asociaciones con los gastos.');">
{% csrf_token %}
<button type="submit" class="btn-icon" title="Eliminar" style="color: var(--efintel-danger); padding: 0.4rem; border-radius: 4px; transition: background 0.2s; background: transparent; border: none; cursor: pointer;">
<i data-lucide="trash-2" style="width: 16px;"></i>
</button>
</form>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" style="text-align: center;">No hay categorías registradas.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Pestaña Proveedores -->
<div id="tab-proveedores" class="tab-content">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<div>
<h2 style="color: var(--efintel-petroleum); margin-bottom: 0.25rem;">Directorio de Proveedores</h2>
<p style="color: var(--efintel-gray); font-size: 0.9rem;">Registro centralizado de proveedores habilitados.</p>
</div>
<a href="{% url 'vendor_create' %}" class="btn-primary" style="text-decoration: none;">
<i data-lucide="plus-circle" style="width: 16px;"></i> Nuevo Proveedor
</a>
</div>
<div class="data-card" style="padding: 0;">
<div class="data-table-wrapper" style="margin: 0; padding: 1.5rem;">
<table class="data-table">
<thead>
<tr>
<th>RUT</th>
<th>Razón Social</th>
<th>Giro Comercial</th>
<th>Datos Bancarios</th>
<th>Plazo Pago</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{% for vendor in vendors %}
<tr>
<td style="font-weight: 600; color: var(--accent-primary);">{{ vendor.rut }}</td>
<td style="font-weight: 600; color: var(--text-primary);">{{ vendor.name }}</td>
<td>{{ vendor.business_line|default:"-" }}</td>
<td>
{% if vendor.bank %}
<div style="font-size: 0.85rem;">{{ vendor.bank }}</div>
<div style="font-size: 0.75rem; color: var(--text-secondary);">{{ vendor.account_type }} - {{ vendor.account_number }}</div>
{% else %}
<em style="color: var(--text-secondary);">No registrados</em>
{% endif %}
</td>
<td>{{ vendor.payment_terms }} días</td>
<td>
<div style="display: flex; gap: 0.25rem;">
<a href="{% url 'vendor_edit' vendor.id %}" class="btn-icon" title="Editar" style="color: var(--efintel-blue); padding: 0.4rem; border-radius: 4px; transition: background 0.2s;">
<i data-lucide="edit-3" style="width: 16px;"></i>
</a>
<form action="{% url 'vendor_delete' vendor.id %}" method="POST" style="display: inline;" onsubmit="return confirm('¿Estás seguro de que deseas eliminar este proveedor? Sus gastos quedarán huérfanos.');">
{% csrf_token %}
<button type="submit" class="btn-icon" title="Eliminar" style="color: var(--efintel-danger); padding: 0.4rem; border-radius: 4px; transition: background 0.2s; background: transparent; border: none; cursor: pointer;">
<i data-lucide="trash-2" style="width: 16px;"></i>
</button>
</form>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" style="text-align: center;">No hay proveedores registrados.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Pestaña Responsables -->
<div id="tab-responsables" class="tab-content">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2>Responsables y Colaboradores</h2>
<a href="{% url 'responsable_create' %}" class="btn btn-primary">➕ Nuevo Responsable</a>
<div>
<h2 style="color: var(--efintel-petroleum); margin-bottom: 0.25rem;">Responsables y Colaboradores</h2>
<p style="color: var(--efintel-gray); font-size: 0.9rem;">Gestión de acceso y roles del personal.</p>
</div>
<a href="{% url 'responsable_create' %}" class="btn-primary" style="text-decoration: none;">
<i data-lucide="plus-circle" style="width: 16px;"></i> Nuevo Responsable
</a>
</div>
<div class="table-container">
<table>
<div class="data-card" style="padding: 0;">
<div class="data-table-wrapper" style="margin: 0; padding: 1.5rem;">
<table class="data-table">
<thead>
<tr>
<th>Usuario</th>
<th>Correo</th>
<th>Rol / Permiso</th>
<th>Especialidades Habilitadas (CBS)</th>
<th>Descripción / Bio</th>
<th>F. Registro</th>
<th>F. Modificación</th>
<th>Acciones</th>
</tr>
</thead>
@@ -93,27 +338,39 @@
{% if resp.groups.all %}
{% with role=resp.groups.all.0.name %}
{% if role == 'Administrador' %}
<span class="badge badge-admin">Administrador</span>
<span class="badge badge-red">Administrador</span>
{% elif role == 'Director de Proyecto' %}
<span class="badge badge-director">Director</span>
<span class="badge badge-blue">Director</span>
{% else %}
<span class="badge badge-gestor">Gestor</span>
<span class="badge badge-emerald">Gestor</span>
{% endif %}
{% endwith %}
{% else %}
<span class="badge badge-gestor">Sin Rol</span>
<span class="badge badge-amber">Sin Rol</span>
{% endif %}
</td>
<td>
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; max-width: 250px;">
{% for spec in resp.profile.specialties.all %}
<span class="badge badge-blue" style="font-size: 0.7rem; padding: 0.15rem 0.35rem;" title="{{ spec.name }}">{{ spec.code }}</span>
{% empty %}
<span style="color: var(--efintel-gray); font-size: 0.75rem; font-style: italic;">Ninguna</span>
{% endfor %}
</div>
</td>
<td>{{ resp.profile.description|default:"Sin descripción personal" }}</td>
<td>{{ resp.profile.created_at|date:"d/m/Y"|default:"-" }}</td>
<td>{{ resp.profile.updated_at|date:"d/m/Y H:i"|default:"-" }}</td>
<td>
<div style="display: flex; gap: 0.5rem;">
<a href="{% url 'responsable_edit' resp.id %}" class="btn btn-outline" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;">✏️ Editar</a>
<div style="display: flex; gap: 0.25rem;">
<a href="{% url 'responsable_edit' resp.id %}" class="btn-icon" title="Editar" style="color: var(--efintel-blue); padding: 0.4rem; border-radius: 4px; transition: background 0.2s;">
<i data-lucide="edit-3" style="width: 16px;"></i>
</a>
{% if resp != request.user %}
<form action="{% url 'responsable_delete' resp.id %}" method="POST" style="display: inline;" onsubmit="return confirm('¿Estás seguro de que deseas eliminar este responsable?');">
{% csrf_token %}
<button type="submit" class="btn btn-danger" style="padding: 0.35rem 0.75rem; font-size: 0.8rem;">🗑️ Eliminar</button>
<button type="submit" class="btn-icon" title="Eliminar" style="color: var(--efintel-danger); padding: 0.4rem; border-radius: 4px; transition: background 0.2s; background: transparent; border: none; cursor: pointer;">
<i data-lucide="trash-2" style="width: 16px;"></i>
</button>
</form>
{% endif %}
</div>
@@ -126,21 +383,23 @@
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Pestaña Configuración -->
<div id="tab-config" class="tab-content">
<div style="margin-bottom: 1.5rem;">
<h2>Configuración General del Sistema</h2>
<h2 style="color: var(--efintel-petroleum); margin-bottom: 0.25rem;">Configuración General del Sistema</h2>
<p style="color: var(--efintel-gray); font-size: 0.9rem;">Parámetros y límites globales del ERP.</p>
</div>
<div style="max-width: 500px; display: flex; flex-direction: column; gap: 1.5rem;">
<div class="glass-card" style="padding: 1.5rem; background: rgba(0, 0, 0, 0.2);">
<div class="data-card" style="padding: 2rem;">
<div style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 0.5rem;">Presupuesto General Asignado</div>
<div style="font-size: 2rem; font-weight: 700; color: var(--accent-secondary);">${{ settings.global_budget|default:"100000.00" }}</div>
<div style="font-size: 2.5rem; font-weight: 700; color: var(--efintel-petroleum);">${{ settings.global_budget|floatformat:"0"|intcomma|default:"100000" }}</div>
</div>
<div>
<a href="{% url 'settings_edit' %}" class="btn btn-primary">✏️ Modificar Presupuesto General</a>
<a href="{% url 'settings_edit' %}" class="btn-primary" style="text-decoration: none;">Modificar Presupuesto General</a>
</div>
</div>
</div>
@@ -148,20 +407,20 @@
<!-- Pestaña Bitácora -->
<div id="tab-bitacora" class="tab-content">
<div style="margin-bottom: 1.5rem;">
<h2>Bitácora de Auditoría y Trazabilidad (Logs)</h2>
<h2 style="color: var(--efintel-petroleum); margin-bottom: 0.25rem;">Bitácora de Auditoría y Trazabilidad (Logs)</h2>
<p style="color: var(--text-secondary); font-size: 0.9rem;">Historial detallado de operaciones realizadas por los administradores en el sistema.</p>
</div>
<div class="log-list">
<div class="data-card" style="padding: 1.5rem;">
{% for log in logs %}
<div class="log-item">
<div class="log-info">
<div class="log-action">
{% if "Creado" in log.action or "Creación" in log.action %}
🟢 {{ log.action }}
{{ log.action }}
{% elif "Eliminado" in log.action or "Eliminación" in log.action %}
🔴 {{ log.action }}
{{ log.action }}
{% else %}
🟡 {{ log.action }}
{{ log.action }}
{% endif %}
</div>
<div class="log-desc">{{ log.description }}</div>
@@ -176,7 +435,8 @@
{% endfor %}
</div>
</div>
</div>
</div> <!-- End Main Content Area -->
</div> <!-- End Gestion Layout -->
{% endblock %}
{% block extra_js %}
@@ -187,7 +447,7 @@
contents.forEach(content => content.classList.remove('active'));
// Desactivar todos los botones de pestaña
const buttons = document.querySelectorAll('.tab-btn');
const buttons = document.querySelectorAll('.sidebar-btn');
buttons.forEach(btn => btn.classList.remove('active'));
// Activar pestaña actual
@@ -202,7 +462,7 @@
document.addEventListener("DOMContentLoaded", function() {
const activeTab = localStorage.getItem('activeTab');
if (activeTab && document.getElementById(activeTab)) {
const btn = Array.from(document.querySelectorAll('.tab-btn')).find(b => b.getAttribute('onclick').includes(activeTab));
const btn = Array.from(document.querySelectorAll('.sidebar-btn')).find(b => b.getAttribute('onclick').includes(activeTab));
if (btn) {
// Simular clic en el botón
btn.click();
+76 -20
View File
@@ -1,42 +1,98 @@
{% extends 'base.html' %}
{% block content %}
<div class="form-container">
<div class="glass-card" style="padding: 2.5rem;">
<h1 style="margin-bottom: 2rem;">
{% if form.instance.pk %}Editar Proyecto{% else %}Nuevo Proyecto{% endif %}
</h1>
<div class="form-container" style="max-width: 800px; margin: 0 auto;">
<div class="data-card" style="padding: 2.5rem; background: white; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);">
<h2 style="color: var(--efintel-petroleum); margin-bottom: 2rem; border-bottom: 2px solid var(--efintel-light); padding-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
<i data-lucide="{% if form.instance.pk %}edit-3{% else %}plus-circle{% endif %}"></i>
{% if form.instance.pk %}Editar Proyecto o Centro de Costo{% else %}Nuevo Proyecto o Centro de Costo{% endif %}
</h2>
<form method="post">
{% csrf_token %}
<div class="form-group">
<label for="{{ form.name.id_for_label }}">Nombre del Proyecto</label>
{{ form.name }}
{% if form.name.errors %}
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.name.errors }}</div>
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem; margin-bottom: 1.5rem;">
<div class="form-group">
<label for="{{ form.code.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Código</label>
{{ form.code }}
{% if form.code.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.code.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<label for="{{ form.name.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Nombre del Proyecto / Centro</label>
{{ form.name }}
{% if form.name.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.name.errors }}</div>
{% endif %}
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
<div class="form-group">
<label for="{{ form.type.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Tipo de Centro</label>
{{ form.type }}
{% if form.type.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.type.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<label for="{{ form.assigned_budget_clp.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Presupuesto Asignado (CLP)</label>
{{ form.assigned_budget_clp }}
{% if form.assigned_budget_clp.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.assigned_budget_clp.errors }}</div>
{% endif %}
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
<div class="form-group">
<label for="{{ form.category.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Categoría del Proyecto (Especialidad)</label>
{{ form.category }}
{% if form.category.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.category.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<label for="{{ form.status.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Estado del Proyecto</label>
{{ form.status }}
{% if form.status.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.status.errors }}</div>
{% endif %}
</div>
</div>
<div class="form-group" style="margin-bottom: 1.5rem;">
<label for="{{ form.parent.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Portafolio Padre (Opcional)</label>
<p style="font-size: 0.75rem; color: var(--text-secondary); margin-bottom: 0.25rem;">Dejar en blanco si este proyecto actúa como un Portafolio principal.</p>
{{ form.parent }}
{% if form.parent.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.parent.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<label for="{{ form.description.id_for_label }}">Descripción</label>
<div class="form-group" style="margin-bottom: 1.5rem;">
<label for="{{ form.description.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Descripción</label>
{{ form.description }}
{% if form.description.errors %}
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.description.errors }}</div>
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.description.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<label for="{{ form.manager.id_for_label }}">Encargado / Responsable del Proyecto</label>
<div class="form-group" style="margin-bottom: 2rem;">
<label for="{{ form.manager.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Encargado / Responsable del Proyecto</label>
{{ form.manager }}
{% if form.manager.errors %}
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.manager.errors }}</div>
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.manager.errors }}</div>
{% endif %}
</div>
<div style="display: flex; gap: 1rem; margin-top: 2rem;">
<button type="submit" class="btn btn-primary">Guardar</button>
<a href="{% url 'gestion_panel' %}" class="btn btn-outline">Cancelar</a>
<div style="display: flex; gap: 1rem; padding-top: 1.5rem; border-top: 1px solid var(--efintel-light);">
<button type="submit" class="btn-primary" style="padding: 0.6rem 2rem; font-size: 0.9rem;">Guardar Proyecto</button>
<a href="{% url 'gestion_panel' %}" class="btn-secondary" style="padding: 0.6rem 2rem; font-size: 0.9rem; text-decoration: none;">Cancelar</a>
</div>
</form>
</div>
+24 -11
View File
@@ -1,8 +1,13 @@
{% extends 'base.html' %}
{% load humanize %}
{% block content %}
<div style="margin-bottom: 2rem;">
<h1>Proyectos</h1>
<div class="view-header">
<div class="view-title">
<h2>Proyectos</h2>
<p>Monitoreo de Centros de Costo</p>
</div>
<a href="{% url 'project_create' %}" class="btn-primary">Nuevo Proyecto / Centro</a>
</div>
<div class="project-grid">
@@ -15,15 +20,23 @@
<strong>Gestor:</strong> {{ project.manager.username|default:"No asignado" }}
</div>
<div class="project-stats">
<div style="display: flex; flex-direction: column;">
<span style="font-size: 0.8rem; color: var(--text-secondary);">Gastos</span>
<span style="font-weight: 600; font-size: 1.1rem;">{{ project.expense_count|default:"0" }}</span>
</div>
<div style="display: flex; flex-direction: column; text-align: right;">
<span style="font-size: 0.8rem; color: var(--text-secondary);">Total</span>
<span style="font-weight: 600; font-size: 1.1rem; color: var(--accent-secondary);">
${{ project.total_expenses|default:"0.00" }}
</span>
<div style="display: flex; flex-direction: column; gap: 0.5rem; width: 100%;">
<div style="display: flex; justify-content: space-between;">
<span style="font-size: 0.8rem; color: var(--efintel-gray);">Presupuesto Asignado</span>
<span style="font-weight: 700; font-family: var(--font-mono); color: var(--efintel-petroleum);">
${{ project.assigned_budget_clp|floatformat:"0"|intcomma }}
</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span style="font-size: 0.8rem; color: var(--efintel-gray);">Costo Real Acumulado</span>
<span style="font-weight: 700; font-family: var(--font-mono); color: var(--efintel-danger);">
${{ project.actual_cost_clp|floatformat:"0"|intcomma }}
</span>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid var(--efintel-border);">
<span style="font-size: 0.8rem; color: var(--efintel-gray);">Nº Erogaciones (Facturas)</span>
<span style="font-weight: 700;">{{ project.expense_count|default:"0" }}</span>
</div>
</div>
</div>
</div>
+81 -44
View File
@@ -1,71 +1,108 @@
{% extends 'base.html' %}
{% block content %}
<div class="form-container">
<div class="glass-card" style="padding: 2.5rem;">
<h1 style="margin-bottom: 2rem;">
<div class="form-container" style="max-width: 800px; margin: 0 auto;">
<div class="data-card" style="padding: 2.5rem; background: white; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);">
<h2 style="color: var(--efintel-petroleum); margin-bottom: 2rem; border-bottom: 2px solid var(--efintel-light); padding-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
<i data-lucide="{% if edit_mode %}edit-3{% else %}plus-circle{% endif %}"></i>
{% if edit_mode %}Editar Colaborador (Responsable): {{ target_user.username }}{% else %}Nuevo Colaborador (Responsable){% endif %}
</h1>
</h2>
<form method="post">
{% csrf_token %}
<div class="form-group">
<label for="{{ form.username.id_for_label }}">Nombre de Usuario</label>
{{ form.username }}
{% if form.username.errors %}
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.username.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<label for="{{ form.email.id_for_label }}">Correo Electrónico</label>
{{ form.email }}
{% if form.email.errors %}
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.email.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<label for="{{ form.password.id_for_label }}">
Contraseña
{% if edit_mode %}
<span style="font-size: 0.8rem; color: var(--text-secondary);">(Opcional: Dejar en blanco para no cambiarla)</span>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
<div class="form-group">
<label for="{{ form.username.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Nombre de Usuario</label>
{{ form.username }}
{% if form.username.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.username.errors }}</div>
{% endif %}
</label>
{{ form.password }}
{% if form.password.errors %}
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.password.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<label for="{{ form.email.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Correo Electrónico</label>
{{ form.email }}
{% if form.email.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.email.errors }}</div>
{% endif %}
</div>
</div>
<div class="form-group">
<label for="{{ form.role.id_for_label }}">Rol / Grupo del Sistema</label>
{{ form.role }}
{% if form.role.errors %}
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.role.errors }}</div>
{% endif %}
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
<div class="form-group">
<label for="{{ form.password.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">
Contraseña
{% if edit_mode %}
<span style="font-size: 0.75rem; color: var(--efintel-gray); font-weight: 400;">(Opcional)</span>
{% endif %}
</label>
{{ form.password }}
{% if form.password.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.password.errors }}</div>
{% endif %}
</div>
<div class="form-group">
<label for="{{ form.role.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Rol / Grupo del Sistema</label>
{{ form.role }}
{% if form.role.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.role.errors }}</div>
{% endif %}
</div>
</div>
<div class="form-group">
<label for="{{ form.description.id_for_label }}">Descripción / Bio del Responsable</label>
<div class="form-group" style="margin-bottom: 2rem;">
<label for="{{ form.description.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Descripción / Bio del Responsable</label>
{{ form.description }}
{% if form.description.errors %}
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.description.errors }}</div>
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.description.errors }}</div>
{% endif %}
</div>
<div style="display: flex; gap: 1rem; margin-top: 2rem;">
<button type="submit" class="btn btn-primary">Guardar</button>
<a href="{% url 'gestion_panel' %}" class="btn btn-outline">Cancelar</a>
<div class="form-group" style="margin-bottom: 2rem;">
<label style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Especialidades Técnicas (Categorías Asociadas)</label>
<p style="font-size: 0.75rem; color: var(--efintel-gray); margin-bottom: 0.75rem;">Selecciona las áreas de especialidad de este colaborador en la cartera técnica.</p>
<div class="specialties-container" style="max-height: 220px; overflow-y: auto; padding: 1rem; border: 1px solid var(--efintel-border); border-radius: 4px; background: #f8fafc;">
<style>
.specialties-container ul {
list-style: none;
padding: 0;
margin: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
gap: 0.75rem;
}
.specialties-container li {
display: flex;
align-items: flex-start;
gap: 0.4rem;
font-size: 0.8rem;
color: var(--efintel-dark);
}
.specialties-container input[type="checkbox"] {
margin-top: 0.2rem;
cursor: pointer;
}
</style>
{{ form.specialties }}
</div>
{% if form.specialties.errors %}
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.specialties.errors }}</div>
{% endif %}
</div>
<div style="display: flex; gap: 1rem; padding-top: 1.5rem; border-top: 1px solid var(--efintel-light);">
<button type="submit" class="btn-primary" style="padding: 0.6rem 2rem; font-size: 0.9rem;">Guardar Responsable</button>
<a href="{% url 'gestion_panel' %}" class="btn-secondary" style="padding: 0.6rem 2rem; font-size: 0.9rem; text-decoration: none;">Cancelar</a>
</div>
</form>
</div>
</div>
<script>
// Apply form-control class to all inputs
document.querySelectorAll('input, select, textarea').forEach(el => {
// Apply form-control class to all inputs except checkboxes
document.querySelectorAll('input:not([type="checkbox"]), select, textarea').forEach(el => {
el.classList.add('form-control');
});
</script>
+85
View File
@@ -0,0 +1,85 @@
{% extends 'base.html' %}
{% block content %}
<div class="form-container" style="max-width: 800px; margin: 0 auto;">
<div class="data-card" style="padding: 2.5rem; background: white; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);">
<h2 style="color: var(--efintel-petroleum); margin-bottom: 2rem; border-bottom: 2px solid var(--efintel-light); padding-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
<i data-lucide="{% if form.instance.pk %}edit-3{% else %}plus-circle{% endif %}"></i>
{% if form.instance.pk %}Editar Proveedor{% else %}Nuevo Proveedor{% endif %}
</h2>
<form method="post">
{% csrf_token %}
<h3 style="margin-bottom: 1rem; color: var(--efintel-dark); font-size: 1rem;">Información General</h3>
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem; margin-bottom: 1.5rem;">
<div class="form-group">
<label for="{{ form.rut.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">RUT Proveedor</label>
{{ form.rut }}
{% if form.rut.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.rut.errors }}</div>{% endif %}
</div>
<div class="form-group">
<label for="{{ form.name.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Razón Social / Nombre</label>
{{ form.name }}
{% if form.name.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.name.errors }}</div>{% endif %}
</div>
</div>
<div class="form-group" style="margin-bottom: 2rem;">
<label for="{{ form.business_line.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Giro Comercial</label>
{{ form.business_line }}
{% if form.business_line.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.business_line.errors }}</div>{% endif %}
</div>
<h3 style="margin-top: 2.5rem; margin-bottom: 1rem; color: var(--efintel-dark); font-size: 1rem; border-top: 1px solid var(--efintel-light); padding-top: 1.5rem;">Datos Bancarios y Facturación</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
<div class="form-group">
<label for="{{ form.bank.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Banco</label>
{{ form.bank }}
{% if form.bank.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.bank.errors }}</div>{% endif %}
</div>
<div class="form-group">
<label for="{{ form.account_type.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Tipo de Cuenta</label>
{{ form.account_type }}
{% if form.account_type.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.account_type.errors }}</div>{% endif %}
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
<div class="form-group">
<label for="{{ form.account_number.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Número de Cuenta</label>
{{ form.account_number }}
{% if form.account_number.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.account_number.errors }}</div>{% endif %}
</div>
<div class="form-group">
<label for="{{ form.email.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Correo de Pago/Facturación</label>
{{ form.email }}
{% if form.email.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.email.errors }}</div>{% endif %}
</div>
</div>
<div class="form-group" style="margin-bottom: 2rem; width: 50%;">
<label for="{{ form.payment_terms.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Plazo de Pago (Días)</label>
{{ form.payment_terms }}
{% if form.payment_terms.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.payment_terms.errors }}</div>{% endif %}
</div>
<div style="display: flex; gap: 1rem; padding-top: 1.5rem; border-top: 1px solid var(--efintel-light);">
<button type="submit" class="btn-primary" style="padding: 0.6rem 2rem; font-size: 0.9rem;">Guardar Proveedor</button>
<a href="{% url 'gestion_panel' %}" class="btn-secondary" style="padding: 0.6rem 2rem; font-size: 0.9rem; text-decoration: none;">Cancelar</a>
</div>
</form>
</div>
</div>
<script>
document.querySelectorAll('input, select, textarea').forEach(el => {
el.classList.add('form-control');
});
</script>
{% endblock %}