Files
Job-Info/Mode2Test/AuthAndToken/frontend/js/home.js
T

345 lines
13 KiB
JavaScript

// 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 = '<div class="text-center text-red-600 p-4">Failed to load jobs. Please refresh the page.</div>';
}
}
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 = '<div class="text-center text-gray-500 p-4">No jobs found</div>';
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 `
<div class="bg-white rounded-lg p-2.5 border border-blue-100 shadow-sm cursor-pointer relative flex flex-col justify-center gap-1 hover:bg-gray-50 transition-colors" data-job-number="${escapeHtml(String(job.Job_Number || ""))}">
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job#</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(String(job.Job_Number || ""))}</div></div>
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job Name:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(String(nameVal))}</div></div>
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Contact:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(String(contactVal))}</div></div>
<div class="absolute top-2.5 right-2.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm" style="background:${isActiveTrue ? "green" : isActiveFalse ? "red" : "#cbd5e1"}"></div>
</div>
`;
}).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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
}
init();