{"id":85,"date":"2026-09-22T04:32:46","date_gmt":"2026-09-22T04:32:46","guid":{"rendered":"https:\/\/gwkitchen.com\/wp2026\/?page_id=85"},"modified":"2026-10-09T04:10:25","modified_gmt":"2026-10-09T04:10:25","slug":"gw-employee-portal","status":"publish","type":"page","link":"https:\/\/gwkitchen.com\/wp2026\/?page_id=85","title":{"rendered":"Employee Portal"},"content":{"rendered":"\n<style data-wp-block-html=\"css\">\n\/* ========================================\n   GW KITCHEN & BATH\n   EMPLOYEE PORTAL \u2014 RESPONSIVE CSS V2\n   ======================================== *\/\n\n\/* Main portal container *\/\n\n.gw-login {\n  box-sizing: border-box;\n  width: 100%;\n  max-width: 420px;\n  margin: 30px auto;\n  padding: 28px;\n  background: #f5f9ff;\n  border: 1px solid #d5e4f5;\n  border-radius: 14px;\n  color: #243447;\n  font-size: 16px;\n  line-height: 1.5;\n}\n\n\/* Expand dashboard after login *\/\n\n.gw-login:has(#gw-job-search:not([hidden])) {\n  max-width: 960px;\n}\n\n\/* Prevent elements from overflowing *\/\n\n.gw-login,\n.gw-login * {\n  box-sizing: border-box;\n}\n\n.gw-login * {\n  min-width: 0;\n}\n\n.gw-login [hidden] {\n  display: none !important;\n}\n\n\/* Headings *\/\n\n.gw-login h2 {\n  color: #174a80;\n  text-align: center;\n  font-size: 25px;\n  margin: 12px 0 22px;\n}\n\n.gw-login h3 {\n  color: #174a80;\n  font-size: 21px;\n  margin: 12px 0 20px;\n}\n\n.gw-login h4 {\n  color: #245787;\n  font-size: 18px;\n  margin: 22px 0 12px;\n}\n\n\/* Form labels *\/\n\n.gw-login label {\n  display: block;\n  margin: 16px 0 7px;\n  font-weight: 600;\n  font-size: 15px;\n}\n\n\/* Text fields, dropdowns and notes *\/\n\n.gw-login input:not([type=\"checkbox\"]),\n.gw-login select,\n.gw-login textarea {\n  display: block;\n  width: 100%;\n  max-width: 100%;\n  min-height: 48px;\n  padding: 12px 14px;\n  border: 1px solid #aabbd0;\n  border-radius: 8px;\n  background: #ffffff;\n  color: #243447;\n  font-family: inherit;\n  font-size: 16px;\n  line-height: 1.4;\n}\n\n.gw-login textarea {\n  min-height: 100px;\n  resize: vertical;\n}\n\n.gw-login input[type=\"file\"] {\n  height: auto;\n  padding: 12px;\n}\n\n\/* Checkbox layout *\/\n\n.gw-login label.gw-remember,\n.gw-login label:has(> input[type=\"checkbox\"]) {\n  display: flex;\n  align-items: flex-start;\n  gap: 12px;\n  margin: 18px 0;\n  font-size: 16px;\n  font-weight: 500;\n  line-height: 1.5;\n  cursor: pointer;\n}\n\n.gw-login input[type=\"checkbox\"] {\n  display: inline-block;\n  flex: 0 0 22px;\n  width: 22px;\n  height: 22px;\n  min-width: 22px;\n  margin: 1px 0 0;\n  padding: 0;\n  accent-color: #1769aa;\n  cursor: pointer;\n}\n\n\/* Buttons *\/\n\n.gw-login button {\n  display: block;\n  width: 100%;\n  max-width: 100%;\n  min-height: 50px;\n  margin-top: 16px;\n  padding: 12px 16px;\n  background: #1769aa;\n  color: #ffffff;\n  border: 0;\n  border-radius: 9px;\n  font-family: inherit;\n  font-size: 16px;\n  font-weight: 600;\n  line-height: 1.35;\n  text-align: center;\n  white-space: normal;\n  overflow-wrap: anywhere;\n  cursor: pointer;\n  touch-action: manipulation;\n}\n\n.gw-login button:hover:not(:disabled) {\n  background: #12568d;\n}\n\n.gw-login button:disabled {\n  opacity: 0.55;\n  cursor: not-allowed;\n}\n\n\/* Keyboard focus *\/\n\n.gw-login button:focus-visible,\n.gw-login input:focus-visible,\n.gw-login select:focus-visible,\n.gw-login textarea:focus-visible {\n  outline: 3px solid #70a9e0;\n  outline-offset: 2px;\n}\n\n\/* Job search *\/\n\n.gw-login #gw-job-search {\n  width: 100%;\n}\n\n.gw-login #gw-job-result {\n  white-space: pre-line;\n  overflow-wrap: anywhere;\n  line-height: 1.7;\n}\n\n\/* Job dashboard *\/\n\n.gw-login #gw-job-dashboard {\n  width: 100%;\n  margin-top: 24px;\n  padding-top: 18px;\n  border-top: 2px solid #d5e4f5;\n}\n\n\/* Dashboard forms *\/\n\n.gw-login #gw-measurement-form,\n.gw-login #gw-preparation-form,\n.gw-login #gw-photo-form,\n.gw-login #gw-installer-review-form {\n  width: 100%;\n  max-width: 100%;\n  margin: 18px 0;\n  padding: 20px;\n  background: #ffffff;\n  border: 1px solid #d5e4f5;\n  border-radius: 12px;\n}\n\n\/* Photo gallery *\/\n\n.gw-login #gw-photo-gallery {\n  width: 100%;\n  max-width: 100%;\n  margin-top: 20px;\n  overflow-wrap: anywhere;\n}\n\n.gw-login #gw-photo-gallery img {\n  display: block;\n  width: 100%;\n  max-width: 400px;\n  height: auto;\n  border-radius: 8px;\n}\n\n.gw-login #gw-photo-gallery > button {\n  max-width: 400px;\n}\n\n.gw-login #gw-photo-gallery p {\n  overflow-wrap: anywhere;\n}\n\n\/* Messages and summaries *\/\n\n.gw-login p,\n.gw-login section,\n.gw-login div {\n  overflow-wrap: anywhere;\n}\n\n.gw-login [role=\"status\"] {\n  line-height: 1.6;\n}\n\n\/* Tables, if added later *\/\n\n.gw-login table {\n  display: block;\n  max-width: 100%;\n  overflow-x: auto;\n}\n\n\/* ========================================\n   MOBILE PHONES\n   ======================================== *\/\n\n@media screen and (max-width: 600px) {\n\n  .gw-login {\n    width: 100%;\n    max-width: 100%;\n    margin: 12px auto;\n    padding: 16px;\n    border-radius: 10px;\n  }\n\n  .gw-login:has(#gw-job-search:not([hidden])) {\n    max-width: 100%;\n  }\n\n  .gw-login h2 {\n    font-size: 23px;\n  }\n\n  .gw-login h3 {\n    font-size: 20px;\n  }\n\n  .gw-login h4 {\n    font-size: 17px;\n  }\n\n  .gw-login button {\n    min-height: 52px;\n    padding: 14px 12px;\n    font-size: 16px;\n  }\n\n  .gw-login input:not([type=\"checkbox\"]),\n  .gw-login select,\n  .gw-login textarea {\n    font-size: 16px;\n    min-height: 48px;\n  }\n\n  .gw-login #gw-measurement-form,\n  .gw-login #gw-preparation-form,\n  .gw-login #gw-photo-form,\n  .gw-login #gw-installer-review-form {\n    padding: 14px;\n    margin: 14px 0;\n  }\n\n  .gw-login #gw-photo-gallery img {\n    max-width: 100%;\n  }\n\n}\n\n\/* Extra-small phones *\/\n\n@media screen and (max-width: 360px) {\n\n  .gw-login {\n    padding: 12px;\n  }\n\n  .gw-login #gw-measurement-form,\n  .gw-login #gw-preparation-form,\n  .gw-login #gw-photo-form,\n  .gw-login #gw-installer-review-form {\n    padding: 10px;\n  }\n\n}\n\/* Hide login heading after employee signs in *\/\n.gw-login:has(#gw-job-search:not([hidden])) > h2 {\n  display: none;\n}\n#gw-language-switcher {\n  display: flex;\n  gap: 8px;\n  justify-content: center;\n  margin-bottom: 14px;\n}\n\n#gw-language-switcher .gw-lang-btn {\n  width: auto;\n  min-width: 90px;\n  padding: 8px 14px;\n  font-size: 14px;\n  border-radius: 8px;\n}\n\n#gw-language-switcher .gw-lang-btn.active {\n  font-weight: 700;\n  outline: 2px solid #0b4f9c;\n}\n<\/style>\n\n<script data-wp-block-html=\"js\">\n(function () {\n  function startPortal() {\n    const URL = \"https:\/\/tepzhgerltbdbhcgksog.supabase.co\";\n    const KEY = \"sb_publishable_12lkMKENfEprSCo__ldjhQ_1Yqd2V0U\";\n    const STORAGE_KEY = \"gw-portal-auth\";\n\n    const form = document.getElementById(\"gw-login-form\");\n    const email = document.getElementById(\"gw-email\");\n    const password = document.getElementById(\"gw-password\");\n    const remember = document.getElementById(\"gw-remember\");\n    const message = document.getElementById(\"gw-login-message\");\nconst jobSearch = document.getElementById(\"gw-job-search\");\nconst setPasswordForm =\n  document.getElementById(\"gw-set-password\");\n\nconst newPassword =\n  document.getElementById(\"gw-new-password\");\n\nconst confirmPassword =\n  document.getElementById(\"gw-confirm-password\");\n\nconst savePassword =\n  document.getElementById(\"gw-save-password\");\n\nconst passwordMessage =\n  document.getElementById(\"gw-password-message\");\n    if (!form || !email || !password || !remember || !message ||\n        !setPasswordForm || !newPassword || !confirmPassword ||\n        !savePassword || !passwordMessage) return;\n\n    \/\/ Keep the password form outside the login form, so either can be hidden.\n    if (form.contains(setPasswordForm)) form.insertAdjacentElement(\"afterend\", setPasswordForm);\n    setPasswordForm.hidden = true;\n    \/\/ Manual password reset controls removed from the Portal UI.\n    let recoveringPassword = false;\n    \/\/ Remember only a short-lived recovery *intent*, never the email link,\n    \/\/ access token, new password, or app password. A mobile browser may reload\n    \/\/ when switching to another app, after Supabase has cleaned the URL.\n    const RECOVERY_MARKER_KEY = \"gw-portal-recovery-pending-v1\";\n    const RECOVERY_MARKER_MS = 30 * 60 * 1000;\n    function clearRecoveryMarker() {\n      try { localStorage.removeItem(RECOVERY_MARKER_KEY); } catch (_error) { \/* best effort *\/ }\n    }\n    function getRecoveryMarker() {\n      try {\n        const raw = localStorage.getItem(RECOVERY_MARKER_KEY);\n        if (!raw) return null;\n        const marker = JSON.parse(raw);\n        if (!marker || typeof marker.userId !== \"string\" || !marker.userId ||\n            !Number.isFinite(marker.createdAt) || marker.createdAt > Date.now() ||\n            Date.now() - marker.createdAt > RECOVERY_MARKER_MS) {\n          clearRecoveryMarker();\n          return null;\n        }\n        return marker;\n      } catch (_error) {\n        clearRecoveryMarker();\n        return null;\n      }\n    }\n    function saveRecoveryMarker(userId) {\n      if (typeof userId !== \"string\" || !userId) return;\n      try {\n        const existing = getRecoveryMarker();\n        if (existing && existing.userId === userId) return; \/\/ Do not extend the window on app switches.\n        localStorage.setItem(RECOVERY_MARKER_KEY, JSON.stringify({\n          userId: userId, createdAt: Date.now()\n        }));\n      } catch (_error) { \/* recovery still works while the current page stays open *\/ }\n    }\n    function showPasswordSetup() {\n      recoveringPassword = true;\n      authViewRequestId++;\n      form.hidden = true;\n      logout.hidden = true;\n      jobSearch.hidden = true;\n      monthlyJobPanel.hidden = true;\n      dashboard.hidden = true;\n      employeeDirectoryToggle.hidden = true;\n      employeeDirectoryPanel.hidden = true;\n      if (typeof resetBulletinBoard === \"function\") resetBulletinBoard();\n      const opening = setPasswordForm.hidden;\n      setPasswordForm.hidden = false;\n      if (opening) {\n        passwordMessage.textContent = \"Set your password to finish account setup.\";\n        newPassword.focus();\n      }\n    }\n    \/\/ Deliberate exit, not an automatic loss of the form when Safari reloads.\n    const cancelPasswordSetup = document.createElement(\"button\");\n    cancelPasswordSetup.type = \"button\";\n    cancelPasswordSetup.textContent = \"Cancel Password Setup \/ Return to Log In\";\n    cancelPasswordSetup.style.cssText = \"display:block;margin:12px 0;\";\n    setPasswordForm.appendChild(cancelPasswordSetup);\n    cancelPasswordSetup.addEventListener(\"click\", async function () {\n      if (!client || !recoveringPassword || cancelPasswordSetup.disabled) return;\n      if (!window.confirm(\"Cancel password setup and return to the login page?\")) return;\n      cancelPasswordSetup.disabled = true;\n      try {\n        const { error } = await client.auth.signOut();\n        if (error) throw error;\n        clearRecoveryMarker();\n        newPassword.value = \"\";\n        confirmPassword.value = \"\";\n        showSignedOut();\n        message.textContent = \"Password setup canceled. You can request another setup link.\";\n      } catch (_error) {\n        passwordMessage.textContent = \"Unable to exit securely. Please try again.\";\n      } finally {\n        cancelPasswordSetup.disabled = false;\n      }\n    });\n    if (!form || !email || !password || !remember || !message) return;\n\n    let client = null;\n\n    \/\/ ------------------------------------------------------------\n    \/\/ Bulletin Board translation bridge\n    \/\/ Uses the signed-in Supabase client to call gw-translate-text.\n    \/\/ Original database text is never overwritten.\n    \/\/ ------------------------------------------------------------\n    const gwTranslationCache = new Map();\n\n    async function gwTranslateTexts(texts, targetLang, mode = \"user\") {\n      const list = Array.isArray(texts) ? texts.map(function (v) {\n        return String(v == null ? \"\" : v);\n      }) : [];\n\n      if (!list.length || targetLang === \"en\") return list;\n      if (!client) throw new Error(\"Portal connection is not ready.\");\n\n      const cacheKey = targetLang + \"|\" + mode + \"|\" + JSON.stringify(list);\n      if (gwTranslationCache.has(cacheKey)) {\n        return gwTranslationCache.get(cacheKey);\n      }\n\n      const { data, error } = await client.functions.invoke(\"gw-translate-text\", {\n        body: {\n          texts: list,\n          target_lang: targetLang,\n          mode: mode\n        }\n      });\n\n      if (error) throw error;\n\n      if (!data || !Array.isArray(data.translations) ||\n          data.translations.length !== list.length) {\n        throw new Error(\"Invalid translation response.\");\n      }\n\n      const translated = data.translations.map(function (v) {\n        return String(v == null ? \"\" : v);\n      });\n\n      gwTranslationCache.set(cacheKey, translated);\n      return translated;\n    }\n\n    function getGwPortalLanguage() {\n      try {\n        const lang = localStorage.getItem(\"gwPortalLanguage\") || \"en\";\n        return lang === \"zh\" || lang === \"es\" ? lang : \"en\";\n      } catch (_error) {\n        return \"en\";\n      }\n    }\n\n    \/\/ Only these database fields are natural-language notes and are safe\n    \/\/ to translate for display. Core job\/customer\/material data is excluded.\n    const GW_SAFE_WORK_NOTE_COLUMNS = new Set([\n      \"field_notes\",\n      \"plumbing_notes\",\n      \"electrical_notes\",\n      \"protection_notes\",\n      \"special_access_notes\",\n      \"installer_notes\"\n    ]);\n\n    async function translateSafeWorkNoteElement(element) {\n      if (!element || !element.dataset) return;\n\n      const source = element.dataset.gwSafeNoteSource || \"\";\n      const lang = getGwPortalLanguage();\n\n      \/\/ English always restores the exact stored\/original text.\n      if (lang === \"en\" || !source) {\n        element.textContent = source || \"\u2014\";\n        return;\n      }\n\n      if (!client) return;\n\n      const requestedLang = lang;\n\n      try {\n        const translated = await gwTranslateTexts([source], requestedLang, \"user\");\n\n        if (getGwPortalLanguage() !== requestedLang) return;\n        if (!element.isConnected) return;\n\n        element.textContent = translated[0] || source;\n      } catch (error) {\n        \/\/ Translation failure must never affect the saved work record or UI.\n        element.textContent = source;\n        console.warn(\"GW work-note translation failed:\", error);\n      }\n    }\n\n    function refreshSafeWorkNoteTranslations() {\n      document.querySelectorAll(\"[data-gw-safe-note-source]\").forEach(function (element) {\n        translateSafeWorkNoteElement(element);\n      });\n    }\n\n    async function translateBulletinCard(titleElement, bodyElement, decoded) {\n      const lang = getGwPortalLanguage();\n      if (lang === \"en\" || !client) return;\n\n      const requestedLang = lang;\n\n      try {\n        const translated = await gwTranslateTexts(\n          [decoded.title || \"\", decoded.body || \"\"],\n          requestedLang,\n          \"user\"\n        );\n\n        \/\/ Do not write an old translation if the user changed language\n        \/\/ while the request was in flight.\n        if (getGwPortalLanguage() !== requestedLang) return;\n        if (!titleElement.isConnected || !bodyElement.isConnected) return;\n\n        titleElement.textContent = translated[0] || decoded.title || \"\";\n        bodyElement.textContent = translated[1] || decoded.body || \"\";\n      } catch (error) {\n        \/\/ Translation failure must never break Bulletin Board.\n        console.warn(\"GW Bulletin translation failed:\", error);\n      }\n    }\n\n    \/\/ Re-render Bulletin Board when the user switches language.\n    window.addEventListener(\"gw-language-changed\", function () {\n      if (!client || logout.hidden) return;\n\n      \/\/ Final Measurement \/ Site Preparation safe note fields.\n      refreshSafeWorkNoteTranslations();\n\n      \/\/ Bulletin Board title\/body.\n      if (typeof loadBulletinNotes === \"function\") {\n        loadBulletinNotes().catch(function (error) {\n          console.warn(\"Unable to refresh Bulletin Board translation:\", error);\n        });\n      }\n    });\n\n    let staySignedIn = !!localStorage.getItem(STORAGE_KEY);\n\n    const logout = document.createElement(\"button\");\n    logout.type = \"button\";\n    logout.textContent = \"Log Out\";\n    logout.hidden = true;\n    form.parentElement.appendChild(logout);\n\n    \/\/ Owner-only employee directory. The database RPC checks owner status;\n    \/\/ non-owners never get the full roster through this interface.\n    const employeeDirectoryToggle = document.createElement(\"button\");\n    employeeDirectoryToggle.type = \"button\";\n    employeeDirectoryToggle.id = \"gw-employee-directory-toggle\";\n    employeeDirectoryToggle.textContent = \"Employee Accounts (Owner Only)\";\n    employeeDirectoryToggle.hidden = true;\n    employeeDirectoryToggle.setAttribute(\"aria-expanded\", \"false\");\n    employeeDirectoryToggle.style.cssText = \"width:100%;margin:12px 0;\";\n    const employeeDirectoryPanel = document.createElement(\"section\");\n    employeeDirectoryPanel.id = \"gw-employee-directory-panel\";\n    employeeDirectoryPanel.hidden = true;\n    employeeDirectoryPanel.style.cssText =\n      \"padding:16px;margin:12px 0 20px;border:1px solid #cbd5e1;border-radius:8px;\";\n    const employeeDirectoryHeading = document.createElement(\"h3\");\n    employeeDirectoryHeading.textContent = \"Employee Accounts (Owner Only)\";\n    const employeeDirectoryBody = document.createElement(\"div\");\n    const employeeDirectoryMessage = document.createElement(\"p\");\n    employeeDirectoryMessage.setAttribute(\"role\", \"status\");\n    employeeDirectoryPanel.append(\n      employeeDirectoryHeading, employeeDirectoryBody, employeeDirectoryMessage\n    );\n    \/\/ Owner-only invite form. The Edge Function independently verifies the caller's\n    \/\/ active Owner profile; hiding this UI is not the security boundary.\n    const inviteToggle = document.createElement(\"button\");\n    inviteToggle.type = \"button\";\n    inviteToggle.textContent = \"Add Employee\";\n    inviteToggle.style.cssText = \"display:block;width:100%;margin:12px 0;\";\n    inviteToggle.setAttribute(\"aria-expanded\", \"false\");\n    const inviteForm = document.createElement(\"form\");\n    inviteForm.hidden = true;\n    inviteForm.autocomplete = \"off\";\n    inviteForm.style.cssText = \"padding:12px;border:1px solid #cbd5e1;border-radius:8px;margin:8px 0 18px;\";\n    function inviteInput(labelText, type, maxLength) {\n      const label = document.createElement(\"label\");\n      label.textContent = labelText;\n      label.style.cssText = \"display:block;margin:8px 0;font-weight:600;\";\n      const input = document.createElement(\"input\");\n      input.type = type;\n      input.required = true;\n      input.maxLength = maxLength;\n      input.style.cssText = \"display:block;box-sizing:border-box;width:100%;font-size:16px;margin-top:5px;\";\n      input.autocomplete = \"off\";\n      label.appendChild(input);\n      inviteForm.appendChild(label);\n      return input;\n    }\n    const inviteName = inviteInput(\"Employee name\", \"text\", 100);\n    const inviteEmail = inviteInput(\"Employee email\", \"email\", 254);\n    const inviteRoleLabel = document.createElement(\"label\");\n    inviteRoleLabel.textContent = \"Employee role\";\n    inviteRoleLabel.style.cssText = \"display:block;margin:8px 0;font-weight:600;\";\n    const inviteRole = document.createElement(\"select\");\n    inviteRole.required = true;\n    inviteRole.style.cssText = \"display:block;width:100%;font-size:16px;margin-top:5px;\";\n    for (const [value, title] of [\n      [\"\", \"Select role\"], [\"installer\", \"Installer\"], [\"fabricator\", \"Fabricator\"],\n      [\"office\", \"Office\"], [\"project_manager\", \"Project Manager\"]\n    ]) {\n      const option = document.createElement(\"option\");\n      option.value = value;\n      option.textContent = title;\n      inviteRole.appendChild(option);\n    }\n    inviteRoleLabel.appendChild(inviteRole);\n    inviteForm.appendChild(inviteRoleLabel);\n    const inviteSubmit = document.createElement(\"button\");\n    inviteSubmit.type = \"submit\";\n    inviteSubmit.textContent = \"Send Employee Invitation\";\n    inviteSubmit.style.cssText = \"display:block;width:100%;margin:12px 0;\";\n    const inviteCancel = document.createElement(\"button\");\n    inviteCancel.type = \"button\";\n    inviteCancel.textContent = \"Cancel\";\n    inviteCancel.style.cssText = \"display:block;width:100%;margin:8px 0;\";\n    const inviteMessage = document.createElement(\"p\");\n    inviteMessage.setAttribute(\"role\", \"status\");\n    inviteMessage.setAttribute(\"aria-live\", \"polite\");\n    inviteForm.append(inviteSubmit, inviteCancel, inviteMessage);\n    employeeDirectoryPanel.insertBefore(inviteToggle, employeeDirectoryBody);\n    employeeDirectoryPanel.insertBefore(inviteForm, employeeDirectoryBody);\n    let inviteBusy = false;\n    function resetInviteForm() {\n      inviteForm.hidden = true;\n      inviteToggle.setAttribute(\"aria-expanded\", \"false\");\n      inviteName.value = \"\";\n      inviteEmail.value = \"\";\n      inviteRole.value = \"\";\n      inviteMessage.textContent = \"\";\n    }\n    inviteToggle.addEventListener(\"click\", function () {\n      if (employeeDirectoryPanel.hidden || employeeDirectoryToggle.hidden || logout.hidden || inviteBusy) return;\n      inviteForm.hidden = !inviteForm.hidden;\n      inviteToggle.setAttribute(\"aria-expanded\", String(!inviteForm.hidden));\n      if (!inviteForm.hidden) inviteName.focus();\n      else resetInviteForm();\n    });\n    inviteCancel.addEventListener(\"click\", function () {\n      if (!inviteBusy) resetInviteForm();\n    });\n    inviteForm.addEventListener(\"submit\", async function (event) {\n      event.preventDefault();\n      if (!client || logout.hidden || employeeDirectoryToggle.hidden ||\n          employeeDirectoryPanel.hidden || inviteForm.hidden || inviteBusy) return;\n      const displayName = inviteName.value.trim();\n      const employeeEmail = inviteEmail.value.trim().toLowerCase();\n      const role = inviteRole.value;\n      if (!displayName || displayName.length > 100 || \/[\\x00-\\x1f\\x7f]\/.test(displayName) ||\n          employeeEmail.length > 254 || !\/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$\/.test(employeeEmail) ||\n          ![\"installer\", \"fabricator\", \"office\", \"project_manager\"].includes(role)) {\n        inviteMessage.textContent = \"Enter a valid name, email and employee role.\";\n        return;\n      }\n      if (!window.confirm(\"Send an invitation to \" + employeeEmail +\n          \" as \" + role + \"? This will create an employee account with this role.\")) return;\n      inviteBusy = true;\n      inviteSubmit.disabled = true;\n      inviteCancel.disabled = true;\n      inviteToggle.disabled = true;\n      inviteMessage.textContent = \"Sending invitation...\";\n      try {\n        \/\/ supabase-js invokes the user-authenticated Edge Function using the\n        \/\/ current session. Never send a service-role or secret key from the page.\n        const { data, error } = await client.functions.invoke(\"gw-invite-employee\", {\n          body: { email: employeeEmail, displayName, role }\n        });\n        if (error || !data || data.invitationSent !== true || data.profileCreated !== true) {\n          const extra = data && typeof data.message === \"string\" ? data.message : \"\";\n          throw new Error(extra || \"Invitation could not be confirmed. Check Supabase before retrying.\");\n        }\n        if (logout.hidden || employeeDirectoryPanel.hidden) return;\n        resetInviteForm();\n        const refreshed = await loadEmployeeDirectory();\n        if (refreshed && !employeeDirectoryPanel.hidden && !logout.hidden)\n          employeeDirectoryMessage.textContent = \"Invitation sent and employee account created. \" +\n            \"The employee must open their email and set their own password.\";\n      } catch (_error) {\n        if (!logout.hidden && !employeeDirectoryPanel.hidden)\n          inviteMessage.textContent = \"Invitation not confirmed. Check Supabase Auth Users and employee_profiles before retrying; \" +\n            \"the email may already have been sent.\";\n      } finally {\n        inviteBusy = false;\n        inviteSubmit.disabled = false;\n        inviteCancel.disabled = false;\n        inviteToggle.disabled = false;\n      }\n    });\n    logout.insertAdjacentElement(\"afterend\", employeeDirectoryToggle);\n    employeeDirectoryToggle.insertAdjacentElement(\"afterend\", employeeDirectoryPanel);\n\n\n    \/\/ ================================================================\n    \/\/ BULLETIN BOARD \/ NOTES V2\n    \/\/ Separate in-Portal view, same browser tab. Uses existing bulletin RPCs.\n    \/\/ Title is stored inside note_text with a private prefix so no DB migration\n    \/\/ is required. Unread state is per signed-in user on this device\/browser.\n    \/\/ ================================================================\n    const BULLETIN_TITLE_PREFIX = \"[[GW_TITLE:\";\n    const BULLETIN_TITLE_SUFFIX = \"]]\";\n    const BULLETIN_SEEN_KEY = \"gw-bulletin-seen-v2\";\n\n    const bulletinEntry = document.createElement(\"button\");\n    bulletinEntry.type = \"button\";\n    bulletinEntry.id = \"gw-bulletin-entry\";\n    bulletinEntry.textContent = \"Bulletin Board\";\n    bulletinEntry.hidden = true;\n    bulletinEntry.style.cssText =\n      \"width:100%;margin:14px 0;padding:15px 16px;text-align:left;font-size:18px;font-weight:700;\" +\n      \"border:1px solid #b9c7d6;border-radius:10px;background:#fff;cursor:pointer;\";\nbulletinEntry.style.setProperty(\"color\", \"#0b4f8a\", \"important\");\nbulletinEntry.style.setProperty(\"-webkit-text-fill-color\", \"#0b4f8a\", \"important\");\n\n    const bulletinBoard = document.createElement(\"section\");\n    bulletinBoard.id = \"gw-bulletin-board\";\n    bulletinBoard.hidden = true;\n    bulletinBoard.style.cssText =\n      \"box-sizing:border-box;width:100%;margin:14px 0 20px;padding:16px;\" +\n      \"border:1px solid #cbd5e1;border-radius:10px;background:#fff;\";\n\n    const bulletinTop = document.createElement(\"div\");\n    bulletinTop.style.cssText =\n      \"display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:14px;\";\n    const bulletinBack = document.createElement(\"button\");\n    bulletinBack.type = \"button\";\n    bulletinBack.textContent = \"\u2190 Back to Portal\";\n    bulletinBack.style.cssText = \"margin:0;padding:8px 10px;font-size:14px;\";\n    const bulletinHeading = document.createElement(\"h2\");\n    bulletinHeading.textContent = \"Bulletin Board\";\n    bulletinHeading.style.cssText = \"margin:0;font-size:26px;line-height:1.2;\";\n    const bulletinAdd = document.createElement(\"button\");\n    bulletinAdd.type = \"button\";\n    bulletinAdd.textContent = \"+ Add Note\";\n    bulletinAdd.hidden = true;\n    bulletinAdd.style.cssText = \"margin:0;padding:9px 12px;\";\n    bulletinTop.append(bulletinBack, bulletinHeading, bulletinAdd);\n\n    const bulletinMessage = document.createElement(\"p\");\n    bulletinMessage.setAttribute(\"role\", \"status\");\n    bulletinMessage.setAttribute(\"aria-live\", \"polite\");\n    bulletinMessage.style.cssText = \"margin:8px 0;font-size:14px;\";\n\n    const bulletinEditor = document.createElement(\"form\");\n    bulletinEditor.hidden = true;\n    bulletinEditor.autocomplete = \"off\";\n    bulletinEditor.style.cssText =\n      \"margin:12px 0 18px;padding:14px;border:1px solid #dbe3ea;border-radius:8px;background:#f8fafc;\";\n\n    function bulletinFieldLabel(labelText) {\n      const label = document.createElement(\"label\");\n      label.textContent = labelText;\n      label.style.cssText = \"display:block;margin:9px 0;font-weight:600;\";\n      return label;\n    }\n\n    const bulletinTitleLabel = bulletinFieldLabel(\"Title\");\n    const bulletinNoteTitle = document.createElement(\"input\");\n    bulletinNoteTitle.type = \"text\";\n    bulletinNoteTitle.required = true;\n    bulletinNoteTitle.maxLength = 100;\n    bulletinNoteTitle.placeholder = \"Example: Smith Kitchen \/ Tomorrow Installation\";\n    bulletinNoteTitle.style.cssText =\n      \"display:block;box-sizing:border-box;width:100%;font-size:17px;margin-top:5px;padding:9px;\";\n    bulletinTitleLabel.appendChild(bulletinNoteTitle);\n\n    const bulletinAudienceLabel = bulletinFieldLabel(\"For\");\n    const bulletinAudience = document.createElement(\"select\");\n    bulletinAudience.required = true;\n    bulletinAudience.style.cssText =\n      \"display:block;width:100%;font-size:16px;margin-top:5px;padding:8px;\";\n    for (const [value, label] of [\n      [\"everyone\", \"Everyone\"],\n      [\"installer\", \"Installer\"],\n      [\"fabricator\", \"Fabricator\"],\n      [\"office\", \"Office\"],\n      [\"project_manager\", \"Project Manager\"]\n    ]) {\n      const option = document.createElement(\"option\");\n      option.value = value;\n      option.textContent = label;\n      bulletinAudience.appendChild(option);\n    }\n    bulletinAudienceLabel.appendChild(bulletinAudience);\n\n    const bulletinJobLabel = bulletinFieldLabel(\"Job ID (optional)\");\n    const bulletinJob = document.createElement(\"input\");\n    bulletinJob.type = \"text\";\n    bulletinJob.maxLength = 80;\n    bulletinJob.placeholder = \"Example: 1234\";\n    bulletinJob.style.cssText =\n      \"display:block;box-sizing:border-box;width:100%;font-size:16px;margin-top:5px;padding:8px;\";\n    bulletinJobLabel.appendChild(bulletinJob);\n\n    const bulletinTextLabel = bulletinFieldLabel(\"Note\");\n    const bulletinText = document.createElement(\"textarea\");\n    bulletinText.required = true;\n    bulletinText.maxLength = 1000;\n    bulletinText.rows = 5;\n    bulletinText.placeholder = \"Example: Installation moved to Friday. Bring sink support tools.\";\n    bulletinText.style.cssText =\n      \"display:block;box-sizing:border-box;width:100%;font-size:18px;line-height:1.5;\" +\n      \"margin-top:5px;padding:9px;resize:vertical;\";\n    bulletinTextLabel.appendChild(bulletinText);\n\n    const bulletinSave = document.createElement(\"button\");\n    bulletinSave.type = \"submit\";\n    bulletinSave.textContent = \"Post Note\";\n    bulletinSave.style.cssText = \"margin:10px 8px 0 0;\";\n    const bulletinCancel = document.createElement(\"button\");\n    bulletinCancel.type = \"button\";\n    bulletinCancel.textContent = \"Cancel\";\n    bulletinCancel.style.cssText = \"margin:10px 0 0;\";\n    const bulletinEditorMessage = document.createElement(\"p\");\n    bulletinEditorMessage.setAttribute(\"role\", \"status\");\n    bulletinEditorMessage.style.cssText = \"margin:8px 0 0;font-size:14px;\";\n    bulletinEditor.append(\n      bulletinTitleLabel, bulletinAudienceLabel, bulletinJobLabel, bulletinTextLabel,\n      bulletinSave, bulletinCancel, bulletinEditorMessage\n    );\n\n    const bulletinOpenList = document.createElement(\"div\");\n    bulletinOpenList.id = \"gw-bulletin-open-notes\";\n\n    const bulletinCompletedToggle = document.createElement(\"button\");\n    bulletinCompletedToggle.type = \"button\";\n    bulletinCompletedToggle.textContent = \"Completed Notes\";\n    bulletinCompletedToggle.setAttribute(\"aria-expanded\", \"false\");\n    bulletinCompletedToggle.style.cssText =\n      \"display:block;width:100%;margin:18px 0 8px;padding:10px;font-size:14px;\";\n    const bulletinCompletedList = document.createElement(\"div\");\n    bulletinCompletedList.hidden = true;\n    bulletinCompletedList.id = \"gw-bulletin-completed-notes\";\n\n    bulletinBoard.append(\n      bulletinTop, bulletinMessage, bulletinEditor,\n      bulletinOpenList, bulletinCompletedToggle, bulletinCompletedList\n    );\n    logout.insertAdjacentElement(\"afterend\", bulletinEntry);\n    bulletinEntry.insertAdjacentElement(\"afterend\", bulletinBoard);\n\n    let bulletinRole = null;\n    let bulletinUserId = null;\n    let bulletinEditingId = null;\n    let bulletinBusy = false;\n    let bulletinGeneration = 0;\n    let bulletinOpenNotes = [];\n    let bulletinCompletedNotes = [];\n    let bulletinPortalState = null;\n\n    function bulletinAudienceLabelText(value) {\n      return ({\n        everyone: \"Everyone\", installer: \"Installer\", fabricator: \"Fabricator\",\n        office: \"Office\", project_manager: \"Project Manager\"\n      })[value] || value || \"\";\n    }\n    function bulletinCanWrite() {\n      return [\"owner\", \"office\", \"project_manager\", \"installer\", \"fabricator\"].includes(bulletinRole);\n    }\n    function bulletinCanDelete() {\n      return [\"owner\", \"office\"].includes(bulletinRole);\n    }\n    function formatBulletinDate(value) {\n      if (!value) return \"\";\n      const d = new Date(value);\n      if (Number.isNaN(d.getTime())) return \"\";\n      return d.toLocaleString([], {\n        month: \"short\", day: \"numeric\", year: \"numeric\", hour: \"numeric\", minute: \"2-digit\"\n      });\n    }\n    function encodeBulletinText(title, body) {\n      return BULLETIN_TITLE_PREFIX + title.replace(\/[\\r\\n]\/g, \" \").trim() +\n        BULLETIN_TITLE_SUFFIX + \"\\n\" + body.trim();\n    }\n    function decodeBulletinText(raw) {\n      const value = String(raw || \"\");\n      if (value.startsWith(BULLETIN_TITLE_PREFIX)) {\n        const end = value.indexOf(BULLETIN_TITLE_SUFFIX, BULLETIN_TITLE_PREFIX.length);\n        if (end >= 0) {\n          return {\n            title: value.slice(BULLETIN_TITLE_PREFIX.length, end).trim() || \"Notice\",\n            body: value.slice(end + BULLETIN_TITLE_SUFFIX.length).replace(\/^\\r?\\n\/, \"\")\n          };\n        }\n      }\n      return { title: \"Notice\", body: value };\n    }\n    function bulletinSeenStorageKey() {\n      return BULLETIN_SEEN_KEY + \":\" + (bulletinUserId || \"unknown\");\n    }\n    function getBulletinSeenIds() {\n      try {\n        const raw = localStorage.getItem(bulletinSeenStorageKey());\n        const list = raw ? JSON.parse(raw) : [];\n        return new Set(Array.isArray(list) ? list.map(String) : []);\n      } catch (_error) { return new Set(); }\n    }\n    function markCurrentBulletinsSeen() {\n      if (!bulletinUserId) return;\n      try {\n        const seen = getBulletinSeenIds();\n        for (const note of bulletinOpenNotes) seen.add(String(note.id));\n        localStorage.setItem(bulletinSeenStorageKey(), JSON.stringify(Array.from(seen).slice(-500)));\n      } catch (_error) { \/* unread indicator is convenience only *\/ }\n      updateBulletinEntry();\n    }\n    function bulletinUnreadCount() {\n      const seen = getBulletinSeenIds();\n      return bulletinOpenNotes.reduce((count, note) =>\n        count + (seen.has(String(note.id)) ? 0 : 1), 0);\n    }\n    function updateBulletinEntry() {\n      const unread = bulletinUnreadCount();\n      bulletinEntry.textContent = unread > 0\n        ? \"Bulletin Board   \u2022 NEW (\" + unread + \")\"\n        : \"Bulletin Board\";\n      bulletinEntry.style.borderColor = unread > 0 ? \"#0b4f8a\" : \"#b9c7d6\";\n    }\n    function closeBulletinEditor() {\n      bulletinEditingId = null;\n      bulletinEditor.hidden = true;\n      bulletinNoteTitle.value = \"\";\n      bulletinAudience.value = \"everyone\";\n      bulletinJob.value = \"\";\n      bulletinText.value = \"\";\n      bulletinSave.textContent = \"Post Note\";\n      bulletinEditorMessage.textContent = \"\";\n    }\n    function openBulletinEditor(note) {\n      if (!bulletinCanWrite() || bulletinBusy || bulletinBoard.hidden) return;\n      const decoded = note ? decodeBulletinText(note.note_text) : { title: \"\", body: \"\" };\n      bulletinEditingId = note ? note.id : null;\n      bulletinNoteTitle.value = decoded.title === \"Notice\" && note &&\n        !String(note.note_text || \"\").startsWith(BULLETIN_TITLE_PREFIX) ? \"\" : decoded.title;\n      bulletinAudience.value = note?.audience || \"everyone\";\n      bulletinJob.value = note?.job_id || \"\";\n      bulletinText.value = decoded.body;\n      bulletinSave.textContent = note ? \"Save Changes\" : \"Post Note\";\n      bulletinEditorMessage.textContent = \"\";\n      bulletinEditor.hidden = false;\n      bulletinNoteTitle.focus();\n    }\nfunction makeBulletinTextAction(label, handler) {\n  const button = document.createElement(\"button\");\n  button.type = \"button\";\n  button.textContent = label;\n  button.style.cssText =\n    \"border:0;background:transparent;padding:2px 10px 2px 0;margin:0;\" +\n    \"font-size:13px;color:#526579;text-decoration:underline;cursor:pointer;\";\n  button.style.setProperty(\"display\", \"inline\", \"important\");\n  button.style.setProperty(\"width\", \"auto\", \"important\");\n  button.style.setProperty(\"min-width\", \"0\", \"important\");\n  button.style.setProperty(\"padding\", \"0\", \"important\");\n  button.style.setProperty(\"margin\", \"0\", \"important\");\n  button.style.setProperty(\"border\", \"0\", \"important\");\n  button.style.setProperty(\"background\", \"transparent\", \"important\");\n  button.style.setProperty(\"box-shadow\", \"none\", \"important\");\n  button.style.setProperty(\"color\", \"#526579\", \"important\");\n  button.style.setProperty(\"-webkit-text-fill-color\", \"#526579\", \"important\");\n  button.addEventListener(\"click\", handler);\n  return button;\n}\n\nfunction renderBulletinNotes(container, notes, completed) {\n      container.replaceChildren();\n      const seen = getBulletinSeenIds();\n      for (const note of notes) {\n        const decoded = decodeBulletinText(note.note_text);\n        const isUnread = !completed && !seen.has(String(note.id));\n        const card = document.createElement(\"article\");\n        card.style.cssText =\n          \"margin:12px 0;padding:16px;border:1px solid \" + (isUnread ? \"#8fb5d8\" : \"#dbe3ea\") +\n          \";border-radius:9px;background:\" + (completed ? \"#f8fafc\" : \"#fff\") +\n          \";overflow-wrap:anywhere;\" + (completed ? \"opacity:.88;\" : \"\");\n        const titleRow = document.createElement(\"div\");\n        titleRow.style.cssText = \"display:flex;align-items:flex-start;gap:8px;flex-wrap:wrap;\";\n        const title = document.createElement(\"h3\");\n        title.textContent = decoded.title;\n        title.style.cssText = \"margin:0 0 8px;font-size:21px;line-height:1.25;\";\n        titleRow.appendChild(title);\n        if (isUnread) {\n          const badge = document.createElement(\"span\");\n          badge.textContent = \"NEW\";\n          badge.style.cssText =\n            \"font-size:11px;font-weight:700;padding:3px 6px;border-radius:999px;background:#e8f2fb;color:#0b4f8a;\";\n          titleRow.appendChild(badge);\n        }\n        const meta = document.createElement(\"div\");\n        meta.style.cssText = \"display:flex;gap:10px;flex-wrap:wrap;margin-bottom:10px;font-size:13px;color:#526579;\";\n        const audience = document.createElement(\"span\");\n        audience.textContent = \"For: \" + bulletinAudienceLabelText(note.audience);\n        meta.appendChild(audience);\n        if (note.job_id) {\n          const job = document.createElement(\"span\");\n          job.textContent = \"Job: \" + note.job_id;\n          meta.appendChild(job);\n        }\n        const body = document.createElement(\"div\");\n        body.textContent = decoded.body;\n        body.style.cssText =\n          \"white-space:pre-wrap;overflow-wrap:anywhere;font-size:19px;line-height:1.55;margin:6px 0 10px;\";\n\n        \/\/ Translate display-only Bulletin text. The saved note remains unchanged.\n        translateBulletinCard(title, body, decoded);\n\n        const detail = document.createElement(\"div\");\n        detail.style.cssText = \"margin-top:8px;font-size:12px;color:#718096;\";\n        if (completed) {\n          detail.textContent = \"Completed\" +\n            (note.completed_by_name ? \" by \" + note.completed_by_name : \"\") +\n            (note.completed_at ? \" \u00b7 \" + formatBulletinDate(note.completed_at) : \"\");\n        } else {\n          detail.textContent = \"Posted\" +\n            (note.created_by_name ? \" by \" + note.created_by_name : \"\") +\n            (note.created_at ? \" \u00b7 \" + formatBulletinDate(note.created_at) : \"\");\n        }\n        const actions = document.createElement(\"div\");\n        actions.style.cssText = \"margin-top:10px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;\";\n        actions.style.setProperty(\"display\", \"flex\", \"important\");\n        actions.style.setProperty(\"gap\", \"10px\", \"important\");\n        if (!completed && bulletinCanWrite()) {\n          actions.appendChild(makeBulletinTextAction(\"Done\", async function () {\n            if (bulletinBusy || bulletinBoard.hidden) return;\n            if (!window.confirm(\"Mark this note Done? It will move to Completed Notes.\")) return;\n            bulletinBusy = true;\n            bulletinMessage.textContent = \"Marking note Done...\";\n            try {\n              const { data, error } = await client.rpc(\"gw_bulletin_complete_note\", { p_note_id: note.id });\n              if (error) throw error;\n              if (data !== true) throw new Error(\"Note could not be completed.\");\n              await loadBulletinNotes();\n              bulletinMessage.textContent = \"Note moved to Completed Notes.\";\n            } catch (error) {\n              bulletinMessage.textContent = \"Unable to complete note: \" + error.message;\n            } finally { bulletinBusy = false; }\n          }));\n          if (bulletinCanWrite()) {\n            actions.appendChild(makeBulletinTextAction(\"Edit\", function () { openBulletinEditor(note); }));\n          }\n        }\n        if (bulletinCanDelete()) {\n          actions.appendChild(makeBulletinTextAction(\"Delete\", async function () {\n            if (bulletinBusy || bulletinBoard.hidden) return;\n            if (!window.confirm(\"Permanently delete this note? This cannot be undone.\")) return;\n            bulletinBusy = true;\n            bulletinMessage.textContent = \"Deleting note...\";\n            try {\n              const { data, error } = await client.rpc(\"gw_bulletin_delete_note\", { p_note_id: note.id });\n              if (error) throw error;\n              if (data !== true) throw new Error(\"Note could not be deleted.\");\n              if (bulletinEditingId === note.id) closeBulletinEditor();\n              await loadBulletinNotes();\n              bulletinMessage.textContent = \"Note permanently deleted.\";\n            } catch (error) {\n              bulletinMessage.textContent = \"Unable to delete note: \" + error.message;\n            } finally { bulletinBusy = false; }\n          }));\n        }\n        card.append(titleRow, meta, body, detail);\n        if (actions.childNodes.length) card.appendChild(actions);\n        container.appendChild(card);\n      }\n    }\n    async function loadBulletinNotes() {\n      if (!client || logout.hidden) return false;\n      const generation = bulletinGeneration;\n      try {\n        const [openResult, completedResult] = await Promise.all([\n          client.rpc(\"gw_bulletin_list_notes\", { p_status: \"open\" }),\n          client.rpc(\"gw_bulletin_list_notes\", { p_status: \"completed\" })\n        ]);\n        if (generation !== bulletinGeneration || logout.hidden) return false;\n        if (openResult.error) throw openResult.error;\n        if (completedResult.error) throw completedResult.error;\n        const bulletinVisibleToRole = function (note) {\n          const audience = note && note.audience ? note.audience : \"everyone\";\n          if ([\"owner\", \"office\", \"project_manager\"].includes(bulletinRole)) return true;\n          if (bulletinRole === \"installer\") return audience === \"everyone\" || audience === \"installer\";\n          if (bulletinRole === \"fabricator\") return audience === \"everyone\" || audience === \"fabricator\";\n          return audience === \"everyone\";\n        };\n        bulletinOpenNotes = (Array.isArray(openResult.data) ? openResult.data : []).filter(bulletinVisibleToRole);\n        bulletinCompletedNotes = (Array.isArray(completedResult.data) ? completedResult.data : []).filter(bulletinVisibleToRole);\n        renderBulletinNotes(bulletinOpenList, bulletinOpenNotes, false);\n        renderBulletinNotes(bulletinCompletedList, bulletinCompletedNotes, true);\n        bulletinCompletedToggle.textContent = \"Completed Notes (\" + bulletinCompletedNotes.length + \")\";\n        updateBulletinEntry();\n        return true;\n      } catch (error) {\n        if (generation === bulletinGeneration && !logout.hidden) {\n          bulletinOpenList.replaceChildren();\n          bulletinCompletedList.replaceChildren();\n          bulletinMessage.textContent = \"Unable to load Bulletin Board: \" + error.message;\n        }\n        return false;\n      }\n    }\n    function openBulletinView() {\n      if (bulletinEntry.hidden || logout.hidden) return;\n      bulletinPortalState = {\n        jobSearchHidden: jobSearch.hidden,\n        dashboardHidden: dashboard.hidden,\n        employeeToggleHidden: employeeDirectoryToggle.hidden,\n        employeePanelHidden: employeeDirectoryPanel.hidden\n      };\n      jobSearch.hidden = true;\n      dashboard.hidden = true;\n      employeeDirectoryToggle.hidden = true;\n      employeeDirectoryPanel.hidden = true;\n      bulletinEntry.hidden = true;\n      bulletinBoard.hidden = false;\n      closeBulletinEditor();\n      renderBulletinNotes(bulletinOpenList, bulletinOpenNotes, false);\n      markCurrentBulletinsSeen();\n      renderBulletinNotes(bulletinOpenList, bulletinOpenNotes, false);\n      bulletinHeading.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n    }\n    function closeBulletinView() {\n      if (bulletinBoard.hidden) return;\n      closeBulletinEditor();\n      bulletinBoard.hidden = true;\n      bulletinEntry.hidden = false;\n      if (bulletinPortalState) {\n        jobSearch.hidden = bulletinPortalState.jobSearchHidden;\n        monthlyJobPanel.hidden = bulletinPortalState.jobSearchHidden;\n        if (!monthlyJobPanel.hidden) void loadMonthlyJobIds();\n        dashboard.hidden = bulletinPortalState.dashboardHidden;\n        employeeDirectoryToggle.hidden = bulletinPortalState.employeeToggleHidden;\n        employeeDirectoryPanel.hidden = bulletinPortalState.employeePanelHidden;\n      } else {\n        jobSearch.hidden = false;\n        monthlyJobPanel.hidden = false;\n        void loadMonthlyJobIds();\n        dashboard.hidden = true;\n      }\n      bulletinPortalState = null;\n      updateBulletinEntry();\n      bulletinEntry.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n    }\n    async function loadBulletinBoard() {\n      const generation = ++bulletinGeneration;\n      \/\/ showSignedIn() has already verified that this is an active employee.\n      \/\/ The Bulletin Board entry must therefore stay visible even if the\n      \/\/ optional profile\/role lookup or note loading has a problem.\n      bulletinEntry.hidden = false;\n      bulletinBoard.hidden = true;\n      bulletinEntry.textContent = \"Bulletin Board\";\n      closeBulletinEditor();\n      bulletinMessage.textContent = \"\";\n      bulletinOpenList.replaceChildren();\n      bulletinCompletedList.replaceChildren();\n      bulletinCompletedList.hidden = true;\n      bulletinCompletedToggle.setAttribute(\"aria-expanded\", \"false\");\n      bulletinRole = null;\n      bulletinUserId = null;\n      bulletinAdd.hidden = true;\n      if (!client || logout.hidden) {\n        bulletinEntry.hidden = true;\n        return;\n      }\n      try {\n        const { data: auth, error: authError } = await client.auth.getUser();\n        if (authError || !auth.user) throw new Error(\"Unable to verify signed-in employee.\");\n        if (generation !== bulletinGeneration || logout.hidden) return;\n\n        bulletinUserId = auth.user.id;\n\n        \/\/ Role controls Add\/Edit\/Delete only. It must never hide the board entry.\n        const { data: profile, error: profileError } = await client\n          .from(\"employee_profiles\").select(\"role\").eq(\"user_id\", auth.user.id).maybeSingle();\n        if (!profileError && profile && profile.role) {\n          bulletinRole = profile.role;\n          bulletinAdd.hidden = !bulletinCanWrite();\n        }\n\n        bulletinEntry.hidden = false;\n        updateBulletinEntry();\n        bulletinMessage.textContent = \"Loading notes...\";\n        const loaded = await loadBulletinNotes();\n        if (loaded && generation === bulletinGeneration && !logout.hidden) {\n          bulletinMessage.textContent = \"\";\n        } else if (generation === bulletinGeneration && !logout.hidden && !bulletinMessage.textContent) {\n          bulletinMessage.textContent = \"Notes are temporarily unavailable.\";\n        }\n      } catch (error) {\n        if (generation === bulletinGeneration && !logout.hidden) {\n          \/\/ Fail open for navigation\/read UI, but keep all write controls hidden.\n          bulletinRole = null;\n          bulletinAdd.hidden = true;\n          bulletinEntry.hidden = false;\n          bulletinEntry.textContent = \"Bulletin Board\";\n          bulletinMessage.textContent = \"Bulletin Board is temporarily unavailable. Please try again.\";\n        }\n      }\n    }\n\n    function resetBulletinBoard() {\n      bulletinGeneration++;\n      bulletinRole = null;\n      bulletinUserId = null;\n      bulletinBusy = false;\n      bulletinPortalState = null;\n      bulletinOpenNotes = [];\n      bulletinCompletedNotes = [];\n      bulletinEntry.hidden = true;\n      bulletinBoard.hidden = true;\n      bulletinAdd.hidden = true;\n      bulletinOpenList.replaceChildren();\n      bulletinCompletedList.replaceChildren();\n      bulletinCompletedList.hidden = true;\n      bulletinCompletedToggle.setAttribute(\"aria-expanded\", \"false\");\n      bulletinCompletedToggle.textContent = \"Completed Notes\";\n      bulletinMessage.textContent = \"\";\n      closeBulletinEditor();\n    }\n\n    bulletinEntry.addEventListener(\"click\", openBulletinView);\n    bulletinBack.addEventListener(\"click\", closeBulletinView);\n    bulletinAdd.addEventListener(\"click\", function () { openBulletinEditor(null); });\n    bulletinCancel.addEventListener(\"click\", function () {\n      if (!bulletinBusy) closeBulletinEditor();\n    });\n    bulletinCompletedToggle.addEventListener(\"click\", function () {\n      if (bulletinBoard.hidden) return;\n      bulletinCompletedList.hidden = !bulletinCompletedList.hidden;\n      bulletinCompletedToggle.setAttribute(\"aria-expanded\", String(!bulletinCompletedList.hidden));\n    });\n    bulletinEditor.addEventListener(\"submit\", async function (event) {\n      event.preventDefault();\n      if (!client || bulletinBoard.hidden || !bulletinCanWrite() || bulletinBusy) return;\n      const title = bulletinNoteTitle.value.trim();\n      const noteText = bulletinText.value.trim();\n      const audience = bulletinAudience.value;\n      const jobId = bulletinJob.value.trim();\n      if (!title || title.length > 100 || !noteText || noteText.length > 1000 ||\n          ![\"everyone\", \"installer\", \"fabricator\", \"office\", \"project_manager\"].includes(audience) ||\n          jobId.length > 80) {\n        bulletinEditorMessage.textContent = \"Enter a title, note, audience and optional Job ID.\";\n        return;\n      }\n      const storedText = encodeBulletinText(title, noteText);\n      if (storedText.length > 1200) {\n        bulletinEditorMessage.textContent = \"This note is too long. Shorten the title or note.\";\n        return;\n      }\n      bulletinBusy = true;\n      bulletinSave.disabled = true;\n      bulletinCancel.disabled = true;\n      bulletinEditorMessage.textContent = bulletinEditingId ? \"Saving changes...\" : \"Posting note...\";\n      try {\n        let result;\n        if (bulletinEditingId) {\n          result = await client.rpc(\"gw_bulletin_update_note\", {\n            p_note_id: bulletinEditingId, p_note_text: storedText,\n            p_audience: audience, p_job_id: jobId || null\n          });\n          if (result.error) throw result.error;\n          if (result.data !== true) throw new Error(\"Note could not be updated.\");\n        } else {\n          result = await client.rpc(\"gw_bulletin_create_note\", {\n            p_note_text: storedText, p_audience: audience, p_job_id: jobId || null\n          });\n          if (result.error) throw result.error;\n          if (!Number.isFinite(Number(result.data))) throw new Error(\"Note could not be confirmed.\");\n        }\n        const wasEditing = bulletinEditingId !== null;\n        closeBulletinEditor();\n        await loadBulletinNotes();\n        markCurrentBulletinsSeen();\n        renderBulletinNotes(bulletinOpenList, bulletinOpenNotes, false);\n        bulletinMessage.textContent = wasEditing ? \"Note updated.\" : \"Note posted.\";\n      } catch (error) {\n        bulletinEditorMessage.textContent = \"Unable to save note: \" + error.message;\n      } finally {\n        bulletinBusy = false;\n        bulletinSave.disabled = false;\n        bulletinCancel.disabled = false;\n      }\n    });\n\n    let employeeDirectoryRequestId = 0;\n    let employeeStatusBusy = false;\n    function resetEmployeeDirectory() {\n      employeeDirectoryRequestId++;\n      employeeDirectoryToggle.hidden = true;\n      employeeDirectoryToggle.setAttribute(\"aria-expanded\", \"false\");\n      employeeDirectoryPanel.hidden = true;\n      employeeDirectoryBody.replaceChildren();\n      employeeDirectoryMessage.textContent = \"\";\n      resetInviteForm();\n    }\n    async function checkEmployeeDirectoryAccess() {\n      const requestId = ++employeeDirectoryRequestId;\n      employeeDirectoryToggle.hidden = true;\n      employeeDirectoryPanel.hidden = true;\n      try {\n        const { data: auth, error: authError } = await client.auth.getUser();\n        if (authError || !auth.user) return;\n        const { data: profile, error: profileError } = await client\n          .from(\"employee_profiles\")\n          .select(\"role\")\n          .eq(\"user_id\", auth.user.id)\n          .maybeSingle();\n        if (profileError || requestId !== employeeDirectoryRequestId ||\n            logout.hidden || !profile || profile.role !== \"owner\") return;\n        employeeDirectoryToggle.hidden = false;\n      } catch (_error) {\n        \/\/ Directory access is optional; ordinary login remains available.\n      }\n    }\n    \/\/ Names are edited through an owner-checked database RPC. UI controls alone\n    \/\/ are not an authorization boundary; owner profiles are deliberately read-only.\n    async function loadEmployeeDirectory() {\n      const requestId = ++employeeDirectoryRequestId;\n      employeeDirectoryBody.textContent = \"Loading employees...\";\n      employeeDirectoryMessage.textContent = \"\";\n      try {\n        const { data: employees, error } =\n          await client.rpc(\"gw_owner_list_employees\");\n        if (error) throw error;\n        if (!Array.isArray(employees)) throw new Error(\"Employee list unavailable.\");\n        \/\/ Separate owner-only RPC avoids changing the established roster RPC return type.\n        const { data: statuses, error: statusError } =\n          await client.rpc(\"gw_owner_list_employee_statuses\");\n        if (statusError) throw statusError;\n        if (!Array.isArray(statuses)) throw new Error(\"Employee statuses unavailable.\");\n        const statusByUserId = new Map();\n        for (const statusRow of statuses) {\n          if (!statusRow || !statusRow.user_id ||\n              typeof statusRow.is_active !== \"boolean\" ||\n              statusByUserId.has(statusRow.user_id)) {\n            throw new Error(\"Employee status information is invalid.\");\n          }\n          statusByUserId.set(statusRow.user_id, statusRow.is_active);\n        }\n        for (const employee of employees) {\n          if (!statusByUserId.has(employee.user_id))\n            throw new Error(\"Employee status could not be verified.\");\n        }\n        if (requestId !== employeeDirectoryRequestId ||\n            employeeDirectoryPanel.hidden || employeeDirectoryToggle.hidden ||\n            logout.hidden) return false;\n        employeeDirectoryBody.replaceChildren();\n        if (!employees || employees.length === 0) {\n          employeeDirectoryBody.textContent = \"No employee profiles found.\";\n          return true;\n        }\n        const sortedEmployees = [...employees].sort(function (a, b) {\n          const aOwner = a.role === \"owner\" ? 0 : 1;\n          const bOwner = b.role === \"owner\" ? 0 : 1;\n          if (aOwner !== bOwner) return aOwner - bOwner;\n          return String(a.display_name || \"\").localeCompare(String(b.display_name || \"\"));\n        });\n        for (const employee of sortedEmployees) {\n          const row = document.createElement(\"div\");\n          row.style.cssText =\n            \"border-bottom:1px solid #e3e8ed;overflow-wrap:anywhere;\";\n\n          const isOwnerAccount = employee.role === \"owner\";\n          const isEditableEmployee =\n            [\"installer\", \"fabricator\", \"office\", \"project_manager\"].includes(employee.role);\n          const isActive = statusByUserId.get(employee.user_id);\n\n          \/\/ Keep the main Employee Accounts screen simple: one small user icon + name.\n          \/\/ All management controls are inside the selected employee row.\n          const accountButton = document.createElement(\"button\");\n          accountButton.type = \"button\";\n          accountButton.setAttribute(\"aria-expanded\", \"false\");\n          accountButton.style.cssText =\n            \"display:flex;align-items:center;gap:9px;width:100%;padding:12px 4px;\" +\n            \"border:0;background:transparent;text-align:left;cursor:pointer;font:inherit;\";\n          const accountIcon = document.createElement(\"span\");\n          accountIcon.textContent = \"\ud83d\udc64\";\n          accountIcon.setAttribute(\"aria-hidden\", \"true\");\n          accountIcon.style.cssText = \"font-size:18px;line-height:1;flex:0 0 auto;\";\n          const accountName = document.createElement(\"strong\");\n          accountName.textContent = employee.display_name || \"(Name not set)\";\n          accountName.style.cssText = \"font-size:16px;line-height:1.3;color:#0b4f8a;font-weight:600;\";\n          accountButton.append(accountIcon, accountName);\n\n          const accountPanel = document.createElement(\"div\");\n          accountPanel.hidden = true;\n          accountPanel.style.cssText =\n            \"padding:2px 8px 14px 35px;box-sizing:border-box;\";\n\n          const accountSummary = document.createElement(\"p\");\n          if (isOwnerAccount) {\n            accountSummary.textContent = \"Owner account \u2014 read only.\";\n          } else {\n            accountSummary.textContent =\n              \"Role: \" + employee.role + \" \u00b7 Portal: \" +\n              (isActive ? \"Active\" : \"Disabled\");\n          }\n          accountSummary.style.cssText = \"margin:4px 0 10px;font-size:14px;\";\n          accountPanel.appendChild(accountSummary);\n\n          accountButton.addEventListener(\"click\", function () {\n            if (employeeDirectoryPanel.hidden || logout.hidden) return;\n            accountPanel.hidden = !accountPanel.hidden;\n            accountButton.setAttribute(\"aria-expanded\", String(!accountPanel.hidden));\n          });\n\n          row.append(accountButton, accountPanel);\n\n          if (isEditableEmployee) {\n            const editButton = document.createElement(\"button\");\n            editButton.type = \"button\";\n            editButton.textContent = \"Edit Name\";\n            editButton.style.cssText = \"display:block;width:100%;margin:8px 0;\";\n            const editor = document.createElement(\"div\");\n            editor.hidden = true;\n            const nameLabel = document.createElement(\"label\");\n            nameLabel.textContent = \"Employee display name\";\n            nameLabel.style.display = \"block\";\n            const nameInput = document.createElement(\"input\");\n            nameInput.type = \"text\";\n            nameInput.maxLength = 80;\n            nameInput.autocomplete = \"off\";\n            nameInput.value = employee.display_name || \"\";\n            nameInput.style.cssText = \"width:100%;max-width:380px;margin:6px 0;\";\n            nameLabel.appendChild(nameInput);\n            const saveButton = document.createElement(\"button\");\n            saveButton.type = \"button\";\n            saveButton.textContent = \"Save Name\";\n            const cancelButton = document.createElement(\"button\");\n            cancelButton.type = \"button\";\n            cancelButton.textContent = \"Cancel\";\n            cancelButton.style.marginLeft = \"8px\";\n            const rowMessage = document.createElement(\"p\");\n            rowMessage.setAttribute(\"role\", \"status\");\n            editor.append(nameLabel, saveButton, cancelButton, rowMessage);\n            editButton.addEventListener(\"click\", function () {\n              if (employeeDirectoryPanel.hidden || logout.hidden) return;\n              editor.hidden = !editor.hidden;\n              if (!editor.hidden) nameInput.focus();\n            });\n            cancelButton.addEventListener(\"click\", function () {\n              if (saveButton.disabled) return;\n              nameInput.value = employee.display_name || \"\";\n              rowMessage.textContent = \"\";\n              editor.hidden = true;\n            });\n            saveButton.addEventListener(\"click\", async function () {\n              if (!client || logout.hidden || employeeDirectoryPanel.hidden ||\n                  employeeDirectoryToggle.hidden || requestId !== employeeDirectoryRequestId ||\n                  saveButton.disabled) return;\n              const newName = nameInput.value.trim();\n              if (!newName || newName.length > 80) {\n                rowMessage.textContent = \"Enter a name between 1 and 80 characters.\";\n                return;\n              }\n              if (newName === (employee.display_name || \"\").trim()) {\n                rowMessage.textContent = \"Name is unchanged.\";\n                return;\n              }\n              if (!window.confirm(\"Change \" + (employee.display_name || \"this employee\") +\n                  \"'s display name to \" + newName + \"?\")) return;\n              saveButton.disabled = true;\n              cancelButton.disabled = true;\n              editButton.disabled = true;\n              rowMessage.textContent = \"Saving name...\";\n              try {\n                const { data: savedName, error: saveError } = await client.rpc(\n                  \"gw_owner_update_employee_name\",\n                  { p_user_id: employee.user_id, p_display_name: newName }\n                );\n                if (saveError) throw saveError;\n                if (savedName !== newName) throw new Error(\"Saved name could not be verified.\");\n                if (requestId !== employeeDirectoryRequestId || logout.hidden ||\n                    employeeDirectoryPanel.hidden) return;\n                const refreshed = await loadEmployeeDirectory();\n                if (refreshed && !employeeDirectoryPanel.hidden && !logout.hidden)\n                  employeeDirectoryMessage.textContent = \"Employee name saved: \" + savedName + \".\";\n              } catch (saveError) {\n                if (requestId === employeeDirectoryRequestId &&\n                    !employeeDirectoryPanel.hidden && !logout.hidden)\n                  rowMessage.textContent = \"Save failed: \" + saveError.message;\n              } finally {\n                saveButton.disabled = false;\n                cancelButton.disabled = false;\n                editButton.disabled = false;\n              }\n            });\n            accountPanel.append(editButton, editor);\n\n            const roleButton = document.createElement(\"button\");\n            roleButton.type = \"button\";\n            roleButton.textContent = \"Edit Role\";\n            roleButton.style.cssText = \"display:block;width:100%;margin:8px 0;\";\n            const roleEditor = document.createElement(\"div\");\n            roleEditor.hidden = true;\n            const roleLabel = document.createElement(\"label\");\n            roleLabel.textContent = \"Employee role\";\n            roleLabel.style.display = \"block\";\n            const roleSelect = document.createElement(\"select\");\n            roleSelect.style.cssText = \"width:100%;max-width:380px;margin:6px 0;\";\n            const roleChoices = [\n              [\"installer\", \"Installer\"],\n              [\"fabricator\", \"Fabricator\"],\n              [\"office\", \"Office\"],\n              [\"project_manager\", \"Project Manager\"]\n            ];\n            for (const [value, label] of roleChoices) {\n              const option = document.createElement(\"option\");\n              option.value = value;\n              option.textContent = label;\n              roleSelect.appendChild(option);\n            }\n            roleSelect.value = employee.role;\n            roleLabel.appendChild(roleSelect);\n            const saveRoleButton = document.createElement(\"button\");\n            saveRoleButton.type = \"button\";\n            saveRoleButton.textContent = \"Save Role\";\n            const cancelRoleButton = document.createElement(\"button\");\n            cancelRoleButton.type = \"button\";\n            cancelRoleButton.textContent = \"Cancel\";\n            cancelRoleButton.style.marginLeft = \"8px\";\n            const roleMessage = document.createElement(\"p\");\n            roleMessage.setAttribute(\"role\", \"status\");\n            roleEditor.append(roleLabel, saveRoleButton, cancelRoleButton, roleMessage);\n            roleButton.addEventListener(\"click\", function () {\n              if (employeeDirectoryPanel.hidden || logout.hidden) return;\n              roleEditor.hidden = !roleEditor.hidden;\n              if (!roleEditor.hidden) roleSelect.focus();\n            });\n            cancelRoleButton.addEventListener(\"click\", function () {\n              if (saveRoleButton.disabled) return;\n              roleSelect.value = employee.role;\n              roleMessage.textContent = \"\";\n              roleEditor.hidden = true;\n            });\n            saveRoleButton.addEventListener(\"click\", async function () {\n              if (!client || logout.hidden || employeeDirectoryPanel.hidden ||\n                  employeeDirectoryToggle.hidden || requestId !== employeeDirectoryRequestId ||\n                  saveRoleButton.disabled) return;\n              const newRole = roleSelect.value;\n              if (![\"installer\", \"fabricator\", \"office\", \"project_manager\"].includes(newRole)) {\n                roleMessage.textContent = \"Select a valid employee role.\";\n                return;\n              }\n              if (newRole === employee.role) {\n                roleMessage.textContent = \"Role is unchanged.\";\n                return;\n              }\n              const employeeName = employee.display_name || \"this employee\";\n              if (!window.confirm(\"Change \" + employeeName + \" from \" + employee.role +\n                  \" to \" + newRole + \"? This changes their access to Portal features. \" +\n                  \"Installers with assigned jobs cannot change to another role until assignments are removed.\")) return;\n              saveRoleButton.disabled = true;\n              cancelRoleButton.disabled = true;\n              roleButton.disabled = true;\n              roleSelect.disabled = true;\n              roleMessage.textContent = \"Saving role...\";\n              try {\n                const { data: savedRole, error: saveError } = await client.rpc(\n                  \"gw_owner_update_employee_role\",\n                  { p_user_id: employee.user_id, p_new_role: newRole }\n                );\n                if (saveError) throw saveError;\n                if (savedRole !== newRole) throw new Error(\"Saved role could not be verified.\");\n                if (requestId !== employeeDirectoryRequestId || logout.hidden ||\n                    employeeDirectoryPanel.hidden) return;\n                const refreshed = await loadEmployeeDirectory();\n                if (refreshed && !employeeDirectoryPanel.hidden && !logout.hidden) {\n                  employeeDirectoryMessage.textContent =\n                    \"Employee role saved: \" + employeeName + \" \u2014 \" + newRole + \".\";\n                }\n              } catch (saveError) {\n                if (requestId === employeeDirectoryRequestId &&\n                    !employeeDirectoryPanel.hidden && !logout.hidden) {\n                  roleMessage.textContent = \"Role change failed: \" + saveError.message;\n                }\n              } finally {\n                saveRoleButton.disabled = false;\n                cancelRoleButton.disabled = false;\n                roleButton.disabled = false;\n                roleSelect.disabled = false;\n              }\n            });\n            accountPanel.append(roleButton, roleEditor);\n\n            const accessButton = document.createElement(\"button\");\n            accessButton.type = \"button\";\n            accessButton.textContent = isActive\n              ? \"Disable Portal Access\" : \"Restore Portal Access\";\n            accessButton.style.cssText = \"display:block;width:100%;margin:8px 0;\";\n            const accessMessage = document.createElement(\"p\");\n            accessMessage.setAttribute(\"role\", \"status\");\n            accessButton.addEventListener(\"click\", async function () {\n              if (!client || logout.hidden || employeeDirectoryPanel.hidden ||\n                  employeeDirectoryToggle.hidden ||\n                  requestId !== employeeDirectoryRequestId ||\n                  employeeStatusBusy || accessButton.disabled) return;\n              const employeeName = employee.display_name || \"this employee\";\n              const nextStatus = !isActive;\n              const question = nextStatus\n                ? \"Restore Portal access for \" + employeeName +\n                  \"? Existing job assignments will become accessible again.\"\n                : \"Disable Portal access for \" + employeeName +\n                  \"? Their job assignments and records will remain saved.\";\n              if (!window.confirm(question)) return;\n              employeeStatusBusy = true;\n              accessButton.disabled = true;\n              accessMessage.textContent = nextStatus\n                ? \"Restoring Portal access...\" : \"Disabling Portal access...\";\n              try {\n                const { data: savedStatus, error: saveError } = await client.rpc(\n                  \"gw_owner_set_employee_active\",\n                  { p_user_id: employee.user_id, p_is_active: nextStatus }\n                );\n                if (saveError) throw saveError;\n                if (savedStatus !== nextStatus)\n                  throw new Error(\"Account status could not be verified.\");\n                if (requestId !== employeeDirectoryRequestId ||\n                    employeeDirectoryPanel.hidden || logout.hidden) return;\n                const refreshed = await loadEmployeeDirectory();\n                if (refreshed && !employeeDirectoryPanel.hidden && !logout.hidden) {\n                  employeeDirectoryMessage.textContent = employeeName +\n                    (nextStatus ? \" \u2014 Portal access restored.\" : \" \u2014 Portal access disabled.\");\n                }\n              } catch (saveError) {\n                if (requestId === employeeDirectoryRequestId &&\n                    !employeeDirectoryPanel.hidden && !logout.hidden)\n                  accessMessage.textContent = \"Status change failed: \" + saveError.message;\n              } finally {\n                employeeStatusBusy = false;\n                accessButton.disabled = false;\n              }\n            });\n            accountPanel.append(accessButton, accessMessage);\n          }\n\n          employeeDirectoryBody.appendChild(row);\n        }\n        employeeDirectoryMessage.textContent =\n          employees.length + \" account(s). Tap a name to open account controls. \" +\n          \"Owner account remains visible and read-only.\";\n        return true;\n      } catch (error) {\n        if (requestId !== employeeDirectoryRequestId ||\n            employeeDirectoryPanel.hidden || logout.hidden) return false;\n        employeeDirectoryBody.textContent = \"Unable to load employee accounts.\";\n        employeeDirectoryMessage.textContent = error.message;\n        return false;\n      }\n    }\n    employeeDirectoryToggle.addEventListener(\"click\", async function () {\n      if (employeeDirectoryToggle.hidden || !client || logout.hidden) return;\n      employeeDirectoryPanel.hidden = !employeeDirectoryPanel.hidden;\n      employeeDirectoryToggle.setAttribute(\n        \"aria-expanded\", String(!employeeDirectoryPanel.hidden)\n      );\n      if (employeeDirectoryPanel.hidden) {\n        employeeDirectoryRequestId++;\n        resetInviteForm();\n        return;\n      }\n      await loadEmployeeDirectory();\n    });\n\n\n    \/\/ ================================================================\n    \/\/ OWNER-ONLY ANNUAL JOB PROBLEM REPORT\n    \/\/ The RPC gw_owner_annual_problem_report also verifies active Owner access\n    \/\/ on the server, so hiding this button is not the security boundary.\n    \/\/ ================================================================\n    let annualProblemAllowed = false;\n    let annualProblemRequestId = 0;\n\n    const annualProblemToggle = document.createElement(\"button\");\n    annualProblemToggle.type = \"button\";\n    annualProblemToggle.id = \"gw-annual-problem-toggle\";\n    annualProblemToggle.textContent = \"Annual Issues Report (Owner Only)\";\n    annualProblemToggle.hidden = true;\n    annualProblemToggle.setAttribute(\"aria-expanded\", \"false\");\n    annualProblemToggle.style.cssText =\n      \"width:100%;margin:12px 0;padding:12px;font-size:17px;font-weight:700;\";\n\n    const annualProblemPanel = document.createElement(\"section\");\n    annualProblemPanel.id = \"gw-annual-problem-panel\";\n    annualProblemPanel.hidden = true;\n    annualProblemPanel.style.cssText =\n      \"box-sizing:border-box;width:100%;margin:8px 0 18px;padding:16px;\" +\n      \"border:1px solid #cbd5e1;border-radius:10px;background:#fff;overflow-wrap:anywhere;\";\n\n    const annualProblemHeading = document.createElement(\"h3\");\n    annualProblemHeading.textContent = \"Annual Job Problem Report\";\n    annualProblemHeading.style.cssText = \"margin:0 0 12px;font-size:24px;\";\n\n    const annualProblemControls = document.createElement(\"div\");\n    annualProblemControls.style.cssText =\n      \"display:flex;align-items:end;gap:10px;flex-wrap:wrap;margin:8px 0 14px;\";\n\n    const annualProblemYearLabel = document.createElement(\"label\");\n    annualProblemYearLabel.textContent = \"Year\";\n    annualProblemYearLabel.style.cssText = \"display:block;font-weight:600;\";\n    const annualProblemYear = document.createElement(\"input\");\n    annualProblemYear.type = \"number\";\n    annualProblemYear.min = \"2020\";\n    annualProblemYear.max = \"2100\";\n    annualProblemYear.step = \"1\";\n    annualProblemYear.value = String(new Date().getFullYear());\n    annualProblemYear.style.cssText =\n      \"display:block;width:120px;box-sizing:border-box;font-size:17px;padding:8px;margin-top:5px;\";\n    annualProblemYearLabel.appendChild(annualProblemYear);\n\n    const annualProblemLoad = document.createElement(\"button\");\n    annualProblemLoad.type = \"button\";\n    annualProblemLoad.textContent = \"Load Report\";\n    annualProblemLoad.style.cssText = \"padding:9px 14px;font-size:16px;\";\n\n    annualProblemControls.append(annualProblemYearLabel, annualProblemLoad);\n\n    const annualProblemMessage = document.createElement(\"p\");\n    annualProblemMessage.setAttribute(\"role\", \"status\");\n    annualProblemMessage.setAttribute(\"aria-live\", \"polite\");\n    annualProblemMessage.style.cssText = \"margin:8px 0 12px;\";\n\n    const annualProblemSummary = document.createElement(\"div\");\n    annualProblemSummary.id = \"gw-annual-problem-summary\";\n    annualProblemSummary.style.cssText =\n      \"display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));\" +\n      \"gap:10px;margin:10px 0 16px;\";\n\n    const annualProblemBreakdowns = document.createElement(\"div\");\n    annualProblemBreakdowns.style.cssText =\n      \"display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));\" +\n      \"gap:14px;margin:10px 0 18px;\";\n\n    const annualProblemListHeading = document.createElement(\"h4\");\n    annualProblemListHeading.textContent = \"Report Details\";\n    annualProblemListHeading.style.cssText = \"margin:18px 0 8px;font-size:20px;\";\n\n    const annualProblemList = document.createElement(\"div\");\n    annualProblemList.id = \"gw-annual-problem-list\";\n\n    annualProblemPanel.append(\n      annualProblemHeading,\n      annualProblemControls,\n      annualProblemMessage,\n      annualProblemSummary,\n      annualProblemBreakdowns,\n      annualProblemListHeading,\n      annualProblemList\n    );\n\n    function annualProblemMetric(label, value) {\n      const box = document.createElement(\"div\");\n      box.style.cssText =\n        \"padding:12px;border:1px solid #dbe3ea;border-radius:8px;background:#f8fafc;\";\n      const title = document.createElement(\"div\");\n      title.textContent = label;\n      title.style.cssText = \"font-size:13px;color:#526579;margin-bottom:4px;\";\n      const number = document.createElement(\"strong\");\n      number.textContent = String(value);\n      number.style.cssText = \"font-size:22px;\";\n      box.append(title, number);\n      return box;\n    }\n\n    function annualProblemBreakdownBox(titleText, counts) {\n      const box = document.createElement(\"section\");\n      box.style.cssText =\n        \"padding:12px;border:1px solid #dbe3ea;border-radius:8px;background:#fff;\";\n      const title = document.createElement(\"h4\");\n      title.textContent = titleText;\n      title.style.cssText = \"margin:0 0 8px;font-size:17px;\";\n      box.appendChild(title);\n\n      const entries = Array.from(counts.entries()).sort(function (a, b) {\n        if (b[1] !== a[1]) return b[1] - a[1];\n        return String(a[0]).localeCompare(String(b[0]));\n      });\n\n      if (entries.length === 0) {\n        const empty = document.createElement(\"p\");\n        empty.textContent = \"None\";\n        empty.style.margin = \"0\";\n        box.appendChild(empty);\n        return box;\n      }\n\n      for (const entry of entries) {\n        const row = document.createElement(\"div\");\n        row.style.cssText =\n          \"display:flex;justify-content:space-between;gap:12px;padding:5px 0;\" +\n          \"border-bottom:1px solid #edf2f7;\";\n        const name = document.createElement(\"span\");\n        name.textContent = entry[0];\n        const count = document.createElement(\"strong\");\n        count.textContent = String(entry[1]);\n        row.append(name, count);\n        box.appendChild(row);\n      }\n      return box;\n    }\n\n    function annualProblemDate(value) {\n      if (!value) return \"Not recorded\";\n      const date = new Date(value);\n      if (Number.isNaN(date.getTime())) return \"Not recorded\";\n      return date.toLocaleString([], {\n        year: \"numeric\",\n        month: \"short\",\n        day: \"numeric\",\n        hour: \"numeric\",\n        minute: \"2-digit\"\n      });\n    }\n\n    function renderAnnualProblemReport(rows, year) {\n      annualProblemSummary.replaceChildren();\n      annualProblemBreakdowns.replaceChildren();\n      annualProblemList.replaceChildren();\n\n      let openCount = 0;\n      let resolvedCount = 0;\n      let totalReworkCost = 0;\n      const issueCounts = new Map();\n      const workerCounts = new Map();\n\n      for (const row of rows) {\n        if (row.status === \"Open\") openCount++;\n        if (row.status === \"Resolved\") resolvedCount++;\n\n        const cost = Number(row.rework_cost);\n        if (Number.isFinite(cost)) totalReworkCost += cost;\n\n        const issueName = row.issue_type || \"Other\";\n        issueCounts.set(issueName, (issueCounts.get(issueName) || 0) + 1);\n\n        const workerRows = Array.isArray(row.workers_json) ? row.workers_json : [];\n        if (workerRows.length > 0) {\n          for (const worker of workerRows) {\n            const workerName =\n              worker && worker.name ? String(worker.name).trim() : \"\";\n            if (workerName) {\n              workerCounts.set(workerName, (workerCounts.get(workerName) || 0) + 1);\n            }\n          }\n        } else if (row.worker_names) {\n          const workerName = String(row.worker_names).trim();\n          if (workerName) {\n            workerCounts.set(workerName, (workerCounts.get(workerName) || 0) + 1);\n          }\n        }\n      }\n\n      annualProblemSummary.append(\n        annualProblemMetric(\"Year\", year),\n        annualProblemMetric(\"Total Issues\", rows.length),\n        annualProblemMetric(\"Open\", openCount),\n        annualProblemMetric(\"Resolved\", resolvedCount),\n        annualProblemMetric(\"Rework Cost\", \"$\" + totalReworkCost.toFixed(2))\n      );\n\n      annualProblemBreakdowns.append(\n        annualProblemBreakdownBox(\"Issues by Type\", issueCounts),\n        annualProblemBreakdownBox(\"Issues by Worker\", workerCounts)\n      );\n\n      if (rows.length === 0) {\n        const empty = document.createElement(\"p\");\n        empty.textContent = \"No problem reports found for \" + year + \".\";\n        annualProblemList.appendChild(empty);\n        return;\n      }\n\n      for (const row of rows) {\n        const card = document.createElement(\"section\");\n        card.style.cssText =\n          \"padding:14px 0;border-bottom:1px solid #dbe3ea;overflow-wrap:anywhere;\";\n\n        const heading = document.createElement(\"h4\");\n        heading.textContent =\n          \"Report #\" + row.id + \" \u2014 \" + (row.issue_type || \"Issue\") +\n          \" \u2014 \" + (row.status || \"Unknown\");\n        heading.style.cssText = \"margin:0 0 7px;font-size:18px;\";\n\n        const meta = document.createElement(\"p\");\n        meta.textContent =\n          \"Job ID: \" + (row.job_id || \"Not recorded\") +\n          \" | Reported: \" + annualProblemDate(row.reported_at) +\n          \" | By: \" + (row.reported_by_name || \"Not recorded\");\n        meta.style.cssText = \"margin:4px 0;font-size:14px;color:#526579;\";\n\n        const workers = document.createElement(\"p\");\n        workers.textContent =\n          \"Worker(s): \" + (row.worker_names || \"Not recorded\");\n        workers.style.cssText = \"margin:6px 0;\";\n\n        const description = document.createElement(\"p\");\n        description.textContent = row.description || \"\";\n        description.style.cssText =\n          \"white-space:pre-wrap;margin:8px 0;font-size:16px;line-height:1.45;\";\n\n        card.append(heading, meta, workers, description);\n\n        if (row.action_needed) {\n          const action = document.createElement(\"p\");\n          action.textContent = \"Action Needed: \" + row.action_needed;\n          action.style.cssText = \"white-space:pre-wrap;margin:6px 0;\";\n          card.appendChild(action);\n        }\n\n        if (row.rework_cost !== null && row.rework_cost !== undefined && row.rework_cost !== \"\") {\n          const costValue = Number(row.rework_cost);\n          const cost = document.createElement(\"p\");\n          cost.textContent =\n            \"Rework Cost: \" +\n            (Number.isFinite(costValue) ? \"$\" + costValue.toFixed(2) : String(row.rework_cost));\n          cost.style.cssText = \"margin:6px 0;\";\n          card.appendChild(cost);\n        }\n\n        if (row.notes) {\n          const notes = document.createElement(\"p\");\n          notes.textContent = \"Internal Notes: \" + row.notes;\n          notes.style.cssText = \"white-space:pre-wrap;margin:6px 0;\";\n          card.appendChild(notes);\n        }\n\n        if (row.status === \"Resolved\") {\n          const resolved = document.createElement(\"p\");\n          resolved.textContent =\n            \"Resolved: \" + annualProblemDate(row.resolved_at) +\n            \" | By: \" + (row.resolved_by_name || \"Not recorded\");\n          resolved.style.cssText = \"margin:6px 0;font-size:14px;color:#526579;\";\n          card.appendChild(resolved);\n\n          const resolution = document.createElement(\"p\");\n          resolution.textContent =\n            \"Resolution Notes: \" + (row.resolution_notes || \"None\");\n          resolution.style.cssText = \"white-space:pre-wrap;margin:6px 0;\";\n          card.appendChild(resolution);\n        }\n\n        annualProblemList.appendChild(card);\n      }\n    }\n\n    async function loadAnnualProblemReport() {\n      if (!annualProblemAllowed) return;\n      if (!client) return;\n      if (logout.hidden) return;\n\n      const year = Number(annualProblemYear.value);\n      if (!Number.isInteger(year) || year < 2020 || year > 2100) {\n        annualProblemMessage.textContent = \"Enter a valid year from 2020 to 2100.\";\n        return;\n      }\n\n      const requestId = ++annualProblemRequestId;\n      annualProblemLoad.disabled = true;\n      annualProblemYear.disabled = true;\n      annualProblemMessage.textContent = \"Loading \" + year + \" report...\";\n\n      try {\n        const result = await client.rpc(\n          \"gw_owner_annual_problem_report\",\n          { p_year: year }\n        );\n        if (result.error) throw result.error;\n        if (requestId !== annualProblemRequestId) return;\n        if (!annualProblemAllowed) return;\n        if (logout.hidden) return;\n\n        const rows = Array.isArray(result.data) ? result.data : [];\n        renderAnnualProblemReport(rows, year);\n        annualProblemMessage.textContent =\n          rows.length + \" issue report(s) found for \" + year + \".\";\n      } catch (error) {\n        if (requestId !== annualProblemRequestId) return;\n        annualProblemSummary.replaceChildren();\n        annualProblemBreakdowns.replaceChildren();\n        annualProblemList.replaceChildren();\n        annualProblemMessage.textContent =\n          \"Unable to load annual report: \" + error.message;\n      } finally {\n        if (requestId === annualProblemRequestId) {\n          if (!logout.hidden) {\n            annualProblemLoad.disabled = false;\n            annualProblemYear.disabled = false;\n          }\n        }\n      }\n    }\n\n    function resetAnnualProblemReport() {\n      annualProblemRequestId++;\n      annualProblemToggle.hidden = true;\n      annualProblemToggle.setAttribute(\"aria-expanded\", \"false\");\n      annualProblemPanel.hidden = true;\n      annualProblemLoad.disabled = false;\n      annualProblemYear.disabled = false;\n      annualProblemMessage.textContent = \"\";\n      annualProblemSummary.replaceChildren();\n      annualProblemBreakdowns.replaceChildren();\n      annualProblemList.replaceChildren();\n    }\n\n    annualProblemToggle.addEventListener(\"click\", function () {\n      if (!annualProblemAllowed) return;\n      if (logout.hidden) return;\n\n      annualProblemPanel.hidden = !annualProblemPanel.hidden;\n      annualProblemToggle.setAttribute(\n        \"aria-expanded\",\n        String(!annualProblemPanel.hidden)\n      );\n\n      if (!annualProblemPanel.hidden) {\n        void loadAnnualProblemReport();\n      }\n    });\n\n    annualProblemLoad.addEventListener(\"click\", function () {\n      void loadAnnualProblemReport();\n    });\n\n    annualProblemYear.addEventListener(\"keydown\", function (event) {\n      if (event.key === \"Enter\") {\n        event.preventDefault();\n        void loadAnnualProblemReport();\n      }\n    });\n\n  let authViewRequestId = 0;\n  async function showSignedIn() {\n    if (recoveringPassword) { showPasswordSetup(); return; }\n    const requestId = ++authViewRequestId;\n    form.hidden = true;\n    logout.hidden = true;\n    jobSearch.hidden = true;\n    dashboard.hidden = true;\n    resetCreateJob();\n    resetCustomerCodeView();\n    resetPhotoApprovalView();\n    resetDrawingView();\n    resetProblemView();\n    annualProblemAllowed = false;\n    resetAnnualProblemReport();\n    message.textContent = \"Checking Portal access...\";\n    try {\n      const { data: auth, error: authError } = await client.auth.getUser();\n      if (authError || !auth.user) throw new Error(\"Please log in again.\");\n      const { data: isActive, error: accessError } =\n        await client.rpc(\"gw_employee_is_active\");\n      if (accessError) throw new Error(\"Unable to verify Portal access. Please try again.\");\n      if (requestId !== authViewRequestId) return;\n      if (isActive !== true) {\n        let signOutFailed = false;\n        try {\n          const { error: signOutError } = await client.auth.signOut();\n          signOutFailed = !!signOutError;\n        } catch (_error) {\n          signOutFailed = true;\n        }\n        if (requestId !== authViewRequestId) return;\n        showSignedOut();\n        message.textContent = \"This account's Portal access is disabled. Contact the office.\" +\n          (signOutFailed ? \" Sign-out failed; close this browser and contact the office.\" : \"\");\n        return;\n      }\n      \/\/ Resolve the employee role at login so Office Accounting is a true\n      \/\/ Portal-level entry instead of appearing only after a successful Job search.\n      const { data: accountingProfile, error: accountingProfileError } = await client\n        .from(\"employee_profiles\")\n        .select(\"role\")\n        .eq(\"user_id\", auth.user.id)\n        .maybeSingle();\n      if (requestId !== authViewRequestId) return;\n      accountingRoleAllowed = !accountingProfileError && !!accountingProfile &&\n        [\"owner\", \"office\"].includes(accountingProfile.role);\n      annualProblemAllowed =\n        !accountingProfileError &&\n        !!accountingProfile &&\n        accountingProfile.role === \"owner\";\n\n      \/\/ Office is handled outside the Field Portal.\n      \/\/ Keep the legacy accounting code available, but do not show its button here.\n      accountingToggle.hidden = true;\n      accountingPanel.hidden = true;\n      accountingToggle.setAttribute(\"aria-expanded\", \"false\");\n      if (accountingToggle.isConnected) accountingToggle.remove();\n      if (accountingPanel.isConnected) accountingPanel.remove();\n\n      accountingPanel.insertAdjacentElement(\"afterend\", annualProblemToggle);\n      annualProblemToggle.insertAdjacentElement(\"afterend\", annualProblemPanel);\n      annualProblemToggle.hidden = !annualProblemAllowed;\n      annualProblemPanel.hidden = true;\n      annualProblemToggle.setAttribute(\"aria-expanded\", \"false\");\n\n      form.hidden = true;\n      logout.hidden = false;\n      jobSearch.hidden = false;\n      monthlyJobPanel.hidden = false;\n      void loadMonthlyJobIds();\n      message.textContent = \"Login successful!\";\n      bulletinEntry.hidden = false;\n      bulletinEntry.textContent = \"Bulletin Board\";\n      void checkEmployeeDirectoryAccess();\n       void checkCreateJobAccess();\n      void loadBulletinBoard();\n    } catch (error) {\n      if (requestId !== authViewRequestId) return;\n      try { await client.auth.signOut(); } catch (_error) { \/* fail closed *\/ }\n      if (requestId !== authViewRequestId) return;\n      showSignedOut();\n      message.textContent = error.message || \"Unable to verify Portal access.\";\n    }\n  }\n\n    function showSignedOut() {\n      clearRecoveryMarker();\n      recoveringPassword = false;\n      setPasswordForm.hidden = true;\n      authViewRequestId++;\n       resetEmployeeDirectory();\n       resetCreateJob();\n       resetCustomerCodeView();\n       resetPhotoApprovalView();\n       resetDrawingView();\n       resetProblemView();\n       annualProblemAllowed = false;\n       resetAnnualProblemReport();\n       resetBulletinBoard();\n    form.hidden = false;\n      logout.hidden = true;\n   jobSearch.hidden = true;\nmonthlyJobPanel.hidden = true;\nmonthlyJobList.replaceChildren();\ndashboard.hidden = true;\naccountingRoleAllowed = false;\nresetAccounting();\nresetCompletion();\nresetOwnerAssignments();\nresetWorkDashboards();\nresetPhotoView();\nif (!photoUploadBusy) clearPendingPhotos();\nallReviewsPanel.hidden = true;\nallReviewsContent.replaceChildren();\nmeasurementLoadedJobId = null;\ndocument.getElementById(\"gw-save-measurement\").disabled = true;\ndocument.getElementById(\"gw-review-notes\").value = \"\";\ndocument.getElementById(\"gw-review-confirmed\").checked = false;\ndocument.getElementById(\"gw-review-ready\").checked = false;\ndocument.getElementById(\"gw-save-review\").disabled = true;\n      message.textContent = \"You have logged out.\";\n    }\n\n    \/\/ Stop the form from navigating to the company homepage.\n    form.addEventListener(\"submit\", async function (event) {\n      event.preventDefault();\n\n      if (!client) {\n        message.textContent = \"Login system is still loading. Please wait.\";\n        return;\n      }\n\n      staySignedIn = remember.checked;\n      message.textContent = \"Signing in...\";\n\n      try {\n        const { error } = await client.auth.signInWithPassword({\n          email: email.value.trim(),\n          password: password.value\n        });\n\n        password.value = \"\";\n\n        if (error) {\n          message.textContent =\n            \"Login failed. Please check your email and password.\";\n          return;\n        }\n\n        await showSignedIn();\n      } catch (error) {\n        password.value = \"\";\n        message.textContent = \"Connection error. Please try again.\";\n      }\n    });\n\n    logout.addEventListener(\"click\", async function () {\n      if (!client) return;\n\n      const { error } = await client.auth.signOut();\n\n      if (error) {\n        message.textContent = \"Unable to log out. Please try again.\";\n        return;\n      }\n\n      showSignedOut();\n    });\n\n    message.textContent = \"Loading login system...\";\n\n    const script = document.createElement(\"script\");\n    script.src =\n      \"https:\/\/cdn.jsdelivr.net\/npm\/@supabase\/supabase-js@2\";\n\n    script.onload = async function () {\n      try {\n        const storage = {\n          getItem(key) {\n            return localStorage.getItem(key) ||\n                   sessionStorage.getItem(key);\n          },\n          setItem(key, value) {\n            localStorage.removeItem(key);\n            sessionStorage.removeItem(key);\n\n            const destination = staySignedIn\n              ? localStorage\n              : sessionStorage;\n\n            destination.setItem(key, value);\n          },\n          removeItem(key) {\n            localStorage.removeItem(key);\n            sessionStorage.removeItem(key);\n          }\n        };\n\n        client = window.supabase.createClient(URL, KEY, {\n          auth: {\n            storage,\n            storageKey: STORAGE_KEY,\n            persistSession: true,\n            autoRefreshToken: true,\n            detectSessionInUrl: true\n          }\n        });\n           \/\/ PASSWORD_RECOVERY fires when Supabase processes the emailed recovery URL.\n        \/\/ Register before checking session to avoid routing the user to the dashboard.\n        client.auth.onAuthStateChange((event, session) => {\n          if (event === \"PASSWORD_RECOVERY\" && session && session.user && session.user.id) {\n            saveRecoveryMarker(session.user.id);\n            showPasswordSetup();\n          }\n        });\n        const urlFlags = new URLSearchParams(window.location.hash.replace(\/^#\/, \"\"));\n        const queryFlags = new URLSearchParams(window.location.search);\n        if (urlFlags.get(\"type\") === \"recovery\" || queryFlags.get(\"type\") === \"recovery\") {\n          recoveringPassword = true;\n        }\n        const { data, error } = await client.auth.getSession();\n        const marker = getRecoveryMarker();\n        if (error) {\n          message.textContent = \"Unable to check login status.\";\n        } else if (data.session && data.session.user &&\n                   (recoveringPassword || (marker && marker.userId === data.session.user.id))) {\n          \/\/ A recovery session must still belong to the same authenticated user.\n          \/\/ A stale marker alone never authenticates anybody.\n          const { data: verified, error: verifyError } = await client.auth.getUser();\n          if (!verifyError && verified.user && verified.user.id === data.session.user.id) {\n            if (recoveringPassword) saveRecoveryMarker(verified.user.id);\n            showPasswordSetup();\n          } else {\n            message.textContent = \"Unable to verify the password setup session. Reopen your email link or request a new one.\";\n          }\n        } else if (data.session) {\n          if (marker && marker.userId !== data.session.user.id) clearRecoveryMarker();\n          await showSignedIn();\n        } else if (recoveringPassword || marker) {\n          \/\/ Do not show password-entry UI without a valid Supabase session.\n          clearRecoveryMarker();\n          recoveringPassword = false;\n          form.hidden = false;\n              message.textContent = \"Password setup session is no longer available. Reopen your email link or request a new one.\";\n        } else {\n          message.textContent = \"Please log in.\";\n        }\n      } catch (error) {\n        message.textContent = \"Unable to initialize login system.\";\n      }\n    };\n\n    script.onerror = function () {\n      message.textContent = \"Unable to load login system.\";\n    };\n    let recoveryResumeBusy = false;\n    async function restoreRecoveryAfterAppSwitch() {\n      if (!client || recoveryResumeBusy || savePassword.disabled) return;\n      const marker = getRecoveryMarker();\n      if (!marker) return;\n      recoveryResumeBusy = true;\n      try {\n        const { data: sessionResult, error: sessionError } = await client.auth.getSession();\n        if (sessionError || !sessionResult.session || !sessionResult.session.user ||\n            sessionResult.session.user.id !== marker.userId) return;\n        const { data: userResult, error: userError } = await client.auth.getUser();\n        if (!userError && userResult.user && userResult.user.id === marker.userId) {\n          if (!getRecoveryMarker()) return; \/\/ User may have completed setup while we awaited.\n          showPasswordSetup();\n        }\n      } catch (_error) { \/* leave current UI unchanged; user can reopen the email link *\/ }\n      finally { recoveryResumeBusy = false; }\n    }\n    window.addEventListener(\"pageshow\", function () {\n      void restoreRecoveryAfterAppSwitch();\n    });\n    document.addEventListener(\"visibilitychange\", function () {\n      if (!document.hidden) void restoreRecoveryAfterAppSwitch();\n    });\nconst findJob = document.getElementById(\"gw-find-job\");\nconst jobIdInput = document.getElementById(\"gw-job-id\");\nconst jobResult = document.getElementById(\"gw-job-result\");\nconst dashboard = document.getElementById(\"gw-job-dashboard\");\n\n\/\/ ================================================================\n\/\/ THIS MONTH JOB ID LIST\n\/\/ Purpose: show only Job IDs already registered in the Portal,\n\/\/ so staff can quickly avoid duplicate Job ID entry.\n\/\/ ================================================================\nconst monthlyJobPanel = document.createElement(\"section\");\nmonthlyJobPanel.id = \"gw-monthly-job-ids\";\nmonthlyJobPanel.hidden = true;\nmonthlyJobPanel.style.cssText =\n  \"box-sizing:border-box;width:100%;margin:12px 0 16px;padding:8px 12px;\" +\n  \"border:1px solid #cbd5e1;border-radius:10px;background:#fff;\";\n\nconst monthlyJobToggle = document.createElement(\"button\");\nmonthlyJobToggle.type = \"button\";\nmonthlyJobToggle.id = \"gw-monthly-job-toggle\";\nmonthlyJobToggle.setAttribute(\"aria-expanded\", \"false\");\nmonthlyJobToggle.setAttribute(\"aria-controls\", \"gw-monthly-job-list\");\nmonthlyJobToggle.style.cssText =\n  \"display:flex;align-items:center;justify-content:space-between;gap:12px;\" +\n  \"box-sizing:border-box;width:100%;padding:10px 6px;margin:0;\" +\n  \"font-size:18px;font-weight:700;color:#0b4f8a;text-align:left;\" +\n  \"background:transparent;border:0;cursor:pointer;\";\nconst monthlyJobTitle = document.createElement(\"span\");\nconst monthlyJobArrow = document.createElement(\"span\");\nmonthlyJobArrow.textContent = \"\u25b8\";\nmonthlyJobArrow.setAttribute(\"aria-hidden\", \"true\");\nmonthlyJobToggle.append(monthlyJobTitle, monthlyJobArrow);\n\nconst monthlyJobList = document.createElement(\"div\");\nmonthlyJobList.id = \"gw-monthly-job-list\";\nmonthlyJobList.hidden = true;\nmonthlyJobList.style.cssText =\n  \"display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,290px),1fr));\" +\n  \"gap:8px 12px;font-size:16px;line-height:1.5;font-weight:600;padding:6px 0 8px;\";\nmonthlyJobList.style.setProperty(\"display\", \"none\");\n\nlet monthlyJobCount = null;\nlet monthlyJobLoadSeq = 0;\nconst monthlyJobWords = {\n  en: [\"This Month Jobs\", \"Loading...\", \"No jobs registered this month.\", \"Unable to load this month's jobs.\", \"Address not entered\"],\n  zh: [\"\u672c\u6708\u5de5\u7a0b\", \"\u8f09\u5165\u4e2d...\", \"\u672c\u6708\u5c1a\u672a\u767b\u8a18\u5de5\u7a0b\u3002\", \"\u7121\u6cd5\u8f09\u5165\u672c\u6708\u5de5\u7a0b\u3002\", \"\u5c1a\u672a\u8f38\u5165\u5730\u5740\"],\n  es: [\"Trabajos de este mes\", \"Cargando...\", \"No hay trabajos registrados este mes.\", \"No se pudieron cargar los trabajos de este mes.\", \"Direcci\u00f3n pendiente\"]\n};\nfunction monthlyWords() {\n  return monthlyJobWords[getGwPortalLanguage()] || monthlyJobWords.en;\n}\nfunction updateMonthlyJobTitle() {\n  const name = monthlyWords()[0];\n  monthlyJobTitle.textContent = name + (monthlyJobCount === null ? \"\" : \" (\" + monthlyJobCount + \")\");\n  monthlyJobArrow.textContent = monthlyJobList.hidden ? \"\u25b8\" : \"\u25be\";\n}\nfunction setMonthlyJobExpanded(open) {\n  monthlyJobList.hidden = !open;\n  monthlyJobList.style.setProperty(\"display\", open ? \"grid\" : \"none\");\n  monthlyJobToggle.setAttribute(\"aria-expanded\", String(open));\n  updateMonthlyJobTitle();\n}\nmonthlyJobToggle.addEventListener(\"click\", function () {\n  setMonthlyJobExpanded(monthlyJobList.hidden);\n});\nmonthlyJobPanel.append(monthlyJobToggle, monthlyJobList);\njobSearch.insertAdjacentElement(\"afterend\", monthlyJobPanel);\nupdateMonthlyJobTitle();\n\nasync function loadMonthlyJobIds() {\n  if (!client || logout.hidden) return;\n  const seq = ++monthlyJobLoadSeq;\n  const now = new Date();\n  const yy = String(now.getFullYear()).slice(-2);\n  const mm = String(now.getMonth() + 1).padStart(2, \"0\");\n  const prefix = \"GW-\" + yy + mm + \"-\";\n  const words = monthlyWords();\n  monthlyJobList.textContent = words[1];\n  try {\n    \/\/ RLS limits accessible Jobs. Keep addresses as stored, never translate them.\n    const { data, error } = await client.from(\"Jobs\")\n      .select(\"job_id,job_address\")\n      .ilike(\"job_id\", prefix + \"%\")\n      .order(\"job_id\", { ascending: true });\n    if (error) throw error;\n    if (logout.hidden || seq !== monthlyJobLoadSeq) return;\n    monthlyJobList.replaceChildren();\n    const rows = (Array.isArray(data) ? data : []).filter(function (row) {\n      return String(row && row.job_id || \"\").startsWith(prefix);\n    });\n    monthlyJobCount = rows.length;\n    updateMonthlyJobTitle();\n    if (!rows.length) { monthlyJobList.textContent = monthlyWords()[2]; return; }\n    rows.forEach(function (row) {\n      const card = document.createElement(\"div\");\n      card.style.cssText = \"box-sizing:border-box;width:100%;padding:9px 12px;border:1px solid #dbe3ea;border-radius:8px;background:#f8fafc;\";\n      const jobId = document.createElement(\"strong\");\n      jobId.textContent = String(row.job_id);\n      jobId.style.cssText = \"display:block;color:#0b4f8a;font-size:16px;\";\n      const address = document.createElement(\"span\");\n      address.textContent = String(row.job_address || \"\").trim() || monthlyWords()[4];\n      address.style.cssText = \"display:block;font-size:15px;font-weight:400;overflow-wrap:anywhere;\";\n      card.append(jobId, address);\n      monthlyJobList.appendChild(card);\n    });\n  } catch (error) {\n    if (!logout.hidden && seq === monthlyJobLoadSeq) monthlyJobList.textContent = monthlyWords()[3];\n    console.error(\"Monthly jobs with addresses:\", error);\n  }\n}\n\nwindow.addEventListener(\"gw-language-changed\", function () {\n  updateMonthlyJobTitle();\n  if (!logout.hidden && !monthlyJobPanel.hidden) void loadMonthlyJobIds();\n});\n\n\/\/ A measurement is editable only after its job's saved data has loaded.\nlet measurementLoadedJobId = null;\n\/\/ Role-scoped dashboard and editor state, reset on each job and logout.\nlet currentWorkRole = null;\nlet currentWorkJobId = null;\nlet measurementLoadOK = false;\nlet preparationLoadOK = false;\n\n\n\/\/ Press Enter in the Job ID field to run the same search as the Find Job button.\n\/\/ Prevent the surrounding WordPress form from submitting or navigating away.\njobIdInput.addEventListener(\"keydown\", function (event) {\n  if (event.key !== \"Enter\" || event.isComposing) return;\n  event.preventDefault();\n  if (!event.repeat && !findJob.disabled) findJob.click();\n});\n\nfindJob.addEventListener(\"click\", async function () {\n  const jobId = jobIdInput.value.trim();\n  resetAccounting();\n    resetCompletion();\n  resetOwnerAssignments();\n  resetWorkDashboards();\n  resetPhotoView();\n  if (!photoUploadBusy) clearPendingPhotos();\n  resetCustomerCodeView();\n  resetPhotoApprovalView();\n  resetDrawingView();\n  resetProblemView();\n  measurementLoadedJobId = null;\n  document.getElementById(\"gw-save-measurement\").disabled = true;\n  document.getElementById(\"gw-measurement-message\").textContent = \"\";\n  \/\/ Prevent measurements from a previous job appearing under this job.\n  document.querySelectorAll(\n    \"#gw-measurement-form input, #gw-measurement-form textarea, #gw-measurement-form select\"\n  ).forEach(function (field) {\n    if (field.type === \"checkbox\") field.checked = false;\n    else field.value = \"\";\n  });\n  \/\/ Clear the previous employee's review before every job search.\n  document.getElementById(\"gw-review-notes\").value = \"\";\n  document.getElementById(\"gw-review-confirmed\").checked = false;\n  document.getElementById(\"gw-review-ready\").checked = false;\n  document.getElementById(\"gw-save-review\").disabled = true;\n  document.getElementById(\"gw-review-message\").textContent = \"\";\n   allReviewsPanel.hidden = true;\n   allReviewsContent.replaceChildren();\ndocument\n  .querySelectorAll(\n    \"#gw-preparation-form input, #gw-preparation-form textarea, #gw-preparation-form select\"\n  )\n  .forEach(function (field) {\n    if (field.type === \"checkbox\") {\n      field.checked = false;\n    } else {\n      field.value = \"\";\n    }\n  });\n  if (!jobId || !client) {\n    jobResult.textContent = \"Please enter a Job ID.\";\n    return;\n  }\ndashboard.hidden = true;\ndocument.getElementById(\"gw-save-preparation\").disabled = true;\n  findJob.disabled = true;\n  jobResult.textContent = \"Searching...\";\n\n  try {\n    const { data: { user }, error: authError } =\n      await client.auth.getUser();\n\n     if (authError || !user) {\n       jobResult.textContent = \"Please log in again.\";\n       return;\n    }\n\n     \/\/ Immediately recheck on a new search: existing sessions can be disabled.\n     const { data: isActive, error: accessError } =\n       await client.rpc(\"gw_employee_is_active\");\n     if (accessError) {\n       jobResult.textContent = \"Unable to verify Portal access. Please try again.\";\n       return;\n     }\n     if (isActive !== true) {\n       try { await client.auth.signOut(); } catch (_error) { \/* deny UI access *\/ }\n       showSignedOut();\n       message.textContent = \"This account's Portal access is disabled. Contact the office.\";\n       return;\n     }\n\n     const { data, error } = await client\n      .from(\"Jobs\")\n      .select(\"job_id, customer_name, customer_phone, job_address, job_status, completed_at\")\n      .eq(\"job_id\", jobId)\n      .maybeSingle();\n\n    if (error) {\n      jobResult.textContent = \"Search error: \" + error.message;\n    } else if (!data) {\n      jobResult.textContent = \"Job not found.\";\n    } else {\n      jobResult.textContent =\n        \"Job ID: \" + data.job_id + \"\\n\" +\n        \"Customer: \" + (data.customer_name || \"\") + \"\\n\" +\n        \"Phone: \" + (data.customer_phone || \"\") + \"\\n\" +\n        \"Address: \" + (data.job_address || \"\") + \"\\n\" +\n        \"Status: \" + (data.job_status || \"\");\n      jobResult.style.whiteSpace = \"pre-line\";\ndashboard.hidden = false;\n\/\/ For older test jobs with duplicates, load the newest row without deleting any.\nconst { data: savedMeasurement, error: measurementLoadError } = await client\n  .from(\"final_measurements\")\n  .select(\"*\")\n  .eq(\"job_id\", data.job_id)\n  .order(\"id\", { ascending: false })\n  .limit(1)\n  .maybeSingle();\n\nif (measurementLoadError) {\n  jobResult.textContent +=\n    \"\\nUnable to load Final Measurement: \" + measurementLoadError.message;\n    measurementDashboardContent.textContent = \"Unable to load Final Measurement.\";\n  \/\/ Leave Save disabled: an empty form must never overwrite stored data.\n} else {\n  const measurementFields = {\n    \"gw-measured-by\": \"measured_by\",\n    \"gw-measurement-date\": \"measurement_date\",\n    \"gw-project-area\": \"project_area\",\n    \"gw-field-notes\": \"field_notes\",\n    \"gw-materials\": \"material\",\n    \"gw-edge\": \"edge\",\n    \"gw-sink-type\": \"sink_type\",\n    \"gw-sink-model\": \"sink_model\",\n    \"gw-stove-type\": \"stove_type\",\n    \"gw-faucet\": \"faucet\",\n    \"gw-backsplash\": \"backsplash\",\n    \"gw-seam-required\": \"seam_required\"\n  };\n  if (savedMeasurement) {\n    Object.entries(measurementFields).forEach(function ([elementId, column]) {\n      const field = document.getElementById(elementId);\n      if (!field) return;\n      if (field.type === \"checkbox\") {\n        field.checked = savedMeasurement[column] === true;\n      } else {\n        field.value = savedMeasurement[column] ?? \"\";\n      }\n    });\n  }\n    measurementLoadedJobId = data.job_id;\n  measurementLoadOK = true;\n  renderWorkSummary(measurementDashboardContent, savedMeasurement, measurementDashboardGroups,\n    \"No Final Measurement recorded yet.\");\n}\nconst { data: savedPreparation, error: loadError } =\n  await client\n    .from(\"site_preparation\")\n    .select(\"*\")\n    .eq(\"job_id\", data.job_id)\n    .order(\"id\", { ascending: false })\n    .limit(1)\n    .maybeSingle();\n\nif (loadError) {\n  jobResult.textContent +=\n    \"\\nUnable to load Site Preparation: \" + loadError.message;\n    preparationDashboardContent.textContent = \"Unable to load Site Preparation.\";\n\n} else if (savedPreparation) {\n    preparationLoadOK = true;\n  renderWorkSummary(preparationDashboardContent, savedPreparation, preparationDashboardGroups,\n    \"No Site Preparation recorded yet.\");\nconst preparationFields = {\n  \"gw-water-condition\": \"water_shutoff_condition\",\n  \"gw-water-replace\": \"water_shutoff_replace\",\n  \"gw-faucet-condition\": \"faucet_condition\",\n  \"gw-plumbing-modification\": \"plumbing_modification\",\n  \"gw-plumbing-notes\": \"plumbing_notes\",\n  \"gw-electrical-modification\": \"electrical_modification\",\n  \"gw-outlet-issue\": \"outlet_issue\",\n  \"gw-mirror-removal\": \"mirror_removal\",\n  \"gw-wall-condition\": \"wall_condition\",\n  \"gw-wall-work-required\": \"wall_work_required\",\n  \"gw-floor-type\": \"floor_type\",\n  \"gw-floor-protection-required\": \"floor_protection_required\",\n  \"gw-floor-protection-type\": \"floor_protection_type\",\n  \"gw-protection-notes\": \"protection_notes\",\n  \"gw-access-condition\": \"access_condition\",\n  \"gw-stairs-required\": \"stairs_required\",\n  \"gw-special-access-notes\": \"special_access_notes\",\n  \"gw-special-tools-required\": \"special_tools_required\",\n  \"gw-special-tools-list\": \"special_tools_list\",\n  \"gw-special-parts-required\": \"special_parts_required\",\n  \"gw-special-parts-list\": \"special_parts_list\",\n  \"gw-installer-notes\": \"installer_notes\",\n  \"gw-prepared-by\": \"prepared_by\",\n  \"gw-preparation-complete\": \"preparation_complete\",\n  \"gw-installer-reviewed\": \"installer_reviewed\",\n  \"gw-installer-reviewed-by\": \"installer_reviewed_by\",\n  \"gw-ready-for-install\": \"ready_for_install\"\n};\n\nObject.entries(preparationFields).forEach(function ([elementId, column]) {\n  const field = document.getElementById(elementId);\n  if (!field) return;\n\n  if (field.type === \"checkbox\") {\n    field.checked = savedPreparation[column] === true;\n  } else {\n    field.value = savedPreparation[column] ?? \"\";\n  }\n});  \ndocument.getElementById(\"gw-electrical-notes\").value =\n    savedPreparation.electrical_notes || \"\";\n} else {\n   preparationLoadOK = true;\n   renderWorkSummary(preparationDashboardContent, null, preparationDashboardGroups,\n     \"No Site Preparation recorded yet.\");\n}\n\n \/\/ Load only the signed-in employee's review for the selected job.\nconst { data: savedReview, error: reviewLoadError } = await client\n  .from(\"installer_reviews\")\n  .select(\"installer_notes, installer_reviewed, ready_for_install\")\n  .eq(\"job_id\", data.job_id)\n  .eq(\"user_id\", user.id)\n  .order(\"id\", { ascending: false })\n  .limit(1)\n  .maybeSingle();\n\nif (reviewLoadError) {\n  jobResult.textContent +=\n    \"\\nUnable to load Installer Review: \" + reviewLoadError.message;\n  \/\/ Leave Save disabled when the existing record cannot be checked.\n} else {\n  document.getElementById(\"gw-review-notes\").value =\n    savedReview?.installer_notes ?? \"\";\n  document.getElementById(\"gw-review-confirmed\").checked =\n    savedReview?.installer_reviewed === true;\n  document.getElementById(\"gw-review-ready\").checked =\n    savedReview?.ready_for_install === true;\n  document.getElementById(\"gw-save-review\").disabled = false;\n}\n\/\/ Only owners get the overview. RLS must independently enforce this access.\nconst { data: profile, error: profileError } = await client\n  .from(\"employee_profiles\")\n  .select(\"role\")\n  .eq(\"user_id\", user.id)\n  .maybeSingle();\nif (profileError) {\n  \/\/ Do not let an optional role lookup failure break the read-only work dashboards.\n  \/\/ No edit privilege is granted because role remains null.\n  configureWorkDashboards(null, data.job_id);\n  jobResult.textContent += \"\\nUnable to check employee role; work details are read-only.\";\n   } else {\n   configureWorkDashboards(profile?.role || null, data.job_id);\n   configureDrawingView(data.job_id, profile?.role || null);\n   configureProblemView(data.job_id, profile?.role || null);\n   configureCustomerCodeView(data.job_id, profile?.role || null, user.id);\n   configurePhotoApprovalView(data.job_id, profile?.role || null, user.id);\n   if (profile?.role === \"owner\") {\n     configureOwnerAssignments(data.job_id);\n     allReviewsPanel.hidden = true;\n     await refreshOwnerReviews(data.job_id, user.id);\n   }\n   if (profile?.role === \"owner\" || profile?.role === \"project_manager\") {\n     configureCompletion(data.job_id, data.job_status, data.completed_at);\n   }\n   if (profile?.role === \"owner\" || profile?.role === \"office\") {\n     \/\/ Office button is intentionally not shown in the Field Portal.\n     accountingToggle.hidden = true;\n   }\n }\n    }\n  } catch (error) {\n    jobResult.textContent = \"Connection error. Please try again.\";\n  } finally {\n    findJob.disabled = false;\n  }\n});\n \/\/ OFFICE \/ OWNER \u2014 REGISTER AN ALREADY APPROVED JOB.\n \/\/ This UI is NOT an authorization boundary. \"Jobs\" INSERT RLS must independently\n \/\/ limit inserts to active owner\/office employees. A DB UNIQUE constraint on job_id\n \/\/ is also required to stop simultaneous submissions from creating duplicate IDs.\n \/\/ A verified Jobs AFTER INSERT trigger creates the invoice; this UI never adds payments.\n const createJobToggle = document.createElement(\"button\");\n createJobToggle.id = \"gw-create-job-toggle\";\n createJobToggle.type = \"button\";\n createJobToggle.textContent = \"Register Approved Job (Office \/ Owner)\";\n createJobToggle.hidden = true;\n createJobToggle.setAttribute(\"aria-expanded\", \"false\");\n createJobToggle.style.cssText = \"width:100%;margin:14px 0 8px;\";\n\n const createJobPanel = document.createElement(\"section\");\n createJobPanel.id = \"gw-create-job-panel\";\n createJobPanel.hidden = true;\n createJobPanel.style.cssText =\n   \"width:100%;max-width:100%;box-sizing:border-box;padding:16px;margin:8px 0 20px;\" +\n   \"border:1px solid #cbd5e1;border-radius:8px;background:#fff;\";\n const createJobHeading = document.createElement(\"h3\");\n createJobHeading.textContent = \"Register Approved Job\";\n const createJobHelp = document.createElement(\"p\");\n createJobHelp.textContent =\n  \"Only approved jobs receive a Job ID. Enter the approved job's assigned ID \" +\n  \"in GW-0000-000 format (example: GW-2609-018). \" +\n  \"An invoice is automatically created by the database after the job is registered.\";\n createJobPanel.append(createJobHeading, createJobHelp);\n\n function createJobField(id, labelText, type, maxLength, required) {\n   const label = document.createElement(\"label\");\n   label.setAttribute(\"for\", id);\n   label.textContent = labelText;\n   const input = document.createElement(\"input\");\n   input.id = id;\n   input.type = type;\n   input.maxLength = maxLength;\n   input.required = required;\n   input.autocomplete = \"off\";\n   input.style.cssText =\n     \"display:block;width:100%;max-width:100%;box-sizing:border-box;font-size:16px;\";\n   createJobPanel.append(label, input);\n   return input;\n }\n const createJobId = createJobField(\"gw-new-job-id\", \"Job ID * (GW-0000-000)\", \"text\", 11, true);\n createJobId.placeholder = \"GW-2609-018\";\n createJobId.pattern = \"GW-[0-9]{4}-[0-9]{3}\";\n const createCustomerName = createJobField(\n   \"gw-new-customer-name\", \"Customer Name *\", \"text\", 120, true\n );\n const createCustomerPhone = createJobField(\n   \"gw-new-customer-phone\", \"Customer Phone (optional)\", \"tel\", 50, false\n );\n const createJobAddress = createJobField(\n   \"gw-new-job-address\", \"Job Address *\", \"text\", 300, true\n );\n \/\/ Manual approval attestation: this UI records approved work, but cannot\n\/\/ independently verify approval from an external approval system.\nconst createJobApprovalLabel = document.createElement(\"label\");\ncreateJobApprovalLabel.id = \"gw-create-job-approval-label\";\ncreateJobApprovalLabel.style.cssText =\n  \"display:flex;align-items:flex-start;gap:10px;margin:18px 0;cursor:pointer;\";\nconst createJobApproved = document.createElement(\"input\");\ncreateJobApproved.id = \"gw-create-job-approved\";\ncreateJobApproved.type = \"checkbox\";\ncreateJobApproved.required = true;\ncreateJobApproved.style.cssText =\n  \"flex:0 0 22px;width:22px;height:22px;min-width:22px;margin:2px 0 0;\";\nconst createJobApprovalText = document.createElement(\"span\");\ncreateJobApprovalText.textContent =\n  \"I confirm this job has already been approved and its Job ID assigned.\";\ncreateJobApprovalLabel.append(createJobApproved, createJobApprovalText);\ncreateJobPanel.appendChild(createJobApprovalLabel);\nconst createJobSave = document.createElement(\"button\");\n createJobSave.id = \"gw-create-job-save\";\n createJobSave.type = \"button\";\n createJobSave.textContent = \"Register Approved Job\";\n createJobSave.style.width = \"100%\";\n const createJobCancel = document.createElement(\"button\");\n createJobCancel.id = \"gw-create-job-cancel\";\n createJobCancel.type = \"button\";\n createJobCancel.textContent = \"Close\";\n createJobCancel.style.width = \"100%\";\n const createJobMessage = document.createElement(\"p\");\n createJobMessage.id = \"gw-create-job-message\";\n createJobMessage.setAttribute(\"role\", \"status\");\n createJobMessage.style.cssText = \"white-space:pre-line;overflow-wrap:anywhere;\";\n createJobPanel.append(createJobSave, createJobCancel, createJobMessage);\n \/\/ Keep the Create Job action inside the existing signed-in Job Search view.\n const firstSearchLabel = jobSearch.querySelector('label[for=\"gw-job-id\"]');\n if (firstSearchLabel) {\n   firstSearchLabel.insertAdjacentElement(\"beforebegin\", createJobToggle);\n } else {\n   jobSearch.insertBefore(createJobToggle, jobSearch.firstChild);\n }\n createJobToggle.insertAdjacentElement(\"afterend\", createJobPanel);\n\n let createJobRequestId = 0;\n let createJobUserId = null;\n let createJobBusy = false;\n function resetCreateJob() {\n   createJobRequestId++;\n   createJobUserId = null;\n   createJobBusy = false;\n   createJobToggle.hidden = true;\n   createJobToggle.setAttribute(\"aria-expanded\", \"false\");\n   createJobPanel.hidden = true;\n   createJobId.value = \"\";\n   createCustomerName.value = \"\";\n   createCustomerPhone.value = \"\";\n  createJobAddress.value = \"\";\n  createJobApproved.checked = false;\n  createJobMessage.textContent = \"\";\n   createJobSave.disabled = false;\n   createJobCancel.disabled = false;\n }\n async function verifyCreateJobRole() {\n   if (!client || logout.hidden || jobSearch.hidden) return null;\n   const { data: auth, error: authError } = await client.auth.getUser();\n   if (authError || !auth.user) return null;\n   const { data: active, error: activeError } =\n     await client.rpc(\"gw_employee_is_active\");\n   if (activeError || active !== true) return null;\n   const { data: profile, error: profileError } = await client\n     .from(\"employee_profiles\")\n     .select(\"role\")\n     .eq(\"user_id\", auth.user.id)\n     .maybeSingle();\n   if (profileError || !profile ||\n       ![\"owner\", \"office\"].includes(profile.role)) return null;\n   return auth.user.id;\n }\n async function checkCreateJobAccess() {\n   const requestId = ++createJobRequestId;\n   createJobToggle.hidden = true;\n   createJobPanel.hidden = true;\n   createJobUserId = null;\n   try {\n     const userId = await verifyCreateJobRole();\n     if (requestId !== createJobRequestId || !userId || logout.hidden ||\n         jobSearch.hidden) return;\n     createJobUserId = userId;\n     createJobToggle.hidden = false;\n   } catch (_error) {\n     \/\/ Fail closed; existing search and login continue to operate.\n   }\n }\n createJobToggle.addEventListener(\"click\", function () {\n   if (createJobToggle.hidden || logout.hidden || jobSearch.hidden ||\n       !createJobUserId || createJobBusy) return;\n   createJobPanel.hidden = !createJobPanel.hidden;\n   createJobToggle.setAttribute(\"aria-expanded\", String(!createJobPanel.hidden));\n   if (!createJobPanel.hidden) createJobId.focus();\n });\n createJobCancel.addEventListener(\"click\", function () {\n   if (createJobBusy) return;\n   createJobPanel.hidden = true;\n   createJobToggle.setAttribute(\"aria-expanded\", \"false\");\n });\n createJobSave.addEventListener(\"click\", async function () {\n   if (!client || createJobBusy || createJobPanel.hidden ||\n       createJobToggle.hidden || jobSearch.hidden || logout.hidden ||\n       !createJobUserId) return;\n   const jobId = createJobId.value.trim();\n   const customerName = createCustomerName.value.trim();\n   const customerPhone = createCustomerPhone.value.trim();\n   const jobAddress = createJobAddress.value.trim();\n  \/\/ Exact company ID convention; do not automatically generate sequential job numbers.\n  if (!\/^GW-[0-9]{4}-[0-9]{3}$\/.test(jobId)) {\n    createJobMessage.textContent =\n      \"Job ID must use GW-0000-000 (example: GW-2609-018).\";\n     return;\n   }\n  if (!customerName || customerName.length > 120 ||\n       customerPhone.length > 50 || !jobAddress || jobAddress.length > 300) {\n    createJobMessage.textContent = \"Enter a customer name and job address.\";\n    return;\n  }\n  if (!createJobApproved.checked) {\n    createJobMessage.textContent =\n      \"Confirm the job has already been approved before registering its Job ID.\";\n    return;\n  }\n   const requestId = ++createJobRequestId;\n   createJobBusy = true;\n   createJobSave.disabled = true;\n   createJobCancel.disabled = true;\n   createJobMessage.textContent = \"Checking Job ID and employee access...\";\n   try {\n     const userId = await verifyCreateJobRole();\n     if (!userId || userId !== createJobUserId)\n       throw new Error(\"Office \/ Owner access could not be verified. Log in again.\");\n     const { data: existing, error: existingError } = await client\n       .from(\"Jobs\").select(\"job_id\").eq(\"job_id\", jobId)\n       .limit(1).maybeSingle();\n     if (existingError) throw existingError;\n     if (existing) {\n       createJobMessage.textContent =\n         \"This Job ID already exists. Choose a different ID; no job was created.\";\n       return;\n     }\n     if (requestId !== createJobRequestId || logout.hidden ||\n         createJobPanel.hidden) return;\n     if (!window.confirm(\n       \"Register this ALREADY APPROVED job?\\n\\n\" +\n       \"Job ID: \" + jobId + \"\\nCustomer: \" + customerName +\n       \"\\nPhone: \" + (customerPhone || \"(none)\") +\n       \"\\nAddress: \" + jobAddress +\n       \"\\nStatus: Approved\" +\n       \"\\n\\nThis creates a Job record and its automatic $0 invoice. No payment is recorded.\"\n     )) {\n       createJobMessage.textContent = \"Cancelled. No job was created.\";\n       return;\n     }\n    \/\/ Recheck after confirmation; client-side attestation cannot prove\n    \/\/ external approval, and database RLS must enforce active owner\/office access.\n    if (!createJobApproved.checked)\n      throw new Error(\"Approval confirmation is required. No create request sent.\");\n     const confirmedUserId = await verifyCreateJobRole();\n     if (!confirmedUserId || confirmedUserId !== userId ||\n         requestId !== createJobRequestId || logout.hidden ||\n         createJobPanel.hidden)\n       throw new Error(\"Session or employee access changed. No create request sent.\");\n     createJobMessage.textContent = \"Creating job...\";\n     const { data: saved, error: saveError } = await client\n       .from(\"Jobs\")\n       .insert({\n         job_id: jobId,\n         customer_name: customerName,\n         customer_phone: customerPhone || null,\n         job_address: jobAddress,\n         job_status: \"Approved\"\n       })\n       .select(\"job_id, job_status\")\n       .maybeSingle();\n     if (saveError) {\n       if (saveError.code === \"23505\") {\n         createJobMessage.textContent =\n           \"This Job ID already exists. No duplicate job was created.\";\n         return;\n       }\n       throw saveError;\n     }\n     if (!saved || saved.job_id !== jobId) {\n       throw new Error(\n         \"Create result could not be verified. Search this Job ID before retrying.\"\n       );\n     }\n     \/\/ Save is complete. The Jobs insert trigger should create a $0 invoice.\n     \/\/ Don't permit a new Create submission of this same record.\n     createJobSave.disabled = true;\n     jobIdInput.value = saved.job_id;\n     createJobMessage.textContent =\n       \"Approved job registered: \" + saved.job_id + \".\\n\" +\n       \"Job ID copied to Find a Job. Press Search Job to open it.\" +\n       (saved.job_status === \"Approved\" ? \"\\nStatus: Approved\" :\n         \"\\nWarning: Saved job status was not Approved. Check the job record.\");\n     try {\n       const { data: invoice, error: invoiceError } = await client\n         .from(\"job_invoices\")\n         .select(\"job_id\")\n         .eq(\"job_id\", saved.job_id)\n         .maybeSingle();\n       if (invoiceError) {\n         createJobMessage.textContent +=\n           \"\\nInvoice status could not be verified; check Office Accounting.\";\n       } else if (!invoice) {\n         createJobMessage.textContent +=\n           \"\\nWarning: Automatic invoice was not found. Check the job trigger \" +\n           \"and Office Accounting before recording payments.\";\n       }\n     } catch (_error) {\n       createJobMessage.textContent +=\n         \"\\nInvoice status could not be verified; check Office Accounting.\";\n     }\n   } catch (error) {\n     if (requestId === createJobRequestId && !logout.hidden &&\n         !createJobPanel.hidden) {\n       createJobMessage.textContent =\n         \"Create failed or could not be confirmed: \" + error.message +\n         \"\\nSearch this Job ID before trying again.\";\n     }\n   } finally {\n     if (requestId === createJobRequestId && !logout.hidden &&\n         !createJobPanel.hidden) {\n       createJobBusy = false;\n       createJobCancel.disabled = false;\n       \/\/ Success leaves Save disabled until a new Job ID is entered.\n       if (!createJobMessage.textContent.startsWith(\"Approved job registered:\"))\n         createJobSave.disabled = false;\n     }\n   }\n });\n createJobId.addEventListener(\"input\", function () {\n   if (!createJobBusy && createJobMessage.textContent.startsWith(\"Approved job registered:\")) {\n     createJobSave.disabled = false;\n     createJobMessage.textContent = \"\";\n   }\n });\n\n\/\/ OWNER \/ OFFICE \u2014 MANUALLY SET OR RESET A CUSTOMER'S PRIVATE LOOKUP CODE.\n\/\/ This creates UI only; the SECURITY DEFINER database RPC enforces active\n\/\/ owner\/office membership. No code is generated, stored in browser storage,\n\/\/ fetched from the database, or included in a customer-facing URL.\nconst customerCodeToggle = document.createElement(\"button\");\ncustomerCodeToggle.id = \"gw-customer-code-toggle\";\ncustomerCodeToggle.type = \"button\";\ncustomerCodeToggle.textContent = \"Customer Lookup Code (Owner \/ Office)\";\ncustomerCodeToggle.hidden = true;\ncustomerCodeToggle.setAttribute(\"aria-expanded\", \"false\");\ncustomerCodeToggle.style.cssText = \"width:100%;margin:12px 0 8px;\";\n\nconst customerCodePanel = document.createElement(\"section\");\ncustomerCodePanel.id = \"gw-customer-code-panel\";\ncustomerCodePanel.hidden = true;\ncustomerCodePanel.style.cssText =\n  \"padding:16px;margin:8px 0 20px;border:1px solid #cbd5e1;border-radius:8px;\";\nconst customerCodeHeading = document.createElement(\"h3\");\ncustomerCodeHeading.textContent = \"Set \/ Reset Customer Lookup Code\";\nconst customerCodeJobLabel = document.createElement(\"p\");\ncustomerCodeJobLabel.id = \"gw-customer-code-job\";\nconst customerCodeHelp = document.createElement(\"p\");\ncustomerCodeHelp.textContent =\n  \"Enter a random 8-character code using English letters and numbers (at least one of each). \" +\n  \"Use a random, hard-to-guess code, not a phone number or address. \" +\n  \"Saving again replaces the old code immediately. The code cannot be read back \" +\n  \"from this Portal; share it privately with this job's customer.\";\n\nfunction customerCodeField(id, title) {\n  const label = document.createElement(\"label\");\n  label.htmlFor = id;\n  label.textContent = title;\n  label.style.cssText = \"display:block;margin:12px 0 4px;\";\n  const input = document.createElement(\"input\");\n  input.id = id;\n  input.type = \"password\";\n  input.autocomplete = \"off\";\n  input.spellcheck = false;\n  input.maxLength = 8;\n  input.style.cssText = \"display:block;width:100%;max-width:440px;\";\n  customerCodePanel.append(label, input);\n  return input;\n}\ncustomerCodePanel.append(customerCodeHeading, customerCodeJobLabel, customerCodeHelp);\nconst customerCodeInput = customerCodeField(\"gw-customer-code-value\", \"Private lookup code\");\nconst customerCodeConfirmInput = customerCodeField(\n  \"gw-customer-code-confirm\", \"Confirm private lookup code\"\n);\n\/\/ Optional local visibility control; defaults to hidden on every opening.\n\/\/ Never persist the checkbox state or either code field.\nconst customerCodeShowLabel = document.createElement(\"label\");\ncustomerCodeShowLabel.style.cssText =\n  \"display:flex;align-items:center;gap:8px;margin:12px 0;cursor:pointer;\";\nconst customerCodeShowCheckbox = document.createElement(\"input\");\ncustomerCodeShowCheckbox.type = \"checkbox\";\ncustomerCodeShowCheckbox.id = \"gw-customer-code-show\";\ncustomerCodeShowCheckbox.autocomplete = \"off\";\nconst customerCodeShowText = document.createElement(\"span\");\ncustomerCodeShowText.textContent = \"Show letters while typing (both fields)\";\ncustomerCodeShowLabel.append(customerCodeShowCheckbox, customerCodeShowText);\ncustomerCodePanel.append(customerCodeShowLabel);\nfunction hideCustomerCodeLetters() {\n  customerCodeShowCheckbox.checked = false;\n  customerCodeInput.type = \"password\";\n  customerCodeConfirmInput.type = \"password\";\n}\ncustomerCodeShowCheckbox.addEventListener(\"change\", function () {\n  const type = customerCodeShowCheckbox.checked ? \"text\" : \"password\";\n  customerCodeInput.type = type;\n  customerCodeConfirmInput.type = type;\n});\nconst customerCodeSave = document.createElement(\"button\");\ncustomerCodeSave.type = \"button\";\ncustomerCodeSave.id = \"gw-customer-code-save\";\ncustomerCodeSave.textContent = \"Save \/ Reset Code\";\ncustomerCodeSave.style.cssText = \"width:100%;margin-top:12px;\";\nconst customerCodeClose = document.createElement(\"button\");\ncustomerCodeClose.type = \"button\";\ncustomerCodeClose.textContent = \"Close\";\ncustomerCodeClose.style.cssText = \"width:100%;margin-top:6px;\";\nconst customerCodeMessage = document.createElement(\"p\");\ncustomerCodeMessage.id = \"gw-customer-code-message\";\ncustomerCodeMessage.setAttribute(\"role\", \"status\");\ncustomerCodePanel.append(customerCodeSave, customerCodeClose, customerCodeMessage);\ndashboard.append(customerCodeToggle, customerCodePanel);\n\nlet customerCodeJobId = null;\nlet customerCodeUserId = null;\nlet customerCodeGeneration = 0;\nlet customerCodeBusy = false;\nfunction resetCustomerCodeView() {\n  customerCodeGeneration++;\n  customerCodeJobId = null;\n  customerCodeUserId = null;\n  customerCodeBusy = false;\n  customerCodeToggle.hidden = true;\n  customerCodeToggle.setAttribute(\"aria-expanded\", \"false\");\n  customerCodePanel.hidden = true;\n  customerCodeJobLabel.textContent = \"\";\n  hideCustomerCodeLetters();\n  customerCodeInput.value = \"\";\n  customerCodeConfirmInput.value = \"\";\n  customerCodeMessage.textContent = \"\";\n  customerCodeInput.disabled = false;\n  customerCodeConfirmInput.disabled = false;\n  customerCodeSave.disabled = false;\n  customerCodeClose.disabled = false;\n}\nfunction customerCodeIsCurrent(jobId, generation, userId) {\n  return !!client && !logout.hidden && !jobSearch.hidden && !dashboard.hidden &&\n    !customerCodeToggle.hidden && !customerCodePanel.hidden &&\n    customerCodeJobId === jobId && customerCodeUserId === userId &&\n    jobIdInput.value.trim() === jobId && generation === customerCodeGeneration;\n}\nfunction configureCustomerCodeView(jobId, role, userId) {\n  resetCustomerCodeView();\n  if (![\"owner\", \"office\"].includes(role) ||\n      !\/^GW-[0-9]{4}-[0-9]{3}$\/.test(jobId || \"\") ||\n      !userId || logout.hidden || jobSearch.hidden || dashboard.hidden ||\n      jobIdInput.value.trim() !== jobId) return;\n  customerCodeJobId = jobId;\n  customerCodeUserId = userId;\n  customerCodeJobLabel.textContent = \"Selected Job ID: \" + jobId;\n  customerCodeToggle.hidden = false;\n}\ncustomerCodeToggle.addEventListener(\"click\", function () {\n  if (!client || customerCodeToggle.hidden || dashboard.hidden || logout.hidden ||\n      !customerCodeJobId || !customerCodeUserId || customerCodeBusy ||\n      jobIdInput.value.trim() !== customerCodeJobId) return;\n  customerCodePanel.hidden = !customerCodePanel.hidden;\n  customerCodeToggle.setAttribute(\"aria-expanded\", String(!customerCodePanel.hidden));\n  if (customerCodePanel.hidden) {\n    hideCustomerCodeLetters();\n    customerCodeInput.value = \"\";\n    customerCodeConfirmInput.value = \"\";\n    customerCodeMessage.textContent = \"\";\n  } else {\n    customerCodeInput.focus();\n  }\n});\ncustomerCodeClose.addEventListener(\"click\", function () {\n  if (customerCodeBusy) return;\n  customerCodePanel.hidden = true;\n  customerCodeToggle.setAttribute(\"aria-expanded\", \"false\");\n  hideCustomerCodeLetters();\n  customerCodeInput.value = \"\";\n  customerCodeConfirmInput.value = \"\";\n  customerCodeMessage.textContent = \"\";\n});\ncustomerCodeSave.addEventListener(\"click\", async function () {\n  const jobId = customerCodeJobId;\n  const userId = customerCodeUserId;\n  const generation = customerCodeGeneration;\n  if (!customerCodeIsCurrent(jobId, generation, userId) || customerCodeBusy) return;\n  const code = customerCodeInput.value;\n  const confirmation = customerCodeConfirmInput.value;\n  if (!\/^(?=.{8}$)(?=.*[A-Za-z])(?=.*[0-9])[A-Za-z0-9]{8}$\/.test(code)) {\n    customerCodeMessage.textContent =\n      \"Use exactly 8 characters: English letters and numbers, at least one of each. No symbols.\";\n    return;\n  }\n  if (code !== confirmation) {\n    customerCodeMessage.textContent = \"The two codes do not match.\";\n    return;\n  }\n  if (!window.confirm(\n    \"Set or RESET the private lookup code for \" + jobId + \"?\\n\\n\" +\n    \"Any existing customer code will stop working immediately. \" +\n    \"The new code will NOT be shown again after saving.\"\n  )) return;\n\n  customerCodeBusy = true;\n  customerCodeSave.disabled = true;\n  customerCodeClose.disabled = true;\n  customerCodeInput.disabled = true;\n  customerCodeConfirmInput.disabled = true;\n  customerCodeMessage.textContent = \"Verifying Owner \/ Office access...\";\n  try {\n    const verifiedUserId = await verifyCreateJobRole();\n    if (!customerCodeIsCurrent(jobId, generation, userId) ||\n        !verifiedUserId || verifiedUserId !== userId) {\n      throw new Error(\"Session or employee access changed.\");\n    }\n    customerCodeMessage.textContent = \"Saving private code...\";\n    const { data: saved, error: saveError } = await client.rpc(\n      \"gw_set_customer_portal_code\", { p_job_id: jobId, p_code: code }\n    );\n    if (saveError || saved !== true) throw new Error(\"Code was not confirmed as saved.\");\n    if (customerCodeIsCurrent(jobId, generation, userId)) {\n      customerCodeMessage.textContent =\n        \"Private lookup code saved for \" + jobId + \". \" +\n        \"Share the new code privately with the correct customer; \" +\n        \"never include it in a URL or public message.\";\n    }\n  } catch (_error) {\n    if (customerCodeIsCurrent(jobId, generation, userId)) {\n      customerCodeMessage.textContent =\n        \"Unable to confirm whether the code was saved. \" +\n        \"Check your connection and reset with a NEW code if needed.\";\n    }\n  } finally {\n    \/\/ Wipe both fields even after an error; never echo, cache, or log the code.\n    if (customerCodeGeneration === generation) {\n      hideCustomerCodeLetters();\n      customerCodeInput.value = \"\";\n      customerCodeConfirmInput.value = \"\";\n      customerCodeBusy = false;\n      customerCodeSave.disabled = false;\n      customerCodeClose.disabled = false;\n      customerCodeInput.disabled = false;\n      customerCodeConfirmInput.disabled = false;\n    }\n  }\n});\n\n\/\/ OFFICE PAGE: separate in-Portal workspace for Owner \/ Office.\n\/\/ Visibility is only a UI convenience. Supabase RLS remains the security boundary.\nconst accountingToggle = document.createElement(\"button\");\naccountingToggle.id = \"gw-accounting-toggle\";\naccountingToggle.type = \"button\";\naccountingToggle.textContent = \"Office\";\naccountingToggle.hidden = true;\n\/\/ Field Portal: Office navigation button intentionally disabled.\naccountingToggle.setAttribute(\"aria-expanded\", \"false\");\naccountingToggle.style.cssText =\n  \"width:100%;margin:12px 0;padding:15px 16px;text-align:left;font-size:18px;font-weight:700;\" +\n  \"border:1px solid #b9c7d6;border-radius:10px;background:#fff;cursor:pointer;\";\naccountingToggle.style.setProperty(\"color\", \"#0b4f8a\", \"important\");\naccountingToggle.style.setProperty(\"-webkit-text-fill-color\", \"#0b4f8a\", \"important\");\n\nconst accountingPanel = document.createElement(\"section\");\naccountingPanel.id = \"gw-accounting-panel\";\naccountingPanel.hidden = true;\naccountingPanel.style.cssText =\n  \"box-sizing:border-box;width:100%;margin:14px 0 20px;padding:16px;\" +\n  \"border:1px solid #cbd5e1;border-radius:10px;background:#fff;\";\n\nconst accountingTop = document.createElement(\"div\");\naccountingTop.style.cssText =\n  \"display:flex;align-items:center;justify-content:space-between;gap:12px;\" +\n  \"flex-wrap:wrap;margin-bottom:14px;\";\n\nconst accountingBack = document.createElement(\"button\");\naccountingBack.type = \"button\";\naccountingBack.textContent = \"\u2190 Back to Portal\";\naccountingBack.style.cssText = \"margin:0;padding:8px 10px;font-size:14px;\";\n\nconst accountingTitle = document.createElement(\"h2\");\naccountingTitle.textContent = \"Office\";\naccountingTitle.style.cssText = \"margin:0;font-size:26px;line-height:1.2;\";\n\naccountingTop.append(accountingBack, accountingTitle);\naccountingPanel.appendChild(accountingTop);\ndashboard.append(accountingToggle, accountingPanel);\n\nlet accountingPortalState = null;\n\nfunction openAccountingView() {\n  if (!accountingRoleAllowed || accountingToggle.hidden || logout.hidden) return;\n\n  accountingPortalState = {\n    jobSearchHidden: jobSearch.hidden,\n    dashboardHidden: dashboard.hidden\n  };\n\n  \/\/ Office is its own page inside the Portal.\n  jobSearch.hidden = true;\n  dashboard.hidden = true;\n  bulletinEntry.hidden = true;\n  bulletinBoard.hidden = true;\n  employeeDirectoryToggle.hidden = true;\n  employeeDirectoryPanel.hidden = true;\n  createJobToggle.hidden = true;\n  createJobPanel.hidden = true;\n  annualProblemToggle.hidden = true;\n  annualProblemPanel.hidden = true;\n\n  accountingToggle.hidden = true;\n  accountingPanel.hidden = false;\n  accountingToggle.setAttribute(\"aria-expanded\", \"true\");\n  accountingTitle.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n}\n\nfunction closeAccountingView() {\n  if (accountingPanel.hidden) return;\n\n  accountingPanel.hidden = true;\n  accountingToggle.hidden = !accountingRoleAllowed;\n  accountingToggle.setAttribute(\"aria-expanded\", \"false\");\n\n  jobSearch.hidden = false;\n  dashboard.hidden = true;\n  bulletinBoard.hidden = true;\n  bulletinEntry.hidden = false;\n\n  employeeDirectoryPanel.hidden = true;\n  void checkEmployeeDirectoryAccess();\n\n  createJobPanel.hidden = true;\n  void checkCreateJobAccess();\n\n  annualProblemPanel.hidden = true;\n  annualProblemToggle.hidden = !annualProblemAllowed;\n  annualProblemToggle.setAttribute(\"aria-expanded\", \"false\");\n\n  accountingPortalState = null;\n  accountingToggle.scrollIntoView({ behavior: \"smooth\", block: \"center\" });\n}\n\naccountingToggle.addEventListener(\"click\", openAccountingView);\naccountingBack.addEventListener(\"click\", closeAccountingView);\n\nfunction accountingElement(tag, parent, text) {\n  const el = document.createElement(tag);\n  if (text !== undefined) el.textContent = text;\n  parent.appendChild(el);\n  return el;\n}\nfunction accountingField(parent, labelText, type) {\n  const wrap = accountingElement(\"div\", parent);\n  wrap.style.marginBottom = \"10px\";\n  const label = accountingElement(\"label\", wrap, labelText);\n  label.style.display = \"block\";\n  const input = accountingElement(\"input\", wrap);\n  input.type = type;\n  input.style.width = \"100%\";\n  input.style.maxWidth = \"400px\";\n  if (type === \"number\") {\n    input.min = \"0\";\n    input.step = \"0.01\";\n  }\n  return input;\n}\nfunction accountingButton(parent, text) {\n  const button = accountingElement(\"button\", parent, text);\n  button.type = \"button\";\n  button.style.margin = \"4px 8px 4px 0\";\n  return button;\n}\nfunction moneyCents(value) {\n  return Math.round(Number(value || 0) * 100);\n}\nfunction moneyText(cents) {\n  return new Intl.NumberFormat(\"en-US\", {\n    style: \"currency\", currency: \"USD\"\n  }).format(cents \/ 100);\n}\nfunction parseDollars(value, allowZero) {\n  const text = String(value).trim();\n  if (!\/^\\d+(?:\\.\\d{1,2})?$\/.test(text)) return null;\n  const cents = moneyCents(text);\n  if (!Number.isSafeInteger(cents) || cents > 999999999999 ||\n      (!allowZero && cents <= 0)) return null;\n  return cents;\n}\nfunction todayLocal() {\n  const d = new Date();\n  const pad = (v) => String(v).padStart(2, \"0\");\n  return d.getFullYear() + \"-\" + pad(d.getMonth() + 1) + \"-\" + pad(d.getDate());\n}\n\n\/\/ ------------------------------------------------------------\n\/\/ OFFICE \u2014 independent Job ID selector\n\/\/ Office Accounting\/Labor\/Material do NOT depend on the main Portal Job Search.\n\/\/ ------------------------------------------------------------\nconst officeJobBox = accountingElement(\"section\", accountingPanel);\nofficeJobBox.style.cssText =\n  \"margin:8px 0 16px;padding:14px;border:1px solid #dbe3ea;border-radius:8px;background:#f8fafc;\";\n\nconst officeJobHeading = accountingElement(\"h3\", officeJobBox, \"Job\");\nofficeJobHeading.style.cssText = \"margin:0 0 10px;font-size:20px;\";\n\nconst officeJobLabel = accountingElement(\"label\", officeJobBox, \"Job ID\");\nofficeJobLabel.style.cssText = \"display:block;font-weight:600;margin-bottom:5px;\";\n\nconst officeJobIdInput = accountingElement(\"input\", officeJobBox);\nofficeJobIdInput.type = \"text\";\nofficeJobIdInput.maxLength = 20;\nofficeJobIdInput.placeholder = \"GW-2610-001\";\nofficeJobIdInput.autocomplete = \"off\";\nofficeJobIdInput.style.cssText =\n  \"box-sizing:border-box;width:100%;max-width:400px;font-size:17px;padding:9px;margin-bottom:8px;\";\n\nconst officeLoadJobButton = accountingButton(officeJobBox, \"Load Job\");\nconst officeClearJobButton = accountingButton(officeJobBox, \"Clear\");\nconst officeJobMessage = accountingElement(\"p\", officeJobBox);\nofficeJobMessage.setAttribute(\"role\", \"status\");\nofficeJobMessage.setAttribute(\"aria-live\", \"polite\");\nofficeJobMessage.style.cssText = \"margin:8px 0 0;font-size:14px;\";\n\n\/\/ Office modules. Labor + Material are now first-class Office sections;\n\/\/ their Supabase write forms are added in the next step without changing navigation again.\nconst officeModuleNav = accountingElement(\"div\", accountingPanel);\nofficeModuleNav.style.cssText =\n  \"display:flex;gap:8px;flex-wrap:wrap;margin:10px 0 16px;padding-bottom:12px;border-bottom:1px solid #dbe3ea;\";\n\nconst officeAccountingButton = accountingButton(officeModuleNav, \"Invoice \/ Payments\");\nconst officeLaborButton = accountingButton(officeModuleNav, \"Labor\");\nconst officeMaterialButton = accountingButton(officeModuleNav, \"Material\");\n\nconst officeAccountingSection = accountingElement(\"section\", accountingPanel);\nconst officeLaborSection = accountingElement(\"section\", accountingPanel);\nconst officeMaterialSection = accountingElement(\"section\", accountingPanel);\n\nofficeLaborSection.hidden = true;\nofficeMaterialSection.hidden = true;\nofficeLaborSection.style.cssText =\n  \"padding:14px;border:1px solid #dbe3ea;border-radius:8px;background:#fff;\";\nofficeMaterialSection.style.cssText =\n  \"padding:14px;border:1px solid #dbe3ea;border-radius:8px;background:#fff;\";\n\nconst officeLaborHeading = accountingElement(\"h3\", officeLaborSection, \"Labor\");\nconst officeLaborStatus = accountingElement(\n  \"p\",\n  officeLaborSection,\n  \"Select and load a Job ID above. Labor entry will use this same Job ID.\"\n);\nofficeLaborStatus.style.margin = \"6px 0\";\n\/\/ ============================================================\n\/\/ OFFICE LABOR \u2014 INSTALLER FORM UI\n\/\/ Step 2A: UI only. Supabase save will be connected next.\n\/\/ ============================================================\n\nconst installerLaborBox = accountingElement(\"section\", officeLaborSection);\ninstallerLaborBox.style.cssText =\n  \"margin-top:16px;padding:16px;border:1px solid #dbe3ea;\" +\n  \"border-radius:8px;background:#f8fafc;\";\n\nconst installerLaborTitle = accountingElement(\n  \"h4\",\n  installerLaborBox,\n  \"Installer Labor\"\n);\ninstallerLaborTitle.style.cssText = \"margin:0 0 14px;font-size:19px;\";\n\nconst installerWorkDate =\n  accountingField(installerLaborBox, \"Work Date\", \"date\");\ninstallerWorkDate.value = todayLocal();\n\nconst installerWorkType =\n  accountingField(installerLaborBox, \"Work Type\", \"text\");\ninstallerWorkType.value = \"Installation\";\n\nconst installerSF =\n  accountingField(installerLaborBox, \"SF\", \"number\");\ninstallerSF.step = \"0.01\";\n\nconst installerKitchenSink =\n  accountingField(installerLaborBox, \"Kitchen Sink Qty\", \"number\");\ninstallerKitchenSink.step = \"1\";\n\nconst installerBathSink =\n  accountingField(installerLaborBox, \"Bath Sink Qty\", \"number\");\ninstallerBathSink.step = \"1\";\n\nconst installerCooktop =\n  accountingField(installerLaborBox, \"Cooktop Qty\", \"number\");\ninstallerCooktop.step = \"1\";\n\nconst installerMiter =\n  accountingField(installerLaborBox, \"Miter LF\", \"number\");\ninstallerMiter.step = \"0.01\";\n\nconst installerSeam =\n  accountingField(installerLaborBox, \"Seam Qty\", \"number\");\ninstallerSeam.step = \"1\";\n\nfunction installerCheckbox(labelText) {\n  const label = accountingElement(\"label\", installerLaborBox);\n  label.style.cssText =\n    \"display:flex;align-items:center;gap:8px;margin:10px 0;font-weight:600;\";\n\n  const input = accountingElement(\"input\", label);\n  input.type = \"checkbox\";\n\n  accountingElement(\"span\", label, labelText);\n\n  return input;\n}\n\nconst installerDemolition =\n  installerCheckbox(\"Demolition\");\n\nconst installerBacksplash =\n  installerCheckbox(\"Backsplash\");\n\nconst installerRepair =\n  installerCheckbox(\"Repair\");\n\nconst installerOtherWork =\n  accountingField(installerLaborBox, \"Other Work\", \"text\");\n\nconst installerNotesWrap =\n  accountingElement(\"div\", installerLaborBox);\n\ninstallerNotesWrap.style.marginBottom = \"10px\";\n\nconst installerNotesLabel =\n  accountingElement(\"label\", installerNotesWrap, \"Notes\");\n\ninstallerNotesLabel.style.display = \"block\";\n\nconst installerNotes =\n  accountingElement(\"textarea\", installerNotesWrap);\n\ninstallerNotes.rows = 3;\ninstallerNotes.style.cssText =\n  \"box-sizing:border-box;width:100%;max-width:500px;\" +\n  \"font-size:16px;padding:8px;\";\n\nconst saveInstallerLaborButton =\n  accountingButton(installerLaborBox, \"Save Installer Labor\");\n\n\/\/ Disabled until Step 2B connects Supabase.\nsaveInstallerLaborButton.disabled = true;\n\nconst installerLaborMessage =\n  accountingElement(\n    \"p\",\n    installerLaborBox,\n    \"Labor form ready. Database save will be connected next.\"\n  );\n\ninstallerLaborMessage.style.cssText =\n  \"margin:10px 0 0;font-size:14px;\";\n\/\/ Step 2B save temporarily removed so Portal login stays stable.\n\nconst officeMaterialHeading = accountingElement(\"h3\", officeMaterialSection, \"Material\");\nconst officeMaterialStatus = accountingElement(\n  \"p\",\n  officeMaterialSection,\n  \"Select and load a Job ID above. Material entry will use this same Job ID.\"\n);\nofficeMaterialStatus.style.margin = \"6px 0\";\n\nfunction showOfficeModule(moduleName) {\n  officeAccountingSection.hidden = moduleName !== \"accounting\";\n  officeLaborSection.hidden = moduleName !== \"labor\";\n  officeMaterialSection.hidden = moduleName !== \"material\";\n}\n\nofficeAccountingButton.addEventListener(\"click\", function () {\n  showOfficeModule(\"accounting\");\n});\nofficeLaborButton.addEventListener(\"click\", function () {\n  showOfficeModule(\"labor\");\n});\nofficeMaterialButton.addEventListener(\"click\", function () {\n  showOfficeModule(\"material\");\n});\n\nconst invoiceHeading = accountingElement(\"h4\", officeAccountingSection, \"Invoice\");\nconst accountingSummary = accountingElement(\"p\", officeAccountingSection, \"Enter a Job ID above to load its invoice.\");\naccountingSummary.style.whiteSpace = \"pre-line\";\nconst invoiceSubtotal = accountingField(officeAccountingSection, \"Subtotal before tax ($)\", \"number\");\nconst invoiceTax = accountingField(officeAccountingSection, \"Sales Tax ($) \u2014 manual entry\", \"number\");\nconst saveInvoiceButton = accountingButton(officeAccountingSection, \"Save Invoice Amounts\");\nconst invoiceMessage = accountingElement(\"p\", officeAccountingSection);\ninvoiceMessage.setAttribute(\"role\", \"status\");\n\naccountingElement(\"h4\", officeAccountingSection, \"Payments (unlimited)\");\nconst paymentList = accountingElement(\"div\", officeAccountingSection);\naccountingElement(\"h4\", officeAccountingSection, \"Add \/ Edit Payment\");\nconst paymentAmount = accountingField(officeAccountingSection, \"Payment amount ($)\", \"number\");\npaymentAmount.min = \"0.01\";\nconst paymentDate = accountingField(officeAccountingSection, \"Payment date\", \"date\");\nconst paymentMethodWrap = accountingElement(\"div\", officeAccountingSection);\naccountingElement(\"label\", paymentMethodWrap, \"Payment method\").style.display = \"block\";\nconst paymentMethod = accountingElement(\"select\", paymentMethodWrap);\nfor (const method of [\"Zelle\", \"Check\", \"Cash\", \"Credit Card\", \"ACH\", \"Wire\", \"Other\"]) {\n  const option = accountingElement(\"option\", paymentMethod, method);\n  option.value = method;\n}\npaymentMethodWrap.style.marginBottom = \"10px\";\nconst paymentReference = accountingField(officeAccountingSection, \"Reference number (optional)\", \"text\");\nconst paymentNotesWrap = accountingElement(\"div\", officeAccountingSection);\naccountingElement(\"label\", paymentNotesWrap, \"Payment notes (optional)\").style.display = \"block\";\nconst paymentNotes = accountingElement(\"textarea\", paymentNotesWrap);\npaymentNotes.rows = 2;\npaymentNotes.style.width = \"100%\";\npaymentNotes.style.maxWidth = \"400px\";\nconst savePaymentButton = accountingButton(officeAccountingSection, \"Add Payment\");\nconst cancelPaymentButton = accountingButton(officeAccountingSection, \"Cancel Edit\");\nconst paymentMessage = accountingElement(\"p\", officeAccountingSection);\npaymentMessage.setAttribute(\"role\", \"status\");\n\nlet accountingRequestId = 0;\nlet accountingLoadedJobId = null;\nlet accountingInvoice = null;\nlet accountingPayments = [];\nlet editingPaymentId = null;\n\/\/ Remember whether the signed-in employee is allowed to see Office Accounting.\n\/\/ resetAccounting() clears job data, but must not hide the Portal-level entry\n\/\/ from an Owner \/ Office employee.\nlet accountingRoleAllowed = false;\n\nfunction resetPaymentEditor() {\n  editingPaymentId = null;\n  paymentAmount.value = \"\";\n  paymentDate.value = todayLocal();\n  paymentMethod.value = \"Zelle\";\n  paymentReference.value = \"\";\n  paymentNotes.value = \"\";\n  savePaymentButton.textContent = \"Add Payment\";\n  cancelPaymentButton.hidden = true;\n}\nfunction resetAccounting() {\n  accountingRequestId++;\n  accountingLoadedJobId = null;\n  accountingInvoice = null;\n  accountingPayments = [];\n  accountingToggle.hidden = !accountingRoleAllowed;\n  accountingToggle.setAttribute(\"aria-expanded\", \"false\");\n  accountingPanel.hidden = true;\n  accountingPortalState = null;\n  officeJobIdInput.value = \"\";\n  officeJobMessage.textContent = \"\";\n  officeLaborStatus.textContent =\n    \"Select and load a Job ID above. Labor entry will use this same Job ID.\";\n  officeMaterialStatus.textContent =\n    \"Select and load a Job ID above. Material entry will use this same Job ID.\";\n  showOfficeModule(\"accounting\");\n  accountingSummary.textContent = \"Enter a Job ID above to load its invoice.\";\n  invoiceSubtotal.value = \"\";\n  invoiceTax.value = \"\";\n  invoiceMessage.textContent = \"\";\n  paymentMessage.textContent = \"\";\n  paymentList.replaceChildren();\n  saveInvoiceButton.disabled = true;\n  savePaymentButton.disabled = true;\n  resetPaymentEditor();\n}\nfunction accountingIsCurrent(jobId) {\n  return client && accountingRoleAllowed && !logout.hidden && !accountingPanel.hidden &&\n    accountingLoadedJobId === jobId &&\n    officeJobIdInput.value.trim().toUpperCase() === jobId;\n}\nfunction drawAccounting() {\n  const subtotal = moneyCents(accountingInvoice.subtotal);\n  const tax = moneyCents(accountingInvoice.sales_tax);\n  const total = subtotal + tax;\n  const received = accountingPayments.reduce((sum, p) => sum + moneyCents(p.amount), 0);\n  const balance = total - received;\n  accountingSummary.textContent =\n    \"Invoice \/ Job ID: \" + accountingInvoice.job_id + \"\\n\" +\n    \"Subtotal: \" + moneyText(subtotal) + \"\\n\" +\n    \"Sales Tax: \" + moneyText(tax) + \"\\n\" +\n    \"Invoice Total: \" + moneyText(total) + \"\\n\" +\n    \"Total Received: \" + moneyText(received) + \"\\n\" +\n    \"Balance Due: \" + moneyText(Math.max(0, balance)) +\n    (balance < 0 ? \"\\nCredit \/ overpayment: \" + moneyText(-balance) : \"\") + \"\\n\" +\n    \"Due Date: \" + (accountingInvoice.due_date || \"Not set \u2014 awaits job completion workflow\");\n  invoiceSubtotal.value = (subtotal \/ 100).toFixed(2);\n  invoiceTax.value = (tax \/ 100).toFixed(2);\n  paymentList.replaceChildren();\n  if (!accountingPayments.length) {\n    accountingElement(\"p\", paymentList, \"No payments recorded.\");\n  }\n  for (const payment of accountingPayments) {\n    const entry = accountingElement(\"div\", paymentList);\n    entry.style.cssText = \"padding:10px 0;border-bottom:1px solid #e3e8ed;\";\n    const description = accountingElement(\"p\", entry);\n    description.style.whiteSpace = \"pre-wrap\";\n    description.textContent =\n      \"Payment #\" + payment.id + \" \u2014 \" + moneyText(moneyCents(payment.amount)) +\n      \" | \" + payment.payment_date + \" | \" + payment.payment_method +\n      (payment.reference_number ? \"\\nReference: \" + payment.reference_number : \"\") +\n      (payment.notes ? \"\\nNotes: \" + payment.notes : \"\");\n    const edit = accountingButton(entry, \"Edit Payment #\" + payment.id);\n    edit.addEventListener(\"click\", function () {\n      if (!accountingIsCurrent(payment.job_id)) return;\n      editingPaymentId = payment.id;\n      paymentAmount.value = (moneyCents(payment.amount) \/ 100).toFixed(2);\n      paymentDate.value = payment.payment_date;\n      if (![...paymentMethod.options].some((o) => o.value === payment.payment_method)) {\n        const option = accountingElement(\"option\", paymentMethod, payment.payment_method);\n        option.value = payment.payment_method;\n      }\n      paymentMethod.value = payment.payment_method;\n      paymentReference.value = payment.reference_number || \"\";\n      paymentNotes.value = payment.notes || \"\";\n      savePaymentButton.textContent = \"Save Payment Changes\";\n      cancelPaymentButton.hidden = false;\n      paymentMessage.textContent = \"Editing payment #\" + payment.id + \".\";\n      paymentAmount.focus();\n    });\n    const remove = accountingButton(entry, \"Delete Payment #\" + payment.id);\n    remove.addEventListener(\"click\", async function () {\n      const jobId = accountingLoadedJobId;\n      if (!accountingIsCurrent(jobId) || jobId !== payment.job_id) return;\n      if (!window.confirm(\"Delete payment #\" + payment.id + \" for \" +\n          moneyText(moneyCents(payment.amount)) + \"? This removes the payment row. An audit record remains.\")) return;\n      remove.disabled = true;\n      paymentMessage.textContent = \"Deleting payment...\";\n      try {\n        const { data, error } = await client.from(\"invoice_payments\")\n          .delete().eq(\"id\", payment.id).eq(\"job_id\", jobId)\n          .select(\"id\").maybeSingle();\n        if (error) throw error;\n        if (!data) throw new Error(\"Payment was not deleted. Please refresh and check permissions.\");\n        if (accountingIsCurrent(jobId)) {\n          const refreshed = await loadAccounting(jobId);\n          paymentMessage.textContent = refreshed\n            ? \"Payment deleted. Audit Log retains the record.\"\n            : \"Payment deleted, but the accounting view could not refresh.\";\n        }\n      } catch (error) {\n        if (accountingIsCurrent(jobId)) paymentMessage.textContent = \"Delete failed: \" + error.message;\n      } finally {\n        remove.disabled = false;\n      }\n    });\n  }\n}\nasync function loadAccounting(jobId) {\n  const requestId = accountingRequestId;\n  \/\/ Refresh data without forcing the Accounting section open.\n  accountingSummary.textContent = \"Loading invoice and payments...\";\n  saveInvoiceButton.disabled = true;\n  savePaymentButton.disabled = true;\n  invoiceMessage.textContent = \"\";\n  paymentList.replaceChildren();\n  try {\n    const { data: invoice, error: invoiceError } = await client\n      .from(\"job_invoices\")\n      .select(\"job_id, subtotal, sales_tax, due_date\")\n      .eq(\"job_id\", jobId).maybeSingle();\n    if (invoiceError) throw invoiceError;\n    if (!invoice) throw new Error(\"Invoice not found. Check the job invoice record.\");\n    const { data: payments, error: paymentsError } = await client\n      .from(\"invoice_payments\")\n      .select(\"id, job_id, amount, payment_date, payment_method, reference_number, notes\")\n      .eq(\"job_id\", jobId)\n      .order(\"payment_date\", { ascending: false })\n      .order(\"id\", { ascending: false });\n    if (paymentsError) throw paymentsError;\n    if (requestId !== accountingRequestId || accountingPanel.hidden ||\n        officeJobIdInput.value.trim().toUpperCase() !== jobId) return;\n    accountingLoadedJobId = jobId;\n    accountingInvoice = invoice;\n    accountingPayments = payments || [];\n    resetPaymentEditor();\n    drawAccounting();\n    saveInvoiceButton.disabled = false;\n    savePaymentButton.disabled = false;\n    return true;\n  } catch (error) {\n    if (requestId !== accountingRequestId || accountingPanel.hidden ||\n        officeJobIdInput.value.trim().toUpperCase() !== jobId) return;\n    accountingSummary.textContent = \"Unable to load accounting: \" + error.message;\n    accountingLoadedJobId = null;\n    return false;\n  }\n}\nsaveInvoiceButton.addEventListener(\"click\", async function () {\n  const jobId = accountingLoadedJobId;\n  if (!accountingIsCurrent(jobId)) return;\n  const subtotal = parseDollars(invoiceSubtotal.value, true);\n  const tax = parseDollars(invoiceTax.value, true);\n  if (subtotal === null || tax === null || subtotal + tax > 999999999999) {\n    invoiceMessage.textContent = \"Enter valid non-negative dollar amounts (max 2 decimal places).\";\n    return;\n  }\n  saveInvoiceButton.disabled = true;\n  invoiceMessage.textContent = \"Saving invoice amounts...\";\n  try {\n    const { data, error } = await client.from(\"job_invoices\")\n      .update({ subtotal: (subtotal \/ 100).toFixed(2), sales_tax: (tax \/ 100).toFixed(2) })\n      .eq(\"job_id\", jobId).select(\"job_id\").maybeSingle();\n    if (error) throw error;\n    if (!data) throw new Error(\"Invoice not updated. Please check permissions.\");\n    if (accountingIsCurrent(jobId)) {\n      const refreshed = await loadAccounting(jobId);\n      invoiceMessage.textContent = refreshed\n        ? \"Invoice amounts saved successfully!\"\n        : \"Invoice saved, but the accounting view could not refresh.\";\n    }\n  } catch (error) {\n    if (accountingIsCurrent(jobId)) invoiceMessage.textContent = \"Save failed: \" + error.message;\n  } finally {\n    if (accountingIsCurrent(jobId)) saveInvoiceButton.disabled = false;\n  }\n});\ncancelPaymentButton.addEventListener(\"click\", function () {\n  resetPaymentEditor();\n  paymentMessage.textContent = \"Edit cancelled.\";\n});\nsavePaymentButton.addEventListener(\"click\", async function () {\n  const jobId = accountingLoadedJobId;\n  if (!accountingIsCurrent(jobId)) return;\n  const cents = parseDollars(paymentAmount.value, false);\n  if (cents === null || !\/^\\d{4}-\\d{2}-\\d{2}$\/.test(paymentDate.value) ||\n      !paymentMethod.value) {\n    paymentMessage.textContent = \"Enter a positive payment amount, date and method.\";\n    return;\n  }\n  const record = {\n    job_id: jobId,\n    amount: (cents \/ 100).toFixed(2),\n    payment_date: paymentDate.value,\n    payment_method: paymentMethod.value,\n    reference_number: paymentReference.value.trim() || null,\n    notes: paymentNotes.value.trim() || null\n  };\n  const editedId = editingPaymentId;\n  savePaymentButton.disabled = true;\n  paymentMessage.textContent = editedId === null ? \"Adding payment...\" : \"Updating payment...\";\n  try {\n    const query = editedId === null\n      ? client.from(\"invoice_payments\").insert(record)\n      : client.from(\"invoice_payments\").update(record)\n          .eq(\"id\", editedId).eq(\"job_id\", jobId);\n    const { data, error } = await query.select(\"id\").maybeSingle();\n    if (error) throw error;\n    if (!data) throw new Error(\"Payment not saved. Please check permissions.\");\n    if (accountingIsCurrent(jobId)) {\n      const refreshed = await loadAccounting(jobId);\n      paymentMessage.textContent = !refreshed\n        ? \"Payment saved, but the accounting view could not refresh.\"\n        : (editedId === null ? \"Payment added successfully!\" : \"Payment updated successfully!\");\n    }\n  } catch (error) {\n    if (accountingIsCurrent(jobId)) paymentMessage.textContent = \"Save failed: \" + error.message;\n  } finally {\n    if (accountingIsCurrent(jobId)) savePaymentButton.disabled = false;\n  }\n});\nresetAccounting();\n\nofficeLoadJobButton.addEventListener(\"click\", async function () {\n  if (!client || !accountingRoleAllowed || logout.hidden || accountingPanel.hidden ||\n      officeLoadJobButton.disabled) return;\n\n  const jobId = officeJobIdInput.value.trim().toUpperCase();\n  officeJobIdInput.value = jobId;\n\n  if (!\/^GW-\\d{4}-\\d{3}$\/.test(jobId)) {\n    officeJobMessage.textContent = \"Enter a valid Job ID, for example GW-2610-001.\";\n    return;\n  }\n\n  accountingRequestId++;\n  accountingLoadedJobId = null;\n  accountingInvoice = null;\n  accountingPayments = [];\n  saveInvoiceButton.disabled = true;\n  savePaymentButton.disabled = true;\n  officeLoadJobButton.disabled = true;\n  officeClearJobButton.disabled = true;\n  officeJobMessage.textContent = \"Checking Job ID...\";\n  accountingSummary.textContent = \"Loading...\";\n  paymentList.replaceChildren();\n  resetPaymentEditor();\n\n  try {\n    const { data: job, error: jobError } = await client\n      .from(\"Jobs\")\n      .select(\"job_id\")\n      .eq(\"job_id\", jobId)\n      .maybeSingle();\n\n    if (jobError) throw jobError;\n    if (!job) throw new Error(\"Job ID not found.\");\n\n    officeJobMessage.textContent = \"Loaded \" + jobId + \".\";\n    officeLaborStatus.textContent =\n      \"Current Job ID: \" + jobId + \" \u2014 ready for Labor entry.\";\n    officeMaterialStatus.textContent =\n      \"Current Job ID: \" + jobId + \" \u2014 ready for Material entry.\";\n\n    const loaded = await loadAccounting(jobId);\n    if (!loaded && !accountingSummary.textContent.startsWith(\"Unable\")) {\n      accountingSummary.textContent = \"Invoice data is not available for this Job ID.\";\n    }\n  } catch (error) {\n    accountingLoadedJobId = null;\n    accountingInvoice = null;\n    accountingPayments = [];\n    accountingSummary.textContent = \"No Office accounting loaded.\";\n    officeLaborStatus.textContent =\n      \"Select and load a valid Job ID above before entering Labor.\";\n    officeMaterialStatus.textContent =\n      \"Select and load a valid Job ID above before entering Material.\";\n    officeJobMessage.textContent = error.message || \"Unable to load Job ID.\";\n  } finally {\n    officeLoadJobButton.disabled = false;\n    officeClearJobButton.disabled = false;\n  }\n});\n\nofficeJobIdInput.addEventListener(\"keydown\", function (event) {\n  if (event.key !== \"Enter\" || event.isComposing) return;\n  event.preventDefault();\n  if (!event.repeat && !officeLoadJobButton.disabled) officeLoadJobButton.click();\n});\n\nofficeClearJobButton.addEventListener(\"click\", function () {\n  accountingRequestId++;\n  accountingLoadedJobId = null;\n  accountingInvoice = null;\n  accountingPayments = [];\n  officeJobIdInput.value = \"\";\n  officeJobMessage.textContent = \"\";\n  accountingSummary.textContent = \"Enter a Job ID above to load its invoice.\";\n  invoiceSubtotal.value = \"\";\n  invoiceTax.value = \"\";\n  invoiceMessage.textContent = \"\";\n  paymentMessage.textContent = \"\";\n  paymentList.replaceChildren();\n  saveInvoiceButton.disabled = true;\n  savePaymentButton.disabled = true;\n  resetPaymentEditor();\n  officeLaborStatus.textContent =\n    \"Select and load a Job ID above. Labor entry will use this same Job ID.\";\n  officeMaterialStatus.textContent =\n    \"Select and load a Job ID above. Material entry will use this same Job ID.\";\n  officeJobIdInput.focus();\n});\n\nconst measurementButton =\n  document.getElementById(\"gw-final-measurement\");\n\nconst measurementForm =\n  document.getElementById(\"gw-measurement-form\");\n\n\nmeasurementButton.addEventListener(\"click\", function () {\n  if (dashboard.hidden || !currentWorkJobId) return;\n  measurementDashboard.hidden = !measurementDashboard.hidden;\n  if (measurementDashboard.hidden) measurementForm.hidden = true;\n});\nconst saveMeasurement =\n  document.getElementById(\"gw-save-measurement\");\n\nconst measurementMessage =\n  document.getElementById(\"gw-measurement-message\");\n\nsaveMeasurement.addEventListener(\"click\", async function () {\n  const jobId = jobIdInput.value.trim();\n  const measuredBy =\n    document.getElementById(\"gw-measured-by\").value.trim();\n  const measurementDate =\n    document.getElementById(\"gw-measurement-date\").value;\n  const projectArea =\n    document.getElementById(\"gw-project-area\").value.trim();\n  const fieldNotes =\n    document.getElementById(\"gw-field-notes\").value.trim();\nconst materials =\n  document.getElementById(\"gw-materials\").value.trim();\n\nconst edge =\n  document.getElementById(\"gw-edge\").value;\n\nconst sinkType =\n  document.getElementById(\"gw-sink-type\").value;\n\nconst backsplash =\n  document.getElementById(\"gw-backsplash\").value.trim();\n\nconst seamRequired =\n  document.getElementById(\"gw-seam-required\").checked;\nconst sinkModel =\n  document.getElementById(\"gw-sink-model\").value.trim();\n\nconst stoveType =\n  document.getElementById(\"gw-stove-type\").value;\n\nconst faucet =\n  document.getElementById(\"gw-faucet\").value.trim(); \n   if (!client || dashboard.hidden || !jobId ||\n       jobId !== measurementLoadedJobId || !canEditMeasurement(jobId) ||\n       !measurementLoadOK || measurementForm.hidden ||\n       !measuredBy || !measurementDate || !projectArea) {\n    measurementMessage.textContent =\n      \"Please find a job and complete all required fields.\";\n    return;\n  }\n\n  saveMeasurement.disabled = true;\n  measurementMessage.textContent = \"Saving...\";\n\n  try {\n    const { data: auth, error: authError } =\n      await client.auth.getUser();\n\n    if (authError || !auth.user) {\n      measurementMessage.textContent =\n        \"Please log in again.\";\n      return;\n    }\n\n    const { data: job, error: jobError } = await client\n      .from(\"Jobs\")\n      .select(\"job_id\")\n      .eq(\"job_id\", jobId)\n      .maybeSingle();\n\n    if (jobError || !job) {\n      measurementMessage.textContent =\n        \"Job ID not found. Please search again.\";\n      return;\n    }\n\n    \/\/ Check for the existing measurement before writing; do not create duplicates.\n    const { data: existingMeasurement, error: measurementLookupError } =\n      await client\n        .from(\"final_measurements\")\n        .select(\"id\")\n        .eq(\"job_id\", jobId)\n        .order(\"id\", { ascending: false })\n        .limit(1)\n        .maybeSingle();\n    if (measurementLookupError) throw measurementLookupError;\n\n    const measurementData = {\n  job_id: jobId,\n  measured_by: measuredBy,\n  measurement_date: measurementDate,\n  project_area: projectArea,\n  field_notes: fieldNotes,\n  material: materials,\n  edge: edge,\n  sink_type: sinkType,\n sink_model: sinkModel,\nstove_type: stoveType,\nfaucet: faucet,\n backsplash: backsplash,\n  seam_required: seamRequired\n};\n        \/\/ RETURNING verifies an actual row was written. Zero updated rows must not\n    \/\/ produce a misleading success message when RLS blocks the operation.\n    const { data: savedRow, error } = existingMeasurement\n       ? await client\n           .from(\"final_measurements\")\n           .update(measurementData)\n           .eq(\"id\", existingMeasurement.id)\n           .eq(\"job_id\", jobId)\n           .select(\"*\").maybeSingle()\n       : await client\n           .from(\"final_measurements\")\n           .insert(measurementData)\n           .select(\"*\").maybeSingle();\n    if (error) throw error;\n    if (!savedRow) throw new Error(\"No measurement was saved. Check database permissions.\");\n    if (canEditMeasurement(jobId)) {\n      renderWorkSummary(measurementDashboardContent, savedRow, measurementDashboardGroups,\n        \"No Final Measurement recorded yet.\");\n      measurementMessage.textContent = \"Measurement saved successfully!\";\n      measurementForm.hidden = true;\n    }\n     } catch (error) {\n     if (canEditMeasurement(jobId))\n       measurementMessage.textContent = \"Save failed: \" + error.message;\n   } finally {\n     saveMeasurement.disabled = !canEditMeasurement(jobId) || !measurementLoadOK;\n  }\n});\n\nconst preparationButton =\n  document.getElementById(\"gw-site-preparation\");\n\nconst preparationForm =\n  document.getElementById(\"gw-preparation-form\");\n\npreparationButton.addEventListener(\"click\", function () {\n  if (dashboard.hidden || !currentWorkJobId) return;\n  preparationDashboard.hidden = !preparationDashboard.hidden;\n  if (preparationDashboard.hidden) preparationForm.hidden = true;\n});\nconst savePreparation =\n  document.getElementById(\"gw-save-preparation\");\n\nconst preparationMessage =\n  document.getElementById(\"gw-preparation-message\");\n\n\/\/ READ-ONLY DASHBOARDS: show persisted data; do not confuse form visibility\n\/\/ with database authorization. Existing edit forms stay hidden until explicitly opened.\nconst measurementDashboard = document.createElement(\"section\");\nmeasurementDashboard.id = \"gw-measurement-dashboard\";\nmeasurementDashboard.hidden = true;\nmeasurementDashboard.style.cssText =\n  \"padding:16px;margin:12px 0 20px;border:1px solid #cbd5e1;border-radius:8px;\";\nconst measurementDashboardTitle = document.createElement(\"h3\");\nmeasurementDashboardTitle.textContent = \"Final Measurement Dashboard\";\nconst measurementDashboardContent = document.createElement(\"div\");\nconst editMeasurementButton = measurementButton.cloneNode(false);\neditMeasurementButton.id = \"gw-edit-measurement\";\neditMeasurementButton.type = \"button\";\neditMeasurementButton.textContent = \"Edit Final Measurement\";\neditMeasurementButton.hidden = true;\neditMeasurementButton.style.cssText = \"width:100%;margin-top:12px;\";\nmeasurementDashboard.append(measurementDashboardTitle, measurementDashboardContent, editMeasurementButton);\n\nconst preparationDashboard = document.createElement(\"section\");\npreparationDashboard.id = \"gw-preparation-dashboard\";\npreparationDashboard.hidden = true;\npreparationDashboard.style.cssText =\n  \"padding:16px;margin:12px 0 20px;border:1px solid #cbd5e1;border-radius:8px;\";\nconst preparationDashboardTitle = document.createElement(\"h3\");\npreparationDashboardTitle.textContent = \"Site Preparation Dashboard\";\nconst preparationDashboardContent = document.createElement(\"div\");\nconst editPreparationButton = preparationButton.cloneNode(false);\neditPreparationButton.id = \"gw-edit-preparation\";\neditPreparationButton.type = \"button\";\neditPreparationButton.textContent = \"Edit Site Preparation\";\neditPreparationButton.hidden = true;\neditPreparationButton.style.cssText = \"width:100%;margin-top:12px;\";\npreparationDashboard.append(preparationDashboardTitle, preparationDashboardContent, editPreparationButton);\n\nconst measurementDashboardGroups = [\n  [\"Measurement & design\", [\n    [\"measured_by\", \"Measured by\"], [\"measurement_date\", \"Measurement date\"],\n    [\"project_area\", \"Project area\"], [\"material\", \"Material\"],\n    [\"edge\", \"Edge\"], [\"sink_type\", \"Sink type\"],\n    [\"sink_model\", \"Sink model\"], [\"stove_type\", \"Stove type\"],\n    [\"faucet\", \"Faucet\"], [\"backsplash\", \"Backsplash\"],\n    [\"seam_required\", \"Seam required\"]\n  ]],\n  [\"Field notes\", [[\"field_notes\", \"Notes\"]]]\n];\nconst preparationDashboardGroups = [\n  [\"Water & plumbing\", [\n    [\"water_shutoff_condition\", \"Water shutoff condition\"],\n    [\"water_shutoff_replace\", \"Replace water shutoff\"],\n    [\"faucet_condition\", \"Faucet condition\"],\n    [\"plumbing_modification\", \"Plumbing modification\"],\n    [\"plumbing_notes\", \"Plumbing notes\"]\n  ]],\n  [\"Electrical, mirrors & walls\", [\n    [\"electrical_modification\", \"Electrical modification\"],\n    [\"outlet_issue\", \"Outlet issue\"], [\"electrical_notes\", \"Electrical notes\"],\n    [\"mirror_removal\", \"Mirror removal\"], [\"wall_condition\", \"Wall condition\"],\n    [\"wall_work_required\", \"Wall work required\"]\n  ]],\n  [\"Floor & access\", [\n    [\"floor_type\", \"Floor type\"], [\"floor_protection_required\", \"Floor protection required\"],\n    [\"floor_protection_type\", \"Floor protection type\"],\n    [\"protection_notes\", \"Protection notes\"],\n    [\"access_condition\", \"Access condition\"], [\"stairs_required\", \"Stairs required\"],\n    [\"special_access_notes\", \"Special access notes\"]\n  ]],\n  [\"Tools & parts\", [\n    [\"special_tools_required\", \"Special tools required\"],\n    [\"special_tools_list\", \"Special tools list\"],\n    [\"special_parts_required\", \"Special parts required\"],\n    [\"special_parts_list\", \"Special parts list\"]\n  ]],\n  [\"Installer handoff\", [\n    [\"installer_notes\", \"Installer notes\"], [\"prepared_by\", \"Prepared by\"],\n    [\"preparation_complete\", \"Preparation complete\"],\n    [\"installer_reviewed\", \"Installer reviewed\"],\n    [\"installer_reviewed_by\", \"Installer reviewed by\"],\n    [\"ready_for_install\", \"Ready for installation\"]\n  ]]\n];\n\nfunction renderWorkSummary(container, savedRow, groups, emptyText) {\n  container.replaceChildren();\n  if (!savedRow) {\n    const p = document.createElement(\"p\");\n    p.textContent = emptyText;\n    container.appendChild(p);\n    return;\n  }\n  for (const [heading, fields] of groups) {\n    const section = document.createElement(\"section\");\n    const title = document.createElement(\"h4\");\n    title.textContent = heading;\n    section.appendChild(title);\n    const grid = document.createElement(\"div\");\n    grid.style.cssText =\n      \"display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,220px),1fr));gap:10px;margin-bottom:16px;\";\n    for (const [column, label] of fields) {\n      const cell = document.createElement(\"div\");\n      cell.style.cssText =\n        \"padding:10px;border:1px solid #e0e8f2;border-radius:6px;background:#f8fbff;min-width:0;\";\n      const name = document.createElement(\"strong\");\n      name.textContent = label;\n      name.style.display = \"block\";\n      const value = document.createElement(\"div\");\n      const raw = savedRow[column];\n      value.textContent = typeof raw === \"boolean\" ? (raw ? \"Yes\" : \"No\") :\n        (raw === null || raw === undefined || raw === \"\" ? \"\u2014\" : String(raw));\n      value.style.cssText = \"white-space:pre-wrap;overflow-wrap:anywhere;margin-top:4px;\";\n\n      \/\/ Translate only the approved natural-language note fields.\n      \/\/ Names, addresses, Job IDs, materials, models, measurements, dates,\n      \/\/ money, filenames, and all other job data remain untouched.\n      if (GW_SAFE_WORK_NOTE_COLUMNS.has(column) &&\n          raw !== null && raw !== undefined && String(raw) !== \"\") {\n        value.dataset.gwSafeNoteSource = String(raw);\n        translateSafeWorkNoteElement(value);\n      }\n\n      cell.append(name, value);\n      grid.appendChild(cell);\n    }\n    section.appendChild(grid);\n    container.appendChild(section);\n  }\n}\nfunction canEditMeasurement(jobId) {\n  return !!jobId && currentWorkJobId === jobId &&\n    (currentWorkRole === \"owner\" || currentWorkRole === \"project_manager\");\n}\nfunction canEditPreparation(jobId) {\n  return !!jobId && currentWorkJobId === jobId &&\n    (currentWorkRole === \"owner\" || currentWorkRole === \"project_manager\");\n}\nfunction setWorkFormEditable(form, allowed) {\n  form.querySelectorAll(\"input, textarea, select, button\").forEach(function (control) {\n    control.disabled = !allowed;\n  });\n}\nfunction resetWorkDashboards() {\n  currentWorkRole = null;\n  currentWorkJobId = null;\n  measurementLoadOK = false;\n  preparationLoadOK = false;\n  measurementButton.hidden = true;\n  preparationButton.hidden = true;\n  measurementDashboard.hidden = true;\n  preparationDashboard.hidden = true;\n  measurementForm.hidden = true;\n  preparationForm.hidden = true;\n  editMeasurementButton.hidden = true;\n  editPreparationButton.hidden = true;\n  measurementDashboardContent.replaceChildren();\n  preparationDashboardContent.replaceChildren();\n  setWorkFormEditable(measurementForm, false);\n  setWorkFormEditable(preparationForm, false);\n  saveMeasurement.hidden = true;\n  savePreparation.hidden = true;\n  measurementMessage.textContent = \"\";\n  preparationMessage.textContent = \"\";\n}\nfunction configureWorkDashboards(role, jobId) {\n  currentWorkRole = role;\n  currentWorkJobId = jobId;\n\n  \/\/ VIEW and EDIT are deliberately separate permissions.\n  \/\/ All active Portal roles may view the saved read-only dashboards.\n  \/\/ Only Owner \/ Project Manager may edit or save.\n  const viewAllowed = !!jobId && [\n    \"owner\", \"office\", \"project_manager\", \"installer\", \"fabricator\"\n  ].includes(role);\n\n  const measurementEditAllowed =\n    canEditMeasurement(jobId) && measurementLoadOK;\n\n  const preparationEditAllowed =\n    canEditPreparation(jobId) && preparationLoadOK;\n\n  \/\/ Keep the main read-only entry buttons available to every approved role.\n  measurementButton.hidden = !viewAllowed;\n  preparationButton.hidden = !viewAllowed;\n\n  setWorkFormEditable(measurementForm, measurementEditAllowed);\n  setWorkFormEditable(preparationForm, preparationEditAllowed);\n\n  editMeasurementButton.hidden = !measurementEditAllowed;\n  editPreparationButton.hidden = !preparationEditAllowed;\n\n  saveMeasurement.hidden = !measurementEditAllowed;\n  savePreparation.hidden = !preparationEditAllowed;\n  saveMeasurement.disabled = !measurementEditAllowed;\n  savePreparation.disabled = !preparationEditAllowed;\n\n  \/\/ Forms stay closed by default. Read-only dashboards open from the main buttons.\n  measurementForm.hidden = true;\n  preparationForm.hidden = true;\n}\neditMeasurementButton.addEventListener(\"click\", function () {\n  if (!dashboard.hidden && canEditMeasurement(currentWorkJobId) && measurementLoadOK)\n    measurementForm.hidden = !measurementForm.hidden;\n});\neditPreparationButton.addEventListener(\"click\", function () {\n  if (!dashboard.hidden && canEditPreparation(currentWorkJobId) && preparationLoadOK)\n    preparationForm.hidden = !preparationForm.hidden;\n});\n\n\nsavePreparation.addEventListener(\"click\", async function () {\n  const jobId = jobIdInput.value.trim();\n\n     if (!client || dashboard.hidden || !jobId || !canEditPreparation(jobId) ||\n       !preparationLoadOK || preparationForm.hidden) {\n     preparationMessage.textContent =\n       \"Site Preparation editing is not available for this account and job.\";\n     return;\n  }\n\n   savePreparation.disabled = true;\n  preparationMessage.textContent = \"Saving...\";\n\n  try {\n    const { data: auth, error: authError } =\n      await client.auth.getUser();\n\n    if (authError || !auth.user) {\n      preparationMessage.textContent = \"Please log in again.\";\n      return;\n    }\nconst { data: existing, error: lookupError } = await client\n  .from(\"site_preparation\")\n  .select(\"id\")\n  .eq(\"job_id\", jobId)\n  .order(\"id\", { ascending: false })\n  .limit(1)\n  .maybeSingle();\n\nif (lookupError) throw lookupError;\n const preparationData = {\n        job_id: jobId,\n        water_shutoff_condition:\n          document.getElementById(\"gw-water-condition\").value.trim(),\n        water_shutoff_replace:\n          document.getElementById(\"gw-water-replace\").checked,\n        faucet_condition:\n          document.getElementById(\"gw-faucet-condition\").value.trim(),\n        plumbing_modification:\n          document.getElementById(\"gw-plumbing-modification\").checked,\n        plumbing_notes:\n  document.getElementById(\"gw-plumbing-notes\").value.trim(),\n\nelectrical_modification:\n  document.getElementById(\"gw-electrical-modification\").checked,\noutlet_issue:\n  document.getElementById(\"gw-outlet-issue\").checked,\nelectrical_notes:\n  document.getElementById(\"gw-electrical-notes\").value.trim(),\nmirror_removal:\n  document.getElementById(\"gw-mirror-removal\").value,\n\nwall_condition:\n  document.getElementById(\"gw-wall-condition\").value.trim(),\nwall_work_required:\n  document.getElementById(\"gw-wall-work-required\").checked,\n\nfloor_type:\n  document.getElementById(\"gw-floor-type\").value.trim(),\nfloor_protection_required:\n  document.getElementById(\"gw-floor-protection-required\").checked,\nfloor_protection_type:\n  document.getElementById(\"gw-floor-protection-type\").value.trim(),\n protection_notes:\n  document.getElementById(\"gw-protection-notes\").value.trim(),\n\naccess_condition:\n  document.getElementById(\"gw-access-condition\").value.trim(),\nstairs_required:\n  document.getElementById(\"gw-stairs-required\").checked,\nspecial_access_notes:\n  document.getElementById(\"gw-special-access-notes\").value.trim(),\n\nspecial_tools_required:\n  document.getElementById(\"gw-special-tools-required\").checked,\nspecial_tools_list:\n  document.getElementById(\"gw-special-tools-list\").value.trim(),\nspecial_parts_required:\n  document.getElementById(\"gw-special-parts-required\").checked,\nspecial_parts_list:\n  document.getElementById(\"gw-special-parts-list\").value.trim(),\n\ninstaller_notes:\n  document.getElementById(\"gw-installer-notes\").value.trim(),\nprepared_by:\n  document.getElementById(\"gw-prepared-by\").value.trim(),\npreparation_complete:\n  document.getElementById(\"gw-preparation-complete\").checked,\ninstaller_reviewed:\n  document.getElementById(\"gw-installer-reviewed\").checked,\ninstaller_reviewed_by:\n  document.getElementById(\"gw-installer-reviewed-by\").value.trim(),\nready_for_install:\n  document.getElementById(\"gw-ready-for-install\").checked\n};\n\nconst { data: savedRow, error } = existing\n   ? await client\n       .from(\"site_preparation\")\n       .update(preparationData)\n       .eq(\"id\", existing.id)\n       .eq(\"job_id\", jobId)\n       .select(\"*\").maybeSingle()\n   : await client\n       .from(\"site_preparation\")\n       .insert(preparationData)\n       .select(\"*\").maybeSingle();\n     if (error) throw error;\n     if (!savedRow) throw new Error(\"No preparation was saved. Check database permissions.\");\n     if (canEditPreparation(jobId)) {\n       renderWorkSummary(preparationDashboardContent, savedRow, preparationDashboardGroups,\n         \"No Site Preparation recorded yet.\");\n       preparationMessage.textContent = \"Site Preparation saved successfully!\";\n       preparationForm.hidden = true;\n     }\n     } catch (error) {\n     if (canEditPreparation(jobId))\n       preparationMessage.textContent = \"Save failed: \" + error.message;\n   } finally {\n     savePreparation.disabled = !canEditPreparation(jobId) || !preparationLoadOK;\n  }\n});\nconst reviewButton =\n  document.getElementById(\"gw-installer-review-button\");\n\/\/ Installer Review has been retired from the Portal UI.\n\/\/ Keep legacy elements\/data intact so this change cannot damage stored review records.\nreviewButton.hidden = true;\nreviewButton.style.display = \"none\";\n\/\/ Keep the Accounting button in the existing Portal flow.\nreviewButton.insertAdjacentElement(\"afterend\", accountingToggle);\naccountingToggle.insertAdjacentElement(\"afterend\", accountingPanel);\n\nconst reviewForm =\n  document.getElementById(\"gw-installer-review-form\");\nreviewForm.hidden = true;\nreviewForm.style.display = \"none\";\n\n\/\/ Legacy owner review overview is also retired from the Portal UI. Access to rows is enforced by Supabase RLS.\nconst allReviewsPanel = document.createElement(\"section\");\nallReviewsPanel.id = \"gw-all-installer-reviews\";\nallReviewsPanel.hidden = true;\nallReviewsPanel.style.marginTop = \"20px\";\nallReviewsPanel.style.padding = \"16px\";\nallReviewsPanel.style.border = \"1px solid #ccd6e0\";\nallReviewsPanel.style.borderRadius = \"8px\";\nconst allReviewsHeading = document.createElement(\"h3\");\nallReviewsHeading.textContent = \"All Installer Reviews (Owner Only)\";\nconst allReviewsContent = document.createElement(\"div\");\nallReviewsPanel.append(allReviewsHeading, allReviewsContent);\nreviewForm.insertAdjacentElement(\"afterend\", allReviewsPanel);\nallReviewsPanel.insertAdjacentElement(\"afterend\", accountingToggle);\naccountingToggle.insertAdjacentElement(\"afterend\", accountingPanel);\n\n\/\/ JOB STATUS is shown only to Owner \/ Project Manager.\n\/\/ Intermediate stages use gw_set_job_status. Final \"Completed\" still uses\n\/\/ gw_mark_job_completed so completed_at and the 30-day invoice due date remain atomic.\nconst completionButton = reviewButton.cloneNode(false);\ncompletionButton.id = \"gw-job-status-toggle\";\ncompletionButton.type = \"button\";\ncompletionButton.textContent = \"Job Status\";\ncompletionButton.hidden = true;\ncompletionButton.disabled = true;\ncompletionButton.style.width = \"100%\";\ncompletionButton.style.marginBottom = \"8px\";\ncompletionButton.setAttribute(\"aria-expanded\", \"false\");\n\nconst jobStatusPanel = document.createElement(\"section\");\njobStatusPanel.id = \"gw-job-status-panel\";\njobStatusPanel.hidden = true;\njobStatusPanel.style.cssText =\n  \"padding:14px;margin:0 0 12px;border:1px solid #cbd5e1;border-radius:8px;\";\n\nconst jobStatusHeading = document.createElement(\"h3\");\njobStatusHeading.textContent = \"Job Status\";\n\nconst jobStatusCurrent = document.createElement(\"p\");\njobStatusCurrent.id = \"gw-job-status-current\";\n\nconst jobStatusLabel = document.createElement(\"label\");\njobStatusLabel.htmlFor = \"gw-job-status-select\";\njobStatusLabel.textContent = \"Change status\";\njobStatusLabel.style.cssText = \"display:block;margin:10px 0 6px;font-weight:600;\";\n\nconst jobStatusSelect = document.createElement(\"select\");\njobStatusSelect.id = \"gw-job-status-select\";\njobStatusSelect.style.cssText = \"width:100%;max-width:460px;padding:10px;\";\n\n[\n  \"Approved\",\n  \"Final Measurement\",\n  \"Fabrication\",\n  \"Ready for Installation\",\n  \"Completed\"\n].forEach(function (stage) {\n  const option = document.createElement(\"option\");\n  option.value = stage;\n  option.textContent = stage;\n  jobStatusSelect.appendChild(option);\n});\n\nconst jobStatusSave = document.createElement(\"button\");\njobStatusSave.type = \"button\";\njobStatusSave.id = \"gw-job-status-save\";\njobStatusSave.textContent = \"Save Status\";\njobStatusSave.style.cssText = \"display:block;width:100%;margin:12px 0 6px;\";\n\nconst completionMessage = document.createElement(\"p\");\ncompletionMessage.id = \"gw-completion-message\";\ncompletionMessage.setAttribute(\"role\", \"status\");\ncompletionMessage.hidden = true;\ncompletionMessage.style.whiteSpace = \"pre-line\";\n\njobStatusPanel.append(\n  jobStatusHeading, jobStatusCurrent, jobStatusLabel,\n  jobStatusSelect, jobStatusSave, completionMessage\n);\n\nallReviewsPanel.insertAdjacentElement(\"afterend\", completionButton);\ncompletionButton.insertAdjacentElement(\"afterend\", jobStatusPanel);\n\/\/ Restore the pre-existing order: Installer Review, owner overview,\n\/\/ Job Status, Office Accounting.\njobStatusPanel.insertAdjacentElement(\"afterend\", accountingToggle);\naccountingToggle.insertAdjacentElement(\"afterend\", accountingPanel);\n\n\/\/ OWNER-ONLY JOB ASSIGNMENTS. Backend RLS and the owner-only roster RPC\n\/\/ enforce permissions independently of this interface.\nconst assignmentToggle = document.createElement(\"button\");\nassignmentToggle.id = \"gw-assignment-toggle\";\nassignmentToggle.type = \"button\";\nassignmentToggle.textContent = \"Assign Installers (Owner Only)\";\nassignmentToggle.hidden = true;\nassignmentToggle.style.cssText = \"width:100%;margin-bottom:12px;\";\nassignmentToggle.setAttribute(\"aria-expanded\", \"false\");\nconst assignmentPanel = document.createElement(\"section\");\nassignmentPanel.id = \"gw-assignment-panel\";\nassignmentPanel.hidden = true;\nassignmentPanel.style.cssText =\n  \"margin:12px 0 20px;padding:16px;border:1px solid #cbd5e1;border-radius:8px;\";\nconst assignmentHeading = document.createElement(\"h3\");\nassignmentHeading.textContent = \"Installer Assignments (Owner Only)\";\nconst assignmentList = document.createElement(\"div\");\nconst assignmentControls = document.createElement(\"div\");\nconst assignmentMessage = document.createElement(\"p\");\nassignmentMessage.setAttribute(\"role\", \"status\");\nassignmentPanel.append(assignmentHeading, assignmentList, assignmentControls, assignmentMessage);\ncompletionMessage.insertAdjacentElement(\"afterend\", assignmentToggle);\nassignmentToggle.insertAdjacentElement(\"afterend\", assignmentPanel);\nassignmentPanel.insertAdjacentElement(\"afterend\", accountingToggle);\naccountingToggle.insertAdjacentElement(\"afterend\", accountingPanel);\n\nlet assignmentJobId = null;\nlet assignmentRequestId = 0;\nlet assignmentBusy = false;\nfunction resetOwnerAssignments() {\n  assignmentRequestId++;\n  assignmentJobId = null;\n  assignmentBusy = false;\n  assignmentToggle.hidden = true;\n  assignmentToggle.setAttribute(\"aria-expanded\", \"false\");\n  assignmentPanel.hidden = true;\n  assignmentList.replaceChildren();\n  assignmentControls.replaceChildren();\n  assignmentMessage.textContent = \"\";\n}\nfunction configureOwnerAssignments(jobId) {\n  assignmentJobId = jobId;\n  assignmentToggle.hidden = false;\n  assignmentPanel.hidden = true;\n  assignmentToggle.setAttribute(\"aria-expanded\", \"false\");\n  assignmentMessage.textContent = \"\";\n}\nfunction ownerAssignmentIsCurrent(jobId, requestId) {\n  return !!client && !dashboard.hidden && !assignmentToggle.hidden &&\n    !assignmentPanel.hidden && assignmentJobId === jobId &&\n    jobIdInput.value.trim() === jobId &&\n    (requestId === undefined || requestId === assignmentRequestId);\n}\nasync function loadOwnerAssignments(jobId) {\n  const requestId = ++assignmentRequestId;\n  assignmentList.textContent = \"Loading assigned installers...\";\n  assignmentControls.replaceChildren();\n  assignmentMessage.textContent = \"\";\n  try {\n    \/\/ Function returns only the Installer display name and ID, and rejects non-owners.\n    const { data: installers, error: rosterError } =\n      await client.rpc(\"gw_owner_list_installers\");\n    if (rosterError) throw rosterError;\n    if (!ownerAssignmentIsCurrent(jobId, requestId)) return false;\n    const { data: assignments, error: assignmentError } = await client\n      .from(\"job_assignments\")\n      .select(\"id, job_id, user_id\")\n      .eq(\"job_id\", jobId)\n      .order(\"id\", { ascending: true });\n    if (assignmentError) throw assignmentError;\n    if (!ownerAssignmentIsCurrent(jobId, requestId)) return false;\n    const installerList = installers || [];\n    const assignedList = assignments || [];\n    const installerNames = new Map(installerList.map(function (installer) {\n      return [installer.user_id, installer.display_name || \"Unnamed installer\"];\n    }));\n    assignmentList.replaceChildren();\n    const count = document.createElement(\"p\");\n    count.textContent = \"Job \" + jobId + \" \u2014 \" + assignedList.length + \" assigned installer(s).\";\n    assignmentList.appendChild(count);\n    if (!assignedList.length) {\n      const empty = document.createElement(\"p\");\n      empty.textContent = \"No installers assigned yet.\";\n      assignmentList.appendChild(empty);\n    }\n    for (const item of assignedList) {\n      const row = document.createElement(\"div\");\n      row.style.cssText = \"padding:10px 0;border-bottom:1px solid #e3e8ed;\";\n      const name = installerNames.get(item.user_id) ||\n        \"Employee \" + item.user_id.slice(0, 8) + \" (not in installer roster)\";\n      const label = document.createElement(\"span\");\n      label.textContent = name + \" \";\n      const removeButton = document.createElement(\"button\");\n      removeButton.type = \"button\";\n      removeButton.textContent = \"Remove \" + name;\n      removeButton.style.marginLeft = \"10px\";\n      removeButton.addEventListener(\"click\", async function () {\n        if (!ownerAssignmentIsCurrent(jobId) || assignmentBusy) return;\n if (!window.confirm(\n  \"Remove \" + name + \" from job \" + jobId +\n  \"? They can still view the job by Job ID.\"\n)) return;\n        assignmentBusy = true;\n        removeButton.disabled = true;\n        assignmentMessage.textContent = \"Removing assignment...\";\n        try {\n          const { data: deleted, error } = await client.from(\"job_assignments\")\n            .delete().eq(\"id\", item.id).eq(\"job_id\", jobId)\n            .eq(\"user_id\", item.user_id).select(\"id\").maybeSingle();\n          if (error) throw error;\n          if (!deleted) throw new Error(\"Assignment not removed. Check permissions and refresh.\");\n          if (ownerAssignmentIsCurrent(jobId)) {\n            const refreshed = await loadOwnerAssignments(jobId);\n            if (refreshed && ownerAssignmentIsCurrent(jobId))\n              assignmentMessage.textContent = name + \" removed from \" + jobId + \".\";\n          }\n        } catch (error) {\n          if (ownerAssignmentIsCurrent(jobId))\n            assignmentMessage.textContent = \"Remove failed: \" + error.message;\n        } finally {\n          if (ownerAssignmentIsCurrent(jobId)) assignmentBusy = false;\n          removeButton.disabled = false;\n        }\n      });\n      row.append(label, removeButton);\n      assignmentList.appendChild(row);\n    }\n    const assignedUserIds = new Set(assignedList.map((item) => item.user_id));\n    const available = installerList.filter((installer) =>\n      !assignedUserIds.has(installer.user_id));\n    assignmentControls.replaceChildren();\n    if (!available.length) {\n      const none = document.createElement(\"p\");\n      none.textContent = \"No additional installer accounts available to assign.\";\n      assignmentControls.appendChild(none);\n    } else {\n      const selectLabel = document.createElement(\"label\");\n      selectLabel.textContent = \"Choose an installer to assign\";\n      selectLabel.style.display = \"block\";\n      const select = document.createElement(\"select\");\n      select.style.cssText = \"width:100%;max-width:400px;margin:8px 0;\";\n      const prompt = document.createElement(\"option\");\n      prompt.value = \"\";\n      prompt.textContent = \"Select an installer\";\n      select.appendChild(prompt);\n      for (const installer of available) {\n        const option = document.createElement(\"option\");\n        option.value = installer.user_id;\n        option.textContent = installer.display_name || \"Installer \" + installer.user_id.slice(0, 8);\n        select.appendChild(option);\n      }\n      const addButton = document.createElement(\"button\");\n      addButton.type = \"button\";\n      addButton.textContent = \"Assign Selected Installer\";\n      addButton.disabled = true;\n      select.addEventListener(\"change\", function () {\n        addButton.disabled = !select.value || assignmentBusy;\n      });\n      addButton.addEventListener(\"click\", async function () {\n        const userId = select.value;\n        if (!ownerAssignmentIsCurrent(jobId) || assignmentBusy ||\n            !available.some((installer) => installer.user_id === userId)) return;\n        assignmentBusy = true;\n        addButton.disabled = true;\n        select.disabled = true;\n        assignmentMessage.textContent = \"Assigning installer...\";\n        try {\n          const { data: added, error } = await client.from(\"job_assignments\")\n            .insert({ job_id: jobId, user_id: userId })\n            .select(\"id\").maybeSingle();\n          if (error) throw error;\n          if (!added) throw new Error(\"Assignment was not saved. Check permissions.\");\n          if (ownerAssignmentIsCurrent(jobId)) {\n            const refreshed = await loadOwnerAssignments(jobId);\n            if (refreshed && ownerAssignmentIsCurrent(jobId))\n              assignmentMessage.textContent = \"Installer assigned to \" + jobId + \".\";\n          }\n        } catch (error) {\n          if (ownerAssignmentIsCurrent(jobId))\n            assignmentMessage.textContent = \"Assign failed: \" + error.message;\n        } finally {\n          if (ownerAssignmentIsCurrent(jobId)) assignmentBusy = false;\n          select.disabled = false;\n          if (ownerAssignmentIsCurrent(jobId)) addButton.disabled = !select.value;\n        }\n      });\n      selectLabel.appendChild(select);\n      assignmentControls.append(selectLabel, addButton);\n    }\n    return true;\n  } catch (error) {\n    if (ownerAssignmentIsCurrent(jobId, requestId)) {\n      assignmentList.textContent = \"Unable to load installer assignments.\";\n      assignmentControls.replaceChildren();\n      assignmentMessage.textContent = error.message;\n    }\n    return false;\n  }\n}\nassignmentToggle.addEventListener(\"click\", async function () {\n  const jobId = assignmentJobId;\n  if (!jobId || dashboard.hidden || assignmentToggle.hidden ||\n      jobIdInput.value.trim() !== jobId) return;\n  assignmentPanel.hidden = !assignmentPanel.hidden;\n  assignmentToggle.setAttribute(\"aria-expanded\", String(!assignmentPanel.hidden));\n  if (assignmentPanel.hidden) assignmentRequestId++;\n  else await loadOwnerAssignments(jobId);\n});\n\nlet completionJobId = null;\nlet completionRequestId = 0;\nlet currentJobStatus = \"Approved\";\nlet currentCompletedAt = null;\n\nconst allowedJobStatuses = [\n  \"Approved\",\n  \"Final Measurement\",\n  \"Fabrication\",\n  \"Ready for Installation\",\n  \"Completed\"\n];\n\nfunction resetCompletion() {\n  completionRequestId++;\n  completionJobId = null;\n  currentJobStatus = \"Approved\";\n  currentCompletedAt = null;\n  completionButton.hidden = true;\n  completionButton.disabled = true;\n  completionButton.textContent = \"Job Status\";\n  completionButton.setAttribute(\"aria-expanded\", \"false\");\n  jobStatusPanel.hidden = true;\n  jobStatusSelect.disabled = false;\n  jobStatusSave.disabled = false;\n  jobStatusSelect.value = \"Approved\";\n  jobStatusCurrent.textContent = \"\";\n  completionMessage.hidden = true;\n  completionMessage.textContent = \"\";\n}\n\nfunction configureCompletion(jobId, jobStatus, completedAt) {\n  completionJobId = jobId;\n  currentCompletedAt = completedAt || null;\n  currentJobStatus = allowedJobStatuses.includes(jobStatus)\n    ? jobStatus\n    : (completedAt ? \"Completed\" : \"Approved\");\n\n  completionButton.hidden = false;\n  completionButton.disabled = false;\n  completionButton.textContent = \"Job Status \u2014 \" + currentJobStatus;\n  completionButton.setAttribute(\"aria-expanded\", \"false\");\n  jobStatusPanel.hidden = true;\n  completionMessage.hidden = false;\n  jobStatusSelect.value = currentJobStatus;\n  jobStatusCurrent.textContent = \"Current status: \" + currentJobStatus;\n\n  if (currentCompletedAt || currentJobStatus === \"Completed\") {\n    currentJobStatus = \"Completed\";\n    jobStatusSelect.value = \"Completed\";\n    jobStatusSelect.disabled = true;\n    jobStatusSave.disabled = true;\n    completionButton.textContent = \"Job Status \u2014 Completed\";\n    jobStatusCurrent.textContent = \"Current status: Completed\";\n    completionMessage.textContent =\n      \"This job is completed. Completion cannot be moved back to an earlier stage.\";\n  } else {\n    jobStatusSelect.disabled = false;\n    jobStatusSave.disabled = false;\n    completionMessage.textContent =\n      \"Owner \/ Project Manager only. Choose the job's current stage and press Save Status.\";\n  }\n}\n\ncompletionButton.addEventListener(\"click\", function () {\n  const jobId = completionJobId;\n  if (!client || dashboard.hidden || !jobId ||\n      jobIdInput.value.trim() !== jobId || completionButton.disabled) return;\n  jobStatusPanel.hidden = !jobStatusPanel.hidden;\n  completionButton.setAttribute(\"aria-expanded\", String(!jobStatusPanel.hidden));\n});\n\nasync function refreshVisibleJobStatus(jobId) {\n  const { data: updatedJob, error: jobError } = await client\n    .from(\"Jobs\")\n    .select(\"job_id, customer_name, customer_phone, job_address, job_status, completed_at\")\n    .eq(\"job_id\", jobId)\n    .maybeSingle();\n\n  if (jobError || !updatedJob) return null;\n\n  jobResult.textContent =\n    \"Job ID: \" + updatedJob.job_id + \"\\n\" +\n    \"Customer: \" + (updatedJob.customer_name || \"\") + \"\\n\" +\n    \"Phone: \" + (updatedJob.customer_phone || \"\") + \"\\n\" +\n    \"Address: \" + (updatedJob.job_address || \"\") + \"\\n\" +\n    \"Status: \" + (updatedJob.job_status || \"\");\n\n  return updatedJob;\n}\n\njobStatusSave.addEventListener(\"click\", async function () {\n  const jobId = completionJobId;\n  const nextStatus = jobStatusSelect.value;\n\n  if (!client || dashboard.hidden || !jobId ||\n      jobIdInput.value.trim() !== jobId || jobStatusSave.disabled) return;\n\n  if (!allowedJobStatuses.includes(nextStatus)) {\n    completionMessage.textContent = \"Select a valid job status.\";\n    return;\n  }\n\n  if (currentCompletedAt || currentJobStatus === \"Completed\") {\n    completionMessage.textContent =\n      \"This job is already completed and cannot be moved back.\";\n    jobStatusSelect.value = \"Completed\";\n    return;\n  }\n\n  if (nextStatus === currentJobStatus) {\n    completionMessage.textContent = \"Status is already \" + currentJobStatus + \".\";\n    return;\n  }\n\n  if (!window.confirm(\n    \"Change job \" + jobId + \" status from \" + currentJobStatus +\n    \" to \" + nextStatus + \"?\"\n  )) {\n    jobStatusSelect.value = currentJobStatus;\n    return;\n  }\n\n  const requestId = ++completionRequestId;\n  completionButton.disabled = true;\n  jobStatusSelect.disabled = true;\n  jobStatusSave.disabled = true;\n  completionMessage.textContent = \"Saving job status...\";\n\n  try {\n    if (nextStatus === \"Completed\") {\n      if (!window.confirm(\n        \"FINAL STEP: Mark job \" + jobId + \" as COMPLETED?\\n\\n\" +\n        \"This permanently records its completion time and sets the invoice due date \" +\n        \"to 30 calendar days after completion. Completion cannot be moved back.\"\n      )) {\n        jobStatusSelect.value = currentJobStatus;\n        return;\n      }\n\n      const { data: dueDate, error } = await client.rpc(\"gw_mark_job_completed\", {\n        p_job_id: jobId\n      });\n      if (error) throw error;\n\n      if (requestId !== completionRequestId || dashboard.hidden ||\n          jobIdInput.value.trim() !== jobId) return;\n\n      const updatedJob = await refreshVisibleJobStatus(jobId);\n      currentJobStatus = \"Completed\";\n      currentCompletedAt = updatedJob?.completed_at || \"completed\";\n      completionButton.textContent = \"Job Status \u2014 Completed\";\n      jobStatusCurrent.textContent = \"Current status: Completed\";\n      jobStatusSelect.value = \"Completed\";\n      jobStatusSelect.disabled = true;\n      jobStatusSave.disabled = true;\n      completionMessage.textContent =\n        \"Job completed. Invoice due date: \" + dueDate + \".\";\n\n      if (!accountingToggle.hidden) {\n        const refreshed = await loadAccounting(jobId);\n        if (!refreshed && requestId === completionRequestId)\n          completionMessage.textContent +=\n            \" Refresh the job to view its updated invoice.\";\n      }\n      return;\n    }\n\n    const { data: savedStatus, error } = await client.rpc(\"gw_set_job_status\", {\n      p_job_id: jobId,\n      p_status: nextStatus\n    });\n    if (error) throw error;\n\n    if (savedStatus !== nextStatus)\n      throw new Error(\"Saved status could not be verified.\");\n\n    if (requestId !== completionRequestId || dashboard.hidden ||\n        jobIdInput.value.trim() !== jobId) return;\n\n    await refreshVisibleJobStatus(jobId);\n    currentJobStatus = nextStatus;\n    completionButton.textContent = \"Job Status \u2014 \" + nextStatus;\n    jobStatusCurrent.textContent = \"Current status: \" + nextStatus;\n    jobStatusSelect.value = nextStatus;\n    completionMessage.textContent = \"Status saved: \" + nextStatus + \".\";\n  } catch (error) {\n    if (requestId !== completionRequestId || dashboard.hidden ||\n        jobIdInput.value.trim() !== jobId) return;\n    completionMessage.textContent =\n      \"Status change failed: \" + error.message +\n      \"\\nNo other Portal section was changed.\";\n    jobStatusSelect.value = currentJobStatus;\n  } finally {\n    if (requestId === completionRequestId && !dashboard.hidden &&\n        jobIdInput.value.trim() === jobId &&\n        currentJobStatus !== \"Completed\" && !currentCompletedAt) {\n      completionButton.disabled = false;\n      jobStatusSelect.disabled = false;\n      jobStatusSave.disabled = false;\n    } else if (currentJobStatus === \"Completed\" || currentCompletedAt) {\n      completionButton.disabled = false;\n    }\n  }\n});\n\nasync function refreshOwnerReviews(jobId, signedInUserId) {\n  allReviewsContent.replaceChildren();\n  allReviewsContent.textContent = \"Loading employee reviews...\";\n  const { data: reviews, error } = await client\n    .from(\"installer_reviews\")\n    .select(\"id, user_id, installer_notes, installer_reviewed, ready_for_install, reviewed_at\")\n    .eq(\"job_id\", jobId)\n    .order(\"id\", { ascending: true });\n  \/\/ Do not display results from a job that is no longer selected.\n  if (dashboard.hidden || jobIdInput.value.trim() !== jobId) return;\n  allReviewsContent.replaceChildren();\n  if (error) {\n    allReviewsContent.textContent = \"Unable to load employee reviews: \" + error.message;\n    return;\n  }\n  if (!reviews || reviews.length === 0) {\n    allReviewsContent.textContent = \"No installer reviews for this job yet.\";\n    return;\n  }\n  for (const review of reviews) {\n    const entry = document.createElement(\"div\");\n    entry.style.padding = \"10px 0\";\n    entry.style.borderBottom = \"1px solid #e3e8ed\";\n    const person = document.createElement(\"strong\");\n    person.textContent = \"User ID: \" + review.user_id +\n      (review.user_id === signedInUserId ? \" (you)\" : \"\");\n    const details = document.createElement(\"p\");\n    details.style.whiteSpace = \"pre-wrap\";\n    details.textContent =\n      \"Notes: \" + (review.installer_notes || \"(none)\") + \"\\n\" +\n      \"Reviewed: \" + (review.installer_reviewed ? \"Yes\" : \"No\") + \"\\n\" +\n      \"Ready for installation: \" + (review.ready_for_install ? \"Yes\" : \"No\") +\n      \"\\nReviewed at: \" + (review.reviewed_at || \"Not confirmed\");\n    entry.append(person, details);\n    allReviewsContent.appendChild(entry);\n  }\n}\n\nreviewButton.addEventListener(\"click\", function () {\n  reviewForm.hidden = !reviewForm.hidden;\n});\nconst saveReview =\n  document.getElementById(\"gw-save-review\");\n\nconst reviewMessage =\n  document.getElementById(\"gw-review-message\");\n\nsaveReview.addEventListener(\"click\", async function () {\n  const jobId = jobIdInput.value.trim();\n\n  if (!client || dashboard.hidden || !jobId) {\n    reviewMessage.textContent = \"Please search for a job first.\";\n    return;\n  }\n\n  saveReview.disabled = true;\n  reviewMessage.textContent = \"Saving...\";\n\n  try {\n    const { data: auth, error: authError } =\n      await client.auth.getUser();\n\n    if (authError || !auth.user) {\n      reviewMessage.textContent = \"Please log in again.\";\n      return;\n    }\n\n    const record = {\n      job_id: jobId,\n      user_id: auth.user.id,\n      installer_notes:\n        document.getElementById(\"gw-review-notes\").value.trim(),\n      installer_reviewed:\n        document.getElementById(\"gw-review-confirmed\").checked,\n      ready_for_install:\n        document.getElementById(\"gw-review-ready\").checked,\n      reviewed_at:\n        document.getElementById(\"gw-review-confirmed\").checked\n          ? new Date().toISOString()\n          : null\n    };\n\n    const { data: existing, error: lookupError } = await client\n      .from(\"installer_reviews\")\n      .select(\"id\")\n      .eq(\"job_id\", jobId)\n      .eq(\"user_id\", auth.user.id)\n      .limit(1)\n      .maybeSingle();\n\n    if (lookupError) throw lookupError;\n\n    const { error } = existing\n      ? await client\n          .from(\"installer_reviews\")\n          .update(record)\n          .eq(\"id\", existing.id)\n      : await client\n          .from(\"installer_reviews\")\n          .insert(record);\n\n    if (error) throw error;\n\n    reviewMessage.textContent =\n      \"Installer Review saved successfully!\";\n     if (!allReviewsPanel.hidden && jobIdInput.value.trim() === jobId) {\n       await refreshOwnerReviews(jobId, auth.user.id);\n     }\n  } catch (error) {\n    reviewMessage.textContent =\n      \"Save failed: \" + error.message;\n  } finally {\n    saveReview.disabled = false;\n  }\n});\nconst photoButton =\n  document.getElementById(\"gw-job-photos\");\n\nconst photoForm =\n  document.getElementById(\"gw-photo-form\");\n\nphotoButton.addEventListener(\"click\", function () {\n  photoForm.hidden = !photoForm.hidden;\n});\nconst uploadPhoto = document.getElementById(\"gw-upload-photo\");\nconst photoMessage = document.getElementById(\"gw-photo-message\");\n\n\/\/ V3: one visible photo picker plus a persistent per-job pending upload queue.\n\/\/ Some iPhone picker routes return only ONE photo, even with `multiple`.\n\/\/ Retain the File objects across repeated selections; upload the whole queue later.\nconst photoFileInput = document.getElementById(\"gw-photo-file\");\nconst originalPhotoLabel = photoForm.querySelector('label[for=\"gw-photo-file\"]');\n\/\/ Hide the old single-photo\/camera picker, which users could accidentally choose.\n\/\/ A visible multi-photo input below still offers Photo Library \/ Take Photo on iOS.\nphotoFileInput.hidden = true;\nphotoFileInput.removeAttribute(\"capture\");\nif (originalPhotoLabel) originalPhotoLabel.hidden = true;\nconst photoLibraryLabel = document.createElement(\"label\");\nphotoLibraryLabel.htmlFor = \"gw-photo-library-file\";\nphotoLibraryLabel.textContent = \"Choose Photos (Photo Library \/ Camera) \u2014 V3\";\nphotoLibraryLabel.style.cssText = \"display:block;font-weight:600;margin:12px 0 6px;\";\nconst photoLibraryInput = document.createElement(\"input\");\nphotoLibraryInput.id = \"gw-photo-library-file\";\nphotoLibraryInput.type = \"file\";\nphotoLibraryInput.accept = \"image\/*\";\nphotoLibraryInput.multiple = true;\n\/\/ Never use capture: it can send iPhone directly to the single-shot camera.\nphotoLibraryInput.removeAttribute(\"capture\");\nphotoLibraryInput.style.cssText = \"display:block;box-sizing:border-box;width:100%;max-width:100%;margin-bottom:6px;\";\nconst photoLibraryHelp = document.createElement(\"p\");\nphotoLibraryHelp.textContent =\n  \"Select several photos in Photo Library if available. If iPhone only allows one, choose one, \" +\n  \"tap Add More Photos and repeat. Then tap Upload All Photos once.\";\nphotoLibraryHelp.style.cssText = \"font-size:14px;margin:4px 0 6px;\";\nconst photoSelectionMessage = document.createElement(\"p\");\nphotoSelectionMessage.id = \"gw-photo-selection-message\";\nphotoSelectionMessage.setAttribute(\"role\", \"status\");\nphotoSelectionMessage.style.cssText = \"font-size:14px;margin:4px 0 10px;\";\nconst photoQueueList = document.createElement(\"div\");\nphotoQueueList.id = \"gw-photo-pending-list\";\nphotoQueueList.style.cssText = \"margin:6px 0 12px;overflow-wrap:anywhere;\";\nconst addMorePhotos = document.createElement(\"button\");\naddMorePhotos.id = \"gw-photo-add-more\";\naddMorePhotos.type = \"button\";\naddMorePhotos.textContent = \"Add More Photos\";\naddMorePhotos.style.cssText = \"display:block;width:100%;margin:6px 0 10px;\";\nconst clearPhotos = document.createElement(\"button\");\nclearPhotos.id = \"gw-photo-clear-selection\";\nclearPhotos.type = \"button\";\nclearPhotos.textContent = \"Clear Pending Photos\";\nclearPhotos.style.cssText = \"display:block;width:100%;margin:6px 0 12px;\";\n(originalPhotoLabel || photoFileInput).before(\n  photoLibraryLabel, photoLibraryInput, photoLibraryHelp,\n  photoSelectionMessage, photoQueueList, addMorePhotos, clearPhotos\n);\nuploadPhoto.textContent = \"Upload All Photos\";\nlet pendingPhotos = [];\nlet pendingPhotoJobId = null;\nlet photoUploadBusy = false;\nfunction photoIdentity(file) {\n  return [file.name, file.size, file.lastModified || 0].join(\"|\");\n}\nfunction renderPendingPhotos() {\n  const count = pendingPhotos.length;\n  photoSelectionMessage.textContent = count\n    ? count + \" photo(s) ready to upload.\"\n    : \"No photos selected yet.\";\n  photoQueueList.replaceChildren();\n  pendingPhotos.forEach(function (file, index) {\n    const row = document.createElement(\"div\");\n    row.style.cssText = \"display:flex;align-items:center;justify-content:space-between;gap:8px;margin:5px 0;\";\n    const name = document.createElement(\"span\");\n    name.textContent = (index + 1) + \". \" + file.name;\n    name.style.cssText = \"min-width:0;overflow-wrap:anywhere;\";\n    const remove = document.createElement(\"button\");\n    remove.type = \"button\";\n    remove.textContent = \"Remove\";\n    remove.setAttribute(\"aria-label\", \"Remove \" + file.name);\n    remove.disabled = photoUploadBusy;\n    remove.addEventListener(\"click\", function () {\n      if (photoUploadBusy) return;\n      pendingPhotos.splice(index, 1);\n      if (!pendingPhotos.length) pendingPhotoJobId = null;\n      renderPendingPhotos();\n    });\n    row.append(name, remove);\n    photoQueueList.appendChild(row);\n  });\n  clearPhotos.disabled = photoUploadBusy || !count;\n  uploadPhoto.disabled = photoUploadBusy || !count;\n}\nfunction clearPendingPhotos() {\n  pendingPhotos = [];\n  pendingPhotoJobId = null;\n  photoLibraryInput.value = \"\";\n  photoFileInput.value = \"\";\n  renderPendingPhotos();\n}\nfunction addChosenPhotos(input) {\n  if (photoUploadBusy) return;\n  const incoming = Array.from(input.files || []);\n  const activeJob = jobIdInput.value.trim();\n  if (incoming.length && pendingPhotos.length && pendingPhotoJobId !== activeJob) {\n    clearPendingPhotos();\n  }\n  if (incoming.length && !pendingPhotos.length) pendingPhotoJobId = activeJob;\n  const seen = new Set(pendingPhotos.map(photoIdentity));\n  let duplicates = 0;\n  for (const file of incoming) {\n    const key = photoIdentity(file);\n    if (seen.has(key)) { duplicates++; continue; }\n    pendingPhotos.push(file);\n    seen.add(key);\n  }\n  \/\/ Reset the picker so another opening never REPLACES earlier queued Files.\n  \/\/ This also allows selecting the same photo after it has been removed.\n  input.value = \"\";\n  renderPendingPhotos();\n  if (duplicates) photoMessage.textContent = duplicates + \" duplicate selection(s) skipped.\";\n}\nphotoLibraryInput.addEventListener(\"change\", function () {\n  addChosenPhotos(photoLibraryInput);\n});\n\/\/ Defensive compatibility with any programmatic use of the original input.\nphotoFileInput.addEventListener(\"change\", function () {\n  addChosenPhotos(photoFileInput);\n});\naddMorePhotos.addEventListener(\"click\", function () {\n  if (!photoUploadBusy) photoLibraryInput.click();\n});\nclearPhotos.addEventListener(\"click\", function () {\n  if (!photoUploadBusy) clearPendingPhotos();\n});\nrenderPendingPhotos();\n\nuploadPhoto.addEventListener(\"click\", async function () {\n  if (photoUploadBusy) return;\n  const jobId = jobIdInput.value.trim();\n  const files = pendingPhotos.slice();\n  const photoType = document.getElementById(\"gw-photo-type\").value;\n  const description = document.getElementById(\"gw-photo-description\").value.trim();\n\n  if (!client || dashboard.hidden || !jobId) {\n    photoMessage.textContent = \"Please search for a job first.\";\n    return;\n  }\n  if (!files.length) {\n    photoMessage.textContent = \"Please choose one or more photos.\";\n    return;\n  }\n  if (pendingPhotoJobId !== jobId) {\n    photoMessage.textContent = \"Job ID changed. Search for the job again and reselect photos.\";\n    return;\n  }\n  const mimeByExtension = {\n    jpg: \"image\/jpeg\", jpeg: \"image\/jpeg\", png: \"image\/png\",\n    webp: \"image\/webp\", heic: \"image\/heic\", heif: \"image\/heif\"\n  };\n  const prepared = [];\n  for (const file of files) {\n    const extension = (file.name.split(\".\").pop() || \"\").toLowerCase();\n    const expectedType = mimeByExtension[extension];\n    const contentType = file.type || expectedType;\n    if (!expectedType || contentType !== expectedType ||\n        file.size > 20 * 1024 * 1024 || file.size === 0) {\n      photoMessage.textContent = \"Unsupported photo or over 20 MB: \" + file.name +\n        \". Use JPG, PNG, WebP, HEIC or HEIF, up to 20 MB per photo.\";\n      return;\n    }\n    prepared.push({ file, extension, contentType });\n  }\n\n  photoUploadBusy = true;\n  uploadPhoto.disabled = true;\n  photoFileInput.disabled = true;\n  photoLibraryInput.disabled = true;\n  addMorePhotos.disabled = true;\n  clearPhotos.disabled = true;\n  renderPendingPhotos();\n  photoMessage.textContent = \"Checking job and employee access...\";\n  try {\n    const { data: auth, error: authError } = await client.auth.getUser();\n    if (authError || !auth.user) {\n      photoMessage.textContent = \"Please log in again.\";\n      return;\n    }\n    const { data: job, error: jobError } = await client\n      .from(\"Jobs\").select(\"job_id\").eq(\"job_id\", jobId).maybeSingle();\n    if (jobError || !job) {\n      photoMessage.textContent = \"Job not found. Search again.\";\n      return;\n    }\n    let savedCount = 0;\n    const failureDetails = [];\n    const successfulFiles = [];\n    for (const { file, extension, contentType } of prepared) {\n      photoMessage.textContent = \"Uploading photo \" +\n        (savedCount + failureDetails.length + 1) + \" of \" + prepared.length + \"...\";\n      try {\n        const path = auth.user.id + \"\/\" + jobId + \"\/\" +\n          crypto.randomUUID() + \".\" + extension;\n        const { error: uploadError } = await client.storage\n          .from(\"job-photos\")\n          .upload(path, file, { contentType, upsert: false });\n        if (uploadError) throw new Error(uploadError.message);\n        const { error: recordError } = await client\n          .from(\"job_photo\")\n          .insert({\n            job_id: jobId, photo_type: photoType, photo_url: path,\n            description, uploaded_by: auth.user.email,\n            taken_at: new Date().toISOString()\n          });\n        if (recordError) {\n          throw new Error(\"Photo stored but record failed: \" + recordError.message);\n        }\n        savedCount++;\n        successfulFiles.push(file);\n      } catch (error) {\n        failureDetails.push(file.name + \": \" + (error.message || \"Upload failed\"));\n      }\n    }\n    photoMessage.textContent = savedCount + \" of \" + prepared.length +\n      \" photo(s) uploaded successfully.\" +\n      (failureDetails.length ? \" Failed: \" + failureDetails.join(\"; \") : \"\");\n    \/\/ Remove only successfully uploaded files, so retry cannot re-upload them.\n    if (pendingPhotoJobId === jobId) {\n      const completed = new Set(successfulFiles);\n      pendingPhotos = pendingPhotos.filter(file => !completed.has(file));\n      if (!pendingPhotos.length) pendingPhotoJobId = null;\n    }\n    if (failureDetails.length) {\n      photoMessage.textContent += \" Check the gallery before retrying failed photos.\";\n    }\n  } catch (error) {\n    photoMessage.textContent =\n      \"Connection error: \" + (error.message || \"Please try again.\");\n  } finally {\n    photoUploadBusy = false;\n    photoFileInput.disabled = false;\n    photoLibraryInput.disabled = false;\n    addMorePhotos.disabled = false;\n    renderPendingPhotos();\n  }\n});\nconst viewPhotos = document.getElementById(\"gw-view-photos\");\nconst photoGallery = document.getElementById(\"gw-photo-gallery\");\n\n\/\/ Full-screen, keyboard-accessible photo viewer. It reuses the same signed URL\n\/\/ as the gallery and does not make the private Storage bucket public.\nconst photoZoom = document.createElement(\"div\");\nphotoZoom.id = \"gw-photo-zoom\";\nphotoZoom.setAttribute(\"role\", \"dialog\");\nphotoZoom.setAttribute(\"aria-modal\", \"true\");\nphotoZoom.setAttribute(\"aria-label\", \"Enlarged job photo\");\nphotoZoom.style.cssText =\n  \"display:none;position:fixed;inset:0;z-index:2147483647;\" +\n  \"background:rgba(0,0,0,.94);color:white;flex-direction:column;padding:12px;box-sizing:border-box;\";\nconst zoomToolbar = document.createElement(\"div\");\nzoomToolbar.style.cssText =\n  \"display:flex;align-items:center;justify-content:flex-end;gap:10px;flex-wrap:wrap;margin-bottom:8px;\";\nconst zoomSize = document.createElement(\"button\");\nzoomSize.type = \"button\";\nzoomSize.textContent = \"Zoom to original size\";\nconst zoomClose = document.createElement(\"button\");\nzoomClose.type = \"button\";\nzoomClose.textContent = \"Close \u00d7\";\nfor (const control of [zoomSize, zoomClose]) {\n  control.style.cssText = \"padding:10px 14px;border:1px solid #cbd5e1;\" +\n    \"border-radius:7px;background:#fff;color:#111;cursor:pointer;font-size:16px;\";\n}\nzoomToolbar.append(zoomSize, zoomClose);\nconst zoomScroll = document.createElement(\"div\");\nzoomScroll.style.cssText =\n  \"flex:1;min-height:0;overflow:auto;display:flex;align-items:center;justify-content:center;\";\nconst zoomImage = document.createElement(\"img\");\nzoomImage.alt = \"Enlarged job photo\";\nzoomImage.style.cssText = \"display:block;max-width:100%;max-height:100%;object-fit:contain;\";\nzoomScroll.appendChild(zoomImage);\nconst zoomCaption = document.createElement(\"p\");\nzoomCaption.style.cssText = \"text-align:center;margin:8px 0 0;overflow-wrap:anywhere;\";\nphotoZoom.append(zoomToolbar, zoomScroll, zoomCaption);\ndocument.body.appendChild(photoZoom);\nlet zoomOriginalSize = false;\nlet zoomReturnFocus = null;\nlet photoRequestId = 0;\nfunction closePhotoZoom() {\n  if (photoZoom.style.display === \"none\") return;\n  photoZoom.style.display = \"none\";\n  zoomImage.removeAttribute(\"src\");\n  zoomCaption.textContent = \"\";\n  if (zoomReturnFocus && zoomReturnFocus.isConnected) zoomReturnFocus.focus();\n  zoomReturnFocus = null;\n}\nfunction openPhotoZoom(url, label, trigger) {\n  if (!client || dashboard.hidden) return;\n  zoomReturnFocus = trigger;\n  zoomOriginalSize = false;\n  zoomImage.src = url;\n  zoomImage.alt = label || \"Job photo\";\n  zoomCaption.textContent = label || \"Job photo\";\n  zoomImage.style.maxWidth = \"100%\";\n  zoomImage.style.maxHeight = \"100%\";\n  zoomSize.textContent = \"Zoom to original size\";\n  photoZoom.style.display = \"flex\";\n  zoomClose.focus();\n}\nzoomClose.addEventListener(\"click\", closePhotoZoom);\nphotoZoom.addEventListener(\"click\", function (event) {\n  if (event.target === photoZoom) closePhotoZoom();\n});\nzoomSize.addEventListener(\"click\", function () {\n  zoomOriginalSize = !zoomOriginalSize;\n  zoomImage.style.maxWidth = zoomOriginalSize ? \"none\" : \"100%\";\n  zoomImage.style.maxHeight = zoomOriginalSize ? \"none\" : \"100%\";\n  zoomSize.textContent = zoomOriginalSize ? \"Fit to screen\" : \"Zoom to original size\";\n  zoomScroll.scrollTop = 0;\n  zoomScroll.scrollLeft = 0;\n});\ndocument.addEventListener(\"keydown\", function (event) {\n  if (event.key === \"Escape\" && photoZoom.style.display !== \"none\") {\n    event.preventDefault();\n    closePhotoZoom();\n  }\n});\nfunction resetPhotoView() {\n  photoRequestId++;\n  closePhotoZoom();\n  photoGallery.hidden = true;\n  photoGallery.replaceChildren();\n}\n\nviewPhotos.addEventListener(\"click\", async function () {\n  if (!photoGallery.hidden &&\n      photoGallery.childNodes.length > 0) {\n    resetPhotoView();\n    return;\n  }\n\n  const requestId = ++photoRequestId;\n  photoGallery.hidden = false;\n  const jobId = jobIdInput.value.trim();\n  photoGallery.replaceChildren();\n\n  if (!client || dashboard.hidden || !jobId) {\n    photoGallery.textContent = \"Please search for a job first.\";\n    return;\n  }\n\n  viewPhotos.disabled = true;\n  photoGallery.textContent = \"Loading photos...\";\n\n  try {\n    const { data: auth, error: authError } =\n      await client.auth.getUser();\n\n    if (authError || !auth.user) {\n      photoGallery.textContent = \"Please log in again.\";\n      return;\n    }\n\n    const { data: photos, error } = await client\n      .from(\"job_photo\")\n      .select(\"photo_url, photo_type, description\")\n      .eq(\"job_id\", jobId);\n\n    if (error) throw error;\n    if (requestId !== photoRequestId || dashboard.hidden || jobIdInput.value.trim() !== jobId) return;\n\n    photoGallery.replaceChildren();\n\n    if (!photos || photos.length === 0) {\n      photoGallery.textContent = \"No photos found.\";\n      return;\n    }\n\n    for (const photo of photos) {\n      const { data: signed, error: urlError } =\n        await client.storage\n          .from(\"job-photos\")\n          .createSignedUrl(photo.photo_url, 300);\n\n      if (requestId !== photoRequestId || dashboard.hidden || jobIdInput.value.trim() !== jobId) return;\n      if (urlError || !signed?.signedUrl) {\n        const warning = document.createElement(\"p\");\n        warning.textContent = \"Unable to open one photo.\";\n        photoGallery.appendChild(warning);\n        continue;\n      }\n\n      const heading = document.createElement(\"h4\");\n      heading.textContent = photo.photo_type || \"Job Photo\";\n\n      const enlarge = document.createElement(\"button\");\n      enlarge.type = \"button\";\n      enlarge.setAttribute(\"aria-label\", \"Enlarge \" + (photo.description || photo.photo_type || \"job photo\"));\n      enlarge.title = \"Click to enlarge photo\";\n      enlarge.style.cssText =\n        \"display:block;max-width:400px;width:100%;padding:0;border:0;background:none;cursor:zoom-in;\";\n      const image = document.createElement(\"img\");\n      image.src = signed.signedUrl;\n      image.alt = photo.description || \"Job photo\";\n      image.style.width = \"100%\";\n      image.style.maxWidth = \"400px\";\n      image.style.height = \"auto\";\n      enlarge.appendChild(image);\n      enlarge.addEventListener(\"click\", function () {\n        if (jobIdInput.value.trim() !== jobId || dashboard.hidden || !client) return;\n        openPhotoZoom(signed.signedUrl, photo.description || photo.photo_type || \"Job photo\", enlarge);\n      });\n\n      const description = document.createElement(\"p\");\n      description.textContent = photo.description || \"\";\n\n      photoGallery.append(heading, enlarge, description);\n    }\n  } catch (error) {\n    if (requestId === photoRequestId) photoGallery.textContent =\n      \"Unable to load photos: \" + error.message;\n  } finally {\n    viewPhotos.disabled = false;\n  }\n});\n\/\/ CUSTOMER PHOTO APPROVALS (OWNER \/ OFFICE \/ PROJECT MANAGER).\n\/\/ DB RLS limits reading approval flags; the SECURITY DEFINER RPC alone changes\n\/\/ visibility. Do not put secret keys or customer lookup codes in this browser.\nconst photoApprovalToggle = document.createElement(\"button\");\nphotoApprovalToggle.type = \"button\";\nphotoApprovalToggle.id = \"gw-customer-photo-toggle\";\nphotoApprovalToggle.textContent = \"Customer Photo Approvals (Owner \/ Office \/ PM)\";\nphotoApprovalToggle.hidden = true;\nphotoApprovalToggle.setAttribute(\"aria-expanded\", \"false\");\nphotoApprovalToggle.style.cssText = \"width:100%;margin:12px 0 8px;\";\nconst photoApprovalPanel = document.createElement(\"section\");\nphotoApprovalPanel.id = \"gw-customer-photo-panel\";\nphotoApprovalPanel.hidden = true;\nphotoApprovalPanel.style.cssText =\n  \"box-sizing:border-box;width:100%;padding:16px;margin:8px 0 20px;\" +\n  \"border:1px solid #cbd5e1;border-radius:8px;overflow-wrap:anywhere;\";\nconst photoApprovalHeading = document.createElement(\"h3\");\nphotoApprovalHeading.textContent = \"Approve Before \/ After Photos for Customer\";\nconst photoApprovalHelp = document.createElement(\"p\");\nphotoApprovalHelp.textContent =\n  \"Only individually approved Before \/ After photos appear in the customer lookup. \" +\n  \"Evidence photos cannot be approved. Revoking prevents new customer lookups, \" +\n  \"but does not recall downloads or previously issued short-lived links.\";\nconst photoApprovalJobLabel = document.createElement(\"p\");\nconst photoApprovalRefresh = document.createElement(\"button\");\nphotoApprovalRefresh.type = \"button\";\nphotoApprovalRefresh.id = \"gw-customer-photo-refresh\";\nphotoApprovalRefresh.textContent = \"Refresh Photo Approvals\";\nphotoApprovalRefresh.style.cssText = \"width:100%;margin:8px 0;\";\nconst photoApprovalMessage = document.createElement(\"p\");\nphotoApprovalMessage.id = \"gw-customer-photo-message\";\nphotoApprovalMessage.setAttribute(\"role\", \"status\");\nconst photoApprovalList = document.createElement(\"div\");\nphotoApprovalList.id = \"gw-customer-photo-list\";\nconst photoApprovalClose = document.createElement(\"button\");\nphotoApprovalClose.type = \"button\";\nphotoApprovalClose.textContent = \"Close Photo Approvals\";\nphotoApprovalClose.style.cssText = \"width:100%;margin-top:12px;\";\nphotoApprovalPanel.append(\n  photoApprovalHeading, photoApprovalHelp, photoApprovalJobLabel,\n  photoApprovalRefresh, photoApprovalMessage, photoApprovalList, photoApprovalClose\n);\n\/\/ Customer Photo Approvals belong inside Job Photos.\n\/\/ Append them to the existing Job Photos form so they open\/close with that section.\nphotoForm.appendChild(photoApprovalToggle);\nphotoForm.appendChild(photoApprovalPanel);\n\nlet photoApprovalJobId = null;\nlet photoApprovalUserId = null;\nlet photoApprovalGeneration = 0;\nlet photoApprovalRequestId = 0;\nlet photoApprovalBusy = false;\nfunction resetPhotoApprovalView() {\n  photoApprovalGeneration++;\n  photoApprovalRequestId++;\n  photoApprovalJobId = null;\n  photoApprovalUserId = null;\n  photoApprovalBusy = false;\n  photoApprovalToggle.hidden = true;\n  photoApprovalToggle.setAttribute(\"aria-expanded\", \"false\");\n  photoApprovalPanel.hidden = true;\n  photoApprovalJobLabel.textContent = \"\";\n  photoApprovalMessage.textContent = \"\";\n  photoApprovalList.replaceChildren();\n  photoApprovalToggle.disabled = false;\n  photoApprovalRefresh.disabled = false;\n  photoApprovalClose.disabled = false;\n}\nfunction photoApprovalIsCurrent(jobId, userId, generation) {\n  return !!client && !logout.hidden && !jobSearch.hidden && !dashboard.hidden &&\n    !photoApprovalToggle.hidden && !photoApprovalPanel.hidden &&\n    photoApprovalJobId === jobId && photoApprovalUserId === userId &&\n    jobIdInput.value.trim() === jobId && photoApprovalGeneration === generation;\n}\nfunction configurePhotoApprovalView(jobId, role, userId) {\n  resetPhotoApprovalView();\n  if (![\"owner\", \"office\", \"project_manager\"].includes(role) ||\n      !jobId || !userId || dashboard.hidden || logout.hidden || jobSearch.hidden ||\n      jobIdInput.value.trim() !== jobId) return;\n  photoApprovalJobId = jobId;\n  photoApprovalUserId = userId;\n  photoApprovalJobLabel.textContent = \"Selected Job ID: \" + jobId;\n  photoApprovalToggle.hidden = false;\n}\nfunction photoApprovalStoragePath(path, jobId) {\n  if (typeof path !== \"string\") return false;\n  const segments = path.split(\"\/\");\n  return segments.length === 3 &&\n    \/^[0-9a-fA-F-]{36}$\/.test(segments[0]) &&\n    segments[1] === jobId && segments[2].length > 0;\n}\nasync function loadPhotoApprovals() {\n  const jobId = photoApprovalJobId;\n  const userId = photoApprovalUserId;\n  const generation = photoApprovalGeneration;\n  if (!photoApprovalIsCurrent(jobId, userId, generation) || photoApprovalBusy) return;\n  const requestId = ++photoApprovalRequestId;\n  photoApprovalRefresh.disabled = true;\n  photoApprovalList.replaceChildren();\n  photoApprovalMessage.textContent = \"Loading Before \/ After photos and approval status...\";\n  try {\n    const { data: auth, error: authError } = await client.auth.getUser();\n    if (authError || !auth.user || auth.user.id !== userId)\n      throw new Error(\"Please log in again.\");\n    const { data: active, error: activeError } = await client.rpc(\"gw_employee_is_active\");\n    if (activeError || active !== true)\n      throw new Error(\"Unable to verify active employee access.\");\n    const { data: profile, error: profileError } = await client.from(\"employee_profiles\")\n      .select(\"role\").eq(\"user_id\", userId).maybeSingle();\n    if (profileError || !profile ||\n        ![\"owner\", \"office\", \"project_manager\"].includes(profile.role))\n      throw new Error(\"Photo approval access is unavailable for this account.\");\n    if (!photoApprovalIsCurrent(jobId, userId, generation) ||\n        requestId !== photoApprovalRequestId) return;\n\n    \/\/ Match the customer endpoint's most recent 100-photo window.\n    const { data: photos, error: photoError } = await client.from(\"job_photo\")\n      .select(\"id,photo_type,photo_url,description,is_evidence\")\n      .eq(\"job_id\", jobId).in(\"photo_type\", [\"Before\", \"After\"])\n      .order(\"id\", { ascending: false }).limit(100);\n    if (photoError) throw photoError;\n    if (!photoApprovalIsCurrent(jobId, userId, generation) ||\n        requestId !== photoApprovalRequestId) return;\n\n    const photoIds = (photos || []).filter((p) => Number.isSafeInteger(p.id))\n      .map((p) => p.id);\n    let approvals = [];\n    if (photoIds.length) {\n      \/\/ Column-level SELECT grant + manager-only RLS is required. Fail closed if absent.\n      const { data, error } = await client.from(\"job_photo_customer_approvals\")\n        .select(\"photo_id,customer_visible\").in(\"photo_id\", photoIds);\n      if (error) throw new Error(\"Cannot read photo approval status. Ask the office to check database permissions.\");\n      approvals = data || [];\n    }\n    if (!photoApprovalIsCurrent(jobId, userId, generation) ||\n        requestId !== photoApprovalRequestId) return;\n    photoApprovalList.replaceChildren();\n    if (!photos || !photos.length) {\n      photoApprovalMessage.textContent = \"No Before \/ After photos found for this job.\";\n      return;\n    }\n    const approvedIds = new Set(approvals.filter((a) => a.customer_visible === true)\n      .map((a) => a.photo_id));\n    photoApprovalMessage.textContent =\n      \"Showing up to the most recent 100 Before \/ After photos. Approval is per photo.\";\n    for (const photo of photos) {\n      if (!photoApprovalIsCurrent(jobId, userId, generation) ||\n          requestId !== photoApprovalRequestId) return;\n      if (!Number.isSafeInteger(photo.id)) continue;\n      const approved = approvedIds.has(photo.id);\n      const eligible = photo.is_evidence !== true &&\n        photoApprovalStoragePath(photo.photo_url, jobId);\n      const item = document.createElement(\"section\");\n      item.style.cssText =\n        \"padding:12px 0;border-bottom:1px solid #cbd5e1;overflow-wrap:anywhere;\";\n      const heading = document.createElement(\"h4\");\n      heading.textContent = \"Photo #\" + photo.id + \" \u2014 \" + photo.photo_type;\n      const status = document.createElement(\"p\");\n      status.textContent = approved ? \"Customer visibility: APPROVED\" :\n        \"Customer visibility: Not shared\";\n      const caption = document.createElement(\"p\");\n      caption.textContent = photo.description || \"No description\";\n      item.append(heading, status, caption);\n      if (!eligible) {\n        const warning = document.createElement(\"p\");\n        warning.textContent = photo.is_evidence === true\n          ? \"Evidence photo \u2014 cannot approve for customers.\"\n          : \"Photo storage path unavailable \u2014 cannot approve.\";\n        item.appendChild(warning);\n      } else {\n        const { data: signed, error: signedError } = await client.storage\n          .from(\"job-photos\").createSignedUrl(photo.photo_url, 300);\n        if (!photoApprovalIsCurrent(jobId, userId, generation) ||\n            requestId !== photoApprovalRequestId) return;\n        if (!signedError && signed?.signedUrl) {\n          const preview = document.createElement(\"img\");\n          preview.src = signed.signedUrl;\n          preview.alt = photo.photo_type + \" photo \" + photo.id;\n          preview.loading = \"lazy\";\n          preview.style.cssText =\n            \"display:block;width:100%;max-width:300px;height:auto;margin:8px 0;\";\n          item.appendChild(preview);\n        } else {\n          const warning = document.createElement(\"p\");\n          warning.textContent = \"Preview unavailable. Approval is disabled until the photo can be opened.\";\n          item.appendChild(warning);\n        }\n        \/\/ If a preview cannot be signed, still allow revoking an existing approval.\n        if (!approved && (signedError || !signed?.signedUrl)) {\n          photoApprovalList.appendChild(item);\n          continue;\n        }\n      }\n      if (eligible || approved) {\n        const changeButton = document.createElement(\"button\");\n        changeButton.type = \"button\";\n        changeButton.textContent = approved ? \"Revoke Customer Access\" :\n          \"Approve for Customer\";\n        changeButton.style.cssText = \"width:100%;margin:8px 0;\";\n        changeButton.addEventListener(\"click\", async function () {\n          if (!photoApprovalIsCurrent(jobId, userId, generation) || photoApprovalBusy) return;\n          const nextVisibility = !approved;\n          const action = nextVisibility ? \"APPROVE\" : \"REVOKE\";\n          if (!window.confirm(action + \" customer visibility for photo #\" + photo.id +\n              \" (\" + photo.photo_type + \") on \" + jobId + \"?\" +\n              (nextVisibility ? \"\\n\\nCustomers with this job's valid private code will be able to see it.\" :\n                \"\\n\\nPreviously downloaded images or unexpired links cannot be recalled.\"))) return;\n          photoApprovalBusy = true;\n          photoApprovalRefresh.disabled = true;\n          photoApprovalClose.disabled = true;\n          photoApprovalList.querySelectorAll(\"button\").forEach((b) => { b.disabled = true; });\n          photoApprovalMessage.textContent = \"Saving photo visibility...\";\n          let rpcConfirmed = false;\n          try {\n            const { data: currentAuth, error: currentAuthError } = await client.auth.getUser();\n            if (currentAuthError || !currentAuth.user || currentAuth.user.id !== userId ||\n                !photoApprovalIsCurrent(jobId, userId, generation))\n              throw new Error(\"Employee session changed.\");\n            const { data: activeNow, error: activeNowError } =\n              await client.rpc(\"gw_employee_is_active\");\n            if (activeNowError || activeNow !== true ||\n                !photoApprovalIsCurrent(jobId, userId, generation))\n              throw new Error(\"Employee access changed.\");\n            const { data: saved, error: saveError } = await client.rpc(\n              \"gw_set_photo_customer_visibility\",\n              { p_photo_id: photo.id, p_visible: nextVisibility }\n            );\n            if (saveError || saved !== true)\n              throw new Error(\"Could not confirm the visibility change.\");\n            rpcConfirmed = true;\n          } catch (_error) {\n            if (photoApprovalIsCurrent(jobId, userId, generation)) {\n              photoApprovalMessage.textContent =\n                \"Unable to confirm photo visibility. Refresh to check current status.\";\n              \/\/ Clear stale buttons and statuses rather than allowing blind retries.\n              photoApprovalList.replaceChildren();\n            }\n          } finally {\n            if (photoApprovalGeneration === generation) {\n              photoApprovalBusy = false;\n              photoApprovalClose.disabled = false;\n              photoApprovalRefresh.disabled = false;\n            }\n          }\n          if (rpcConfirmed && photoApprovalIsCurrent(jobId, userId, generation)) {\n            \/\/ Re-read flags from the database; no unverified 'success' message.\n            await loadPhotoApprovals();\n          }\n        });\n        item.appendChild(changeButton);\n      }\n      photoApprovalList.appendChild(item);\n    }\n  } catch (error) {\n    if (photoApprovalIsCurrent(jobId, userId, generation) &&\n        requestId === photoApprovalRequestId) {\n      photoApprovalList.replaceChildren();\n      photoApprovalMessage.textContent = error.message ||\n        \"Unable to load photo approval status. No changes were made.\";\n    }\n  } finally {\n    if (photoApprovalGeneration === generation &&\n        requestId === photoApprovalRequestId) photoApprovalRefresh.disabled = false;\n  }\n}\nphotoApprovalToggle.addEventListener(\"click\", function () {\n  if (!client || photoApprovalToggle.hidden || photoApprovalBusy ||\n      dashboard.hidden || logout.hidden ||\n      jobIdInput.value.trim() !== photoApprovalJobId) return;\n  photoApprovalPanel.hidden = !photoApprovalPanel.hidden;\n  photoApprovalToggle.setAttribute(\"aria-expanded\", String(!photoApprovalPanel.hidden));\n  photoApprovalRequestId++;\n  if (photoApprovalPanel.hidden) {\n    photoApprovalList.replaceChildren();\n    photoApprovalMessage.textContent = \"\";\n  } else {\n    void loadPhotoApprovals();\n  }\n});\nphotoApprovalRefresh.addEventListener(\"click\", function () {\n  if (!photoApprovalBusy) void loadPhotoApprovals();\n});\nphotoApprovalClose.addEventListener(\"click\", function () {\n  if (photoApprovalBusy) return;\n  photoApprovalPanel.hidden = true;\n  photoApprovalToggle.setAttribute(\"aria-expanded\", \"false\");\n  photoApprovalRequestId++;\n  photoApprovalList.replaceChildren();\n  photoApprovalMessage.textContent = \"\";\n});\n\n\/\/ JOB DRAWINGS: upload existing PDF\/images and list drawings for an authorized job.\n\/\/ The job-drawings Storage bucket and public.job_drawings RLS are the actual access\n\/\/ boundaries. UI visibility is not authorization. No overwrite or delete is provided.\nconst drawingToggle = document.createElement(\"button\");\ndrawingToggle.id = \"gw-drawing-toggle\";\ndrawingToggle.type = \"button\";\ndrawingToggle.textContent = \"Drawing \/ Cutting Plans\";\ndrawingToggle.hidden = true;\ndrawingToggle.setAttribute(\"aria-expanded\", \"false\");\ndrawingToggle.style.cssText = \"width:100%;margin:14px 0 8px;\";\n\nconst drawingPanel = document.createElement(\"section\");\ndrawingPanel.id = \"gw-drawing-panel\";\ndrawingPanel.hidden = true;\ndrawingPanel.style.cssText =\n  \"box-sizing:border-box;width:100%;padding:16px;margin:8px 0 20px;\" +\n  \"border:1px solid #cbd5e1;border-radius:10px;background:#fff;overflow-wrap:anywhere;\";\nconst drawingHeading = document.createElement(\"h3\");\ndrawingHeading.textContent = \"Job Drawings \/ Cutting Plans\";\nconst drawingHelp = document.createElement(\"p\");\ndrawingHelp.textContent =\n  \"Upload a prepared PDF, JPG, PNG, WebP or HEIC for this job (maximum 50 MB). \" +\n  \"Existing drawings remain saved; this form does not replace or delete them.\";\nconst drawingFileLabel = document.createElement(\"label\");\ndrawingFileLabel.htmlFor = \"gw-drawing-file\";\ndrawingFileLabel.textContent = \"Choose Drawing (PDF \/ Image)\";\ndrawingFileLabel.style.cssText = \"display:block;margin:16px 0 6px;font-weight:600;\";\nconst drawingFile = document.createElement(\"input\");\ndrawingFile.id = \"gw-drawing-file\";\ndrawingFile.type = \"file\";\ndrawingFile.accept = \"application\/pdf,image\/jpeg,image\/png,image\/webp,image\/heic\";\ndrawingFile.style.cssText = \"box-sizing:border-box;width:100%;max-width:100%;\";\nconst drawingUpload = document.createElement(\"button\");\ndrawingUpload.id = \"gw-upload-drawing\";\ndrawingUpload.type = \"button\";\ndrawingUpload.textContent = \"Upload Drawing\";\ndrawingUpload.style.width = \"100%\";\nconst drawingUploadMessage = document.createElement(\"p\");\ndrawingUploadMessage.id = \"gw-drawing-upload-message\";\ndrawingUploadMessage.setAttribute(\"role\", \"status\");\nconst drawingListHeading = document.createElement(\"h4\");\ndrawingListHeading.textContent = \"Drawings for This Job\";\nconst drawingRefresh = document.createElement(\"button\");\ndrawingRefresh.id = \"gw-view-drawings\";\ndrawingRefresh.type = \"button\";\ndrawingRefresh.textContent = \"Refresh Drawings\";\ndrawingRefresh.style.width = \"100%\";\nconst drawingList = document.createElement(\"div\");\ndrawingList.id = \"gw-drawing-list\";\ndrawingList.setAttribute(\"aria-live\", \"polite\");\ndrawingPanel.append(\n  drawingHeading, drawingHelp, drawingFileLabel, drawingFile,\n  drawingUpload, drawingUploadMessage, drawingListHeading,\n  drawingRefresh, drawingList\n);\n\/\/ Drawing \/ Cutting Plans belongs inside Final Measurement.\nmeasurementDashboard.append(drawingToggle, drawingPanel);\n\nlet drawingJobId = null;\nlet drawingRequestId = 0;\nlet drawingUploadBusy = false;\nconst drawingMimeExtensions = Object.freeze({\n  \"application\/pdf\": \"pdf\",\n  \"image\/jpeg\": \"jpg\",\n  \"image\/png\": \"png\",\n  \"image\/webp\": \"webp\",\n  \"image\/heic\": \"heic\"\n});\n\nfunction resetDrawingView() {\n  drawingRequestId++;\n  drawingJobId = null;\n  drawingUploadBusy = false;\n  drawingToggle.hidden = true;\n  drawingToggle.disabled = false;\n  drawingToggle.setAttribute(\"aria-expanded\", \"false\");\n  drawingPanel.hidden = true;\n  drawingFile.value = \"\";\n  drawingUpload.disabled = false;\n  drawingRefresh.disabled = false;\n  drawingUploadMessage.textContent = \"\";\n  drawingList.replaceChildren();\n}\n\nfunction drawingIsCurrent(jobId, requestId) {\n  return !!client && !logout.hidden && !jobSearch.hidden && !dashboard.hidden &&\n    !drawingToggle.hidden && !drawingPanel.hidden &&\n    drawingJobId === jobId && jobIdInput.value.trim() === jobId &&\n    (requestId === undefined || requestId === drawingRequestId);\n}\n\nfunction configureDrawingView(jobId, role) {\n  resetDrawingView();\n  if (!\/^(owner|office|project_manager|installer|fabricator)$\/.test(role || \"\") ||\n      !jobId || dashboard.hidden || logout.hidden ||\n      jobIdInput.value.trim() !== jobId) return;\n  drawingJobId = jobId;\n  drawingToggle.hidden = false;\n}\n\nasync function loadDrawingList() {\n  const jobId = drawingJobId;\n  if (!drawingIsCurrent(jobId)) return;\n  const requestId = ++drawingRequestId;\n  drawingRefresh.disabled = true;\n  drawingList.textContent = \"Loading drawings...\";\n  try {\n    const { data: auth, error: authError } = await client.auth.getUser();\n    if (authError || !auth.user) throw new Error(\"Please log in again.\");\n    const { data: isActive, error: accessError } =\n      await client.rpc(\"gw_employee_is_active\");\n    if (accessError || isActive !== true)\n      throw new Error(\"Unable to verify active Portal access.\");\n    const { data: drawings, error } = await client.from(\"job_drawings\")\n      .select(\"id, job_id, storage_path, file_name, mime_type, created_at\")\n      .eq(\"job_id\", jobId)\n      .order(\"created_at\", { ascending: false })\n      .order(\"id\", { ascending: false });\n    if (error) throw error;\n    if (!drawingIsCurrent(jobId, requestId)) return;\n    drawingList.replaceChildren();\n    if (!drawings || drawings.length === 0) {\n      drawingList.textContent = \"No drawings uploaded for this job yet.\";\n      return;\n    }\n    for (const drawing of drawings) {\n      const row = document.createElement(\"div\");\n      row.style.cssText = \"padding:12px 0;border-bottom:1px solid #e2e8f0;\";\n\n      const title = document.createElement(\"strong\");\n      title.textContent = drawing.file_name || \"Drawing\";\n\n      const details = document.createElement(\"p\");\n      details.textContent = drawing.created_at\n        ? \"Uploaded: \" + new Date(drawing.created_at).toLocaleString()\n        : \"Drawing\";\n      details.style.cssText = \"margin:6px 0;overflow-wrap:anywhere;\";\n\n      const viewer = document.createElement(\"div\");\n      viewer.style.cssText = \"margin-top:10px;\";\n\n      const rowMessage = document.createElement(\"p\");\n      rowMessage.setAttribute(\"role\", \"status\");\n      rowMessage.textContent = \"Opening drawing...\";\n      viewer.appendChild(rowMessage);\n\n      row.append(title, details, viewer);\n      drawingList.appendChild(row);\n\n      try {\n        const { data: signed, error: urlError } = await client.storage\n          .from(\"job-drawings\")\n          .createSignedUrl(drawing.storage_path, 300);\n\n        if (urlError || !signed?.signedUrl)\n          throw urlError || new Error(\"Unable to open drawing.\");\n\n        if (!drawingIsCurrent(jobId, requestId)) return;\n\n        viewer.replaceChildren();\n\n        const mime = (drawing.mime_type || \"\").toLowerCase();\n        const isPdf = mime === \"application\/pdf\";\n        const canPreviewImage =\n          mime === \"image\/jpeg\" ||\n          mime === \"image\/png\" ||\n          mime === \"image\/webp\";\n\n        if (canPreviewImage) {\n          const imageButton = document.createElement(\"button\");\n          imageButton.type = \"button\";\n          imageButton.style.cssText =\n            \"display:block;width:100%;padding:0;border:1px solid #cbd5e1;\" +\n            \"border-radius:8px;background:#fff;overflow:hidden;cursor:pointer;\";\n\n          const preview = document.createElement(\"img\");\n          preview.src = signed.signedUrl;\n          preview.alt = drawing.file_name || \"Job drawing\";\n          preview.loading = \"lazy\";\n          preview.style.cssText =\n            \"display:block;width:100%;max-height:420px;object-fit:contain;background:#f8fafc;\";\n\n          imageButton.appendChild(preview);\n          imageButton.addEventListener(\"click\", function () {\n            if (!drawingIsCurrent(jobId, requestId)) return;\n            openPhotoZoom(\n              signed.signedUrl,\n              drawing.file_name || \"Job drawing\",\n              imageButton\n            );\n          });\n\n          const hint = document.createElement(\"p\");\n          hint.textContent = \"Tap drawing to enlarge\";\n          hint.style.cssText = \"margin:6px 0 0;font-size:14px;\";\n\n          viewer.append(imageButton, hint);\n        } else {\n          const link = document.createElement(\"a\");\n          link.href = signed.signedUrl;\n          link.target = \"_blank\";\n          link.rel = \"noopener noreferrer\";\n          link.textContent = isPdf ? \"Open PDF\" : \"Open Drawing\";\n          link.style.cssText =\n            \"display:block;box-sizing:border-box;width:100%;padding:12px 14px;\" +\n            \"border-radius:7px;background:#1f6fb2;color:#fff;text-align:center;\" +\n            \"text-decoration:none;font-weight:600;\";\n          viewer.appendChild(link);\n        }\n      } catch (error) {\n        if (drawingIsCurrent(jobId, requestId)) {\n          viewer.replaceChildren();\n          const warning = document.createElement(\"p\");\n          warning.textContent =\n            \"Unable to open drawing: \" +\n            (drawing.file_name || \"Drawing\") +\n            \" \u2014 \" + error.message;\n          warning.style.cssText =\n            \"padding:10px;border:1px solid #f1b9b9;border-radius:6px;overflow-wrap:anywhere;\";\n          viewer.appendChild(warning);\n          console.error(\"GW Portal drawing signed URL failed\", {\n            jobId: jobId,\n            storage_path: drawing.storage_path,\n            error: error\n          });\n        }\n      }\n    }\n  } catch (error) {\n    if (drawingIsCurrent(jobId, requestId))\n      drawingList.textContent = \"Unable to load drawings: \" + error.message;\n  } finally {\n    if (drawingJobId === jobId && requestId === drawingRequestId &&\n        !drawingToggle.hidden && !logout.hidden) drawingRefresh.disabled = false;\n  }\n}\n\ndrawingToggle.addEventListener(\"click\", function () {\n  if (!client || drawingToggle.hidden || drawingToggle.disabled ||\n      dashboard.hidden || logout.hidden || !drawingJobId ||\n      jobIdInput.value.trim() !== drawingJobId) return;\n  drawingPanel.hidden = !drawingPanel.hidden;\n  drawingToggle.setAttribute(\"aria-expanded\", String(!drawingPanel.hidden));\n  if (drawingPanel.hidden) {\n    drawingRequestId++;\n    drawingList.replaceChildren(); \/\/ discard expiring signed links\n    drawingRefresh.disabled = false;\n  } else {\n    void loadDrawingList();\n  }\n});\n\ndrawingRefresh.addEventListener(\"click\", function () {\n  if (!drawingUploadBusy) void loadDrawingList();\n});\n\ndrawingUpload.addEventListener(\"click\", async function () {\n  const jobId = drawingJobId;\n  if (!drawingIsCurrent(jobId) || drawingUploadBusy) return;\n  const file = drawingFile.files && drawingFile.files[0];\n  if (!file) {\n    drawingUploadMessage.textContent = \"Choose a PDF or image first.\";\n    return;\n  }\n  const extension = drawingMimeExtensions[file.type];\n  if (!extension || file.size === 0 || file.size > 50 * 1024 * 1024 ||\n      file.name.length > 180) {\n    drawingUploadMessage.textContent =\n      \"Choose a nonempty PDF\/JPG\/PNG\/WebP\/HEIC, at most 50 MB, with a filename under 181 characters.\";\n    return;\n  }\n  if (!window.confirm(\"Upload drawing '\" + file.name + \"' to job \" + jobId +\n      \"? Existing drawings will not be replaced.\")) return;\n  drawingUploadBusy = true;\n  drawingUpload.disabled = true;\n  drawingToggle.disabled = true;\n  drawingUploadMessage.textContent = \"Checking permissions...\";\n  try {\n    const requestId = drawingRequestId;\n    const { data: auth, error: authError } = await client.auth.getUser();\n    if (authError || !auth.user) throw new Error(\"Please log in again.\");\n    const { data: isActive, error: accessError } =\n      await client.rpc(\"gw_employee_is_active\");\n    if (accessError || isActive !== true)\n      throw new Error(\"Unable to verify active Portal access.\");\n    if (!drawingIsCurrent(jobId, requestId)) return;\n    const storagePath = auth.user.id + \"\/\" + jobId + \"\/\" +\n      crypto.randomUUID() + \".\" + extension;\n    drawingUploadMessage.textContent = \"Uploading drawing...\";\n    const { error: uploadError } = await client.storage.from(\"job-drawings\")\n      .upload(storagePath, file, { contentType: file.type, upsert: false });\n    if (uploadError) throw uploadError;\n    \/\/ No Storage DELETE permission: if metadata fails, report the orphaned path\n    \/\/ to the office instead of falsely claiming upload success or retrying.\n    const { data: saved, error: recordError } = await client.from(\"job_drawings\")\n      .insert({\n        job_id: jobId,\n        storage_path: storagePath,\n        file_name: file.name,\n        mime_type: file.type,\n        uploaded_by: auth.user.id\n      }).select(\"id, job_id\").maybeSingle();\n    if (recordError || !saved || saved.job_id !== jobId) {\n      if (drawingIsCurrent(jobId))\n        drawingUploadMessage.textContent =\n          \"File stored but its drawing record could not be saved. \" +\n          \"Contact the office; do not upload this file again. \" +\n          \"Storage path: \" + storagePath;\n      return;\n    }\n    if (!drawingIsCurrent(jobId)) return;\n    drawingFile.value = \"\";\n    drawingUploadMessage.textContent = \"Drawing uploaded and recorded successfully.\";\n    await loadDrawingList();\n  } catch (error) {\n    if (drawingIsCurrent(jobId))\n      drawingUploadMessage.textContent =\n        \"Upload failed or could not be confirmed: \" + error.message +\n        \". Check the drawing list before trying again.\";\n  } finally {\n    if (drawingJobId === jobId && !logout.hidden) {\n      drawingUploadBusy = false;\n      drawingUpload.disabled = false;\n      drawingToggle.disabled = false;\n    }\n  }\n});\n\n\/\/ JOB PROBLEM REPORTS: create and view an immutable issue history. Authorized\n\/\/ employees resolve Open problems via the server-checked RPC; no delete\/edit UI.\n\/\/ Database RLS and the SECURITY DEFINER RPC enforce access independently of UI.\nconst problemToggle = document.createElement(\"button\");\nproblemToggle.id = \"gw-problem-toggle\";\nproblemToggle.type = \"button\";\nproblemToggle.textContent = \"Issues \/ Rework\";\nproblemToggle.hidden = true;\nproblemToggle.setAttribute(\"aria-expanded\", \"false\");\nproblemToggle.style.cssText = \"width:100%;margin:14px 0 8px;\";\n\nconst problemPanel = document.createElement(\"section\");\nproblemPanel.id = \"gw-problem-panel\";\nproblemPanel.hidden = true;\nproblemPanel.style.cssText =\n  \"box-sizing:border-box;width:100%;padding:16px;margin:8px 0 20px;\" +\n  \"border:1px solid #cbd5e1;border-radius:10px;background:#fff;overflow-wrap:anywhere;\";\nconst problemHeading = document.createElement(\"h3\");\nproblemHeading.textContent = \"Job Issues \/ Rework\";\nconst problemHelp = document.createElement(\"p\");\nproblemHelp.textContent =\n  \"Record job issues and rework for the selected job. Resolved reports remain in the history. \" +\n  \"For photos, use Job Photos \u2192 Problem \/ Damage for this same job.\";\nconst problemIssueTypeLabel = document.createElement(\"label\");\nproblemIssueTypeLabel.textContent = \"Issue Type\";\nproblemIssueTypeLabel.style.cssText = \"display:block;margin:16px 0 6px;font-weight:600;\";\nconst problemIssueType = document.createElement(\"select\");\nproblemIssueType.id = \"gw-problem-issue-type\";\nproblemIssueType.style.cssText = \"box-sizing:border-box;width:100%;font-size:16px;padding:8px;\";\nfor (const [value, label] of [\n  [\"\", \"Select issue type\"],\n  [\"Measurement Error\", \"Measurement Error\"],\n  [\"Fabrication \/ Cutting Error\", \"Fabrication \/ Cutting Error\"],\n  [\"Sink \/ Cooktop Error\", \"Sink \/ Cooktop Error\"],\n  [\"Edge Error\", \"Edge Error\"],\n  [\"Installation Error\", \"Installation Error\"],\n  [\"Material Damage\", \"Material Damage\"],\n  [\"Other\", \"Other\"]\n]) {\n  const option = document.createElement(\"option\");\n  option.value = value;\n  option.textContent = label;\n  problemIssueType.appendChild(option);\n}\nproblemIssueTypeLabel.appendChild(problemIssueType);\n\nconst problemWorkersLabel = document.createElement(\"label\");\nproblemWorkersLabel.textContent = \"Worker(s) involved\";\nproblemWorkersLabel.style.cssText = \"display:block;margin:16px 0 6px;font-weight:600;\";\nconst problemWorkers = document.createElement(\"input\");\nproblemWorkers.type = \"text\";\nproblemWorkers.id = \"gw-problem-workers\";\nproblemWorkers.maxLength = 500;\nproblemWorkers.placeholder = \"Example: Jose, Carlos\";\nproblemWorkers.style.cssText = \"box-sizing:border-box;width:100%;font-size:16px;padding:8px;\";\nproblemWorkersLabel.appendChild(problemWorkers);\nconst problemWorkerSelect = document.createElement(\"select\");\nproblemWorkerSelect.id = \"gw-problem-worker-select\";\nproblemWorkerSelect.multiple = true;\nproblemWorkerSelect.size = 6;\nproblemWorkerSelect.style.cssText =\n  \"box-sizing:border-box;width:100%;font-size:16px;padding:8px;margin-top:6px;\";\n\nproblemWorkers.hidden = true;\n\nproblemWorkersLabel.appendChild(problemWorkerSelect);\n\nasync function loadProblemWorkerOptions() {\n  problemWorkerSelect.replaceChildren();\n\n  const loading = document.createElement(\"option\");\n  loading.textContent = \"Loading employees...\";\n  loading.disabled = true;\n  problemWorkerSelect.appendChild(loading);\n\n  const result = await client.rpc(\"gw_problem_worker_list\");\n\n  problemWorkerSelect.replaceChildren();\n\n  if (result.error) {\n    const failed = document.createElement(\"option\");\n    failed.textContent = \"Unable to load employee list\";\n    failed.disabled = true;\n    problemWorkerSelect.appendChild(failed);\n    return;\n  }\n\n  for (const employee of result.data || []) {\n    const option = document.createElement(\"option\");\n    option.value = employee.user_id;\n    option.textContent =\n      employee.display_name + \" \u2014 \" + employee.role;\n    option.dataset.employeeName = employee.display_name;\n    problemWorkerSelect.appendChild(option);\n  }\n}\n\nproblemWorkerSelect.addEventListener(\"change\", function () {\n  const names = Array.from(problemWorkerSelect.selectedOptions)\n    .map(function (option) {\n      return option.dataset.employeeName || option.textContent;\n    });\n\n  problemWorkers.value = names.join(\", \");\n});\n\nconst problemDescriptionLabel = document.createElement(\"label\");\nproblemDescriptionLabel.htmlFor = \"gw-problem-description\";\nproblemDescriptionLabel.textContent = \"Describe the problem (required)\";\nproblemDescriptionLabel.style.cssText =\n  \"display:block;margin:16px 0 6px;font-weight:600;\";\nconst problemDescription = document.createElement(\"textarea\");\nproblemDescription.id = \"gw-problem-description\";\nproblemDescription.rows = 4;\nproblemDescription.maxLength = 4000;\nproblemDescription.style.cssText =\n  \"box-sizing:border-box;width:100%;max-width:100%;font-size:16px;\";\n\nconst problemActionLabel = document.createElement(\"label\");\nproblemActionLabel.textContent = \"Action Needed\";\nproblemActionLabel.style.cssText = \"display:block;margin:16px 0 6px;font-weight:600;\";\nconst problemAction = document.createElement(\"textarea\");\nproblemAction.rows = 3;\nproblemAction.maxLength = 4000;\nproblemAction.style.cssText = \"box-sizing:border-box;width:100%;max-width:100%;font-size:16px;\";\nproblemActionLabel.appendChild(problemAction);\n\nconst problemReworkCostLabel = document.createElement(\"label\");\nproblemReworkCostLabel.textContent = \"Rework Cost ($)\";\nproblemReworkCostLabel.style.cssText = \"display:block;margin:16px 0 6px;font-weight:600;\";\nconst problemReworkCost = document.createElement(\"input\");\nproblemReworkCost.type = \"number\";\nproblemReworkCost.min = \"0\";\nproblemReworkCost.step = \"0.01\";\nproblemReworkCost.placeholder = \"0.00\";\nproblemReworkCost.style.cssText = \"box-sizing:border-box;width:100%;font-size:16px;padding:8px;\";\nproblemReworkCostLabel.appendChild(problemReworkCost);\n\nconst problemNotesLabel = document.createElement(\"label\");\nproblemNotesLabel.textContent = \"Internal Notes\";\nproblemNotesLabel.style.cssText = \"display:block;margin:16px 0 6px;font-weight:600;\";\nconst problemNotes = document.createElement(\"textarea\");\nproblemNotes.rows = 3;\nproblemNotes.maxLength = 4000;\nproblemNotes.style.cssText = \"box-sizing:border-box;width:100%;max-width:100%;font-size:16px;\";\nproblemNotesLabel.appendChild(problemNotes);\nconst problemSubmit = document.createElement(\"button\");\nproblemSubmit.id = \"gw-submit-problem\";\nproblemSubmit.type = \"button\";\nproblemSubmit.textContent = \"Submit Issue \/ Rework\";\nproblemSubmit.style.width = \"100%\";\nconst problemSubmitMessage = document.createElement(\"p\");\nproblemSubmitMessage.id = \"gw-problem-submit-message\";\nproblemSubmitMessage.setAttribute(\"role\", \"status\");\nconst problemListHeading = document.createElement(\"h4\");\nproblemListHeading.textContent = \"Issue \/ Rework History (Open and Resolved)\";\nconst problemRefresh = document.createElement(\"button\");\nproblemRefresh.id = \"gw-refresh-problems\";\nproblemRefresh.type = \"button\";\nproblemRefresh.textContent = \"Refresh Issues \/ Rework\";\nproblemRefresh.style.width = \"100%\";\nconst problemList = document.createElement(\"div\");\nproblemList.id = \"gw-problem-list\";\nproblemList.setAttribute(\"aria-live\", \"polite\");\nproblemPanel.append(\n  problemHeading, problemHelp, problemIssueTypeLabel, problemWorkersLabel,\n  problemDescriptionLabel, problemDescription, problemActionLabel, problemReworkCostLabel,\n  problemNotesLabel, problemSubmit, problemSubmitMessage, problemListHeading,\n  problemRefresh, problemList\n);\ndrawingPanel.insertAdjacentElement(\"afterend\", problemToggle);\nproblemToggle.insertAdjacentElement(\"afterend\", problemPanel);\n\nlet problemJobId = null;\nlet problemGeneration = 0;\nlet problemRequestId = 0;\nlet problemSubmitBusy = false;\nlet problemResolveBusy = false;\n\nfunction resetProblemView() {\n  problemGeneration++;\n  problemRequestId++;\n  problemJobId = null;\n  problemSubmitBusy = false;\n  problemResolveBusy = false;\n  problemToggle.hidden = true;\n  problemToggle.setAttribute(\"aria-expanded\", \"false\");\n  problemPanel.hidden = true;\n  problemIssueType.value = \"\";\n  problemWorkers.value = \"\";\n  for (const option of problemWorkerSelect.options) {\n    option.selected = false;\n  }\n  problemDescription.value = \"\";\n  problemAction.value = \"\";\n  problemReworkCost.value = \"\";\n  problemNotes.value = \"\";\n  problemIssueType.disabled = false;\n  problemWorkers.disabled = false;\n  problemWorkerSelect.disabled = false;\n  problemDescription.disabled = false;\n  problemAction.disabled = false;\n  problemReworkCost.disabled = false;\n  problemNotes.disabled = false;\n  problemSubmit.disabled = false;\n  problemRefresh.disabled = false;\n  problemSubmitMessage.textContent = \"\";\n  problemList.replaceChildren();\n}\n\nfunction problemIsCurrent(jobId, generation) {\n  return !!client && !logout.hidden && !jobSearch.hidden && !dashboard.hidden &&\n    !problemToggle.hidden && !problemPanel.hidden &&\n    problemJobId === jobId && jobIdInput.value.trim() === jobId &&\n    (generation === undefined || generation === problemGeneration);\n}\n\nfunction configureProblemView(jobId, role) {\n  resetProblemView();\n  if (!\/^(owner|office|project_manager|installer|fabricator)$\/.test(role || \"\") ||\n      !jobId || dashboard.hidden || logout.hidden ||\n      jobIdInput.value.trim() !== jobId) return;\n  problemJobId = jobId;\n  problemToggle.hidden = false;\nvoid loadProblemWorkerOptions();\n}\n\nfunction problemEmployeeLabel(userId, currentUserId) {\n  if (!userId) return \"Not recorded\";\n  return userId === currentUserId ? \"You\" : \"Employee \" + String(userId).slice(0, 8);\n}\n\nasync function loadProblemList() {\n  const jobId = problemJobId;\n  if (!problemIsCurrent(jobId)) return;\n  const generation = problemGeneration;\n  const requestId = ++problemRequestId;\n  problemRefresh.disabled = true;\n  problemList.textContent = \"Loading problem reports...\";\n  try {\n    const { data: auth, error: authError } = await client.auth.getUser();\n    if (authError || !auth.user) throw new Error(\"Please log in again.\");\n    const { data: isActive, error: accessError } =\n      await client.rpc(\"gw_employee_is_active\");\n    if (accessError || isActive !== true)\n      throw new Error(\"Unable to verify active Portal access.\");\n    const { data: problems, error } = await client.from(\"job_problems\")\n      .select(\"id, job_id, issue_type, workers, description, action_needed, rework_cost, photo_path, notes, status, created_by, created_at, resolution_notes, resolved_by, resolved_at\")\n      .eq(\"job_id\", jobId)\n      .order(\"created_at\", { ascending: false })\n      .order(\"id\", { ascending: false });\n    if (error) throw error;\n    if (!problemIsCurrent(jobId, generation) || requestId !== problemRequestId) return;\n    problemList.replaceChildren();\n    if (!problems || problems.length === 0) {\n      problemList.textContent = \"No problem reports for this job yet.\";\n      return;\n    }\n    for (const problem of problems) {\n      const row = document.createElement(\"section\");\n      row.style.cssText =\n        \"box-sizing:border-box;padding:14px 0;border-bottom:1px solid #e2e8f0;overflow-wrap:anywhere;\";\n      const heading = document.createElement(\"h4\");\n      heading.textContent = \"Report #\" + problem.id + \" \u2014 \" + (problem.issue_type || \"Issue\") + \" \u2014 \" + problem.status;\n      const report = document.createElement(\"p\");\n      report.textContent = problem.description;\n      report.style.cssText = \"white-space:pre-wrap;overflow-wrap:anywhere;\";\n      const details = document.createElement(\"p\");\n      const detailParts = [];\n      if (problem.workers) detailParts.push(\"Worker(s): \" + problem.workers);\n      if (problem.action_needed) detailParts.push(\"Action Needed: \" + problem.action_needed);\n      if (problem.rework_cost !== null && problem.rework_cost !== undefined && problem.rework_cost !== \"\") {\n        const cost = Number(problem.rework_cost);\n        detailParts.push(\"Rework Cost: \" + (Number.isFinite(cost) ? \"$\" + cost.toFixed(2) : problem.rework_cost));\n      }\n      if (problem.notes) detailParts.push(\"Internal Notes: \" + problem.notes);\n      details.textContent = detailParts.join(\"\\n\");\n      details.style.cssText = \"white-space:pre-wrap;overflow-wrap:anywhere;\";\n      const submitted = document.createElement(\"p\");\n      submitted.textContent = \"Reported: \" +\n        (problem.created_at ? new Date(problem.created_at).toLocaleString() : \"Not recorded\") +\n        \" | By: \" + problemEmployeeLabel(problem.created_by, auth.user.id);\n      row.append(heading, report);\n      if (details.textContent) row.appendChild(details);\n      row.appendChild(submitted);\n      if (problem.status === \"Resolved\") {\n        const resolved = document.createElement(\"p\");\n        resolved.textContent = \"Resolved: \" +\n          (problem.resolved_at ? new Date(problem.resolved_at).toLocaleString() : \"Not recorded\") +\n          \" | By: \" + problemEmployeeLabel(problem.resolved_by, auth.user.id);\n        const resolution = document.createElement(\"p\");\n        resolution.textContent = \"Resolution notes: \" + (problem.resolution_notes || \"None\");\n        resolution.style.cssText = \"white-space:pre-wrap;overflow-wrap:anywhere;\";\n        row.append(resolved, resolution);\n      } else if (problem.status === \"Open\") {\n        const notesLabel = document.createElement(\"label\");\n        const notes = document.createElement(\"textarea\");\n        notes.id = \"gw-problem-resolution-\" + problem.id;\n        notesLabel.htmlFor = notes.id;\n        notesLabel.textContent = \"Resolution notes (optional)\";\n        notesLabel.style.cssText = \"display:block;margin:12px 0 6px;font-weight:600;\";\n        notes.maxLength = 4000;\n        notes.rows = 3;\n        notes.style.cssText =\n          \"box-sizing:border-box;width:100%;max-width:100%;font-size:16px;\";\n        const resolveButton = document.createElement(\"button\");\n        resolveButton.type = \"button\";\n        resolveButton.textContent = \"Mark Resolved\";\n        resolveButton.style.width = \"100%\";\n        const rowMessage = document.createElement(\"p\");\n        rowMessage.setAttribute(\"role\", \"status\");\n        resolveButton.addEventListener(\"click\", async function () {\n          if (!problemIsCurrent(jobId, generation) || problemResolveBusy ||\n              resolveButton.disabled) return;\n          if (!window.confirm(\"Mark report #\" + problem.id +\n              \" as Resolved? The original report will remain saved.\")) return;\n          problemResolveBusy = true;\n          resolveButton.disabled = true;\n          notes.disabled = true;\n          rowMessage.textContent = \"Resolving problem...\";\n          try {\n            const { data: userData, error: userError } = await client.auth.getUser();\n            if (userError || !userData.user) throw new Error(\"Please log in again.\");\n            const { data: resolvedId, error: resolveError } = await client.rpc(\n              \"gw_resolve_job_problem\",\n              { p_problem_id: problem.id, p_resolution_notes: notes.value.trim() || null }\n            );\n            if (resolveError) throw resolveError;\n            if (Number(resolvedId) !== Number(problem.id))\n              throw new Error(\"The resolution could not be verified. Refresh before trying again.\");\n            if (!problemIsCurrent(jobId, generation)) return;\n            rowMessage.textContent = \"Resolved. Refreshing saved history...\";\n            await loadProblemList();\n          } catch (error) {\n            if (problemIsCurrent(jobId, generation))\n              rowMessage.textContent = \"Unable to resolve: \" + error.message;\n       } finally {\n  if (problemGeneration === generation) {\n    if (problemJobId === jobId) {\n      if (!logout.hidden) {\n        problemSubmitBusy = false;\n        problemSubmit.disabled = false;\n        problemIssueType.disabled = false;\n        problemWorkers.disabled = false;\n        problemDescription.disabled = false;\n        problemAction.disabled = false;\n        problemReworkCost.disabled = false;\n        problemNotes.disabled = false;\n      }\n    }\n  }\n}\n        });\n        row.append(notesLabel, notes, resolveButton, rowMessage);\n      }\n      problemList.appendChild(row);\n    }\n  } catch (error) {\n    if (problemIsCurrent(jobId, generation) && requestId === problemRequestId)\n      problemList.textContent = \"Unable to load problems: \" + error.message;\n  } finally {\n    if (problemIsCurrent(jobId, generation) && requestId === problemRequestId)\n      problemRefresh.disabled = false;\n  }\n}\n\nproblemToggle.addEventListener(\"click\", function () {\n  if (!client || problemToggle.hidden || dashboard.hidden || logout.hidden ||\n      !problemJobId || jobIdInput.value.trim() !== problemJobId) return;\n  problemPanel.hidden = !problemPanel.hidden;\n  problemToggle.setAttribute(\"aria-expanded\", String(!problemPanel.hidden));\n  if (problemPanel.hidden) {\n    problemRequestId++;\n    problemList.replaceChildren();\n    problemRefresh.disabled = false;\n  } else {\n    void loadProblemList();\n  }\n});\n\nproblemRefresh.addEventListener(\"click\", function () {\n  if (!problemSubmitBusy && !problemResolveBusy) void loadProblemList();\n});\n\nproblemSubmit.addEventListener(\"click\", async function () {\n  const jobId = problemJobId;\n  if (!problemIsCurrent(jobId) || problemSubmitBusy) return;\n  const issueType = problemIssueType.value.trim();\n  const workers = problemWorkers.value.trim();\n  const description = problemDescription.value.trim();\n  const actionNeeded = problemAction.value.trim();\n  const notesValue = problemNotes.value.trim();\n  const rawCost = problemReworkCost.value.trim();\n  if (!issueType) {\n    problemSubmitMessage.textContent = \"Select an Issue Type.\";\n    return;\n  }\n  if (!description || description.length > 4000) {\n    problemSubmitMessage.textContent = \"Enter a problem description (1\u20134000 characters).\";\n    return;\n  }\n  let reworkCost = null;\n  if (rawCost !== \"\") {\n    reworkCost = Number(rawCost);\n    if (!Number.isFinite(reworkCost) || reworkCost < 0) {\n      problemSubmitMessage.textContent = \"Enter a valid Rework Cost of 0 or more.\";\n      return;\n    }\n  }\n  problemSubmitBusy = true;\n  problemSubmit.disabled = true;\n  problemIssueType.disabled = true;\n  problemWorkers.disabled = true;\n  problemWorkerSelect.disabled = true;\n  problemDescription.disabled = true;\n  problemAction.disabled = true;\n  problemReworkCost.disabled = true;\n  problemNotes.disabled = true;\n  const generation = problemGeneration;\n  problemSubmitMessage.textContent = \"Submitting problem...\";\n  try {\n    const { data: auth, error: authError } = await client.auth.getUser();\n    if (authError || !auth.user) throw new Error(\"Please log in again.\");\n    const { data: isActive, error: accessError } =\n      await client.rpc(\"gw_employee_is_active\");\n    if (accessError || isActive !== true)\n      throw new Error(\"Unable to verify active Portal access.\");\n    if (!problemIsCurrent(jobId, generation)) return;\n    const { data: saved, error } = await client.from(\"job_problems\")\n      .insert({\n        job_id: jobId,\n        issue_type: issueType,\n        workers: workers || null,\n        description,\n        action_needed: actionNeeded || null,\n        rework_cost: reworkCost,\n        photo_path: null,\n        notes: notesValue || null,\n        created_by: auth.user.id\n      })\n      .select(\"id, job_id, status\").maybeSingle();\n    if (error) throw error;\n    if (!saved || saved.job_id !== jobId || saved.status !== \"Open\")\n      throw new Error(\"Submission could not be confirmed. Refresh the list before retrying.\");\n    if (!problemIsCurrent(jobId, generation)) return;\n\n    const selectedWorkerRows = Array.from(problemWorkerSelect.selectedOptions)\n      .filter(function (option) { return !!option.value; })\n      .map(function (option) {\n        return {\n          problem_id: saved.id,\n          worker_user_id: option.value,\n          worker_name: option.dataset.employeeName || option.textContent.split(\" \u2014 \")[0]\n        };\n      });\n\n    if (selectedWorkerRows.length > 0) {\n      const workerResult = await client\n        .from(\"job_problem_workers\")\n        .insert(selectedWorkerRows);\n\n      if (workerResult.error) {\n        problemSubmitMessage.textContent =\n          \"Issue \/ Rework #\" + saved.id +\n          \" was saved, but worker link failed: \" + workerResult.error.message +\n          \". Do not submit the issue again.\";\n        await loadProblemList();\n        return;\n      }\n    }\n\n    problemIssueType.value = \"\";\n    problemWorkers.value = \"\";\n    for (const option of problemWorkerSelect.options) {\n      option.selected = false;\n    }\n    problemDescription.value = \"\";\n    problemAction.value = \"\";\n    problemReworkCost.value = \"\";\n    problemNotes.value = \"\";\n    problemSubmitMessage.textContent = \"Issue \/ Rework #\" + saved.id + \" submitted and saved.\";\n    await loadProblemList();\n  } catch (error) {\n    if (problemIsCurrent(jobId, generation))\n      problemSubmitMessage.textContent = \"Unable to submit: \" + error.message +\n        \". Check the problem list before retrying.\";\n } finally {\n  if (problemGeneration === generation) {\n    if (problemJobId === jobId) {\n      if (!logout.hidden) {\n        problemSubmitBusy = false;\n        problemSubmit.disabled = false;\n        problemIssueType.disabled = false;\n        problemWorkers.disabled = false;\n        problemWorkerSelect.disabled = false;\n        problemDescription.disabled = false;\n        problemAction.disabled = false;\n        problemReworkCost.disabled = false;\n        problemNotes.disabled = false;\n      }\n    }\n  }\n}\n});\n\ndocument.getElementById(\"gw-final-measurement\")\n   .insertAdjacentElement(\"afterend\", measurementDashboard);\nmeasurementDashboard.insertAdjacentElement(\"afterend\", measurementForm);\n\ndocument.getElementById(\"gw-site-preparation\")\n   .insertAdjacentElement(\"afterend\", preparationDashboard);\npreparationDashboard.insertAdjacentElement(\"afterend\", preparationForm);\n\ndocument.getElementById(\"gw-job-photos\")\n  .insertAdjacentElement(\n    \"afterend\",\n    document.getElementById(\"gw-photo-form\")\n  );\n resetWorkDashboards();\n document.head.appendChild(script);\n savePassword.addEventListener(\"click\", async function () {\n  const newValue = newPassword.value;\n  const confirmValue = confirmPassword.value;\n\n  if (newValue.length < 12) {\n    passwordMessage.textContent =\n      \"Password must be at least 12 characters.\";\n    return;\n  }\n\n  if (newValue !== confirmValue) {\n    passwordMessage.textContent =\n      \"Passwords do not match.\";\n    return;\n  }\n\n  if (!client) {\n    passwordMessage.textContent =\n      \"Login system is still loading.\";\n    return;\n  }\n\n  savePassword.disabled = true;\n  passwordMessage.textContent = \"Setting password...\";\n\n  try {\n    const { data, error: authError } =\n      await client.auth.getUser();\n\n    if (authError || !data.user) {\n      passwordMessage.textContent =\n        \"Invitation not verified. Please open your invitation link.\";\n      return;\n    }\n\n    const { error } = await client.auth.updateUser({\n      password: newValue\n    });\n\n    if (error) {\n      passwordMessage.textContent =\n        \"Unable to set password: \" + error.message;\n      return;\n    }\n\n    newPassword.value = \"\";\n    confirmPassword.value = \"\";\n    passwordMessage.textContent = \"Password set successfully! Opening Portal...\";\n    clearRecoveryMarker();\n    recoveringPassword = false;\n    setPasswordForm.hidden = true;\n    await showSignedIn();\n  } catch (error) {\n    passwordMessage.textContent =\n      \"Connection error. Please try again.\";\n  } finally {\n    savePassword.disabled = false;\n  }\n});\n  }\n\n  if (document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", startPortal);\n  } else {\n    startPortal();\n  }\n})();\n\n\/\/ ================================================================\n\/\/ GW PORTAL LANGUAGE SWITCHER\n\/\/ English \/ Traditional Chinese \/ Spanish\n\/\/ This block is deliberately outside startPortal(). It waits for the DOM\n\/\/ when needed, but also starts immediately if WordPress loaded the page first.\n\/\/ ================================================================\n(function () {\n  const LANGUAGE_KEY = \"gwPortalLanguage\";\n\n  const gwTranslations = {\n    en: {\n      employeeLogin: \"Employee Login\",\n      email: \"Email\",\n      password: \"Password\",\n      remember: \"Remember Me on this device\",\n      login: \"Log In\"\n    },\n\n    zh: {\n      employeeLogin: \"\u54e1\u5de5\u767b\u5165\",\n      email: \"\u96fb\u90f5\",\n      password: \"\u5bc6\u78bc\",\n      remember: \"\u5728\u6b64\u88dd\u7f6e\u8a18\u4f4f\u6211\",\n      login: \"\u767b\u5165\"\n    },\n\n    es: {\n      employeeLogin: \"Inicio de sesi\u00f3n de empleados\",\n      email: \"Correo electr\u00f3nico\",\n      password: \"Contrase\u00f1a\",\n      remember: \"Recordarme en este dispositivo\",\n      login: \"Iniciar sesi\u00f3n\"\n    }\n  };\n\n  function gwSetLanguage(lang) {\n    if (!Object.prototype.hasOwnProperty.call(gwTranslations, lang)) {\n      lang = \"en\";\n    }\n\n    const t = gwTranslations[lang];\n\n    const loginTitle = document.querySelector(\".gw-login > h2\");\n    const emailLabel = document.querySelector('label[for=\"gw-email\"]');\n    const passwordLabel = document.querySelector('label[for=\"gw-password\"]');\n    const rememberCheckbox = document.getElementById(\"gw-remember\");\n    const loginButton = document.querySelector('#gw-login-form button[type=\"submit\"]');\n\n    if (loginTitle) loginTitle.textContent = t.employeeLogin;\n    if (emailLabel) emailLabel.textContent = t.email;\n    if (passwordLabel) passwordLabel.textContent = t.password;\n\n    if (rememberCheckbox) {\n      const rememberLabel = rememberCheckbox.closest(\"label\");\n      if (rememberLabel) {\n        \/\/ Preserve the existing checkbox element and its checked state.\n        rememberLabel.replaceChildren(\n          rememberCheckbox,\n          document.createTextNode(\" \" + t.remember)\n        );\n      }\n    }\n\n    if (loginButton) loginButton.textContent = t.login;\n\n    document.querySelectorAll(\".gw-lang-btn\").forEach(function (button) {\n      const active = button.dataset.lang === lang;\n      button.classList.toggle(\"active\", active);\n      button.setAttribute(\"aria-pressed\", active ? \"true\" : \"false\");\n    });\n\n    try {\n      localStorage.setItem(LANGUAGE_KEY, lang);\n    } catch (_error) {\n      \/\/ Language still works for this page even if storage is unavailable.\n    }\n\n    document.documentElement.lang = lang === \"zh\" ? \"zh-Hant\" : lang;\n\n    \/\/ Notify signed-in Portal features that the display language changed.\n    window.dispatchEvent(new CustomEvent(\"gw-language-changed\", {\n      detail: { lang: lang }\n    }));\n  }\n\n  function startGwTranslation() {\n    const buttons = document.querySelectorAll(\".gw-lang-btn\");\n\n    if (!buttons.length) {\n      console.warn(\"GW Portal language buttons were not found.\");\n      return;\n    }\n\n    buttons.forEach(function (button) {\n      \/\/ Prevent duplicate listeners if a page builder re-runs this script.\n      if (button.dataset.gwLanguageBound === \"1\") return;\n      button.dataset.gwLanguageBound = \"1\";\n\n      button.addEventListener(\"click\", function () {\n        gwSetLanguage(button.dataset.lang || \"en\");\n      });\n    });\n\n    let savedLanguage = \"en\";\n    try {\n      savedLanguage = localStorage.getItem(LANGUAGE_KEY) || \"en\";\n    } catch (_error) {\n      savedLanguage = \"en\";\n    }\n\n    gwSetLanguage(savedLanguage);\n  }\n\n  \/\/ Expose only the setter so the rest of the Portal can reuse the same\n  \/\/ language choice when the full Portal translation layer is added.\n  window.gwSetLanguage = gwSetLanguage;\n\n  if (document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", startGwTranslation, { once: true });\n  } else {\n    startGwTranslation();\n  }\n})();\n\n\/\/ ================================================================\n\/\/ GW FIELD PORTAL UI LANGUAGE LABELS V1\n\/\/ Translates static interface text only. Never translates addresses, names,\n\/\/ job IDs, materials, or free-text records. Works for dynamically opened tabs.\n\/\/ ================================================================\n(function () {\n  const dictionary = {\n    \"Log Out\": [\"\u767b\u51fa\", \"Cerrar sesi\u00f3n\"],\n    \"Bulletin Board\": [\"\u516c\u544a\u6b04\", \"Tabl\u00f3n de anuncios\"],\n    \"\u2190 Back to Portal\": [\"\u2190 \u8fd4\u56de\u5de5\u4f5c\u5e73\u53f0\", \"\u2190 Volver al portal\"],\n    \"+ Add Note\": [\"\uff0b \u65b0\u589e\u901a\u544a\", \"+ Agregar nota\"],\n    \"Completed Notes\": [\"\u5df2\u5b8c\u6210\u901a\u544a\", \"Notas completadas\"],\n    \"Post Note\": [\"\u767c\u4f48\u901a\u544a\", \"Publicar nota\"],\n    \"Save Changes\": [\"\u5132\u5b58\u4fee\u6539\", \"Guardar cambios\"],\n    \"Save\": [\"\u5132\u5b58\", \"Guardar\"],\n    \"Cancel\": [\"\u53d6\u6d88\", \"Cancelar\"],\n    \"Edit\": [\"\u7de8\u8f2f\", \"Editar\"],\n    \"Delete\": [\"\u522a\u9664\", \"Eliminar\"],\n    \"Done\": [\"\u5b8c\u6210\", \"Hecho\"],\n    \"Title\": [\"\u6a19\u984c\", \"T\u00edtulo\"],\n    \"Note\": [\"\u5099\u8a3b\", \"Nota\"],\n    \"For\": [\"\u5c0d\u8c61\", \"Para\"],\n    \"Everyone\": [\"\u6240\u6709\u54e1\u5de5\", \"Todos\"],\n    \"Installer\": [\"\u5b89\u88dd\u5de5\", \"Instalador\"],\n    \"Fabricator\": [\"\u52a0\u5de5\u5de5\", \"Fabricador\"],\n    \"Office\": [\"\u8fa6\u516c\u5ba4\", \"Oficina\"],\n    \"Project Manager\": [\"\u5de5\u7a0b\u7d93\u7406\", \"Gerente de proyectos\"],\n    \"Owner\": [\"\u8001\u95c6\", \"Propietario\"],\n    \"Employee Accounts (Owner Only)\": [\"\u54e1\u5de5\u5e33\u6236\uff08\u50c5\u8001\u95c6\uff09\", \"Cuentas de empleados (solo propietario)\"],\n    \"Add Employee\": [\"\u65b0\u589e\u54e1\u5de5\", \"Agregar empleado\"],\n    \"Employee name\": [\"\u54e1\u5de5\u59d3\u540d\", \"Nombre del empleado\"],\n    \"Employee email\": [\"\u54e1\u5de5\u96fb\u90f5\", \"Correo del empleado\"],\n    \"Employee role\": [\"\u54e1\u5de5\u8077\u4f4d\", \"Cargo del empleado\"],\n    \"Select role\": [\"\u9078\u64c7\u8077\u4f4d\", \"Seleccionar cargo\"],\n    \"Send Employee Invitation\": [\"\u767c\u9001\u54e1\u5de5\u9080\u8acb\", \"Enviar invitaci\u00f3n\"],\n    \"Edit Name\": [\"\u4fee\u6539\u59d3\u540d\", \"Editar nombre\"],\n    \"Save Name\": [\"\u5132\u5b58\u59d3\u540d\", \"Guardar nombre\"],\n    \"Edit Role\": [\"\u4fee\u6539\u8077\u4f4d\", \"Editar cargo\"],\n    \"Save Role\": [\"\u5132\u5b58\u8077\u4f4d\", \"Guardar cargo\"],\n    \"Job ID\": [\"\u5de5\u7a0b\u7de8\u865f\", \"ID del trabajo\"],\n    \"Job ID (optional)\": [\"\u5de5\u7a0b\u7de8\u865f\uff08\u53ef\u9078\uff09\", \"ID del trabajo (opcional)\"],\n    \"Job Address\": [\"\u5de5\u7a0b\u5730\u5740\", \"Direcci\u00f3n del trabajo\"],\n    \"Search Job\": [\"\u641c\u5c0b\u5de5\u7a0b\", \"Buscar trabajo\"],\n    \"Find Job\": [\"\u5c0b\u627e\u5de5\u7a0b\", \"Buscar trabajo\"],\n    \"This Month Job IDs\": [\"\u672c\u6708\u5de5\u7a0b\", \"Trabajos de este mes\"],\n    \"This Month Jobs\": [\"\u672c\u6708\u5de5\u7a0b\", \"Trabajos de este mes\"],\n    \"Loading...\": [\"\u8f09\u5165\u4e2d...\", \"Cargando...\"],\n    \"No Job IDs yet.\": [\"\u66ab\u6642\u672a\u6709\u5de5\u7a0b\u7de8\u865f\u3002\", \"Todav\u00eda no hay trabajos.\"],\n    \"Final Measurement\": [\"\u6700\u7d42\u91cf\u5c3a\", \"Medici\u00f3n final\"],\n    \"Site Preparation\": [\"\u5de5\u5730\u6e96\u5099\", \"Preparaci\u00f3n del sitio\"],\n    \"Problem Report\": [\"\u554f\u984c\u5831\u544a\", \"Informe de problema\"],\n    \"Photos\": [\"\u76f8\u7247\", \"Fotos\"],\n    \"Upload Photos\": [\"\u4e0a\u50b3\u76f8\u7247\", \"Subir fotos\"],\n    \"Installation\": [\"\u5b89\u88dd\", \"Instalaci\u00f3n\"],\n    \"Fabrication\": [\"\u52a0\u5de5\", \"Fabricaci\u00f3n\"],\n    \"Approved\": [\"\u5df2\u6279\u51c6\", \"Aprobado\"],\n    \"Completed\": [\"\u5df2\u5b8c\u6210\", \"Completado\"],\n    \"Status\": [\"\u72c0\u614b\", \"Estado\"],\n    \"Date\": [\"\u65e5\u671f\", \"Fecha\"],\n    \"Address\": [\"\u5730\u5740\", \"Direcci\u00f3n\"],\n    \"Material\": [\"\u6750\u6599\", \"Material\"],\n    \"Notes\": [\"\u5099\u8a3b\", \"Notas\"],\n    \"Refresh\": [\"\u91cd\u65b0\u6574\u7406\", \"Actualizar\"],\n    \"Back\": [\"\u8fd4\u56de\", \"Volver\"],\n    \"Search\": [\"\u641c\u5c0b\", \"Buscar\"],\n    \"Submit\": [\"\u63d0\u4ea4\", \"Enviar\"],\n    \"Close\": [\"\u95dc\u9589\", \"Cerrar\"]\n  };\n  const reverse = new Map();\n  Object.keys(dictionary).forEach(function (english) {\n    reverse.set(english, english);\n    dictionary[english].forEach(function (translated) { reverse.set(translated, english); });\n  });\n  const variableLabels = [\n    [\/^Bulletin Board\\s+\u2022 NEW \\(\\d+\\)$\/, \/^Bulletin Board\\s+\u2022 NEW \\((\\d+)\\)$\/],\n    [\/^Completed Notes \\(\\d+\\)$\/, \/^Completed Notes \\((\\d+)\\)$\/]\n  ];\n  function originalString(value) {\n    const clean = String(value || \"\").trim();\n    if (reverse.has(clean)) return reverse.get(clean);\n    return null;\n  }\n  function translateElement(el, language) {\n    if (!el || !el.isConnected || el.closest(\"#gw-monthly-job-ids\")) return;\n    \/\/ Skip editor fields and all user\/business data displays.\n    const tag = el.tagName;\n    if (![\"BUTTON\", \"LABEL\", \"H1\", \"H2\", \"H3\", \"H4\", \"OPTION\"].includes(tag)) return;\n    if (el.querySelector(\"input,select,textarea,button\")) return;\n    const en = originalString(el.textContent);\n    if (!en) return;\n    const index = language === \"zh\" ? 0 : language === \"es\" ? 1 : -1;\n    const next = index < 0 ? en : dictionary[en][index];\n    if (el.textContent !== next) el.textContent = next;\n  }\n  let running = false;\n  function applyLanguage() {\n    if (running) return;\n    running = true;\n    try {\n      let lang = \"en\";\n      try { lang = localStorage.getItem(\"gwPortalLanguage\") || \"en\"; } catch (_) {}\n      document.querySelectorAll(\"button,label,h1,h2,h3,h4,option\").forEach(function (el) {\n        translateElement(el, lang);\n      });\n    } finally { running = false; }\n  }\n  function start() {\n    applyLanguage();\n    window.addEventListener(\"gw-language-changed\", applyLanguage);\n    let pending = false;\n    new MutationObserver(function () {\n      if (pending) return;\n      pending = true;\n      Promise.resolve().then(function () { pending = false; applyLanguage(); });\n    }).observe(document.body, { childList: true, subtree: true, characterData: true });\n  }\n  if (document.readyState === \"loading\") document.addEventListener(\"DOMContentLoaded\", start, { once: true });\n  else start();\n})();\n\n<\/script>\n\n<div class=\"gw-login\">\n  <div id=\"gw-language-switcher\">\n    <button type=\"button\" class=\"gw-lang-btn\" data-lang=\"en\">English<\/button>\n    <button type=\"button\" class=\"gw-lang-btn\" data-lang=\"zh\">\u4e2d\u6587<\/button>\n    <button type=\"button\" class=\"gw-lang-btn\" data-lang=\"es\">Espa\u00f1ol<\/button>\n  <\/div>\n  <h2>Employee Login<\/h2>\n\n  <form id=\"gw-login-form\" onsubmit=\"return false;\">\n    <label for=\"gw-email\">Email<\/label>\n    <input id=\"gw-email\" type=\"email\" autocomplete=\"username\" required=\"\">\n\n    <label for=\"gw-password\">Password<\/label>\n    <input id=\"gw-password\" type=\"password\" autocomplete=\"current-password\" required=\"\">\n\n    <label class=\"gw-remember\">\n      <input id=\"gw-remember\" type=\"checkbox\" checked=\"\">\n      Remember Me on this device\n    <\/label>\n\n    <button type=\"submit\">Log In<\/button>\n    <p id=\"gw-login-message\" role=\"status\"><\/p>\n  <\/form>\n\n  <div id=\"gw-set-password\" hidden=\"\">\n    <h3>Set Your Password<\/h3>\n\n    <label for=\"gw-new-password\">New Password<\/label>\n    <input id=\"gw-new-password\" type=\"password\" autocomplete=\"new-password\" minlength=\"12\">\n\n    <label for=\"gw-confirm-password\">Confirm Password<\/label>\n    <input id=\"gw-confirm-password\" type=\"password\" autocomplete=\"new-password\" minlength=\"12\">\n\n    <button id=\"gw-save-password\" type=\"button\">\n      Set Password\n    <\/button>\n\n    <p id=\"gw-password-message\" role=\"status\"><\/p>\n  <\/div>\n\n  <div id=\"gw-job-search\" hidden=\"\">\n    <h2>Find a Job<\/h2>\n\n    <label for=\"gw-job-id\">Job ID<\/label>\n    <input id=\"gw-job-id\" type=\"text\" placeholder=\"Enter Job ID\">\n\n    <button id=\"gw-find-job\" type=\"button\">\n      Search Job\n    <\/button>\n\n    <p id=\"gw-job-result\" role=\"status\"><\/p>\n\n    <div id=\"gw-job-dashboard\" hidden=\"\">\n      <h2>Job Dashboard<\/h2>\n\n      <button id=\"gw-final-measurement\" type=\"button\">\n        Final Measurement\n      <\/button>\n\n      <button id=\"gw-site-preparation\" type=\"button\">\n        Site Preparation\n      <\/button>\n\n      <button id=\"gw-job-photos\" type=\"button\">\n        Job Photos\n      <\/button>\n\n      <!-- JOB PHOTOS -->\n      <div id=\"gw-photo-form\" hidden=\"\">\n        <h3>Job Photos<\/h3>\n\n        <label for=\"gw-photo-type\">Photo Type<\/label>\n        <select id=\"gw-photo-type\">\n          <option value=\"Before\">Before Installation<\/option>\n          <option value=\"After\">After Installation<\/option>\n          <option value=\"Problem\">Problem \/ Damage<\/option>\n        <\/select>\n\n        <label for=\"gw-photo-file\">Choose Photo<\/label>\n\n        <!-- iPhone Photo Library \/ Camera Supported -->\n        <input id=\"gw-photo-file\" type=\"file\" accept=\"image\/jpeg,image\/png,image\/webp,image\/heic\">\n\n        <label for=\"gw-photo-description\">Description<\/label>\n        <textarea id=\"gw-photo-description\" rows=\"3\"><\/textarea>\n\n        <button id=\"gw-upload-photo\" type=\"button\">\n          Upload Photo\n        <\/button>\n\n        <p id=\"gw-photo-message\" role=\"status\"><\/p>\n\n        <h4>Job Photo Gallery<\/h4>\n\n        <button id=\"gw-view-photos\" type=\"button\">\n          View Job Photos\n        <\/button>\n\n        <div id=\"gw-photo-gallery\"><\/div>\n      <\/div>\n\n      <!-- SITE PREPARATION -->\n      <div id=\"gw-preparation-form\" hidden=\"\">\n        <h3>Site Preparation<\/h3>\n\n        <label for=\"gw-water-condition\">Water Shutoff Condition<\/label>\n        <input id=\"gw-water-condition\" type=\"text\" placeholder=\"Good \/ Leaking \/ Not accessible\">\n\n        <label class=\"gw-remember\">\n          <input id=\"gw-water-replace\" type=\"checkbox\">\n          Water Shutoff Replacement Required\n        <\/label>\n\n        <label for=\"gw-faucet-condition\">Faucet Condition<\/label>\n        <input id=\"gw-faucet-condition\" type=\"text\" placeholder=\"Good \/ Damaged \/ Replacement needed\">\n\n        <label class=\"gw-remember\">\n          <input id=\"gw-plumbing-modification\" type=\"checkbox\">\n          Plumbing Modification Required\n        <\/label>\n\n        <label for=\"gw-plumbing-notes\">Plumbing Notes<\/label>\n        <textarea id=\"gw-plumbing-notes\" rows=\"3\"><\/textarea>\n\n        <h4>Electrical<\/h4>\n\n        <label class=\"gw-remember\">\n          <input id=\"gw-electrical-modification\" type=\"checkbox\">\n          Electrical Modification Required\n        <\/label>\n\n        <label class=\"gw-remember\">\n          <input id=\"gw-outlet-issue\" type=\"checkbox\">\n          Outlet Issue\n        <\/label>\n\n        <label for=\"gw-electrical-notes\">Electrical Notes<\/label>\n        <textarea id=\"gw-electrical-notes\" rows=\"3\"><\/textarea>\n\n        <h4>Bathroom Mirror<\/h4>\n\n        <label for=\"gw-mirror-removal\">Mirror Removal<\/label>\n        <select id=\"gw-mirror-removal\">\n          <option value=\"\">Select an option<\/option>\n          <option value=\"Not Required\">Not Required<\/option>\n          <option value=\"Required\">Required<\/option>\n          <option value=\"To Be Confirmed\">To Be Confirmed<\/option>\n        <\/select>\n\n        <h4>Wall<\/h4>\n\n        <label for=\"gw-wall-condition\">Wall Condition<\/label>\n        <input id=\"gw-wall-condition\" type=\"text\" placeholder=\"Good \/ Damaged \/ Uneven\">\n\n        <label class=\"gw-remember\">\n          <input id=\"gw-wall-work-required\" type=\"checkbox\">\n          Wall Work Required\n        <\/label>\n\n        <h4>Floor Protection<\/h4>\n\n        <label for=\"gw-floor-type\">Floor Type<\/label>\n        <input id=\"gw-floor-type\" type=\"text\" placeholder=\"Wood \/ Tile \/ Carpet\">\n\n        <label class=\"gw-remember\">\n          <input id=\"gw-floor-protection-required\" type=\"checkbox\">\n          Floor Protection Required\n        <\/label>\n\n        <label for=\"gw-floor-protection-type\">Protection Type<\/label>\n        <input id=\"gw-floor-protection-type\" type=\"text\" placeholder=\"Ram Board \/ Plywood \/ Other\">\n\n        <label for=\"gw-protection-notes\">Protection Notes<\/label>\n        <textarea id=\"gw-protection-notes\" rows=\"3\"><\/textarea>\n\n        <h4>Site Access<\/h4>\n\n        <label for=\"gw-access-condition\">Access Condition<\/label>\n        <input id=\"gw-access-condition\" type=\"text\" placeholder=\"Clear \/ Narrow \/ Difficult\">\n\n        <label class=\"gw-remember\">\n          <input id=\"gw-stairs-required\" type=\"checkbox\">\n          Stairs Required\n        <\/label>\n\n        <label for=\"gw-special-access-notes\">Special Access Notes<\/label>\n        <textarea id=\"gw-special-access-notes\" rows=\"3\"><\/textarea>\n\n        <h4>Special Tools &amp; Parts<\/h4>\n\n        <label class=\"gw-remember\">\n          <input id=\"gw-special-tools-required\" type=\"checkbox\">\n          Special Tools Required\n        <\/label>\n\n        <label for=\"gw-special-tools-list\">Special Tools List<\/label>\n        <textarea id=\"gw-special-tools-list\" rows=\"3\"><\/textarea>\n\n        <label class=\"gw-remember\">\n          <input id=\"gw-special-parts-required\" type=\"checkbox\">\n          Special Parts Required\n        <\/label>\n\n        <label for=\"gw-special-parts-list\">Special Parts List<\/label>\n        <textarea id=\"gw-special-parts-list\" rows=\"3\"><\/textarea>\n\n        <h4>Preparation &amp; Installer Review<\/h4>\n\n        <label for=\"gw-installer-notes\">Installer Notes<\/label>\n        <textarea id=\"gw-installer-notes\" rows=\"3\"><\/textarea>\n\n        <label for=\"gw-prepared-by\">Prepared By<\/label>\n        <input id=\"gw-prepared-by\" type=\"text\">\n\n        <label class=\"gw-remember\">\n          <input id=\"gw-preparation-complete\" type=\"checkbox\">\n          Preparation Complete\n        <\/label>\n\n        <label class=\"gw-remember\">\n          <input id=\"gw-installer-reviewed\" type=\"checkbox\">\n          Installer Reviewed\n        <\/label>\n\n        <label for=\"gw-installer-reviewed-by\">Reviewed By<\/label>\n        <input id=\"gw-installer-reviewed-by\" type=\"text\">\n\n        <label class=\"gw-remember\">\n          <input id=\"gw-ready-for-install\" type=\"checkbox\">\n          Ready for Installation\n        <\/label>\n\n        <button id=\"gw-save-preparation\" type=\"button\">\n          Save Site Preparation\n        <\/button>\n\n        <p id=\"gw-preparation-message\" role=\"status\"><\/p>\n      <\/div>\n\n      <!-- INSTALLER REVIEW -->\n      <button id=\"gw-installer-review-button\" type=\"button\">\n        Installer Review\n      <\/button>\n\n      <div id=\"gw-installer-review-form\" hidden=\"\">\n        <h3>Installer Review<\/h3>\n\n        <label for=\"gw-review-notes\">Installer Notes<\/label>\n        <textarea id=\"gw-review-notes\" rows=\"3\"><\/textarea>\n\n        <label>\n          <input id=\"gw-review-confirmed\" type=\"checkbox\">\n          Installer Reviewed\n        <\/label>\n\n        <label>\n          <input id=\"gw-review-ready\" type=\"checkbox\">\n          Ready for Installation\n        <\/label>\n\n        <button id=\"gw-save-review\" type=\"button\">\n          Save Installer Review\n        <\/button>\n\n        <p id=\"gw-review-message\" role=\"status\"><\/p>\n      <\/div>\n\n      <!-- FINAL MEASUREMENT -->\n      <div id=\"gw-measurement-form\" hidden=\"\">\n        <h3>Final Measurement<\/h3>\n\n        <label for=\"gw-measured-by\">Measured By<\/label>\n        <input id=\"gw-measured-by\" type=\"text\">\n\n        <label for=\"gw-measurement-date\">Measurement Date<\/label>\n        <input id=\"gw-measurement-date\" type=\"date\">\n\n        <label for=\"gw-project-area\">Project Area<\/label>\n        <input id=\"gw-project-area\" type=\"text\" placeholder=\"Kitchen, Bathroom...\">\n\n        <label for=\"gw-materials\">Material<\/label>\n        <input id=\"gw-materials\" type=\"text\" placeholder=\"Quartz, Granite, Quartzite...\">\n\n        <label for=\"gw-edge\">Edge<\/label>\n        <select id=\"gw-edge\">\n          <option value=\"\">Select Edge<\/option>\n          <option value=\"Eased\">Eased<\/option>\n          <option value=\"Bevel\">Bevel<\/option>\n          <option value=\"Bullnose\">Bullnose<\/option>\n          <option value=\"Mitered\">Mitered<\/option>\n          <option value=\"Other\">Other<\/option>\n        <\/select>\n\n        <label for=\"gw-sink-type\">Sink Type<\/label>\n        <select id=\"gw-sink-type\">\n          <option value=\"\">Select Sink<\/option>\n          <option value=\"Undermount\">Undermount<\/option>\n          <option value=\"Drop-in\">Drop-in<\/option>\n          <option value=\"Farmhouse\">Farmhouse<\/option>\n          <option value=\"None\">None<\/option>\n        <\/select>\n\n        <label for=\"gw-sink-model\">Sink Model<\/label>\n        <input id=\"gw-sink-model\" type=\"text\" placeholder=\"Brand \/ Model Number\">\n\n        <label for=\"gw-stove-type\">Stove Size \/ Model<\/label>\n        <input id=\"gw-stove-type\" type=\"text\" placeholder=\"e.g. 30 inches \/ GE Model JGS760\">\n\n        <label for=\"gw-faucet\">Faucet<\/label>\n        <input id=\"gw-faucet\" type=\"text\" placeholder=\"Brand \/ Model \/ Hole Location\">\n\n        <label for=\"gw-backsplash\">Backsplash<\/label>\n        <input id=\"gw-backsplash\" type=\"text\" placeholder=\"4 inch, Full Height, None...\">\n\n        <label class=\"gw-remember\">\n          <input id=\"gw-seam-required\" type=\"checkbox\">\n          Seam Required\n        <\/label>\n\n        <label for=\"gw-field-notes\">Field Notes<\/label>\n        <textarea id=\"gw-field-notes\" rows=\"4\"><\/textarea>\n\n        <button id=\"gw-save-measurement\" type=\"button\">\n          Save Measurement\n        <\/button>\n\n        <p id=\"gw-measurement-message\" role=\"status\"><\/p>\n      <\/div>\n\n    <\/div>\n  <\/div>\n<\/div>\n\n\n\n<script data-wp-block-html=\"js\">\n(function () {\n  \"use strict\";\n\n  const SUPABASE_URL = \"https:\/\/tepzhgerltbdbhcgksog.supabase.co\";\n  const SUPABASE_KEY = \"sb_publishable_12lkMKENfEprSCo__ldjhQ_1Yqd2V0U\";\n  const STORAGE_KEY = \"gw-portal-auth\";\n\n  function exactText(root, selector, text) {\n    return Array.from(root.querySelectorAll(selector)).find(function (el) {\n      return String(el.textContent || \"\").trim() === text;\n    }) || null;\n  }\n\n  function fieldFromLabel(root, text) {\n    const label = exactText(root, \"label\", text);\n    if (!label) return null;\n    const inside = label.querySelector(\"input,textarea,select\");\n    if (inside) return inside;\n    return label.parentElement\n      ? label.parentElement.querySelector(\"input,textarea,select\")\n      : null;\n  }\n\n  function checkboxFromLabel(root, text) {\n    const label = exactText(root, \"label\", text);\n    return label ? label.querySelector('input[type=\"checkbox\"]') : null;\n  }\n\n  function getAccessToken() {\n    for (const store of [localStorage, sessionStorage]) {\n      try {\n        const raw = store.getItem(STORAGE_KEY);\n        if (!raw) continue;\n        const parsed = JSON.parse(raw);\n        const session = parsed && parsed.currentSession ? parsed.currentSession : parsed;\n        if (session && typeof session.access_token === \"string\" && session.access_token) {\n          return session.access_token;\n        }\n      } catch (_error) {\n        \/\/ Try the other browser storage location.\n      }\n    }\n    return null;\n  }\n\n  function numberValue(input, integerOnly) {\n    const raw = input.value.trim();\n    if (raw === \"\") return null;\n    const value = Number(raw);\n    if (!Number.isFinite(value) || value < 0) return undefined;\n    if (integerOnly &#038;&#038; !Number.isInteger(value)) return undefined;\n    return value;\n  }\n\n  async function callLaborRpc(payload) {\n    const token = getAccessToken();\n    if (!token) throw new Error(\"Please log in again.\");\n\n    const response = await fetch(\n      SUPABASE_URL + \"\/rest\/v1\/rpc\/gw_office_create_installer_labor\",\n      {\n        method: \"POST\",\n        headers: {\n          apikey: SUPABASE_KEY,\n          Authorization: \"Bearer \" + token,\n          \"Content-Type\": \"application\/json\",\n          Accept: \"application\/json\"\n        },\n        body: JSON.stringify(payload)\n      }\n    );\n\n    const text = await response.text();\n    if (!response.ok) {\n      let message = text || (\"HTTP \" + response.status);\n      try {\n        const parsed = JSON.parse(text);\n        if (parsed &#038;&#038; parsed.message) message = parsed.message;\n      } catch (_error) {}\n      throw new Error(message);\n    }\n\n    if (!text) throw new Error(\"Save was not confirmed.\");\n    try {\n      return JSON.parse(text);\n    } catch (_error) {\n      return text;\n    }\n  }\n\n  function bind() {\n    const panel = document.getElementById(\"gw-accounting-panel\");\n    if (!panel) return false;\n\n    const saveButton = exactText(panel, \"button\", \"Save Installer Labor\");\n    if (!saveButton) return false;\n    if (saveButton.dataset.gwStandaloneLaborSave === \"1\") return true;\n\n    const installerBox = saveButton.parentElement;\n    if (!installerBox) return false;\n\n    const jobInput = panel.querySelector('input[placeholder=\"GW-2610-001\"]');\n    if (!jobInput) return false;\n\n    const jobBox = jobInput.closest(\"section\");\n    const jobMessage = jobBox ? jobBox.querySelector('p[role=\"status\"]') : null;\n\n    const workDate = fieldFromLabel(installerBox, \"Work Date\");\n    const workType = fieldFromLabel(installerBox, \"Work Type\");\n    const sf = fieldFromLabel(installerBox, \"SF\");\n    const kitchenSink = fieldFromLabel(installerBox, \"Kitchen Sink Qty\");\n    const bathSink = fieldFromLabel(installerBox, \"Bath Sink Qty\");\n    const cooktop = fieldFromLabel(installerBox, \"Cooktop Qty\");\n    const miter = fieldFromLabel(installerBox, \"Miter LF\");\n    const seam = fieldFromLabel(installerBox, \"Seam Qty\");\n    const demolition = checkboxFromLabel(installerBox, \"Demolition\");\n    const backsplash = checkboxFromLabel(installerBox, \"Backsplash\");\n    const repair = checkboxFromLabel(installerBox, \"Repair\");\n    const otherWork = fieldFromLabel(installerBox, \"Other Work\");\n    const notes = fieldFromLabel(installerBox, \"Notes\");\n\n    if (!workDate || !workType || !sf || !kitchenSink || !bathSink ||\n        !cooktop || !miter || !seam || !demolition || !backsplash ||\n        !repair || !otherWork || !notes) return false;\n\n    const messages = installerBox.querySelectorAll(\"p\");\n    const message = messages.length ? messages[messages.length - 1] : null;\n    if (!message) return false;\n\n    saveButton.dataset.gwStandaloneLaborSave = \"1\";\n    saveButton.disabled = false;\n    message.textContent = \"Labor form ready to save.\";\n\n    saveButton.addEventListener(\"click\", async function () {\n      if (saveButton.disabled) return;\n\n      const jobId = jobInput.value.trim().toUpperCase();\n      if (!\/^GW-\\d{4}-\\d{3}$\/.test(jobId)) {\n        message.textContent = \"Enter a valid Job ID first.\";\n        return;\n      }\n\n      if (!jobMessage || jobMessage.textContent.trim() !== \"Loaded \" + jobId + \".\") {\n        message.textContent = \"Click Load Job first.\";\n        return;\n      }\n\n      if (!\/^\\d{4}-\\d{2}-\\d{2}$\/.test(workDate.value)) {\n        message.textContent = \"Enter a valid Work Date.\";\n        return;\n      }\n\n      const values = {\n        sf: numberValue(sf, false),\n        kitchen: numberValue(kitchenSink, true),\n        bath: numberValue(bathSink, true),\n        cooktop: numberValue(cooktop, true),\n        miter: numberValue(miter, false),\n        seam: numberValue(seam, true)\n      };\n\n      if (Object.values(values).some(function (value) { return value === undefined; })) {\n        message.textContent = \"Check the quantity fields. Qty fields must be whole numbers.\";\n        return;\n      }\n\n      saveButton.disabled = true;\n      message.textContent = \"Saving Installer Labor...\";\n\n      try {\n        const id = await callLaborRpc({\n          p_job_id: jobId,\n          p_work_date: workDate.value,\n          p_work_type: workType.value.trim() || null,\n          p_sf: values.sf,\n          p_kitchen_sink_qty: values.kitchen,\n          p_bath_sink_qty: values.bath,\n          p_cooktop_qty: values.cooktop,\n          p_miter_lf: values.miter,\n          p_seam_qty: values.seam,\n          p_demolition: demolition.checked,\n          p_backsplash: backsplash.checked,\n          p_repair: repair.checked,\n          p_other_work: otherWork.value.trim() || null,\n          p_notes: notes.value.trim() || null\n        });\n\n        message.textContent = \"Installer Labor saved successfully. Record #\" + id + \".\";\n        sf.value = \"\";\n        kitchenSink.value = \"\";\n        bathSink.value = \"\";\n        cooktop.value = \"\";\n        miter.value = \"\";\n        seam.value = \"\";\n        demolition.checked = false;\n        backsplash.checked = false;\n        repair.checked = false;\n        otherWork.value = \"\";\n        notes.value = \"\";\n      } catch (error) {\n        message.textContent = \"Save failed: \" + (error &#038;&#038; error.message ? error.message : \"Unknown error\");\n      } finally {\n        saveButton.disabled = false;\n      }\n    });\n\n    return true;\n  }\n\n  function start() {\n    if (bind()) return;\n    let tries = 0;\n    const timer = setInterval(function () {\n      tries += 1;\n      if (bind() || tries >= 60) clearInterval(timer);\n    }, 500);\n  }\n\n  if (document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", start, { once: true });\n  } else {\n    start();\n  }\n})();\n\n<\/script>\n\n\n\n<script data-wp-block-html=\"js\">\n(function () {\n  \"use strict\";\n\n  const SUPABASE_URL = \"https:\/\/tepzhgerltbdbhcgksog.supabase.co\";\n  const SUPABASE_KEY = \"sb_publishable_12lkMKENfEprSCo__ldjhQ_1Yqd2V0U\";\n  const STORAGE_KEY = \"gw-portal-auth\";\n\n  function exactText(root, selector, text) {\n    return Array.from(root.querySelectorAll(selector)).find(function (el) {\n      return String(el.textContent || \"\").trim() === text;\n    }) || null;\n  }\n\n  function fieldFromLabel(root, text) {\n    const label = exactText(root, \"label\", text);\n    if (!label) return null;\n    const inside = label.querySelector(\"input,textarea,select\");\n    if (inside) return inside;\n    return label.parentElement\n      ? label.parentElement.querySelector(\"input,textarea,select\")\n      : null;\n  }\n\n  function checkboxFromLabel(root, text) {\n    const label = exactText(root, \"label\", text);\n    return label ? label.querySelector('input[type=\"checkbox\"]') : null;\n  }\n\n  function getAccessToken() {\n    for (const store of [localStorage, sessionStorage]) {\n      try {\n        const raw = store.getItem(STORAGE_KEY);\n        if (!raw) continue;\n        const parsed = JSON.parse(raw);\n        const session = parsed && parsed.currentSession ? parsed.currentSession : parsed;\n        if (session && typeof session.access_token === \"string\" && session.access_token) {\n          return session.access_token;\n        }\n      } catch (_error) {\n        \/\/ Try the other browser storage location.\n      }\n    }\n    return null;\n  }\n\n  function numberValue(input, integerOnly) {\n    const raw = input.value.trim();\n    if (raw === \"\") return null;\n    const value = Number(raw);\n    if (!Number.isFinite(value) || value < 0) return undefined;\n    if (integerOnly &#038;&#038; !Number.isInteger(value)) return undefined;\n    return value;\n  }\n\n  async function callLaborRpc(payload) {\n    const token = getAccessToken();\n    if (!token) throw new Error(\"Please log in again.\");\n\n    const response = await fetch(\n      SUPABASE_URL + \"\/rest\/v1\/rpc\/gw_office_create_installer_labor\",\n      {\n        method: \"POST\",\n        headers: {\n          apikey: SUPABASE_KEY,\n          Authorization: \"Bearer \" + token,\n          \"Content-Type\": \"application\/json\",\n          Accept: \"application\/json\"\n        },\n        body: JSON.stringify(payload)\n      }\n    );\n\n    const text = await response.text();\n    if (!response.ok) {\n      let message = text || (\"HTTP \" + response.status);\n      try {\n        const parsed = JSON.parse(text);\n        if (parsed &#038;&#038; parsed.message) message = parsed.message;\n      } catch (_error) {}\n      throw new Error(message);\n    }\n\n    if (!text) throw new Error(\"Save was not confirmed.\");\n    try {\n      return JSON.parse(text);\n    } catch (_error) {\n      return text;\n    }\n  }\n\n  function bind() {\n    const panel = document.getElementById(\"gw-accounting-panel\");\n    if (!panel) return false;\n\n    const saveButton = exactText(panel, \"button\", \"Save Installer Labor\");\n    if (!saveButton) return false;\n    if (saveButton.dataset.gwStandaloneLaborSave === \"1\") return true;\n\n    const installerBox = saveButton.parentElement;\n    if (!installerBox) return false;\n\n    const jobInput = panel.querySelector('input[placeholder=\"GW-2610-001\"]');\n    if (!jobInput) return false;\n\n    const jobBox = jobInput.closest(\"section\");\n    const jobMessage = jobBox ? jobBox.querySelector('p[role=\"status\"]') : null;\n\n    const workDate = fieldFromLabel(installerBox, \"Work Date\");\n    const workType = fieldFromLabel(installerBox, \"Work Type\");\n    const sf = fieldFromLabel(installerBox, \"SF\");\n    const kitchenSink = fieldFromLabel(installerBox, \"Kitchen Sink Qty\");\n    const bathSink = fieldFromLabel(installerBox, \"Bath Sink Qty\");\n    const cooktop = fieldFromLabel(installerBox, \"Cooktop Qty\");\n    const miter = fieldFromLabel(installerBox, \"Miter LF\");\n    const seam = fieldFromLabel(installerBox, \"Seam Qty\");\n    const demolition = checkboxFromLabel(installerBox, \"Demolition\");\n    const backsplash = checkboxFromLabel(installerBox, \"Backsplash\");\n    const repair = checkboxFromLabel(installerBox, \"Repair\");\n    const otherWork = fieldFromLabel(installerBox, \"Other Work\");\n    const notes = fieldFromLabel(installerBox, \"Notes\");\n\n    if (!workDate || !workType || !sf || !kitchenSink || !bathSink ||\n        !cooktop || !miter || !seam || !demolition || !backsplash ||\n        !repair || !otherWork || !notes) return false;\n\n    const messages = installerBox.querySelectorAll(\"p\");\n    const message = messages.length ? messages[messages.length - 1] : null;\n    if (!message) return false;\n\n    saveButton.dataset.gwStandaloneLaborSave = \"1\";\n    saveButton.disabled = false;\n    message.textContent = \"Labor form ready to save.\";\n\n    saveButton.addEventListener(\"click\", async function () {\n      if (saveButton.disabled) return;\n\n      const jobId = jobInput.value.trim().toUpperCase();\n      if (!\/^GW-\\d{4}-\\d{3}$\/.test(jobId)) {\n        message.textContent = \"Enter a valid Job ID first.\";\n        return;\n      }\n\n      if (!jobMessage || jobMessage.textContent.trim() !== \"Loaded \" + jobId + \".\") {\n        message.textContent = \"Click Load Job first.\";\n        return;\n      }\n\n      if (!\/^\\d{4}-\\d{2}-\\d{2}$\/.test(workDate.value)) {\n        message.textContent = \"Enter a valid Work Date.\";\n        return;\n      }\n\n      const values = {\n        sf: numberValue(sf, false),\n        kitchen: numberValue(kitchenSink, true),\n        bath: numberValue(bathSink, true),\n        cooktop: numberValue(cooktop, true),\n        miter: numberValue(miter, false),\n        seam: numberValue(seam, true)\n      };\n\n      if (Object.values(values).some(function (value) { return value === undefined; })) {\n        message.textContent = \"Check the quantity fields. Qty fields must be whole numbers.\";\n        return;\n      }\n\n      saveButton.disabled = true;\n      message.textContent = \"Saving Installer Labor...\";\n\n      try {\n        const id = await callLaborRpc({\n          p_job_id: jobId,\n          p_work_date: workDate.value,\n          p_work_type: workType.value.trim() || null,\n          p_sf: values.sf,\n          p_kitchen_sink_qty: values.kitchen,\n          p_bath_sink_qty: values.bath,\n          p_cooktop_qty: values.cooktop,\n          p_miter_lf: values.miter,\n          p_seam_qty: values.seam,\n          p_demolition: demolition.checked,\n          p_backsplash: backsplash.checked,\n          p_repair: repair.checked,\n          p_other_work: otherWork.value.trim() || null,\n          p_notes: notes.value.trim() || null\n        });\n\n        message.textContent = \"Installer Labor saved successfully. Record #\" + id + \".\";\n        sf.value = \"\";\n        kitchenSink.value = \"\";\n        bathSink.value = \"\";\n        cooktop.value = \"\";\n        miter.value = \"\";\n        seam.value = \"\";\n        demolition.checked = false;\n        backsplash.checked = false;\n        repair.checked = false;\n        otherWork.value = \"\";\n        notes.value = \"\";\n      } catch (error) {\n        message.textContent = \"Save failed: \" + (error &#038;&#038; error.message ? error.message : \"Unknown error\");\n      } finally {\n        saveButton.disabled = false;\n      }\n    });\n\n    return true;\n  }\n\n  function start() {\n    if (bind()) return;\n    let tries = 0;\n    const timer = setInterval(function () {\n      tries += 1;\n      if (bind() || tries >= 60) clearInterval(timer);\n    }, 500);\n  }\n\n  if (document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", start, { once: true });\n  } else {\n    start();\n  }\n})();\n\n<\/script>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>English \u4e2d\u6587 Espa\u00f1ol Employee Login Email Password Remember Me on this device Log In Set Your Password New Password Confirm [&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-85","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/gwkitchen.com\/wp2026\/index.php?rest_route=\/wp\/v2\/pages\/85","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=85"}],"version-history":[{"count":145,"href":"https:\/\/gwkitchen.com\/wp2026\/index.php?rest_route=\/wp\/v2\/pages\/85\/revisions"}],"predecessor-version":[{"id":325,"href":"https:\/\/gwkitchen.com\/wp2026\/index.php?rest_route=\/wp\/v2\/pages\/85\/revisions\/325"}],"wp:attachment":[{"href":"https:\/\/gwkitchen.com\/wp2026\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=85"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}