{"id":468,"date":"2026-07-23T16:36:11","date_gmt":"2026-07-23T14:36:11","guid":{"rendered":"https:\/\/blirtyapp.com\/?page_id=468"},"modified":"2026-07-24T00:39:35","modified_gmt":"2026-07-23T22:39:35","slug":"smf32593nxjs434jsmf39mx23473n82mzue82bna7b638jj9ql2","status":"publish","type":"page","link":"https:\/\/blirtyapp.com\/index.php\/smf32593nxjs434jsmf39mx23473n82mzue82bna7b638jj9ql2\/","title":{"rendered":""},"content":{"rendered":"\n<div id=\"partner-panel-root\"><\/div>\n<div id=\"recaptcha-container\"><\/div>\n\n<script src=\"https:\/\/www.gstatic.com\/firebasejs\/10.12.5\/firebase-app-compat.js\"><\/script>\n<script src=\"https:\/\/www.gstatic.com\/firebasejs\/10.12.5\/firebase-auth-compat.js\"><\/script>\n<script src=\"https:\/\/www.gstatic.com\/firebasejs\/10.12.5\/firebase-app-check-compat.js\"><\/script>\n\n<script>\n(function () {\n  const DEBUG = false;\n\n  const API_BASE = \"https:\/\/ktor-service-245368367879.europe-central2.run.app\";\n\n  const ROUTES = {\n    PARTNER_ME: \"\/api\/partner\/me\",\n    PARTNER_EVENT_PHOTO: \"\/api\/partner\/event\/photo\",\n    PARTNER_EVENT_ADD: \"\/api\/partner\/event\/add\",\n    PARTNER_EVENT_CANCEL: \"\/api\/partner\/event\/cancel\",\n    PARTNER_EVENT_UPDATE: \"\/api\/partner\/event\/update\",\n    PARTNER_VALIDATOR_LINK: \"\/api\/partner\/validator\/link\"\n  };\n\n  const firebaseConfig = {\n    apiKey: \"AIzaSyAeDy-oSX1PSFNutZehacTve8QsccRALtA\",\n    authDomain: \"blirty-61e48.firebaseapp.com\",\n    projectId: \"blirty-61e48\",\n    appId: \"1:245368367879:web:5d5dededbf737e3bc573a2\"\n  };\n\n  if (!firebase.apps.length) firebase.initializeApp(firebaseConfig);\n\n  firebase.appCheck().activate(\n    \"6LcdTd8rAAAAAII9d3Im3cV_vDN0zmw2Cmkks6P_\",\n    true\n  );\n\n  const auth = firebase.auth();\n  auth.languageCode = \"pl\";\n\n  const root = document.getElementById(\"partner-panel-root\");\n\n  const state = {\n    user: null,\n    confirmationResult: null,\n    smsInFlight: false,\n    smsSentAt: 0,\n    phoneNumber: \"\",\n    partner: null,\n    mode: \"login\",\n    editingEvent: null,\n    selectedPhotoFile: null,\n    generatedValidatorUrl: \"\",\n    loading: false,\n    savingEvent: false,\n    debugLines: []\n  };\n\n  const savedOrganizerIdKey = \"blirty_partner_organizer_id\";\nconst smsCooldownMs = 60000;\n\n  function debug(label, data) {\n    if (!DEBUG) return;\n\n    const line = `[${new Date().toLocaleTimeString()}] ${label}: ${\n      typeof data === \"string\" ? data : JSON.stringify(data, null, 2)\n    }`;\n\n    console.log(\"[PartnerPanel]\", label, data);\n\n    state.debugLines.unshift(line);\n    state.debugLines = state.debugLines.slice(0, 20);\n\n    const el = document.getElementById(\"partner-debug\");\n    if (el) {\n      el.textContent = state.debugLines.join(\"\\n\\n\");\n    }\n  }\n\n  function debugHtml() {\n    if (!DEBUG) return \"\";\n\n    return `\n      <details class=\"debug-box\" open>\n        <summary>Debug<\/summary>\n        <pre id=\"partner-debug\">${escapeHtml(state.debugLines.join(\"\\n\\n\"))}<\/pre>\n      <\/details>\n    `;\n  }\n\n  function escapeHtml(value) {\n    return String(value ?? \"\")\n      .replaceAll(\"&\", \"&amp;\")\n      .replaceAll(\"<\", \"&lt;\")\n      .replaceAll(\">\", \"&gt;\")\n      .replaceAll('\"', \"&quot;\")\n      .replaceAll(\"'\", \"&#039;\");\n  }\n\n  function minorToPln(value) {\n    const amount = Number(value || 0) \/ 100;\n    return amount.toFixed(2).replace(\".\", \",\");\n  }\n\n  function plnToMinor(value) {\n  const normalized = String(value || \"0\")\n    .toLowerCase()\n    .replace(\/z\u0142|zl\/g, \"\")\n    .replace(\/\\s\/g, \"\")\n    .replace(\",\", \".\")\n    .trim();\n\n  const parsed = Number(normalized);\n  if (!Number.isFinite(parsed)) return 0;\n\n  return Math.round(parsed * 100);\n}\n\n  function formatStatus(status) {\n    if (status === \"ACTIVE\") return \"Aktywny\";\n    if (status === \"CANCELLED\") return \"Odwo\u0142any\";\n    if (status === \"SOLD_OUT\") return \"Wyprzedany\";\n    if (status === \"EXPIRED\") return \"Zako\u0144czony\";\n    return status || \"-\";\n  }\n\n  function timestampToLocalInput(ms) {\n    if (!ms) return \"\";\n    const d = new Date(Number(ms));\n    d.setMinutes(d.getMinutes() - d.getTimezoneOffset());\n    return d.toISOString().slice(0, 16);\n  }\n\n  function localInputToTimestamp(value) {\n    if (!value) return 0;\n    return new Date(value).getTime();\n  }\n\n  function unwrapApiResponse(json) {\n    if (json && typeof json === \"object\" && \"data\" in json) return json.data;\n    return json;\n  }\n\n  async function apiJson(path, body) {\n    const user = auth.currentUser;\n    if (!user) throw new Error(\"Brak sesji Firebase.\");\n\n    const token = await user.getIdToken(false);\n\n    debug(\"REQUEST JSON \" + path, body || {});\n\n    const res = await fetch(API_BASE + path, {\n      method: \"POST\",\n      headers: {\n        \"Authorization\": \"Bearer \" + token,\n        \"Content-Type\": \"application\/json\"\n      },\n      body: JSON.stringify(body || {})\n    });\n\n    const text = await res.text();\n\n    let json = null;\n    try {\n      json = text ? JSON.parse(text) : null;\n    } catch (_) {\n      json = text;\n    }\n\n    debug(\"RESPONSE JSON \" + path, {\n      status: res.status,\n      ok: res.ok,\n      body: json\n    });\n\n    if (!res.ok) {\n      throw new Error(\n        \"HTTP \" + res.status + \": \" +\n        (typeof json === \"string\" ? json : JSON.stringify(json))\n      );\n    }\n\n    return unwrapApiResponse(json);\n  }\n\n  async function apiMultipart(path, formData) {\n    const user = auth.currentUser;\n    if (!user) throw new Error(\"Brak sesji Firebase.\");\n\n    const token = await user.getIdToken(false);\n\n    const debugForm = {};\n    for (const [key, value] of formData.entries()) {\n      if (value instanceof File) {\n        debugForm[key] = {\n          name: value.name,\n          size: value.size,\n          type: value.type\n        };\n      } else {\n        debugForm[key] = value;\n      }\n    }\n\n    debug(\"REQUEST MULTIPART \" + path, debugForm);\n\n    const res = await fetch(API_BASE + path, {\n      method: \"POST\",\n      headers: {\n        \"Authorization\": \"Bearer \" + token\n      },\n      body: formData\n    });\n\n    const text = await res.text();\n\n    let json = null;\n    try {\n      json = text ? JSON.parse(text) : null;\n    } catch (_) {\n      json = text;\n    }\n\n    debug(\"RESPONSE MULTIPART \" + path, {\n      status: res.status,\n      ok: res.ok,\n      body: json\n    });\n\n    if (!res.ok) {\n      throw new Error(\n        \"HTTP \" + res.status + \": \" +\n        (typeof json === \"string\" ? json : JSON.stringify(json))\n      );\n    }\n\n    return unwrapApiResponse(json);\n  }\n\n  function setMessage(text, type) {\n    const el = document.getElementById(\"partner-message\");\n    if (!el) return;\n\n    el.textContent = text || \"\";\n    el.className = \"partner-message \" + (type || \"\");\n  }\n\n  function render() {\n    if (state.mode === \"dashboard\") renderDashboard();\n    else if (state.mode === \"form\") renderEventForm();\n    else renderLogin();\n  }\n\n  function renderLogin() {\n  const savedOrganizerId = localStorage.getItem(savedOrganizerIdKey) || \"\";\n\n  root.innerHTML = `\n    <style>${styles()}<\/style>\n\n    <div class=\"partner-card\">\n      ${auth.currentUser ? `\n        <label>Id organizatora \/ nazwa firmy<\/label>\n        <input id=\"organizer-id\" type=\"text\" placeholder=\"np. cafe\" value=\"${escapeHtml(savedOrganizerId)}\">\n\n        <div class=\"hint\">Masz aktywn\u0105 sesj\u0119. Wpisz id organizatora i wejd\u017a do panelu.<\/div>\n        <button id=\"enter-panel\">Wejd\u017a do panelu<\/button>\n        <button id=\"logout\" class=\"secondary\">Wyloguj<\/button>\n      ` : state.confirmationResult ? `\n        <label>Numer telefonu<\/label>\n        <input id=\"phone-number\" type=\"tel\" value=\"${escapeHtml(state.phoneNumber || \"\")}\" disabled>\n\n        <label>Id organizatora \/ nazwa firmy<\/label>\n        <input id=\"organizer-id\" type=\"text\" placeholder=\"np. cafe\" value=\"${escapeHtml(savedOrganizerId)}\">\n\n        <label>Kod SMS<\/label>\n        <input id=\"sms-code\" type=\"text\" inputmode=\"numeric\" maxlength=\"6\" placeholder=\"123456\">\n\n        <button id=\"verify-code\">Zaloguj<\/button>\n      ` : `\n        <label>Numer telefonu<\/label>\n        <input id=\"phone-number\" type=\"tel\" placeholder=\"+48...\" autocomplete=\"tel\">\n\n        <button id=\"send-sms\">Wy\u015blij SMS<\/button>\n      `}\n\n      <div id=\"partner-message\" class=\"partner-message\"><\/div>\n      ${debugHtml()}\n    <\/div>\n  `;\n\n  const enterPanel = document.getElementById(\"enter-panel\");\n  if (enterPanel) {\n    enterPanel.onclick = async () => {\n      const organizerId = document.getElementById(\"organizer-id\").value.trim();\n      if (!organizerId) return setMessage(\"Podaj id organizatora.\", \"error\");\n      await loadPartnerMe(organizerId);\n    };\n  }\n\n  const logout = document.getElementById(\"logout\");\n  if (logout) {\n    logout.onclick = async () => {\n      localStorage.removeItem(savedOrganizerIdKey);\n      await auth.signOut();\n      state.partner = null;\n      state.confirmationResult = null;\n      state.mode = \"login\";\n      render();\n    };\n  }\n\n  const sendSms = document.getElementById(\"send-sms\");\n  if (sendSms) sendSms.onclick = sendSmsCode;\n\n  const verifyCode = document.getElementById(\"verify-code\");\n  if (verifyCode) verifyCode.onclick = verifySmsCode;\n}\n\n  async function sendSmsCode() {\n    const now = Date.now();\n\n  if (state.smsInFlight) {\n    setMessage(\"SMS jest ju\u017c wysy\u0142any, poczekaj chwil\u0119.\", \"info\");\n    return;\n  }\n\n  if (state.smsSentAt > 0 && now - state.smsSentAt < smsCooldownMs) {\n    const secondsLeft = Math.ceil((smsCooldownMs - (now - state.smsSentAt)) \/ 1000);\n    setMessage(\"SMS zosta\u0142 ju\u017c wys\u0142any. Spr\u00f3buj ponownie za \" + secondsLeft + \"s.\", \"info\");\n    return;\n  }\n\n  const phoneInput = document.getElementById(\"phone-number\");\n  const phone = (phoneInput?.value || state.phoneNumber || \"\").trim();\n\n  if (!phone.startsWith(\"+\")) {\n    setMessage(\"Podaj numer w formacie mi\u0119dzynarodowym, np. +48123456789.\", \"error\");\n    return;\n  }\n\n  const button =\n    document.getElementById(\"send-sms\") ||\n    document.getElementById(\"send-sms-again\");\n\n  try {\n    state.smsInFlight = true;\n    state.smsSentAt = now;\n\n    if (button) {\n      button.disabled = true;\n      button.textContent = \"Wysy\u0142am...\";\n    }\n\n    setMessage(\"Wysy\u0142am SMS...\", \"info\");\n\n      debug(\"SEND SMS\", { phone });\n\n      if (!window.recaptchaVerifier) {\n        window.recaptchaVerifier = new firebase.auth.RecaptchaVerifier(\"recaptcha-container\", {\n          size: \"invisible\"\n        });\n\n        await window.recaptchaVerifier.render();\n      }\n\n      state.confirmationResult = await auth.signInWithPhoneNumber(phone, window.recaptchaVerifier);\n      state.phoneNumber = phone;\n\n      debug(\"SEND SMS SUCCESS\", { phone });\n\n      setMessage(\"SMS wys\u0142any. Wpisz kod.\", \"success\");\n      render();\n    } catch (e) {\n      state.smsSentAt = 0;\n\n      console.error(\"sendSms error:\", e);\n\n      debug(\"SEND SMS ERROR\", {\n        code: e.code || \"\",\n        message: e.message || \"\"\n      });\n\n      setMessage(\n        \"SMS error: \" + (e.code || \"\") + \" \" + (e.message || \"\"),\n        \"error\"\n      );\n\n      if (window.recaptchaVerifier) {\n        try { window.recaptchaVerifier.clear(); } catch (_) {}\n        window.recaptchaVerifier = null;\n      }\n    } finally {\n      state.smsInFlight = false;\n    }\n  }\n\n  async function verifySmsCode() {\n    const organizerId = document.getElementById(\"organizer-id\").value.trim();\n    const code = document.getElementById(\"sms-code\").value.trim();\n\n    if (!organizerId) return setMessage(\"Podaj id organizatora.\", \"error\");\n    if (!code || code.length !== 6) return setMessage(\"Podaj 6-cyfrowy kod SMS.\", \"error\");\n    if (!state.confirmationResult) return setMessage(\"Najpierw wy\u015blij SMS.\", \"error\");\n\n    try {\n      setMessage(\"Loguj\u0119...\", \"info\");\n      debug(\"VERIFY SMS\", { organizerId });\n\n      await state.confirmationResult.confirm(code);\n      await loadPartnerMe(organizerId);\n    } catch (e) {\n      console.error(\"verifySmsCode error:\", e);\n\n      debug(\"VERIFY SMS ERROR\", {\n        code: e.code || \"\",\n        message: e.message || \"\"\n      });\n\n      setMessage(\n        \"Niepoprawny kod lub brak uprawnie\u0144: \" + (e.message || \"\"),\n        \"error\"\n      );\n    }\n  }\n\n  async function loadPartnerMe(organizerId) {\n    try {\n      state.loading = true;\n      setMessage(\"Pobieram panel...\", \"info\");\n\n      const data = await apiJson(ROUTES.PARTNER_ME, { organizerId });\n\n      if (!data) {\n        setMessage(\"Brak uprawnie\u0144 do tego organizatora.\", \"error\");\n        return;\n      }\n\n      state.partner = data;\n      state.mode = \"dashboard\";\n      state.generatedValidatorUrl = \"\";\n      localStorage.setItem(savedOrganizerIdKey, data.organizerId || organizerId);\n\n      debug(\"PARTNER ME LOADED\", data);\n\n      render();\n    } catch (e) {\n      console.error(\"loadPartnerMe error:\", e);\n\n      debug(\"PARTNER ME ERROR\", {\n        message: e.message || String(e)\n      });\n\n      setMessage(\n        \"Brak uprawnie\u0144 albo b\u0142\u0105d po\u0142\u0105czenia: \" + (e.message || \"\"),\n        \"error\"\n      );\n    } finally {\n      state.loading = false;\n    }\n  }\n\n  function renderDashboard() {\n    const partner = state.partner;\n    const events = partner.events || [];\n\n    root.innerHTML = `\n      <style>${styles()}<\/style>\n\n      <div class=\"partner-wrap\">\n        <div class=\"top-row\">\n          <div>\n            <h1>Panel partnera<\/h1>\n            <p class=\"partner-name\">${escapeHtml(partner.organizerName || partner.organizerId)}<\/p>\n          <\/div>\n          <button id=\"logout\" class=\"secondary small\">Wyloguj<\/button>\n        <\/div>\n\n        <div id=\"partner-message\" class=\"partner-message\"><\/div>\n        ${debugHtml()}\n\n        <h2>Eventy<\/h2>\n\n        <div class=\"events-list\">\n          ${\n            events.length === 0\n              ? `<div class=\"empty\">Brak event\u00f3w.<\/div>`\n              : events.map((event, index) => eventCard(event, index)).join(\"\")\n          }\n        <\/div>\n\n        <button id=\"add-event\">Dodaj event<\/button>\n\n        <hr>\n\n        <button id=\"generate-validator-link\" class=\"secondary\">\n          Wygeneruj link do skanera\n        <\/button>\n\n        ${\n          state.generatedValidatorUrl\n            ? `\n              <div class=\"link-box\">\n                <div>Link do skanera, skopiuj i wy\u015blij pracownikom:<\/div>\n                <input id=\"validator-link\" readonly value=\"${escapeHtml(state.generatedValidatorUrl)}\">\n                <button id=\"copy-validator-link\" class=\"small\">Kopiuj<\/button>\n                <div class=\"hint\">Po od\u015bwie\u017ceniu strony ten link zniknie. W razie potrzeby wygeneruj nowy.<\/div>\n              <\/div>\n            `\n            : \"\"\n        }\n      <\/div>\n    `;\n\n    document.getElementById(\"logout\").onclick = async () => {\n      localStorage.removeItem(savedOrganizerIdKey);\n      await auth.signOut();\n      state.partner = null;\n      state.mode = \"login\";\n      render();\n    };\n\n    document.getElementById(\"add-event\").onclick = () => {\n      state.editingEvent = null;\n      state.selectedPhotoFile = null;\n      state.mode = \"form\";\n      render();\n    };\n\n    document.querySelectorAll(\"[data-edit-index]\").forEach(btn => {\n      btn.onclick = () => {\n        const index = Number(btn.getAttribute(\"data-edit-index\"));\n        state.editingEvent = events[index];\n        state.selectedPhotoFile = null;\n        state.mode = \"form\";\n        render();\n      };\n    });\n\n    document.querySelectorAll(\"[data-cancel-index]\").forEach(btn => {\n      btn.onclick = async () => {\n        const index = Number(btn.getAttribute(\"data-cancel-index\"));\n        const event = events[index];\n\n        if (!confirm(`Czy na pewno odwo\u0142a\u0107 event \"${event.title}\"?`)) return;\n\n        try {\n          setMessage(\"Odwo\u0142uj\u0119 event...\", \"info\");\n\n          await apiJson(ROUTES.PARTNER_EVENT_CANCEL, {\n            organizerId: partner.organizerId,\n            eventId: event.id\n          });\n\n          await loadPartnerMe(partner.organizerId);\n        } catch (e) {\n          console.error(\"cancel event error:\", e);\n\n          debug(\"CANCEL EVENT ERROR\", {\n            message: e.message || String(e)\n          });\n\n          setMessage(\"Nie uda\u0142o si\u0119 odwo\u0142a\u0107 eventu: \" + (e.message || \"\"), \"error\");\n        }\n      };\n    });\n\n    document.getElementById(\"generate-validator-link\").onclick = async () => {\n      try {\n        setMessage(\"Generuj\u0119 link...\", \"info\");\n\n        const data = await apiJson(ROUTES.PARTNER_VALIDATOR_LINK, {\n          organizerId: partner.organizerId\n        });\n\n        state.generatedValidatorUrl =\n          data?.url ||\n          data?.link ||\n          \"\";\n\n        if (!state.generatedValidatorUrl && data?.token) {\n          state.generatedValidatorUrl =\n            \"https:\/\/blirtyapp.com\/index.php\/beort04jce593sfd54jw3h424h53b2j3h\/#\" +\n            encodeURIComponent(data.token);\n        }\n\n        if (!state.generatedValidatorUrl) {\n          setMessage(\"Backend nie zwr\u00f3ci\u0142 linku.\", \"error\");\n          return;\n        }\n\n        debug(\"VALIDATOR LINK GENERATED\", {\n          url: state.generatedValidatorUrl\n        });\n\n        render();\n      } catch (e) {\n        console.error(\"generate validator link error:\", e);\n\n        debug(\"VALIDATOR LINK ERROR\", {\n          message: e.message || String(e)\n        });\n\n        setMessage(\"Nie uda\u0142o si\u0119 wygenerowa\u0107 linku: \" + (e.message || \"\"), \"error\");\n      }\n    };\n\n    const copyBtn = document.getElementById(\"copy-validator-link\");\n    if (copyBtn) {\n      copyBtn.onclick = async () => {\n        const input = document.getElementById(\"validator-link\");\n        await navigator.clipboard.writeText(input.value);\n        setMessage(\"Link skopiowany.\", \"success\");\n      };\n    }\n  }\n\nfunction canManageEvent(event) {\n  const status = event?.status || \"\";\n  const startAt = Number(event?.startAt || 0);\n  const now = Date.now();\n\n  return status === \"ACTIVE\" && startAt > now;\n}\n\n  function eventCard(event, index) {\n  const manageable = canManageEvent(event);\n\n  return `\n    <div class=\"event-card\">\n      <img decoding=\"async\" src=\"${escapeHtml(event.photo || \"\")}\" alt=\"\">\n      <div class=\"event-info\">\n        <div class=\"event-title\">${escapeHtml(event.title)}<\/div>\n        <div class=\"event-meta\">${escapeHtml(event.date)} \u00b7 ${escapeHtml(event.meetingTime)}<\/div>\n        <div class=\"event-meta\">${escapeHtml(event.city)}<\/div>\n        <div class=\"event-meta\">Status: ${escapeHtml(formatStatus(event.status))}<\/div>\n        <div class=\"event-meta\">Cena: ${minorToPln(event.amount)} ${escapeHtml(event.currency || \"pln\")}<\/div>\n        <div class=\"event-meta\">Wolne miejsca: ${escapeHtml(event.availableTickets ?? \"-\")}<\/div>\n      <\/div>\n      <div class=\"event-actions\">\n        ${\n          manageable\n            ? `\n              <button class=\"small\" data-edit-index=\"${index}\">Edytuj<\/button>\n              <button class=\"small danger\" data-cancel-index=\"${index}\">Odwo\u0142aj<\/button>\n            `\n            : `<div class=\"readonly-badge\">Tylko podgl\u0105d<\/div>`\n        }\n      <\/div>\n    <\/div>\n  `;\n}\n\n  function renderEventForm() {\n    const partner = state.partner;\n    const event = state.editingEvent;\n    const isEdit = !!event;\n\n    root.innerHTML = `\n      <style>${styles()}<\/style>\n\n      <div class=\"partner-wrap\">\n        <button id=\"back-to-list\" class=\"secondary small\">\u2190 Wr\u00f3\u0107<\/button>\n\n        <h1>${isEdit ? \"Edytuj event\" : \"Dodaj event\"}<\/h1>\n        <div id=\"partner-message\" class=\"partner-message\"><\/div>\n        ${debugHtml()}\n\n        <div class=\"form-grid\">\n          <label>Tytu\u0142<\/label>\n          <input id=\"title\" value=\"${escapeHtml(event?.title || \"\")}\">\n\n          <label>Opis<\/label>\n          <textarea id=\"description\" rows=\"5\">${escapeHtml(event?.description || \"\")}<\/textarea>\n\n          <label>Miasto<\/label>\n          <input id=\"city\" value=\"${escapeHtml(event?.city || \"\")}\">\n\n          <label>Lokalizacja<\/label>\n          <input id=\"location\" value=\"${escapeHtml(event?.location || \"\")}\">\n\n          <label>Data do wy\u015bwietlenia, np. 25\/07 Sobota<\/label>\n          <input id=\"date\" value=\"${escapeHtml(event?.date || \"\")}\">\n\n          <label>Godzina do wy\u015bwietlenia, np. 10:00<\/label>\n          <input id=\"meetingTime\" value=\"${escapeHtml(event?.meetingTime || \"\")}\">\n\n          <label>Start eventu<\/label>\n          <input id=\"startAt\" type=\"datetime-local\" value=\"${timestampToLocalInput(event?.startAt)}\">\n\n          <label>Koniec eventu<\/label>\n          <input id=\"endAt\" type=\"datetime-local\" value=\"${timestampToLocalInput(event?.endAt)}\">\n\n          <label>Liczba miejsc<\/label>\n          <input id=\"capacity\" type=\"number\" min=\"0\" value=\"${escapeHtml(event?.capacity || \"\")}\">\n\n          <label>Cena aktualna, z\u0142<\/label>\n          <input id=\"amount\" inputmode=\"decimal\" value=\"${minorToPln(event?.amount || 0)}\">\n\n          <label>Cena poprzednia, z\u0142 \u2014 opcjonalnie<\/label>\n          <input id=\"previousAmount\" inputmode=\"decimal\" value=\"${minorToPln(event?.previousAmount || 0)}\">\n\n          <label>Waluta<\/label>\n          <input id=\"currency\" value=\"${escapeHtml(event?.currency || \"pln\")}\">\n\n          <label>\n            <input id=\"discountEnabled\" type=\"checkbox\" ${event?.discountEnabled ? \"checked\" : \"\"}>\n            Zni\u017cka dost\u0119pna\n          <\/label>\n\n          <label>Tekst zni\u017cki<\/label>\n          <input id=\"discountInfo\" value=\"${escapeHtml(event?.discountInfo || \"\")}\">\n\n          <label>Cena po zni\u017cce, z\u0142<\/label>\n          <input id=\"discountAmount\" inputmode=\"decimal\" value=\"${minorToPln(event?.discountAmount || 0)}\">\n\n          <label>Zdj\u0119cie eventu<\/label>\n          <input id=\"photo\" type=\"file\" accept=\"image\/*\">\n\n          ${\n            event?.photo\n              ? `<img decoding=\"async\" class=\"preview\" src=\"${escapeHtml(event.photo)}\" alt=\"\">`\n              : `<div class=\"hint\">Zdj\u0119cie jest wymagane przy dodawaniu eventu.<\/div>`\n          }\n        <\/div>\n\n        <button id=\"save-event\">${isEdit ? \"Aktualizuj\" : \"Dodaj event\"}<\/button>\n      <\/div>\n    `;\n\n    document.getElementById(\"back-to-list\").onclick = () => {\n      state.mode = \"dashboard\";\n      state.editingEvent = null;\n      state.selectedPhotoFile = null;\n      state.savingEvent = false;\n      render();\n    };\n\n    document.getElementById(\"photo\").onchange = (e) => {\n      state.selectedPhotoFile = e.target.files?.[0] || null;\n\n      if (state.selectedPhotoFile) {\n        debug(\"PHOTO SELECTED\", {\n          name: state.selectedPhotoFile.name,\n          size: state.selectedPhotoFile.size,\n          type: state.selectedPhotoFile.type\n        });\n      }\n    };\n\n    document.getElementById(\"save-event\").onclick = async () => {\n      if (state.savingEvent) {\n        setMessage(\"Zapis ju\u017c trwa, poczekaj chwil\u0119.\", \"info\");\n        return;\n      }\n\n      const saveButton = document.getElementById(\"save-event\");\n\n      try {\n        state.savingEvent = true;\n        saveButton.disabled = true;\n        saveButton.textContent = isEdit ? \"Aktualizuj\u0119...\" : \"Dodaj\u0119...\";\n\n        setMessage(\"Zapisuj\u0119...\", \"info\");\n\n        const payload = collectEventPayload();\n\n        debug(\"COLLECTED EVENT PAYLOAD BEFORE PHOTO\", payload);\n\n        if (!isEdit && !state.selectedPhotoFile) {\n          setMessage(\"Dodaj zdj\u0119cie eventu.\", \"error\");\n          return;\n        }\n\n        if (!isEdit) {\n          const upload = await uploadEventPhoto(partner.organizerId, state.selectedPhotoFile);\n\n          payload.photoUploadId = upload.uploadId;\n\n          debug(\"PAYLOAD AFTER PHOTO UPLOAD\", payload);\n        } else {\n          \/\/ Obecny backend updatePartnerEvent nie aktualizuje zdj\u0119cia.\n          \/\/ Je\u015bli wybierzesz nowe zdj\u0119cie przy edycji, uploadujemy je tylko diagnostycznie.\n          \/\/ Do pe\u0142nej zmiany zdj\u0119cia trzeba doda\u0107 photoUploadId\/photo update po stronie Ktor.\n          if (state.selectedPhotoFile) {\n            const upload = await uploadEventPhoto(partner.organizerId, state.selectedPhotoFile);\n            payload.photoUploadId = upload.uploadId;\n            debug(\"EDIT PHOTO UPLOADED BUT BACKEND MAY IGNORE IT\", upload);\n          }\n\n          debug(\"UPDATE PAYLOAD\", payload);\n        }\n\n        if (isEdit) {\n          await apiJson(ROUTES.PARTNER_EVENT_UPDATE, {\n            organizerId: partner.organizerId,\n            eventId: event.id,\n            ...payload\n          });\n        } else {\n          await apiJson(ROUTES.PARTNER_EVENT_ADD, {\n            organizerId: partner.organizerId,\n            ...payload\n          });\n        }\n\n        setMessage(\"Event zapisany.\", \"success\");\n\n        await loadPartnerMe(partner.organizerId);\n      } catch (e) {\n        console.error(\"save event error:\", e);\n\n        debug(\"SAVE EVENT ERROR\", {\n          message: e.message || String(e),\n          stack: e.stack || \"\"\n        });\n\n        setMessage(\n          \"Nie uda\u0142o si\u0119 zapisa\u0107 eventu: \" + (e.message || JSON.stringify(e)),\n          \"error\"\n        );\n      } finally {\n        state.savingEvent = false;\n\n        if (saveButton) {\n          saveButton.disabled = false;\n          saveButton.textContent = isEdit ? \"Aktualizuj\" : \"Dodaj event\";\n        }\n      }\n    };\n  }\n\n  function collectEventPayload() {\n    const startAtInput = document.getElementById(\"startAt\").value;\n    const endAtInput = document.getElementById(\"endAt\").value;\n\n    const startAt = localInputToTimestamp(startAtInput);\n    const endAt = localInputToTimestamp(endAtInput);\n\n    if (!startAt || !endAt) throw new Error(\"Brak startAt\/endAt.\");\n    if (endAt <= startAt) throw new Error(\"Koniec eventu musi by\u0107 po starcie.\");\n\n    const payload = {\n      title: document.getElementById(\"title\").value.trim(),\n      description: document.getElementById(\"description\").value.trim(),\n      city: document.getElementById(\"city\").value.trim(),\n      location: document.getElementById(\"location\").value.trim(),\n      date: document.getElementById(\"date\").value.trim(),\n      meetingTime: document.getElementById(\"meetingTime\").value.trim(),\n      capacity: Number(document.getElementById(\"capacity\").value || 0),\n      amount: plnToMinor(document.getElementById(\"amount\").value),\n      previousAmount: plnToMinor(document.getElementById(\"previousAmount\").value),\n      currency: document.getElementById(\"currency\").value.trim().toLowerCase() || \"pln\",\n      startAt,\n      endAt,\n      discountEnabled: document.getElementById(\"discountEnabled\").checked,\n      discountInfo: document.getElementById(\"discountInfo\").value.trim(),\n      discountAmount: plnToMinor(document.getElementById(\"discountAmount\").value)\n    };\n\n    debug(\"TIME INPUTS\", {\n      startAtInput,\n      endAtInput,\n      startAt,\n      endAt,\n      startDateReadable: new Date(startAt).toString(),\n      endDateReadable: new Date(endAt).toString()\n    });\n\n    return payload;\n  }\n\n  async function uploadEventPhoto(organizerId, file) {\n    if (!file) throw new Error(\"Brak pliku zdj\u0119cia.\");\n\n    const formData = new FormData();\n    formData.append(\"organizerId\", organizerId);\n    formData.append(\"photo\", file);\n\n    const data = await apiMultipart(ROUTES.PARTNER_EVENT_PHOTO, formData);\n\n    const uploadId = data?.uploadId || \"\";\n    const photoUrl = data?.photoUrl || data?.url || data?.photo || \"\";\n\n    debug(\"PHOTO UPLOAD RESULT\", {\n      uploadId,\n      photoUrl,\n      raw: data\n    });\n\n    if (!uploadId) {\n      throw new Error(\"Backend nie zwr\u00f3ci\u0142 uploadId zdj\u0119cia.\");\n    }\n\n    if (!photoUrl) {\n      debug(\"PHOTO UPLOAD WARNING\", \"Backend nie zwr\u00f3ci\u0142 photoUrl, ale uploadId istnieje.\");\n    }\n\n    return {\n      uploadId,\n      photoUrl\n    };\n  }\n\n  auth.onAuthStateChanged(async (user) => {\n    state.user = user;\n\n    debug(\"AUTH STATE\", {\n      uid: user?.uid || null,\n      phoneNumber: user?.phoneNumber || null\n    });\n\n    const savedOrganizerId = localStorage.getItem(savedOrganizerIdKey);\n\n    if (user && savedOrganizerId && !state.partner) {\n      await loadPartnerMe(savedOrganizerId);\n    } else {\n      render();\n    }\n  });\n\n  function styles() {\n    return `\n      #partner-panel-root {\n        max-width: 920px;\n        margin: 32px auto;\n        padding: 0 16px;\n        font-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n        color: #151515;\n      }\n\n      .partner-card,\n      .partner-wrap {\n        background: #fff;\n        border: 1px solid #eee;\n        border-radius: 16px;\n        padding: 24px;\n        box-shadow: 0 8px 30px rgba(0,0,0,0.04);\n      }\n\n.partner-name {\n  margin: -8px 0 0;\n  font-size: 22px;\n  line-height: 1.25;\n  font-weight: 750;\n  color: #5b0b14;\n}\n\n.readonly-badge {\n  border: 1px solid #ddd;\n  border-radius: 999px;\n  padding: 8px 12px;\n  color: #777;\n  font-size: 13px;\n  font-weight: 700;\n  white-space: nowrap;\n  text-align: center;\n  background: #fff;\n}\n\n      h1 {\n        margin: 0 0 18px;\n        font-size: 32px;\n        line-height: 1.15;\n      }\n\n      h2 {\n        margin-top: 28px;\n        font-size: 24px;\n      }\n\n      .subtitle,\n      .hint,\n      .event-meta {\n        color: #777;\n      }\n\n      label {\n        display: block;\n        margin: 14px 0 6px;\n        font-weight: 650;\n      }\n\n      input,\n      textarea {\n        width: 100%;\n        box-sizing: border-box;\n        border: 1px solid #ddd;\n        border-radius: 12px;\n        padding: 12px 14px;\n        font-size: 16px;\n        background: #fff;\n      }\n\n      textarea {\n        resize: vertical;\n      }\n\n      button {\n        width: 100%;\n        margin-top: 14px;\n        padding: 13px 16px;\n        border: none;\n        border-radius: 12px;\n        background: #5b0b14;\n        color: #fff;\n        font-weight: 750;\n        font-size: 16px;\n        cursor: pointer;\n      }\n\n      button:disabled {\n        opacity: 0.65;\n        cursor: not-allowed;\n      }\n\n      button.secondary {\n        background: #fff;\n        color: #111;\n        border: 1px solid #ddd;\n      }\n\n      button.danger {\n        background: #fff;\n        color: #b00020;\n        border: 1px solid #f0c4ca;\n      }\n\n      button.small {\n        width: auto;\n        padding: 9px 12px;\n        font-size: 14px;\n        margin-top: 0;\n      }\n\n      .partner-message {\n        margin-top: 14px;\n        min-height: 22px;\n        font-weight: 600;\n        white-space: pre-wrap;\n      }\n\n      .partner-message.error { color: #b00020; }\n      .partner-message.success { color: #0b7a32; }\n      .partner-message.info { color: #555; }\n\n      .debug-box {\n        margin-top: 14px;\n        border: 1px dashed #ddd;\n        border-radius: 12px;\n        padding: 12px;\n        background: #fafafa;\n      }\n\n      .debug-box summary {\n        cursor: pointer;\n        font-weight: 700;\n        color: #555;\n      }\n\n      .debug-box pre {\n        margin: 10px 0 0;\n        white-space: pre-wrap;\n        word-break: break-word;\n        font-size: 12px;\n        line-height: 1.45;\n        color: #333;\n      }\n\n      .top-row {\n        display: flex;\n        justify-content: space-between;\n        gap: 16px;\n        align-items: flex-start;\n      }\n\n      .events-list {\n        display: flex;\n        flex-direction: column;\n        gap: 14px;\n      }\n\n      .event-card {\n        display: grid;\n        grid-template-columns: 110px 1fr auto;\n        gap: 14px;\n        align-items: center;\n        border: 1px solid #eee;\n        border-radius: 14px;\n        padding: 12px;\n      }\n\n      .event-card img {\n        width: 110px;\n        aspect-ratio: 4 \/ 5;\n        height: auto;\n        object-fit: cover;\n        border-radius: 12px;\n        background: #eee;\n      }\n\n      .event-title {\n        font-size: 18px;\n        font-weight: 800;\n        margin-bottom: 4px;\n      }\n\n      .event-actions {\n        display: flex;\n        flex-direction: column;\n        gap: 8px;\n      }\n\n      .empty {\n        padding: 18px;\n        border: 1px dashed #ddd;\n        border-radius: 14px;\n        color: #777;\n      }\n\n      hr {\n        border: none;\n        border-top: 1px solid #eee;\n        margin: 28px 0;\n      }\n\n      .link-box {\n        margin-top: 14px;\n        padding: 14px;\n        border: 1px solid #eee;\n        border-radius: 14px;\n        background: #fafafa;\n      }\n\n      .link-box input {\n        margin-top: 8px;\n        font-size: 14px;\n      }\n\n      .form-grid {\n        display: grid;\n        gap: 4px;\n      }\n\n      .preview {\n        width: min(100%, 360px);\n        aspect-ratio: 4 \/ 5;\n        height: 320px;\n        object-fit: cover;\n        border-radius: 14px;\n        margin-top: 10px;\n        background: #eee;\n      }\n\n      @media (max-width: 720px) {\n        .event-card {\n          grid-template-columns: 86px 1fr;\n        }\n\n        .event-card img {\n          width: 86px;\n          aspect-ratio: 4 \/ 5;\n          height: auto;\n        }\n\n        .event-actions {\n          grid-column: 1 \/ -1;\n          flex-direction: row;\n        }\n\n        .event-actions button {\n          flex: 1;\n        }\n\n        .top-row {\n          flex-direction: column;\n        }\n      }\n    `;\n  }\n})();\n<\/script>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_uag_custom_page_level_css":"","site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"class_list":["post-468","page","type-page","status-publish","hentry"],"uagb_featured_image_src":{"full":false,"thumbnail":false,"medium":false,"medium_large":false,"large":false,"1536x1536":false,"2048x2048":false},"uagb_author_info":{"display_name":"admin-iPcaUszA","author_link":"https:\/\/blirtyapp.com\/index.php\/author\/admin-ipcausza\/"},"uagb_comment_info":0,"uagb_excerpt":null,"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/blirtyapp.com\/index.php\/wp-json\/wp\/v2\/pages\/468","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blirtyapp.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/blirtyapp.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/blirtyapp.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blirtyapp.com\/index.php\/wp-json\/wp\/v2\/comments?post=468"}],"version-history":[{"count":28,"href":"https:\/\/blirtyapp.com\/index.php\/wp-json\/wp\/v2\/pages\/468\/revisions"}],"predecessor-version":[{"id":507,"href":"https:\/\/blirtyapp.com\/index.php\/wp-json\/wp\/v2\/pages\/468\/revisions\/507"}],"wp:attachment":[{"href":"https:\/\/blirtyapp.com\/index.php\/wp-json\/wp\/v2\/media?parent=468"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}