Actualizar presupuestos.html
This commit is contained in:
@@ -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,6 +470,7 @@ 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;
|
||||
@@ -478,33 +479,27 @@ 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>
|
||||
@@ -524,8 +519,13 @@ 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 items-center">
|
||||
${actionBtns}
|
||||
<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>
|
||||
<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,138 +681,6 @@ 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>
|
||||
Reference in New Issue
Block a user