{"id":1738,"date":"2026-09-28T08:00:02","date_gmt":"2026-09-28T08:00:02","guid":{"rendered":"https:\/\/www.kuvaukset.com\/kuvaukset\/?page_id=1738"},"modified":"2026-09-28T08:00:06","modified_gmt":"2026-09-28T08:00:06","slug":"varaa","status":"publish","type":"page","link":"https:\/\/www.kuvaukset.com\/kuvaukset\/varaa\/","title":{"rendered":"Varaa"},"content":{"rendered":"    <div id=\"kc-chat\" style=\"max-width:640px;\">\n      <div id=\"kc-chat-loading\" style=\"color:#444;\">Ladataan\u2026<\/div>\n\n      <div id=\"kc-chat-login\" style=\"display:none;\">\n        <p style=\"color:#222;\">Kirjaudu sis\u00e4\u00e4n, niin voit varata kuvausajan t\u00e4ss\u00e4 keskustelussa.<\/p>\n        <a id=\"kc-chat-login-link\" href=\"https:\/\/www.kuvaukset.com\/kuvaukset\/kirjaudu\/\" style=\"display:inline-block; padding:8px 16px; background:#2563eb; color:#fff; border-radius:6px; text-decoration:none;\">Kirjaudu sis\u00e4\u00e4n<\/a>\n      <\/div>\n\n      <div id=\"kc-chat-main\" style=\"display:none;\">\n        <div style=\"display:flex; justify-content:space-between; align-items:center; margin-bottom:8px; gap:8px; flex-wrap:wrap;\">\n          <strong style=\"color:#111;\">Varaa kuvausaika<\/strong>\n          <span>\n            <button type=\"button\" id=\"kc-chat-mybookings\" style=\"padding:6px 12px; border:1px solid #2563eb; background:#fff; color:#2563eb; border-radius:6px; cursor:pointer; margin-right:10px;\">Kaikki varaukseni<\/button>\n            <a href=\"#\" id=\"kc-chat-reset\" style=\"color:#2563eb; text-decoration:underline; font-size:14px;\">Aloita alusta<\/a>\n            <a href=\"#\" id=\"kc-chat-logout\" style=\"color:#2563eb; text-decoration:underline; font-size:14px; margin-left:10px;\">Kirjaudu ulos<\/a>\n          <\/span>\n        <\/div>\n\n        <div id=\"kc-mode-switch\" style=\"margin-bottom:10px; font-size:14px;\">\n          <a href=\"#\" id=\"kc-to-chat\" style=\"color:#2563eb; text-decoration:underline;\">Varaa chatin kautta<\/a>\n          <a href=\"#\" id=\"kc-to-form\" style=\"color:#2563eb; text-decoration:underline; display:none;\">Varaa lomakkeella<\/a>\n        <\/div>\n\n        <div id=\"kc-chat-bookingsview\" style=\"display:none;\">\n          <div style=\"margin-bottom:10px;\">\n            <button type=\"button\" id=\"kc-chat-back\" style=\"padding:6px 12px; border:1px solid #9ca3af; background:#fff; color:#374151; border-radius:6px; cursor:pointer;\">\u2190 Takaisin varaamiseen<\/button>\n          <\/div>\n          <div id=\"kc-chat-bookings-notice\" style=\"display:none; padding:10px 14px; border:1px solid #16a34a; background:#f0fdf4; color:#166534; border-radius:8px; margin-bottom:10px; font-size:14px;\"><\/div>\n          <div id=\"kc-chat-bookings-list\" style=\"color:#111827;\"><\/div>\n        <\/div>\n\n        <div id=\"kc-chat-formview\" style=\"display:none;\">\n          <div id=\"kc-form-card\" style=\"border:1px solid #e5e7eb; border-radius:12px; padding:16px; background:#fff; color:#111827;\">\n            <div id=\"kc-form-included\" style=\"display:none; font-size:14px; color:#374151; margin-bottom:12px;\"><\/div>\n            <div id=\"kc-form-service-wrap\" style=\"display:none; margin-bottom:14px;\">\n              <label for=\"kc-form-service\" style=\"display:block; font-weight:600; margin-bottom:4px;\">Palvelu<\/label>\n              <select id=\"kc-form-service\" style=\"width:100%; box-sizing:border-box; padding:9px; border:1px solid #ccc; border-radius:8px; color:#111; background:#fff; font-size:15px;\"><\/select>\n            <\/div>\n\n            <label for=\"kc-form-address\" style=\"display:block; font-weight:600; margin-bottom:4px;\">Osoite<\/label>\n            <div id=\"kc-form-addr-help\" style=\"font-size:13px; color:#6b7280; margin-bottom:6px;\">Kirjoita katuosoite ja talon numero. Jos osoite on muualla kuin Tampereella, kirjoita kaupunki osoitteen per\u00e4\u00e4n, esimerkiksi Kahvimyllynkatu 1, Nokia.<\/div>\n            <div>\n              <input type=\"text\" id=\"kc-form-address\" autocomplete=\"off\" maxlength=\"120\" placeholder=\"Esimerkiksi Kuninkaankatu 24 B 61, Tampere\" role=\"combobox\" aria-autocomplete=\"list\" aria-expanded=\"false\" aria-controls=\"kc-form-suggest\" style=\"width:100%; box-sizing:border-box; padding:10px; border:1px solid #ccc; border-radius:8px; color:#111; background:#fff; font-size:15px;\">\n              <div id=\"kc-form-suggest\" role=\"listbox\" style=\"display:none; background:#fff; border:1px solid #9ca3af; border-radius:8px; margin-top:4px; overflow:hidden;\"><\/div>\n            <\/div>\n            <div id=\"kc-form-addr-msg\" aria-live=\"polite\" style=\"font-size:13px; color:#6b7280; margin-top:4px; min-height:18px;\"><\/div>\n\n            <div id=\"kc-form-times\" style=\"display:none; margin-top:14px;\">\n              <div style=\"font-weight:600; margin-bottom:6px;\">Vapaita aikoja<\/div>\n              <div id=\"kc-form-notice\" role=\"alert\" style=\"color:#991b1b; font-size:14px; margin-bottom:8px;\"><\/div>\n              <div id=\"kc-form-times-status\" aria-live=\"polite\" style=\"font-size:14px; color:#6b7280;\"><\/div>\n              <div id=\"kc-form-daynav\" style=\"display:none; align-items:center; justify-content:space-between; gap:8px; margin-bottom:10px;\">\n                <button type=\"button\" id=\"kc-form-prev\" aria-label=\"Edellinen p\u00e4iv\u00e4\" style=\"min-width:48px; padding:8px 14px; font-size:20px; line-height:1; border:1px solid #2563eb; background:#fff; color:#2563eb; border-radius:8px; cursor:pointer;\">\u2039<\/button>\n                <div id=\"kc-form-daylabel\" aria-live=\"polite\" style=\"flex:1; text-align:center; font-weight:600; font-size:16px; color:#111827;\"><\/div>\n                <button type=\"button\" id=\"kc-form-nextday\" aria-label=\"Seuraava p\u00e4iv\u00e4\" style=\"min-width:48px; padding:8px 14px; font-size:20px; line-height:1; border:1px solid #2563eb; background:#fff; color:#2563eb; border-radius:8px; cursor:pointer;\">\u203a<\/button>\n              <\/div>\n              <div id=\"kc-form-days\"><\/div>\n            <\/div>\n\n            <div id=\"kc-form-details\" style=\"display:none; margin-top:16px; padding-top:14px; border-top:1px solid #e5e7eb;\">\n              <div id=\"kc-form-summary\" style=\"background:#f8fbff; border:1px solid #bfdbfe; border-radius:8px; padding:10px 12px; font-size:14px; line-height:1.5; margin-bottom:12px;\"><\/div>\n              <div style=\"font-weight:600; margin-bottom:6px;\">Asuntoon p\u00e4\u00e4sy<\/div>\n              <div id=\"kc-form-access\" role=\"group\" aria-label=\"Asuntoon p\u00e4\u00e4sy\" style=\"display:flex; flex-wrap:wrap; gap:8px;\">\n                <button type=\"button\" class=\"kc-access-btn\" data-mode=\"customer\" aria-pressed=\"false\" style=\"padding:9px 14px; border:1px solid #2563eb; background:#fff; color:#2563eb; border-radius:8px; cursor:pointer; font-size:15px;\">Asiakas paikalla<\/button>\n                <button type=\"button\" class=\"kc-access-btn\" data-mode=\"agent\" aria-pressed=\"false\" style=\"padding:9px 14px; border:1px solid #2563eb; background:#fff; color:#2563eb; border-radius:8px; cursor:pointer; font-size:15px;\">V\u00e4litt\u00e4j\u00e4 paikalla<\/button>\n                <button type=\"button\" class=\"kc-access-btn\" data-mode=\"empty\" aria-pressed=\"false\" style=\"padding:9px 14px; border:1px solid #2563eb; background:#fff; color:#2563eb; border-radius:8px; cursor:pointer; font-size:15px;\">Tyhj\u00e4 asunto<\/button>\n              <\/div>\n              <div id=\"kc-form-empty-opts\" style=\"display:none; margin-top:10px; font-size:15px;\">\n                <label style=\"display:block; margin-bottom:6px; cursor:pointer;\"><input type=\"checkbox\" id=\"kc-form-key-office\" style=\"margin-right:8px;\">Avain toimistolla<\/label>\n                <label style=\"display:block; margin-bottom:6px; cursor:pointer;\"><input type=\"checkbox\" id=\"kc-form-other\" style=\"margin-right:8px;\">Muu vaihtoehto<\/label>\n                <input type=\"text\" id=\"kc-form-other-text\" maxlength=\"150\" placeholder=\"esim. v\u00e4litt\u00e4j\u00e4 tuo avaimen kuvaajalle\" style=\"display:none; width:100%; box-sizing:border-box; padding:10px; border:1px solid #ccc; border-radius:8px; color:#111; background:#fff; font-size:15px;\">\n              <\/div>\n              <label for=\"kc-form-notes\" style=\"display:block; font-weight:600; margin:14px 0 4px;\">Lis\u00e4tietoja<\/label>\n              <textarea id=\"kc-form-notes\" rows=\"3\" maxlength=\"500\" placeholder=\"Esimerkiksi onko erillisi\u00e4 piharakennuksia\/autotalli, halutaanko jotain asiaa erikseen korostaa kuvissa (maisemat, takka, huonekorkeus jne.)\" style=\"width:100%; box-sizing:border-box; padding:10px; border:1px solid #ccc; border-radius:8px; color:#111; background:#fff; font-size:15px; resize:vertical;\"><\/textarea>\n              <div id=\"kc-form-error\" role=\"alert\" style=\"color:#991b1b; font-size:14px; margin-top:8px;\"><\/div>\n              <button type=\"button\" id=\"kc-form-submit\" style=\"margin-top:10px; padding:11px 22px; background:#2563eb; color:#fff; border:none; border-radius:8px; cursor:pointer; font-size:16px;\">Varaa aika<\/button>\n            <\/div>\n          <\/div>\n\n          <div id=\"kc-form-done\" style=\"display:none; border:1px solid #16a34a; background:#f0fdf4; border-radius:12px; padding:16px; color:#166534;\">\n            <div style=\"font-size:18px; font-weight:600; margin-bottom:8px;\">Varaus vahvistettu<\/div>\n            <div id=\"kc-form-done-body\" style=\"line-height:1.6; color:#14532d;\"><\/div>\n            <div style=\"margin-top:14px;\">\n              <button type=\"button\" id=\"kc-form-done-bookings\" style=\"padding:8px 14px; border:1px solid #2563eb; background:#fff; color:#2563eb; border-radius:8px; cursor:pointer; margin-right:8px;\">Kaikki varaukseni<\/button>\n              <button type=\"button\" id=\"kc-form-done-new\" style=\"padding:8px 14px; border:1px solid #9ca3af; background:#fff; color:#374151; border-radius:8px; cursor:pointer;\">Tee uusi varaus<\/button>\n            <\/div>\n          <\/div>\n        <\/div>\n\n        <div id=\"kc-chat-chatview\">\n        <div id=\"kc-chat-log\" aria-live=\"polite\" style=\"height:min(60vh, 520px); min-height:320px; overflow-y:auto; border:1px solid #e5e7eb; border-radius:12px; padding:12px; background:#fff; display:flex; flex-direction:column; gap:8px;\"><\/div>\n\n        <div style=\"display:flex; gap:8px; margin-top:10px; align-items:flex-end;\">\n          <textarea id=\"kc-chat-input\" rows=\"2\" maxlength=\"1000\" placeholder=\"Kirjoita viesti\u2026\" style=\"flex:1; padding:8px; border:1px solid #ccc; border-radius:8px; color:#111; background:#fff; resize:vertical; font-size:15px;\"><\/textarea>\n          <button type=\"button\" id=\"kc-chat-send\" style=\"padding:10px 18px; background:#2563eb; color:#fff; border:none; border-radius:8px; cursor:pointer;\">L\u00e4het\u00e4<\/button>\n        <\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <script>\n    (function() {\n      const REST_BASE = \"https:\\\/\\\/www.kuvaukset.com\\\/kuvaukset\\\/wp-json\\\/kuvaukse\\\/v1\";\n\n      const loadingEl = document.getElementById('kc-chat-loading');\n      const loginEl = document.getElementById('kc-chat-login');\n      const mainEl = document.getElementById('kc-chat-main');\n      const logEl = document.getElementById('kc-chat-log');\n      const inputEl = document.getElementById('kc-chat-input');\n      const sendBtn = document.getElementById('kc-chat-send');\n      const resetEl = document.getElementById('kc-chat-reset');\n      const chatViewEl = document.getElementById('kc-chat-chatview');\n      const bookingsViewEl = document.getElementById('kc-chat-bookingsview');\n      const bookingsListEl = document.getElementById('kc-chat-bookings-list');\n      const bookingsNoticeEl = document.getElementById('kc-chat-bookings-notice');\n      const myBookingsBtn = document.getElementById('kc-chat-mybookings');\n      const backBtn = document.getElementById('kc-chat-back');\n      let busy = false;\n      let firstName = '';\n\n      \/\/ Every call is a POST: nothing between here and the server caches those.\n      function api(path, body) {\n        return fetch(REST_BASE + path, {\n          method: 'POST',\n          credentials: 'same-origin',\n          cache: 'no-store',\n          headers: { 'Content-Type': 'application\/json' },\n          body: JSON.stringify(body || {})\n        }).then(function(res) {\n          return res.json().catch(function() { return {}; }).then(function(data) {\n            if (res.status === 401 || res.status === 403) { throw { auth: true }; }\n            if (!res.ok && !data.message) { throw { message: 'Virhe' }; }\n            return data;\n          });\n        });\n      }\n\n      \/\/ Text only, never HTML: a reply can't inject markup into the page.\n      function addBubble(role, text) {\n        const b = document.createElement('div');\n        b.textContent = text;\n        let style = 'max-width:85%; padding:8px 12px; border-radius:12px; line-height:1.4; font-size:15px; white-space:pre-wrap; word-wrap:break-word;';\n        if (role === 'user') style += ' align-self:flex-end; background:#2563eb; color:#fff;';\n        else if (role === 'error') style += ' align-self:flex-start; background:#fef2f2; color:#991b1b; border:1px solid #fecaca;';\n        else style += ' align-self:flex-start; background:#f3f4f6; color:#111827;';\n        b.setAttribute('style', style);\n        b.setAttribute('data-role', role);\n        logEl.appendChild(b);\n        logEl.scrollTop = logEl.scrollHeight;\n        return b;\n      }\n\n      \/\/ ---- Time buttons under a reply ----\n      \/\/ Built with textContent only (never HTML). A tap submits the button's own text as the customer's next\n      \/\/ message, so the assistant treats it like typed text - and still asks for confirmation before booking.\n      function textOf(v, max) { return (typeof v === 'string') ? v.slice(0, max) : ''; }\n\n      \/\/ Old buttons go stale as soon as anything else is sent: fade and disable them (the tapped one stays highlighted).\n      function disableOffers() {\n        logEl.querySelectorAll('.kc-offer button').forEach(function(b) {\n          b.disabled = true;\n          b.style.cursor = 'default';\n          if (b.getAttribute('data-chosen') === '1') {\n            b.style.background = '#2563eb'; b.style.color = '#fff'; b.style.opacity = '1';\n          } else {\n            b.style.opacity = '0.45';\n          }\n        });\n      }\n\n      \/\/ If sending failed, the times are still valid: make the buttons that were usable before usable again.\n      function restoreOffers(buttons) {\n        buttons.forEach(function(b) {\n          b.disabled = false;\n          b.removeAttribute('data-chosen');\n          b.style.cursor = 'pointer'; b.style.opacity = '1'; b.style.background = '#fff'; b.style.color = '#2563eb';\n        });\n      }\n\n      function addOffer(offer) {\n        if (!offer || !Array.isArray(offer.days) || offer.days.length === 0) return;\n        const wrap = node('div', 'align-self:flex-start; max-width:100%; box-sizing:border-box; padding:10px 12px; border:1px solid #bfdbfe; background:#f8fbff; border-radius:12px;');\n        wrap.className = 'kc-offer';\n\n        const service = textOf(offer.service, 80);\n        const minutes = parseInt(offer.duration_minutes, 10);\n        let title = 'Vapaat ajat' + (service ? ': ' + service : '') + (minutes > 0 ? ' (' + minutes + ' min)' : '');\n        wrap.appendChild(node('div', 'font-weight:600; color:#111827; font-size:14px;', title));\n        const where = textOf(offer.address, 200);\n        if (where) wrap.appendChild(node('div', 'color:#4b5563; font-size:13px; margin-top:2px;', 'Kohde: ' + where));\n\n        const cols = node('div', 'display:flex; flex-wrap:wrap; gap:14px; margin-top:10px;');\n        let shown = 0;\n        offer.days.slice(0, 6).forEach(function(day) {\n          if (!day || !Array.isArray(day.times)) return;\n          const col = node('div', 'flex:1 1 140px; min-width:130px; max-width:210px;');\n          col.appendChild(node('div', 'font-weight:600; color:#111827; font-size:14px; margin-bottom:6px;', textOf(day.label, 40)));\n          let n = 0;\n          day.times.slice(0, 8).forEach(function(t) {\n            if (!t || typeof t.message !== 'string' || t.message === '') return;\n            const btn = node('button', 'display:block; width:100%; box-sizing:border-box; padding:8px 10px; margin-bottom:6px; border:1px solid #2563eb; background:#fff; color:#2563eb; border-radius:8px; font-size:14px; cursor:pointer; text-align:center;', textOf(t.start, 5) + ' \u2013 ' + textOf(t.end, 5));\n            btn.type = 'button';\n            btn.addEventListener('click', function() {\n              if (busy) return;\n              btn.setAttribute('data-chosen', '1');\n              submit(textOf(t.message, 200));\n            });\n            col.appendChild(btn);\n            n++;\n          });\n          if (n > 0) { cols.appendChild(col); shown++; }\n        });\n        if (shown === 0) return;\n        wrap.appendChild(cols);\n        wrap.appendChild(node('div', 'color:#6b7280; font-size:13px; margin-top:4px;', (offer.more_days ? 'N\u00e4yt\u00e4n l\u00e4himm\u00e4t p\u00e4iv\u00e4t. ' : '') + 'Ei sopivaa aikaa? Kirjoita toivomasi p\u00e4iv\u00e4 tai kellonaika viestikentt\u00e4\u00e4n.'));\n        logEl.appendChild(wrap);\n        logEl.scrollTop = logEl.scrollHeight;\n      }\n\n      \/\/ Answer buttons (service type, size, yes\/no): a tap sends the button's text as the customer's message.\n      function addChoices(choices) {\n        if (!Array.isArray(choices) || choices.length < 1) return;\n        const wrap = node('div', 'align-self:flex-start; max-width:100%; box-sizing:border-box; display:flex; flex-wrap:wrap; gap:8px; padding:10px 12px; border:1px solid #bfdbfe; background:#f8fbff; border-radius:12px;');\n        wrap.className = 'kc-offer kc-choices';\n        choices.slice(0, 6).forEach(function(label) {\n          const text = textOf(label, 60);\n          if (!text) return;\n          const btn = node('button', 'padding:8px 14px; border:1px solid #2563eb; background:#fff; color:#2563eb; border-radius:8px; font-size:14px; cursor:pointer;', text);\n          btn.type = 'button';\n          btn.addEventListener('click', function() {\n            if (busy) return;\n            btn.setAttribute('data-chosen', '1');\n            submit(text);\n          });\n          wrap.appendChild(btn);\n        });\n        if (!wrap.firstChild) return;\n        logEl.appendChild(wrap);\n        logEl.scrollTop = logEl.scrollHeight;\n      }\n\n      function setTyping(on) {\n        const existing = document.getElementById('kc-chat-typing');\n        if (existing) existing.remove();\n        if (on) {\n          const t = addBubble('assistant', 'Kirjoittaa\u2026');\n          t.id = 'kc-chat-typing';\n        }\n      }\n\n      function greet() {\n        addBubble('assistant', 'Hei' + (firstName ? ' ' + firstName : '') + '! Autan varaamaan kuvausajan. Sy\u00f6t\u00e4 ensin kuvattavan kohteen osoite, niin katsotaan vapaita aikoja. T\u00e4m\u00e4 on beta-versio: jos huomaat jotain parannettavaa, kerro palaute, niin parannamme mielell\u00e4mme k\u00e4ytt\u00f6kokemusta :)');\n      }\n\n      function showLogin() {\n        loadingEl.style.display = 'none';\n        mainEl.style.display = 'none';\n        loginEl.style.display = '';\n      }\n\n      \/\/ ---- \"Kaikki varaukseni\": the customer's own bookings, with cancelling ----\n      \/\/ Everything below builds elements with textContent, never HTML, so nothing from the server can inject markup.\n\n      \/\/ The three views: the booking form (the default), the chat, and the customer's own bookings.\n      \/\/ bookingMode remembers which of the two ways of booking was in use, so \"Takaisin varaamiseen\" returns to it.\n      let bookingMode = 'form';\n      const formViewEl = document.getElementById('kc-chat-formview');\n      const toChatLink = document.getElementById('kc-to-chat');\n      const toFormLink = document.getElementById('kc-to-form');\n      const modeSwitchEl = document.getElementById('kc-mode-switch');\n\n      function showView(name) {\n        if (name === 'form' || name === 'chat') bookingMode = name;\n        const booking = (name === 'form' || name === 'chat');\n        formViewEl.style.display = (name === 'form') ? '' : 'none';\n        chatViewEl.style.display = (name === 'chat') ? '' : 'none';\n        bookingsViewEl.style.display = (name === 'bookings') ? '' : 'none';\n        myBookingsBtn.style.display = booking ? '' : 'none';\n        resetEl.style.display = booking ? '' : 'none';\n        modeSwitchEl.style.display = booking ? '' : 'none';\n        toChatLink.style.display = (name === 'form') ? '' : 'none';\n        toFormLink.style.display = (name === 'chat') ? '' : 'none';\n      }\n\n      toChatLink.addEventListener('click', function(e) { e.preventDefault(); showView('chat'); inputEl.focus(); });\n      toFormLink.addEventListener('click', function(e) { e.preventDefault(); showView('form'); });\n\n      function node(tag, style, text) {\n        const n = document.createElement(tag);\n        if (style) n.setAttribute('style', style);\n        if (text !== undefined) n.textContent = text;\n        return n;\n      }\n\n      function makeButton(label, kind) {\n        const colours = {\n          danger: 'border:1px solid #b91c1c; background:#b91c1c; color:#fff;',\n          outline: 'border:1px solid #b91c1c; background:#fff; color:#b91c1c;',\n          neutral: 'border:1px solid #9ca3af; background:#fff; color:#374151;'\n        };\n        const b = node('button', 'padding:8px 14px; border-radius:6px; cursor:pointer; font-size:14px; margin-right:8px; ' + colours[kind], label);\n        b.type = 'button';\n        return b;\n      }\n\n      function showCancelButton(actions, msg, b) {\n        actions.textContent = '';\n        const btn = makeButton('Peruuta varaus', 'outline');\n        btn.addEventListener('click', function() { msg.textContent = ''; askConfirm(actions, msg, b); });\n        actions.appendChild(btn);\n      }\n\n      \/\/ Cancelling is never one click: the customer must answer this question first.\n      function askConfirm(actions, msg, b) {\n        actions.textContent = '';\n        actions.appendChild(node('div', 'font-weight:600; margin-bottom:8px;', 'Oletko varma, ett\u00e4 haluat peruuttaa t\u00e4m\u00e4n varauksen?'));\n        const yes = makeButton('Kyll\u00e4, peruuta', 'danger');\n        const no = makeButton('Ei, s\u00e4ilyt\u00e4 varaus', 'neutral');\n        no.addEventListener('click', function() { showCancelButton(actions, msg, b); });\n        yes.addEventListener('click', function() {\n          yes.disabled = true;\n          no.disabled = true;\n          msg.textContent = '';\n          api('\/my-bookings\/cancel', { booking_id: b.id }).then(function(data) {\n            if (data.ok) {\n              bookingsNoticeEl.textContent = data.message || 'Varaus on peruttu.';\n              bookingsNoticeEl.style.display = '';\n              loadBookings(true);\n            } else {\n              msg.textContent = data.message || 'Peruutus ei onnistunut.';\n              showCancelButton(actions, msg, b);\n            }\n          }, function(err) {\n            if (err && err.auth) { showLogin(); return; }\n            msg.textContent = 'Yhteysvirhe. Tarkista verkkoyhteytesi ja yrit\u00e4 uudelleen.';\n            showCancelButton(actions, msg, b);\n          });\n        });\n        actions.appendChild(yes);\n        actions.appendChild(no);\n      }\n\n      function bookingCard(b) {\n        const cancelled = (b.status === 'cancelled');\n        const card = node('div', 'border:1px solid #e5e7eb; border-radius:10px; padding:12px; margin-bottom:10px; background:#fff; color:#111827;' + (cancelled ? ' opacity:0.7;' : ''));\n        card.setAttribute('data-booking-id', String(b.id));\n        card.appendChild(node('div', 'font-weight:600;', b.service));\n        card.appendChild(node('div', 'margin-top:2px;', b.label));\n        if (b.address) card.appendChild(node('div', 'margin-top:2px; color:#4b5563;', b.address));\n        if (b.notes) { const nb = node('div', 'margin-top:2px; color:#4b5563;'); notesNodes(nb, b.notes); card.appendChild(nb); }\n        const badge = cancelled ? '#b91c1c' : (b.status_label === 'Vahvistettu' ? '#166534' : '#4b5563');\n        card.appendChild(node('div', 'margin-top:6px; font-size:13px; font-weight:600; color:' + badge + ';', b.status_label));\n        const actions = node('div', 'margin-top:8px;');\n        const msg = node('div', 'margin-top:6px; font-size:14px; color:#b91c1c;');\n        card.appendChild(actions);\n        card.appendChild(msg);\n        if (b.can_cancel) { showCancelButton(actions, msg, b); }\n        else if (b.note) { actions.appendChild(node('div', 'font-size:13px; color:#4b5563;', b.note)); }\n        return card;\n      }\n\n      function loadBookings(keepNotice) {\n        if (!keepNotice) { bookingsNoticeEl.style.display = 'none'; bookingsNoticeEl.textContent = ''; }\n        bookingsListEl.textContent = 'Ladataan\u2026';\n        api('\/my-bookings').then(function(data) {\n          bookingsListEl.textContent = '';\n          const upcoming = data.upcoming || [];\n          const past = data.past || [];\n          if (upcoming.length === 0 && past.length === 0) {\n            bookingsListEl.appendChild(node('p', 'color:#4b5563;', 'Sinulla ei ole viel\u00e4 varauksia.'));\n            return;\n          }\n          bookingsListEl.appendChild(node('h4', 'margin:8px 0; color:#111827; font-size:16px;', 'Tulevat varaukset'));\n          if (upcoming.length === 0) bookingsListEl.appendChild(node('p', 'color:#4b5563;', 'Sinulla ei ole tulevia varauksia.'));\n          upcoming.forEach(function(b) { bookingsListEl.appendChild(bookingCard(b)); });\n          if (past.length > 0) {\n            bookingsListEl.appendChild(node('h4', 'margin:16px 0 8px; color:#111827; font-size:16px;', 'Aiemmat ja peruutetut'));\n            past.forEach(function(b) { bookingsListEl.appendChild(bookingCard(b)); });\n          }\n        }, function(err) {\n          if (err && err.auth) { showLogin(); return; }\n          bookingsListEl.textContent = 'Varausten lataus ep\u00e4onnistui. Yrit\u00e4 uudelleen.';\n        });\n      }\n\n      myBookingsBtn.addEventListener('click', function() {\n        showView('bookings');\n        loadBookings(false);\n      });\n\n      backBtn.addEventListener('click', function() {\n        showView(bookingMode);\n        if (bookingMode === 'chat') inputEl.focus();\n      });\n\n\n      \/\/ ---- The booking form ----\n      \/\/ The customer types an address and picks one of the suggestions (the same address lookup the chat uses),\n      \/\/ sees the free times for exactly that place (driving time included), picks one, adds notes and books.\n      \/\/ Everything is built with textContent, never HTML, so nothing from the server can inject markup.\n      const fService = document.getElementById('kc-form-service');\n      const fServiceWrap = document.getElementById('kc-form-service-wrap');\n      const fIncluded = document.getElementById('kc-form-included');\n      let fIncludedAll = '';\n      const fAddress = document.getElementById('kc-form-address');\n      const fSuggest = document.getElementById('kc-form-suggest');\n      const fAddrMsg = document.getElementById('kc-form-addr-msg');\n      const fTimes = document.getElementById('kc-form-times');\n      const fTimesStatus = document.getElementById('kc-form-times-status');\n      const fNotice = document.getElementById('kc-form-notice');\n      const fDays = document.getElementById('kc-form-days');\n      const fDayNav = document.getElementById('kc-form-daynav');\n      const fPrev = document.getElementById('kc-form-prev');\n      const fNextDay = document.getElementById('kc-form-nextday');\n      const fDayLabel = document.getElementById('kc-form-daylabel');\n      const fDetails = document.getElementById('kc-form-details');\n      const fSummary = document.getElementById('kc-form-summary');\n      const fNotes = document.getElementById('kc-form-notes');\n      const fAccessBtns = Array.prototype.slice.call(document.querySelectorAll('#kc-form-access .kc-access-btn'));\n      const fEmptyOpts = document.getElementById('kc-form-empty-opts');\n      const fKeyOffice = document.getElementById('kc-form-key-office');\n      const fOther = document.getElementById('kc-form-other');\n      const fOtherText = document.getElementById('kc-form-other-text');\n      let fAccessMode = '';\n\n      function setAccess(mode) {\n        fAccessMode = mode;\n        fAccessBtns.forEach(function(b) {\n          const on = b.getAttribute('data-mode') === mode;\n          b.setAttribute('aria-pressed', on ? 'true' : 'false');\n          b.style.background = on ? '#2563eb' : '#fff';\n          b.style.color = on ? '#fff' : '#2563eb';\n        });\n        fEmptyOpts.style.display = mode === 'empty' ? '' : 'none';\n      }\n      fAccessBtns.forEach(function(b) {\n        b.addEventListener('click', function() {\n          const m = b.getAttribute('data-mode');\n          setAccess(fAccessMode === m ? '' : m);   \/\/ a second tap on the same button clears the choice\n        });\n      });\n      fOther.addEventListener('change', function() { fOtherText.style.display = fOther.checked ? '' : 'none'; if (fOther.checked) fOtherText.focus(); });\n\n      \/\/ The same split the server uses: the \"Asuntoon p\u00e4\u00e4sy: ...\" first line, then the customer's own text.\n      function notesNodes(parent, notes) {\n        let access = '', rest = String(notes || '');\n        const m = rest.match(\/^(Asuntoon p\u00e4\u00e4sy: [^\\n]*)\\n?([\\s\\S]*)$\/);\n        if (m) { access = m[1].replace('Asuntoon p\u00e4\u00e4sy: ', ''); rest = m[2].trim(); }\n        if (access) parent.appendChild(node('div', 'white-space:pre-wrap;', 'Asuntoon p\u00e4\u00e4sy: ' + textOf(access, 300)));\n        if (rest) parent.appendChild(node('div', 'white-space:pre-wrap;', 'Lis\u00e4tietoja: ' + textOf(rest, 500)));\n      }\n      const fError = document.getElementById('kc-form-error');\n      const fSubmit = document.getElementById('kc-form-submit');\n      const fCard = document.getElementById('kc-form-card');\n      const fDone = document.getElementById('kc-form-done');\n      const fDoneBody = document.getElementById('kc-form-done-body');\n\n      let fServices = [];\n      let fServiceId = null;\n      let fAddressId = '';\n      let fChosen = null;\n      let fNext = null;\n      let fBusy = false;\n      let suggestTimer = null;\n      let suggestSeq = 0;\n      let timesSeq = 0;\n      let suggestItems = [];\n      let suggestActive = -1;\n      let fDayIndex = 0;\n      let fSkips = 0;          \/\/ empty two-week stretches skipped in a row while looking for the next free day\n\n      function fiDate(ymd) {   \/\/ 2026-10-13 -> 13.10.2026\n        const m = \/^(\\d{4})-(\\d{2})-(\\d{2})$\/.exec(ymd || '');\n        return m ? (parseInt(m[3], 10) + '.' + parseInt(m[2], 10) + '.' + m[1]) : '';\n      }\n\n      function addrMsg(text, isError) {\n        fAddrMsg.textContent = text || '';\n        fAddrMsg.style.color = isError ? '#991b1b' : '#6b7280';\n      }\n\n      function currentService() {\n        for (let i = 0; i < fServices.length; i++) if (fServices[i].id === fServiceId) return fServices[i];\n        return null;\n      }\n\n      function loadFormOptions() {\n        api('\/booking\/options').then(function(data) {\n          fIncludedAll = textOf(data.included, 200);\n          fServices = (data.services || []).map(function(sv) { return { id: parseInt(sv.id, 10), name: textOf(sv.name, 80), minutes: parseInt(sv.duration_minutes, 10) || 0, includes: textOf(sv.includes, 120) }; });\n          fService.textContent = '';\n          fServices.forEach(function(sv) {\n            const o = document.createElement('option');\n            o.value = String(sv.id);\n            o.textContent = sv.name + (sv.minutes > 0 ? ' (' + sv.minutes + ' min)' : '');\n            fService.appendChild(o);\n          });\n          if (fServices.length === 0) {\n            fServiceId = null;\n            fAddress.disabled = true;\n            addrMsg('Palveluita ei ole viel\u00e4 asetettu. Ota yhteytt\u00e4 kuvaajaan.', true);\n            return;\n          }\n          fServiceId = fServices[0].id;\n          fServiceWrap.style.display = (fServices.length > 1) ? '' : 'none';\n          showIncluded();\n        }, function(err) {\n          if (err && err.auth) showLogin();\n          else addrMsg('Lomakkeen lataus ep\u00e4onnistui. P\u00e4ivit\u00e4 sivu.', true);\n        });\n      }\n\n      \/\/ What the chosen service includes, and what every shooting includes.\n      function showIncluded() {\n        let sv = null;\n        for (let i = 0; i < fServices.length; i++) if (fServices[i].id === fServiceId) sv = fServices[i];\n        const parts = [];\n        if (sv && sv.includes) parts.push(sv.includes);\n        if (fIncludedAll) parts.push(fIncludedAll);\n        fIncluded.textContent = parts.join(' ');\n        fIncluded.style.display = parts.length ? '' : 'none';\n      }\n\n      fService.addEventListener('change', function() {\n        fServiceId = parseInt(fService.value, 10) || null;\n        showIncluded();\n        if (fAddressId) loadTimes('');\n      });\n\n      \/\/ -- address suggestions --\n      function hideSuggest() {\n        fSuggest.style.display = 'none';\n        fSuggest.textContent = '';\n        suggestItems = [];\n        suggestActive = -1;\n        fAddress.setAttribute('aria-expanded', 'false');\n      }\n\n      function markActive(i) {\n        suggestActive = i;\n        Array.prototype.forEach.call(fSuggest.querySelectorAll('[role=option]'), function(el, idx) {\n          el.style.background = (idx === i) ? '#eff6ff' : '#fff';\n          el.setAttribute('aria-selected', idx === i ? 'true' : 'false');\n        });\n      }\n\n      function renderSuggest(candidates, message) {\n        fSuggest.textContent = '';\n        suggestItems = candidates;\n        suggestActive = -1;\n        candidates.forEach(function(c, i) {\n          const el = node('div', 'padding:10px 12px; cursor:pointer; font-size:15px; color:#111827; background:#fff; border-bottom:1px solid #f3f4f6;', textOf(c.label, 200));\n          el.setAttribute('role', 'option');\n          el.setAttribute('data-address-id', textOf(c.address_id, 30));\n          el.addEventListener('mousedown', function(e) { e.preventDefault(); });   \/\/ keep the focus in the field\n          el.addEventListener('click', function() { chooseAddress(c); });\n          fSuggest.appendChild(el);\n        });\n        if (message) fSuggest.appendChild(node('div', 'padding:10px 12px; font-size:14px; color:#6b7280;', message));\n        const show = candidates.length > 0 || !!message;\n        fSuggest.style.display = show ? '' : 'none';\n        fAddress.setAttribute('aria-expanded', show ? 'true' : 'false');\n      }\n\n      function lookupAddress(q) {\n        const seq = ++suggestSeq;\n        api('\/booking\/address', { q: q }).then(function(data) {\n          if (seq !== suggestSeq) return;   \/\/ a newer search has started: this answer is stale\n          if (!data.ok) { renderSuggest([], data.message || 'Osoitehaku ei onnistunut.'); return; }\n          const list = Array.isArray(data.candidates) ? data.candidates : [];\n          renderSuggest(list, list.length ? '' : 'Osoitetta ei l\u00f6ytynyt. Kirjoita katuosoite, talon numero ja kaupunki, esimerkiksi Kuninkaankatu 24 B 61, Tampere.');\n        }, function(err) {\n          if (seq !== suggestSeq) return;\n          if (err && err.auth) showLogin();\n          else renderSuggest([], 'Yhteysvirhe. Tarkista verkkoyhteytesi ja yrit\u00e4 uudelleen.');\n        });\n      }\n\n      function chooseAddress(c) {\n        suggestSeq++;                    \/\/ forget any search still on its way\n        fAddressId = textOf(c.address_id, 30);\n        fAddress.value = textOf(c.label, 200);\n        hideSuggest();\n        addrMsg('');\n        loadTimes('');\n      }\n\n      fAddress.addEventListener('input', function() {\n        fAddressId = '';                 \/\/ typing again means the choice no longer stands\n        clearTimes();\n        clearTimeout(suggestTimer);\n        suggestSeq++;\n        const q = fAddress.value.trim();\n        if (q.length < 3) { hideSuggest(); addrMsg(''); return; }\n        addrMsg('Valitse osoite ehdotuksista.');\n        suggestTimer = setTimeout(function() { lookupAddress(q); }, 350);\n      });\n\n      fAddress.addEventListener('keydown', function(e) {\n        if (fSuggest.style.display === 'none' || suggestItems.length === 0) return;\n        if (e.key === 'ArrowDown') { e.preventDefault(); markActive((suggestActive + 1) % suggestItems.length); }\n        else if (e.key === 'ArrowUp') { e.preventDefault(); markActive((suggestActive - 1 + suggestItems.length) % suggestItems.length); }\n        else if (e.key === 'Enter') { e.preventDefault(); chooseAddress(suggestItems[suggestActive >= 0 ? suggestActive : 0]); }\n        else if (e.key === 'Escape') { hideSuggest(); }\n      });\n\n      fAddress.addEventListener('blur', function() { setTimeout(hideSuggest, 150); });\n\n      \/\/ -- free times --\n      function clearTimes() {\n        timesSeq++;\n        fChosen = null;\n        fNext = null;\n        fDays.textContent = '';\n        fTimesStatus.textContent = '';\n        fNotice.textContent = '';\n        fTimes.style.display = 'none';\n        fDayNav.style.display = 'none';\n        fDayIndex = 0;\n        fSkips = 0;\n        fDetails.style.display = 'none';\n        fError.textContent = '';\n      }\n\n      const TIME_BTN = 'padding:9px 14px; margin:0 8px 8px 0; border:1px solid #2563eb; background:#fff; color:#2563eb; border-radius:8px; font-size:15px; cursor:pointer;';\n\n      function chooseTime(day, t, btn) {\n        Array.prototype.forEach.call(fDays.querySelectorAll('button'), function(b) { b.setAttribute('style', TIME_BTN); b.removeAttribute('aria-pressed'); });\n        btn.setAttribute('style', TIME_BTN + ' background:#2563eb; color:#fff;');\n        btn.setAttribute('aria-pressed', 'true');\n        fChosen = { date: day.date, start: t.start, end: t.end };\n        fNotice.textContent = '';\n        const sv = currentService();\n        fSummary.textContent = '';\n        fSummary.appendChild(node('div', 'font-weight:600;', textOf(day.weekday, 20) + ' ' + fiDate(day.date) + ' klo ' + t.start + '\u2013' + t.end));\n        if (sv) fSummary.appendChild(node('div', '', 'Palvelu: ' + sv.name + (sv.minutes > 0 ? ' (' + sv.minutes + ' min)' : '')));\n        fSummary.appendChild(node('div', '', 'Osoite: ' + fAddress.value));\n        fError.textContent = '';\n        fDetails.style.display = '';\n        fSubmit.disabled = false;\n        if (fDetails.scrollIntoView) fDetails.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n      }\n\n      \/\/ \"tiistai 6.10.\" for the arrows row (the server's short label is \"ti 6.10.\").\n      function dayTitle(day) {\n        const rest = textOf(day.label, 40).replace(\/^\\S+\\s*\/, '');\n        return textOf(day.weekday, 20) + (rest ? ' ' + rest : '');\n      }\n\n      function dayBoxes() { return Array.prototype.slice.call(fDays.querySelectorAll('.kc-form-day')); }\n\n      \/\/ One day is shown at a time; the arrows move between the days that have free times.\n      \/\/ Past the last day we have, the right arrow fetches the next two weeks (while there are any).\n      function showDay(i) {\n        const boxes = dayBoxes();\n        if (boxes.length === 0) { fDayNav.style.display = 'none'; return; }\n        i = Math.max(0, Math.min(i, boxes.length - 1));\n        fDayIndex = i;\n        boxes.forEach(function(b, idx) { b.style.display = (idx === i) ? '' : 'none'; });\n        fDayLabel.textContent = boxes[i].getAttribute('data-title') || '';\n        fDayNav.style.display = 'flex';\n        setNavDisabled(false);\n      }\n\n      function setNavDisabled(loading) {\n        const n = dayBoxes().length;\n        fPrev.disabled = loading || fDayIndex <= 0;\n        fNextDay.disabled = loading || (fDayIndex >= n - 1 && !fNext);\n        fPrev.style.opacity = fPrev.disabled ? '0.35' : '1';\n        fNextDay.style.opacity = fNextDay.disabled ? '0.35' : '1';\n      }\n\n      fPrev.addEventListener('click', function() { showDay(fDayIndex - 1); });\n      fNextDay.addEventListener('click', function() {\n        const n = dayBoxes().length;\n        if (fDayIndex < n - 1) showDay(fDayIndex + 1);\n        else if (fNext) loadTimes(fNext, n);\n      });\n\n      function renderDays(days) {\n        days.forEach(function(day) {\n          if (!day || !Array.isArray(day.times) || day.times.length === 0) return;\n          const box = node('div', 'margin-bottom:8px;');\n          box.className = 'kc-form-day';\n          box.setAttribute('data-title', dayTitle(day));\n          const row = node('div', 'display:flex; flex-wrap:wrap;');\n          day.times.forEach(function(t) {\n            const btn = node('button', TIME_BTN, textOf(t.start, 5));\n            btn.type = 'button';\n            btn.title = textOf(t.start, 5) + '\u2013' + textOf(t.end, 5);\n            btn.addEventListener('click', function() { chooseTime(day, { start: textOf(t.start, 5), end: textOf(t.end, 5) }, btn); });\n            row.appendChild(btn);\n          });\n          box.appendChild(row);\n          fDays.appendChild(box);\n        });\n      }\n\n      function formProblem(data) {\n        \/\/ The address went stale: ask for it again. Anything else is just shown.\n        if (data.error === 'unknown_address_id' || data.error === 'address_required') {\n          fAddressId = '';\n          clearTimes();\n          addrMsg(data.message || 'Valitse osoite uudelleen.', true);\n        } else {\n          fTimesStatus.textContent = data.message || 'Aikojen haku ei onnistunut.';\n        }\n      }\n\n      \/\/ fromDate '' = from today; otherwise adds the stretch after the days already shown. focusIndex = the day to show afterwards.\n      function loadTimes(fromDate, focusIndex) {\n        if (!fServiceId || !fAddressId) return;\n        const seq = ++timesSeq;\n        const more = !!fromDate;\n        if (!more) { fDays.textContent = ''; fChosen = null; fDetails.style.display = 'none'; fDayIndex = 0; fSkips = 0; fNext = null; }\n        const focus = (typeof focusIndex === 'number') ? focusIndex : 0;\n        fTimes.style.display = '';\n        fTimesStatus.textContent = 'Haetaan vapaita aikoja\u2026';\n        setNavDisabled(true);\n        api('\/booking\/times', { service_id: fServiceId, address_id: fAddressId, from_date: fromDate || '' }).then(function(data) {\n          if (seq !== timesSeq) return;   \/\/ the customer has changed the address or service meanwhile\n          if (!data.ok) { formProblem(data); setNavDisabled(false); return; }\n          renderDays(Array.isArray(data.days) ? data.days : []);\n          fNext = data.next_from || null;\n          const total = dayBoxes().length;\n          if (total <= focus && fNext && fSkips < 8) {   \/\/ nothing free in this stretch: look at the next one\n            fSkips++;\n            loadTimes(fNext, focus);\n            return;\n          }\n          fSkips = 0;\n          if (total === 0) {\n            fTimesStatus.textContent = 'Ei vapaita aikoja.';\n            fDayNav.style.display = 'none';\n            return;\n          }\n          fTimesStatus.textContent = '';\n          showDay(total > focus ? focus : total - 1);\n        }, function(err) {\n          if (seq !== timesSeq) return;\n          setNavDisabled(false);\n          if (err && err.auth) { showLogin(); return; }\n          fTimesStatus.textContent = 'Yhteysvirhe. Tarkista verkkoyhteytesi ja yrit\u00e4 uudelleen.';\n        });\n      }\n\n      \/\/ -- booking --\n      function accessText(a) {\n        if (!a) return '';\n        if (a.mode === 'customer') return 'Asuntoon p\u00e4\u00e4sy: Asiakas paikalla';\n        if (a.mode === 'agent') return 'Asuntoon p\u00e4\u00e4sy: V\u00e4litt\u00e4j\u00e4 paikalla';\n        const parts = [];\n        if (a.key_office) parts.push('Avain toimistolla');\n        if (a.other) parts.push('Muu vaihtoehto' + (a.other_text ? ': ' + a.other_text : ''));\n        return 'Asuntoon p\u00e4\u00e4sy: Tyhj\u00e4 asunto' + (parts.length ? ' \u2013 ' + parts.join(', ') : '');\n      }\n\n      function showDone(b) {\n        fCard.style.display = 'none';\n        fDone.style.display = '';\n        fDoneBody.textContent = '';\n        fDoneBody.appendChild(node('div', 'font-weight:600;', textOf(b.weekday, 20) + ' ' + fiDate(b.date) + ' klo ' + textOf(b.start, 5) + '\u2013' + textOf(b.end, 5)));\n        fDoneBody.appendChild(node('div', '', textOf(b.service, 80)));\n        fDoneBody.appendChild(node('div', '', textOf(b.address, 255)));\n        if (b.notes) notesNodes(fDoneBody, b.notes);\n        if (b.email) fDoneBody.appendChild(node('div', 'margin-top:6px;', 'Vahvistus on l\u00e4hetetty osoitteeseen ' + textOf(b.email, 255) + '.'));\n      }\n\n      function resetForm() {\n        clearTimeout(suggestTimer);\n        suggestSeq++;\n        fAddressId = '';\n        fAddress.value = '';\n        fNotes.value = '';\n        setAccess('');\n        fKeyOffice.checked = false;\n        fOther.checked = false;\n        fOtherText.value = '';\n        fOtherText.style.display = 'none';\n        hideSuggest();\n        clearTimes();\n        addrMsg('');\n        fCard.style.display = '';\n        fDone.style.display = 'none';\n        fAddress.focus();\n      }\n\n      fSubmit.addEventListener('click', function() {\n        if (fBusy || !fChosen || !fAddressId || !fServiceId) return;\n        fBusy = true;\n        fSubmit.disabled = true;\n        fError.textContent = '';\n        const sentNotes = fNotes.value.trim();\n        const sentAccess = fAccessMode ? { mode: fAccessMode, key_office: fAccessMode === 'empty' && fKeyOffice.checked, other: fAccessMode === 'empty' && fOther.checked, other_text: fOtherText.value.trim() } : null;\n        api('\/booking\/create', { service_id: fServiceId, address_id: fAddressId, start: fChosen.date + ' ' + fChosen.start, notes: sentNotes, access: sentAccess }).then(function(data) {\n          if (data.ok) { const done = data.booking || {}; if (sentAccess || sentNotes) done.notes = accessText(sentAccess) + (accessText(sentAccess) && sentNotes ? '\\n' : '') + sentNotes; showDone(done); return; }\n          fError.textContent = data.message || 'Varaus ei onnistunut. Yrit\u00e4 uudelleen.';\n          if (data.error === 'slot_not_available' || data.error === 'too_soon') {   \/\/ the list was stale: show the real one, and say why\n            fError.textContent = '';\n            loadTimes('');\n            fNotice.textContent = data.message || 'Valitse toinen aika.';\n          }\n          else if (data.error === 'unknown_address_id' || data.error === 'address_required') { formProblem(data); }\n        }, function(err) {\n          if (err && err.auth) showLogin();\n          else fError.textContent = 'Yhteysvirhe. Tarkista verkkoyhteytesi ja yrit\u00e4 uudelleen.';\n        }).then(function() {\n          fBusy = false;\n          fSubmit.disabled = false;\n        });\n      });\n\n      document.getElementById('kc-form-done-new').addEventListener('click', resetForm);\n      document.getElementById('kc-form-done-bookings').addEventListener('click', function() { myBookingsBtn.click(); });\n\n      \/\/ Typed messages and button taps both end up here.\n      function submit(text) {\n        if (busy) return;\n        text = (text || '').trim();\n        if (!text) return;\n\n        busy = true;\n        sendBtn.disabled = true;\n        const usable = Array.prototype.slice.call(logEl.querySelectorAll('.kc-offer button:not(:disabled)'));\n        disableOffers();\n        addBubble('user', text);\n        setTyping(true);\n\n        api('\/chat', { message: text }).then(function(data) {\n          setTyping(false);\n          if (data.ok) { addBubble('assistant', data.reply); addOffer(data.offer); addChoices(data.choices); }\n          else { restoreOffers(usable); addBubble('error', data.message || 'Jokin meni pieleen. Yrit\u00e4 uudelleen.'); }\n        }, function(err) {\n          setTyping(false);\n          if (err && err.auth) { showLogin(); }\n          else { restoreOffers(usable); addBubble('error', 'Yhteysvirhe. Tarkista verkkoyhteytesi ja yrit\u00e4 uudelleen.'); }\n        }).then(function() {\n          busy = false;\n          sendBtn.disabled = false;\n          inputEl.focus();\n        });\n      }\n\n      function send() {\n        if (busy) return;\n        const text = inputEl.value.trim();\n        if (!text) return;\n        inputEl.value = '';\n        submit(text);\n      }\n\n      sendBtn.addEventListener('click', send);\n\n      inputEl.addEventListener('keydown', function(e) {\n        if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {\n          e.preventDefault();\n          send();\n        }\n      });\n\n      document.getElementById('kc-chat-logout').addEventListener('click', function(e) {\n        e.preventDefault();\n        \/\/ The server ends the session and clears the cookie; either way the page then asks to log in again.\n        api('\/auth\/logout').then(showLogin, showLogin);\n      });\n\n      resetEl.addEventListener('click', function(e) {\n        e.preventDefault();\n        if (bookingMode === 'form') { resetForm(); return; }   \/\/ \"Aloita alusta\" in the form clears the form\n        if (busy) return;\n        api('\/chat\/reset').then(function() {\n          logEl.innerHTML = '';\n          greet();\n        }, function() {\n          addBubble('error', 'Uuden keskustelun aloitus ei onnistunut. P\u00e4ivit\u00e4 sivu.');\n        });\n      });\n\n      api('\/chat\/history').then(function(data) {\n        loadingEl.style.display = 'none';\n        loginEl.style.display = 'none';\n        mainEl.style.display = '';\n        firstName = (data.name || '').split(' ')[0];\n        const msgs = data.messages || [];\n        if (msgs.length === 0) { greet(); }\n        msgs.forEach(function(m) { addBubble(m.role, m.text); });\n        addOffer(data.offer);   \/\/ the latest time buttons, if the page was reloaded meanwhile\n        addChoices(data.choices);\n        showView('form');       \/\/ the simple booking form is what customers see first\n        loadFormOptions();\n      }, function(err) {\n        if (err && err.auth) { showLogin(); }\n        else {\n          loadingEl.style.display = '';\n          loadingEl.textContent = 'Chatin lataus ep\u00e4onnistui. P\u00e4ivit\u00e4 sivu.';\n        }\n      });\n    })();\n    <\/script>\n    \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":{"footnotes":""},"class_list":["post-1738","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.kuvaukset.com\/kuvaukset\/wp-json\/wp\/v2\/pages\/1738","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.kuvaukset.com\/kuvaukset\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.kuvaukset.com\/kuvaukset\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.kuvaukset.com\/kuvaukset\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.kuvaukset.com\/kuvaukset\/wp-json\/wp\/v2\/comments?post=1738"}],"version-history":[{"count":1,"href":"https:\/\/www.kuvaukset.com\/kuvaukset\/wp-json\/wp\/v2\/pages\/1738\/revisions"}],"predecessor-version":[{"id":1740,"href":"https:\/\/www.kuvaukset.com\/kuvaukset\/wp-json\/wp\/v2\/pages\/1738\/revisions\/1740"}],"wp:attachment":[{"href":"https:\/\/www.kuvaukset.com\/kuvaukset\/wp-json\/wp\/v2\/media?parent=1738"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}