// job-cache.js var DB_NAME = "JobInfoCache"; var DB_VERSION = 2; var JOBS_STORE = "jobs"; var META_STORE = "metadata"; var CACHE_TTL = 5 * 60 * 1000; var PAGE_SIZE = 500; class JobCache { db = null; async initialize() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => reject(request.error); request.onsuccess = () => { this.db = request.result; resolve(); }; request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(JOBS_STORE)) { db.createObjectStore(JOBS_STORE, { keyPath: "Job_Number" }); } if (!db.objectStoreNames.contains(META_STORE)) { db.createObjectStore(META_STORE); } }; }); } async getCachedJobs() { if (!this.db) throw new Error("Database not initialized"); return new Promise((resolve, reject) => { const transaction = this.db.transaction([JOBS_STORE], "readonly"); const store = transaction.objectStore(JOBS_STORE); const request = store.getAll(); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async isCacheValid() { if (!this.db) return false; try { const metadata = await this.getMetadata(); if (!metadata || !metadata.lastUpdated) return false; const age = Date.now() - metadata.lastUpdated; return age < CACHE_TTL; } catch { return false; } } async saveJobs(jobs) { if (!this.db) throw new Error("Database not initialized"); return new Promise((resolve, reject) => { const transaction = this.db.transaction([JOBS_STORE, META_STORE], "readwrite"); const jobsStore = transaction.objectStore(JOBS_STORE); const metaStore = transaction.objectStore(META_STORE); jobsStore.clear(); jobs.forEach((job) => jobsStore.add(job)); const metadata = { lastUpdated: Date.now(), version: DB_VERSION }; metaStore.put(metadata, "main"); transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }); } async refreshJobs() { try { let page = 1; let all = []; let totalPages = 1; while (page <= totalPages) { const url = `/api/jobs?page=${page}&perPage=${PAGE_SIZE}&sort=-Job_Number`; const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch jobs (status ${response.status})`); } const data = await response.json(); const items = data.items || []; const reportedTotalPages = data.totalPages || 1; all = all.concat(items); totalPages = Math.max(totalPages, reportedTotalPages); page += 1; } await this.saveJobs(all); return all; } catch (err) { console.error("Failed to refresh jobs cache:", err); throw err; } } async getMetadata() { if (!this.db) return null; return new Promise((resolve, reject) => { const transaction = this.db.transaction([META_STORE], "readonly"); const store = transaction.objectStore(META_STORE); const request = store.get("main"); request.onsuccess = () => resolve(request.result || null); request.onerror = () => reject(request.error); }); } async clearCache() { if (!this.db) throw new Error("Database not initialized"); return new Promise((resolve, reject) => { const transaction = this.db.transaction([JOBS_STORE, META_STORE], "readwrite"); transaction.objectStore(JOBS_STORE).clear(); transaction.objectStore(META_STORE).clear(); transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }); } async getCacheAge() { const metadata = await this.getMetadata(); if (!metadata || !metadata.lastUpdated) return null; return Date.now() - metadata.lastUpdated; } } var jobCache = new JobCache; // home.ts var allJobs = []; var filteredJobs = []; var currentPage = 1; var perPage = 50; var isRefreshing = false; async function checkAuth() { try { const response = await fetch("/api/auth/status"); const data = await response.json(); if (!data.authenticated) { window.location.href = "/signin.html"; return false; } return true; } catch (err) { console.error("Auth check failed:", err); window.location.href = "/signin.html"; return false; } } async function fetchJobs() { try { await jobCache.initialize(); const isCacheValid = await jobCache.isCacheValid(); if (isCacheValid) { const cachedJobs = await jobCache.getCachedJobs(); allJobs = cachedJobs; filteredJobs = [...allJobs]; renderJobs(); const cacheAge = await jobCache.getCacheAge(); console.log(`Loaded ${allJobs.length} jobs from cache (age: ${Math.round((cacheAge || 0) / 1000)}s)`); refreshJobsInBackground(); } else { const jobs = await jobCache.refreshJobs(); allJobs = jobs; filteredJobs = [...allJobs]; renderJobs(); console.log(`Loaded ${allJobs.length} jobs from server (cache updated)`); } } catch (err) { console.error("Failed to fetch jobs:", err); const results = document.getElementById("results"); results.innerHTML = '
Failed to load jobs. Please refresh the page.
'; } } async function refreshJobsInBackground() { if (isRefreshing) return; isRefreshing = true; try { const freshJobs = await jobCache.refreshJobs(); allJobs = freshJobs; applyFilters(); console.log(`Background refresh complete (${freshJobs.length} jobs)`); } catch (err) { console.error("Background refresh failed:", err); } finally { isRefreshing = false; } } function renderJobs() { const results = document.getElementById("results"); if (filteredJobs.length === 0) { results.innerHTML = '
No jobs found
'; return; } const start = (currentPage - 1) * perPage; const end = start + perPage; const pageJobs = filteredJobs.slice(start, end); results.innerHTML = pageJobs.map((job) => { const isActiveTrue = job.Active === true; const isActiveFalse = job.Active === false; const nameVal = job.Job_Full_Name || ""; const contactVal = job.Contact_Person || ""; return `
Job#
${escapeHtml(String(job.Job_Number || ""))}
Job Name:
${escapeHtml(String(nameVal))}
Contact:
${escapeHtml(String(contactVal))}
`; }).join(""); results.querySelectorAll("[data-job-number]").forEach((el) => { el.addEventListener("click", (e) => { const jobNumber = e.currentTarget.dataset.jobNumber; if (jobNumber) { openDetails(jobNumber); } }); }); } function openDetails(jobNumber) { window.location.href = `/details.html?job=${encodeURIComponent(jobNumber)}`; } function setupSearch() { const searchBox = document.getElementById("searchBox"); searchBox.addEventListener("input", () => { const query = searchBox.value.toLowerCase().trim(); if (!query) { filteredJobs = [...allJobs]; } else { filteredJobs = allJobs.filter((job) => { return job.Job_Number?.toString().toLowerCase().includes(query) || job.Job_Full_Name?.toLowerCase().includes(query) || job.Job_Address?.toLowerCase().includes(query) || job.Contact_Person?.toLowerCase().includes(query) || job.Company_Client?.toLowerCase().includes(query) || job.Notes?.toLowerCase().includes(query) || job.Job_Status?.toLowerCase().includes(query) || job.Status?.toLowerCase().includes(query) || job.Estimator?.toLowerCase().includes(query); }); } currentPage = 1; renderJobs(); }); } function setupFilters() { const filterToggle = document.getElementById("filterToggle"); const filtersPanel = document.getElementById("filtersPanel"); const clearFilters = document.getElementById("clearFilters"); filterToggle.addEventListener("click", () => { const isHidden = filtersPanel.classList.contains("hidden"); if (isHidden) { filtersPanel.classList.remove("hidden"); filterToggle.textContent = "Hide Filters"; } else { filtersPanel.classList.add("hidden"); filterToggle.textContent = "Show Filters"; } }); const filterInputs = filtersPanel.querySelectorAll('input[type="checkbox"]'); filterInputs.forEach((input) => { input.addEventListener("change", applyFilters); }); clearFilters.addEventListener("click", () => { filterInputs.forEach((input) => { input.checked = false; }); applyFilters(); }); } function applyFilters() { const divisions = Array.from(document.querySelectorAll(".filter-division:checked")).map((el) => el.value); const actives = Array.from(document.querySelectorAll(".filter-active:checked")).map((el) => el.value); const estimators = Array.from(document.querySelectorAll(".filter-estimator:checked")).map((el) => el.value.toLowerCase()); const statuses = Array.from(document.querySelectorAll(".filter-status:checked")).map((el) => el.value); filteredJobs = allJobs.filter((job) => { const divisionVal = job.Job_Division || job.Division; const activeVal = job.Active === true || job.Active === "Yes" ? "Yes" : job.Active === false || job.Active === "No" ? "No" : "—"; const estimatorVal = (job.Estimator || "").toLowerCase(); const statusVal = job.Job_Status || job.Status; if (divisions.length > 0 && !divisions.some((d) => divisionVal?.startsWith(d))) return false; if (actives.length > 0 && !actives.includes(activeVal)) return false; if (estimators.length > 0 && !estimators.some((e) => estimatorVal.includes(e))) return false; if (statuses.length > 0 && !statuses.includes(statusVal || "")) return false; return true; }); currentPage = 1; renderJobs(); } function setupVoiceSearch() { const voiceBtn = document.getElementById("voiceSearchBtn"); const micIcon = document.getElementById("micIcon"); const micRecordingIcon = document.getElementById("micRecordingIcon"); const searchBox = document.getElementById("searchBox"); if (!("webkitSpeechRecognition" in window) && !("SpeechRecognition" in window)) { voiceBtn.style.display = "none"; return; } const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const recognition = new SpeechRecognition; recognition.continuous = false; recognition.interimResults = false; voiceBtn.addEventListener("click", () => { recognition.start(); micIcon.classList.add("hidden"); micRecordingIcon.classList.remove("hidden"); }); recognition.onresult = (event) => { const transcript = event.results[0][0].transcript; searchBox.value = transcript; searchBox.dispatchEvent(new Event("input")); }; recognition.onend = () => { micIcon.classList.remove("hidden"); micRecordingIcon.classList.add("hidden"); }; recognition.onerror = () => { micIcon.classList.remove("hidden"); micRecordingIcon.classList.add("hidden"); }; } function setupSignOut() { const signOutBtn = document.getElementById("signOutBtn"); signOutBtn.addEventListener("click", async () => { try { await fetch("/api/auth/logout", { method: "POST" }); window.location.href = "/signin.html"; } catch (err) { console.error("Sign out failed:", err); window.location.href = "/signin.html"; } }); } async function init() { const isAuthenticated = await checkAuth(); if (!isAuthenticated) return; setupSearch(); setupFilters(); setupVoiceSearch(); setupSignOut(); await fetchJobs(); } function escapeHtml(str) { return str.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); } init();