/* Eye Clinic Prescription Manager — public Clinic Queue Registration page */ /* Relies on ecpQueueSignup.ajaxUrl, localized via wp_localize_script(). */ document.addEventListener('DOMContentLoaded', function () { const wrapper = document.querySelector('[data-ecp-queue-signup="1"]'); if (!wrapper) return; const form = wrapper.querySelector('[data-queue-form="1"]'); const feedback = wrapper.querySelector('[data-queue-feedback]'); const result = wrapper.querySelector('[data-queue-result]'); let nonce = wrapper.getAttribute('data-queue-nonce'); const submitButton = form.querySelector('.ecp-queue-submit'); let isSubmitting = false; let submissionCompleted = false; // Manual x-www-form-urlencoded body builder. We deliberately do NOT use // `new URLSearchParams({...})` (the object-record constructor form) here. // That constructor is the confirmed source of the // "TypeError: The string did not match the expected pattern" crash seen // on iPad/older Safari — it happened whether the call was made through // fetch() or through XMLHttpRequest, so swapping transport alone did not // fix it. Plain string concatenation has no such history and works // identically across every browser, old or new. function buildFormBody(params) { const pairs = []; for (const key in params) { if (!Object.prototype.hasOwnProperty.call(params, key)) continue; const value = params[key]; pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value === null || value === undefined ? '' : value)); } return pairs.join('&'); } // TEMP DEBUG HELPER — remove once the iPad/old-Safari "string did not // match the expected pattern" error is root-caused. Turns any caught // JS error into a short on-screen string (message + first stack // frame) so we can see exactly which line threw it on a device with // no Mac/cable access for the Safari remote inspector. function describeError(error) { if (!error) { return 'unknown error'; } const name = error.name || 'Error'; const message = error.message || String(error); let firstFrame = ''; if (typeof error.stack === 'string' && error.stack) { const lines = error.stack.split('\n').map(function (l) { return l.trim(); }).filter(Boolean); // Skip the first line if it's just "Name: message" repeated, // grab the next one or two actual call-site frames instead. const frames = lines.filter(function (l) { return l !== (name + ': ' + message); }); firstFrame = frames.slice(0, 2).join(' <- '); } let out = name + ': ' + message; if (firstFrame) { out += ' [' + firstFrame.slice(0, 200) + ']'; } return out; } function postFormViaXhr(action, payload) { // Fallback path for any genuine network-level fetch() failure // (some embedded in-app WebViews on iOS are flaky with fetch() // regardless of the URLSearchParams issue above). XMLHttpRequest // is the older, most broadly-compatible transport. return new Promise(function (resolve, reject) { try { const body = buildFormBody(Object.assign({ action: action, _wpnonce: nonce || '' }, payload || {})); const xhr = new XMLHttpRequest(); xhr.open('POST', ecpQueueSignup.ajaxUrl, true); // Required so PHP populates $_POST from the raw body — without // this, the server sees an empty POST regardless of what we send. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.onload = function () { if (xhr.status < 200 || xhr.status >= 300) { const err = new Error('HTTP ' + xhr.status + (xhr.responseText ? ': ' + xhr.responseText.slice(0, 160) : '')); err.httpStatus = xhr.status; reject(err); return; } try { resolve(JSON.parse(xhr.responseText)); } catch (parseError) { reject(new Error('Invalid server response.')); } }; xhr.onerror = function () { reject(new Error('network error')); }; xhr.send(body); } catch (err) { reject(err); } }); } async function postForm(action, payload) { let response; try { const body = buildFormBody(Object.assign({ action: action, _wpnonce: nonce || '' }, payload || {})); response = await fetch(ecpQueueSignup.ajaxUrl, { method: 'POST', // Minimal, parameter-free Content-Type. WordPress/PHP only // needs the media type to populate $_POST — no charset // parameter needed, and keeping this simple avoids any // WebKit header-value edge cases entirely. headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body }); } catch (error) { // Any fetch()-level failure (network blip, some in-app WebView // quirks) — retry once via XMLHttpRequest before giving up. // TEMP DEBUG: this used to be silent, so if fetch() itself was // throwing the "string did not match the expected pattern" // error (e.g. from the headers object literal below) it was // invisible — the XHR fallback would just quietly retry. Log // it so we know if this catch is actually firing. console.error('[ECP queue] fetch() failed, falling back to XHR:', describeError(error)); return postFormViaXhr(action, payload); } if (!response.ok) { // Non-2xx (e.g. 429 rate limit, 500 server error, WAF block) — surface // the real status instead of letting response.json() throw a vague // "unexpected token" error that just shows up as "Unable to join queue". let bodyText = ''; try { bodyText = await response.text(); } catch (e) {} const err = new Error('HTTP ' + response.status + (bodyText ? ': ' + bodyText.slice(0, 160) : '')); err.httpStatus = response.status; throw err; } return response.json(); } async function refreshQueueFormToken() { const response = await postForm('ecp_public_queue_refresh_token', {}); if (!response.success || !response.data) { throw new Error('Unable to refresh queue form token.'); } if (form.elements.form_issued_at) { form.elements.form_issued_at.value = response.data.issued_at || ''; } if (form.elements.form_signature) { form.elements.form_signature.value = response.data.signature || ''; } if (response.data.queue_nonce) { // Keep the ajax nonce itself fresh too — the one baked into // the page HTML may be stale (e.g. if this page was served // from a cache), so every call after this uses the live one. nonce = response.data.queue_nonce; } } // NOTE: we intentionally do NOT proactively refresh the token on page // load. The submit handler below always fetches a guaranteed-fresh // token immediately before submitting, so a page-load refresh added // an extra network round trip to every visit without actually // reducing risk — on slower/congested branch WiFi (iPad included) // this was adding noticeable delay before the patient could even // start filling the form. function generateSubmissionRequestId() { if (window.crypto && typeof window.crypto.randomUUID === 'function') { return window.crypto.randomUUID(); } return 'ecp-' + Date.now() + '-' + Math.random().toString(16).slice(2); } function resetQueueSubmissionState() { submissionCompleted = false; isSubmitting = false; if (submitButton) { submitButton.disabled = false; submitButton.removeAttribute('aria-busy'); } if (form.elements.submission_request_id) { form.elements.submission_request_id.value = generateSubmissionRequestId(); } } function handleNewPatientIdentityInput() { // Local-only reset (no network call). A different patient typing // into the form gets a fresh submission-request-id so their entry // isn't mistaken for a duplicate of the previous patient's. The // actual server token is refreshed once, right before submit — // see the submit handler below — so no background refresh is // needed here on every field's typing pause. resetQueueSubmissionState(); feedback.textContent = ''; result.innerHTML = ''; } ['first_name', 'last_name', 'phone', 'branch_name'].forEach(function (fieldName) { const field = form.elements[fieldName]; if (!field) { return; } field.addEventListener('input', handleNewPatientIdentityInput); field.addEventListener('change', handleNewPatientIdentityInput); }); wrapper.querySelector('[data-queue-lookup="1"]').addEventListener('click', async function () { const phone = form.elements.phone.value.trim(); if (!phone) { feedback.textContent = 'Enter mobile number first.'; return; } feedback.textContent = 'Checking mobile number...'; result.innerHTML = ''; let response; try { response = await postForm('ecp_public_queue_lookup', { phone, website: form.elements.website.value, form_issued_at: form.elements.form_issued_at.value, form_signature: form.elements.form_signature.value }); } catch (error) { console.error('[ECP queue] lookup failed:', error); feedback.textContent = 'Unable to check mobile number (' + describeError(error) + '). Please try again.'; return; } if (!response.success || !response.data) { feedback.textContent = response.data && response.data.message ? response.data.message : 'No existing account found. Continue filling in your details.'; return; } const customer = response.data.customer || {}; form.elements.first_name.value = customer.first_name || ''; form.elements.last_name.value = customer.last_name || ''; form.elements.email.value = customer.email || ''; form.elements.birthdate.value = customer.birthdate || ''; form.elements.address.value = customer.address || ''; feedback.textContent = 'Existing account found. Details loaded.'; }); form.addEventListener('submit', async function (event) { event.preventDefault(); if (isSubmitting || submissionCompleted) { return; } isSubmitting = true; if (submitButton) { submitButton.disabled = true; submitButton.setAttribute('aria-busy', 'true'); } feedback.textContent = 'Saving account and queueing patient...'; result.innerHTML = ''; const payload = { branch_name: form.elements.branch_name.value, phone: form.elements.phone.value.trim(), first_name: form.elements.first_name.value.trim(), last_name: form.elements.last_name.value.trim(), email: form.elements.email.value.trim(), birthdate: form.elements.birthdate.value, address: form.elements.address.value.trim(), submission_request_id: form.elements.submission_request_id.value, website: form.elements.website.value, form_issued_at: '', form_signature: '' }; try { await refreshQueueFormToken(); payload.form_issued_at = form.elements.form_issued_at.value; payload.form_signature = form.elements.form_signature.value; const response = await postForm('ecp_public_queue_submit', payload); if (!response.success) { feedback.textContent = response.data && response.data.message ? response.data.message : 'Unable to join queue.'; return; } const data = response.data || {}; submissionCompleted = true; feedback.textContent = data.message || 'Queue created successfully.'; // Navigate to the patient's own Queue Status page as a real, // dedicated confirmation screen — the &kiosk=1 flag tells that // page (instead of its normal 10-second self-refresh) to // auto-return here to a blank signup form after a few // seconds, so this shared device is ready for the next patient. if (data.queue_status_url) { const statusUrl = data.queue_status_url + (data.queue_status_url.indexOf('?') === -1 ? '?' : '&') + 'kiosk=1'; window.location.href = statusUrl; return; } } catch (error) { // Show the real reason (HTTP status / rate-limit / server error) instead // of a generic message, so this is diagnosable next time it happens. // TEMP DEBUG: describeError() adds the error name + a stack // frame on top of the plain message, so a screenshot of this // text is enough to pinpoint the exact throwing line without // needing Safari's remote Web Inspector on the device. console.error('[ECP queue] submit failed:', error); const detail = describeError(error); feedback.textContent = 'Unable to join queue (' + detail + '). Please try again in a moment.'; } finally { if (submitButton && !submissionCompleted) { submitButton.disabled = false; submitButton.removeAttribute('aria-busy'); } if (!submissionCompleted) { isSubmitting = false; } } }); }); https://berrylensestm.ph/page-sitemap.xml 2026-04-18T05:39:35+00:00 https://berrylensestm.ph/product-sitemap.xml 2026-09-08T07:58:22+00:00 https://berrylensestm.ph/faq-sitemap.xml 2025-07-13T04:15:11+00:00 https://berrylensestm.ph/portfolio-sitemap.xml 2013-12-23T13:53:18+00:00 https://berrylensestm.ph/testimonial-sitemap.xml 2022-04-10T11:11:01+00:00