Actualizar presupuestos.html

This commit is contained in:
2026-03-25 08:46:33 +00:00
parent 5b2fa54174
commit de52717c68

View File

@@ -6,6 +6,7 @@
<title>Presupuestos - IntegraRepara</title> <title>Presupuestos - IntegraRepara</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>
/* VARIABLES CORPORATIVAS DINÁMICAS (Igual que en el menú) */ /* VARIABLES CORPORATIVAS DINÁMICAS (Igual que en el menú) */
:root { :root {
@@ -109,6 +110,89 @@
</form> </form>
</main> </main>
<div id="pdf-wrapper" class="hidden">
<div id="pdf-content" style="background-color: white; color: #1e293b; font-family: Arial, Helvetica, sans-serif; width: 800px; min-height: 1120px; position: relative; padding: 30px 40px; box-sizing: border-box;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 2px solid #e2e8f0; padding-bottom: 15px; margin-bottom: 20px;">
<div style="max-width: 250px; max-height: 80px;">
<img id="pdf-company-logo" src="" alt="Logo Empresa" style="max-width: 100%; max-height: 80px; object-fit: contain; display: none;">
<h2 id="pdf-company-name-fallback" style="margin:0; color:#2563eb; font-size: 22px; font-weight: 900; display: none;">IntegraRepara</h2>
</div>
<div style="text-align: right; font-size: 11px; color: #64748b; line-height: 1.4;">
<div id="pdf-company-name" style="font-size: 14px; font-weight: 900; color: #0f172a; margin-bottom: 2px;">Empresa S.L.</div>
<div id="pdf-company-dni">CIF: B12345678</div>
<div id="pdf-company-address">Calle Falsa 123</div>
<div id="pdf-company-location">28000 Madrid (Madrid)</div>
</div>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 20px;">
<div>
<h1 style="font-size: 24px; font-weight: 900; color: #2563eb; margin: 0 0 5px 0; letter-spacing: -1px;">PRESUPUESTO</h1>
<div style="font-size: 12px; color: #64748b; line-height: 1.4;">
<strong>Referencia:</strong> <span id="pdf-budget-id">#PRE-000</span><br>
<strong>Fecha:</strong> <span id="pdf-date">01/01/2026</span><br>
<strong>Validez:</strong> 30 días<br>
</div>
</div>
<div style="background-color: #f8fafc; border: 1px solid #e2e8f0; padding: 12px 15px; border-radius: 8px; width: 280px;">
<h3 style="margin: 0 0 5px 0; font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: #94a3b8; font-weight: 800;">Presupuestado a:</h3>
<p id="pdf-client-name" style="font-size: 14px; font-weight: 800; color: #0f172a; margin: 0 0 4px 0;">Nombre Cliente</p>
<p style="font-size: 12px; margin: 0; color: #475569; line-height: 1.4;">
<span id="pdf-client-phone">Teléfono</span><br>
<span id="pdf-client-address">Dirección</span>
</p>
</div>
</div>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
<thead>
<tr>
<th style="background: #f1f5f9; color: #475569; font-weight: 800; text-transform: uppercase; font-size: 10px; padding: 8px 10px; text-align: left; border-bottom: 2px solid #cbd5e1; border-radius: 6px 0 0 0;">Concepto</th>
<th style="background: #f1f5f9; color: #475569; font-weight: 800; text-transform: uppercase; font-size: 10px; padding: 8px 10px; text-align: center; border-bottom: 2px solid #cbd5e1;">Cant.</th>
<th style="background: #f1f5f9; color: #475569; font-weight: 800; text-transform: uppercase; font-size: 10px; padding: 8px 10px; text-align: right; border-bottom: 2px solid #cbd5e1;">Precio Ud.</th>
<th style="background: #f1f5f9; color: #475569; font-weight: 800; text-transform: uppercase; font-size: 10px; padding: 8px 10px; text-align: right; border-bottom: 2px solid #cbd5e1; border-radius: 0 6px 0 0;">Subtotal</th>
</tr>
</thead>
<tbody id="pdf-items">
</tbody>
</table>
<div style="width: 250px; margin-left: auto; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; padding: 8px 15px; font-size: 11px; border-bottom: 1px solid #f1f5f9; color: #475569;">
<span style="font-weight: 600; text-transform: uppercase;">Base Imponible</span>
<span id="pdf-subtotal" style="font-weight: 800; color: #0f172a;">0.00 €</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 8px 15px; font-size: 11px; border-bottom: 1px solid #f1f5f9; color: #475569;">
<span style="font-weight: 600; text-transform: uppercase;">IVA (21%)</span>
<span id="pdf-tax" style="font-weight: 800; color: #0f172a;">0.00 €</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 10px 15px; background: var(--primary); color: white;">
<span style="font-weight: 900; font-size: 13px;">TOTAL A PAGAR</span>
<span id="pdf-total" style="font-weight: 900; font-size: 16px;">0.00 €</span>
</div>
</div>
<div style="position: absolute; bottom: 30px; left: 40px; right: 40px;">
<div id="pdf-bank-info" style="margin-bottom: 10px; padding: 12px 15px; background-color: #f0fdf4; border-left: 4px solid #22c55e; border-radius: 0 6px 6px 0; font-size: 11px; color: #166534; display: none;">
<strong>INFORMACIÓN DE PAGO</strong><br>
Para confirmar el presupuesto, por favor realice una transferencia a la siguiente cuenta:<br>
<span id="pdf-bank-account" style="font-size: 14px; font-weight: 900; letter-spacing: 1px; margin-top: 4px; display: inline-block;">ES00 0000 0000 0000 0000</span>
</div>
<div id="pdf-obs-info" style="margin-bottom: 15px; padding: 12px 15px; background-color: #fefce8; border-left: 4px solid #f59e0b; border-radius: 0 6px 6px 0; font-size: 10px; color: #713f12; display: none;">
<strong>OBSERVACIONES Y CONDICIONES</strong><br>
<span id="pdf-obs-text"></span>
</div>
<div style="text-align: center; font-size: 9px; color: #94a3b8; border-top: 1px solid #e2e8f0; padding-top: 10px;">
Documento generado por IntegraRepara - Sistema de Gestión de Asistencias
</div>
</div>
</div>
</div>
<div id="toast" class="fixed bottom-5 left-1/2 -translate-x-1/2 bg-slate-800 text-white px-6 py-3 rounded-full text-xs font-bold tracking-wide shadow-2xl opacity-0 pointer-events-none transition-opacity z-50 flex items-center gap-2"> <div id="toast" class="fixed bottom-5 left-1/2 -translate-x-1/2 bg-slate-800 text-white px-6 py-3 rounded-full text-xs font-bold tracking-wide shadow-2xl opacity-0 pointer-events-none transition-opacity z-50 flex items-center gap-2">
<i data-lucide="info" class="w-4 h-4"></i> <span id="toastMsg">Mensaje</span> <i data-lucide="info" class="w-4 h-4"></i> <span id="toastMsg">Mensaje</span>
</div> </div>
@@ -238,81 +322,133 @@
} }
} catch (e) { showToast("Error de conexión"); } } catch (e) { showToast("Error de conexión"); }
} }
async function downloadPDF(id) {
// Buscamos el presupuesto en el array de la app móvil
const budget = allBudgets.find(b => b.id === id);
if(!budget) return showToast("❌ Error: Presupuesto no encontrado");
// FUNCIÓN PARA GENERAR Y DESCARGAR PDF BÁSICO showToast("⏳ Generando PDF, espera unos segundos...");
function downloadPDF(id) {
const b = allBudgets.find(x => x.id === id);
if(!b) return;
// Creamos un HTML limpio imprimible // Valores por defecto
let itemsHtml = (b.items || []).map(i => ` let empNombre = "IntegraRepara";
let empDni = "CIF/NIF no configurado";
let empAddress = "Dirección no configurada";
let empCity = "";
let empState = "";
let empZip = "";
let empLogo = null;
let empIban = null;
let empObs = null;
try {
// Descargamos la configuración de tu empresa desde la API
const confRes = await fetch(`${API_URL}/config/company`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const confData = await confRes.json();
if (confData.ok && confData.config) {
empLogo = confData.config.company_logo || null;
if (confData.config.billing_settings) {
const bSet = confData.config.billing_settings;
empNombre = bSet.name || confData.config.full_name || empNombre;
empDni = bSet.dni ? `CIF/NIF: ${bSet.dni}` : empDni;
empAddress = bSet.address || empAddress;
empCity = bSet.city || "";
empState = bSet.state || "";
empZip = bSet.zip || "";
empIban = bSet.iban || null;
empObs = bSet.obs || null;
} else {
// Fallback por si no hay billing config guardada
empNombre = confData.config.full_name || empNombre;
}
}
} catch(e) { console.error("Error al cargar facturación", e); }
// Inyectamos los datos en el diseño
document.getElementById('pdf-company-name').innerText = empNombre;
document.getElementById('pdf-company-dni').innerText = empDni;
document.getElementById('pdf-company-address').innerText = empAddress;
const locText = [empZip, empCity, empState ? `(${empState})` : ''].filter(Boolean).join(' ');
document.getElementById('pdf-company-location').innerText = locText;
const logoImg = document.getElementById('pdf-company-logo');
const logoTxt = document.getElementById('pdf-company-name-fallback');
if (empLogo) {
logoImg.src = empLogo;
logoImg.style.display = 'block';
logoTxt.style.display = 'none';
} else {
logoImg.style.display = 'none';
logoTxt.innerText = empNombre;
logoTxt.style.display = 'block';
}
document.getElementById('pdf-budget-id').innerText = `#PRE-${budget.id}`;
document.getElementById('pdf-date').innerText = 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> <tr>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">${i.concept}</td> <td style="padding: 8px 10px; border-bottom: 1px solid #e2e8f0; font-size: 12px; color: #334155;">${item.concept}</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd; text-align: center;">${i.qty}</td> <td style="padding: 8px 10px; border-bottom: 1px solid #e2e8f0; font-size: 12px; color: #334155; text-align: center;">${item.qty}</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd; text-align: right;">${parseFloat(i.price).toFixed(2)}€</td> <td style="padding: 8px 10px; border-bottom: 1px solid #e2e8f0; font-size: 12px; color: #334155; text-align: right;">${parseFloat(item.price).toFixed(2)} €</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd; text-align: right;">${parseFloat(i.total).toFixed(2)}€</td> <td style="padding: 8px 10px; border-bottom: 1px solid #e2e8f0; font-size: 12px; font-weight: 700; color: #0f172a; text-align: right;">${(item.qty * item.price).toFixed(2)} €</td>
</tr> </tr>
`).join(''); `).join('');
document.getElementById('pdf-items').innerHTML = itemsHtml;
const companyName = localStorage.getItem('userName') || 'Empresa Instaladora'; document.getElementById('pdf-subtotal').innerText = parseFloat(budget.subtotal).toFixed(2) + " €";
const dateStr = new Date(b.created_at).toLocaleDateString('es-ES'); document.getElementById('pdf-tax').innerText = parseFloat(budget.tax).toFixed(2) + " €";
document.getElementById('pdf-total').innerText = parseFloat(budget.total).toFixed(2) + " €";
const printContent = ` const bankContainer = document.getElementById('pdf-bank-info');
<html> if (empIban) {
<head> document.getElementById('pdf-bank-account').innerText = empIban;
<title>Presupuesto PRE-${b.id}</title> bankContainer.style.display = 'block';
<style> } else {
body { font-family: Arial, sans-serif; padding: 40px; color: #333; } bankContainer.style.display = 'none';
.header { display: flex; justify-content: space-between; margin-bottom: 40px; border-bottom: 2px solid #2563eb; padding-bottom: 20px; } }
h1 { color: #2563eb; margin: 0; }
table { w-full; border-collapse: collapse; margin-top: 30px; width: 100%; }
th { background-color: #f8fafc; padding: 10px; text-align: left; border-bottom: 2px solid #ddd; }
.totals { margin-top: 30px; width: 50%; float: right; border-top: 2px solid #333; padding-top: 10px; }
.totals div { display: flex; justify-content: space-between; margin-bottom: 5px; }
.totals .final { font-size: 1.2em; font-weight: bold; color: #2563eb; }
</style>
</head>
<body>
<div class="header">
<div>
<h1>PRESUPUESTO PRE-${b.id}</h1>
<p>Fecha: ${dateStr}</p>
<p>Emitido por: <strong>${companyName}</strong></p>
</div>
<div style="text-align: right;">
<h3>Datos del Cliente</h3>
<p><strong>${b.client_name}</strong></p>
<p>Tel: ${b.client_phone}</p>
<p>${b.client_address || ''}</p>
</div>
</div>
<table>
<thead>
<tr>
<th>Concepto</th>
<th style="text-align:center;">Cant.</th>
<th style="text-align:right;">Precio/U</th>
<th style="text-align:right;">Total</th>
</tr>
</thead>
<tbody>${itemsHtml}</tbody>
</table>
<div class="totals">
<div><span>Subtotal:</span> <span>${parseFloat(b.subtotal).toFixed(2)}€</span></div>
<div><span>IVA (21%):</span> <span>${parseFloat(b.tax).toFixed(2)}€</span></div>
<div class="final"><span>TOTAL:</span> <span>${parseFloat(b.total).toFixed(2)}€</span></div>
</div>
<script>
window.onload = function() { window.print(); }
<\/script>
</body>
</html>
`;
// Abre una ventana nueva oculta y lanza el diálogo de imprimir nativo del móvil (que permite guardar como PDF) const obsContainer = document.getElementById('pdf-obs-info');
const printWindow = window.open('', '_blank'); if (empObs) {
printWindow.document.write(printContent); document.getElementById('pdf-obs-text').innerHTML = empObs.replace(/\n/g, '<br>');
printWindow.document.close(); obsContainer.style.display = 'block';
} else {
obsContainer.style.display = 'none';
}
// Generar PDF con html2pdf
const wrapper = document.getElementById('pdf-wrapper');
wrapper.classList.remove('hidden');
wrapper.style.position = 'absolute';
wrapper.style.left = '-9999px'; // Lo escondemos fuera de la pantalla del móvil
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, 500)); // Esperamos medio segundo para cargar logo
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 = '';
}
} }
// ------------------ RENDERIZAR LA LISTA EN PANTALLA ------------------ // ------------------ RENDERIZAR LA LISTA EN PANTALLA ------------------