Actualizar contabilidad.html

This commit is contained in:
2026-03-01 21:32:13 +00:00
parent bd1baa0dd1
commit 062a78df2b

View File

@@ -6,6 +6,7 @@
<title>Contabilidad - IntegraReparaPro</title> <title>Contabilidad - IntegraReparaPro</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script> <script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<style> <style>
.fade-in { animation: fadeIn 0.3s ease-in-out; } .fade-in { animation: fadeIn 0.3s ease-in-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } } @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
@@ -244,10 +245,70 @@
</div> </div>
</div> </div>
</div> </div>
<div id="pdf-wrapper" class="hidden">
<div id="pdf-content" class="p-10 bg-white text-slate-800 font-sans" style="width: 800px; min-height: 1120px; position: relative;">
<div class="flex gap-2"> <div class="flex justify-between items-start border-b-2 border-slate-200 pb-6 mb-8">
<button onclick="document.getElementById('convertModal').classList.add('hidden')" class="flex-1 bg-slate-100 text-slate-600 font-bold py-3 rounded-xl hover:bg-slate-200 transition-colors">Cancelar</button> <div class="flex items-center gap-4">
<button onclick="confirmConversion()" class="flex-1 bg-emerald-500 text-white font-black uppercase tracking-widest shadow-md py-3 rounded-xl hover:bg-emerald-600 transition-colors">Procesar</button> <img id="pdf-company-logo" src="" alt="Logo Empresa" class="h-16 w-auto hidden object-contain">
<div>
<h1 class="text-3xl font-black text-slate-800 tracking-tight" id="pdf-company-name">IntegraRepara</h1>
<p class="text-xs text-slate-500 font-bold mt-1 tracking-widest uppercase">Presupuesto de Servicios</p>
</div>
</div>
<div class="text-right">
<p class="text-2xl font-black text-blue-600 tracking-widest" id="pdf-budget-id">#PRE-000</p>
<p class="text-sm font-bold text-slate-500 mt-1" id="pdf-date">Fecha: 01/01/2026</p>
</div>
</div>
<div class="mb-10 bg-slate-50 p-6 rounded-2xl border border-slate-100">
<h3 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-3">Datos del Cliente</h3>
<p class="font-black text-xl text-slate-800 mb-1" id="pdf-client-name">Nombre Cliente</p>
<p class="text-sm font-bold text-slate-600 mb-1"><i data-lucide="phone" class="w-3 h-3 inline mr-1"></i> <span id="pdf-client-phone">Teléfono</span></p>
<p class="text-sm font-bold text-slate-600"><i data-lucide="map-pin" class="w-3 h-3 inline mr-1"></i> <span id="pdf-client-address">Dirección</span></p>
</div>
<table class="w-full mb-10 text-left border-collapse">
<thead>
<tr class="border-b-2 border-slate-200 text-xs font-black uppercase tracking-widest text-slate-400">
<th class="py-3 px-2 w-1/2">Concepto</th>
<th class="py-3 px-2 text-center">Cantidad</th>
<th class="py-3 px-2 text-right">Precio Ud.</th>
<th class="py-3 px-2 text-right">Total</th>
</tr>
</thead>
<tbody id="pdf-items" class="text-sm font-bold text-slate-700 divide-y divide-slate-100">
</tbody>
</table>
<div class="flex justify-end mb-10">
<div class="w-1/2 bg-slate-50 p-6 rounded-2xl border border-slate-100">
<div class="flex justify-between py-2">
<span class="font-bold text-slate-500 text-sm uppercase tracking-widest">Subtotal</span>
<span class="font-black text-slate-700" id="pdf-subtotal">0.00€</span>
</div>
<div class="flex justify-between py-2 border-b border-slate-200">
<span class="font-bold text-slate-500 text-sm uppercase tracking-widest">IVA (21%)</span>
<span class="font-black text-amber-600" id="pdf-tax">0.00€</span>
</div>
<div class="flex justify-between py-4 text-xl font-black text-slate-800">
<span class="uppercase tracking-widest">TOTAL A PAGAR</span>
<span id="pdf-total" class="text-emerald-600 text-2xl">0.00€</span>
</div>
</div>
</div>
<div class="absolute bottom-10 left-10 right-10">
<div id="pdf-bank-info" class="mb-4 p-4 bg-blue-50 text-blue-800 rounded-xl text-xs font-bold border border-blue-100 hidden">
Para confirmar el presupuesto, puede realizar una transferencia o bizum a la siguiente cuenta:<br>
<span class="text-sm font-black tracking-widest mt-1 block" id="pdf-bank-account">ES00 0000 0000 0000 0000</span>
</div>
<div class="pt-4 border-t border-slate-200 text-center text-xs font-bold text-slate-400">
Documento generado automáticamente por el sistema. Este documento tiene validez como presupuesto inicial.
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -256,9 +317,6 @@
<script src="js/layout.js"></script> <script src="js/layout.js"></script>
<script> <script>
// ==========================================
// LÓGICA DE COBROS (CONTABILIDAD)
// ==========================================
let allFinancials = []; let allFinancials = [];
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
@@ -431,9 +489,6 @@
finally { amountInput.disabled = false; methodInput.disabled = false; } finally { amountInput.disabled = false; methodInput.disabled = false; }
} }
// ==========================================
// LÓGICA DE PESTAÑAS Y PRESUPUESTOS
// ==========================================
function toggleTab(tab) { function toggleTab(tab) {
const vCobros = document.getElementById('view-cobros'); const vCobros = document.getElementById('view-cobros');
const vPres = document.getElementById('view-presupuestos'); const vPres = document.getElementById('view-presupuestos');
@@ -471,7 +526,7 @@
} catch(e) {} } catch(e) {}
} }
function renderBudgets() { function renderBudgets() {
const list = document.getElementById('budgetsList'); const list = document.getElementById('budgetsList');
list.innerHTML = ""; list.innerHTML = "";
if(myBudgets.length === 0) { list.innerHTML = `<div class="p-6 text-center text-slate-400">Sin presupuestos</div>`; return; } if(myBudgets.length === 0) { list.innerHTML = `<div class="p-6 text-center text-slate-400">Sin presupuestos</div>`; return; }
@@ -480,7 +535,6 @@
const date = new Date(b.created_at).toLocaleDateString('es-ES'); const date = new Date(b.created_at).toLocaleDateString('es-ES');
let bStatus = ''; let bStatus = '';
// Comprobamos si nos viene el estado del servicio como anulado desde el backend
let isAnulado = false; let isAnulado = false;
if (b.linked_service_status_name && b.linked_service_status_name.toLowerCase().includes('anulado')) { if (b.linked_service_status_name && b.linked_service_status_name.toLowerCase().includes('anulado')) {
isAnulado = true; isAnulado = true;
@@ -511,7 +565,10 @@
actions = `<button onclick="openConvertModal(${b.id})" class="bg-emerald-500 text-white px-3 py-1.5 rounded font-black text-[10px] uppercase tracking-widest shadow-md hover:bg-emerald-600">Crear Servicio</button>`; actions = `<button onclick="openConvertModal(${b.id})" class="bg-emerald-500 text-white px-3 py-1.5 rounded font-black text-[10px] uppercase tracking-widest shadow-md hover:bg-emerald-600">Crear Servicio</button>`;
} }
// La papelera de borrar se muestra siempre, y le pasamos el estado 'isAnulado' // Botón de PDF
actions += `<button onclick="generatePDF(${b.id})" class="text-slate-400 hover:text-blue-600 p-2 ml-2 transition-colors" title="Descargar PDF"><i data-lucide="file-text" class="w-4 h-4"></i></button>`;
// Botón de Borrar
actions += `<button onclick="deleteBudget(${b.id}, '${b.status}', ${isAnulado})" class="text-slate-300 hover:text-red-500 p-2 ml-2 transition-colors" title="Borrar Presupuesto"><i data-lucide="trash-2" class="w-4 h-4"></i></button>`; actions += `<button onclick="deleteBudget(${b.id}, '${b.status}', ${isAnulado})" class="text-slate-300 hover:text-red-500 p-2 ml-2 transition-colors" title="Borrar Presupuesto"><i data-lucide="trash-2" class="w-4 h-4"></i></button>`;
list.innerHTML += ` list.innerHTML += `
@@ -531,12 +588,16 @@
lucide.createIcons(); lucide.createIcons();
} }
async function updateBudgetStatus(id, status) {
if(!confirm("¿Actualizar estado?")) return;
await fetch(`${API_URL}/budgets/${id}/status`, { method: 'PATCH', headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, body: JSON.stringify({status}) });
loadBudgets();
}
async function deleteBudget(id, status, isAnulado) { async function deleteBudget(id, status, isAnulado) {
// Si está convertido/aceptado, Y NO ESTÁ anulado, bloqueamos
if ((status === 'accepted' || status === 'converted') && !isAnulado) { if ((status === 'accepted' || status === 'converted') && !isAnulado) {
return showToast("⚠️ Para borrar el presupuesto, el servicio vinculado debe estar Anulado."); return showToast("⚠️ Para borrar el presupuesto, el servicio vinculado debe estar Anulado.");
} }
if (!confirm("¿Seguro que quieres borrar este presupuesto definitivamente?")) return; if (!confirm("¿Seguro que quieres borrar este presupuesto definitivamente?")) return;
try { try {
@@ -545,60 +606,39 @@
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
}); });
const data = await res.json(); const data = await res.json();
if (data.ok) { if (data.ok) {
showToast("🗑️ Presupuesto eliminado"); showToast("🗑️ Presupuesto eliminado");
loadBudgets(); loadBudgets();
} else { } else { showToast("❌ " + (data.error || "Error al borrar")); }
showToast("❌ " + (data.error || "Error al borrar")); } catch (e) { showToast("❌ Error de conexión"); }
}
} catch (e) {
showToast("❌ Error de conexión");
}
} }
// ==========================================
// LÓGICA DE CONVERSIÓN A SERVICIO (MODAL)
// ==========================================
async function openConvertModal(id) { async function openConvertModal(id) {
document.getElementById('convBudgetId').value = id; document.getElementById('convBudgetId').value = id;
// 1. Limpiamos y cargamos los gremios directamente de la base de datos
const gSel = document.getElementById('convGuild'); const gSel = document.getElementById('convGuild');
gSel.innerHTML = '<option value="">Cargando gremios...</option>'; gSel.innerHTML = '<option value="">Cargando gremios...</option>';
try { try {
const res = await fetch(`${API_URL}/guilds`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } }); const res = await fetch(`${API_URL}/guilds`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json(); const data = await res.json();
gSel.innerHTML = '<option value="">Selecciona Gremio...</option>'; gSel.innerHTML = '<option value="">Selecciona Gremio...</option>';
if (data.ok && data.guilds) { if (data.ok && data.guilds) {
data.guilds.forEach(g => { data.guilds.forEach(g => { gSel.innerHTML += `<option value="${g.id}">${g.name}</option>`; });
gSel.innerHTML += `<option value="${g.id}">${g.name}</option>`;
});
} }
} catch (e) { } catch (e) { gSel.innerHTML = '<option value="">Error al cargar</option>'; }
gSel.innerHTML = '<option value="">Error al cargar</option>';
}
// 2. Reseteamos el resto del formulario
document.getElementById('convOperator').innerHTML = '<option value="AUTO">⚡ MANDAR A LA BOLSA (AUTO)</option>'; document.getElementById('convOperator').innerHTML = '<option value="AUTO">⚡ MANDAR A LA BOLSA (AUTO)</option>';
document.getElementById('convDateContainer').classList.add('hidden'); document.getElementById('convDateContainer').classList.add('hidden');
document.getElementById('convDate').value = ""; document.getElementById('convDate').value = "";
document.getElementById('convTime').value = ""; document.getElementById('convTime').value = "";
document.getElementById('convertModal').classList.remove('hidden'); document.getElementById('convertModal').classList.remove('hidden');
} }
async function loadOpsForGuildConv(guildId) { async function loadOpsForGuildConv(guildId) {
const sel = document.getElementById('convOperator'); const sel = document.getElementById('convOperator');
const dateCont = document.getElementById('convDateContainer'); const dateCont = document.getElementById('convDateContainer');
sel.innerHTML = '<option value="AUTO">⚡ MANDAR A LA BOLSA (AUTO)</option>'; sel.innerHTML = '<option value="AUTO">⚡ MANDAR A LA BOLSA (AUTO)</option>';
// Ocultamos la fecha si cambiamos de gremio (vuelve a Auto por defecto)
dateCont.classList.add('hidden'); dateCont.classList.add('hidden');
document.getElementById('convDate').value = ""; document.getElementById('convDate').value = ""; document.getElementById('convTime').value = "";
document.getElementById('convTime').value = "";
if(!guildId) return; if(!guildId) return;
@@ -606,60 +646,42 @@
const res = await fetch(`${API_URL}/operators?guild_id=${guildId}`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } }); const res = await fetch(`${API_URL}/operators?guild_id=${guildId}`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json(); const data = await res.json();
if(data.ok && data.operators) { if(data.ok && data.operators) {
data.operators.forEach(op => { data.operators.forEach(op => { sel.innerHTML += `<option value="${op.id}">👤 ${op.full_name}</option>`; });
sel.innerHTML += `<option value="${op.id}">👤 ${op.full_name}</option>`;
});
} }
} catch(e) {} } catch(e) {}
} }
function handleOperatorChange(val) { function handleOperatorChange(val) {
const dateCont = document.getElementById('convDateContainer'); const dateCont = document.getElementById('convDateContainer');
if(val === 'AUTO') { if(val === 'AUTO') dateCont.classList.add('hidden');
dateCont.classList.add('hidden'); else dateCont.classList.remove('hidden');
} else {
// Si elige un técnico concreto, mostramos los campos de fecha
dateCont.classList.remove('hidden');
}
} }
async function confirmConversion() { async function confirmConversion() {
const id = document.getElementById('convBudgetId').value; const id = document.getElementById('convBudgetId').value;
const guild_id = document.getElementById('convGuild').value; const guild_id = document.getElementById('convGuild').value;
const assigned_to = document.getElementById('convOperator').value; const assigned_to = document.getElementById('convOperator').value;
const date = document.getElementById('convDate').value; const date = document.getElementById('convDate').value;
const time = document.getElementById('convTime').value; const time = document.getElementById('convTime').value;
if (!guild_id) return showToast("⚠️ El Gremio es obligatorio."); if (!guild_id) return showToast("⚠️ El Gremio es obligatorio.");
const use_automation = (assigned_to === 'AUTO'); const use_automation = (assigned_to === 'AUTO');
if (!use_automation && (!date || !time)) return showToast("⚠️ Si asignas a un técnico, debes fijar Fecha y Hora.");
if (!use_automation && (!date || !time)) {
return showToast("⚠️ Si asignas a un técnico, debes fijar Fecha y Hora.");
}
const payload = { const payload = {
guild_id: guild_id, guild_id: guild_id, assigned_to: use_automation ? null : assigned_to,
assigned_to: use_automation ? null : assigned_to, use_automation: use_automation, date: use_automation ? null : date, time: use_automation ? null : time
use_automation: use_automation,
date: use_automation ? null : date,
time: use_automation ? null : time
}; };
try { try {
await fetch(`${API_URL}/budgets/${id}/convert`, { await fetch(`${API_URL}/budgets/${id}/convert`, {
method: 'POST', method: 'POST', headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
document.getElementById('convertModal').classList.add('hidden'); document.getElementById('convertModal').classList.add('hidden');
showToast(use_automation ? "✅ ¡Enviado a la bolsa con éxito!" : "✅ ¡Servicio agendado con éxito!"); showToast(use_automation ? "✅ ¡Enviado a la bolsa con éxito!" : "✅ ¡Servicio agendado con éxito!");
loadBudgets(); loadBudgets();
} catch(e) { } catch(e) { showToast("❌ Error al convertir el presupuesto."); }
showToast("❌ Error al convertir el presupuesto.");
}
} }
function openBudgetModal() { function openBudgetModal() {
@@ -796,6 +818,93 @@
} }
} }
async function generatePDF(id) {
const budget = myBudgets.find(b => b.id === id);
if(!budget) return showToast("❌ Error: Presupuesto no encontrado");
showToast("⏳ Generando PDF, espera unos segundos...");
let companyName = "IntegraRepara";
let companyLogo = null;
let bankAccount = null;
try {
const confRes = await fetch(`${API_URL}/config/company`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const confData = await confRes.json();
if (confData.ok && confData.config) {
companyName = confData.config.full_name || companyName;
companyLogo = confData.config.company_logo;
bankAccount = confData.config.portal_settings ? confData.config.portal_settings.bank_account : null;
}
} catch(e) {}
document.getElementById('pdf-company-name').innerText = companyName;
const logoEl = document.getElementById('pdf-company-logo');
if (companyLogo) {
logoEl.src = companyLogo;
logoEl.classList.remove('hidden');
} else {
logoEl.classList.add('hidden');
}
document.getElementById('pdf-budget-id').innerText = `#PRE-${budget.id}`;
document.getElementById('pdf-date').innerText = `Fecha: ${new Date(budget.created_at).toLocaleDateString('es-ES')}`;
document.getElementById('pdf-client-name').innerText = budget.client_name || "Cliente Sin Nombre";
document.getElementById('pdf-client-phone').innerText = budget.client_phone || "-";
document.getElementById('pdf-client-address').innerText = budget.client_address || "-";
const itemsHtml = (budget.items || []).map(item => `
<tr class="hover:bg-slate-50 transition-colors">
<td class="py-4 px-2 border-b border-slate-50">${item.concept}</td>
<td class="py-4 px-2 border-b border-slate-50 text-center">${item.qty}</td>
<td class="py-4 px-2 border-b border-slate-50 text-right">${parseFloat(item.price).toFixed(2)} €</td>
<td class="py-4 px-2 border-b border-slate-50 text-right font-black">${(item.qty * item.price).toFixed(2)} €</td>
</tr>
`).join('');
document.getElementById('pdf-items').innerHTML = itemsHtml;
document.getElementById('pdf-subtotal').innerText = parseFloat(budget.subtotal).toFixed(2) + " €";
document.getElementById('pdf-tax').innerText = parseFloat(budget.tax).toFixed(2) + " €";
document.getElementById('pdf-total').innerText = parseFloat(budget.total).toFixed(2) + " €";
const bankContainer = document.getElementById('pdf-bank-info');
if (bankAccount) {
document.getElementById('pdf-bank-account').innerText = bankAccount;
bankContainer.classList.remove('hidden');
} else {
bankContainer.classList.add('hidden');
}
lucide.createIcons();
const wrapper = document.getElementById('pdf-wrapper');
wrapper.classList.remove('hidden');
wrapper.style.position = 'absolute';
wrapper.style.left = '-9999px';
const element = document.getElementById('pdf-content');
const opt = {
margin: 0,
filename: `Presupuesto_PRE${budget.id}_${budget.client_name.replace(/\s+/g, '_')}.pdf`,
image: { type: 'jpeg', quality: 1 },
html2canvas: { scale: 2, useCORS: true, logging: false },
jsPDF: { unit: 'in', format: 'a4', orientation: 'portrait' }
};
try {
await new Promise(r => setTimeout(r, 300));
await html2pdf().set(opt).from(element).save();
showToast("✅ PDF Descargado con éxito");
} catch (error) {
console.error(error);
showToast("❌ Error al generar el PDF");
} finally {
wrapper.classList.add('hidden');
wrapper.style.position = '';
wrapper.style.left = '';
}
}
function showToast(msg) { function showToast(msg) {
const t = document.getElementById('toast'); const t = document.getElementById('toast');
t.innerHTML = `<i data-lucide="check-circle" class="w-5 h-5 text-emerald-400"></i> ${msg}`; t.innerHTML = `<i data-lucide="check-circle" class="w-5 h-5 text-emerald-400"></i> ${msg}`;