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) {
|
function renderBudgetsList(budgets) {
|
||||||
const container = document.getElementById('budgetsList');
|
const container = document.getElementById('budgetsList');
|
||||||
if(budgets.length === 0) {
|
if(budgets.length === 0) {
|
||||||
@@ -470,6 +470,7 @@ async function downloadPDF(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = budgets.map(b => {
|
container.innerHTML = budgets.map(b => {
|
||||||
|
// 🔴 LÓGICA DE ESTADOS Y CADUCIDAD (Igual que en el Portal)
|
||||||
let bDate = new Date(b.created_at);
|
let bDate = new Date(b.created_at);
|
||||||
let diffDays = Math.ceil(Math.abs(new Date() - bDate) / (1000 * 60 * 60 * 24));
|
let diffDays = Math.ceil(Math.abs(new Date() - bDate) / (1000 * 60 * 60 * 24));
|
||||||
let isExpired = diffDays > 30;
|
let isExpired = diffDays > 30;
|
||||||
@@ -478,33 +479,27 @@ async function downloadPDF(id) {
|
|||||||
let statusText = "Pte. Resolver";
|
let statusText = "Pte. Resolver";
|
||||||
let icon = "clock";
|
let icon = "clock";
|
||||||
|
|
||||||
|
// Prioridad de estados
|
||||||
if (b.status === 'paid') {
|
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') {
|
} 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') {
|
} 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') {
|
} 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' });
|
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 `
|
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="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>
|
<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>
|
||||||
|
|
||||||
<div class="pt-3 border-t border-slate-100 flex justify-between items-center pl-2">
|
<div class="pt-3 border-t border-slate-100 flex justify-between items-center pl-2">
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2">
|
||||||
${actionBtns}
|
${ (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>
|
||||||
<div class="flex gap-2">
|
<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>
|
<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();
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user