El contador de menu.html (badgeBolsa) solo contaba avisos con gremio coincidente, igual que antes de la regla nueva de solicitar.html que deja ver los avisos sin gremio a cualquiera. Ahora usa el mismo criterio (guildOk) y la burbuja muestra "X servicios y X sin gremio" en vez de solo "X libres", para que cuadre con lo que se ve al entrar en Solicitar Servicios.
279 lines
16 KiB
HTML
279 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
|
<title>Menú - IntegraRepara</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
<style>
|
|
/* VARIABLES CORPORATIVAS DINÁMICAS */
|
|
:root {
|
|
--primary: #2563eb;
|
|
--secondary: #f59e0b;
|
|
--app-bg: #f4f7f9;
|
|
}
|
|
|
|
body { background-color: var(--app-bg); -webkit-tap-highlight-color: transparent; }
|
|
.fade-in { animation: fadeIn 0.4s ease-out forwards; }
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
|
|
|
|
/* Ajuste limpio para el scroll sin barra abajo */
|
|
.main-content { padding-bottom: calc(env(safe-area-inset-bottom) + 30px); }
|
|
|
|
.app-card { box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05); }
|
|
|
|
/* Clases CSS dinámicas para aplicar las variables */
|
|
.bg-primary-dynamic { background-color: var(--primary) !important; }
|
|
.text-primary-dynamic { color: var(--primary) !important; }
|
|
</style>
|
|
</head>
|
|
<body class="text-slate-800 font-sans antialiased h-screen flex flex-col overflow-hidden relative">
|
|
|
|
<div class="absolute top-0 left-0 w-full h-48 bg-primary-dynamic rounded-b-[2.5rem] z-0 transition-colors duration-500"></div>
|
|
|
|
<header class="px-6 pt-safe mt-8 pb-6 z-10 relative text-white shrink-0">
|
|
<div class="flex justify-between items-start mb-6">
|
|
<div>
|
|
<p class="text-[10px] font-black text-white/70 uppercase tracking-widest mb-0.5" id="headerDate">HOY</p>
|
|
<h1 class="text-3xl font-black tracking-tight leading-none">Hola, <span id="opName"></span></h1>
|
|
</div>
|
|
|
|
<button onclick="logout()" class="flex flex-col items-center justify-center w-12 h-12 bg-white/20 backdrop-blur-md rounded-full border border-white/30 shadow-inner hover:bg-rose-500 hover:border-rose-400 transition-colors active:scale-95" title="Cerrar sesión">
|
|
<i data-lucide="log-out" class="w-5 h-5 text-white"></i>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="flex-1 overflow-y-auto px-5 pt-4 main-content z-10 relative fade-in">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
|
|
<a href="asignados.html" class="app-card bg-white p-6 rounded-[2rem] border border-slate-100 flex flex-col items-center text-center active:scale-95 transition-transform relative overflow-hidden">
|
|
<div class="absolute top-0 right-0 w-16 h-16 bg-amber-50 rounded-bl-full z-0"></div>
|
|
<div class="w-14 h-14 bg-amber-100 text-amber-500 rounded-2xl flex items-center justify-center mb-4 z-10 shadow-inner">
|
|
<i data-lucide="inbox" class="w-7 h-7"></i>
|
|
</div>
|
|
<h3 class="font-black text-slate-800 text-sm leading-tight z-10">Asignados<br>Sin Cita</h3>
|
|
<span id="badgeAsignados" class="hidden mt-3 bg-rose-500 text-white text-[10px] font-black px-2.5 py-1 rounded-full uppercase tracking-widest shadow-sm z-10 animate-pulse">0 pendientes</span>
|
|
</a>
|
|
|
|
<a href="solicitar.html" class="app-card bg-white p-6 rounded-[2rem] border border-slate-100 flex flex-col items-center text-center active:scale-95 transition-transform relative overflow-hidden">
|
|
<div class="absolute top-0 right-0 w-16 h-16 bg-emerald-50 rounded-bl-full z-0"></div>
|
|
<div class="w-14 h-14 bg-emerald-100 text-emerald-500 rounded-2xl flex items-center justify-center mb-4 z-10 shadow-inner">
|
|
<i data-lucide="bell-ring" class="w-7 h-7"></i>
|
|
</div>
|
|
<h3 class="font-black text-slate-800 text-sm leading-tight z-10">Solicitar<br>Servicios</h3>
|
|
<span id="badgeBolsa" class="hidden mt-3 bg-emerald-500 text-white text-[10px] font-black px-2.5 py-1 rounded-full uppercase tracking-widest shadow-sm z-10 animate-pulse">0 libres</span>
|
|
</a>
|
|
|
|
<a href="calendario.html" class="app-card bg-white p-6 rounded-[2rem] border border-slate-100 flex flex-col items-center text-center active:scale-95 transition-transform relative overflow-hidden">
|
|
<div class="absolute top-0 right-0 w-16 h-16 bg-blue-50 rounded-bl-full z-0"></div>
|
|
|
|
<div id="badgeChat" class="hidden absolute top-4 left-4 w-6 h-6 bg-red-600 text-white text-[10px] font-black flex items-center justify-center rounded-full shadow-lg z-20 animate-bounce border-2 border-white">0</div>
|
|
|
|
<div class="w-14 h-14 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center mb-4 z-10 shadow-inner">
|
|
<i data-lucide="calendar-days" class="w-7 h-7"></i>
|
|
</div>
|
|
<h3 class="font-black text-slate-800 text-sm leading-tight z-10">Mi<br>Calendario</h3>
|
|
</a>
|
|
|
|
<a href="buscar.html" class="app-card bg-white p-6 rounded-[2rem] border border-slate-100 flex flex-col items-center text-center active:scale-95 transition-transform relative overflow-hidden">
|
|
<div class="absolute top-0 right-0 w-16 h-16 bg-purple-50 rounded-bl-full z-0"></div>
|
|
<div class="w-14 h-14 bg-purple-100 text-purple-600 rounded-2xl flex items-center justify-center mb-4 z-10 shadow-inner">
|
|
<i data-lucide="search" class="w-7 h-7"></i>
|
|
</div>
|
|
<h3 class="font-black text-slate-800 text-sm leading-tight z-10">Buscar<br>Servicio</h3>
|
|
</a>
|
|
|
|
<a href="ranking.html" class="app-card bg-white p-6 rounded-[2rem] border border-slate-100 flex flex-col items-center text-center active:scale-95 transition-transform relative overflow-hidden">
|
|
<div class="absolute top-0 right-0 w-16 h-16 bg-indigo-50 rounded-bl-full z-0"></div>
|
|
<div class="w-14 h-14 bg-indigo-100 text-indigo-600 rounded-2xl flex items-center justify-center mb-4 z-10 shadow-inner">
|
|
<i data-lucide="award" class="w-7 h-7"></i>
|
|
</div>
|
|
<h3 class="font-black text-slate-800 text-sm leading-tight z-10">Ranking<br>y Estadísticas</h3>
|
|
</a>
|
|
|
|
<a href="configuracion.html" class="app-card bg-white p-6 rounded-[2rem] border border-slate-100 flex flex-col items-center text-center active:scale-95 transition-transform relative overflow-hidden">
|
|
<div class="absolute top-0 right-0 w-16 h-16 bg-slate-100 rounded-bl-full z-0"></div>
|
|
<div class="w-14 h-14 bg-slate-100 text-slate-500 rounded-2xl flex items-center justify-center mb-4 z-10 shadow-inner">
|
|
<i data-lucide="settings" class="w-7 h-7"></i>
|
|
</div>
|
|
<h3 class="font-black text-slate-800 text-sm leading-tight z-10">Mi<br>Configuración</h3>
|
|
</a>
|
|
|
|
<a href="presupuestos.html" class="app-card bg-white p-6 rounded-[2rem] border border-slate-100 flex flex-col items-center text-center active:scale-95 transition-transform relative overflow-hidden">
|
|
<div class="absolute top-0 right-0 w-16 h-16 bg-indigo-50 rounded-bl-full z-0"></div>
|
|
<div class="w-14 h-14 bg-indigo-100 text-indigo-600 rounded-2xl flex items-center justify-center mb-4 z-10 shadow-inner">
|
|
<i data-lucide="file-signature" class="w-7 h-7"></i>
|
|
</div>
|
|
<h3 class="font-black text-slate-800 text-sm leading-tight z-10">Crear<br>Presupuesto</h3>
|
|
</a>
|
|
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
const API_URL = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
|
? 'http://localhost:3000'
|
|
: 'https://integrarepara-api.integrarepara.es';
|
|
|
|
let userZones = [];
|
|
let userGuilds = [];
|
|
let systemStatuses = [];
|
|
|
|
async function applyTheme() {
|
|
try {
|
|
let theme = JSON.parse(localStorage.getItem('app_theme'));
|
|
const res = await fetch(`${API_URL}/config/company`, {
|
|
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
|
|
});
|
|
const data = await res.json();
|
|
if(data.ok && data.config && data.config.portal_settings && data.config.portal_settings.app_settings) {
|
|
theme = data.config.portal_settings.app_settings;
|
|
localStorage.setItem('app_theme', JSON.stringify(theme));
|
|
}
|
|
if(theme) {
|
|
document.documentElement.style.setProperty('--primary', theme.primary);
|
|
document.documentElement.style.setProperty('--secondary', theme.secondary);
|
|
document.documentElement.style.setProperty('--app-bg', theme.bg);
|
|
}
|
|
} catch (e) { console.warn("Usando tema por defecto"); }
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", async () => {
|
|
const token = localStorage.getItem("token");
|
|
const role = localStorage.getItem("role");
|
|
|
|
if (!token || (role !== 'operario' && role !== 'operario_cerrado')) {
|
|
window.location.href = "index.html";
|
|
return;
|
|
}
|
|
|
|
await applyTheme();
|
|
lucide.createIcons();
|
|
|
|
const rawName = localStorage.getItem("userName") || "Operario";
|
|
document.getElementById('opName').innerText = rawName.split(' ')[0];
|
|
|
|
const options = { weekday: 'long', day: 'numeric', month: 'long' };
|
|
document.getElementById('headerDate').innerText = new Date().toLocaleDateString('es-ES', options);
|
|
|
|
await loadStatuses();
|
|
await fetchUserData();
|
|
fetchBadges();
|
|
fetchChatNotifications(); // <-- Nueva función
|
|
});
|
|
|
|
async function fetchChatNotifications() {
|
|
try {
|
|
const res = await fetch(`${API_URL}/worker/notifications`, {
|
|
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
|
|
});
|
|
const data = await res.json();
|
|
if (data.ok && data.unreadCount > 0) {
|
|
const badge = document.getElementById('badgeChat');
|
|
badge.innerText = data.unreadCount;
|
|
badge.classList.remove('hidden');
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
async function loadStatuses() {
|
|
try {
|
|
const res = await fetch(`${API_URL}/statuses`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
|
|
const data = await res.json();
|
|
if (data.ok) systemStatuses = data.statuses;
|
|
} catch (e) { console.error("Error cargando estados:", e); }
|
|
}
|
|
|
|
async function fetchUserData() {
|
|
try {
|
|
const res = await fetch(`${API_URL}/auth/me`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
|
|
const data = await res.json();
|
|
if (data.ok && data.user) {
|
|
userZones = Array.isArray(data.user.zones) ? data.user.zones.map(z => z.cps) : [];
|
|
const resG = await fetch(`${API_URL}/admin/users`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
|
|
const dataG = await resG.json();
|
|
if(dataG.ok) {
|
|
const myUser = dataG.users.find(u => u.id === data.user.id);
|
|
if(myUser) userGuilds = myUser.guilds || [];
|
|
}
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
async function fetchBadges() {
|
|
try {
|
|
const headers = { "Authorization": `Bearer ${localStorage.getItem("token")}` };
|
|
const resActive = await fetch(`${API_URL}/services/active`, { headers });
|
|
const dataActive = await resActive.json();
|
|
const resReqs = await fetch(`${API_URL}/agenda/requests`, { headers });
|
|
const dataReqs = await resReqs.json();
|
|
|
|
if (dataActive.ok && dataReqs.ok) {
|
|
let sinCitaCount = 0;
|
|
sinCitaCount += dataReqs.requests.length;
|
|
dataActive.services.forEach(s => {
|
|
if (s.provider === 'SYSTEM_BLOCK') return;
|
|
const raw = s.raw_data || {};
|
|
if (raw.appointment_status === 'pending') return;
|
|
if (!raw.scheduled_date || raw.scheduled_date.trim() === "") {
|
|
if (raw.status_operativo) {
|
|
const st = systemStatuses.find(x => String(x.id) === String(raw.status_operativo));
|
|
if (st && st.is_final) return;
|
|
}
|
|
sinCitaCount++;
|
|
}
|
|
});
|
|
if (sinCitaCount > 0) {
|
|
const b1 = document.getElementById('badgeAsignados');
|
|
b1.innerText = `${sinCitaCount} pendiente${sinCitaCount > 1 ? 's' : ''}`;
|
|
b1.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
if (localStorage.getItem("role") === 'operario') {
|
|
const resScraped = await fetch(`${API_URL}/providers/scraped`, { headers });
|
|
const dataScraped = await resScraped.json();
|
|
if (dataScraped.ok) {
|
|
let bolsaCount = 0;
|
|
let sinGremioCount = 0;
|
|
dataScraped.services.forEach(s => {
|
|
if(s.assigned_to || s.provider === 'SYSTEM_BLOCK') return;
|
|
const raw = s.raw_data || {};
|
|
const dbStatus = raw.status_operativo;
|
|
if(s.status === 'archived' || (dbStatus && ['anulado', 'terminado', 'finalizado'].includes(dbStatus.toLowerCase()))) return;
|
|
// 🆕 Mismo criterio que solicitar.html: si el aviso no tiene gremio asignado
|
|
// de verdad, es visible para cualquier operario (antes esto no se contaba aquí,
|
|
// por eso el número de la burbuja no cuadraba con lo que salía en la lista).
|
|
const rawGuildId = s.guild_id ?? raw.guild_id;
|
|
const hasGuild = rawGuildId !== undefined && rawGuildId !== null && String(rawGuildId).trim() !== "" && String(rawGuildId) !== "0";
|
|
const sGuild = String(rawGuildId);
|
|
const sCp = String(raw['Código Postal'] || raw['CP'] || "").trim();
|
|
const matchGuild = hasGuild && (userGuilds.includes(Number(sGuild)) || userGuilds.includes(String(sGuild)));
|
|
const matchZone = userZones.some(z => sCp.startsWith(z));
|
|
const guildOk = !hasGuild || userGuilds.length === 0 || matchGuild;
|
|
if(guildOk && (userZones.length === 0 || matchZone)) {
|
|
bolsaCount++;
|
|
if (!hasGuild) sinGremioCount++;
|
|
}
|
|
});
|
|
if (bolsaCount > 0) {
|
|
const b2 = document.getElementById('badgeBolsa');
|
|
let texto = `${bolsaCount} servicio${bolsaCount > 1 ? 's' : ''}`;
|
|
if (sinGremioCount > 0) texto += ` y ${sinGremioCount} sin gremio`;
|
|
b2.innerText = texto;
|
|
b2.classList.remove('hidden');
|
|
}
|
|
}
|
|
}
|
|
} catch (e) { console.log("Error cargando badges"); }
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.clear();
|
|
window.location.href = "index.html";
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |