Anade parte de trabajo con IA y firma
This commit is contained in:
405
calendario.html
405
calendario.html
@@ -6,6 +6,7 @@
|
||||
<title>Calendario - IntegraRepara</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>
|
||||
/* VARIABLES DINÁMICAS: Se actualizarán por JS desde la base de datos */
|
||||
:root {
|
||||
@@ -280,6 +281,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-5 rounded-[2rem] shadow-sm border border-slate-200 text-left">
|
||||
<p class="text-[10px] font-black text-primary-dynamic uppercase tracking-widest mb-3 flex items-center gap-1.5"><i data-lucide="clipboard-signature" class="w-4 h-4"></i> Parte de Trabajo</p>
|
||||
<button onclick="openWorkReportModal()" class="w-full bg-slate-800 text-white font-black py-3.5 rounded-xl shadow-md flex items-center justify-center gap-2 uppercase text-xs tracking-widest active:scale-95 transition-all">
|
||||
<i data-lucide="file-signature" class="w-4 h-4"></i> Crear Parte de Trabajo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 text-center">
|
||||
<button onclick="askToFinish()" class="w-full bg-emerald-600 text-white font-black py-5 rounded-[2rem] shadow-xl flex items-center justify-center gap-3 uppercase text-sm tracking-widest active:scale-95 transition-all">
|
||||
<i data-lucide="check-circle" class="w-6 h-6"></i> Finalizar Trabajo
|
||||
@@ -289,6 +297,102 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="workReportModal" class="translate-y-full transition-transform duration-300 fixed inset-0 z-[160] flex flex-col bg-slate-50" style="display:none;">
|
||||
<div class="pt-safe bg-white border-b border-slate-200 flex justify-between items-center p-4 shrink-0 shadow-sm">
|
||||
<button type="button" onclick="closeWorkReportModal()" class="bg-slate-100 p-2.5 rounded-full text-slate-600 active:scale-90 transition-transform"><i data-lucide="x" class="w-6 h-6"></i></button>
|
||||
<h2 class="font-black text-slate-800 text-sm uppercase tracking-widest text-center">Parte de Trabajo</h2>
|
||||
<div class="w-10"></div>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto p-5 space-y-5 no-scrollbar pb-40">
|
||||
|
||||
<div class="bg-white p-5 rounded-3xl shadow-sm border border-slate-200 space-y-3 text-left">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-1.5"><i data-lucide="pencil-line" class="w-4 h-4 text-blue-500"></i> Descripción del trabajo realizado</p>
|
||||
<textarea id="wrText" rows="6" placeholder="Escribe aquí, con tus palabras, lo que has hecho (diagnóstico, piezas, reparación...)" class="input-modern w-full resize-none leading-relaxed"></textarea>
|
||||
<button type="button" id="btnImproveAI" onclick="improveReportWithAI()" class="w-full bg-indigo-50 text-indigo-700 hover:bg-indigo-100 font-black py-3 rounded-xl border border-indigo-200 flex items-center justify-center gap-2 text-[10px] uppercase tracking-widest active:scale-95 transition-all">
|
||||
<i data-lucide="sparkles" class="w-4 h-4"></i> Mejorar con IA
|
||||
</button>
|
||||
<p class="text-[9px] text-slate-400 font-medium leading-relaxed">La IA redacta el texto de forma profesional. Puedes seguir editándolo después a mano.</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-5 rounded-3xl shadow-sm border border-slate-200 space-y-3 text-left">
|
||||
<div class="flex justify-between items-center">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-1.5"><i data-lucide="signature" class="w-4 h-4 text-blue-500"></i> Firma del cliente</p>
|
||||
<button type="button" onclick="clearSignature()" class="text-[9px] font-black text-slate-400 uppercase tracking-widest active:scale-95">Borrar</button>
|
||||
</div>
|
||||
<canvas id="signatureCanvas" class="w-full h-44 bg-slate-50 rounded-2xl border-2 border-dashed border-slate-300" style="touch-action:none;"></canvas>
|
||||
<p class="text-[9px] text-slate-400 font-medium">Pásale el móvil al cliente para que firme aquí antes de enviar el parte.</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white p-5 rounded-3xl shadow-sm border border-slate-200 space-y-3 text-left">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-1.5"><i data-lucide="phone-forwarded" class="w-4 h-4 text-blue-500"></i> Enviar también a otro número (opcional)</p>
|
||||
<input type="tel" id="wrExtraPhone" placeholder="Ej: 600123456" class="input-modern w-full">
|
||||
<p class="text-[9px] text-slate-400 font-medium">El parte se envía siempre al teléfono del cliente. Si quieres, aquí puedes añadir un número extra (p.ej. la aseguradora).</p>
|
||||
</div>
|
||||
|
||||
<button type="button" id="btnGenerateReport" onclick="generateAndSendWorkReport()" class="w-full bg-primary-dynamic text-white font-black py-4 rounded-2xl shadow-xl flex items-center justify-center gap-2 uppercase tracking-widest active:scale-95 transition-all text-xs">
|
||||
<i data-lucide="send" class="w-5 h-5"></i> Generar PDF y Enviar por WhatsApp
|
||||
</button>
|
||||
|
||||
<div class="pt-2">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2 flex items-center gap-1.5"><i data-lucide="history" class="w-4 h-4"></i> Partes anteriores de este servicio</p>
|
||||
<div id="wrHistoryListModal" class="space-y-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="wr-pdf-wrapper" class="hidden">
|
||||
<div id="wr-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="wrPdfLogo" src="" alt="Logo Empresa" style="max-width: 100%; max-height: 80px; object-fit: contain; display: none;">
|
||||
<h2 id="wrPdfLogoFallback" style="margin:0; color:#1e3a8a; 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="wrPdfCompanyName" style="font-size: 14px; font-weight: 900; color: #0f172a; margin-bottom: 2px;">Empresa S.L.</div>
|
||||
<div id="wrPdfCompanyDni"></div>
|
||||
<div id="wrPdfCompanyAddress"></div>
|
||||
<div id="wrPdfCompanyLocation"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 20px;">
|
||||
<div>
|
||||
<h1 style="font-size: 24px; font-weight: 900; color: #1e3a8a; margin: 0 0 5px 0; letter-spacing: -1px;">PARTE DE TRABAJO</h1>
|
||||
<div style="font-size: 12px; color: #64748b; line-height: 1.4;">
|
||||
<strong>Expediente:</strong> <span id="wrPdfRef">#---</span><br>
|
||||
<strong>Fecha:</strong> <span id="wrPdfDate">01/01/2026</span><br>
|
||||
<strong>Atendido por:</strong> <span id="wrPdfOperatorName">-</span>
|
||||
</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;">Cliente:</h3>
|
||||
<p id="wrPdfClientName" 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="wrPdfClientPhone">Teléfono</span><br>
|
||||
<span id="wrPdfClientAddress">Dirección</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 30px;">
|
||||
<h3 style="margin: 0 0 8px 0; font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: #94a3b8; font-weight: 800;">Trabajo realizado</h3>
|
||||
<div id="wrPdfText" style="font-size: 12px; color: #334155; line-height: 1.7; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 15px; white-space: pre-wrap;"></div>
|
||||
</div>
|
||||
|
||||
<div style="position: absolute; bottom: 30px; left: 40px; right: 40px;">
|
||||
<div style="border: 1px solid #e2e8f0; border-radius: 8px; padding: 15px; margin-bottom: 15px;">
|
||||
<p style="margin: 0 0 8px 0; font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: #94a3b8; font-weight: 800;">Firma del cliente (conforme al trabajo realizado)</p>
|
||||
<img id="wrPdfSignatureImg" src="" style="max-height: 90px; display: none;">
|
||||
<p id="wrPdfSignaturePlaceholder" style="font-size: 11px; color: #94a3b8; font-style: italic; display: none;">Sin firma</p>
|
||||
</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="newServiceModal" class="translate-y-full transition-transform duration-300 fixed inset-0 z-[150] flex flex-col bg-slate-50" style="display:none;">
|
||||
<div class="pt-safe bg-white border-b border-slate-200 flex justify-between items-center p-4 shrink-0 shadow-sm">
|
||||
<button type="button" onclick="closeNewServiceModal()" class="bg-slate-100 p-2.5 rounded-full text-slate-600 active:scale-90 transition-transform"><i data-lucide="x" class="w-6 h-6"></i></button>
|
||||
@@ -949,6 +1053,307 @@
|
||||
setTimeout(() => modal.style.display = 'none', 300);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 🧾 PARTE DE TRABAJO (texto con IA + firma táctil + PDF + envío WhatsApp)
|
||||
// ==========================================
|
||||
let wrOriginalText = "";
|
||||
let sigCtx = null, sigDrawing = false, sigHasContent = false;
|
||||
|
||||
function initSignatureCanvas() {
|
||||
const canvas = document.getElementById('signatureCanvas');
|
||||
const ratio = window.devicePixelRatio || 1;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = rect.width * ratio;
|
||||
canvas.height = rect.height * ratio;
|
||||
sigCtx = canvas.getContext('2d');
|
||||
sigCtx.scale(ratio, ratio);
|
||||
sigCtx.strokeStyle = '#0f172a';
|
||||
sigCtx.lineWidth = 2.5;
|
||||
sigCtx.lineCap = 'round';
|
||||
sigCtx.lineJoin = 'round';
|
||||
sigHasContent = false;
|
||||
|
||||
const getPos = (e) => {
|
||||
const r = canvas.getBoundingClientRect();
|
||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
||||
return { x: clientX - r.left, y: clientY - r.top };
|
||||
};
|
||||
const start = (e) => { e.preventDefault(); sigDrawing = true; const p = getPos(e); sigCtx.beginPath(); sigCtx.moveTo(p.x, p.y); };
|
||||
const move = (e) => { if (!sigDrawing) return; e.preventDefault(); const p = getPos(e); sigCtx.lineTo(p.x, p.y); sigCtx.stroke(); sigHasContent = true; };
|
||||
const end = (e) => { if (e) e.preventDefault(); sigDrawing = false; };
|
||||
|
||||
canvas.onmousedown = start; canvas.onmousemove = move; canvas.onmouseup = end; canvas.onmouseleave = end;
|
||||
canvas.ontouchstart = start; canvas.ontouchmove = move; canvas.ontouchend = end;
|
||||
}
|
||||
|
||||
function clearSignature() {
|
||||
const canvas = document.getElementById('signatureCanvas');
|
||||
if (sigCtx) sigCtx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
sigHasContent = false;
|
||||
}
|
||||
|
||||
async function openWorkReportModal() {
|
||||
if (!currentServiceId) return;
|
||||
document.getElementById('wrText').value = '';
|
||||
wrOriginalText = '';
|
||||
document.getElementById('wrExtraPhone').value = '';
|
||||
|
||||
const modal = document.getElementById('workReportModal');
|
||||
modal.style.display = 'flex';
|
||||
setTimeout(() => modal.classList.remove('translate-y-full'), 10);
|
||||
setTimeout(() => { initSignatureCanvas(); safeLoadIcons(); }, 60);
|
||||
|
||||
loadWorkReportHistory(currentServiceId);
|
||||
}
|
||||
|
||||
function closeWorkReportModal() {
|
||||
const modal = document.getElementById('workReportModal');
|
||||
modal.classList.add('translate-y-full');
|
||||
setTimeout(() => modal.style.display = 'none', 300);
|
||||
}
|
||||
|
||||
async function improveReportWithAI() {
|
||||
const textarea = document.getElementById('wrText');
|
||||
const text = textarea.value.trim();
|
||||
if (!text) { showToast("Escribe primero unas notas del trabajo", true); return; }
|
||||
wrOriginalText = text;
|
||||
|
||||
const btn = document.getElementById('btnImproveAI');
|
||||
const original = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> Mejorando...';
|
||||
safeLoadIcons();
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/work-reports/improve`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` },
|
||||
body: JSON.stringify({ text })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
textarea.value = data.text;
|
||||
showToast("Texto mejorado. Puedes editarlo antes de enviar.");
|
||||
} else {
|
||||
showToast(data.error || "No se pudo mejorar el texto", true);
|
||||
}
|
||||
} catch (e) {
|
||||
showToast("Error de conexión con la IA", true);
|
||||
} finally {
|
||||
btn.disabled = false; btn.innerHTML = original; safeLoadIcons();
|
||||
}
|
||||
}
|
||||
|
||||
async function generateAndSendWorkReport() {
|
||||
const text = document.getElementById('wrText').value.trim();
|
||||
if (!text) { showToast("Escribe el texto del parte de trabajo", true); return; }
|
||||
if (!sigHasContent) {
|
||||
if (!confirm("No hay firma del cliente. ¿Generar el parte sin firma?")) return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnGenerateReport');
|
||||
const original = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> Generando PDF...';
|
||||
safeLoadIcons();
|
||||
|
||||
try {
|
||||
// 1. Datos de la empresa (mismo patrón que en Presupuestos)
|
||||
let empNombre = "IntegraRepara", empDni = "", empAddress = "", empCity = "", empState = "", empZip = "", empLogo = 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 b = confData.config.billing_settings;
|
||||
empNombre = b.name || confData.config.full_name || empNombre;
|
||||
empDni = b.dni ? `CIF/NIF: ${b.dni}` : "";
|
||||
empAddress = b.address || "";
|
||||
empCity = b.city || ""; empState = b.state || ""; empZip = b.zip || "";
|
||||
} else {
|
||||
empNombre = confData.config.full_name || empNombre;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 2. Nombre del operario logueado
|
||||
let operatorName = "";
|
||||
try {
|
||||
const meRes = await fetch(`${API_URL}/auth/me`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
|
||||
const meData = await meRes.json();
|
||||
if (meData.ok && meData.user) operatorName = meData.user.full_name || "";
|
||||
} catch (e) {}
|
||||
|
||||
// 3. Rellenar la plantilla oculta del PDF
|
||||
document.getElementById('wrPdfCompanyName').innerText = empNombre;
|
||||
document.getElementById('wrPdfCompanyDni').innerText = empDni;
|
||||
document.getElementById('wrPdfCompanyAddress').innerText = empAddress;
|
||||
document.getElementById('wrPdfCompanyLocation').innerText = [empZip, empCity, empState ? `(${empState})` : ''].filter(Boolean).join(' ');
|
||||
|
||||
const logoImg = document.getElementById('wrPdfLogo');
|
||||
const logoTxt = document.getElementById('wrPdfLogoFallback');
|
||||
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('wrPdfRef').innerText = document.getElementById('detRef').innerText || '#---';
|
||||
document.getElementById('wrPdfDate').innerText = new Date().toLocaleDateString('es-ES');
|
||||
document.getElementById('wrPdfOperatorName').innerText = operatorName || "-";
|
||||
document.getElementById('wrPdfClientName').innerText = document.getElementById('detName').innerText || "Cliente";
|
||||
document.getElementById('wrPdfClientPhone').innerText = document.getElementById('detPhoneRaw').value || "-";
|
||||
document.getElementById('wrPdfClientAddress').innerText = document.getElementById('detAddress').innerText || "-";
|
||||
document.getElementById('wrPdfText').innerText = text;
|
||||
|
||||
const sigCanvas = document.getElementById('signatureCanvas');
|
||||
const sigImg = document.getElementById('wrPdfSignatureImg');
|
||||
const sigPlaceholder = document.getElementById('wrPdfSignaturePlaceholder');
|
||||
let signatureDataUrl = null;
|
||||
if (sigHasContent) {
|
||||
signatureDataUrl = sigCanvas.toDataURL('image/png');
|
||||
sigImg.src = signatureDataUrl;
|
||||
sigImg.style.display = 'block';
|
||||
sigPlaceholder.style.display = 'none';
|
||||
} else {
|
||||
sigImg.style.display = 'none';
|
||||
sigPlaceholder.style.display = 'block';
|
||||
}
|
||||
|
||||
// 4. Generar el PDF en base64 a partir de la plantilla oculta
|
||||
const wrapper = document.getElementById('wr-pdf-wrapper');
|
||||
wrapper.classList.remove('hidden');
|
||||
wrapper.style.position = 'absolute';
|
||||
wrapper.style.left = '-9999px';
|
||||
const element = document.getElementById('wr-pdf-content');
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
|
||||
const pdfDataUri = await html2pdf().set({
|
||||
margin: 0,
|
||||
image: { type: 'jpeg', quality: 1 },
|
||||
html2canvas: { scale: 2, useCORS: true, logging: false },
|
||||
jsPDF: { unit: 'in', format: 'a4', orientation: 'portrait' }
|
||||
}).from(element).outputPdf('datauristring');
|
||||
|
||||
wrapper.classList.add('hidden');
|
||||
wrapper.style.position = '';
|
||||
wrapper.style.left = '';
|
||||
|
||||
// 5. Guardar el parte en el backend
|
||||
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> Guardando...';
|
||||
safeLoadIcons();
|
||||
|
||||
const saveRes = await fetch(`${API_URL}/services/${currentServiceId}/work-report`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` },
|
||||
body: JSON.stringify({
|
||||
raw_text: wrOriginalText || text,
|
||||
final_text: text,
|
||||
signature_data: signatureDataUrl,
|
||||
pdf_base64: pdfDataUri
|
||||
})
|
||||
});
|
||||
const saveData = await saveRes.json();
|
||||
if (!saveData.ok) { showToast(saveData.error || "Error al guardar el parte", true); return; }
|
||||
|
||||
// 6. Enviarlo por WhatsApp: al cliente automáticamente + número extra si se indicó
|
||||
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> Enviando por WhatsApp...';
|
||||
safeLoadIcons();
|
||||
|
||||
const extraPhone = document.getElementById('wrExtraPhone').value.trim();
|
||||
const phones = [];
|
||||
if (saveData.report.client_phone) phones.push(saveData.report.client_phone);
|
||||
if (extraPhone) phones.push(extraPhone);
|
||||
|
||||
const sendRes = await fetch(`${API_URL}/work-reports/${saveData.report.id}/send`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` },
|
||||
body: JSON.stringify({ phones })
|
||||
});
|
||||
const sendData = await sendRes.json();
|
||||
|
||||
if (sendData.ok) {
|
||||
showToast("✅ Parte generado y enviado por WhatsApp");
|
||||
} else {
|
||||
const firstFail = (sendData.results || []).find(r => !r.ok);
|
||||
showToast("⚠️ Parte guardado, pero no se pudo enviar por WhatsApp" + (firstFail?.detail ? `: ${firstFail.detail}` : ""), true);
|
||||
}
|
||||
|
||||
closeWorkReportModal();
|
||||
sendLog("Parte de Trabajo", "Se ha generado un parte de trabajo para este servicio.");
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast("❌ Error generando el parte de trabajo", true);
|
||||
} finally {
|
||||
btn.disabled = false; btn.innerHTML = original; safeLoadIcons();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkReportHistory(serviceId) {
|
||||
const list = document.getElementById('wrHistoryListModal');
|
||||
list.innerHTML = '<p class="text-center text-[10px] text-slate-400 font-bold py-3">Cargando...</p>';
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/services/${serviceId}/work-reports`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } });
|
||||
const data = await res.json();
|
||||
if (!data.ok || !data.reports || data.reports.length === 0) {
|
||||
list.innerHTML = '<p class="text-center text-[10px] text-slate-400 font-bold py-3">Sin partes anteriores.</p>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = data.reports.map(r => {
|
||||
const d = new Date(r.created_at).toLocaleString('es-ES', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||
const wasSent = Array.isArray(r.sent_to) && r.sent_to.length > 0;
|
||||
const sentBadge = wasSent
|
||||
? `<span class="text-[8px] font-black bg-emerald-50 text-emerald-600 px-2 py-0.5 rounded uppercase border border-emerald-100">Enviado</span>`
|
||||
: `<span class="text-[8px] font-black bg-amber-50 text-amber-600 px-2 py-0.5 rounded uppercase border border-amber-100">No enviado</span>`;
|
||||
const preview = (r.final_text || '').substring(0, 60);
|
||||
return `
|
||||
<div class="bg-white border border-slate-200 rounded-xl p-3 flex items-center justify-between gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-1.5 mb-0.5">${sentBadge}<span class="text-[9px] font-bold text-slate-400">${d}</span></div>
|
||||
<p class="text-[10px] font-bold text-slate-600 truncate">${preview}</p>
|
||||
</div>
|
||||
<div class="flex gap-1.5 shrink-0">
|
||||
<button onclick="viewWorkReportPdf(${r.id})" class="w-8 h-8 bg-slate-50 border border-slate-200 rounded-full flex items-center justify-center active:scale-90"><i data-lucide="eye" class="w-3.5 h-3.5 text-slate-500"></i></button>
|
||||
<button onclick="resendWorkReport(${r.id})" class="w-8 h-8 bg-slate-50 border border-slate-200 rounded-full flex items-center justify-center active:scale-90"><i data-lucide="send" class="w-3.5 h-3.5 text-blue-500"></i></button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
safeLoadIcons();
|
||||
} catch (e) {
|
||||
list.innerHTML = '<p class="text-center text-[10px] text-rose-400 font-bold py-3">Error al cargar el historial.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function viewWorkReportPdf(reportId) {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/work-reports/${reportId}/pdf`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } });
|
||||
const data = await res.json();
|
||||
if (!data.ok || !data.pdf_base64) return showToast("No se pudo abrir el PDF", true);
|
||||
const base64 = data.pdf_base64.includes(',') ? data.pdf_base64.split(',').pop() : data.pdf_base64;
|
||||
const byteChars = atob(base64);
|
||||
const byteNumbers = new Array(byteChars.length);
|
||||
for (let i = 0; i < byteChars.length; i++) byteNumbers[i] = byteChars.charCodeAt(i);
|
||||
const blob = new Blob([new Uint8Array(byteNumbers)], { type: 'application/pdf' });
|
||||
window.open(URL.createObjectURL(blob), '_blank');
|
||||
} catch (e) {
|
||||
showToast("Error al abrir el PDF", true);
|
||||
}
|
||||
}
|
||||
|
||||
async function resendWorkReport(reportId) {
|
||||
if (!confirm("¿Reenviar este parte por WhatsApp al teléfono del cliente?")) return;
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/work-reports/${reportId}/send`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` },
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) { showToast("✅ Reenviado por WhatsApp"); loadWorkReportHistory(currentServiceId); }
|
||||
else showToast("❌ No se pudo reenviar", true);
|
||||
} catch (e) { showToast("Error de conexión", true); }
|
||||
}
|
||||
|
||||
function sendLog(action, details) {
|
||||
if(!currentServiceId) return;
|
||||
fetch(`${API_URL}/services/${currentServiceId}/log`, {
|
||||
|
||||
Reference in New Issue
Block a user