Actualizar presupuestos.html

This commit is contained in:
2026-03-29 20:29:51 +00:00
parent db8cb8f3a0
commit e1378bd048

View File

@@ -455,7 +455,7 @@ async function downloadPDF(id) {
}
}
// ------------------ RENDERIZAR LA LISTA EN PANTALLA ------------------
// ------------------ RENDERIZAR LA LISTA EN PANTALLA ------------------
function renderBudgetsList(budgets) {
const container = document.getElementById('budgetsList');
if(budgets.length === 0) {
@@ -470,7 +470,6 @@ async function downloadPDF(id) {
}
container.innerHTML = budgets.map(b => {
// 🔴 LÓGICA DE ESTADOS Y CADUCIDAD (Igual que en el Portal)
let bDate = new Date(b.created_at);
let diffDays = Math.ceil(Math.abs(new Date() - bDate) / (1000 * 60 * 60 * 24));
let isExpired = diffDays > 30;
@@ -479,27 +478,33 @@ async function downloadPDF(id) {
let statusText = "Pte. Resolver";
let icon = "clock";
// Prioridad de estados
if (b.status === 'paid') {
statusColor = "bg-emerald-100 text-emerald-700 border border-emerald-200";
statusText = "Pagado Online";
icon = "badge-check";
statusColor = "bg-emerald-100 text-emerald-700 border border-emerald-200"; statusText = "Pagado Online"; icon = "badge-check";
} else if (isExpired && b.status !== 'rejected') {
statusColor = "bg-slate-100 text-slate-500 border border-slate-200";
statusText = "Caducado";
icon = "clock-alert";
statusColor = "bg-slate-100 text-slate-500 border border-slate-200"; statusText = "Caducado"; icon = "clock-alert";
} else if (b.status === 'accepted' || b.status === 'converted') {
statusColor = "bg-blue-100 text-blue-700 border border-blue-200";
statusText = "Aceptado (Pte. Pago)";
icon = "clock";
statusColor = "bg-blue-100 text-blue-700 border border-blue-200"; statusText = "Aceptado (Pte. Pago)"; icon = "clock";
} else if (b.status === 'rejected') {
statusColor = "bg-rose-100 text-rose-700";
statusText = "Rechazado";
icon = "x-circle";
statusColor = "bg-rose-100 text-rose-700"; statusText = "Rechazado"; icon = "x-circle";
}
const d = bDate.toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: '2-digit' });
// LÓGICA DE BOTONES PARA EL TÉCNICO
let actionBtns = '';
if (b.status === 'pending' && !isExpired) {
actionBtns = `
<button onclick="updateStatus(${b.id}, 'accepted')" class="w-9 h-9 bg-emerald-50 text-emerald-600 rounded-full flex items-center justify-center active:scale-95 transition-transform border border-emerald-100"><i data-lucide="check" class="w-5 h-5"></i></button>
<button onclick="updateStatus(${b.id}, 'rejected')" class="w-9 h-9 bg-rose-50 text-rose-600 rounded-full flex items-center justify-center active:scale-95 transition-transform border border-rose-100"><i data-lucide="x" class="w-5 h-5"></i></button>
`;
} else if ((b.status === 'accepted' || b.status === 'paid') && !isExpired) {
actionBtns = `<button onclick="openAppointmentModal(${b.id}, '${b.client_name}')" class="bg-emerald-500 text-white px-3 py-1.5 rounded-xl font-black text-[10px] uppercase tracking-widest shadow-md hover:bg-emerald-600 active:scale-95 transition-transform">Dar Cita</button>`;
} else if (b.status === 'converted') {
actionBtns = `<span class="text-[9px] font-black text-blue-600 uppercase tracking-widest bg-blue-50 px-2 py-1 rounded-md">Agendado</span>`;
} else {
actionBtns = `<span class="text-[9px] font-black text-slate-400 uppercase tracking-widest">Cerrado</span>`;
}
return `
<div class="bg-white p-5 rounded-[2rem] border border-slate-100 shadow-sm flex flex-col gap-4 relative overflow-hidden fade-in">
<div class="absolute top-0 left-0 w-1.5 h-full ${b.status === 'paid' ? 'bg-emerald-500' : (isExpired ? 'bg-slate-300' : 'bg-primary-dynamic')}"></div>
@@ -519,13 +524,8 @@ async function downloadPDF(id) {
</div>
<div class="pt-3 border-t border-slate-100 flex justify-between items-center pl-2">
<div class="flex gap-2">
${ (b.status === 'pending' && !isExpired) ? `
<button onclick="updateStatus(${b.id}, 'accepted')" class="w-9 h-9 bg-emerald-50 text-emerald-600 rounded-full flex items-center justify-center active:scale-95 transition-transform border border-emerald-100"><i data-lucide="check" class="w-5 h-5"></i></button>
<button onclick="updateStatus(${b.id}, 'rejected')" class="w-9 h-9 bg-rose-50 text-rose-600 rounded-full flex items-center justify-center active:scale-95 transition-transform border border-rose-100"><i data-lucide="x" class="w-5 h-5"></i></button>
` : (b.status === 'paid' ?
`<span class="text-[9px] font-black text-emerald-600 uppercase tracking-widest bg-emerald-50 px-2 py-1 rounded-md">Cobrado</span>` :
`<span class="text-[9px] font-black text-slate-400 uppercase tracking-widest">Cerrado</span>`) }
<div class="flex gap-2 items-center">
${actionBtns}
</div>
<div class="flex gap-2">
<button onclick="downloadPDF(${b.id})" class="w-9 h-9 bg-slate-50 border border-slate-200 text-slate-600 rounded-full flex items-center justify-center active:scale-95 transition-transform"><i data-lucide="download" class="w-4 h-4"></i></button>
@@ -681,6 +681,138 @@ async function downloadPDF(id) {
lucide.createIcons();
}
}
// ------------------ SISTEMA DE CITA Y AGENDA EN TIEMPO REAL ------------------
function openAppointmentModal(id, clientName) {
document.getElementById('apptBudgetId').value = id;
document.getElementById('apptClientName').innerText = clientName || "Cliente";
document.getElementById('apptDate').value = "";
document.getElementById('apptTime').value = "";
document.getElementById('agendaPreview').classList.add('hidden');
document.getElementById('appointmentModal').classList.remove('hidden');
document.getElementById('appointmentModal').classList.add('flex');
setTimeout(() => document.getElementById('apptModalSheet').classList.remove('translate-y-full'), 10);
loadGuilds();
}
function closeAppointmentModal() {
document.getElementById('apptModalSheet').classList.add('translate-y-full');
setTimeout(() => {
document.getElementById('appointmentModal').classList.add('hidden');
document.getElementById('appointmentModal').classList.remove('flex');
}, 300);
}
async function loadGuilds() {
const gSel = document.getElementById('apptGuild');
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>'; }
}
// 🟢 MAGIA: BUSCAR LA RUTA DEL TÉCNICO ESE DÍA
async function checkAgendaForDate(dateStr) {
const preview = document.getElementById('agendaPreview');
const list = document.getElementById('agendaList');
if(!dateStr) {
preview.classList.add('hidden');
return;
}
preview.classList.remove('hidden');
list.innerHTML = '<div class="text-center text-blue-400 py-2"><i data-lucide="loader-2" class="w-4 h-4 animate-spin mx-auto"></i></div>';
lucide.createIcons();
try {
// Descargamos las averías del técnico logueado
const res = await fetch(`${API_URL}/services`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
if(data.ok && data.services) {
// Filtramos solo las que coinciden con el día seleccionado
const dayServices = data.services.filter(s => s.scheduled_date === dateStr && s.status !== 'archived');
if(dayServices.length === 0) {
list.innerHTML = '<p class="text-emerald-600 font-bold text-center py-2 bg-white rounded-lg border border-emerald-100 shadow-sm">Libre: No tienes nada agendado este día.</p>';
return;
}
// Ordenar por hora
dayServices.sort((a, b) => (a.scheduled_time || "23:59").localeCompare(b.scheduled_time || "23:59"));
list.innerHTML = dayServices.map(s => {
const raw = s.raw_data || {};
const pob = raw['Población'] || raw['POBLACION-PROVINCIA'] || raw['Dirección'] || 'Sin ubicación';
const time = s.scheduled_time ? s.scheduled_time.substring(0,5) : 'S/H';
return `
<div class="flex justify-between items-center bg-white p-2.5 rounded-lg border border-blue-100 shadow-sm">
<span class="font-black text-blue-700 bg-blue-50 px-2 py-0.5 rounded border border-blue-100">${time}</span>
<span class="text-blue-600 truncate max-w-[150px] text-right font-bold text-[10px] uppercase tracking-widest" title="${pob}">${pob}</span>
</div>
`;
}).join('');
} else {
list.innerHTML = '<p class="text-slate-500 text-center py-2">No se pudo cargar la agenda.</p>';
}
} catch(e) {
list.innerHTML = '<p class="text-rose-500 text-center py-2">Error de conexión.</p>';
}
}
async function confirmAppointment() {
const id = document.getElementById('apptBudgetId').value;
const guild_id = document.getElementById('apptGuild').value;
const date = document.getElementById('apptDate').value;
const time = document.getElementById('apptTime').value;
if (!guild_id || !date || !time) return showToast("⚠️ Gremio, Fecha y Hora son obligatorios.");
const btn = document.getElementById('btnConfirmAppt');
btn.disabled = true;
btn.innerHTML = '<i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i> Procesando...';
lucide.createIcons();
// El técnico se auto-asigna al enviar assigned_to: 'self'
const payload = {
guild_id: guild_id,
date: date,
time: time,
use_automation: false,
assigned_to: 'self'
};
try {
const res = await fetch(`${API_URL}/budgets/${id}/convert`, {
method: 'POST',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify(payload)
});
const data = await res.json();
if(data.ok) {
showToast("✅ ¡Cita agendada con éxito!");
closeAppointmentModal();
fetchBudgets();
} else {
showToast("❌ " + (data.error || "Error al agendar cita."));
}
} catch(e) {
showToast("❌ Error de conexión al convertir.");
} finally {
btn.disabled = false;
btn.innerHTML = '<i data-lucide="check-circle" class="w-5 h-5"></i> Confirmar Cita';
lucide.createIcons();
}
}
</script>
</body>
</html>