Actualizar contabilidad.html
This commit is contained in:
@@ -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); } }
|
||||||
@@ -245,9 +246,69 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div id="pdf-wrapper" class="hidden">
|
||||||
<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 id="pdf-content" class="p-10 bg-white text-slate-800 font-sans" style="width: 800px; min-height: 1120px; position: relative;">
|
||||||
<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>
|
|
||||||
|
<div class="flex justify-between items-start border-b-2 border-slate-200 pb-6 mb-8">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<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}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user