Actualizar index2.html

This commit is contained in:
2026-03-29 10:52:30 +00:00
parent 3952cd01f0
commit 670b4e1f4c

View File

@@ -7,6 +7,7 @@
<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>
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js"></script>
<style>
:root { --app-bg: #f8fafc; }
@@ -144,7 +145,7 @@
</nav>
<div id="quoteModal" class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-[100] hidden flex-col justify-end">
<div class="bg-white w-full rounded-t-[2.5rem] p-6 pt-8 pb-10 transition-transform transform translate-y-full duration-300" id="quoteModalSheet">
<div class="bg-white w-full rounded-t-[2.5rem] p-6 pt-8 pb-10 transition-transform transform translate-y-full duration-300 max-h-[90vh] overflow-y-auto no-scrollbar" id="quoteModalSheet">
<div class="flex justify-between items-start mb-6">
<div>
<span class="text-[10px] font-black text-slate-400 uppercase tracking-widest bg-slate-100 px-2 py-1 rounded-md" id="qmRef">REF</span>
@@ -158,14 +159,48 @@
<span class="text-xs font-bold text-slate-500">Fecha de emisión</span>
<span class="text-xs font-black text-slate-800" id="qmDate">--</span>
</div>
<div class="border-t border-slate-200 pt-3 mt-3 mb-3">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2">Desglose de Conceptos</p>
<ul id="qmItemsList" class="space-y-2 text-xs font-medium text-slate-600">
</ul>
</div>
<div class="flex justify-between items-center border-t border-slate-200 pt-3">
<span class="text-sm font-black text-slate-500 uppercase tracking-widest">Total Estimado</span>
<span class="text-sm font-black text-slate-500 uppercase tracking-widest">Total Presupuestado</span>
<span class="text-3xl font-black text-blue-600" id="qmAmount">0.00€</span>
</div>
</div>
<button id="btnDownloadPdf" class="w-full bg-blue-600 text-white font-black py-4 rounded-2xl shadow-xl flex items-center justify-center gap-2 uppercase tracking-widest text-xs active:scale-95 transition-transform">
<i data-lucide="download" class="w-4 h-4"></i> Descargar PDF
<div id="qmActionsContainer" class="mb-4">
<div id="qmDecisionButtons" class="flex gap-3">
<button onclick="openSignatureArea()" class="flex-1 bg-emerald-50 text-emerald-600 border border-emerald-200 font-black py-4 rounded-2xl flex flex-col items-center justify-center gap-1 uppercase tracking-widest text-[10px] active:scale-95 transition-all hover:bg-emerald-500 hover:text-white">
<i data-lucide="thumbs-up" class="w-5 h-5"></i> Aceptar
</button>
<button onclick="rejectBudget()" class="flex-1 bg-rose-50 text-rose-600 border border-rose-200 font-black py-4 rounded-2xl flex flex-col items-center justify-center gap-1 uppercase tracking-widest text-[10px] active:scale-95 transition-all hover:bg-rose-500 hover:text-white">
<i data-lucide="thumbs-down" class="w-5 h-5"></i> Rechazar
</button>
</div>
<div id="qmStatusMessage" class="hidden text-center p-4 rounded-xl font-black text-xs uppercase tracking-widest"></div>
</div>
<div id="qmSignatureArea" class="hidden mb-6 bg-slate-50 p-4 rounded-2xl border border-slate-200">
<p class="text-[10px] font-black text-emerald-600 uppercase tracking-widest mb-2 text-center"><i data-lucide="pen-tool" class="w-3 h-3 inline"></i> Dibuja tu firma para confirmar</p>
<div class="border-2 border-dashed border-slate-300 bg-white rounded-xl overflow-hidden mb-3">
<canvas id="signatureCanvas" class="w-full h-32 touch-none"></canvas>
</div>
<div class="flex gap-2">
<button onclick="clearSignature()" class="px-3 py-3 bg-slate-200 text-slate-600 rounded-xl text-xs font-bold uppercase active:scale-95"><i data-lucide="eraser" class="w-4 h-4"></i></button>
<button onclick="confirmAcceptBudget()" class="flex-1 bg-emerald-500 text-white font-black py-3 rounded-xl shadow-md flex items-center justify-center gap-2 uppercase tracking-widest text-xs active:scale-95 transition-transform">
Confirmar y Enviar
</button>
</div>
<button onclick="closeSignatureArea()" class="w-full mt-2 text-[10px] font-bold text-slate-400 p-2 uppercase tracking-widest">Cancelar</button>
</div>
<button id="btnDownloadPdf" class="w-full bg-slate-800 text-white font-black py-4 rounded-2xl shadow-xl flex items-center justify-center gap-2 uppercase tracking-widest text-xs active:scale-95 transition-transform">
<i data-lucide="download" class="w-4 h-4"></i> Descargar Copia en PDF
</button>
</div>
</div>
@@ -409,10 +444,14 @@
}
}
let signaturePad = null;
let currentBudgetIdForSignature = null;
function openQuoteModal(id) {
const q = currentQuotes.find(x => x.id === id);
if (!q) return;
currentBudgetIdForSignature = id;
localStorage.setItem(`quote_viewed_${id}`, 'true');
renderQuotes();
@@ -425,13 +464,53 @@
document.getElementById('qmDate').innerText = fDate;
document.getElementById('qmAmount').innerText = parseFloat(q.total || q.amount || 0).toFixed(2) + "€";
// 🛑 ENLAZAMOS EL BOTÓN DE DESCARGAR PDF
// PINTAR LÍNEAS DE CONCEPTOS
let itemsArr = [];
if (typeof q.items === 'string') {
try { itemsArr = JSON.parse(q.items); } catch(e) { itemsArr = []; }
} else if (Array.isArray(q.items)) {
itemsArr = q.items;
}
let itemsHtml = '';
if(itemsArr.length > 0) {
itemsArr.forEach(i => {
itemsHtml += `<li class="flex justify-between border-b border-slate-100 pb-2 mb-2"><span class="w-2/3 pr-2 leading-tight">${i.qty}x ${i.concept}</span> <span class="font-black text-slate-800 shrink-0 text-right">${(i.qty * i.price).toFixed(2)}€</span></li>`;
});
} else {
itemsHtml = `<li class="text-slate-400 italic">Desglose general aplicado en el importe total.</li>`;
}
document.getElementById('qmItemsList').innerHTML = itemsHtml;
// CONTROL DE ESTADOS (ACEPTADO / RECHAZADO)
const btnContainer = document.getElementById('qmDecisionButtons');
const msgContainer = document.getElementById('qmStatusMessage');
const sigArea = document.getElementById('qmSignatureArea');
sigArea.classList.add('hidden');
if (!q.status || q.status === 'pending') {
btnContainer.classList.remove('hidden');
msgContainer.classList.add('hidden');
} else if (q.status === 'accepted' || q.status === 'converted') {
btnContainer.classList.add('hidden');
msgContainer.classList.remove('hidden');
msgContainer.className = "text-center p-4 rounded-xl font-black text-xs uppercase tracking-widest bg-emerald-50 text-emerald-600 border border-emerald-100";
msgContainer.innerHTML = '<i data-lucide="check-circle-2" class="w-5 h-5 inline mb-1"></i><br>Presupuesto Aceptado';
} else if (q.status === 'rejected') {
btnContainer.classList.add('hidden');
msgContainer.classList.remove('hidden');
msgContainer.className = "text-center p-4 rounded-xl font-black text-xs uppercase tracking-widest bg-rose-50 text-rose-600 border border-rose-100";
msgContainer.innerHTML = '<i data-lucide="x-circle" class="w-5 h-5 inline mb-1"></i><br>Presupuesto Rechazado';
}
document.getElementById('btnDownloadPdf').setAttribute('onclick', `generatePDF(${q.id})`);
const modal = document.getElementById('quoteModal');
const sheet = document.getElementById('quoteModalSheet');
modal.classList.remove('hidden');
modal.classList.add('flex');
lucide.createIcons();
setTimeout(() => sheet.classList.remove('translate-y-full'), 10);
}
@@ -445,6 +524,78 @@
}, 300);
}
// --- SISTEMA DE FIRMA Y RESPUESTA ---
function openSignatureArea() {
document.getElementById('qmDecisionButtons').classList.add('hidden');
document.getElementById('qmSignatureArea').classList.remove('hidden');
const canvas = document.getElementById('signatureCanvas');
if(!signaturePad) {
signaturePad = new SignaturePad(canvas, { backgroundColor: 'rgb(255, 255, 255)' });
}
signaturePad.clear();
const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext("2d").scale(ratio, ratio);
}
function closeSignatureArea() {
document.getElementById('qmSignatureArea').classList.add('hidden');
document.getElementById('qmDecisionButtons').classList.remove('hidden');
}
function clearSignature() {
if(signaturePad) signaturePad.clear();
}
async function confirmAcceptBudget() {
if(signaturePad.isEmpty()) return showToast("⚠️ Por favor, dibuja tu firma para aceptar.");
const signatureBase64 = signaturePad.toDataURL("image/png");
showToast("⏳ Guardando aceptación...");
try {
const res = await fetch(`${API_URL}/public/portal/${urlToken}/budget/${currentBudgetIdForSignature}/respond`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'accept', signature: signatureBase64 })
});
const data = await res.json();
if(data.ok) {
const b = currentQuotes.find(x => x.id === currentBudgetIdForSignature);
if(b) b.status = 'accepted';
showToast("✅ ¡Presupuesto Aceptado! La oficina ha sido notificada.");
closeQuoteModal();
renderQuotes();
}
} catch(e) { showToast("❌ Error de conexión al guardar."); }
}
async function rejectBudget() {
if(!confirm("¿Estás seguro de que deseas rechazar este presupuesto?")) return;
showToast("⏳ Registrando...");
try {
const res = await fetch(`${API_URL}/public/portal/${urlToken}/budget/${currentBudgetIdForSignature}/respond`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'reject' })
});
const data = await res.json();
if(data.ok) {
const b = currentQuotes.find(x => x.id === currentBudgetIdForSignature);
if(b) b.status = 'rejected';
showToast("❌ Presupuesto rechazado. Gracias por avisarnos.");
closeQuoteModal();
renderQuotes();
}
} catch(e) { showToast("❌ Error de conexión al guardar."); }
}
// --- LÓGICA DE PDF AÑADIDA ---
let allServicesGlobalBackup = []; // <--- Variable para no perder los datos del cliente