Actualizar presupuestos.html
This commit is contained in:
@@ -114,6 +114,44 @@
|
|||||||
</form>
|
</form>
|
||||||
</main>
|
</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-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;">
|
<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 => {
|
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;
|
||||||
@@ -479,27 +516,33 @@ 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";
|
statusColor = "bg-emerald-100 text-emerald-700 border border-emerald-200"; statusText = "Pagado Online"; icon = "badge-check";
|
||||||
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";
|
statusColor = "bg-slate-100 text-slate-500 border border-slate-200"; statusText = "Caducado"; icon = "clock-alert";
|
||||||
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";
|
statusColor = "bg-blue-100 text-blue-700 border border-blue-200"; statusText = "Aceptado (Pte. Pago)"; icon = "clock";
|
||||||
statusText = "Aceptado (Pte. Pago)";
|
|
||||||
icon = "clock";
|
|
||||||
} else if (b.status === 'rejected') {
|
} else if (b.status === 'rejected') {
|
||||||
statusColor = "bg-rose-100 text-rose-700";
|
statusColor = "bg-rose-100 text-rose-700"; statusText = "Rechazado"; icon = "x-circle";
|
||||||
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>
|
||||||
@@ -519,13 +562,8 @@ 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">
|
<div class="flex gap-2 items-center">
|
||||||
${ (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>
|
|
||||||
` : (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>
|
||||||
@@ -535,8 +573,6 @@ async function downloadPDF(id) {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
lucide.createIcons();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------ BUSCADOR DE CLIENTES AUTOMÁTICO ------------------
|
// ------------------ BUSCADOR DE CLIENTES AUTOMÁTICO ------------------
|
||||||
async function searchClientByPhone() {
|
async function searchClientByPhone() {
|
||||||
@@ -681,6 +717,114 @@ 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>'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user