999 lines
64 KiB
HTML
999 lines
64 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Contabilidad - IntegraReparaPro</title>
|
|
<script src="https://cdn.tailwindcss.com"></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>
|
|
.fade-in { animation: fadeIn 0.3s ease-in-out; }
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
|
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50 text-gray-800 font-sans antialiased overflow-hidden">
|
|
|
|
<div class="flex h-screen overflow-hidden text-left">
|
|
<div id="sidebar-container" class="h-full shrink-0"></div>
|
|
|
|
<div class="flex-1 flex flex-col overflow-hidden relative">
|
|
<div id="header-container"></div>
|
|
|
|
<main class="flex-1 overflow-x-hidden overflow-y-auto bg-slate-50/50 p-6 relative">
|
|
<div class="max-w-6xl mx-auto fade-in">
|
|
|
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-end gap-4 mb-6">
|
|
<div>
|
|
<h2 class="text-2xl font-black text-slate-800 flex items-center gap-3">
|
|
<span class="bg-emerald-500 p-2.5 rounded-xl text-white shadow-lg shadow-emerald-200"><i data-lucide="wallet"></i></span>
|
|
Finanzas y Cobros
|
|
</h2>
|
|
<p class="text-sm text-slate-500 mt-1 font-medium">Gestiona la facturación mensual y el estado de los pagos.</p>
|
|
</div>
|
|
|
|
<button class="bg-white border-2 border-indigo-100 text-indigo-400 px-5 py-2.5 rounded-xl font-black flex items-center gap-2 text-xs uppercase tracking-widest cursor-not-allowed opacity-70" title="Próximamente">
|
|
<i data-lucide="bot" class="w-4 h-4"></i> Robot Contable PDF <span class="bg-indigo-100 text-indigo-600 px-1.5 py-0.5 rounded text-[8px] ml-1">BETA</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex gap-2 border-b border-slate-200 mb-6">
|
|
<button onclick="toggleTab('cobros')" id="tab-cobros" class="px-6 py-3 font-black text-sm text-emerald-600 border-b-2 border-emerald-500 flex items-center gap-2 transition-colors">
|
|
<i data-lucide="coins" class="w-4 h-4"></i> Cobros a Clientes/Cías
|
|
</button>
|
|
<button onclick="toggleTab('presupuestos')" id="tab-presupuestos" class="px-6 py-3 font-bold text-sm text-slate-400 border-b-2 border-transparent hover:text-slate-600 transition-colors flex items-center gap-2">
|
|
<i data-lucide="file-spreadsheet" class="w-4 h-4"></i> Presupuestos
|
|
</button>
|
|
</div>
|
|
|
|
<div id="view-cobros">
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
<div class="bg-white p-5 rounded-[1.5rem] border border-slate-200 shadow-sm flex items-center gap-4">
|
|
<div class="w-12 h-12 rounded-full bg-blue-50 text-blue-500 flex items-center justify-center shrink-0"><i data-lucide="sigma"></i></div>
|
|
<div>
|
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Total Facturado</p>
|
|
<p class="text-2xl font-black text-slate-800" id="kpi-total">0.00 €</p>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white p-5 rounded-[1.5rem] border border-slate-200 shadow-sm flex items-center gap-4">
|
|
<div class="w-12 h-12 rounded-full bg-emerald-50 text-emerald-500 flex items-center justify-center shrink-0"><i data-lucide="check-circle-2"></i></div>
|
|
<div>
|
|
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Total Cobrado</p>
|
|
<p class="text-2xl font-black text-emerald-600" id="kpi-paid">0.00 €</p>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white p-5 rounded-[1.5rem] border border-red-100 shadow-sm flex items-center gap-4 relative overflow-hidden">
|
|
<div class="absolute right-0 top-0 bottom-0 w-2 bg-red-400"></div>
|
|
<div class="w-12 h-12 rounded-full bg-red-50 text-red-500 flex items-center justify-center shrink-0"><i data-lucide="clock"></i></div>
|
|
<div>
|
|
<p class="text-[10px] font-black text-red-400 uppercase tracking-widest">Pendiente de Cobro</p>
|
|
<p class="text-2xl font-black text-red-600" id="kpi-pending">0.00 €</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white p-4 rounded-2xl border border-slate-200 shadow-sm mb-6 flex flex-wrap gap-3 items-center">
|
|
<div class="flex-1 min-w-[200px] relative">
|
|
<i data-lucide="search" class="w-4 h-4 absolute left-4 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
|
<input type="text" id="searchBox" oninput="renderList()" placeholder="Buscar por cliente, referencia..." class="w-full pl-11 pr-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-sm focus:ring-2 focus:ring-emerald-500 outline-none transition-all">
|
|
</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
<div class="flex items-center bg-slate-50 border border-slate-200 rounded-xl overflow-hidden">
|
|
<div class="px-3 text-slate-400 border-r border-slate-200"><i data-lucide="calendar-days" class="w-4 h-4"></i></div>
|
|
<input type="month" id="filterMonth" onchange="renderList()" class="bg-transparent text-xs font-black px-3 py-2.5 outline-none text-slate-600 uppercase tracking-widest cursor-pointer">
|
|
</div>
|
|
<select id="filterCompany" onchange="renderList()" class="bg-slate-50 border border-slate-200 text-[10px] font-black px-4 py-2.5 rounded-xl outline-none focus:ring-2 focus:ring-emerald-500 uppercase tracking-widest max-w-[200px]">
|
|
<option value="ALL">TODAS LAS COMPAÑÍAS</option>
|
|
</select>
|
|
<select id="filterStatus" onchange="renderList()" class="bg-slate-50 border border-slate-200 text-[10px] font-black px-4 py-2.5 rounded-xl outline-none focus:ring-2 focus:ring-emerald-500 uppercase tracking-widest">
|
|
<option value="ALL">ESTADO DE COBRO</option>
|
|
<option value="paid">PAGADOS</option>
|
|
<option value="pending">PENDIENTES</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white border border-slate-200 rounded-[2rem] shadow-sm overflow-hidden mb-10">
|
|
<div class="grid grid-cols-12 gap-4 p-4 bg-slate-50 border-b border-slate-100 text-[10px] font-black uppercase tracking-widest text-slate-400">
|
|
<div class="col-span-4 pl-2">Servicio / Cliente / Fecha</div>
|
|
<div class="col-span-2">Compañía</div>
|
|
<div class="col-span-2 text-center">Importe (€)</div>
|
|
<div class="col-span-2">Método de Pago</div>
|
|
<div class="col-span-2 text-center">Estado</div>
|
|
</div>
|
|
<div id="financialList" class="divide-y divide-slate-100">
|
|
<div class="p-10 text-center text-slate-400 flex flex-col items-center">
|
|
<i data-lucide="loader-2" class="w-8 h-8 animate-spin mb-3"></i>
|
|
<span class="font-bold text-sm tracking-widest uppercase">Cargando contabilidad...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="view-presupuestos" class="hidden">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="font-black text-slate-700 text-lg">Listado de Presupuestos</h3>
|
|
<div class="flex gap-2">
|
|
<button onclick="openArticlesModal()" class="bg-white border border-slate-200 text-slate-600 px-4 py-2 rounded-xl font-bold text-xs uppercase tracking-widest hover:bg-slate-50 transition-colors shadow-sm">
|
|
<i data-lucide="tags" class="w-4 h-4 inline mr-1"></i> Artículos
|
|
</button>
|
|
<button onclick="openBudgetModal()" class="bg-blue-600 text-white px-4 py-2 rounded-xl font-black text-xs uppercase tracking-widest hover:bg-blue-700 transition-colors shadow-md">
|
|
<i data-lucide="plus" class="w-4 h-4 inline mr-1"></i> Crear Presupuesto
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white border border-slate-200 rounded-[2rem] shadow-sm overflow-hidden mb-10">
|
|
<div class="grid grid-cols-12 gap-4 p-4 bg-slate-50 border-b border-slate-100 text-[10px] font-black uppercase tracking-widest text-slate-400">
|
|
<div class="col-span-4 pl-2">Cliente / Fecha</div>
|
|
<div class="col-span-3">Dirección</div>
|
|
<div class="col-span-2 text-center">Total (+IVA)</div>
|
|
<div class="col-span-3 text-center">Acciones</div>
|
|
</div>
|
|
<div id="budgetsList" class="divide-y divide-slate-100">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="budgetModal" class="fixed inset-0 bg-slate-900/75 hidden z-[100] flex items-center justify-center backdrop-blur-sm p-4">
|
|
<div class="bg-white rounded-[2rem] w-full max-w-3xl flex flex-col max-h-[90vh] shadow-2xl">
|
|
<div class="p-6 border-b border-slate-100 flex justify-between items-center">
|
|
<h3 class="font-black text-xl text-slate-800">Nuevo Presupuesto</h3>
|
|
<button onclick="document.getElementById('budgetModal').classList.add('hidden')" class="p-2 bg-slate-100 rounded-full text-slate-500 hover:text-red-500"><i data-lucide="x" class="w-5 h-5"></i></button>
|
|
</div>
|
|
<div class="p-6 overflow-y-auto no-scrollbar flex-1 space-y-4">
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div class="md:col-span-1">
|
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Teléfono (Busca auto)</label>
|
|
<input type="text" id="bPhone" onblur="searchClientByPhone()" placeholder="Ej: 600123456" class="w-full bg-slate-50 border border-slate-200 px-3 py-2.5 rounded-xl text-sm font-bold focus:border-blue-500 outline-none">
|
|
</div>
|
|
<div class="md:col-span-2">
|
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Nombre Completo</label>
|
|
<input type="text" id="bName" class="w-full bg-slate-50 border border-slate-200 px-3 py-2.5 rounded-xl text-sm font-bold focus:border-blue-500 outline-none">
|
|
</div>
|
|
<div class="md:col-span-3">
|
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Dirección de la obra</label>
|
|
<input type="text" id="bAddress" class="w-full bg-slate-50 border border-slate-200 px-3 py-2.5 rounded-xl text-sm font-bold focus:border-blue-500 outline-none">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 border border-slate-200 rounded-2xl overflow-hidden">
|
|
<div class="bg-slate-50 px-4 py-2 flex justify-between items-center border-b border-slate-200">
|
|
<span class="text-[10px] font-black text-slate-500 uppercase tracking-widest">Conceptos</span>
|
|
<button onclick="addBudgetLine()" class="text-[10px] bg-white border border-slate-200 px-2 py-1 rounded text-blue-600 font-bold hover:bg-blue-50">+ Añadir Línea</button>
|
|
</div>
|
|
<div id="budgetLines" class="p-2 space-y-2 bg-slate-50/50"></div>
|
|
</div>
|
|
|
|
<div class="bg-slate-800 text-white rounded-2xl p-5 flex flex-col md:flex-row justify-between items-center gap-4 shadow-lg">
|
|
<div class="flex gap-6 text-center md:text-left">
|
|
<div><p class="text-[10px] text-slate-400 uppercase tracking-widest">Subtotal</p><p class="text-lg font-bold" id="bSubtotal">0.00€</p></div>
|
|
<div><p class="text-[10px] text-slate-400 uppercase tracking-widest">IVA (21%)</p><p class="text-lg font-bold text-amber-400" id="bTax">0.00€</p></div>
|
|
</div>
|
|
<div class="text-center md:text-right">
|
|
<p class="text-[10px] text-emerald-400 uppercase tracking-widest font-black">TOTAL A PAGAR</p>
|
|
<p class="text-3xl font-black text-emerald-300" id="bTotal">0.00€</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="p-4 border-t border-slate-100">
|
|
<button onclick="saveBudget()" class="w-full bg-blue-600 text-white py-3 rounded-xl font-black uppercase tracking-widest shadow-md hover:bg-blue-700">Guardar Presupuesto</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="articlesModal" class="fixed inset-0 bg-slate-900/75 hidden z-[110] flex items-center justify-center p-4 backdrop-blur-sm">
|
|
<div class="bg-white rounded-[2rem] w-full max-w-lg shadow-2xl">
|
|
<div class="p-6 border-b border-slate-100 flex justify-between items-center">
|
|
<h3 class="font-black text-xl text-slate-800">Catálogo de Artículos</h3>
|
|
<button onclick="document.getElementById('articlesModal').classList.add('hidden')" class="p-2 bg-slate-100 rounded-full text-slate-500 hover:text-red-500"><i data-lucide="x" class="w-5 h-5"></i></button>
|
|
</div>
|
|
<div class="p-6 flex gap-2 border-b border-slate-100 bg-slate-50">
|
|
<input type="text" id="newArtName" placeholder="Nombre Artículo..." class="flex-1 px-3 py-2 rounded-xl border border-slate-200 text-sm font-bold outline-none">
|
|
<input type="number" id="newArtPrice" placeholder="Precio Base (€)" class="w-24 px-3 py-2 rounded-xl border border-slate-200 text-sm font-bold text-center outline-none">
|
|
<button onclick="saveArticle()" class="bg-slate-800 text-white px-4 py-2 rounded-xl font-bold shadow-sm">Crear</button>
|
|
</div>
|
|
<div id="articlesList" class="p-4 max-h-60 overflow-y-auto space-y-2"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="convertModal" class="fixed inset-0 bg-slate-900/75 hidden z-[110] flex items-center justify-center p-4 backdrop-blur-sm">
|
|
<div class="bg-white rounded-[2rem] w-full max-w-md shadow-2xl p-6">
|
|
<div class="w-16 h-16 bg-emerald-100 text-emerald-500 rounded-full flex items-center justify-center mx-auto mb-4"><i data-lucide="briefcase" class="w-8 h-8"></i></div>
|
|
<h3 class="font-black text-xl text-slate-800 mb-1 text-center">Pasar a Operativa</h3>
|
|
<p class="text-xs text-slate-500 mb-6 text-center">Asigna este presupuesto a un técnico o mándalo a la bolsa para que lo coja el primero disponible.</p>
|
|
|
|
<input type="hidden" id="convBudgetId">
|
|
|
|
<div class="space-y-4 text-left mb-6 bg-slate-50 p-4 rounded-2xl border border-slate-100">
|
|
<div>
|
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Gremio Especialista *</label>
|
|
<select id="convGuild" onchange="loadOpsForGuildConv(this.value)" class="w-full border-2 border-slate-200 rounded-xl px-3 py-2 text-sm font-bold outline-none focus:border-emerald-500">
|
|
<option value="">Selecciona Gremio...</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Asignar a Operario</label>
|
|
<select id="convOperator" onchange="handleOperatorChange(this.value)" class="w-full border-2 border-slate-200 rounded-xl px-3 py-2 text-sm font-bold outline-none focus:border-emerald-500 text-blue-600">
|
|
<option value="AUTO">⚡ MANDAR A LA BOLSA (AUTO)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div id="convDateContainer" class="grid grid-cols-2 gap-2 pt-2 border-t border-slate-200 hidden">
|
|
<div>
|
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Fecha</label>
|
|
<input type="date" id="convDate" class="w-full border-2 border-slate-200 rounded-xl px-3 py-2 text-sm font-bold outline-none focus:border-emerald-500">
|
|
</div>
|
|
<div>
|
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Hora</label>
|
|
<input type="time" id="convTime" class="w-full border-2 border-slate-200 rounded-xl px-3 py-2 text-sm font-bold outline-none focus:border-emerald-500">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-2">
|
|
<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>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
<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: #2563eb; 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 id="toast" class="fixed bottom-8 right-8 bg-slate-900 text-white px-6 py-3 rounded-2xl shadow-2xl hidden z-[200] font-bold text-sm flex items-center gap-2 transition-all"></div>
|
|
|
|
<script src="js/layout.js"></script>
|
|
<script>
|
|
let allFinancials = [];
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
if (!localStorage.getItem("token")) window.location.href = "index.html";
|
|
|
|
const now = new Date();
|
|
const currentMonthStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
document.getElementById('filterMonth').value = currentMonthStr;
|
|
|
|
setTimeout(loadFinancials, 200);
|
|
});
|
|
|
|
async function loadFinancials() {
|
|
try {
|
|
const res = await fetch(`${API_URL}/financials`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
|
|
const data = await res.json();
|
|
|
|
if(data.ok) {
|
|
allFinancials = data.financials.filter(f => f.service_ref && !f.service_ref.includes('BLOCK-'));
|
|
|
|
const compSelect = document.getElementById('filterCompany');
|
|
const currentVal = compSelect.value;
|
|
const uniqueCompanies = [...new Set(allFinancials.map(f => {
|
|
const raw = f.raw_data || {};
|
|
return (raw['Compañía'] || raw['COMPAÑIA'] || "Particular").toString().toUpperCase().trim();
|
|
}))].sort();
|
|
|
|
compSelect.innerHTML = '<option value="ALL">TODAS LAS COMPAÑÍAS</option>';
|
|
uniqueCompanies.forEach(c => compSelect.innerHTML += `<option value="${c}">${c}</option>`);
|
|
compSelect.value = currentVal;
|
|
|
|
renderList();
|
|
}
|
|
} catch (error) { console.error("Error cargando contabilidad:", error); }
|
|
}
|
|
|
|
function renderList() {
|
|
const container = document.getElementById('financialList');
|
|
const searchTerm = document.getElementById('searchBox').value.toLowerCase();
|
|
const filterCompany = document.getElementById('filterCompany').value;
|
|
const filterStatus = document.getElementById('filterStatus').value;
|
|
const filterMonth = document.getElementById('filterMonth').value;
|
|
|
|
const filtered = allFinancials.filter(f => {
|
|
const raw = f.raw_data || {};
|
|
const name = (raw['Nombre Cliente'] || raw['CLIENTE'] || "").toLowerCase();
|
|
const ref = (f.service_ref || "").toLowerCase();
|
|
const company = (raw['Compañía'] || raw['COMPAÑIA'] || "Particular").toUpperCase().trim();
|
|
|
|
let serviceMonth = "";
|
|
if (raw.scheduled_date) serviceMonth = raw.scheduled_date.substring(0, 7);
|
|
else if (f.created_at) serviceMonth = f.created_at.substring(0, 7);
|
|
|
|
const matchSearch = name.includes(searchTerm) || ref.includes(searchTerm);
|
|
const matchCompany = filterCompany === 'ALL' || company === filterCompany;
|
|
const matchStatus = filterStatus === 'ALL' || (filterStatus === 'paid' ? f.is_paid : !f.is_paid);
|
|
const matchMonth = !filterMonth || serviceMonth === filterMonth;
|
|
|
|
return matchSearch && matchCompany && matchStatus && matchMonth;
|
|
});
|
|
|
|
let totalFacturado = 0;
|
|
let totalCobrado = 0;
|
|
|
|
container.innerHTML = '';
|
|
|
|
if (filtered.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="p-16 text-center flex flex-col items-center">
|
|
<div class="w-16 h-16 bg-slate-100 text-slate-300 rounded-full flex items-center justify-center mb-4"><i data-lucide="inbox" class="w-8 h-8"></i></div>
|
|
<p class="font-bold text-slate-500 mb-1">No se encontraron cobros</p>
|
|
<p class="text-xs text-slate-400">Prueba a cambiar los filtros o el mes de búsqueda.</p>
|
|
</div>`;
|
|
updateKPIs(0, 0);
|
|
lucide.createIcons();
|
|
return;
|
|
}
|
|
|
|
filtered.forEach(f => {
|
|
const raw = f.raw_data || {};
|
|
const name = raw['Nombre Cliente'] || raw['CLIENTE'] || "Sin Nombre";
|
|
const company = raw['Compañía'] || raw['COMPAÑIA'] || "Particular";
|
|
|
|
let fechaVis = "Sin fecha";
|
|
if(raw.scheduled_date) {
|
|
const [y, m, d] = raw.scheduled_date.split('-');
|
|
fechaVis = `${d}/${m}/${y}`;
|
|
}
|
|
|
|
const amt = parseFloat(f.amount || 0);
|
|
totalFacturado += amt;
|
|
if (f.is_paid) totalCobrado += amt;
|
|
|
|
const statusBadge = f.is_paid
|
|
? `<span class="bg-emerald-100 text-emerald-700 px-3 py-1 rounded-lg text-[10px] font-black uppercase tracking-widest flex items-center justify-center gap-1 border border-emerald-200"><i data-lucide="check-circle-2" class="w-3 h-3"></i> Pagado</span>`
|
|
: `<span class="bg-red-50 text-red-600 px-3 py-1 rounded-lg text-[10px] font-black uppercase tracking-widest flex items-center justify-center gap-1 border border-red-200"><i data-lucide="clock" class="w-3 h-3"></i> Pendiente</span>`;
|
|
|
|
container.innerHTML += `
|
|
<div class="grid grid-cols-12 gap-4 p-4 items-center hover:bg-slate-50 transition-colors group">
|
|
<div class="col-span-4 pl-2 min-w-0">
|
|
<p class="text-xs font-black text-slate-800 uppercase truncate" title="${name}">${name}</p>
|
|
<div class="flex items-center gap-2 mt-1 text-[10px] font-bold text-slate-400">
|
|
<span class="bg-slate-100 px-1.5 py-0.5 rounded border border-slate-200">#${f.service_ref}</span>
|
|
<span><i data-lucide="calendar" class="w-3 h-3 inline mr-0.5"></i> ${fechaVis}</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-span-2 min-w-0">
|
|
<span class="bg-blue-50 text-blue-700 border border-blue-100 px-2 py-1 rounded text-[9px] font-black uppercase tracking-widest truncate max-w-full inline-block">${company}</span>
|
|
</div>
|
|
<div class="col-span-2 relative">
|
|
<span class="absolute right-4 top-1/2 -translate-y-1/2 text-sm font-black text-slate-400 pointer-events-none">€</span>
|
|
<input type="number" step="0.01" id="amt-${f.scraped_id}" value="${f.amount}"
|
|
class="w-full bg-white border border-slate-200 pl-3 pr-8 py-2.5 rounded-xl text-sm font-black text-right outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-100 transition-all ${f.is_paid ? 'text-emerald-700 bg-emerald-50/30' : 'text-slate-700'}">
|
|
</div>
|
|
<div class="col-span-2">
|
|
<select id="method-${f.scraped_id}" class="w-full bg-white border border-slate-200 px-2 py-2.5 rounded-xl text-[10px] font-black uppercase tracking-widest text-slate-600 outline-none focus:border-emerald-500 cursor-pointer">
|
|
<option value="Pendiente" ${f.payment_method === 'Pendiente' ? 'selected' : ''}>⏳ Pendiente</option>
|
|
<option value="Cobro Banco" ${f.payment_method === 'Cobro Banco' ? 'selected' : ''}>🏦 Banco</option>
|
|
<option value="Efectivo" ${f.payment_method === 'Efectivo' ? 'selected' : ''}>💵 Efectivo</option>
|
|
<option value="Tarjeta" ${f.payment_method === 'Tarjeta' ? 'selected' : ''}>💳 Tarjeta</option>
|
|
<option value="Transferencia" ${f.payment_method === 'Transferencia' ? 'selected' : ''}>🔄 Transfer.</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-span-2 flex items-center justify-between gap-2">
|
|
<div id="badge-container-${f.scraped_id}" class="flex-1">${statusBadge}</div>
|
|
<button onclick="savePayment(${f.scraped_id})" class="bg-slate-100 text-slate-500 border border-slate-200 hover:bg-emerald-500 hover:border-emerald-600 hover:text-white p-2 rounded-xl transition-all shrink-0 active:scale-95 shadow-sm" title="Guardar Cambios"><i data-lucide="save" class="w-4 h-4"></i></button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
updateKPIs(totalFacturado, totalCobrado);
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function updateKPIs(facturado, cobrado) {
|
|
const pendiente = facturado - cobrado;
|
|
document.getElementById('kpi-total').innerText = facturado.toLocaleString('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €';
|
|
document.getElementById('kpi-paid').innerText = cobrado.toLocaleString('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €';
|
|
document.getElementById('kpi-pending').innerText = pendiente.toLocaleString('es-ES', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €';
|
|
}
|
|
|
|
async function savePayment(scrapedId) {
|
|
const amountInput = document.getElementById(`amt-${scrapedId}`);
|
|
const methodInput = document.getElementById(`method-${scrapedId}`);
|
|
|
|
const amount = amountInput.value;
|
|
const method = methodInput.value;
|
|
|
|
amountInput.disabled = true; methodInput.disabled = true;
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/financials/${scrapedId}`, {
|
|
method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem("token")}` },
|
|
body: JSON.stringify({ amount, payment_method: method })
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.ok) {
|
|
showToast("Cobro actualizado");
|
|
const fIndex = allFinancials.findIndex(f => f.scraped_id === scrapedId);
|
|
if (fIndex > -1) {
|
|
allFinancials[fIndex].amount = parseFloat(amount || 0);
|
|
allFinancials[fIndex].payment_method = method;
|
|
allFinancials[fIndex].is_paid = method !== 'Pendiente';
|
|
}
|
|
renderList();
|
|
} else { showToast("Error al guardar."); }
|
|
} catch (error) { showToast("Error de conexión."); }
|
|
finally { amountInput.disabled = false; methodInput.disabled = false; }
|
|
}
|
|
|
|
function toggleTab(tab) {
|
|
const vCobros = document.getElementById('view-cobros');
|
|
const vPres = document.getElementById('view-presupuestos');
|
|
const tCobros = document.getElementById('tab-cobros');
|
|
const tPres = document.getElementById('tab-presupuestos');
|
|
|
|
if (tab === 'cobros') {
|
|
vCobros.classList.remove('hidden'); vPres.classList.add('hidden');
|
|
tCobros.className = "px-6 py-3 font-black text-sm text-emerald-600 border-b-2 border-emerald-500 flex items-center gap-2 transition-colors";
|
|
tPres.className = "px-6 py-3 font-bold text-sm text-slate-400 border-b-2 border-transparent hover:text-slate-600 transition-colors flex items-center gap-2";
|
|
} else {
|
|
vCobros.classList.add('hidden'); vPres.classList.remove('hidden');
|
|
tPres.className = "px-6 py-3 font-black text-sm text-blue-600 border-b-2 border-blue-500 flex items-center gap-2 transition-colors";
|
|
tCobros.className = "px-6 py-3 font-bold text-sm text-slate-400 border-b-2 border-transparent hover:text-slate-600 transition-colors flex items-center gap-2";
|
|
loadBudgets();
|
|
}
|
|
}
|
|
|
|
let myArticles = [];
|
|
let myBudgets = [];
|
|
|
|
async function loadBudgets() {
|
|
try {
|
|
const [rB, rA] = await Promise.all([
|
|
fetch(`${API_URL}/budgets`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } }),
|
|
fetch(`${API_URL}/articles`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } })
|
|
]);
|
|
const dataB = await rB.json();
|
|
const dataA = await rA.json();
|
|
|
|
if (dataB.ok) myBudgets = dataB.budgets;
|
|
if (dataA.ok) myArticles = dataA.articles;
|
|
|
|
renderBudgets();
|
|
} catch(e) {}
|
|
}
|
|
|
|
function renderBudgets() {
|
|
const list = document.getElementById('budgetsList');
|
|
list.innerHTML = "";
|
|
if(myBudgets.length === 0) {
|
|
list.innerHTML = `<div class="p-16 text-center text-slate-400">Sin presupuestos generados aún.</div>`;
|
|
return;
|
|
}
|
|
|
|
myBudgets.forEach(b => {
|
|
const bDate = new Date(b.created_at);
|
|
const dateDisplay = bDate.toLocaleDateString('es-ES');
|
|
|
|
// 🔴 LÓGICA DE CADUCIDAD (30 DÍAS)
|
|
const diffDays = Math.ceil(Math.abs(new Date() - bDate) / (1000 * 60 * 60 * 24));
|
|
const isExpired = diffDays > 30;
|
|
|
|
let bStatus = '';
|
|
let isAnulado = false;
|
|
if (b.linked_service_status_name && b.linked_service_status_name.toLowerCase().includes('anulado')) {
|
|
isAnulado = true;
|
|
}
|
|
|
|
// 🎨 DEFINICIÓN DE ETIQUETAS UNIFICADAS
|
|
if (b.status === 'paid') {
|
|
bStatus = `<span class="bg-emerald-100 text-emerald-700 px-3 py-1 rounded-lg text-[10px] font-black uppercase border border-emerald-200 shadow-sm"><i data-lucide="badge-check" class="w-3 h-3 inline mr-1"></i> Pagado Online</span>`;
|
|
} else if (isExpired && b.status !== 'rejected' && b.status !== 'converted') {
|
|
bStatus = `<span class="bg-slate-100 text-slate-500 px-3 py-1 rounded-lg text-[10px] font-black uppercase border border-slate-200"><i data-lucide="clock-alert" class="w-3 h-3 inline mr-1"></i> Caducado</span>`;
|
|
} else if (b.status === 'pending') {
|
|
bStatus = `<span class="bg-amber-100 text-amber-700 px-3 py-1 rounded-lg text-[10px] font-black uppercase border border-amber-200"><i data-lucide="clock" class="w-3 h-3 inline mr-1"></i> Pte. Resolver</span>`;
|
|
} else if (b.status === 'rejected') {
|
|
bStatus = `<span class="bg-red-100 text-red-700 px-3 py-1 rounded-lg text-[10px] font-black uppercase"><i data-lucide="x" class="w-3 h-3 inline mr-1"></i> Rechazado</span>`;
|
|
} else if (b.status === 'accepted') {
|
|
bStatus = `<span class="bg-blue-100 text-blue-700 px-3 py-1 rounded-lg text-[10px] font-black uppercase border border-blue-200"><i data-lucide="check" class="w-3 h-3 inline mr-1"></i> Aceptado</span>`;
|
|
} else if (b.status === 'converted') {
|
|
if (isAnulado) {
|
|
bStatus = `<span class="bg-red-50 text-red-600 px-3 py-1 rounded-lg text-[10px] font-black uppercase border border-red-100"><i data-lucide="x-circle" class="w-3 h-3 inline mr-1"></i> Servicio Anulado</span>`;
|
|
} else {
|
|
const sName = b.linked_service_status_name || 'En gestión';
|
|
bStatus = `<span class="bg-emerald-50 text-emerald-700 px-3 py-1 rounded-lg text-[10px] font-black uppercase border border-emerald-100" title="Estado en Operativa"><i data-lucide="briefcase" class="w-3 h-3 inline mr-1"></i> ${sName}</span>`;
|
|
}
|
|
}
|
|
|
|
// 🛠️ LÓGICA DE ACCIONES (Botón de "Crear Servicio")
|
|
let actions = '';
|
|
if(b.status === 'pending' && !isExpired) {
|
|
actions = `
|
|
<button onclick="updateBudgetStatus(${b.id}, 'accepted')" class="bg-blue-50 text-blue-600 hover:bg-blue-500 hover:text-white p-2 rounded-xl shadow-sm transition-all" title="Aceptar"><i data-lucide="thumbs-up" class="w-4 h-4"></i></button>
|
|
<button onclick="updateBudgetStatus(${b.id}, 'rejected')" class="bg-rose-50 text-rose-600 hover:bg-rose-500 hover:text-white p-2 rounded-xl shadow-sm transition-all" title="Rechazar"><i data-lucide="thumbs-down" class="w-4 h-4"></i></button>
|
|
`;
|
|
} else if((b.status === 'accepted' || b.status === 'paid') && !isExpired) {
|
|
// El botón de crear servicio aparece tanto si está aceptado por firma como si ya está PAGADO
|
|
actions = `<button onclick="openConvertModal(${b.id})" class="bg-emerald-500 text-white px-4 py-2 rounded-xl font-black text-[10px] uppercase tracking-widest shadow-md hover:bg-emerald-600 transition-all active:scale-95">Crear Servicio</button>`;
|
|
}
|
|
|
|
// Botones fijos (PDF y Borrar)
|
|
let utilityButtons = `
|
|
<button onclick="generatePDF(${b.id})" class="text-slate-400 hover:text-blue-600 p-2 transition-colors" title="Descargar PDF"><i data-lucide="file-text" class="w-5 h-5"></i></button>
|
|
<button onclick="deleteBudget(${b.id}, '${b.status}', ${isAnulado})" class="text-slate-300 hover:text-red-500 p-2 transition-colors" title="Borrar"><i data-lucide="trash-2" class="w-5 h-5"></i></button>
|
|
`;
|
|
|
|
list.innerHTML += `
|
|
<div class="grid grid-cols-12 gap-4 p-5 items-center hover:bg-slate-50/50 transition-colors border-b border-slate-100">
|
|
<div class="col-span-4 pl-2 min-w-0">
|
|
<p class="text-sm font-black text-slate-800 uppercase truncate">
|
|
<span class="text-blue-500 mr-1 tracking-tighter">#PRE-${b.id}</span> ${b.client_name}
|
|
</p>
|
|
<p class="text-[10px] font-bold text-slate-400 mt-1 flex items-center gap-2">
|
|
<span><i data-lucide="calendar" class="w-3 h-3 inline"></i> ${dateDisplay}</span>
|
|
<span>•</span>
|
|
<span><i data-lucide="phone" class="w-3 h-3 inline"></i> ${b.client_phone}</span>
|
|
</p>
|
|
</div>
|
|
<div class="col-span-3 min-w-0">
|
|
<p class="text-[11px] text-slate-500 font-medium truncate italic">${b.client_address || 'Sin dirección'}</p>
|
|
</div>
|
|
<div class="col-span-2 text-center">
|
|
<p class="text-base font-black text-slate-700">${parseFloat(b.total).toFixed(2)}€</p>
|
|
</div>
|
|
<div class="col-span-3 flex justify-end items-center gap-3 pr-4">
|
|
<div class="shrink-0">${bStatus}</div>
|
|
<div class="flex items-center gap-1">${actions} ${utilityButtons}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
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) {
|
|
if ((status === 'accepted' || status === 'converted') && !isAnulado) {
|
|
return showToast("⚠️ Para borrar el presupuesto, el servicio vinculado debe estar Anulado.");
|
|
}
|
|
if (!confirm("¿Seguro que quieres borrar este presupuesto definitivamente?")) return;
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/budgets/${id}`, {
|
|
method: 'DELETE',
|
|
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
|
|
});
|
|
const data = await res.json();
|
|
if (data.ok) {
|
|
showToast("🗑️ Presupuesto eliminado");
|
|
loadBudgets();
|
|
} else { showToast("❌ " + (data.error || "Error al borrar")); }
|
|
} catch (e) { showToast("❌ Error de conexión"); }
|
|
}
|
|
|
|
async function openConvertModal(id) {
|
|
document.getElementById('convBudgetId').value = id;
|
|
const gSel = document.getElementById('convGuild');
|
|
gSel.innerHTML = '<option value="">Cargando gremios...</option>';
|
|
try {
|
|
const res = await fetch(`${API_URL}/guilds`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
|
|
const data = await res.json();
|
|
gSel.innerHTML = '<option value="">Selecciona Gremio...</option>';
|
|
if (data.ok && data.guilds) {
|
|
data.guilds.forEach(g => { gSel.innerHTML += `<option value="${g.id}">${g.name}</option>`; });
|
|
}
|
|
} catch (e) { gSel.innerHTML = '<option value="">Error al cargar</option>'; }
|
|
|
|
document.getElementById('convOperator').innerHTML = '<option value="AUTO">⚡ MANDAR A LA BOLSA (AUTO)</option>';
|
|
document.getElementById('convDateContainer').classList.add('hidden');
|
|
document.getElementById('convDate').value = "";
|
|
document.getElementById('convTime').value = "";
|
|
document.getElementById('convertModal').classList.remove('hidden');
|
|
}
|
|
|
|
async function loadOpsForGuildConv(guildId) {
|
|
const sel = document.getElementById('convOperator');
|
|
const dateCont = document.getElementById('convDateContainer');
|
|
sel.innerHTML = '<option value="AUTO">⚡ MANDAR A LA BOLSA (AUTO)</option>';
|
|
dateCont.classList.add('hidden');
|
|
document.getElementById('convDate').value = ""; document.getElementById('convTime').value = "";
|
|
|
|
if(!guildId) return;
|
|
|
|
try {
|
|
const res = await fetch(`${API_URL}/operators?guild_id=${guildId}`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
|
|
const data = await res.json();
|
|
if(data.ok && data.operators) {
|
|
data.operators.forEach(op => { sel.innerHTML += `<option value="${op.id}">👤 ${op.full_name}</option>`; });
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
function handleOperatorChange(val) {
|
|
const dateCont = document.getElementById('convDateContainer');
|
|
if(val === 'AUTO') dateCont.classList.add('hidden');
|
|
else dateCont.classList.remove('hidden');
|
|
}
|
|
|
|
async function confirmConversion() {
|
|
const id = document.getElementById('convBudgetId').value;
|
|
const guild_id = document.getElementById('convGuild').value;
|
|
const assigned_to = document.getElementById('convOperator').value;
|
|
const date = document.getElementById('convDate').value;
|
|
const time = document.getElementById('convTime').value;
|
|
|
|
if (!guild_id) return showToast("⚠️ El Gremio es obligatorio.");
|
|
const use_automation = (assigned_to === 'AUTO');
|
|
if (!use_automation && (!date || !time)) return showToast("⚠️ Si asignas a un técnico, debes fijar Fecha y Hora.");
|
|
|
|
const payload = {
|
|
guild_id: guild_id, assigned_to: use_automation ? null : assigned_to,
|
|
use_automation: use_automation, date: use_automation ? null : date, time: use_automation ? null : time
|
|
};
|
|
|
|
try {
|
|
await fetch(`${API_URL}/budgets/${id}/convert`, {
|
|
method: 'POST', headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
document.getElementById('convertModal').classList.add('hidden');
|
|
showToast(use_automation ? "✅ ¡Enviado a la bolsa con éxito!" : "✅ ¡Servicio agendado con éxito!");
|
|
loadBudgets();
|
|
} catch(e) { showToast("❌ Error al convertir el presupuesto."); }
|
|
}
|
|
|
|
function openBudgetModal() {
|
|
document.getElementById('bPhone').value = ""; document.getElementById('bName').value = ""; document.getElementById('bAddress').value = "";
|
|
document.getElementById('budgetLines').innerHTML = "";
|
|
addBudgetLine();
|
|
calcBudget();
|
|
document.getElementById('budgetModal').classList.remove('hidden');
|
|
}
|
|
|
|
async function searchClientByPhone() {
|
|
const phone = document.getElementById('bPhone').value;
|
|
if(!phone || phone.length < 9) return;
|
|
const res = await fetch(`${API_URL}/clients/search?phone=${encodeURIComponent(phone)}`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
|
|
const data = await res.json();
|
|
if (data.client) {
|
|
document.getElementById('bName').value = data.client.full_name;
|
|
if(data.client.addresses && data.client.addresses.length > 0) document.getElementById('bAddress').value = data.client.addresses[0];
|
|
showToast("Datos de cliente cargados");
|
|
}
|
|
}
|
|
|
|
function addBudgetLine() {
|
|
const lineId = Date.now();
|
|
const artOptions = myArticles.map(a => `<option value="${a.name}|${a.price}">${a.name} (${a.price}€)</option>`).join("");
|
|
|
|
const html = `
|
|
<div class="flex flex-col md:flex-row gap-2 items-center bg-white p-2 rounded-xl border border-slate-100 shadow-sm" id="bl-${lineId}">
|
|
<select onchange="applyArticleToLine(${lineId}, this.value)" class="w-full md:w-1/3 border border-slate-200 rounded px-2 py-1.5 text-xs outline-none">
|
|
<option value="">Elegir catálogo...</option>${artOptions}
|
|
</select>
|
|
<input type="text" class="b-concept w-full border border-slate-200 rounded px-2 py-1.5 text-xs font-bold outline-none" placeholder="Concepto manual...">
|
|
<div class="flex items-center gap-2 w-full md:w-auto">
|
|
<input type="number" class="b-qty w-16 border border-slate-200 rounded px-2 py-1.5 text-xs text-center outline-none" value="1" oninput="calcBudget()">
|
|
<div class="relative w-24">
|
|
<input type="number" class="b-price w-full border border-slate-200 rounded pl-2 pr-6 py-1.5 text-xs text-right outline-none" value="0.00" oninput="calcBudget()">
|
|
<span class="absolute right-2 top-1.5 text-xs text-slate-400">€</span>
|
|
</div>
|
|
<button onclick="document.getElementById('bl-${lineId}').remove(); calcBudget();" class="text-red-400 hover:text-red-600 p-1"><i data-lucide="trash" class="w-4 h-4"></i></button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.getElementById('budgetLines').insertAdjacentHTML('beforeend', html);
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function applyArticleToLine(lineId, val) {
|
|
if(!val) return;
|
|
const [name, price] = val.split('|');
|
|
const line = document.getElementById(`bl-${lineId}`);
|
|
line.querySelector('.b-concept').value = name;
|
|
line.querySelector('.b-price').value = price;
|
|
calcBudget();
|
|
}
|
|
|
|
function calcBudget() {
|
|
let sub = 0;
|
|
document.querySelectorAll('#budgetLines > div').forEach(line => {
|
|
const q = parseFloat(line.querySelector('.b-qty').value) || 0;
|
|
const p = parseFloat(line.querySelector('.b-price').value) || 0;
|
|
sub += (q * p);
|
|
});
|
|
const tax = sub * 0.21;
|
|
const tot = sub + tax;
|
|
|
|
document.getElementById('bSubtotal').innerText = sub.toFixed(2) + "€";
|
|
document.getElementById('bTax').innerText = tax.toFixed(2) + "€";
|
|
document.getElementById('bTotal').innerText = tot.toFixed(2) + "€";
|
|
}
|
|
|
|
async function saveBudget() {
|
|
const phone = document.getElementById('bPhone').value;
|
|
const name = document.getElementById('bName').value;
|
|
if(!name) return showToast("El nombre es obligatorio");
|
|
|
|
const items = [];
|
|
document.querySelectorAll('#budgetLines > div').forEach(line => {
|
|
const c = line.querySelector('.b-concept').value;
|
|
const q = parseFloat(line.querySelector('.b-qty').value) || 0;
|
|
const p = parseFloat(line.querySelector('.b-price').value) || 0;
|
|
if(c && q > 0) items.push({concept: c, qty: q, price: p});
|
|
});
|
|
|
|
if(items.length === 0) return showToast("Añade al menos un concepto");
|
|
|
|
const sub = parseFloat(document.getElementById('bSubtotal').innerText);
|
|
const tax = parseFloat(document.getElementById('bTax').innerText);
|
|
const tot = parseFloat(document.getElementById('bTotal').innerText);
|
|
|
|
const payload = { client_phone: phone, client_name: name, client_address: document.getElementById('bAddress').value, items, subtotal: sub, tax, total: tot };
|
|
|
|
await fetch(`${API_URL}/budgets`, { method: 'POST', headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, body: JSON.stringify(payload) });
|
|
|
|
document.getElementById('budgetModal').classList.add('hidden');
|
|
showToast("Presupuesto guardado");
|
|
loadBudgets();
|
|
}
|
|
|
|
function openArticlesModal() {
|
|
renderArticles();
|
|
document.getElementById('articlesModal').classList.remove('hidden');
|
|
}
|
|
|
|
function renderArticles() {
|
|
const list = document.getElementById('articlesList');
|
|
list.innerHTML = myArticles.map(a => `
|
|
<div class="flex justify-between items-center bg-white p-3 rounded-xl border border-slate-100 shadow-sm">
|
|
<span class="font-bold text-xs uppercase text-slate-700">${a.name}</span>
|
|
<div class="flex items-center gap-4">
|
|
<span class="font-black text-emerald-600">${a.price}€</span>
|
|
<button onclick="editArticle(${a.id}, '${a.name}', ${a.price})" class="text-blue-500 hover:text-blue-700"><i data-lucide="edit" class="w-4 h-4"></i></button>
|
|
</div>
|
|
</div>
|
|
`).join("");
|
|
lucide.createIcons();
|
|
}
|
|
|
|
async function saveArticle() {
|
|
const name = document.getElementById('newArtName').value;
|
|
const price = document.getElementById('newArtPrice').value;
|
|
if(!name || !price) return;
|
|
await fetch(`${API_URL}/articles`, { method: 'POST', headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, body: JSON.stringify({name, price}) });
|
|
document.getElementById('newArtName').value = ""; document.getElementById('newArtPrice').value = "";
|
|
await loadBudgets();
|
|
renderArticles();
|
|
}
|
|
|
|
async function editArticle(id, oldName, oldPrice) {
|
|
const n = prompt("Nuevo nombre:", oldName);
|
|
const p = prompt("Nuevo precio:", oldPrice);
|
|
if(n && p) {
|
|
await fetch(`${API_URL}/articles/${id}`, { method: 'PUT', headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, body: JSON.stringify({name: n, price: p}) });
|
|
await loadBudgets(); renderArticles();
|
|
}
|
|
}
|
|
|
|
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...");
|
|
|
|
// Valores por defecto
|
|
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 {
|
|
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
|
|
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>
|
|
<td style="padding: 8px 10px; border-bottom: 1px solid #e2e8f0; font-size: 12px; color: #334155;">${item.concept}</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 10px; border-bottom: 1px solid #e2e8f0; font-size: 12px; color: #334155; text-align: right;">${parseFloat(item.price).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>
|
|
`).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 (empIban) {
|
|
document.getElementById('pdf-bank-account').innerText = empIban;
|
|
bankContainer.style.display = 'block';
|
|
} else {
|
|
bankContainer.style.display = 'none';
|
|
}
|
|
|
|
const obsContainer = document.getElementById('pdf-obs-info');
|
|
if (empObs) {
|
|
document.getElementById('pdf-obs-text').innerHTML = empObs.replace(/\n/g, '<br>');
|
|
obsContainer.style.display = 'block';
|
|
} else {
|
|
obsContainer.style.display = 'none';
|
|
}
|
|
|
|
// Generar PDF
|
|
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, 500)); // Esperamos medio segundo para asegurar fuentes
|
|
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) {
|
|
const t = document.getElementById('toast');
|
|
t.innerHTML = `<i data-lucide="check-circle" class="w-5 h-5 text-emerald-400"></i> ${msg}`;
|
|
lucide.createIcons();
|
|
t.classList.remove('hidden');
|
|
setTimeout(() => t.classList.add('hidden'), 3000);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |