Actualizar presupuestos.html

This commit is contained in:
2026-03-29 21:08:12 +00:00
parent e8160c0b34
commit 0f881ae7c4

View File

@@ -114,6 +114,44 @@
</form>
</main>
<div id="appointmentModal" class="fixed inset-0 bg-slate-900/75 hidden z-[100] flex-col justify-end backdrop-blur-sm">
<div class="bg-white w-full rounded-t-[2.5rem] p-6 pt-8 pb-safe transition-transform transform translate-y-full duration-300 max-h-[90vh] overflow-y-auto no-scrollbar" id="apptModalSheet">
<div class="flex justify-between items-center mb-6">
<div>
<span class="text-[10px] font-black text-blue-600 uppercase tracking-widest bg-blue-50 px-2 py-1 rounded-md">Agendar Cita</span>
<h3 class="text-xl font-black text-slate-800 mt-2 leading-tight" id="apptClientName">Cliente</h3>
</div>
<button onclick="closeAppointmentModal()" class="p-2 bg-slate-100 rounded-full text-slate-500 active:scale-95"><i data-lucide="x" class="w-5 h-5"></i></button>
</div>
<input type="hidden" id="apptBudgetId">
<div class="space-y-4">
<div>
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Gremio Especialista *</label>
<select id="apptGuild" class="w-full bg-slate-50 border border-slate-200 px-4 py-3 rounded-xl text-sm font-bold outline-none focus:border-blue-500">
<option value="">Selecciona gremio...</option>
</select>
</div>
<div>
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Fecha de la visita *</label>
<input type="date" id="apptDate" onchange="checkAgendaForDate(this.value)" class="w-full bg-slate-50 border border-slate-200 px-4 py-3 rounded-xl text-sm font-bold outline-none focus:border-blue-500 text-blue-600">
</div>
<div id="agendaPreview" class="hidden bg-blue-50/50 border border-blue-100 rounded-2xl p-4 shadow-inner">
<p class="text-[10px] font-black text-blue-600 uppercase tracking-widest mb-3 flex items-center gap-1.5"><i data-lucide="map-pin" class="w-3.5 h-3.5"></i> Tu ruta este día</p>
<div id="agendaList" class="space-y-2 text-xs font-medium text-slate-600"></div>
</div>
<div>
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Hora de llegada (Aprox) *</label>
<input type="time" id="apptTime" class="w-full bg-slate-50 border border-slate-200 px-4 py-3 rounded-xl text-sm font-bold outline-none focus:border-blue-500 text-blue-600">
</div>
<button onclick="confirmAppointment()" id="btnConfirmAppt" class="w-full bg-emerald-500 text-white py-4 rounded-xl font-black uppercase tracking-widest shadow-md hover:bg-emerald-600 active:scale-95 transition-transform mt-4 flex items-center justify-center gap-2">
<i data-lucide="check-circle" class="w-5 h-5"></i> Confirmar Cita
</button>
</div>
</div>
</div>
<div id="pdf-wrapper" class="hidden">
<div id="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;">
@@ -470,7 +508,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 +516,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 +562,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>
@@ -535,8 +573,6 @@ async function downloadPDF(id) {
</div>
`;
}).join('');
lucide.createIcons();
}
// ------------------ BUSCADOR DE CLIENTES AUTOMÁTICO ------------------
async function searchClientByPhone() {
@@ -681,6 +717,114 @@ 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>'; }
}
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 {
const res = await fetch(`${API_URL}/services`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json();
if(data.ok && data.services) {
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;
}
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('');
}
} 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...';
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>