const state = { rows: [], mode: "machine", suggestionIndex: -1 }; const labels = { oil_fuel_filters: "Olej / paliwo", air_cooling_filters: "Powietrze / chłodzenie", hydraulic_other_filters: "Hydrauliczny / inne" }; const $ = (selector) => document.querySelector(selector); function parseCsv(text) { const rows = [], record = []; let field = "", quoted = false; for (let i = 0; i < text.length; i += 1) { const char = text[i], next = text[i + 1]; if (char === '"' && quoted && next === '"') { field += '"'; i += 1; } else if (char === '"') quoted = !quoted; else if (char === "," && !quoted) { record.push(field); field = ""; } else if ((char === "\n" || char === "\r") && !quoted) { if (char === "\r" && next === "\n") i += 1; record.push(field); field = ""; if (record.some((value) => value.trim())) rows.push(record.splice(0)); } else field += char; } record.push(field); if (record.some((value) => value.trim())) rows.push(record); const headers = rows.shift().map((header) => header.replace(/^\uFEFF/, "").trim()); return rows.map((values) => Object.fromEntries(headers.map((header, index) => [header, (values[index] || "").trim()]))); } function normalized(value) { return (value || "").toLocaleLowerCase("pl-PL").normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } function escapeHtml(value) { return String(value || "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char])); } function highlighted(value, query) { const safe = escapeHtml(value); if (!query) return safe; const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return safe.replace(new RegExp(`(${escaped})`, "ig"), "$1"); } function allFilterText(row) { return ["oil_fuel_filters", "air_cooling_filters", "hydraulic_other_filters"].map((key) => row[key]).join(" "); } function filterParts(value) { const parts = value.split(/\s*\|\s*|(?=(?:Olej|Paliwo|Powietrze|Hydrauliczny|Odpowietrznik|Płyn|Kabinowy)\b)/i) .map((part) => part.replace(/\s*\|\s*$/, "").trim()).filter(Boolean); return parts.length ? parts : [value]; } function suggestionData(query) { const needle = normalized(query); if (state.mode === "machine") { return [...new Set(state.rows.map((row) => row.model).filter(Boolean))] .filter((value) => normalized(value).includes(needle)) .sort((a, b) => a.localeCompare(b, "pl")) .slice(0, 8) .map((value) => ({ value, meta: "model maszyny" })); } const values = new Map(); Object.entries(labels).forEach(([key, label]) => { state.rows.forEach((row) => { const value = row[key]; if (!value) return; filterParts(value).forEach((part) => { if (normalized(part).includes(needle) && !values.has(part)) values.set(part, label); }); }); }); return [...values.entries()].sort(([a], [b]) => a.localeCompare(b, "pl")).slice(0, 8) .map(([value, meta]) => ({ value, meta })); } function renderSuggestions() { const input = $("#searchInput"); const query = input.value.trim(); const container = $("#suggestions"); state.suggestionIndex = -1; if (query.length < 2 || !state.rows.length) { container.classList.remove("visible"); container.innerHTML = ""; return; } const suggestions = suggestionData(query); container.innerHTML = suggestions.map((item, index) => ``).join(""); container.classList.toggle("visible", suggestions.length > 0); container.querySelectorAll(".suggestion").forEach((button) => button.addEventListener("mousedown", (event) => { event.preventDefault(); input.value = button.dataset.value; container.classList.remove("visible"); $("#clearButton").classList.add("visible"); render(); })); } function render() { const query = $("#searchInput").value.trim(); const needle = normalized(query); const results = state.rows.filter((row) => { const haystack = state.mode === "machine" ? [row.model, row.machine_type, row.engine, row.year, row.notes].join(" ") : allFilterText(row); return !needle || normalized(haystack).includes(needle); }); $("#resultsTitle").textContent = state.mode === "machine" ? (query ? "Pasujące maszyny" : "Maszyny w bazie") : (query ? "Maszyny pasujące do filtra" : "Wyszukaj numer filtra"); $("#resultCount").textContent = query ? `${results.length} wyników` : `${state.rows.length} rekordów`; const container = $("#results"); container.innerHTML = ""; if (!results.length) { container.innerHTML = `
Nie znaleziono wyników. Spróbuj krótszego numeru filtra lub nazwy modelu.
`; return; } results.forEach((row) => { const card = $("#machineTemplate").content.cloneNode(true); card.querySelector(".card-kicker").textContent = row.machine_type || "Maszyna"; card.querySelector(".machine-name").innerHTML = highlighted(row.model, query); card.querySelector(".year").textContent = row.year ? `Rok ${row.year.replace(".0", "")}` : ""; card.querySelector(".engine").textContent = row.engine ? `Silnik: ${row.engine}` : "Silnik: brak danych"; const grid = card.querySelector(".filter-grid"); Object.entries(labels).forEach(([key, label]) => { if (!row[key]) return; const item = document.createElement("div"); item.className = "filter"; const values = filterParts(row[key]) .map((value) => `${highlighted(value, query)}`) .join(""); item.innerHTML = `${label}${values}`; grid.appendChild(item); }); card.querySelector(".source").textContent = row.source_page ? `Źródło: strona ${row.source_page} PDF` : ""; container.appendChild(card); }); } function setLoaded(rows, source) { state.rows = rows; $("#statusDot").classList.add("ready"); $("#statusTitle").textContent = "Baza załadowana"; $("#statusText").textContent = source; $("#stats").textContent = `${rows.length} rekordów · ${new Set(rows.map((row) => row.model)).size} modeli`; render(); renderSuggestions(); } async function loadDefault() { try { const response = await fetch("atut_baza.csv"); if (!response.ok) throw new Error(`HTTP ${response.status}`); setLoaded(parseCsv(await response.text()), "Źródło: atut_baza.csv"); } catch (error) { $("#statusTitle").textContent = "Nie udało się wczytać CSV"; $("#statusText").textContent = "Uruchom aplikację przez lokalny serwer, aby wczytać bazę danych."; } } document.querySelectorAll(".tab").forEach((tab) => tab.addEventListener("click", () => { document.querySelectorAll(".tab").forEach((item) => item.classList.remove("active")); tab.classList.add("active"); state.mode = tab.dataset.mode; $("#searchLabel").textContent = state.mode === "machine" ? "Model, typ lub silnik maszyny" : "Numer lub oznaczenie filtra"; $("#searchInput").placeholder = state.mode === "machine" ? "np. QES 20, AMMANN albo KUBOTA" : "np. C 14 200, WK 1020X albo FF00758"; $("#searchHint").textContent = state.mode === "machine" ? "Wpisz minimum 2 znaki. Wyniki aktualizują się automatycznie." : "Wpisz fragment oznaczenia, np. C 14 200 albo WK 1020X."; render(); renderSuggestions(); })); $("#searchInput").addEventListener("input", () => { $("#clearButton").classList.toggle("visible", Boolean($("#searchInput").value)); render(); renderSuggestions(); }); $("#searchInput").addEventListener("keydown", (event) => { const items = [...document.querySelectorAll(".suggestion")]; if (!items.length) return; if (event.key === "ArrowDown" || event.key === "ArrowUp") { event.preventDefault(); state.suggestionIndex = (state.suggestionIndex + (event.key === "ArrowDown" ? 1 : items.length - 1)) % items.length; items.forEach((item, index) => item.classList.toggle("selected", index === state.suggestionIndex)); } else if (event.key === "Enter" && state.suggestionIndex >= 0) { event.preventDefault(); items[state.suggestionIndex].dispatchEvent(new MouseEvent("mousedown", { bubbles: true })); } else if (event.key === "Escape") { $("#suggestions").classList.remove("visible"); } }); $("#searchInput").addEventListener("focus", renderSuggestions); $("#searchInput").addEventListener("blur", () => setTimeout(() => $("#suggestions").classList.remove("visible"), 120)); $("#clearButton").addEventListener("click", () => { $("#searchInput").value = ""; $("#clearButton").classList.remove("visible"); render(); renderSuggestions(); $("#searchInput").focus(); }); loadDefault();