{"id":27920,"date":"2025-12-11T12:31:01","date_gmt":"2025-12-11T11:31:01","guid":{"rendered":"https:\/\/oferty.k4.pl\/?page_id=27920"},"modified":"2026-03-10T09:31:31","modified_gmt":"2026-03-10T08:31:31","slug":"moje-analizy","status":"publish","type":"page","link":"https:\/\/oferty.k4.pl\/index.php\/moje-analizy\/","title":{"rendered":"Moje analizy"},"content":{"rendered":"\n<div class=\"wp-block-group alignwide has-global-padding is-content-justification-left is-layout-constrained wp-container-core-group-is-layout-353cc7fb wp-block-group-is-layout-constrained\">\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*{ box-sizing:border-box; font-family:\"Inter\",sans-serif !important; }\n\n\/* ====== TOP BAR ====== *\/\n.my-analyses-top{\n  box-sizing: border-box !important;\n  max-width: 1150px !important;\n  width: calc(100% - 40px) !important;\n  margin: 0 auto !important;\n\n  display: flex !important;\n  align-items: center !important;\n  justify-content: space-between !important;\n  gap: 24px !important;\n\n  padding: 0 !important;\n}\n.my-analyses-left{ flex: 1 1 auto !important; min-width: 0 !important; }\n.my-analyses-desc{\n  margin: 0 !important;\n  max-width: 720px;\n  font-size:12px;\n  line-height:1.45;\n  color:#6b7280;\n  font-weight:600;\n}\n.my-analyses-right{\n  flex: 0 0 auto !important;\n  display: flex !important;\n  align-items: center !important;\n  gap: 12px !important;\n}\n.my-analyses-meta{\n  font-size:12px;\n  color:#6b7280;\n  font-weight:700;\n  white-space:nowrap;\n}\n.my-analyses-reload{\n  border:0;\n  background:#2563EB;\n  color:white;\n  border-radius:999px;\n  height:40px;\n  padding:0 18px;\n  font-weight:700;\n  cursor:pointer;\n  box-shadow: 0 14px 30px rgba(37,99,235,.35);\n  outline:none;\n}\n.my-analyses-reload:active{ transform: translateY(1px);}\n.my-analyses-alert{\n  max-width:1150px;\n  margin: 8px auto 0;\n  padding: 0 20px;\n  font-size:12px;\n  font-weight:600;\n  color:#6b7280;\n}\n\n\/* ====== HEADER TABELI ====== *\/\n.table-header-wrapper{\n  width:100%;\n  display:flex;\n  justify-content:center;\n  padding: 14px 0 0;\n}\n.table-header{\n  width:100%;\n  max-width:1150px;\n  background:white;\n  border-radius:999px;\n  padding:14px 28px;\n  display:grid;\n  grid-template-columns: minmax(0, 1fr) repeat(4, minmax(0, 0.65fr));\n  align-items:center;\n  text-align:center;\n  font-size:14px;\n  font-weight:600;\n  box-shadow:\n    0px 8px 30px rgba(0, 0, 0, 0.15),\n    0px 8px 20px rgba(0, 0, 0, 0.15);\n}\n\n\/* LISTA *\/\n.tenders-list{ margin:24px auto; padding:0 20px; }\n\n\/* KARTA *\/\n.tender-card{\n  width:100%;\n  max-width:1150px;\n  background:#fff;\n  border-radius:24px;\n  padding:14px 28px;\n  margin:12px auto 0;\n  border: 1px solid #e5e7eb;\n  box-shadow: 0px 8px 20px rgba(0,0,0,0.25);\n  display:grid;\n  grid-template-columns: minmax(0, 0.9fr) minmax(0, 0.75fr) repeat(3, minmax(0, 0.65fr));\n  column-gap:28px;\n  align-items:center;\n  font-size:13px;\n  color:#111827;\n  transition: transform 0.15s ease-out;\n}\n.tender-card:hover { transform: translateY(-1px); }\n\n.tender-card__cats{ padding-right: 36px; }\n\n.tender-card__name{ text-align:left; }\n.tender-card__cats{ text-align:left; }\n.tender-card__date{ text-align:center; }\n.tender-card__source{ text-align:center; }\n.tender-card__actions{ text-align:left !important; }\n\n.tender-title{\n  margin:0;\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\n  overflow: hidden;\n  text-overflow: ellipsis;\n  word-break: break-word;\n}\n\n\/* ====== KOLOR CHMURKI WG WYNIKU ====== *\/\n.tender-card.is-meets{\n  background:#DCFCE7 !important;\n  border-color:transparent !important;\n}\n.tender-card.is-not-meets{\n  background:#FCDAD9 !important;\n  border-color:transparent !important;\n}\n.tender-card.is-unknown{\n  background:#e5e7eb !important;\n  border-color:transparent !important;\n}\n\n.tender-tags{ display:flex; flex-wrap:wrap; gap:6px; }\n.tender-tag{\n  display:inline-flex; align-items:center; padding:4px 12px; border-radius:999px;\n  font-size:11px; font-weight:500; width:auto; max-width:max-content !important; white-space:nowrap;\n}\n.tender-tag--green{ background:#DCFCE7; color:#16A34A; }\n.tender-tag--yellow{ background:#FCDAD9; color:#c48280; }\n.tender-tag--gray{ background:#E5E7EB; color:#374151; }\n\n.tender-deadline{ display:inline-flex; align-items:center; gap:8px; justify-content:center; }\n.tender-deadline-icon{ display:inline-flex; align-items:center; justify-content:center; line-height:0; }\n\n.tender-source{ display:block; max-width:100%; font-weight:600; padding-left:10px; }\n.tender-source a{\n  color:#2563EB; text-decoration:none; line-height:1.3; outline:none;\n  display:inline-block; max-width:100%; white-space:normal; overflow-wrap:anywhere; word-break:break-word;\n}\n.tender-source a:hover{ text-decoration:underline; }\n\n.tender-actions{\n  display:flex;\n  flex-direction:column;\n  align-items:flex-start;\n  padding-left:50px;\n  justify-content:center;\n  line-height:1.3;\n}\n.tender-action-link{\n  font-size:13px; font-weight:600; color:#2563EB; text-decoration:none; outline:none; cursor:pointer;\n}\n.tender-action-link:hover{ text-decoration:underline; }\n.tender-action-delete{ color:#dc2626; }\n.tender-action-delete:hover{ text-decoration:underline; }\n\n\/* ====== PANEL ANALIZY ====== *\/\n.analysis-panel{\n  max-width:1150px;\n  margin: 8px auto 0;\n  padding: 14px 18px;\n  border: 1px solid #e5e7eb;\n  border-radius: 18px;\n  background: #ffffff;\n  box-shadow: 0px 8px 20px rgba(0,0,0,0.10);\n  display:none;\n}\n.analysis-panel.is-open{ display:block; }\n.analysis-head{\n  display:flex;\n  align-items:center;\n  justify-content:space-between;\n  gap:10px;\n}\n.analysis-badges{ display:none;}\n\n\/* ====== TABELA ANALIZY ====== *\/\n.analysis-table{\n  width:100%;\n  border-collapse:separate;\n  border-spacing:0;\n  border:1px solid #e5e7eb;\n  border-radius:14px;\n  overflow:hidden;\n  background:#fff;\n  font-size:12px;\n}\n.analysis-table th,\n.analysis-table td{\n  padding:10px 12px;\n  vertical-align:top;            \n  border-bottom:1px solid #e5e7eb;\n}\n\n\/* akapity w kom\u00f3rkach *\/\n.analysis-cell p{\n  margin:0 0 5px 0;\n  line-height:1.5;\n}\n.analysis-cell p:last-child{ margin-bottom:0; }\n.analysis-table tr:last-child th,\n.analysis-table tr:last-child td{ border-bottom:0; }\n.analysis-table th{\n  width:220px;\n  color:black;\n  font-weight:500;\n  text-transform:uppercase;\n  letter-spacing:.01em;\n  background:#E5E7EB;\n  white-space:nowrap;\n}\n.analysis-table td p:last-child{\n  margin-bottom:0;\n}\n\n.analysis-table td{\n  color:#111827;\n  font-size:12px;\n  font-weight:500;\n  word-break:break-word;\n}\n\n\n\/* ====== BUTTON: GENERUJ MAIL ====== *\/\n.df-generate-mail-btn{\n  border:0;\n  background:transparent;\n  padding:0;\n  margin-top:6px;\n\n  font-size:13px;\n  font-weight:600;\n  color:#111827;\n\n  cursor:pointer;\n  text-align:left;\noutline:none;\n}\n\n.df-generate-mail-btn:hover{\n  text-decoration:underline;\n}\n\n.df-generate-mail-btn:disabled{\n  opacity:.6;\n  cursor:default;\n  text-decoration:none;\n}\n\n.df-generate-mail-btn:active{\n  transform:none;\n}\n\/* ====== MODAL MAILA ====== *\/\n.df-mail-modal{ position:fixed; inset:0; display:none; z-index:9999; }\n.df-mail-modal.is-open{ display:block; }\n.df-mail-modal__backdrop{ position:absolute; inset:0; background: rgba(17,24,39,.55); }\n.df-mail-modal__card{\n  position:relative;\n  width: calc(100% - 32px);\n  max-width: 760px;\n  margin: 6vh auto 0;\n  background:#fff;\n  border-radius:24px;\n  border:1px solid #e5e7eb;\n  box-shadow: 0 20px 60px rgba(0,0,0,.35);\n  overflow:hidden;\n}\n.df-mail-modal__top{\n  display:flex; align-items:flex-start; justify-content:space-between; gap:12px;\n  padding:16px 18px; border-bottom:1px solid #e5e7eb;\n}\n.df-mail-modal__title{ font-size:14px; font-weight:800; color:#111827; }\n.df-mail-modal__subtitle{ font-size:12px; font-weight:600; color:#6b7280; margin-top:4px; }\n.df-mail-modal__close{ border:0; background:transparent; font-size:18px; cursor:pointer; padding:4px 8px; color:#111827; outline:none; }\n.df-mail-modal__body{ padding:14px 18px; }\n.df-mail-field{ display:block; margin-bottom:12px; }\n.df-mail-field span{ display:block; font-size:12px; font-weight:700; color:#374151; margin-bottom:6px; }\n.df-mail-field input, .df-mail-field textarea{\n  width:100%; border:1px solid #e5e7eb; border-radius:14px;\n  padding:10px 12px; font-size:13px; font-weight:600; outline:none;\n}\n.df-mail-field textarea{ resize:vertical; min-height:180px; }\n.df-mail-hint{ font-size:12px; font-weight:700; color:#6b7280; margin-top:8px; }\n.df-mail-modal__actions{\n  display:flex; justify-content:flex-end; gap:10px;\n  padding:14px 18px; border-top:1px solid #e5e7eb;\n}\n.df-btn{\n  border:0; background:#2563EB; color:white; border-radius:999px;\n  height:38px; padding:0 16px; font-weight:800; cursor:pointer;\n}\n.df-btn--ghost{ background:#fff; color:#2563EB; border:1px solid #2563EB; }\n\n\/* ====== DOKUMENTY W PANELU ANALIZY ====== *\/\n.analysis-docs{\n  margin-top: 12px;\n  padding-top: 12px;\n  border-top: 1px solid #e5e7eb;\n}\n\n.analysis-docs__head{\n  display:flex;\n  align-items:center;\n  justify-content:space-between;\n  gap:12px;\n  margin-bottom: 8px;\n}\n\n.analysis-docs__label{\n  font-size: 12px;\n  font-weight: 600;\n  color:#111827;\n}\n\n.analysis-docs__meta{\n  font-size: 13px;\n  color:black;\n  font-weight: 300;\n}\n\n.analysis-docs__list{\n  display:flex;\n  flex-direction:column;\n  gap: 8px;\n}\n\n.analysis-doc{\n  display:flex;\n  align-items:center;\n  justify-content:space-between;\n  gap: 12px;\n\n  padding: 1px 12px;\n  border: 1px solid #E5E7EB;\n  border-radius: 16px;\n  background:#E5E7EB;\n}\n\n.analysis-doc__name{\n  min-width:0;\n  font-size: 13px;\n  font-weight: 300;\n  color:#6b7280;\n  overflow:hidden;\n  text-overflow:ellipsis;\n  white-space:nowrap;\n}\n\n.analysis-doc__open{\n  flex: 0 0 auto;\n  display:inline-flex;\n  align-items:center;\n  justify-content:center;\n  padding: 8px 12px;\n  border-radius: 999px;\n  border: 1px solid #e5e7eb;\n  font-size: 13px;\n  font-weight: 600;\n  text-decoration:none;\noutline:none;\n  color:#2563EB;\n  background:none;\n}\n.analysis-doc__open:hover{  }\n\n.analysis-docs__state{\n  padding: 10px 12px;\n  border: 1px dashed #e5e7eb;\n  border-radius: 16px;\n  font-size: 12px;\n  color:#6b7280;\n  background:#fff;\n}\n\n\/* MOBILE *\/\n@media (max-width:720px){\n  .table-header-wrapper{ display:none; }\n\n  .my-analyses-top{\n    flex-direction: column;\n    align-items: stretch;\n  }\n  .my-analyses-right{\n    justify-content: space-between;\n    padding-top: 0;\n  }\n  .my-analyses-reload{ width: 140px; }\n\n  .tender-card{\n    grid-template-columns: 1fr auto;\n    grid-template-areas: \"name name\" \"cats cats\" \"date date\" \"source actions\";\n    row-gap: 12px;\n    column-gap: 16px;\n    align-items: center;\n  }\n\n  .tender-card__name { grid-area: name; }\n  .tender-card__cats { grid-area: cats; }\n  .tender-card__date { grid-area: date; }\n  .tender-card__source { grid-area: source; }\n  .tender-card__actions{ grid-area: actions; }\n\n  .tender-deadline{ justify-content:flex-start; }\n  .tender-actions{ display:flex; align-items:flex-end; gap:12px; padding-left:0; }\n  .analysis-panel{ margin-left:20px; margin-right:20px; }\n  .analysis-table th{ width: 140px; }\n}\n<\/style>\n\n<div class=\"my-analyses-top\">\n  <div class=\"my-analyses-left\">\n    <p class=\"my-analyses-desc\" id=\"myAnalysesDesc\"><\/p>\n  <\/div>\n  <div class=\"my-analyses-right\">\n    <span class=\"my-analyses-meta\" id=\"myAnalysesMeta\">\u0141adowanie\u2026<\/span>\n    <button class=\"my-analyses-reload\" id=\"myAnalysesReload\" type=\"button\">Od\u015bwie\u017c<\/button>\n  <\/div>\n<\/div>\n\n<div class=\"my-analyses-alert\" id=\"myAnalysesAlert\"><\/div>\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>Akcje<\/div>\n  <\/div>\n<\/div>\n\n<div class=\"tenders-list\" id=\"my-analyses-list\"><\/div>\n\n<!-- ====== MODAL: Wygenerowany mail ====== -->\n<div class=\"df-mail-modal\" id=\"dfMailModal\" aria-hidden=\"true\">\n  <div class=\"df-mail-modal__backdrop\" data-df-mail-close><\/div>\n\n  <div class=\"df-mail-modal__card\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"dfMailTitle\">\n    <div class=\"df-mail-modal__top\">\n      <div>\n        <div class=\"df-mail-modal__title\" id=\"dfMailTitle\">Wygenerowana wiadomo\u015b\u0107<\/div>\n      <\/div>\n      <button class=\"df-mail-modal__close\" type=\"button\" data-df-mail-close>\u2715<\/button>\n    <\/div>\n\n    <div class=\"df-mail-modal__body\">\n      \n      <label class=\"df-mail-field\">\n        <span>Tre\u015b\u0107<\/span>\n        <textarea id=\"dfMailBody\" rows=\"12\" placeholder=\"Tre\u015b\u0107 wiadomo\u015bci\"><\/textarea>\n      <\/label>\n\n      <div class=\"df-mail-hint\" id=\"dfMailHint\"><\/div>\n    <\/div>\n\n    <div class=\"df-mail-modal__actions\">\n      <button class=\"df-btn df-btn--ghost\" type=\"button\" id=\"dfCopyMail\">Kopiuj<\/button>\n      <button class=\"df-btn\" type=\"button\" data-df-mail-close>Zamknij<\/button>\n    <\/div>\n  <\/div>\n<\/div>\n\n\n<script>\n(function(){\n  const AJAX_URL = (window.ajaxurl) ? window.ajaxurl : (location.origin + \"\/wp-admin\/admin-ajax.php\");\n  const ACTION = \"get_my_analysis\";\n  const DELETE_ACTION = \"delete_my_analysis\";\n\n  const PENDING_KEY = \"k4_pending_analyses\";\n\n  const listEl  = document.getElementById(\"my-analyses-list\");\n  const metaEl  = document.getElementById(\"myAnalysesMeta\");\n  const alertEl = document.getElementById(\"myAnalysesAlert\");\n  const reloadBtn = document.getElementById(\"myAnalysesReload\");\n  const descEl = document.getElementById(\"myAnalysesDesc\");\n  if (!listEl) return;\n\n  \/\/ ====== AUTO-OD\u015aWIE\u017bANIE ======\n  const POLL_EVERY_MS = 15000;\n  const POLL_MAX_MS   = 8 * 60 * 1000;\n  let pollTimer = null;\n  let pollStopAt = 0;\n\n  function shouldPoll(){\n    const qs = new URLSearchParams(location.search);\n    const sent = qs.get(\"sent\") === \"1\";\n    const pending = loadPending();\n    return sent || pending.length > 0;\n  }\n\n  function startPolling(){\n    if (!shouldPoll()) return;\n    if (pollTimer) return;\n\n    pollStopAt = Date.now() + POLL_MAX_MS;\n\n    pollTimer = setInterval(() => {\n      if (Date.now() > pollStopAt){\n        stopPolling();\n        setDesc(\"Oferta pojawi si\u0119 tutaj po zako\u0144czeniu analizy.\");\n        return;\n      }\n      fetchAnalyses(true);\n    }, POLL_EVERY_MS);\n  }\n\n  function stopPolling(){\n    if (!pollTimer) return;\n    clearInterval(pollTimer);\n    pollTimer = null;\n  }\n\n  \/\/ ====== HELPERS ======\n  function setMeta(t){ if (metaEl) metaEl.textContent = t; }\n  function setDesc(t){ if (descEl) descEl.textContent = t || \"\"; }\n  function setAlert(t, type){\n    if (!alertEl) return;\n    alertEl.style.color = (type === \"error\") ? \"#dc2626\" : \"#6b7280\";\n    alertEl.textContent = t || \"\";\n  }\n\n  function esc(s){\n    return String(s ?? \"\")\n      .replaceAll(\"&\",\"&amp;\")\n      .replaceAll(\"<\",\"&lt;\")\n      .replaceAll(\">\",\"&gt;\")\n      .replaceAll('\"',\"&quot;\")\n      .replaceAll(\"'\",\"&#039;\");\n  }\n  function escText(s){\n    return String(s ?? \"\")\n      .replaceAll(\"&\",\"&amp;\")\n      .replaceAll(\"<\",\"&lt;\")\n      .replaceAll(\">\",\"&gt;\")\n      .replaceAll('\"',\"&quot;\")\n      .replaceAll(\"'\",\"&#039;\");\n  }\n  function isObj(v){ return v && typeof v === \"object\" && !Array.isArray(v); }\n\nfunction normalizeMeets(value){\n  if (value === true || value === 1) return true;\n  if (value === false || value === 0) return false;\n\n  const v = String(value ?? \"\").trim().toLowerCase();\n\n  \/\/ true\n  if ([\"tak\",\"true\",\"1\",\"yes\",\"y\"].includes(v)) return true;\n\n  \/\/ false\n  if ([\"nie\",\"false\",\"0\",\"no\",\"n\"].includes(v)) return false;\n\n  \/\/ cz\u0119ste backendowe opisy\n  if (v.includes(\"nie spe\u0142nia\")) return false;\n  if (v.includes(\"spe\u0142nia\")) return true;\n\n  return null;\n}\n\nfunction findMeetsInAnalysis(analysis){\n  if (!analysis || typeof analysis !== \"object\") return null;\n\n  \/\/ przeszukujemy zar\u00f3wno top-level jak i zagnie\u017cd\u017cenia przez flatten\n  const flat = flattenObject(analysis, \"\", {}, 0, 3);\n\n  const entries = Object.entries(flat);\n\n  for (const [k, v] of entries){\n    const keyNorm = String(k || \"\")\n      .trim()\n      .toLowerCase()\n      .replace(\/[:?]+$\/g, \"\");\n\n    \/\/ typowe etykiety z backendu\n    if (\n      keyNorm.includes(\"spe\u0142niasz warunki\") ||\n      keyNorm.includes(\"spelnasz warunki\") ||\n      keyNorm.includes(\"meets_conditions\") ||\n      keyNorm === \"spelnia warunki\" ||\n      keyNorm === \"spe\u0142nia warunki\"\n    ){\n      const m = normalizeMeets(v);\n      if (m !== null) return m;\n    }\n  }\n\n  return null;\n}\n\nfunction resolveMeets(item){\n  \/\/ 1) g\u0142\u00f3wne pole z backendu\n  const direct = normalizeMeets(item?.meets_conditions);\n  if (direct !== null) return direct;\n\n  \/\/ 2) fallback: szukamy w analysis\n  const fromAnalysis = findMeetsInAnalysis(item?.analysis);\n  if (fromAnalysis !== null) return fromAnalysis;\n\n  return null;\n}\n\n  function unwrapToArray(payload){\n    if (Array.isArray(payload)) return payload;\n\n    if (isObj(payload) && \"success\" in payload){\n      if (!payload.success){\n        const msg = typeof payload.data === \"string\" ? payload.data : (payload.data?.message || \"B\u0142\u0105d get_my_analysis\");\n        throw new Error(msg);\n      }\n      return unwrapToArray(payload.data);\n    }\n\n    if (isObj(payload)){\n      const candidates = [\"items\",\"analyses\",\"results\",\"data\"];\n      for (const k of candidates){\n        if (Array.isArray(payload[k])) return payload[k];\n        if (isObj(payload[k])){\n          for (const kk of candidates){\n            if (Array.isArray(payload[k][kk])) return payload[k][kk];\n          }\n        }\n      }\n    }\n    return [];\n  }\n\nconst GEN_MAIL_ACTION = \"df_generate_mail\";\nconst GET_MAIL_ACTION = \"df_get_mail\";\n\nfunction openMailModal(){\n  const m = document.getElementById(\"dfMailModal\");\n  if (!m) return;\n  m.classList.add(\"is-open\");\n  m.setAttribute(\"aria-hidden\",\"false\");\n}\nfunction closeMailModal(){\n  const m = document.getElementById(\"dfMailModal\");\n  if (!m) return;\n  m.classList.remove(\"is-open\");\n  m.setAttribute(\"aria-hidden\",\"true\");\n}\nfunction setMailHint(t, type){\n  const el = document.getElementById(\"dfMailHint\");\n  if (!el) return;\n\n  el.textContent = t || \"\";\n  \/\/ typy: info | error | success\n  el.style.color =\n    (type === \"error\") ? \"#dc2626\" :\n    (type === \"success\") ? \"#16a34a\" :\n    \"#6b7280\";\n\n  el.style.display = t ? \"block\" : \"none\";\n}\n\nfunction setMailLoading(on, msg){\n  setMailHint(msg || (on ? \"Generuj\u0119 wiadomo\u015b\u0107\u2026\" : \"\"), \"info\");\n  [\"dfMailTo\",\"dfMailSubject\",\"dfMailBody\",\"dfCopyMail\"].forEach(id=>{\n    const el = document.getElementById(id);\n    if (el) el.disabled = !!on;\n  });\n}\n\nfunction unwrapWp(payload){\n  if (payload && typeof payload === \"object\" && \"success\" in payload){\n    if (!payload.success){\n      const d = payload.data;\n      const msg =\n        (typeof d === \"string\" && d) ? d :\n        (d?.msg || d?.message || payload.message || \"B\u0142\u0105d\");\n      throw new Error(msg);\n    }\n    return payload.data;\n  }\n  return payload;\n}\n\nasync function postAjax(action, analysisId){\n  const fd = new FormData();\n  fd.append(\"action\", action);\n  fd.append(\"analysis_id\", String(analysisId));\n\n  const res = await fetch(AJAX_URL, {\n    method:\"POST\",\n    body: fd,\n    credentials: \"same-origin\",\n    cache: \"no-store\"\n  });\n\n  const raw = await res.text();\n  let json = raw;\n  try{ json = JSON.parse(raw); }catch(e){}\n  return unwrapWp(json);\n}\n\nfunction mailLooksReady(data){\n  const status = String(data?.status || \"\").toLowerCase();\n  if (status === \"pending\" || status === \"processing\") return false;\n\n  \/\/ \u2705 je\u015bli status=ready -> uznaj za gotowe\n  if (status === \"ready\" || status === \"done\") return true;\n\n  const mail = data?.mail ?? data;\n\n  \/\/ \u2705 je\u015bli mail jest stringiem i niepusty -> gotowe\n  if (typeof mail === \"string\") return mail.trim().length > 0;\n\n  \/\/ \u2705 je\u015bli mail jest obiektem\n  if (mail && typeof mail === \"object\"){\n    const subject = String(mail.subject || mail.mail_subject || \"\").trim();\n    const body = String(mail.body || mail.mail_body || mail.message || \"\").trim();\n    return (subject.length > 0 || body.length > 0);\n  }\n\n  return false;\n}\n\nasync function pollGetMail(analysisId, title, maxMs = 8 * 60 * 1000, everyMs = 5000){\n  const started = Date.now();\n\n  while (Date.now() - started < maxMs){\n    let mailData = null;\n\n    try{\n      mailData = await postAjax(GET_MAIL_ACTION, analysisId);\n    }catch(e){\n      \/\/ je\u015bli chwilowo backend nie odpowiada, po prostu pr\u00f3bujemy dalej\n      mailData = null;\n    }\n\n    \/\/ \ud83d\udd39 je\u015bli ju\u017c mamy subject\/body -> uzupe\u0142nij modal i ko\u0144cz\n    if (mailData && mailLooksReady(mailData)){\n      fillMailModal(mailData, title);\n      setMailLoading(false, \"\");\n      setMailHint(\"Gotowe. Mo\u017cesz skopiowa\u0107 tre\u015b\u0107.\", \"success\");\n      return true;\n    }\n\n   const status = String(mailData?.status || \"\").toLowerCase();\nif (!mailData || status === \"pending\" || status === \"processing\" || status === \"\"){\n  setMailHint(\"Generowanie w toku\u2026 to mo\u017ce potrwa\u0107 1\u20133 min. (od\u015bwie\u017cam automatycznie)\", \"info\");\n}\n\n    await new Promise(r => setTimeout(r, everyMs));\n  }\n\n  \/\/ po max czasie nadal nic -> nie strasz curl errorem, daj UX\n  setMailLoading(false, \"\");\n  setMailHint(\"Wci\u0105\u017c trwa generowanie. Zamknij okno i spr\u00f3buj za chwil\u0119 ponownie.\", \"error\");\n  return false;\n}\n\nfunction fillMailModal(data, title){\n  const mail = data?.mail ?? data ?? {};\n\n  \/\/ mail mo\u017ce by\u0107 stringiem (u Ciebie backend zwraca \"mail\": \"Temat: ...\\n\\nTre\u015b\u0107...\")\n  const mailStr = (typeof mail === \"string\") ? mail : null;\n\n  let to = \"\";\n  let subject = \"\";\n  let body = \"\";\n\n  if (mailStr){\n    \/\/ Spr\u00f3buj wyci\u0105gn\u0105\u0107 \"Temat:\" z pierwszej linii\n    const m = mailStr.match(\/^\\s*Temat:\\s*(.+)\\s*$\/m);\n    subject = m ? m[1].trim() : \"\";\n\n    \/\/ Body: wszystko bez linii \"Temat: ...\"\n    body = mailStr.replace(\/^\\s*Temat:\\s*.+\\s*\\n?\/m, \"\").trim();\n  } else {\n    subject = (mail.subject || mail.mail_subject || \"\").trim();\n    body    = (mail.body || mail.mail_body || mail.message || \"\").trim();\n    to      = (mail.to || mail.mail_to || \"\").trim();\n  }\n\n  \/\/ \u2705 \"Dotyczy: ...\" ma i\u015b\u0107 do pola TEMAT, jako fallback\n  const fallbackSubject = title ? `Dotyczy: ${title}` : \"\";\n  if (!subject) subject = fallbackSubject;\n\n  const toEl = document.getElementById(\"dfMailTo\");\n  const sEl  = document.getElementById(\"dfMailSubject\");\n  const bEl  = document.getElementById(\"dfMailBody\");\n\n  if (toEl) toEl.value = to;\n  if (sEl)  sEl.value = subject;\n  if (bEl)  bEl.value = body;\n\n  \/\/ \u2705 subtitle pod nag\u0142\u00f3wkiem ju\u017c nie u\u017cywany\n  const sub = document.getElementById(\"dfMailSubtitle\");\n  if (sub) sub.textContent = \"\";\n}\n\n\n\/\/ ====== DOKUMENTY (jak w podgl\u0105dzie) ======\nconst DOCS_ACTION = \"df_get_media_files\";\nconst DOCS_CACHE = new Map(); \/\/ key -> files[]\n\nfunction filenameFromUrl(url){\n  try{\n    const u = new URL(String(url), location.origin);\n    const last = u.pathname.split(\"\/\").pop() || \"Dokument\";\n    return decodeURIComponent(last);\n  }catch{\n    const s = String(url || \"\");\n    return s.split(\"\/\").pop() || \"Dokument\";\n  }\n}\n\nfunction setDocsState(panelEl, text){\n  const list = panelEl.querySelector(\".analysis-docs__list\");\n  if (!list) return;\n  list.innerHTML = `<div class=\"analysis-docs__state\">${esc(text)}<\/div>`;\n}\n\nfunction renderDocs(panelEl, files){\n  const meta = panelEl.querySelector(\".analysis-docs__meta\");\n  const list = panelEl.querySelector(\".analysis-docs__list\");\n  if (!meta || !list) return;\n\n  if (!Array.isArray(files) || files.length === 0){\n    meta.textContent = \"\";\n    setDocsState(panelEl, \"Brak dokument\u00f3w.\");\n    return;\n  }\n\n  meta.textContent =\n    `${files.length} ${files.length === 1 ? \"plik\" : (files.length < 5 ? \"pliki\" : \"plik\u00f3w\")}`;\n\n  list.innerHTML = files.map(f => {\n    \/\/ backend mo\u017ce zwraca\u0107 stringi (URL) albo obiekty\n    const url = (typeof f === \"string\")\n      ? f\n      : (f?.url || f?.link || f?.file_url || f?.attachment_url || \"\");\n\n    const name = (typeof f === \"string\")\n      ? filenameFromUrl(f)\n      : (f?.name || f?.filename || f?.title || (url ? filenameFromUrl(url) : \"Dokument\"));\n\n    return `\n      <div class=\"analysis-doc\">\n        <div class=\"analysis-doc__name\" title=\"${esc(name)}\">${esc(name)}<\/div>\n        ${url ? `<a class=\"analysis-doc__open\" href=\"${esc(url)}\" target=\"_blank\" rel=\"noopener\">Otw\u00f3rz<\/a>` : ``}\n      <\/div>\n    `;\n  }).join(\"\");\n}\n\nasync function loadDocsIntoPanel(panelEl){\n  if (!panelEl) return;\n\n  \/\/ nie \u0142aduj drugi raz\n  if (panelEl.getAttribute(\"data-docs-loaded\") === \"1\") return;\n\n  const postId = panelEl.getAttribute(\"data-post-id\") || \"\";\n  const title = panelEl.getAttribute(\"data-docs-title\") || \"\";\n  const deadline = panelEl.getAttribute(\"data-docs-deadline\") || \"\";\n\n  \/\/ cache key\n  const cacheKey = `${postId}|${title}|${deadline}`;\n  if (DOCS_CACHE.has(cacheKey)){\n    renderDocs(panelEl, DOCS_CACHE.get(cacheKey));\n    panelEl.setAttribute(\"data-docs-loaded\",\"1\");\n    return;\n  }\n\n  setDocsState(panelEl, \"\u0141adowanie dokument\u00f3w\u2026\");\n\n  try{\n    const body = new URLSearchParams();\n    body.set(\"action\", DOCS_ACTION);\n    body.set(\"post_id\", postId);\n    body.set(\"title\", title);\n    body.set(\"deadline\", deadline);\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: body.toString(),\n      credentials: \"same-origin\",\n      cache: \"no-store\"\n    });\n\n    const raw = await res.text();\n    if (!raw || raw.trim() === \"\" || raw.trim() === \"0\"){\n      setDocsState(panelEl, \"Brak danych z backendu.\");\n      return;\n    }\n\n    let data = null;\n    try{ data = JSON.parse(raw); } catch(e){ data = null; }\n\n    const files =\n      Array.isArray(data) ? data :\n      Array.isArray(data?.data) ? data.data :\n      Array.isArray(data?.files) ? data.files :\n      Array.isArray(data?.data?.files) ? data.data.files :\n      [];\n\n    DOCS_CACHE.set(cacheKey, files);\n    renderDocs(panelEl, files);\n    panelEl.setAttribute(\"data-docs-loaded\",\"1\");\n\n  }catch(e){\n    console.error(\"df_get_media_files error:\", e);\n    setDocsState(panelEl, \"Nie uda\u0142o si\u0119 pobra\u0107 dokument\u00f3w.\");\n  }\n}\n\n  \/\/ ====== PENDING QUEUE ======\n  function loadPending(){\n    try{\n      const arr = JSON.parse(localStorage.getItem(PENDING_KEY) || \"[]\");\n      return normalizePending(arr);\n    }catch(e){\n      return [];\n    }\n  }\n  function savePending(arr){\n    try{ localStorage.setItem(PENDING_KEY, JSON.stringify(arr || [])); }catch(e){}\n  }\n  function removePendingAt(idx){\n    const arr = loadPending();\n    arr.splice(idx, 1);\n    savePending(arr);\n  }\n\n  function normalizePending(arr){\n    const now = Date.now();\n    let changed = false;\n\n    const out = (Array.isArray(arr) ? arr : []).map((p, i) => {\n      if (!p || typeof p !== \"object\") return p;\n      if (typeof p.ts === \"number\") return p;\n\n      changed = true;\n      \/\/ je\u015bli nie ma ts \u2013 ustaw (z zachowaniem kolejno\u015bci)\n      return { ...p, ts: now - (i * 1000) };\n    });\n\n    if (changed) savePending(out);\n    return out;\n  }\n\n  \/\/ ====== KATEGORIE ROOT-ONLY (index z backendu) ======\n  let TERM_BY_ID = new Map();\n  let CHILDREN_BY_PARENT = new Map();\n  let TERM_IDS_BY_NAME = new Map();\n\n  let categoriesReady = false;\n  let categoriesPromise = null;\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 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 : null;\n  }\n\n\/\/ ====== GROUPING: to samo co w podgl\u0105dzie (1 root + podkategorie) ======\n  function normalizeName(s){\n    return String(s || \"\").trim().replace(\/\\s+\/g, \" \");\n  }\n\n  function getTermIdsForName(name){\n    const key = normalizeName(name).toLowerCase();\n    return TERM_IDS_BY_NAME.get(key) || [];\n  }\n\n  \/\/ grupy {rootId, rootName, subs:[...], hits}\n  function getTenderCategoryGroups(tender){\n    if (!TERM_BY_ID || TERM_BY_ID.size === 0) return [];\n\n    const names = (extractTenderCategoryNames(tender) || [])\n      .map(normalizeName)\n      .filter(Boolean);\n\n    if (!names.length) return [];\n\n    const groups = new Map();\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\n  function dedupe(arr){\n    const seen = new Set();\n    const out = [];\n    for (const x of (arr || [])){\n      const s = String(x || \"\").trim();\n      if (!s) continue;\n      const k = s.toLowerCase();\n      if (seen.has(k)) continue;\n      seen.add(k);\n      out.push(s);\n    }\n    return out;\n  }\n\n  function extractTenderCategoryNames(tender){\n    const c =\n      tender?.categories ??\n      tender?.category ??\n      tender?.terms ??\n      tender?.tags ??\n      tender?.taxonomy ??\n      tender?.cats ??\n      tender?.category_name ??\n      tender?.category_slug ??\n      \"\";\n\n    if (Array.isArray(c)){\n      return dedupe(c.map(x => typeof x === \"string\" ? x : (x?.name ?? x?.label ?? x?.slug ?? \"\")).filter(Boolean));\n    }\n    if (typeof c === \"string\"){\n      return dedupe(c.split(\",\").map(s => s.trim()).filter(Boolean));\n    }\n    if (c && typeof c === \"object\"){\n      const name = c?.name ?? c?.label ?? c?.slug ?? c?.title;\n      return name ? [String(name).trim()] : [];\n    }\n    return [];\n  }\n\n  function getRootCategoryNamesForNames(names){\n    const input = dedupe(names || []);\n    if (!input.length) return [];\n\n    const out = [];\n    const seen = new Set();\n\n    for (const catName of input){\n      const ids = TERM_IDS_BY_NAME.get(catName.toLowerCase()) || [];\n      for (const id of ids){\n        const root = getRootTermById(id);\n        if (root?.name && !seen.has(root.name)){\n          seen.add(root.name);\n          out.push(root.name);\n        }\n      }\n    }\n\n    out.sort((a,b)=>String(a).localeCompare(String(b), \"pl\"));\n    return out;\n  }\n\n  async function ensureCategoriesIndex(){\n    if (categoriesReady) return true;\n    if (categoriesPromise) return categoriesPromise;\n\n    categoriesPromise = (async () => {\n      try{\n        const fd = new FormData();\n        fd.append(\"action\", \"get_tenders\");\n        fd.append(\"paged\", \"1\");\n        fd.append(\"posts_per_page\", \"1\");\n        fd.append(\"search\", \"\");\n        fd.append(\"category\", \"\");\n        fd.append(\"source\", \"\");\n\n        const res = await fetch(AJAX_URL, {\n          method: \"POST\",\n          body: fd,\n          credentials: \"same-origin\",\n          cache: \"no-store\"\n        });\n\n        const raw = await res.text();\n        let json = raw;\n        try{ json = JSON.parse(raw); }catch(e){}\n\n        const cats =\n          json?.data?.categories ||\n          json?.data?.data?.categories ||\n          json?.categories ||\n          [];\n\n        if (Array.isArray(cats) && cats.length){\n          buildCategoryIndex(cats);\n          categoriesReady = true;\n          return true;\n        }\n      }catch(e){\n        console.warn(\"Nie uda\u0142o si\u0119 pobra\u0107 kategorii dla analiz:\", e);\n      }\n      categoriesReady = false;\n      return false;\n    })();\n\n    return categoriesPromise;\n  }\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  function categoryStyleAuto(name){\n    const h = Math.abs(hashString(String(name || \"\")));\n    const hue = h % 360;\n    return { bg: `hsl(${hue}, 85%, 92%)`, color: `hsl(${hue}, 45%, 32%)` };\n  }\n\n function renderCategoryTagsAuto(tender, max=3){\n  \/\/ max = \u0142\u0105czna liczba tag\u00f3w (root + subs)\n  const MAX = Math.max(1, max);\n\n  \/\/ je\u015bli nie mamy indeksu kategorii -> fallback do surowych nazw\n  if (!categoriesReady){\n    const raw = dedupe(extractTenderCategoryNames(tender)).slice(0, MAX);\n    if (!raw.length) return `<div class=\"tender-tags\"><span class=\"tender-tag tender-tag--gray\">\u2014<\/span><\/div>`;\n    return `\n      <div class=\"tender-tags\">\n        ${raw.map(name => {\n          const st = categoryStyleAuto(name);\n          return `<span class=\"tender-tag\" style=\"background:${st.bg};color:${st.color};\">${esc(name)}<\/span>`;\n        }).join(\"\")}\n      <\/div>\n    `;\n  }\n\n  \/\/ \u2705 to samo co w pogl\u0105dzie: 1 root + podkategorie\n  const groups = getTenderCategoryGroups(tender);\n  const top = groups[0];\n  const names = [];\n\n  if (top?.rootName) names.push(top.rootName);\n  (top?.subs || []).forEach(s => names.push(s));\n\n  const final = dedupe(names).slice(0, MAX);\n\n  if (!final.length) return `<div class=\"tender-tags\"><span class=\"tender-tag tender-tag--gray\">\u2014<\/span><\/div>`;\n\n  return `\n    <div class=\"tender-tags\">\n      ${final.map(name => {\n        const st = categoryStyleAuto(name);\n        return `<span class=\"tender-tag\" style=\"background:${st.bg};color:${st.color};\">${esc(name)}<\/span>`;\n      }).join(\"\")}\n    <\/div>\n  `;\n}\n\n  \/\/ ====== ANALIZA -> TABELA ======\n  function flattenObject(obj, prefix = \"\", out = {}, depth = 0, maxDepth = 3){\n    if (!obj || typeof obj !== \"object\") return out;\n    if (depth > maxDepth) return out;\n\n    for (const [k, v] of Object.entries(obj)){\n      const key = prefix ? `${prefix}.${k}` : k;\n\n      if (v == null){\n        out[key] = \"\u2014\";\n        continue;\n      }\n      if (Array.isArray(v)){\n        if (v.length === 0) out[key] = \"\u2014\";\n        else if (v.every(x => typeof x === \"string\" || typeof x === \"number\")){\n          out[key] = v.join(\", \");\n        } else {\n          out[key] = `(${v.length}) element\u00f3w`;\n        }\n        continue;\n      }\n      if (typeof v === \"object\"){\n        flattenObject(v, key, out, depth + 1, maxDepth);\n        continue;\n      }\n      out[key] = String(v);\n    }\n    return out;\n  }\n\nfunction escapeHtml(s){\n  return String(s ?? \"\")\n    .replaceAll(\"&\",\"&amp;\")\n    .replaceAll(\"<\",\"&lt;\")\n    .replaceAll(\">\",\"&gt;\")\n    .replaceAll('\"',\"&quot;\")\n    .replaceAll(\"'\",\"&#039;\");\n}\n\n\/\/ robi \u0142adne akapity\/linijki\nfunction formatCellHtml(key, value){\n  let raw = String(value ?? \"\").trim();\n\n\/\/ usuwa chi\u0144skie nawiasy u\u017cywane przez LLM\nraw = raw.replace(\/[\u3010\u3011]\/g, \"\");\n  if (!raw || raw === \"\u2014\") return escapeHtml(raw || \"\u2014\");\n\n  const k = String(key || \"\").toLowerCase();\n\n  \/\/ tylko dla \"tekstowych\" p\u00f3l (\u017ceby nie rozwali\u0107 kr\u00f3tkich warto\u015bci)\n  const isLongField =\n    k.includes(\"dlaczego\") ||\n    k.includes(\"ryzyk\") ||\n    k.includes(\"opis\") ||\n    k.includes(\"uzasad\") ||\n    raw.length > 160;\n\n  \/\/ je\u015bli mamy entery w tek\u015bcie -> u\u017cyj ich\n  if (raw.includes(\"\\n\")){\n    const paragraphs = raw\n      .split(\/\\n{2,}\/)                \/\/ akapit = pusta linia\n      .map(p => p.trim())\n      .filter(Boolean)\n      .map(p => {\n        \/\/ pojedyncze nowe linie w akapicie jako <br>\n        const withBr = escapeHtml(p).replace(\/\\n\/g, \"<br>\");\n        return `<p>${withBr}<\/p>`;\n      })\n      .join(\"\");\n    return `<div class=\"analysis-cell\">${paragraphs}<\/div>`;\n  }\n\n  \/\/ je\u015bli brak enter\u00f3w, a to d\u0142ugie pole -> tnij po zdaniach\n  if (isLongField){\n    const parts = raw\n  .replace(\/([.!?])\\s+\/g, \"$1\\n\")   \/\/ wstaw newline po . ! ?\n  .split(\"\\n\")\n  .map(s => s.trim())\n  .filter(Boolean);\n\n    \/\/ je\u017celi wysz\u0142o ma\u0142o cz\u0119\u015bci, to chocia\u017c \u0142am po \";\"\n    const finalParts = (parts.length >= 3)\n      ? parts\n      : raw.split(\/\\s*;\\s*\/).map(s => s.trim()).filter(Boolean);\n\n    const paragraphs = finalParts\n      .map(s => `<p>${escapeHtml(s)}<\/p>`)\n      .join(\"\");\n\n    return `<div class=\"analysis-cell\">${paragraphs}<\/div>`;\n  }\n\n  \/\/ kr\u00f3tkie pola normalnie\n  return escapeHtml(raw);\n}\n\n  function renderAnalysisTableFromPairs(pairs){\n  const rows = Object.entries(pairs).map(([k, v]) => `\n    <tr>\n      <th>${escapeHtml(k)}<\/th>\n      <td>${formatCellHtml(k, v)}<\/td>\n    <\/tr>\n  `).join(\"\");\n\n  return `\n    <table class=\"analysis-table\" role=\"table\">\n      <tbody>\n        ${rows || `<tr><th>info<\/th><td>Brak danych<\/td><\/tr>`}\n      <\/tbody>\n    <\/table>\n  `;\n}\n\n  function renderPendingTable(){\n    return `\n      <table class=\"analysis-table\" role=\"table\">\n        <tbody>\n          <tr>\n            <th>Informacja<\/th>\n            <td>Analiza jest w trakcie przetwarzania. Pojawi si\u0119 tutaj po zako\u0144czeniu.<\/td>\n          <\/tr>\n        <\/tbody>\n      <\/table>\n    `;\n  }\n\n  function renderFinalAnalysisTable(analysis, meets){\n  const flat = flattenObject(\n    (analysis && typeof analysis === \"object\") ? analysis : { raw: String(analysis ?? \"\") },\n    \"\", {}, 0, 3\n  );\n\n  \/\/ opcjonalnie: je\u015bli backend zwraca klucz \"Wynik\", te\u017c go usu\u0144\n  const keys = Object.keys(flat);\n  for (const k of keys){\n    const norm = String(k)\n      .trim()\n      .toLowerCase()\n      .replace(\/[:?]+$\/g, \"\");\n\n    if (norm === \"wynik\"){\n      delete flat[k];\n      break;\n    }\n  }\n\n  return renderAnalysisTableFromPairs(flat);\n}\n\n  \/\/ ====== DELETE ======\n  async function deleteAnalysisById(id){\n    const fd = new FormData();\n    fd.append(\"action\", DELETE_ACTION);\n    fd.append(\"analysis_id\", String(id));\n\n    const res = await fetch(AJAX_URL, {\n      method: \"POST\",\n      body: fd,\n      credentials: \"same-origin\",\n      cache: \"no-store\"\n    });\n\n    const raw = await res.text();\n    let payload = raw;\n    try{ payload = JSON.parse(raw); }catch(e){}\n\n    if (payload && typeof payload === \"object\" && \"success\" in payload){\n      if (!payload.success){\n        const msg = typeof payload.data === \"string\" ? payload.data : (payload.data?.message || \"Nie uda\u0142o si\u0119 usun\u0105\u0107 analizy.\");\n        throw new Error(msg);\n      }\n      return true;\n    }\n\n    const s = String(raw).trim().toLowerCase();\n    if (s === \"1\" || s.includes(\"ok\") || s.includes(\"deleted\")) return true;\n\n    throw new Error(\"Nieznana odpowied\u017a serwera przy usuwaniu.\");\n  }\n\n  \/\/ ====== SAFE HELPERS (wa\u017cne \u017ceby nie wywala\u0142o renderu) ======\n  function safeHost(link){\n    try { return new URL(String(link), location.origin).host; }\n    catch { return \"\u2014\"; }\n  }\n  function cssEscapeSafe(val){\n    return (window.CSS && CSS.escape) ? CSS.escape(String(val)) : String(val).replace(\/\"\/g,'\\\\\"');\n  }\n\n  \/\/ ====== DEDUPE \/ CLEANUP PENDING (NAPRAWA DUBLI) ======\nfunction normalizeUrlKey(url){\n  try{\n    const u = new URL(String(url || \"\"), location.origin);\n    const host = (u.host || \"\").toLowerCase().replace(\/^www\\.\/, \"\");\n    const cleanPath = (u.pathname || \"\").replace(\/\\\/+$\/, \"\");\n    return (host + cleanPath).toLowerCase();\n  }catch(e){\n    return \"\";\n  }\n}\n\nfunction normalizeTextKey(s){\n  return String(s ?? \"\")\n    .trim()\n    .toLowerCase()\n    .replace(\/\\s+\/g, \" \");\n}\n\nfunction getTenderPostIdLike(x){\n  const v =\n    x?.tender?.post_id ??\n    x?.tender?.id ??\n    x?.tender_id ??\n    x?.post_id ??\n    x?.id ??\n    \"\";\n  const s = String(v).trim();\n  return s ? s : \"\";\n}\n\nfunction getTenderLinkLike(x){\n  const link =\n    x?.tender?.link ??\n    x?.tender?.url ??\n    x?.tender?.permalink ??\n    x?.link ??\n    x?.url ??\n    \"\";\n  return String(link || \"\").trim();\n}\n\nfunction getTenderTitleLike(x){\n  const t =\n    x?.tender?.title ??\n    x?.tender?.post_title ??\n    x?.tender?.name ??\n    x?.title ??\n    \"\";\n  return String(t || \"\").trim();\n}\n\nfunction getTenderSourceLike(x){\n  const tender = x?.tender || {};\n  const link = getTenderLinkLike(x);\n  const src =\n    tender?.source_name ??\n    tender?.source ??\n    tender?.origin ??\n    x?.source ??\n    (link ? safeHost(link) : \"\");\n  return String(src || \"\").trim();\n}\n\nfunction hostFromAny(x){\n  const link = getTenderLinkLike(x);\n  if (link) return safeHost(link).toLowerCase().replace(\/^www\\.\/, \"\");\n  const src = getTenderSourceLike(x);\n  \/\/ je\u015bli source wygl\u0105da jak domena\n  if (src && src.includes(\".\")) return src.toLowerCase().replace(\/^www\\.\/, \"\");\n  return \"\";\n}\n\n\/\/ Klucze dopasowania (od najsilniejszego do najs\u0142abszego)\nfunction keysFor(obj){\n  const keys = [];\n\n  const pid = getTenderPostIdLike(obj);\n  if (pid) keys.push(\"pid:\" + pid);\n\n  const urlKey = normalizeUrlKey(getTenderLinkLike(obj));\n  if (urlKey) keys.push(\"url:\" + urlKey);\n\n  const title = normalizeTextKey(getTenderTitleLike(obj));\n  if (title) keys.push(\"title:\" + title);\n\n  \/\/ super praktyczne: tytu\u0142 + host (usuwa duble nawet jak linki si\u0119 r\u00f3\u017cni\u0105)\n  const host = normalizeTextKey(hostFromAny(obj));\n  if (title && host) keys.push(`th:${title}|${host}`);\n\n  \/\/ fallback: title + source string (gdy host nie do wyci\u0105gni\u0119cia)\n  const src = normalizeTextKey(getTenderSourceLike(obj));\n  if (title && src) keys.push(`ts:${title}|${src}`);\n\n  return keys;\n}\n\n\/**\n * Usuwa z localStorage pending, kt\u00f3re \u201cju\u017c s\u0105\u201d w DB.\n * Zwraca przefiltrowan\u0105 tablic\u0119 pending (do renderu).\n *\/\nfunction reconcilePendingWithDb(pendingArr, dbArr){\n  const pending = Array.isArray(pendingArr) ? pendingArr : [];\n  const db = Array.isArray(dbArr) ? dbArr : [];\n\n  const dbKeys = new Set();\n  for (const item of db){\n    for (const k of keysFor(item)) dbKeys.add(k);\n  }\n\n  \/\/ filtrowanie pending\n  const filtered = pending.filter(p => {\n    const pKeys = keysFor(p);\n    if (!pKeys.length) return true; \/\/ nie umiemy dopasowa\u0107 \u2192 zostaw\n    \/\/ je\u015bli JAKIKOLWIEK klucz pending istnieje w DB \u2192 usu\u0144 z pending\n    return !pKeys.some(k => dbKeys.has(k));\n  });\n\n  \/\/ je\u015bli co\u015b uby\u0142o \u2192 zapis do localStorage (to jest ten \u201ctwardy\u201d cleanup)\n  if (filtered.length !== pending.length){\n    savePending(filtered);\n  }\n\n  return filtered;\n}\n\n\nfunction formatText(text){\n  if(!text) return \"\";\n\n  return text\n    .split(\/\\.\\s+\/)\n    .map(sentence => `<p>${sentence.trim()}.<\/p>`)\n    .join(\"\");\n}\n\n  \/\/ ====== UI RENDER ======\n  function renderAnalysisCard(item, opts){\n    const isPending = !!opts?.isPending;\n    const pendingIndex = (typeof opts?.pendingIndex === \"number\") ? opts.pendingIndex : null;\n\n    const id = item?.id ?? item?.analysis_id ?? \"\u2014\";\n    const tender = item?.tender || {};\n    const analysis = item?.analysis ?? {};\n    const meets = resolveMeets(item);\n\n    const title = tender.title || tender.post_title || tender.name || \"\u2014\";\n    const link  = tender.link || tender.url || tender.permalink || \"\";\n    const src   = tender.source_name || tender.source || tender.origin || (link ? safeHost(link) : \"\u2014\");\n\n    const deadline =\n      tender.deadline ??\n      tender.deadline_human ??\n      tender.termin ??\n      tender.date_deadline ??\n      tender.deadline_date ??\n      item?.created_at ??\n      item?.date ??\n      item?.created ??\n      \"\u2014\";\n\n    const dateText = (deadline != null && String(deadline).trim() !== \"\") ? String(deadline) : \"\u2014\";\n    const safeIdForPanel = String(id).replace(\/\\W+\/g,\"\");\n    const panelId = \"analysis-panel-\" + safeIdForPanel;\n\n    const catsHtml = renderCategoryTagsAuto(tender, 3);\n\n    \/\/ ====== KLASA KOLORU CHMURKI ======\nlet resultClass = \"\";\nif (!isPending){\n  if (meets === true) resultClass = \" is-meets\";\n  else if (meets === false) resultClass = \" is-not-meets\";\n  else resultClass = \" is-unknown\";\n}\n\n    const deleteAttrs = isPending\n      ? `data-pending-index=\"${pendingIndex}\"`\n      : `data-delete=\"${esc(id)}\"`;\n\n    const tableHtml = isPending\n      ? renderPendingTable()\n      : renderFinalAnalysisTable(analysis, meets);\n\n\/\/ poka\u017c tylko dla zako\u0144czonych i \"nie spe\u0142nia\"\nconst showGenerateMail = (!isPending && meets === false);\n\nconst mailBtnHtml = showGenerateMail ? `\n  <button\n    class=\"df-generate-mail-btn\"\n    type=\"button\"\n    data-generate-mail=\"${esc(String(id))}\"\n    data-title=\"${esc(title)}\">\n    Wygeneruj pytanie o elastyczno\u015b\u0107\n  <\/button>\n` : \"\";\n\n    const cardHtml = `\n      <div class=\"tender-card${resultClass}\"\n           data-analysis-id=\"${esc(String(id))}\"\n           data-is-pending=\"${isPending ? \"1\" : \"0\"}\"\n           ${isPending ? `data-pending-index=\"${pendingIndex}\"` : \"\"}>\n\n        <div class=\"tender-card__name\">\n          <p class=\"tender-title\">${esc(title)}<\/p>\n        <\/div>\n\n        <div class=\"tender-card__cats\">\n          ${catsHtml}\n        <\/div>\n\n        <div class=\"tender-card__date\">\n          <span 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(dateText)}<\/span>\n          <\/span>\n        <\/div>\n\n        <div class=\"tender-card__source\">\n          <span class=\"tender-source\">\n            ${link\n              ? `<a href=\"${esc(link)}\" target=\"_blank\" rel=\"noopener\">${esc(src)}<\/a>`\n              : `<span style=\"opacity:.6;\">${esc(src || \"\u2014\")}<\/span>`\n            }\n          <\/span>\n        <\/div>\n\n        <div class=\"tender-card__actions\">\n          <div class=\"tender-actions\">\n            <a class=\"tender-action-link\" href=\"#\" data-toggle=\"${esc(panelId)}\">Zobacz analiz\u0119<\/a>\n            <a class=\"tender-action-link tender-action-delete\" href=\"#\" ${deleteAttrs}>Usu\u0144<\/a>\n${mailBtnHtml}\n          <\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"analysis-panel\"\n     id=\"${esc(panelId)}\"\n     data-analysis-id=\"${esc(String(id))}\"\n     data-docs-loaded=\"0\"\n     data-post-id=\"${esc(String(tender.post_id ?? tender.id ?? tender.ID ?? item?.post_id ?? \"\"))}\"\n     data-docs-title=\"${esc(title)}\"\n     data-docs-deadline=\"${esc(dateText)}\">\n\n  <div class=\"analysis-head\">\n    <div class=\"analysis-badges\"><\/div>\n  <\/div>\n\n  ${tableHtml}\n\n  <!-- DOKUMENTY -->\n  <div class=\"analysis-docs\">\n    <div class=\"analysis-docs__head\">\n      <span class=\"analysis-docs__label\">Dokumenty:<\/span>\n      <span class=\"analysis-docs__meta\"><\/span>\n    <\/div>\n    <div class=\"analysis-docs__list\">\n      <div class=\"analysis-docs__state\">Otw\u00f3rz panel, \u017ceby wczyta\u0107 dokumenty.<\/div>\n    <\/div>\n  <\/div>\n<\/div>\n    `;\n\n    const wrap = document.createElement(\"div\");\n    wrap.innerHTML = cardHtml;\n    return wrap;\n  }\n\n  function removeFromDomBySelector(cardEl){\n    if (!cardEl) return;\n    const id = cardEl.getAttribute(\"data-analysis-id\");\n    const safe = cssEscapeSafe(id);\n    const panel = listEl.querySelector(`.analysis-panel[data-analysis-id=\"${safe}\"]`);\n    if (panel) panel.remove();\n    cardEl.remove();\n  }\n\n  function updateMetaCount(){\n    const count = listEl.querySelectorAll(\".tender-card\").length;\n    setMeta(`${count} analiz`);\n    if (count === 0){\n      setDesc(\"Nie masz jeszcze zapisanych analiz.\");\n    }\n  }\n\n  function parseAnalysisDate(item){\n    const cand = [\n      item?.created_at,\n      item?.created,\n      item?.date,\n      item?.analysis?.created_at,\n      item?.analysis?.created,\n    ].filter(Boolean);\n\n    for (const v of cand){\n      const s = String(v).trim();\n      if (!s) continue;\n\n      const iso = s.replace(\" \", \"T\");\n      const dt = new Date(iso);\n      if (!isNaN(dt)) return dt;\n\n      const 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 dt2 = new Date(Date.UTC(y, mo - 1, d));\n        if (!isNaN(dt2)) return dt2;\n      }\n    }\n    return null;\n  }\n\n  function sortAnalysesNewestFirst(a,b){\n    const da = parseAnalysisDate(a);\n    const db = parseAnalysisDate(b);\n    const ta = da ? da.getTime() : 0;\n    const tb = db ? db.getTime() : 0;\n    return tb - ta;\n  }\n\n  async function fetchAnalyses(silent=false){\n    if (!silent){\n      setAlert(\"\");\n      setMeta(\"\u0141adowanie\u2026\");\n      setDesc(\"Pobieranie Twoich analiz\u2026\");\n    }\n\n    try{\n      const fd = new FormData();\n      fd.append(\"action\", ACTION);\n\n      const res = await fetch(AJAX_URL, {\n        method: \"POST\",\n        body: fd,\n        credentials: \"same-origin\",\n        cache: \"no-store\"\n      });\n\n      const raw = await res.text();\n      const rawTrim = String(raw).trim();\n\n      if (rawTrim === \"0\"){\n        setMeta(\"0 analiz\");\n        setDesc(\"Zaloguj si\u0119, aby zobaczy\u0107 swoje analizy.\");\n        setAlert(\"\");\n        stopPolling();\n        return;\n      }\n\n      let payload = raw;\n      try{ payload = JSON.parse(raw); } catch(e){}\n\n      const dbArr = unwrapToArray(payload);\n      listEl.innerHTML = \"\";\n\n      await ensureCategoriesIndex();\n\n      dbArr.sort(sortAnalysesNewestFirst);\n\n      \/\/ \u2705 tu dzieje si\u0119 magia: pending, kt\u00f3re ju\u017c jest w DB, jest usuwane\n     let pending = loadPending();\npending = reconcilePendingWithDb(pending, dbArr);\n\n      if (dbArr.length || pending.length){\n        if (pending.length) startPolling();\n        else stopPolling();\n\n        setMeta((dbArr.length + pending.length) + \" analiz\");\n        setDesc(\"\");\n        setAlert(\"\");\n\n        \/\/ 1) pending (nowo dodane)\n        pending.forEach((p, idx) => {\n          const fake = {\n            id: \"pending-\" + idx,\n            tender: p?.tender || p || {},\n            analysis: { status:\"pending\" },\n            meets_conditions: null,\n            created_at: \"\u2014\"\n          };\n          listEl.appendChild(renderAnalysisCard(fake, { isPending:true, pendingIndex: idx }));\n        });\n\n        \/\/ 2) gotowe z bazy\n        dbArr.forEach(item => {\n          listEl.appendChild(renderAnalysisCard(item, { isPending:false }));\n        });\n\n        return;\n      }\n\n      setMeta(\"0 analiz\");\n      setDesc(\"Nie masz jeszcze zapisanych analiz.\");\n      setAlert(\"\");\n      stopPolling();\n\n    } catch(err){\n      console.error(err);\n\n      listEl.innerHTML = \"\";\n      const pending = loadPending();\n\n      if (pending.length){\n        setMeta(`${pending.length} analiz`);\n        setDesc(\"Analizy s\u0105 w trakcie przetwarzania.\");\n        setAlert(\"\");\n\n        pending.forEach((p, idx) => {\n          const fake = {\n            id: \"pending-\" + idx,\n            tender: p?.tender || p || {},\n            analysis: { status:\"pending\" },\n            meets_conditions: null,\n            created_at: \"\u2014\"\n          };\n          listEl.appendChild(renderAnalysisCard(fake, { isPending:true, pendingIndex: idx }));\n        });\n\n        startPolling();\n        return;\n      }\n\n      setMeta(\"0 analiz\");\n      setDesc(\"Nie masz jeszcze zapisanych analiz.\");\n      setAlert(\"\");\n      stopPolling();\n    }\n  }\n\n\/\/ ====== GENERUJ MAIL ======\ndocument.addEventListener(\"click\", async (e) => {\n  const btn = e.target.closest(\"button[data-generate-mail]\");\n  if (!btn) return;\n\n  e.preventDefault();\n\n  const analysisId = btn.getAttribute(\"data-generate-mail\");\n  const title = btn.getAttribute(\"data-title\") || \"\";\n\n  btn.disabled = true;\n  openMailModal();\n  setMailLoading(true, \"Generuj\u0119 wiadomo\u015b\u0107\u2026\");\n\n  try{\n    await postAjax(GEN_MAIL_ACTION, analysisId);\n\n    setMailLoading(true, \"Pobieram tre\u015b\u0107 wiadomo\u015bci\u2026\");\n    const mailData = await postAjax(GET_MAIL_ACTION, analysisId);\n\n    \/\/ \u2705 je\u015bli jeszcze pending -> polling zamiast wype\u0142nia\u0107 puste pola\n    if (!mailLooksReady(mailData)){\n      setMailHint(\"Generowanie w toku\u2026 sprawdzam automatycznie.\", \"info\");\n      await pollGetMail(analysisId, title); \/\/ domy\u015blnie 8 min\n      return;\n    }\n\n    fillMailModal(mailData, title);\n    setMailLoading(false, \"\");\n    setMailHint(\"Gotowe. Mo\u017cesz skopiowa\u0107 tre\u015b\u0107.\", \"success\");\n\n  }catch(err){\n    console.error(err);\n\n    const msg = String(err?.message || err || \"\");\n    const low = msg.toLowerCase();\n    const isTimeout = low.includes(\"timed out\") || low.includes(\"curl error 28\");\n\n    if (isTimeout){\n      setMailHint(\"Generowanie trwa d\u0142u\u017cej ni\u017c zwykle\u2026 sprawdzam automatycznie.\", \"info\");\n      await pollGetMail(analysisId, title); \/\/ domy\u015blnie 8 min\n      return;\n    }\n\n    \/\/ \u2705 inne b\u0142\u0119dy\n    setMailLoading(false, \"\");\n    setMailHint(\"Nie uda\u0142o si\u0119 wygenerowa\u0107 wiadomo\u015bci. Spr\u00f3buj ponownie.\", \"error\");\n\n  }finally{\n    btn.disabled = false;\n  }\n}, { passive:false });\n\n  \/\/ ====== CLICK HANDLERS: toggle + delete ======\n  document.addEventListener(\"click\", async (e) => {\n    const a = e.target.closest(\"a\");\n    if (!a) return;\n\n    const toggleId = a.getAttribute(\"data-toggle\");\n    if (toggleId){\n  e.preventDefault();\n  const panel = document.getElementById(toggleId);\n  if (!panel) return;\n\n  const willOpen = !panel.classList.contains(\"is-open\");\n  panel.classList.toggle(\"is-open\");\n\n  \/\/ \u2705 doci\u0105gnij dokumenty dopiero przy otwarciu\n  if (willOpen) {\n    loadDocsIntoPanel(panel);\n  }\n  return;\n}\n\n    const pendingIndexAttr = a.getAttribute(\"data-pending-index\");\n    if (pendingIndexAttr !== null){\n      e.preventDefault();\n      const idx = parseInt(pendingIndexAttr, 10);\n      if (Number.isNaN(idx)) return;\n\n      if (!confirm(\"Usun\u0105\u0107 ten wpis?\")) return;\n\n      removePendingAt(idx);\n      const card = a.closest(\".tender-card\");\n      removeFromDomBySelector(card);\n      updateMetaCount();\n\n      if (loadPending().length === 0 && listEl.querySelectorAll(\".tender-card\").length === 0){\n        setDesc(\"Nie masz jeszcze zapisanych analiz.\");\n        stopPolling();\n      }\n      return;\n    }\n\n    const deleteId = a.getAttribute(\"data-delete\");\n    if (deleteId){\n      e.preventDefault();\n      if (!confirm(\"Na pewno usun\u0105\u0107 t\u0119 analiz\u0119?\")) return;\n\n      a.style.pointerEvents = \"none\";\n      a.style.opacity = \"0.55\";\n      setAlert(\"Usuwanie\u2026\", \"info\");\n\n      try{\n        await deleteAnalysisById(deleteId);\n        const card = a.closest(\".tender-card\");\n        removeFromDomBySelector(card);\n        updateMetaCount();\n        setAlert(\"Usuni\u0119to analiz\u0119.\", \"info\");\n      } catch(err){\n        console.error(err);\n        a.style.pointerEvents = \"\";\n        a.style.opacity = \"\";\n        setAlert(\"Nie uda\u0142o si\u0119 usun\u0105\u0107 analizy: \" + (err?.message || err), \"error\");\n      }\n      return;\n    }\n  }, { passive:false });\n\ndocument.addEventListener(\"click\", (e) => {\n  if (e.target.closest(\"[data-df-mail-close]\")) closeMailModal();\n});\n\ndocument.addEventListener(\"keydown\", (e) => {\n  if (e.key === \"Escape\") closeMailModal();\n});\n\nconst copyBtn = document.getElementById(\"dfCopyMail\");\ncopyBtn && copyBtn.addEventListener(\"click\", async () => {\n  const to = (document.getElementById(\"dfMailTo\")?.value || \"\").trim();\n  const subject = (document.getElementById(\"dfMailSubject\")?.value || \"\").trim();\n  const body = (document.getElementById(\"dfMailBody\")?.value || \"\");\n\n  const full = [\n    to ? `Do: ${to}` : null,\n    subject ? `Temat: ${subject}` : null,\n    \"\",\n    body\n  ].filter(Boolean).join(\"\\n\");\n\n  try{\n    await navigator.clipboard.writeText(full);\n    setMailHint(\"Skopiowano\");\n  }catch(e){\n    setMailHint(\"Nie uda\u0142o si\u0119 skopiowa\u0107 \u2014 zaznacz i skopiuj r\u0119cznie.\");\n  }\n});\n\n  reloadBtn && reloadBtn.addEventListener(\"click\", () => fetchAnalyses(false));\n\n  fetchAnalyses(false);\n  startPolling();\n})();\n<\/script>\n<\/div>\n\n\n\n<div class=\"wp-block-group alignwide has-global-padding is-content-justification-left is-layout-constrained wp-container-core-group-is-layout-353cc7fb wp-block-group-is-layout-constrained\">\n<div class=\"pdf-actions\">\n  <button id=\"pdfSavedBtn\">Pobierz PDF<\/button>\n<\/div>\n\n<style>\n.pdf-actions{\n  box-sizing: border-box !important;\n\n  max-width:1150px !important;\n  width: calc(100% - 40px) !important;   \/* 20px + 20px *\/\n  margin:16px auto 0 !important;\n\n  display:flex !important;\n  justify-content:flex-end !important;\n}\n\n\/* BUTTON *\/\n#pdfSavedBtn{\n  border:none;\n  background:#111827;\n  color:#fff;\n  font-weight:600;\n  padding:14px 18px;\n  border-radius:999px;\n  cursor:pointer;\n<\/style>\n\n<script>\nfunction downloadSavedOffersPDF_FromPage(){\n  const list =\n    document.getElementById(\"tenders-list\") ||\n    document.querySelector(\".tenders-list\");\n\n  if (!list) {\n    alert(\"Nie znalaz\u0142am listy zapisanych ofert (#tenders-list lub .tenders-list).\");\n    return;\n  }\n\n  const clone = list.cloneNode(true);\n\n  clone.querySelectorAll(\"button, .actions, .tender-actions, .delete-btn\").forEach(el => el.remove());\n\n  const w = window.open(\"\", \"_blank\");\n  w.document.write(`\n    <html><head><meta charset=\"utf-8\" \/>\n      <title>Moje Analizy<\/title>\n      <style>\n        body{font-family:Inter, Arial, sans-serif; padding:24px; color:#111827;}\n        h1{margin:0 0 6px; font-size:22px;}\n        .muted{color:#6b7280; font-size:12px; margin-bottom:18px;}\n        \/* tu mo\u017cesz wklei\u0107 swoje CSS kafelk\u00f3w je\u015bli chcesz 1:1 *\/\n        .tender-card{page-break-inside:avoid;}\n        @media print { a{color:#111827; text-decoration:none;} }\n      <\/style>\n    <\/head><body>\n      <h1>Moje Analizy \u2013 zapisane oferty<\/h1>\n      <div class=\"muted\">Wygenerowano: ${new Date().toLocaleString()}<\/div>\n      ${clone.outerHTML}\n      <script>window.onload=()=>window.print();<\\\/script>\n    <\/body><\/html>\n  `);\n  w.document.close();\n}\n\ndocument.addEventListener(\"click\", (e)=>{\n  if (!e.target.closest(\"#pdfSavedBtn\")) return;\n  downloadSavedOffersPDF_FromPage();\n});\n<\/script>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>\u0141adowanie\u2026 Od\u015bwie\u017c Nazwa Kategoria Termin \u0179r\u00f3d\u0142o Akcje Wygenerowana wiadomo\u015b\u0107 \u2715 Tre\u015b\u0107 Kopiuj Zamknij Pobierz PDF<\/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-27920","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/oferty.k4.pl\/index.php\/wp-json\/wp\/v2\/pages\/27920","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=27920"}],"version-history":[{"count":787,"href":"https:\/\/oferty.k4.pl\/index.php\/wp-json\/wp\/v2\/pages\/27920\/revisions"}],"predecessor-version":[{"id":37465,"href":"https:\/\/oferty.k4.pl\/index.php\/wp-json\/wp\/v2\/pages\/27920\/revisions\/37465"}],"wp:attachment":[{"href":"https:\/\/oferty.k4.pl\/index.php\/wp-json\/wp\/v2\/media?parent=27920"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}