{"id":161,"date":"2026-09-22T04:27:46","date_gmt":"2026-09-22T04:27:46","guid":{"rendered":"https:\/\/gwkitchen.com\/wp2026\/?page_id=161"},"modified":"2026-09-24T01:11:15","modified_gmt":"2026-09-24T01:11:15","slug":"customer-job-status","status":"publish","type":"page","link":"https:\/\/gwkitchen.com\/wp2026\/?page_id=161","title":{"rendered":"Customer Job Status"},"content":{"rendered":"\n<!-- GW Kitchen & Bath \u2014 Customer Job Status V5 (8-character lookup; show-code checkbox; read-only project details). Paste this entire block into ONE WordPress Custom HTML block. -->\n<section id=\"gw-customer-portal\" aria-labelledby=\"gw-customer-heading\">\n  <style>\n    #gw-customer-portal{box-sizing:border-box;max-width:760px;margin:28px auto;padding:24px;border:1px solid #d7e2ef;border-radius:14px;background:#fff;color:#172b45;font:inherit}\n    #gw-customer-portal *{box-sizing:border-box}\n    #gw-customer-portal h2{margin:0 0 10px;font-size:1.65rem;color:#153f70}\n    #gw-customer-portal p{margin:0 0 16px}\n    #gw-customer-portal label{display:block;margin:18px 0 6px;font-weight:650}\n    #gw-customer-portal input{display:block;width:100%;max-width:460px;padding:12px;border:1px solid #9fb2c9;border-radius:7px;background:#fff;color:#172b45;font-size:1rem}\n    #gw-customer-portal .gw-show-code{display:flex;align-items:center;gap:9px;margin:10px 0 0;font-weight:400;cursor:pointer}\n    #gw-customer-portal .gw-show-code input{display:inline-block;width:18px;height:18px;max-width:none;margin:0;flex:0 0 auto}\n    #gw-customer-portal input:focus-visible,#gw-customer-portal button:focus-visible{outline:3px solid #70a8e5;outline-offset:2px}\n    #gw-customer-portal button{margin:20px 0 8px;padding:12px 20px;background:#17599b;border:0;border-radius:7px;color:white;font-size:1rem;cursor:pointer}\n    #gw-customer-portal button:disabled{opacity:.65;cursor:wait}\n    #gw-customer-message{min-height:1.5em;margin-top:10px!important}\n    #gw-customer-message[data-error=\"true\"]{color:#a02525}\n    #gw-customer-results{margin-top:18px;border-top:1px solid #d7e2ef;padding-top:18px}\n    #gw-customer-results h3{margin:18px 0 8px;color:#153f70}\n    #gw-customer-results .gw-photo-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:14px}\n    #gw-customer-results figure{margin:0;border:1px solid #d7e2ef;border-radius:8px;overflow:hidden}\n    #gw-customer-results img{display:block;width:100%;height:auto;max-height:320px;object-fit:contain;background:#f4f7fb}\n    #gw-customer-results figcaption{padding:8px;font-weight:600}\n  <\/style>\n\n  <h2 id=\"gw-customer-heading\">Check Your Job Status<\/h2>\n  <p>Enter the Job ID and 8-character letter-and-number lookup code provided by GW Kitchen &#038; Bath.<\/p>\n  <form id=\"gw-customer-lookup-form\" autocomplete=\"off\">\n    <label for=\"gw-customer-job-id\">Job ID<\/label>\n    <input id=\"gw-customer-job-id\" name=\"gw_project_reference\" type=\"text\" placeholder=\"GW-0000-000\" maxlength=\"11\" spellcheck=\"false\" autocapitalize=\"characters\" autocomplete=\"off\" inputmode=\"text\" required>\n    <label for=\"gw-customer-code\">Private Lookup Code<\/label>\n    <input id=\"gw-customer-code\" name=\"gw_private_project_lookup\" type=\"password\" minlength=\"8\" maxlength=\"64\" autocomplete=\"new-password\" spellcheck=\"false\" required>\n    <label class=\"gw-show-code\" for=\"gw-customer-show-code\"><input id=\"gw-customer-show-code\" type=\"checkbox\" autocomplete=\"off\"> Show lookup code while typing<\/label>\n    <button id=\"gw-customer-submit\" type=\"submit\">Check Status<\/button>\n  <\/form>\n  <p id=\"gw-customer-message\" role=\"status\" aria-live=\"polite\"><\/p>\n  <section id=\"gw-customer-results\" aria-label=\"Job information\" hidden><\/section>\n\n  <script>\n    (function () {\n      \"use strict\";\n      const root = document.getElementById(\"gw-customer-portal\");\n      if (!root || root.dataset.initialized === \"true\") return;\n      root.dataset.initialized = \"true\";\n      const endpoint = \"https:\/\/tepzhgerltbdbhcgksog.supabase.co\/functions\/v1\/gw-customer-lookup\";\n      const trustedOrigin = \"https:\/\/tepzhgerltbdbhcgksog.supabase.co\";\n      const form = root.querySelector(\"#gw-customer-lookup-form\");\n      const jobInput = root.querySelector(\"#gw-customer-job-id\");\n      const codeInput = root.querySelector(\"#gw-customer-code\");\n      const showCode = root.querySelector(\"#gw-customer-show-code\");\n      showCode.checked = false;\n      codeInput.type = \"password\";\n      showCode.addEventListener(\"change\", function () {\n        codeInput.type = showCode.checked ? \"text\" : \"password\";\n      });\n      const submit = root.querySelector(\"#gw-customer-submit\");\n      const message = root.querySelector(\"#gw-customer-message\");\n      const results = root.querySelector(\"#gw-customer-results\");\n      let busy = false;\n\n      function setMessage(text, isError) {\n        message.textContent = text;\n        message.dataset.error = isError ? \"true\" : \"false\";\n      }\n      function addLine(label, value) {\n        const p = document.createElement(\"p\");\n        const strong = document.createElement(\"strong\");\n        strong.textContent = label + \": \";\n        p.append(strong, document.createTextNode(value));\n        results.appendChild(p);\n      }\n      function safePhotoUrl(raw) {\n        if (typeof raw !== \"string\") return null;\n        try {\n          const url = new URL(raw, trustedOrigin);\n          if (url.protocol !== \"https:\" || url.origin !== trustedOrigin ||\n              !url.pathname.startsWith(\"\/storage\/v1\/object\/sign\/job-photos\/\")) return null;\n          return url.href;\n        } catch { return null; }\n      }\n      function showJob(data, requestedId) {\n        if (!data || typeof data !== \"object\" || Array.isArray(data) ||\n            data.jobId !== requestedId || typeof data.status !== \"string\" ||\n            !Array.isArray(data.photos)) throw new Error(\"Unexpected response\");\n        results.replaceChildren();\n        addLine(\"Job ID\", data.jobId);\n        addLine(\"Status\", data.status || \"Not available\");\n        addLine(\"Completion date\", \/^\\d{4}-\\d{2}-\\d{2}$\/.test(data.completionDate || \"\")\n          ? data.completionDate : \"Not available yet\");\n        \/\/ Customer-facing specifications only; never render internal measurement fields.\n        \/\/ A null details object means an employee has not saved project details yet.\n        const details = data.projectDetails;\n        if (details !== null && details !== undefined) {\n          if (typeof details !== \"object\" || Array.isArray(details))\n            throw new Error(\"Unexpected project details\");\n          const heading = document.createElement(\"h3\");\n          heading.textContent = \"Your Project Details\";\n          results.appendChild(heading);\n          const specs = [\n            [\"Material\", details.material],\n            [\"Edge\", details.edge],\n            [\"Sink type\", details.sinkType],\n            [\"Sink model\", details.sinkModel],\n            [\"Backsplash\", details.backsplash],\n            [\"Stove\", details.stoveType],\n            [\"Faucet\", details.faucet],\n          ];\n          for (const [label, raw] of specs) {\n            const value = typeof raw === \"string\" ? raw.trim() : \"\";\n            addLine(label, value || \"Not specified yet\");\n          }\n          const seam = details.seamRequired;\n          addLine(\"Seam required\", seam === true ? \"Yes\" : seam === false ? \"No\" : \"Not specified yet\");\n        }\n        const safePhotos = data.photos.slice(0, 100).filter((p) =>\n          p && (p.type === \"Before\" || p.type === \"After\") && safePhotoUrl(p.url));\n        if (safePhotos.length) {\n          const h3 = document.createElement(\"h3\");\n          h3.textContent = \"Approved project photos\";\n          results.appendChild(h3);\n          const grid = document.createElement(\"div\");\n          grid.className = \"gw-photo-grid\";\n          safePhotos.forEach((p) => {\n            const figure = document.createElement(\"figure\");\n            const img = document.createElement(\"img\");\n            img.src = safePhotoUrl(p.url);\n            img.alt = p.type + \" project photo\";\n            img.loading = \"lazy\";\n            img.referrerPolicy = \"no-referrer\";\n            const caption = document.createElement(\"figcaption\");\n            caption.textContent = p.type;\n            figure.append(img, caption);\n            grid.appendChild(figure);\n          });\n          results.appendChild(grid);\n        }\n        results.hidden = false;\n      }\n\n      form.addEventListener(\"submit\", async function (event) {\n        event.preventDefault();\n        if (busy) return;\n        results.replaceChildren();\n        results.hidden = true;\n        const jobId = jobInput.value.trim().toUpperCase();\n        const code = codeInput.value;\n        if (!\/^GW-[0-9]{4}-[0-9]{3}$\/.test(jobId) ||\n            !( \/^(?=.{8}$)(?=.*[A-Za-z])(?=.*[0-9])[A-Za-z0-9]{8}$\/.test(code) || \/^[A-Za-z0-9!@#$%^&*._+\\-]{16,64}$\/.test(code) )) {\n          codeInput.value = \"\";\n          setMessage(\"Please check your Job ID and private lookup code.\", true);\n          return;\n        }\n        busy = true;\n        submit.disabled = true;\n        setMessage(\"Checking your job status\u2026\", false);\n        const controller = new AbortController();\n        const timeout = setTimeout(() => controller.abort(), 20000);\n        try {\n          const response = await fetch(endpoint, {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application\/json\" },\n            body: JSON.stringify({ jobId, code }),\n            mode: \"cors\",\n            credentials: \"omit\",\n            cache: \"no-store\",\n            referrerPolicy: \"no-referrer\",\n            signal: controller.signal\n          });\n          if (!response.ok) {\n            if (response.status === 401) setMessage(\"Job ID or lookup code is incorrect.\", true);\n            else if (response.status === 429) setMessage(\"Too many attempts. Please try again later.\", true);\n            else setMessage(\"Lookup is unavailable right now. Please try again later.\", true);\n            return;\n          }\n          const data = await response.json();\n          showJob(data, jobId);\n          setMessage(\"Job status loaded.\", false);\n        } catch (_error) {\n          results.replaceChildren();\n          results.hidden = true;\n          setMessage(\"Unable to load job status. Please try again later.\", true);\n        } finally {\n          clearTimeout(timeout);\n          codeInput.value = \"\";\n          busy = false;\n          submit.disabled = false;\n        }\n      });\n    })();\n  <\/script>\n<\/section>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Check Your Job Status Enter the Job ID and 8-character letter-and-number lookup code provided by GW Kitchen &#038; Bath. Job [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","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":"","ast-disable-related-posts":"","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-161","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/gwkitchen.com\/wp2026\/index.php?rest_route=\/wp\/v2\/pages\/161","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/gwkitchen.com\/wp2026\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/gwkitchen.com\/wp2026\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/gwkitchen.com\/wp2026\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/gwkitchen.com\/wp2026\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=161"}],"version-history":[{"count":12,"href":"https:\/\/gwkitchen.com\/wp2026\/index.php?rest_route=\/wp\/v2\/pages\/161\/revisions"}],"predecessor-version":[{"id":191,"href":"https:\/\/gwkitchen.com\/wp2026\/index.php?rest_route=\/wp\/v2\/pages\/161\/revisions\/191"}],"wp:attachment":[{"href":"https:\/\/gwkitchen.com\/wp2026\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=161"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}