{"id":27889,"date":"2025-12-11T11:56:34","date_gmt":"2025-12-11T10:56:34","guid":{"rendered":"https:\/\/oferty.k4.pl\/?page_id=27889"},"modified":"2026-02-25T10:29:29","modified_gmt":"2026-02-25T09:29:29","slug":"przetargi","status":"publish","type":"page","link":"https:\/\/oferty.k4.pl\/index.php\/przetargi\/","title":{"rendered":"Przetargi"},"content":{"rendered":"\n<div class=\"wp-block-group alignwide has-global-padding is-layout-constrained wp-container-core-group-is-layout-93f53ff7 wp-block-group-is-layout-constrained\" style=\"min-height:0px;margin-top:0;margin-bottom:0;padding-top:0;padding-bottom:0\">\n<script>\n(function(){\n  const LOGIN_URL = \"https:\/\/oferty.k4.pl\/index.php\/login\/\";\n\n  const logged =\n    document.getElementById(\"wpadminbar\") ||\n    document.cookie.includes(\"wordpress_logged_in_\");\n\n  if (logged) return;\n\n  const url = new URL(LOGIN_URL);\n  url.searchParams.set(\"redirect_to\", window.location.href);\n  window.location.replace(url.toString());\n})();\n<\/script>\n\n<style>\n@import url('https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;500;600;700&display=swap');\n\n*{ box-sizing:border-box; font-family:\"Inter\", sans-serif !important; }\n\n:root{\n  --blue:#2563eb;\n  --text:#111827;\n  --muted:#9CA3AF;\n  --line:#E5E7EB;\n  --bg:#f5f5f5;\n}\n\n\/* =========================\n   SEARCH ROW\n   ========================= *\/\n.search-row{\n  width:100%;\n  max-width:1150px;\n  margin:24px auto 10px;\n  display:flex;\n  align-items:center;\n  gap:16px;\n  padding:10px 0;\n}\n\n.search-wrapper{ position:relative; flex:1.6; }\n\n.search-input{\n  width:100%;\n  background:#fff;\n  border-radius:999px;\n  padding:14px 22px 14px 44px;\n  border:none;\n  outline:none;\n  font-size:14px;\n  color:var(--text);\n  box-shadow:0px 8px 30px rgba(0,0,0,0.22), 0px 0px 20px rgba(0,0,0,0.20);\n}\n.search-input::placeholder{ color:var(--muted); }\n\n.search-icon{\n  position:absolute;\n  top:50%;\n  left:16px;\n  transform:translateY(-50%);\n  font-size:16px;\n  color:#6B7280;\n}\n\n.filters-panel{\n  max-width:1150px;\n  margin:0 auto;\n  display:block;\n}\n\n.filter-bar{\n  margin:0;\n  padding:14px 24px;\n  background:#fff;\n  border-radius:32px;\n  box-shadow:0px 8px 60px rgba(0,0,0,0.18), 0px 8px 20px rgba(0,0,0,0.18);\n\n  display:grid;\n  grid-template-columns:repeat(3, 1fr);\n  gap:22px;\n  align-items:end;\n}\n\n.filter-item{ display:flex; flex-direction:column; gap:8px; min-width:0; }\n.filter-label{ font-size:14px; font-weight:600; color:var(--text); }\n\n.field{ position:relative; }\n\n.field select,\n.field input{\n  width:100%;\n  height:44px;\n  padding:0 44px 0 16px;\n  border-radius:999px;\n  border:1px solid var(--line);\n  outline:none;\n  background:var(--bg);\n  font-size:14px;\n  color:var(--text);\n}\n\n.field select:required:invalid{ color:var(--muted); }\n.field select option{ color:var(--text); }\n.field select option[disabled]{ color:var(--muted); }\n\n.field select{\n  appearance:none;\n  -webkit-appearance:none;\n  -moz-appearance:none;\n}\n\n.field select:focus{\n  background:#fff;\n  border-color:var(--blue);\n}\n\n.field-btn{\n  position:absolute;\n  right:10px;\n  top:50%;\n  transform:translateY(-50%);\n  width:34px;\n  height:34px;\n  border-radius:999px;\n  border:none;\n  background:transparent;\n  display:grid;\n  place-items:center;\n  cursor:pointer;\n  opacity:0.65;\n  outline:none;\n}\n.field-btn:hover{ opacity:1; }\n.field-btn svg{ width:16px; height:16px; fill:#6B7280; }\n\n\/* ====== MINI RESET (X) inside fields ====== *\/\n\n\/* search needs space on right for X *\/\n.search-input{ padding-right: 48px; }\n\n.search-wrapper .clear-btn{\n  right:16px; \/* inside search pill *\/\n}\n\n.field{\n  position:relative;\n  display:flex;\n  align-items:center; \/* to centrowanie pionowe *\/\n}\n\n\/* select needs space for TWO buttons (X + caret) *\/\n.field select,\n.field input{\n  padding-right: 78px; \/* by\u0142o 44px *\/\n}\n\n.clear-btn{\n  position:absolute;\n  right:46px;\n  top:0;\n  bottom:0;\n  margin:auto 0;   \/* to ustawia dok\u0142adnie \u015brodek *\/\n  width:22px;\n  height:22px;\n  border-radius:999px;\n  border:1px solid var(--line);\n  background:#fff;\n  color:var(--muted);\n  font-size:20px;\nline-height:0;\n  display:none;\n  align-items:center;\n  justify-content:center;\n}\n\/* w selectach X stoi \"przed\" strza\u0142k\u0105 *\/\n.field .clear-btn{\n  right:46px; \/* obok caret buttona *\/\n}\n\n.clear-btn:hover{\n  background:#f3f4f6;\n  color:var(--blue);\n  border-color:#d1d5db;\n outline:none;\n}\n\n.clear-btn.show{ display:grid; }\n\n@media (max-width: 720px){\n  .search-row{\n    width:calc(100% - 40px);\n    margin:16px auto 12px;\n    padding:0;\n    flex-wrap:wrap;\n    align-items:stretch;\n    gap:12px;\n  }\n  .search-wrapper{ flex:1 1 100%; min-width:0; }\n  .search-input{ width:100%; }\n\n  .filters-panel{\n    width:calc(100% - 40px);\n    margin:0 auto;\n    padding:0;\n  }\n\n  .filter-bar{\n    grid-template-columns:repeat(2, minmax(0, 1fr));\n    gap:14px;\n    margin:0;\n    padding:16px 16px;\n    border-radius:24px;\n  }\n}\n<\/style>\n\n<div class=\"search-row\">\n  <div class=\"search-wrapper\">\n    <span class=\"search-icon\">\n      <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-search\" viewBox=\"0 0 16 16\">\n        <path d=\"M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0\"\/>\n      <\/svg>\n    <\/span>\n\n    <input id=\"kwInput\" type=\"text\" class=\"search-input\" placeholder=\"S\u0142owo kluczowe...\" \/>\n<button class=\"clear-btn\" type=\"button\" aria-label=\"Wyczy\u015b\u0107 wyszukiwanie\" data-clear=\"kwInput\">\u00d7<\/button>\n  <\/div>\n<\/div>\n\n<div class=\"filters-panel\" id=\"filtersPanel\">\n  <div class=\"filter-bar\">\n\n    <!-- KATEGORIA -->\n    <div class=\"filter-item\">\n      <div class=\"filter-label\">Kategoria<\/div>\n      <div class=\"field\">\n        <select id=\"filterCategory\" name=\"category\" required>\n          <option value=\"\" selected disabled>Wszystkie kategorie<\/option>\n        <\/select>\n\n        <button class=\"field-btn\" type=\"button\" aria-label=\"Rozwi\u0144 kategorie\" data-open=\"#filterCategory\">\n          <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 16 16\">\n            <path d=\"M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"\/>\n          <\/svg>\n        <\/button>\n<button class=\"clear-btn\" type=\"button\" aria-label=\"Wyczy\u015b\u0107 kategori\u0119\" data-clear=\"filterCategory\">\u00d7<\/button>\n      <\/div>\n    <\/div>\n\n    <!-- PODKATEGORIA (ZAWSZE dost\u0119pna) -->\n    <div class=\"filter-item\">\n      <div class=\"filter-label\">Podkategoria<\/div>\n      <div class=\"field\">\n        <select id=\"filterSubcategory\" name=\"subcategory\" required>\n          <option value=\"\" selected disabled>Wszystkie podkategorie<\/option>\n        <\/select>\n\n        <button class=\"field-btn\" type=\"button\" aria-label=\"Rozwi\u0144 podkategorie\" data-open=\"#filterSubcategory\">\n          <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 16 16\">\n            <path d=\"M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"\/>\n          <\/svg>\n        <\/button>\n<button class=\"clear-btn\" type=\"button\" aria-label=\"Wyczy\u015b\u0107 podkategori\u0119\" data-clear=\"filterSubcategory\">\u00d7<\/button>\n      <\/div>\n    <\/div>\n\n    <!-- \u0179R\u00d3D\u0141O -->\n    <div class=\"filter-item\">\n      <div class=\"filter-label\">\u0179r\u00f3d\u0142o<\/div>\n      <div class=\"field\">\n        <select id=\"filterSource\" name=\"source\" required>\n          <option value=\"\" selected disabled>Wszystkie \u017ar\u00f3d\u0142a<\/option>\n          <option value=\"funduszeeuropejskie\">Fundusze Europejskie<\/option>\n          <option value=\"bzp\">BZP \/ eZam\u00f3wienia<\/option>\n          <option value=\"orlen\">Platforma Zakupowa ORLEN<\/option>\n          <option value=\"enea\">Grupy Enea<\/option>\n          <option value=\"platformazakupowa\">Platforma Zakupowa<\/option>\n          <option value=\"kghm\">KGHM<\/option>\n          <option value=\"marketplanet\">Marketplanet<\/option>\n          <option value=\"tauron\">Tauron<\/option>\n          <option value=\"pge\">GK PGE<\/option>\n        <\/select>\n\n        <button class=\"field-btn\" type=\"button\" aria-label=\"Rozwi\u0144 \u017ar\u00f3d\u0142a\" data-open=\"#filterSource\">\n          <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 16 16\">\n            <path d=\"M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z\"\/>\n          <\/svg>\n        <\/button>\n<button class=\"clear-btn\" type=\"button\" aria-label=\"Wyczy\u015b\u0107 \u017ar\u00f3d\u0142o\" data-clear=\"filterSource\">\u00d7<\/button>\n      <\/div>\n    <\/div>\n\n  <\/div>\n<\/div>\n\n<script>\n(function(){\n  const kwInput        = document.getElementById(\"kwInput\");\n  const categorySel    = document.getElementById(\"filterCategory\");\n  const subcategorySel = document.getElementById(\"filterSubcategory\");\n  const sourceSel      = document.getElementById(\"filterSource\");\n  const resetBtn       = document.getElementById(\"resetFiltersBtn\");\n\n  const AJAX_URL = (window.ajaxurl) ? window.ajaxurl : (location.origin + \"\/wp-admin\/admin-ajax.php\");\n\n  \/\/ =========================\n  \/\/ Klik ikonki select\n  \/\/ =========================\n  document.addEventListener(\"click\", (e) => {\n    const btn = e.target.closest(\".field-btn\");\n    if (!btn) return;\n\n    const selector = btn.getAttribute(\"data-open\");\n    const el = selector ? document.querySelector(selector) : null;\n    if (!el) return;\n\n    el.focus();\n\n    if (el.tagName === \"SELECT\") {\n      el.dispatchEvent(new MouseEvent(\"mousedown\", { bubbles:true }));\n      el.click();\n    }\n  });\n\n  document.querySelectorAll(\".field select\").forEach((select) => {\n    select.addEventListener(\"change\", () => {\n      select.style.color = \"#111827\";\n    });\n  });\n\n  \/\/ =========================\n  \/\/ Kategorie\/podkategorie z get_tenders -> data.categories[]\n  \/\/ =========================\n  const termById = new Map();         \/\/ term_id -> term\n  const childrenByParent = new Map(); \/\/ parentId -> [childTerm]\n  let rootCategories = [];            \/\/ parent=0\n  let allSubcategories = [];          \/\/ parent!=0 (do selecta \"globalnego\")\nlet keepSubOnCategoryChange = false;\nlet pendingSubId = \"\";\n\n  function normalizeTerm(t){\n    if (!t || typeof t !== \"object\") return null;\n    const term_id = Number(t.term_id);\n    const name = String(t.name ?? \"\").trim();\n    const parent = Number(t.parent ?? 0);\n    if (!term_id || !name) return null;\n    return { term_id, name, parent };\n  }\n\n  function setSelectOptions(selectEl, items, placeholderText){\n    selectEl.innerHTML = \"\";\n\n    const ph = document.createElement(\"option\");\n    ph.value = \"\";\n    ph.disabled = true;\n    ph.selected = true;\n    ph.textContent = placeholderText;\n    selectEl.appendChild(ph);\n\n    items.forEach(it => {\n      const opt = document.createElement(\"option\");\n      opt.value = String(it.term_id);\n      opt.textContent = it.label ?? it.name; \/\/ label je\u015bli mamy \"Kategoria \u2192 Podkategoria\"\n      selectEl.appendChild(opt);\n    });\n\n    selectEl.style.color = \"\";\n  }\n\n  async function ajaxPost(params){\n    const body = new URLSearchParams(params);\n    const res = await fetch(AJAX_URL, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application\/x-www-form-urlencoded; charset=UTF-8\" },\n      body\n    });\n    const txt = await res.text();\n    try { return JSON.parse(txt); } catch(e){ return txt; }\n  }\n\n  function buildIndexes(terms){\n    termById.clear();\n    childrenByParent.clear();\n\n    terms.forEach(t => {\n      termById.set(t.term_id, t);\n\n      const pid = Number(t.parent || 0);\n      if (!childrenByParent.has(pid)) childrenByParent.set(pid, []);\n      childrenByParent.get(pid).push(t);\n    });\n\n    \/\/ sort A-Z\n    for (const [pid, arr] of childrenByParent.entries()){\n      arr.sort((a,b)=>a.name.localeCompare(b.name, \"pl\"));\n    }\n\n    rootCategories = (childrenByParent.get(0) || []).slice();\n    allSubcategories = [];\n    for (const [pid, arr] of childrenByParent.entries()){\n      if (Number(pid) !== 0) allSubcategories.push(...arr);\n    }\n  }\n\n  function getChildren(parentId){\n    return childrenByParent.get(Number(parentId)) || [];\n  }\n\n  function labelSubcategory(term){\n    const parent = termById.get(Number(term.parent));\n    const parentName = parent?.name ? parent.name : \"\u2014\";\n    return `${parentName} \u2192 ${term.name}`;\n  }\n\n  async function loadCategoriesFromBackend(){\n    const data = await ajaxPost({ action: \"get_tenders\", paged: 1 });\n\n    const catsRaw = data?.data?.categories;\n    const terms = Array.isArray(catsRaw) ? catsRaw.map(normalizeTerm).filter(Boolean) : [];\n\n    \/\/ je\u015bli mamy te\u017c rodzic\u00f3w; - brakuje to placeholder\n    const ids = new Set(terms.map(t => t.term_id));\n    const missingParents = new Set();\n    terms.forEach(t => { if (t.parent && !ids.has(t.parent)) missingParents.add(t.parent); });\n    missingParents.forEach(pid => terms.push({ term_id: pid, name: \"\u2014\", parent: 0 }));\n\n    buildIndexes(terms);\n  }\n\n  \/\/ =========================\n  \/\/ MINI RESET per filtr (X)\n  \/\/ =========================\n\n  function showHideClear(btn, el){\n    if (!btn || !el) return;\n    const hasValue = (el.value && String(el.value).trim() !== \"\");\n    btn.classList.toggle(\"show\", !!hasValue);\n  }\n\n  function setPlaceholder(selectEl){\n    \/\/ reset do placeholdera (pierwsza opcja disabled selected)\n    selectEl.selectedIndex = 0;\n    selectEl.style.color = \"\"; \/\/ przywr\u00f3\u0107 muted placeholder\n  }\n\n  function resetCategoryOnly(){\n    \/\/ resetuje KATEGORI\u0118 + przywraca globalne podkategorie\n    keepSubOnCategoryChange = false;\n    pendingSubId = \"\";\n\n    if (categorySel) setPlaceholder(categorySel);\n\n    if (subcategorySel){\n      const subPlain = allSubcategories\n        .slice()\n        .sort((a,b)=>String(a.name).localeCompare(String(b.name), \"pl\"));\n      setSelectOptions(subcategorySel, subPlain, \"Wszystkie podkategorie\");\n      setPlaceholder(subcategorySel);\n    }\n  }\n\n  function resetSubcategoryOnly(){\n    \/\/ resetuje TYLKO podkategori\u0119, nie rusza kategorii\n    if (!subcategorySel) return;\n\n    \/\/ je\u015bli kategoria jest wybrana -> zostaw zaw\u0119\u017con\u0105 list\u0119\n    const catId = categorySel?.value || \"\";\n    if (catId){\n      const subs = getChildren(catId).slice();\n      setSelectOptions(subcategorySel, subs, \"Wszystkie podkategorie\");\n      setPlaceholder(subcategorySel);\n    } else {\n      const subPlain = allSubcategories\n        .slice()\n        .sort((a,b)=>String(a.name).localeCompare(String(b.name), \"pl\"));\n      setSelectOptions(subcategorySel, subPlain, \"Wszystkie podkategorie\");\n      setPlaceholder(subcategorySel);\n    }\n\n    keepSubOnCategoryChange = false;\n    pendingSubId = \"\";\n  }\n\n  function resetSourceOnly(){\n    if (sourceSel) setPlaceholder(sourceSel);\n  }\n\n  function resetSearchOnly(){\n    if (kwInput) kwInput.value = \"\";\n  }\n\n  \/\/ podpinamy X do element\u00f3w\n  const clearButtons = Array.from(document.querySelectorAll(\".clear-btn[data-clear]\"));\n\n  clearButtons.forEach(btn => {\n    const id = btn.getAttribute(\"data-clear\");\n    const el = document.getElementById(id);\n    if (!el) return;\n\n    \/\/ pokazuj\/ukrywaj gdy zmienia si\u0119 warto\u015b\u0107\n    const onChange = () => showHideClear(btn, el);\n    el.addEventListener(\"input\", onChange);\n    el.addEventListener(\"change\", onChange);\n\n    \/\/ klik X -> reset tylko danego filtra + jeden emit\n    btn.addEventListener(\"click\", () => {\n      suppressEmit = true;\n\n      if (id === \"kwInput\") resetSearchOnly();\n      else if (id === \"filterCategory\") resetCategoryOnly();\n      else if (id === \"filterSubcategory\") resetSubcategoryOnly();\n      else if (id === \"filterSource\") resetSourceOnly();\n      else {\n        \/\/ fallback\n        if (el.tagName === \"SELECT\") setPlaceholder(el);\n        else el.value = \"\";\n      }\n\n      \/\/ od\u015bwie\u017c widoczno\u015b\u0107 X-\u00f3w\n      clearButtons.forEach(b => {\n        const eid = b.getAttribute(\"data-clear\");\n        const eel = document.getElementById(eid);\n        showHideClear(b, eel);\n      });\n\n      suppressEmit = false;\n      emitFiltersChanged({ paged: 1 });\n    });\n\n    \/\/ stan pocz\u0105tkowy\n    showHideClear(btn, el);\n  });\n\n  async function initCategoryUI(){\n    if (!categorySel || !subcategorySel) return;\n\n    \/\/ placeholdery start\n    setSelectOptions(categorySel, [], \"Wszystkie kategorie\");\n    setSelectOptions(subcategorySel, [], \"Wszystkie podkategorie\");\n\n    await loadCategoriesFromBackend();\n\n    \/\/ 1) KATEGORIE = parent=0\n    setSelectOptions(categorySel, rootCategories, \"Wszystkie kategorie\");\n\n    \/\/ 2) PODKATEGORIE = wszystkie (ale opisane jako \"Kategoria \u2192 Podkategoria\")\n   const subPlain = allSubcategories\n  .slice()\n  .sort((a,b)=>String(a.name).localeCompare(String(b.name), \"pl\"));\n\nsetSelectOptions(subcategorySel, subPlain, \"Wszystkie podkategorie\");\n\n    \/\/ 3) Je\u015bli user wybierze kategori\u0119 \u2192 mo\u017cemy opcjonalnie zaw\u0119zi\u0107 list\u0119 podkategorii\n    categorySel.addEventListener(\"change\", () => {\n  const catId = categorySel.value;\n\n  \/\/ je\u015bli zmiana kategorii zosta\u0142a wywo\u0142ana przez wyb\u00f3r podkategorii,\n  \/\/ to NIE czy\u015bcimy podkategorii\n  if (!keepSubOnCategoryChange){\n    subcategorySel.value = \"\";\n  }\n\n  if (!catId){\n    \/\/ jak wracamy do \"Wszystkie kategorie\" -> poka\u017c globaln\u0105 list\u0119 sub\n    const subPlain = allSubcategories\n      .slice()\n      .sort((a,b)=>String(a.name).localeCompare(String(b.name), \"pl\"));\n    setSelectOptions(subcategorySel, subPlain, \"Wszystkie podkategorie\");\n\n    \/\/ je\u015bli mieli\u015bmy podtrzyma\u0107 wyb\u00f3r, przywr\u00f3\u0107\n    if (keepSubOnCategoryChange && pendingSubId){\n      subcategorySel.value = String(pendingSubId);\n      pendingSubId = \"\";\n      keepSubOnCategoryChange = false;\n    }\n    return;\n  }\n\n  const subs = getChildren(catId).slice();\n  setSelectOptions(subcategorySel, subs, \"Wszystkie podkategorie\");\n\n  \/\/ przywr\u00f3\u0107 wybran\u0105 podkategori\u0119 (po przebudowaniu opcji)\n  if (keepSubOnCategoryChange && pendingSubId){\n    subcategorySel.value = String(pendingSubId);\n    pendingSubId = \"\";\n    keepSubOnCategoryChange = false;\n  }\n});\n\n    \/\/ 4) Je\u015bli user wybierze podkategori\u0119 BEZ kategorii \u2192 automatycznie ustawiamy jej rodzica\nsubcategorySel.addEventListener(\"change\", () => {\n  const subId = Number(subcategorySel.value);\n  const sub = termById.get(subId);\n  if (!sub) return;\n\n  const parentId = Number(sub.parent || 0);\n  if (parentId && (!categorySel.value || Number(categorySel.value) !== parentId)){\n    \/\/ zapami\u0119taj wyb\u00f3r podkategorii i nie czy\u015b\u0107 go przy change kategorii\n    keepSubOnCategoryChange = true;\n    pendingSubId = String(subId);\n\n    categorySel.value = String(parentId);\n    categorySel.dispatchEvent(new Event(\"change\", { bubbles:true }));\n  }\n});\n  }\n\n  \/\/ =========================\n  \/\/ EMIT FILTR\u00d3W\n  \/\/ =========================\nlet suppressEmit = false;\n\n  function emitFiltersChanged(extra = {}){\n  if (suppressEmit) return;\n\n  const detail = {\n    search: (kwInput?.value || \"\").trim(),\n    category: categorySel?.value || \"\",\n    subcategory: subcategorySel?.value || \"\",\n    source: sourceSel?.value || \"\",\n    ...extra\n  };\n\n  const ev = new CustomEvent(\"k4:filtersChanged\", { detail, bubbles:true, composed:true });\n  document.dispatchEvent(ev);\n  window.dispatchEvent(ev);\n}\n\n  if (categorySel)\n    categorySel.addEventListener(\"change\", () => emitFiltersChanged({ paged: 1 }));\n\n  if (subcategorySel)\n    subcategorySel.addEventListener(\"change\", () => emitFiltersChanged({ paged: 1 }));\n\n  if (sourceSel)\n    sourceSel.addEventListener(\"change\", () => emitFiltersChanged({ paged: 1 }));\n\n  if (kwInput){\n    let t = null;\n    kwInput.addEventListener(\"input\", () => {\n      clearTimeout(t);\n      t = setTimeout(() => emitFiltersChanged({ paged: 1 }), 250);\n    });\n\n    kwInput.addEventListener(\"keydown\", (e) => {\n      if (e.key === \"Enter\"){\n        e.preventDefault();\n        emitFiltersChanged({ paged: 1 });\n      }\n      if (e.key === \"Escape\"){\n        kwInput.value = \"\";\n        emitFiltersChanged({ paged: 1 });\n      }\n    });\n  }\n\nif (resetBtn){\n  resetBtn.addEventListener(\"click\", () => {\n    suppressEmit = true;\n\n    \/\/ 1) keyword\n    if (kwInput) kwInput.value = \"\";\n\n    \/\/ 2) \u0179r\u00f3d\u0142o (statyczne opcje) -> wr\u00f3\u0107 na placeholder\n    if (sourceSel) sourceSel.selectedIndex = 0;\n\n    \/\/ 3) wy\u0142\u0105cz logik\u0119 podtrzymywania sub\n    keepSubOnCategoryChange = false;\n    pendingSubId = \"\";\n\n    \/\/ 4) Kategoria -> wr\u00f3\u0107 na placeholder\n    if (categorySel){\n      \/\/ upewniamy si\u0119, \u017ce kategorie nadal maj\u0105 placeholder + list\u0119\n      setSelectOptions(categorySel, rootCategories, \"Wszystkie kategorie\");\n      categorySel.selectedIndex = 0; \/\/ zawsze dzia\u0142a \"raz i porz\u0105dnie\"\n    }\n\n    \/\/ 5) Podkategoria -> zawsze globalna lista + placeholder\n    if (subcategorySel){\n      const subPlain = allSubcategories\n        .slice()\n        .sort((a,b)=>String(a.name).localeCompare(String(b.name), \"pl\"));\n\n      setSelectOptions(subcategorySel, subPlain, \"Wszystkie podkategorie\");\n      subcategorySel.selectedIndex = 0;\n    }\n\n    \/\/ 6) przywr\u00f3\u0107 wygl\u0105d placeholder\u00f3w (muted)\n    [categorySel, subcategorySel, sourceSel].forEach(sel => {\n      if (sel) sel.style.color = \"\";\n    });\n\n    suppressEmit = false;\n\n    \/\/ 7) jeden event na koniec\n    emitFiltersChanged({ paged: 1 });\n  });\n}\n\n  initCategoryUI()\n    .catch(console.error)\n    .finally(() => emitFiltersChanged({ paged: 1 }));\n\n})();\n<\/script>\n\n<script>\n(function(){\n  const AJAX_URL = (window.ajaxurl) ? window.ajaxurl : (location.origin + \"\/wp-admin\/admin-ajax.php\");\n\n  \/\/ =========================\n  \/\/ STAN\n  \/\/ =========================\n  const state = {\n    paged: 1,\n    posts_per_page: 200,\n    search: \"\",\n    category: \"\",\n    subcategory: \"\",\n    source: \"\"\n    \/\/ dateFrom\/dateTo je\u015bli wr\u00f3c\u0105, dopisz tu\n  };\n\n  \/\/ OSTATNI request (\u017ceby abortowa\u0107 poprzedni przy szybkim wpisywaniu)\n  let controller = null;\n  let debounceTimer = null;\n\n  \/\/ =========================\n  \/\/ HELPERS\n  \/\/ =========================\n  function toFormBody(obj){\n    const p = new URLSearchParams();\n    Object.entries(obj).forEach(([k,v]) => p.set(k, (v ?? \"\").toString()));\n    return p.toString();\n  }\n\n  async function ajaxPost(params){\n    if (controller) controller.abort();\n    controller = new AbortController();\n\n    const res = await fetch(AJAX_URL, {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application\/x-www-form-urlencoded; charset=UTF-8\" },\n      body: toFormBody(params),\n      signal: controller.signal\n    });\n\n    const txt = await res.text();\n    try { return JSON.parse(txt); } catch(e){ return txt; }\n  }\n\n  function normalizeTendersResponse(payload){\n    \/\/ dopasuj do swojego backendu:\n    \/\/ cz\u0119sto: { success:true, data:{ tenders:[...], max_num_pages: X, found_posts: Y } }\n    const data = payload?.data ?? payload;\n    const tenders = data?.tenders ?? data?.posts ?? data?.items ?? [];\n    const maxPages = Number(data?.max_num_pages ?? data?.maxPages ?? 1);\n    const found = Number(data?.found_posts ?? data?.found ?? tenders.length);\n    return { tenders, maxPages, found, raw:data };\n  }\n\n  \/\/ =========================\n  \/\/ (OPCJONALNIE) URL SYNC\n  \/\/ =========================\n  function writeStateToURL(){\n    const url = new URL(window.location.href);\n\n    \/\/ czy\u015b\u0107 stare\n    [\"paged\",\"search\",\"category\",\"subcategory\",\"source\",\"ppp\"].forEach(k => url.searchParams.delete(k));\n\n    if (state.paged && state.paged !== 1) url.searchParams.set(\"paged\", String(state.paged));\n    if (state.search) url.searchParams.set(\"search\", state.search);\n    if (state.category) url.searchParams.set(\"category\", state.category);\n    if (state.source) url.searchParams.set(\"source\", state.source);\n    if (state.posts_per_page && state.posts_per_page !== 200) url.searchParams.set(\"ppp\", String(state.posts_per_page));\n\n    window.history.replaceState(null, \"\", url.toString());\n  }\n\n  function readStateFromURL(){\n    const url = new URL(window.location.href);\n    state.paged = Number(url.searchParams.get(\"paged\") || 1);\n    state.search = (url.searchParams.get(\"search\") || \"\").trim();\n    state.category = url.searchParams.get(\"category\") || \"\";\n    state.source = url.searchParams.get(\"source\") || \"\";\n    state.posts_per_page = Number(url.searchParams.get(\"ppp\") || 200);\n  }\n\n  \/\/ =========================\n  \/\/ KLUCZ: MAPOWANIE SUBKATEGORII\n  \/\/ =========================\n  \/\/ Je\u015bli backend NIE ma parametru \"subcategory\",\n  \/\/ to cz\u0119sto wysy\u0142a si\u0119 po prostu ID podkategorii w polu \"category\".\n  \/\/ Ustaw na true je\u015bli tak macie.\n  const BACKEND_USES_SUBCATEGORY_PARAM = false;\n\n  function buildBackendParams(){\n  const params = {\n    action: \"get_tenders\",\n    paged: state.paged,\n    posts_per_page: state.posts_per_page,\n    search: state.search,\n    source: state.source,\n    category: state.category\n  };\n\n  \/\/ je\u015bli user wybra\u0142 podkategori\u0119\n  if (state.subcategory) {\n    params.category = state.subcategory;\n  }\n\n  return params;\n}\n\n  \/\/ =========================\n  \/\/ TU PODPINASZ SW\u00d3J RENDER\n  \/\/ =========================\n  function renderTenders({ tenders, maxPages, found }){\n    \/\/ Podmie\u0144 to na sw\u00f3j renderer.\n    \/\/ Na start chocia\u017c log:\n    console.log(\"[get_tenders] found:\", found, \"maxPages:\", maxPages, \"tenders:\", tenders);\n\n    \/\/ Je\u015bli masz list\u0119:\n    \/\/ const listEl = document.getElementById(\"tenders-list\");\n    \/\/ listEl.innerHTML = tenders.map(t => `<div>${t.title}<\/div>`).join(\"\");\n  }\n\n  async function loadTenders(){\n    const params = buildBackendParams();\n\n    \/\/ w network zobaczysz warto\u015bci != \"\"\n    const payload = await ajaxPost(params);\n    const parsed = normalizeTendersResponse(payload);\n    renderTenders(parsed);\n  }\n\n  function scheduleLoad(){\n    clearTimeout(debounceTimer);\n    debounceTimer = setTimeout(() => {\n      writeStateToURL(); \/\/ opcjonalne\n      loadTenders().catch(err => {\n        if (err?.name === \"AbortError\") return;\n        console.error(err);\n      });\n    }, 200);\n  }\n\n  \/\/ =========================\n  \/\/ LISTENER Z TWOJEGO UI\n  \/\/ =========================\n  document.addEventListener(\"k4:filtersChanged\", (e) => {\n    const d = e.detail || {};\n\n    state.search = (d.search ?? \"\").trim();\n    state.category = d.category ?? \"\";\n    state.subcategory = d.subcategory ?? \"\";\n    state.source = d.source ?? \"\";\n    state.paged = Number(d.paged || 1);\n\n    scheduleLoad();\n  });\n\n  \/\/ =========================\n  \/\/ START\n  \/\/ =========================\n  readStateFromURL();\n  \/\/ Odpal pierwszy load (je\u015bli Tw\u00f3j UI emituje event na starcie, to i tak zadzia\u0142a;\n  \/\/ to jest fallback, \u017ceby strona dzia\u0142a\u0142a nawet bez eventu)\n  scheduleLoad();\n\n})();\n<\/script>\n<\/div>\n\n\n\n<div class=\"wp-block-group alignwide has-global-padding is-layout-constrained wp-container-core-group-is-layout-12243e0f wp-block-group-is-layout-constrained\">\n<style>\n@import url('https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;500;600;700&display=swap');\n\n* {\n  font-family: \"Inter\", sans-serif !important;\n  box-sizing: border-box;\n}\n\n.table-header-wrapper {\n  width: 100%;\n  display: flex;\n  justify-content: center;\n  padding: 0px 0;\n}\n\n.table-header {\n  width: 100%;\n  max-width: 1150px;\n  background: white;          \n  border-radius: 999px;\n  padding: 14px 28px;\n\n  display: grid;\n  grid-template-columns: 1fr 1fr 1fr 1fr;\n  align-items: center;\n  text-align: center;\n  font-size: 14px;\n  font-weight: 600;\n\n  box-shadow:\n    0px 8px 70px rgba(0, 0, 0, 0.22),\n    0px 8px 20px rgba(0, 0, 0, 0.20);\n}\n\n@media (max-width: 720px) {\n  .table-header-wrapper {\ndisplay: none;\n  }\n}\n}\n<\/style>\n\n<div class=\"table-header-wrapper\">\n  <div class=\"table-header\">\n    <div>Nazwa<\/div>\n    <div>Kategoria<\/div>\n    <div>Termin<\/div>\n    <div>\u0179r\u00f3d\u0142o<\/div>\n  <\/div>\n<\/div>\n<\/div>\n\n\n\n<div class=\"wp-block-group alignwide has-global-padding is-layout-constrained wp-container-core-group-is-layout-12243e0f wp-block-group-is-layout-constrained\">\n<style>\n@import url('https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;500;600;700&display=swap');\n\n* { box-sizing: border-box; font-family: \"Inter\", sans-serif !important; }\n\n\/* LISTA PRZETARG\u00d3W *\/\n.tenders-list { margin: 24px auto; padding: 0 20px; }\n\n\/* CHMURKA *\/\n.tender-card {\n  width: 100%;\n  max-width: 1150px;\n  background: #ffffff;\n  border-radius: 24px;\n  padding: 14px 28px;\n  margin: 12px auto 0;\n\n  border: 1px solid transparent;\n  box-shadow: 0px 8px 20px rgba(0,0,0,0.18);\n\n  display: grid;\n  grid-template-columns: repeat(4, minmax(0, 1fr));\n  gap: 24px;\n  align-items: center;\n\n  font-size: 13px;\n  color: #111827;\n  cursor: pointer;\n\n  transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;\n}\n.tender-col{ min-width: 0; }\n.tender-card:hover { transform: translateY(-1px); }\n\n\/* KOLUMNY *\/\n.tender-card__name    { text-align: left; }\n.tender-card__cats    { text-align: center; }\n.tender-card__date    { text-align: center; }\n.tender-card__source  { text-align: center; justify-self: stretch; width:100%; }\n\n.tender-card__source a{\n   width: 100%;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 2;\n  overflow: hidden;\n  overflow-wrap: anywhere;\n  line-height: 1.3;\n}\n\n\/* NAZWA *\/\n.tender-title{\n  font-size:14px;\n  line-height:1.4;\n  font-weight:500;\n\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  -webkit-line-clamp: 3;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  word-break: break-word;\n\n  margin: 0;\n}\n\n.tender-source-link{\n  color:#2563eb;\n  text-decoration:none;\n  font-weight:600;\n}\n.tender-source-link:hover{ text-decoration:underline; }\n\n\/* KATEGORIE *\/\n.tender-tags { display: flex; flex-wrap: wrap; gap: 6px; }\n.tender-tag {\n  display: inline-flex;\n  align-items: center;\n  padding: 4px 12px;\n  border-radius: 999px;\n  font-size: 11px;\n  font-weight: 500;\n  width: auto;\n  max-width: max-content !important;\n  white-space:nowrap;\n}\n.tender-tag--muted{ background:#f3f4f6; color:#6b7280; }\n\n\/* TERMIN *\/\n.tender-deadline {\n  display: inline-flex;\n  align-items: center;\n  gap: 8px;\n  justify-content: center;\n}\n.tender-deadline-icon { display:flex; }\n\n\/* \u0179R\u00d3D\u0141O *\/\n.tender-source {\n  display: flex;\n  width: 100%;\n  flex-direction: column;\n  gap: 2px;\n  font-weight: 600;\n}\n.tender-source a {\n  color: #2563EB;\n  text-decoration: none;\n  line-height: 1.3;\n}\n.tender-source-link,\n.tender-source-text {\n  display: inline-block;\n  max-width: 100%;\n  white-space: normal;\n  overflow-wrap: anywhere;\n  word-break: break-word;\n  line-height: 1.3;\n}\n.tender-source a:hover { text-decoration: underline; outline: none; }\n\n\/* PAGINACJA *\/\n.tenders-pagination{\n  max-width:1150px;\n  margin:16px auto 0;\n  padding:0 20px;\n  display:flex;\n  justify-content:space-between;\n  align-items:center;\n  gap:12px;\n  font-size:13px;\n  outline:none;\n}\n\n\/* lewa strona: przyciski + meta po prawej w tej samej linii *\/\n.tenders-pagination-left{\n  display:flex;\n  align-items:center;\n  gap:10px;\n  flex: 1 1 auto;\n  min-width: 0;\n}\n\n.tenders-meta--right{\n  margin-left:auto;      \/* wypycha meta na praw\u0105 stron\u0119 obok przycisk\u00f3w *\/\n  white-space:nowrap;\n}\n\n\/* prawa strona: label + input *\/\n.tenders-pagination-right{\n  display:flex;\n  gap:20px;\n  align-items:center;\n  justify-content:flex-end;\n  flex: 0 0 auto;\n  white-space:nowrap;\n}\n\n.page-size-label{\n  color:#6b7280;\n  font-weight:300;\n  font-size:13px;\n  white-space:nowrap;\n}\n.tenders-pagination select#pageSizeSelect{\n  padding: 10px 16px;\n  border-radius: 999px;\n  border: 1px solid #e5e7eb;\n  background: #f5f5f5;\n  font-size: 13px;\n  color: #111827;\n  outline: none;\n  transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out, background-color 0.15s ease-out;\n  cursor: pointer;\n}\n\n.tenders-pagination select#pageSizeSelect:focus{\n  background:#fff;\n  border-color:#2563eb;\n}\n\n\/* usu\u0144 strza\u0142ki w Chrome\/Safari *\/\n.tenders-pagination input#pageSizeInput::-webkit-outer-spin-button,\n.tenders-pagination input#pageSizeInput::-webkit-inner-spin-button{\n  -webkit-appearance: none;\n  margin: 0;\n}\n\n.tenders-pagination input#pageSizeInput:focus {\n  background: #ffffff;\n  border-color: #2563eb;\n}\n\n.tenders-pagination button{\n  border:0;\n  background:#2563EB;\n  color:#fff;\n  font-weight:600;\n  padding:10px 14px;\n  border-radius:999px;\n  outline: none;\n  cursor:pointer;\n  box-shadow:0px 8px 20px rgba(0,0,0,0.12);\n  transition:transform .12s ease;\n}\n\n.tenders-meta{ color:#6b7280; }\n.tenders-pagination select{\n  border:1px solid #e5e7eb;\n  border-radius:999px;\n  padding:8px 12px;\n  background:#fff;\n  outline: none;\n}\n\n\/* MOBILE *\/\n@media (max-width: 720px) {\n  .tenders-list{ padding: 0 20px; }\n\n  .tender-card{\n    grid-template-columns: repeat(2, minmax(0, 1fr));\n    column-gap: 14px;\n    row-gap: 12px;\n    padding: 14px 16px;\n    border-radius: 20px;\n    align-items: start;\n  }\n\n  .tender-card__name{ grid-column: 1 \/ -1; text-align: left; }\n  .tender-card__cats{ grid-column: 1 \/ -1; text-align: left; }\n  .tender-tags{ justify-content: flex-start; }\n\n  .tender-card__date{ grid-column: 1 \/ 2; text-align: left; }\n  .tender-deadline{ justify-content: flex-start; }\n\n  .tender-card__source{ grid-column: 1; text-align: left; }\n  .tender-source{ min-width: 0; }\n\n  .tenders-pagination{ flex-direction:column; align-items:stretch; }\n  .tenders-pagination .row{ display:flex; justify-content:space-between; gap:10px; }\n  .tenders-meta--right{ margin-left:0; }\n.tenders-pagination-left{ justify-content:space-between; flex-wrap:wrap; }\n}\n}\n<\/style>\n\n<div id=\"tenders-app\">\n  <div class=\"tenders-list\" id=\"tenders-list\"><\/div>\n\n<div class=\"tenders-pagination\">\n  <div class=\"row tenders-pagination-left\">\n<span class=\"page-size-label\">Ilo\u015b\u0107 przetarg\u00f3w:<\/span>\n    <select id=\"pageSizeSelect\" aria-label=\"Ilo\u015b\u0107 przetarg\u00f3w na stron\u0119\">\n  <option value=\"10\" selected>10<\/option>\n  <option value=\"25\">25<\/option>\n  <option value=\"50\">50<\/option>\n  <option value=\"100\">100<\/option>\n  <option value=\"200\">200<\/option>\n<\/select>\n  <\/div>\n\n  <div class=\"row tenders-pagination-right\">\n     <span class=\"tenders-meta tenders-meta--right\" id=\"meta\">\u0141adowanie\u2026<\/span>\n    <button id=\"prevBtn\" type=\"button\">\u2190 Poprzednia<\/button>\n    <button id=\"nextBtn\" type=\"button\">Nast\u0119pna \u2192<\/button>\n  <\/div>\n<\/div>\n<\/div>\n\n<script src=\"https:\/\/code.jquery.com\/jquery-3.7.1.min.js\"><\/script>\n<script>\njQuery(function ($) {\n  \/\/ =========================\n  \/\/ KONFIG\n  \/\/ =========================\n  const AJAX_URL = \"https:\/\/oferty.k4.pl\/wp-admin\/admin-ajax.php\";\n  const tenderPageUrl = \"https:\/\/oferty.k4.pl\/index.php\/przetarg-podstrona\/\";\n  const SELECTED_KEY = \"k4_selected_tender_v1\";\n\n  \/\/ ile maks pobra\u0107 \u0142\u0105cznie (bezpiecznik)\n  const FETCH_PER_PAGE = 200;\n  const MAX_TOTAL_ITEMS = 5000;   \/\/ ustaw np. 10000 je\u015bli musisz, ale ro\u015bnie czas \u0142adowania\n  const MAX_PAGES = 80;           \/\/ dodatkowy bezpiecznik\n\n  \/\/ =========================\n  \/\/ DOM\n  \/\/ =========================\n  const $list = $(\"#tenders-list\");\n  const $prev = $(\"#prevBtn\");\n  const $next = $(\"#nextBtn\");\n  const $meta = $(\"#meta\");\n  const $pageSizeSelect = $(\"#pageSizeSelect\");\n\n  const $kw          = $(\"#kwInput\");\n  const $category    = $(\"#filterCategory\");       \/\/ value = term_id (rodzic)\n  const $subcategory = $(\"#filterSubcategory\");    \/\/ value = term_id (dziecko)\n  const $source      = $(\"#filterSource\");\n\n  const $dateFrom    = $(\"#dateFrom\");\n  const $dateTo      = $(\"#dateTo\");\n\n  if (!$list.length || !$prev.length || !$next.length || !$meta.length) return;\n\n  \/\/ =========================\n  \/\/ STAN\n  \/\/ =========================\n  const state = {\n    paged: 1,                 \/\/ lokalna strona (client-side)\n    totalPages: 1,            \/\/ lokalna ilo\u015b\u0107 stron\n    totalItems: 0,            \/\/ lokalna ilo\u015b\u0107 wynik\u00f3w po filtrach\n    posts_per_page: parseInt(($pageSizeSelect.val() || \"10\"), 10) || 10,\n    catKwIdx: 0,\n\n    \/\/ cache lokalny:\n    _allPosts: [],\n    _filteredAll: []\n  };\n\n  \/\/ =========================\n  \/\/ HELPERS\n  \/\/ =========================\n  function esc(s) {\n    return String(s ?? \"\").replace(\/[&<>\"']\/g, m =>\n      ({ \"&\":\"&amp;\", \"<\":\"&lt;\", \">\":\"&gt;\", '\"':\"&quot;\", \"'\":\"&#39;\" }[m])\n    );\n  }\n\n  function truncate(str, max = 110) {\n    const s = String(str ?? \"\").trim();\n    if (s.length <= max) return s;\n    return s.slice(0, max - 1).trimEnd() + \"\u2026\";\n  }\n\n  function clampInt(n, min, max) {\n    const x = parseInt(n, 10);\n    if (isNaN(x)) return min;\n    return Math.max(min, Math.min(max, x));\n  }\n\n  function setLoading(on) {\n    if (on) $meta.text(\"\u0141adowanie\u2026\");\n    $prev.prop(\"disabled\", on);\n    $next.prop(\"disabled\", on);\n    $pageSizeSelect.prop(\"disabled\", on);\n  }\n\n  function updatePager() {\n    const p  = Number(state.paged || 1);\n    const tp = Number(state.totalPages || 1);\n    $prev.prop(\"disabled\", p <= 1);\n    $next.prop(\"disabled\", p >= tp);\n    $meta.text(`Strona ${p} \/ ${tp}`);\n  }\n\n  function applyPageSizeLimit() {\n    if (!$pageSizeSelect.length) return;\n\n    const total = Number(state.totalItems || 0);\n\n    $pageSizeSelect.find(\"option\").each(function(){\n      const v = Number($(this).val());\n      if (!v) return;\n      if (total > 0) $(this).prop(\"disabled\", v > total);\n      else $(this).prop(\"disabled\", false);\n    });\n\n    const cur = Number($pageSizeSelect.val() || 10);\n\n    if (total > 0 && cur > total) {\n      const enabled = $pageSizeSelect.find(\"option:not(:disabled)\").map(function(){\n        return Number($(this).val());\n      }).get().sort((a,b)=>a-b);\n\n      const best = enabled.length ? enabled[enabled.length - 1] : 10;\n      $pageSizeSelect.val(String(best));\n      state.posts_per_page = best;\n    } else {\n      state.posts_per_page = cur || 10;\n    }\n  }\n\n  \/\/ =========================\n  \/\/ NORMALIZE RESPONSE\n  \/\/ =========================\n  function normalizeResponse(res) {\n    if (!res) return { ok:false, message:\"Pusta odpowied\u017a serwera.\" };\n\n    const payload = (typeof res.success !== \"undefined\") ? res.data : res;\n    const ok = (typeof res.success === \"undefined\") ? true : !!res.success;\n\n    if (!ok) {\n      return { ok:false, message: res?.data?.message || \"Nie uda\u0142o si\u0119 pobra\u0107 przetarg\u00f3w.\" };\n    }\n\n    const posts = payload?.posts || payload?.data?.posts || [];\n    const pagination = payload?.pagination || payload?.data?.pagination || {};\n\n    const totalPages =\n      pagination?.total_pages ??\n      pagination?.pages ??\n      pagination?.max_pages ??\n      pagination?.totalPages ??\n      1;\n\n    const currentPage =\n      pagination?.current_page ??\n      pagination?.paged ??\n      pagination?.page ??\n      1;\n\n    const totalItems =\n      pagination?.total ??\n      pagination?.found_posts ??\n      payload?.total ??\n      payload?.found_posts ??\n      null;\n\n    return {\n      ok:true,\n      posts: Array.isArray(posts) ? posts : [],\n      pagination: {\n        totalPages: Number(totalPages || 1),\n        currentPage: Number(currentPage || 1),\n        totalItems: (totalItems == null ? 0 : Number(totalItems))\n      }\n    };\n  }\n\n  \/\/ =========================\n  \/\/ SEARCH BUILD\n  \/\/ =========================\n  function getAutoKeywords(){ return []; }\n\n  function buildFinalSearch(){\n    const raw = ($kw.val() || \"\").toString().trim();\n    const autoList = getAutoKeywords();\n    const auto = autoList[state.catKwIdx] || \"\";\n    return [raw, auto].filter(Boolean).join(\" \").trim();\n  }\n\n  \/\/ =========================\n  \/\/ \u0179R\u00d3D\u0141A \u2014 STABILNIE\n  \/\/ =========================\n  function normText(s){ return String(s || \"\").trim().toLowerCase(); }\n\n  function buildSource(t) {\n    const label = (t.source ?? t.source_name ?? t.origin ?? t.basis ?? \"\u0179r\u00f3d\u0142o\").toString();\n    const href = t.source_url ?? t.url ?? t.link ?? t.permalink ?? t.external_url ?? \"\";\n    return { label, href: href ? String(href) : \"\" };\n  }\n\n  function tenderSourceHay(t){\n    const src = buildSource(t);\n    const arr = [\n      src?.href, src?.label,\n      t?.source_url, t?.external_url, t?.origin_url, t?.source_link,\n      t?.source, t?.source_name, t?.origin, t?.basis\n    ]\n    .filter(Boolean)\n    .map(normText);\n\n    return arr.join(\" \");\n  }\n\n  const SOURCE_RULES = {\n    funduszeeuropejskie: \/bazakonkurencyjnosci(\\.funduszeeuropejskie\\.gov\\.pl)?|funduszeeuropejskie\/i,\n    bzp: \/ezamowienia\\.gov\\.pl|bzp\\.uzp\\.gov\\.pl|\\bbzp\\b|e-?zam[o\u00f3]wienia\/i,\n    orlen: \/connect\\.orlen\\.pl|platforma\\s+zakupowa\\s+orlen|\\borlen\\b\/i,\n    enea: \/https?:\\\/\\\/(www\\.)?enea\\.pl\\\/bip\\\/zamowienia|enea\\.pl\\\/bip\\\/zamowienia|\\benea\\b|grupy\\s+enea\/i,\n    platformazakupowa: \/platformazakupowa\\.pl|platforma\\s+zakupowa\/i,\n    kghm: \/kghm\\.pl|kghm\\.com|\\bkghm\\b|polska\\s+mied\/i,\n    marketplanet: \/oneplace\\.marketplanet\\.pl|marketplanet\\.pl|marketplanet\/i,\n    tauron: \/swoz\\.tauron\\.pl|tauron\\.pl|\\btauron\\b\/i,\n    pge: \/swpp2\\.gkpge\\.pl|gkpge\\.pl|\\bpge\\b|gk\\s*pge\/i\n  };\n\n  function matchesSelectedSource(t){\n    try{\n      const sel = ($source.val() || \"\").toString().trim().toLowerCase();\n      if (!sel) return true;\n      const rule = SOURCE_RULES[sel];\n      if (!rule) return true;\n      const hay = tenderSourceHay(t);\n      if (!hay) return false;\n      return rule.test(hay);\n    }catch(err){\n      console.error(\"matchesSelectedSource error:\", err, t);\n      return true;\n    }\n  }\n\n  \/\/ =========================\n  \/\/ KATEGORIE \/ PODKATEGORIE\n  \/\/ =========================\n  function extractCategories(t) {\n    const candidates = [t.categories, t.category, t.terms, t.tags, t.taxonomy, t.cats].filter(Boolean);\n\n    for (const c of candidates) {\n      if (Array.isArray(c)) {\n        const arr = c\n          .map(x => (typeof x === \"string\" ? x : (x?.name ?? x?.slug ?? x?.title ?? \"\")))\n          .map(s => String(s).trim())\n          .filter(Boolean);\n        if (arr.length) return arr;\n      }\n      if (typeof c === \"string\") {\n        const arr = c.split(\",\").map(s => s.trim()).filter(Boolean);\n        if (arr.length) return arr;\n      }\n      if (typeof c === \"object\") {\n        const name = c?.name ?? c?.slug ?? c?.title;\n        if (name) return [String(name)];\n      }\n    }\n\n    const fallback = t.category_name ?? t.category_slug ?? \"\";\n    return fallback ? [String(fallback)] : [];\n  }\n\n  let TERM_BY_ID = new Map();\n  let CHILDREN_BY_PARENT = new Map();\n  let TERM_IDS_BY_NAME = new Map();\n\n  function buildCategoryIndex(categoriesArr){\n    TERM_BY_ID = new Map();\n    CHILDREN_BY_PARENT = new Map();\n    TERM_IDS_BY_NAME = new Map();\n\n    (categoriesArr || []).forEach(t => {\n      const id = Number(t?.term_id);\n      const name = String(t?.name || \"\").trim();\n      const parent = Number(t?.parent || 0);\n      if (!id || !name) return;\n\n      TERM_BY_ID.set(id, { term_id:id, name, parent });\n\n      if (!CHILDREN_BY_PARENT.has(parent)) CHILDREN_BY_PARENT.set(parent, []);\n      CHILDREN_BY_PARENT.get(parent).push(id);\n\n      const key = name.toLowerCase();\n      if (!TERM_IDS_BY_NAME.has(key)) TERM_IDS_BY_NAME.set(key, []);\n      TERM_IDS_BY_NAME.get(key).push(id);\n    });\n  }\n\n  function getDescendantIds(rootId){\n    const out = new Set();\n    const stack = [Number(rootId)];\n    while (stack.length){\n      const id = stack.pop();\n      if (!id || out.has(id)) continue;\n      out.add(id);\n      const kids = CHILDREN_BY_PARENT.get(id) || [];\n      kids.forEach(k => { if (!out.has(k)) stack.push(k); });\n    }\n    return out;\n  }\n\n  function matchesSelectedCategory(t){\n    const catId = ($category.val() || \"\").toString().trim();\n    const subId = ($subcategory.val() || \"\").toString().trim();\n    if (!catId && !subId) return true;\n\n    const tenderCats = extractCategories(t).map(s => String(s).trim()).filter(Boolean);\n    if (!tenderCats.length) return false;\n\n    if (subId){\n      const sub = TERM_BY_ID.get(Number(subId));\n      if (!sub?.name) return true; \/\/ mapa niegotowa => nie blokuj\n      return tenderCats.includes(sub.name);\n    }\n\n    const root = TERM_BY_ID.get(Number(catId));\n    if (!root?.name) return true;\n\n    const ids = getDescendantIds(Number(catId));\n    const names = [];\n    ids.forEach(id => {\n      const term = TERM_BY_ID.get(id);\n      if (term?.name) names.push(term.name);\n    });\n\n    return tenderCats.some(n => names.includes(n));\n  }\n\n\/\/ ====== GROUPING NA KARCIE (ROOT + SUBS) ======\nfunction getRootTermById(termId){\n  let t = TERM_BY_ID.get(Number(termId));\n  if (!t) return null;\n\n  const guard = new Set();\n  while (t && t.parent && !guard.has(t.term_id)) {\n    guard.add(t.term_id);\n    t = TERM_BY_ID.get(Number(t.parent)) || t;\n    if (t.parent === 0) break;\n  }\n  return t && Number(t.parent) === 0 ? t : (t?.parent === 0 ? t : null);\n}\n\nfunction normalizeName(s){\n  return String(s || \"\").trim().replace(\/\\s+\/g, \" \");\n}\n\nfunction getTermIdsForName(name){\n  const key = normalizeName(name).toLowerCase();\n  return TERM_IDS_BY_NAME.get(key) || [];\n}\n\nfunction getTenderCategoryGroups(t){\n  if (!TERM_BY_ID || TERM_BY_ID.size === 0) return [];\n\n  const names = extractCategories(t).map(normalizeName).filter(Boolean);\n  if (!names.length) return [];\n\n  const groups = new Map(); \/\/ rootId -> {rootId, rootName, subs:Set, hits}\n\n  const addHit = (rootTerm, subTermOrNull) => {\n    const rid = Number(rootTerm.term_id);\n    if (!groups.has(rid)){\n      groups.set(rid, { rootId: rid, rootName: rootTerm.name, subs: new Set(), hits: 0 });\n    }\n    const g = groups.get(rid);\n    g.hits += 1;\n    if (subTermOrNull?.name) g.subs.add(subTermOrNull.name);\n  };\n\n  for (const name of names){\n    const ids = getTermIdsForName(name);\n    for (const id of ids){\n      const term = TERM_BY_ID.get(Number(id));\n      if (!term?.name) continue;\n\n      const root = getRootTermById(term.term_id);\n      if (!root?.term_id || !root?.name) continue;\n\n      if (Number(term.term_id) === Number(root.term_id)){\n        addHit(root, null);\n      } else {\n        addHit(root, term);\n      }\n    }\n  }\n\n  const out = Array.from(groups.values()).map(g => ({\n    rootId: g.rootId,\n    rootName: g.rootName,\n    subs: Array.from(g.subs).sort((a,b)=>String(a).localeCompare(String(b), \"pl\")),\n    hits: g.hits\n  }));\n\n  out.sort((a,b)=>{\n    if (b.hits !== a.hits) return b.hits - a.hits;\n    return String(a.rootName).localeCompare(String(b.rootName), \"pl\");\n  });\n\n  return out;\n}\n\nfunction pickGroupForCard(t, maxSubs = 3){\n  const groups = getTenderCategoryGroups(t);\n  if (!groups.length) return { rootName:null, subs:[] };\n\n  const selectedRootId = Number(($category.val() || \"\").toString().trim() || 0);\n  if (selectedRootId){\n    const g = groups.find(x => Number(x.rootId) === selectedRootId);\n    if (g) return { rootName: g.rootName, subs: g.subs.slice(0, maxSubs) };\n    return { rootName:null, subs:[] };\n  }\n\n  const top = groups[0];\n  return { rootName: top.rootName, subs: top.subs.slice(0, maxSubs) };\n}\n\n  \/\/ =========================\n  \/\/ TERMINY \/ DEADLINE\n  \/\/ =========================\n  const INCLUDE_UNKNOWN_DEADLINES = false;\n\n  function parseAnyDate(input){\n    const s0 = String(input ?? \"\").trim();\n    if (!s0) return null;\n    const s = s0.replace(\/\\s+\/g, \" \").trim();\n\n    let m = s.match(\/(\\d{4})-(\\d{1,2})-(\\d{1,2})\/);\n    if (m) {\n      const y = +m[1], mo = +m[2], d = +m[3];\n      const dt = new Date(Date.UTC(y, mo - 1, d));\n      return isNaN(dt) ? null : dt;\n    }\n\n    m = s.match(\/(\\d{1,2})[.\\-\/](\\d{1,2})[.\\-\/](\\d{4})\/);\n    if (m) {\n      const d = +m[1], mo = +m[2], y = +m[3];\n      const dt = new Date(Date.UTC(y, mo - 1, d));\n      return isNaN(dt) ? null : dt;\n    }\n\n    m = s.match(\/(\\d{1,2})[.\\-\/](\\d{1,2})[.\\-\/](\\d{2})(?!\\d)\/);\n    if (m) {\n      const d = +m[1], mo = +m[2], yy = +m[3];\n      const y = 2000 + yy;\n      const dt = new Date(Date.UTC(y, mo - 1, d));\n      return isNaN(dt) ? null : dt;\n    }\n\n    return null;\n  }\n\n  function dayStartUTC(d){\n    return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));\n  }\n\n  function tenderDeadlineDate(t){\n    const candidates = [\n      t.date_deadline, t.deadline_date, t.submission_deadline,\n      t.deadline, t.deadline_human, t.termin, t.end_date, t.date_to\n    ].filter(Boolean);\n\n    for (const c of candidates) {\n      const dt = parseAnyDate(c);\n      if (dt) return dt;\n    }\n\n    try {\n      const KEY_OK = \/(deadline|termin|end|date_to|do|submission|sklad|ofert|offer|closing)\/i;\n      const stack = [t];\n      const seen = new Set();\n\n      while (stack.length) {\n        const cur = stack.pop();\n        if (!cur || typeof cur !== \"object\") continue;\n        if (seen.has(cur)) continue;\n        seen.add(cur);\n\n        for (const k in cur) {\n          const v = cur[k];\n          if (typeof v === \"string\" && KEY_OK.test(k)) {\n            const dt = parseAnyDate(v);\n            if (dt) return dt;\n          } else if (v && typeof v === \"object\") {\n            stack.push(v);\n          }\n        }\n      }\n    } catch {}\n\n    return null;\n  }\n\n  function matchesSelectedDateRange(t){\n    const fromStr = ($(\"#dateFrom\").val() || \"\").toString();\n    const toStr   = ($(\"#dateTo\").val() || \"\").toString();\n\n    if (!fromStr && !toStr) return true;\n\n    const fromDt = fromStr ? parseAnyDate(fromStr) : null;\n    const toDt   = toStr ? parseAnyDate(toStr) : null;\n\n    const deadlineDt = tenderDeadlineDate(t);\n    if (!deadlineDt) return INCLUDE_UNKNOWN_DEADLINES;\n\n    const deadlineDay = dayStartUTC(deadlineDt).getTime();\n\n    if (fromDt) {\n      const fromDay = dayStartUTC(fromDt).getTime();\n      if (deadlineDay < fromDay) return false;\n    }\n    if (toDt) {\n      const toDay = dayStartUTC(toDt).getTime();\n      if (deadlineDay > toDay) return false;\n    }\n    return true;\n  }\n\n  function withDeadlineTs(t){\n    const dt = tenderDeadlineDate(t);\n    return dt ? dt.getTime() : Number.POSITIVE_INFINITY;\n  }\n\n  function sortBySoonestDeadline(a, b){\n    const ta = a.__dlTs ?? (a.__dlTs = withDeadlineTs(a));\n    const tb = b.__dlTs ?? (b.__dlTs = withDeadlineTs(b));\n    return ta - tb;\n  }\n\n  \/\/ =========================\n  \/\/ RENDER\n  \/\/ =========================\n  function hashString(str) {\n    let hash = 0;\n    for (let i = 0; i < str.length; i++) {\n      hash = str.charCodeAt(i) + ((hash << 5) - hash);\n    }\n    return hash;\n  }\n\n  function categoryStyleAuto(name) {\n    const hash = Math.abs(hashString(name));\n    const hue = hash % 360;\n    const bg = `hsl(${hue}, 85%, 92%)`;\n    const color = `hsl(${hue}, 45%, 32%)`;\n    return { bg, color };\n  }\n\n  \/\/ (zostawiamy Twoje pickGroupForCard \/ grupowanie - je\u015bli masz je wy\u017cej, nic nie zmieniaj)\n  \/\/ U Ciebie pickGroupForCard zale\u017cy od indeksu kategorii, wi\u0119c musi dzia\u0142a\u0107 po buildCategoryIndex().\n\n  function cardTemplate(t) {\n    const id = t.id ?? t.ID ?? \"\";\n    const titleRaw = t.title ?? t.post_title ?? \"Przetarg\";\n    const title = truncate(titleRaw, 105);\n    const deadline = t.deadline ?? t.deadline_human ?? t.termin ?? t.date_deadline ?? \"\u2014\";\n\n    \/\/ je\u015bli masz w kodzie pickGroupForCard, u\u017cyj; je\u015bli nie, poka\u017c tylko \"Brak kategorii\"\n    let catsHtml = `<span class=\"tender-tag tender-tag--muted\">Brak kategorii<\/span>`;\n    try{\n      if (typeof pickGroupForCard === \"function\") {\n        const picked = pickGroupForCard(t, 3);\n        const catsOrdered = [\n          ...(picked.rootName ? [picked.rootName] : []),\n          ...(picked.subs || [])\n        ];\n        catsHtml = catsOrdered.length\n          ? catsOrdered.map((name, idx) => {\n              const c = categoryStyleAuto(name);\n              const extra = idx === 0 ? \"font-weight:700;\" : \"\";\n              return `<span class=\"tender-tag\" style=\"background:${c.bg}; color:${c.color}; ${extra}\">${esc(name)}<\/span>`;\n            }).join(\"\")\n          : `<span class=\"tender-tag tender-tag--muted\">Brak kategorii<\/span>`;\n      }\n    }catch(e){}\n\n    const src = buildSource(t);\n    const sourceHtml = src.href\n      ? `<a class=\"tender-source-link\" href=\"${esc(src.href)}\" target=\"_blank\" rel=\"noopener\">${esc(src.label)}<\/a>`\n      : `<span class=\"tender-source-text\">${esc(src.label)}<\/span>`;\n\n    const payload = encodeURIComponent(JSON.stringify(t));\n\n    return `\n      <article class=\"tender-card\"\n        data-href=\"${tenderPageUrl}?id=${encodeURIComponent(id)}\"\n        data-payload=\"${payload}\">\n        <div class=\"tender-col tender-col--title\">\n          <p class=\"tender-title\" title=\"${esc(titleRaw)}\">\u201e${esc(title)}\u201d<\/p>\n        <\/div>\n        <div class=\"tender-col tender-col--cats\">\n          <div class=\"tender-tags tender-tags--col\">${catsHtml}<\/div>\n        <\/div>\n        <div class=\"tender-col tender-col--deadline\">\n          <div class=\"tender-deadline\">\n            <span class=\"tender-deadline-icon\" aria-hidden=\"true\">\n              <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n                <path d=\"M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71z\"\/>\n                <path d=\"M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0\"\/>\n              <\/svg>\n            <\/span>\n            <span>${esc(deadline)}<\/span>\n          <\/div>\n        <\/div>\n        <div class=\"tender-col tender-col--source\">\n          <div class=\"tender-source\">${sourceHtml}<\/div>\n        <\/div>\n      <\/article>\n    `;\n  }\n\n  function renderList(posts) {\n    if (!posts.length) {\n      $list.html(`<div style=\"padding:16px;color:#6b7280;\">Brak wynik\u00f3w dla wybranych filtr\u00f3w.<\/div>`);\n      return;\n    }\n    $list.html(posts.map(cardTemplate).join(\"\"));\n  }\n\n  $list.on(\"click\", \".tender-card\", function () {\n    const $card = $(this);\n    const payloadRaw = $card.attr(\"data-payload\");\n    const href = $card.attr(\"data-href\");\n\n    if (payloadRaw) {\n      try {\n        const obj = JSON.parse(decodeURIComponent(payloadRaw));\n        localStorage.setItem(SELECTED_KEY, JSON.stringify(obj));\n      } catch {}\n    }\n    if (href) window.location.href = href;\n  });\n\n  \/\/ =========================\n  \/\/ CLIENT PAGINATION (na ju\u017c posortowanej li\u015bcie)\n  \/\/ =========================\n  function clampPage(){\n    const total = Number(state.totalItems || 0);\n    const per = Number(state.posts_per_page || 10) || 10;\n    state.totalPages = Math.max(1, Math.ceil(total \/ per));\n    state.paged = clampInt(state.paged, 1, state.totalPages);\n  }\n\n  function renderCurrentPage(){\n    clampPage();\n    const start = (state.paged - 1) * state.posts_per_page;\n    const end   = start + state.posts_per_page;\n    renderList(state._filteredAll.slice(start, end));\n    updatePager();\n  }\n\n  \/\/ =========================\n  \/\/ FETCH: pobierz WSZYSTKO z backendu, potem filtruj+sortuj, potem paginuj lokalnie\n  \/\/ =========================\n  function fetchAllTenders() {\n    setLoading(true);\n\n    const collected = [];\n    let categoriesArr = null;\n\n    function ajaxPage(page){\n      return $.ajax({\n        url: AJAX_URL,\n        method: \"POST\",\n        timeout: 20000,\n        headers: { \"X-Requested-With\": \"XMLHttpRequest\" },\n        data: {\n          action: \"get_tenders\",\n          paged: page,\n          category: \"\",\n          search: buildFinalSearch(),\n          source: \"\",\n          posts_per_page: FETCH_PER_PAGE,\n          dateFrom: ($dateFrom.val() || \"\").toString(),\n          dateTo: ($dateTo.val() || \"\").toString(),\n        }\n      });\n    }\n\n    function step(page){\n      if (page > MAX_PAGES) return $.Deferred().resolve({ done:true }).promise();\n\n      return ajaxPage(page).then(function(raw){\n        let res = raw;\n        if (typeof raw === \"string\") {\n          try { res = JSON.parse(raw); } catch {}\n        }\n\n        const norm = normalizeResponse(res);\n        if (!norm.ok) {\n          return $.Deferred().reject({ message: norm.message || \"B\u0142\u0105d pobierania danych.\" }).promise();\n        }\n\n        if (!categoriesArr) {\n  const cats =\n    res?.data?.categories ||\n    res?.data?.data?.categories ||\n    res?.categories ||\n    [];\n  if (Array.isArray(cats) && cats.length) categoriesArr = cats;\n}\n\n        const posts = norm.posts || [];\n        collected.push(...posts);\n\n        const totalPages = Number(norm.pagination.totalPages || page);\n        const isLast = page >= totalPages;\n\n        \/\/ je\u015bli backend zwraca mniej ni\u017c FETCH_PER_PAGE, to te\u017c traktujemy jako koniec\n        const looksLikeEnd = posts.length < FETCH_PER_PAGE;\n\n        if (isLast || looksLikeEnd || collected.length >= MAX_TOTAL_ITEMS) {\n          return $.Deferred().resolve({ done:true }).promise();\n        }\n\n        return step(page + 1);\n      });\n    }\n\n    return step(1)\n      .done(function(){\n        \/\/ indeks kategorii (1x)\n        try{\n          if (Array.isArray(categoriesArr) && categoriesArr.length) buildCategoryIndex(categoriesArr);\n        }catch(e){}\n\n        state._allPosts = collected;\n\n        \/\/ filtruj + SORT GLOBALNIE\n        let filtered = collected.slice();\n        try{\n          filtered = filtered\n            .filter(matchesSelectedSource)\n            .filter(matchesSelectedCategory)\n            .filter(matchesSelectedDateRange)\n            .sort(sortBySoonestDeadline);\n        }catch(err){\n          console.error(\"Filtering error:\", err);\n        }\n\n        state._filteredAll = filtered;\n        state.totalItems = filtered.length;\n\n        applyPageSizeLimit();\n        state.paged = clampInt(state.paged, 1, 999999); \/\/ potem clampPage ustawi realnie\n        renderCurrentPage();\n\n        \/\/ opcjonalny komunikat, je\u015bli uci\u0119li\u015bmy list\u0119 bezpiecznikiem\n        if (collected.length >= MAX_TOTAL_ITEMS) {\n          console.warn(\"Osi\u0105gni\u0119to MAX_TOTAL_ITEMS, lista mo\u017ce by\u0107 uci\u0119ta:\", MAX_TOTAL_ITEMS);\n        }\n      })\n      .fail(function(err){\n        const msg = err?.message || \"B\u0142\u0105d pobierania danych.\";\n        $meta.text(msg);\n        renderList([]);\n        $prev.prop(\"disabled\", true);\n        $next.prop(\"disabled\", true);\n      })\n      .always(function(){\n        setLoading(false);\n      });\n  }\n\n  \/\/ =========================\n  \/\/ EVENT Z FILTR\u00d3W\n  \/\/ =========================\n  document.addEventListener(\"k4:filtersChanged\", function(e){\n    const d = e.detail || {};\n\n    if ($category.length && typeof d.category === \"string\") $category.val(d.category);\n    if ($subcategory.length && typeof d.subcategory === \"string\") $subcategory.val(d.subcategory);\n    if ($source.length && typeof d.source === \"string\") $source.val(d.source);\n    if ($dateFrom.length && typeof d.dateFrom === \"string\") $dateFrom.val(d.dateFrom);\n    if ($dateTo.length && typeof d.dateTo === \"string\") $dateTo.val(d.dateTo);\n\n    state.catKwIdx = 0;\n    state.paged = 1;\n\n    fetchAllTenders();\n  });\n\n  \/\/ =========================\n  \/\/ PAGINACJA (ju\u017c lokalna)\n  \/\/ =========================\n  $prev.on(\"click\", function (e) {\n    e.preventDefault();\n    if (state.paged > 1) {\n      state.paged--;\n      renderCurrentPage();\n    }\n  });\n\n  $next.on(\"click\", function (e) {\n    e.preventDefault();\n    if (state.paged < state.totalPages) {\n      state.paged++;\n      renderCurrentPage();\n    }\n  });\n\n  \/\/ =========================\n  \/\/ PAGE SIZE SELECT \u2014 lokalnie\n  \/\/ =========================\n  if ($pageSizeSelect.length) {\n    $pageSizeSelect.on(\"change\", function () {\n      const val = parseInt(($pageSizeSelect.val() || \"10\"), 10) || 10;\n      state.posts_per_page = val;\n      state.paged = 1;\n      applyPageSizeLimit();\n      renderCurrentPage();\n    });\n  }\n\n  \/\/ =========================\n  \/\/ FILTRY\n  \/\/ =========================\n  let kwTimer = null;\n  if ($kw.length) {\n    $kw.on(\"input\", function () {\n      clearTimeout(kwTimer);\n      kwTimer = setTimeout(() => {\n        state.paged = 1;\n        fetchAllTenders();\n      }, 250);\n    });\n\n    $kw.on(\"keydown\", function (e) {\n      if (e.key === \"Enter\") {\n        e.preventDefault();\n        state.paged = 1;\n        fetchAllTenders();\n      }\n    });\n  }\n\n  if ($category.length) {\n    $category.on(\"change\", function () {\n      state.catKwIdx = 0;\n      state.paged = 1;\n      fetchAllTenders();\n    });\n  }\n\n  if ($subcategory.length) {\n    $subcategory.on(\"change\", function () {\n      state.catKwIdx = 0;\n      state.paged = 1;\n      fetchAllTenders();\n    });\n  }\n\n  if ($source.length) {\n    $source.on(\"change\", function () {\n      state.catKwIdx = 0;\n      state.paged = 1;\n      fetchAllTenders();\n    });\n  }\n\n  if ($dateFrom.length) {\n    $dateFrom.on(\"change\", function () {\n      state.paged = 1;\n      fetchAllTenders();\n    });\n  }\n\n  if ($dateTo.length) {\n    $dateTo.on(\"change\", function () {\n      state.paged = 1;\n      fetchAllTenders();\n    });\n  }\n\n  \/\/ START\n  fetchAllTenders();\n});\n<\/script>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>\u00d7 Kategoria Wszystkie kategorie \u00d7 Podkategoria Wszystkie podkategorie \u00d7 \u0179r\u00f3d\u0142o Wszystkie \u017ar\u00f3d\u0142aFundusze EuropejskieBZP \/ eZam\u00f3wieniaPlatforma Zakupowa ORLENGrupy EneaPlatforma ZakupowaKGHMMarketplanetTauronGK PGE \u00d7 Nazwa Kategoria Termin \u0179r\u00f3d\u0142o Ilo\u015b\u0107 przetarg\u00f3w: 102550100200 \u0141adowanie\u2026 \u2190 Poprzednia Nast\u0119pna \u2192<\/p>\n","protected":false},"author":6,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-27889","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/oferty.k4.pl\/index.php\/wp-json\/wp\/v2\/pages\/27889","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/oferty.k4.pl\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/oferty.k4.pl\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/oferty.k4.pl\/index.php\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/oferty.k4.pl\/index.php\/wp-json\/wp\/v2\/comments?post=27889"}],"version-history":[{"count":703,"href":"https:\/\/oferty.k4.pl\/index.php\/wp-json\/wp\/v2\/pages\/27889\/revisions"}],"predecessor-version":[{"id":36107,"href":"https:\/\/oferty.k4.pl\/index.php\/wp-json\/wp\/v2\/pages\/27889\/revisions\/36107"}],"wp:attachment":[{"href":"https:\/\/oferty.k4.pl\/index.php\/wp-json\/wp\/v2\/media?parent=27889"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}