/* CONFIG */ var N8N_BASE = 'https://n8n.justedo.ca'; var STAFF_SECRET = 'kaname_staff_ops_v1_c9bc9ccedaea0e8d'; var CHECKED_OUT_STAGES = { 'fs0z78GJdzzgH9ih5GTK':'c7fc4709-e925-4b25-893d-4b7eccc42317', 'UqA1X5l5M44bmR4AHMUq':'16b76842-c7bf-43e9-ad60-daebabb418ff', 'ZHjUiZ0vY2hfoS8bjFN4':'bae32eb5-69d0-45ac-9c4e-67f5f43a3e8c', 'rLD6NbyZqDU1pucQ9a3l':'6f67e3dd-1dad-45fa-b78d-1eaf75634b47', 'FivMjVYbCX1s9MtG2san':'3d22fdb7-f1f7-48f9-8ac1-bd0bb0bfafcf', '6wWyqcKcZLbar7KMDaVM':'2ff4b199-2df3-4855-a0cd-00af24b7abff', 'EkADsEd5y64yx33if2Mz':'1472d8e7-51c2-4c74-a3da-61d9381e92b8' }; var NO_SHOW_STAGES = { 'fs0z78GJdzzgH9ih5GTK':'b823f9f2-b52d-40ff-b6db-9f9dc5b16f17', 'UqA1X5l5M44bmR4AHMUq':'7577f907-8b07-4665-a36c-4682499bf958', 'ZHjUiZ0vY2hfoS8bjFN4':'4ec2831c-450d-4820-99a1-f2cf8a4a4058', 'rLD6NbyZqDU1pucQ9a3l':'40b41a0a-fc62-47e6-bcf5-1efb2209e5a3', 'FivMjVYbCX1s9MtG2san':'560b9961-ef3b-4a3c-84e4-d2dafabf719c', '6wWyqcKcZLbar7KMDaVM':'ab89f12e-6c57-4a24-8eba-a7ff5478f26a', 'EkADsEd5y64yx33if2Mz':'f257048d-4ba3-4336-8173-824218e5242e' }; var OVERDUE_STAGES = { 'fs0z78GJdzzgH9ih5GTK':'4cd319b5-d07c-463f-bd5f-df8341e7b3b9', 'UqA1X5l5M44bmR4AHMUq':'458a3821-a8be-4452-8a57-a9914b10be30', 'ZHjUiZ0vY2hfoS8bjFN4':'42cae546-95a1-457a-8582-24e8aaea827e', 'rLD6NbyZqDU1pucQ9a3l':'3074317c-d1c4-48c9-9859-318c8a85e1e8', 'FivMjVYbCX1s9MtG2san':'3ac2e7d5-e2a1-4fd2-bbe6-7670c2e2fa72', '6wWyqcKcZLbar7KMDaVM':'82c187e6-4398-4a84-bc8c-37e246e03890', 'EkADsEd5y64yx33if2Mz':'33b098e7-46f8-4d16-b384-a9a8715584f2' }; var DOG_FALLBACK = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='50' fill='%23e8e8e0'/%3E%3Ctext x='50' y='55' text-anchor='middle' font-size='40' fill='%23999'%3E%F0%9F%90%95%3C/text%3E%3C/svg%3E"; /* STATE */ var state = { location: 'both', boardData: null, scheduleData: null, products: [], checkoutDog: null, checkoutItems: [], paymentMethod: null, receiptEmail: true, receiptSms: true, incidentOn: false, usePackage: false, checkoutMode: 'standard', packageInfo: null, confirmTimers: {}, refreshTimer: null, boardDate: null }; /* Board date — defaults to today, can be overridden via date picker */ function getBoardDate() { if (state.boardDate) return state.boardDate; var now = new Date(); var y = now.getFullYear(); var m = String(now.getMonth() + 1).padStart(2, '0'); var d = String(now.getDate()).padStart(2, '0'); return y + '-' + m + '-' + d; } function setBoardDate(dateStr) { state.boardDate = dateStr; var today = new Date(); var todayStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0'); if (dateStr === todayStr) state.boardDate = null; updateDateDisplay(); refreshAll(); } function updateDateDisplay() { var dateStr = getBoardDate(); var d = new Date(dateStr + 'T12:00:00'); var days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; var months = ['January','February','March','April','May','June','July','August','September','October','November','December']; var dashDateEl = document.getElementById('dashDate'); if (dashDateEl) { var label = days[d.getDay()] + ', ' + months[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear(); if (state.boardDate) label += ' (viewing different date)'; dashDateEl.textContent = label; } } /* Strip PetExec disambiguation suffixes: "Roma 3090" → "Roma", "Miso_6442989" → "Miso" */ function cleanPetDisplayName(name) { if (!name) return ''; return ('' + name).replace(/[\s_]+\d{4,}$/, '').trim(); } /* parseDog */ function parseDog(dog) { // Priority: pet_name (from enrichment) > dog_name (opp custom field) > opp name parsing > contact name fallback var name = dog.pet_name || dog.dog_name || ''; var service = dog.service_type || ''; // If name contains " — " separator (opp name format "BuddyBoo — Full Day Daycamp"), split var parts = name.split(' \u2014 '); if (parts.length >= 2) { name = parts[0].trim(); if (parts[1] && !/^\d{4}-\d{2}-\d{2}/.test(parts[1].trim())) { service = parts[1].trim(); } } // If still empty, try opp contactName or opportunityName if (!name && dog.contactName) { name = dog.contactName.split(' ')[0] + "'s dog"; } if (!name && dog.opportunityName) { var oppParts = dog.opportunityName.split(' \u2014 '); name = oppParts[0].trim(); if (oppParts[1] && !/^\d{4}-\d{2}-\d{2}/.test(oppParts[1].trim())) { service = service || oppParts[1].trim(); } } dog._parsedName = name || 'Unknown'; dog._parsedService = service || dog.service_type || ''; dog._needsEnrichment = !dog.pet_name && !dog.dog_name; return dog; } /* PIPELINE → SECTION MAPPING */ function sectionForPipeline(pipelineId) { var map = { 'fs0z78GJdzzgH9ih5GTK': 'daycare', 'UqA1X5l5M44bmR4AHMUq': 'daycare', 'ZHjUiZ0vY2hfoS8bjFN4': 'grooming', 'rLD6NbyZqDU1pucQ9a3l': 'grooming', 'EkADsEd5y64yx33if2Mz': 'boarding', 'FivMjVYbCX1s9MtG2san': 'training', '6wWyqcKcZLbar7KMDaVM': 'training' }; return map[pipelineId] || 'daycare'; } /* DEBOUNCED BACKGROUND SYNC */ var _bgSyncTimer = null; function scheduleBgSync() { if (_bgSyncTimer) clearTimeout(_bgSyncTimer); _bgSyncTimer = setTimeout(function() { _bgSyncTimer = null; refreshAll(); }, 5000); } /* RE-RENDER A SINGLE SECTION FROM LOCAL STATE */ function rerenderSection(pipelineId) { var section = sectionForPipeline(pipelineId); if (section === 'daycare') renderDaycare(); else if (section === 'grooming') renderGrooming(); else if (section === 'boarding') renderBoarding(); updateStats(); } /* FIND DOG IN LOCAL STATE BY OPP_ID — returns { dog, array, index } or null */ function findDogInState(oppId, pipelineId) { var section = sectionForPipeline(pipelineId); if (section === 'daycare') { var board = state.boardData || {}; var locs = ['east', 'west']; for (var l = 0; l < locs.length; l++) { var dogs = (board[locs[l]] && board[locs[l]].dogs) ? board[locs[l]].dogs : []; for (var i = 0; i < dogs.length; i++) { if (dogs[i].opp_id === oppId) return { dog: dogs[i], array: dogs, index: i }; } } } else { var sched = (state.scheduleData && state.scheduleData.schedule) ? state.scheduleData.schedule : {}; var svcKey = section; /* 'grooming' or 'boarding' */ var svcData = sched[svcKey] || {}; var sLocs = ['east', 'west']; for (var sl = 0; sl < sLocs.length; sl++) { var arr = svcData[sLocs[sl]] || []; for (var si = 0; si < arr.length; si++) { if (arr[si].opp_id === oppId) return { dog: arr[si], array: arr, index: si }; } } } return null; } /* LOCATION */ function setLocation(loc) { state.location = loc; try { localStorage.setItem('spk_ops_location', loc); } catch(e) {} var btns = document.querySelectorAll('.loc-btn'); for (var i = 0; i < btns.length; i++) { btns[i].classList[btns[i].getAttribute('data-loc') === loc ? 'add' : 'remove']('active'); } updateLocationVisibility(); updateStats(); renderDaycare(); renderGrooming(); renderTraining(); renderBoarding(); } function initLocation() { var saved = 'both'; try { saved = localStorage.getItem('spk_ops_location') || 'both'; } catch(e) {} state.location = saved; var btns = document.querySelectorAll('.loc-btn'); for (var i = 0; i < btns.length; i++) { if (btns[i].getAttribute('data-loc') === saved) btns[i].classList.add('active'); } } /* CLOCK */ function updateClock() { var now = new Date(); var days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; var h = now.getHours(); var m = now.getMinutes(); var ampm = h >= 12 ? 'PM' : 'AM'; h = h % 12 || 12; var mm = m < 10 ? '0' + m : '' + m; var clockEl = document.getElementById('topbarClock'); if (clockEl) clockEl.textContent = days[now.getDay()] + ' ' + months[now.getMonth()] + ' ' + now.getDate() + ' \u00b7 ' + h + ':' + mm + ' ' + ampm; } /* STATUS */ function setStatus(s) { var dot = document.getElementById('statusDot'); if (!dot) return; dot.className = 'status-dot' + (s !== 'ok' ? ' ' + s : ''); } /* TOAST */ function showToast(msg, type, duration) { type = type || 'info'; duration = duration || 3000; var container = document.getElementById('toastContainer'); var toast = document.createElement('div'); toast.className = 'toast ' + type; toast.textContent = msg; container.appendChild(toast); setTimeout(function() { if (toast.parentNode) toast.parentNode.removeChild(toast); }, duration); } /* WEBHOOK */ function callWebhook(path, payload, cb) { fetch(N8N_BASE + path, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-kaname-staff-secret': STAFF_SECRET }, body: JSON.stringify(payload) }) .then(function(res) { return res.json(); }) .then(function(data) { if (Array.isArray(data)) data = data[0] || {}; /* Detect _respondWith wrapper from n8n (responseNode mode) */ if (data && data._respondWith) { if (data._respondWith.statusCode >= 400) { var errMsg = (data._respondWith.body && data._respondWith.body.message) || 'Server error'; cb(new Error(errMsg), null); return; } /* Unwrap successful _respondWith responses */ if (data._respondWith.body) { data = data._respondWith.body; } } /* Detect status: 'error' from lastNode mode responses */ if (data && data.status === 'error') { cb(new Error(data.message || 'Server error'), null); return; } cb(null, data); }) .catch(function(err) { cb(err, null); }); } /* SECTION HELPERS */ function showLoading(s) { var L = document.getElementById(s + 'Loading'); var B = document.getElementById(s + 'Body'); var E = document.getElementById(s + 'Error'); if (L) L.style.display = 'flex'; if (B) B.style.display = 'none'; if (E) E.style.display = 'none'; } function showBody(s) { var L = document.getElementById(s + 'Loading'); var B = document.getElementById(s + 'Body'); var E = document.getElementById(s + 'Error'); if (L) L.style.display = 'none'; if (B) B.style.display = 'block'; if (E) E.style.display = 'none'; } function showError(s, msg) { var L = document.getElementById(s + 'Loading'); var B = document.getElementById(s + 'Body'); var E = document.getElementById(s + 'Error'); if (L) L.style.display = 'none'; if (B) B.style.display = 'none'; if (E) { E.style.display = 'block'; E.textContent = '\u26a0\ufe0f ' + msg; } } function setCount(id, n) { var el = document.getElementById(id); if (el) el.textContent = n; } /* FETCH BOARD */ function fetchBoard(cb) { setStatus('loading'); var payload = {}; if (state.boardDate) payload.board_date = state.boardDate; callWebhook('/webhook/staff-board', payload, function(err, data) { if (err) { setStatus('error'); cb(err, null); return; } state.boardData = data; setStatus('ok'); cb(null, data); }); } /* FETCH SCHEDULE */ function fetchSchedule(cb) { callWebhook('/webhook/staff-schedule', {}, function(err, data) { if (err) { cb(err, null); return; } state.scheduleData = data; cb(null, data); }); } /* PRODUCT CATALOG — hardcoded with GHL product IDs for tax config */ /* ghlId maps to GHL product so invoice inherits per-product tax (GST, GST+PST, exempt) */ /* Tax rates: Manitoba GST 5% on all services, PST 7% additional on grooming only */ var TAX_BY_GROUP = { 'Daycamp': { gst: 0.05, pst: 0 }, 'Boarding': { gst: 0.05, pst: 0 }, 'Full Groom \u2014 Standard': { gst: 0.05, pst: 0.07 }, 'Full Groom \u2014 Oodles': { gst: 0.05, pst: 0.07 }, 'Bath & Tidy \u2014 Standard': { gst: 0.05, pst: 0.07 }, 'Bath & Tidy \u2014 Oodles': { gst: 0.05, pst: 0.07 }, 'Bath & Shed-less': { gst: 0.05, pst: 0.07 }, 'Groom A-la-carte': { gst: 0.05, pst: 0.07 }, 'Add-on': { gst: 0.05, pst: 0 }, 'Fee': { gst: 0.05, pst: 0 }, 'Groom Fee': { gst: 0.05, pst: 0.07 } }; var PRODUCT_CATALOG = [ /* Daycamp */ { id: 'full_day', name: 'Full Day Daycamp', price: 38.15, group: 'Daycamp', ghlId: '68c34cff0c5c61a39567104d' }, { id: 'full_day_multi', name: 'Full Day Multi-Dog', price: 32.85, group: 'Daycamp', ghlId: '68c34e437ff433edbfcebd85' }, { id: 'full_day_puppy', name: 'Full Day Puppy', price: 31.80, group: 'Daycamp', ghlId: '68ddea8e22640a1c3fc37efb' }, { id: 'half_day', name: 'Half Day Daycamp', price: 28.00, group: 'Daycamp', ghlId: '68c34cffbc438e4a94d98c5c' }, { id: 'half_day_multi', name: 'Half Day Multi-Dog', price: 24.40, group: 'Daycamp', ghlId: '68c34e437ff433b4cccebd86' }, { id: 'power_hour', name: 'Power Hour', price: 19.60, group: 'Daycamp', ghlId: '68c34cff7ff4335fe6ce5290' }, { id: 'power_hour_multi', name: 'Power Hour Multi-Dog', price: 16.95, group: 'Daycamp', ghlId: '68c34e433834771a7a17d432' }, /* Boarding (East only) */ { id: 'board_1_first', name: 'Boarding 1 Night (First Dog)', price: 74.00, group: 'Boarding', ghlId: '68c35f337ff4336127d39c4a' }, { id: 'board_26_first', name: 'Boarding 2-6 Nights (First Dog)', price: 58.00, group: 'Boarding', ghlId: '68c361100c5c614de66cd3ed' }, { id: 'board_7_first', name: 'Boarding 7+ Nights (First Dog)', price: 53.00, group: 'Boarding', ghlId: '68c361e4f845915ff258665c' }, { id: 'board_1_add', name: 'Boarding 1 Night (Additional)', price: 53.00, group: 'Boarding', ghlId: '68c362760c5c6104896d2ffb' }, { id: 'board_26_add', name: 'Boarding 2-6 Nights (Additional)',price: 47.50, group: 'Boarding', ghlId: '68c362f0bc438e3f8edfe116' }, { id: 'board_7_add', name: 'Boarding 7+ Nights (Additional)', price: 42.50, group: 'Boarding', ghlId: '68c3632a0c5c6112d36d50c7' }, { id: 'board_deposit', name: 'Boarding Security Deposit', price: 74.00, group: 'Boarding', ghlId: '69c07284f1e32d65df63842f' }, /* Groom: Full Groom — Standard */ { id: 'fg_u10', name: 'Under 10lbs', price: 70.00, group: 'Full Groom — Standard', ghlId: '68bd11c195406d83f4d3b236' }, { id: 'fg_1030', name: '10-30lbs', price: 75.00, group: 'Full Groom — Standard', ghlId: '68bd114c2ed7998d97310629' }, { id: 'fg_3050', name: '30-50lbs', price: 90.00, group: 'Full Groom — Standard', ghlId: '68bd122295406d81d9d3ebfb' }, { id: 'fg_5070', name: '50-70lbs', price: 110.00, group: 'Full Groom — Standard', ghlId: '68bd124295406d41d4d3efb2' }, { id: 'fg_7090', name: '70-90lbs', price: 130.00,group: 'Full Groom — Standard', ghlId: '68bd12c02ed7997231318281' }, { id: 'fg_90p', name: '90lbs+', price: 150.00,group: 'Full Groom — Standard', ghlId: '68bd1398e91a395567544454' }, /* Groom: Full Groom — Oodles */ { id: 'fgo_u10', name: 'Under 10lbs', price: 75.00, group: 'Full Groom — Oodles', ghlId: '68bf5ce6a177e8cafe816d6b' }, { id: 'fgo_1030', name: '10-30lbs', price: 80.00, group: 'Full Groom — Oodles', ghlId: '68bf5ce62ed799b28ffbdb27' }, { id: 'fgo_3050', name: '30-50lbs', price: 100.00, group: 'Full Groom — Oodles', ghlId: '68bf5ce6b2dce788239ff0f0' }, { id: 'fgo_5070', name: '50-70lbs', price: 120.00,group: 'Full Groom — Oodles', ghlId: '68bf5ce695406d3750a0def5' }, { id: 'fgo_7090', name: '70-90lbs', price: 130.00,group: 'Full Groom — Oodles', ghlId: '68bf5ce6e91a3973cf1f622b' }, { id: 'fgo_90p', name: '90lbs+', price: 150.00,group: 'Full Groom — Oodles', ghlId: '68bf5ce695406db5caa0def4' }, /* Groom: Bath & Tidy — Standard */ { id: 'bt_u10', name: 'Under 10lbs', price: 65.00, group: 'Bath & Tidy — Standard',ghlId: '68bf6315b2dce71dcaa4020d' }, { id: 'bt_1030', name: '10-30lbs', price: 70.00, group: 'Bath & Tidy — Standard',ghlId: '68bf6315e91a3996d3236c21' }, { id: 'bt_3050', name: '30-50lbs', price: 85.00, group: 'Bath & Tidy — Standard',ghlId: '68bf631595406d8038a4d4ef' }, { id: 'bt_5070', name: '50-70lbs', price: 100.00, group: 'Bath & Tidy — Standard',ghlId: '68bf63152ed7997fe0ffe52f' }, { id: 'bt_7090', name: '70-90lbs', price: 120.00, group: 'Bath & Tidy — Standard',ghlId: '68bf6315a177e896f2855e42' }, { id: 'bt_90p', name: '90lbs+', price: 130.00, group: 'Bath & Tidy — Standard',ghlId: '68bf6315a177e823ee855e41' }, /* Groom: Bath & Tidy — Oodles */ { id: 'bto_u10', name: 'Under 10lbs', price: 70.00, group: 'Bath & Tidy — Oodles', ghlId: '68bf63df2ed79980cd000a36' }, { id: 'bto_1030', name: '10-30lbs', price: 75.00, group: 'Bath & Tidy — Oodles', ghlId: '68bf63dfe91a39ba792391a6' }, { id: 'bto_3050', name: '30-50lbs', price: 90.00, group: 'Bath & Tidy — Oodles', ghlId: '68bf63dfe91a3982e12391a8' }, { id: 'bto_5070', name: '50-70lbs', price: 110.00, group: 'Bath & Tidy — Oodles', ghlId: '68bf63dfa177e868828585e9' }, { id: 'bto_7090', name: '70-90lbs', price: 130.00, group: 'Bath & Tidy — Oodles', ghlId: '68bf63df2ed79985db000a34' }, { id: 'bto_90p', name: '90lbs+', price: 150.00, group: 'Bath & Tidy — Oodles', ghlId: '68bf63dfa177e82ec88585eb' }, /* Groom: Bath & Shed-less (De-Shedding) */ { id: 'bs_u10', name: 'Under 10lbs', price: 55.00, group: 'Bath & Shed-less', ghlId: '68bf650ab2dce75fb1a45912' }, { id: 'bs_1030', name: '10-30lbs', price: 65.00, group: 'Bath & Shed-less', ghlId: '68bf650aa177e839a385b7e9' }, { id: 'bs_3050', name: '30-50lbs', price: 75.00, group: 'Bath & Shed-less', ghlId: '68bf650ab2dce74897a45911' }, { id: 'bs_5070', name: '50-70lbs', price: 90.00, group: 'Bath & Shed-less', ghlId: '68bf650a95406d7d29a52c3e' }, { id: 'bs_7090', name: '70-90lbs', price: 100.00, group: 'Bath & Shed-less', ghlId: '68bf650ae91a392fcf23c44a' }, { id: 'bs_90p', name: '90lbs+', price: 120.00, group: 'Bath & Shed-less', ghlId: '68bf650a2ed799f2fa003db6' }, /* Groom: A-la-carte */ { id: 'bff', name: 'BFF (Bum/Face/Feet)', price: 35.00, group: 'Groom A-la-carte', ghlId: '68bf6d552ed7992b84024867' }, { id: 'puppy_groom',name: 'Puppy Intro Groom', price: 45.00, group: 'Groom A-la-carte', ghlId: '68bf6c77a177e8a16c878a72' }, { id: 'puppy_bath', name: 'Puppy Intro Bath', price: 40.00, group: 'Groom A-la-carte', ghlId: '68bf6c8a95406d337aa70921' }, { id: 'face_tidy', name: 'Face Tidy', price: 15.00, group: 'Groom A-la-carte', ghlId: '68bf6fafa177e84b04885ae8' }, { id: 'pad_shave', name: 'Pad Shave', price: 5.00, group: 'Groom A-la-carte', ghlId: '68bf70eea177e86e2e8912d0' }, { id: 'ear_clean', name: 'Ear Cleaning & Plucking', price: 15.00, group: 'Groom A-la-carte', ghlId: '68bf708cb2dce77bffa776a1' }, { id: 'sani_trim', name: 'Sani-Trim', price: 5.00, group: 'Groom A-la-carte', ghlId: '68bf715de91a39529127684e' }, { id: 'brush_out', name: 'Brush Out', price: 15.00, group: 'Groom A-la-carte', ghlId: '68bf71b6a177e80791896727' }, { id: 'hair_dye', name: 'Hair Dye/Colour (quoted)', price: 0.00, group: 'Groom A-la-carte', ghlId: '68bd0e69e91a3919475331db' }, { id: 'desensitize', name:'Desensitization (quoted)', price: 0.00, group: 'Groom A-la-carte', ghlId: '68dc9b4dbff2494d94e0dd90' }, { id: 'pawdicure', name: 'Pawdicure', price: 35.00, group: 'Groom A-la-carte', ghlId: '68bf6def2ed7997b33025eeb' }, /* Add-ons */ { id: 'nail_trim_dc', name: 'Nail Trim (Daycare Dog)', price: 15.00, group: 'Add-on', ghlId: '699b95edc060745009a7943d' }, { id: 'nail_trim_walkin', name: 'Nail Trim (Walk-in)', price: 20.00, group: 'Add-on', ghlId: '68bf6f2de91a396fa5262aac' }, { id: 'nail_polish', name: 'Nail Trim w/ Polish', price: 25.00, group: 'Add-on', ghlId: '68bf6f86e91a396eff26547c' }, { id: 'nail_plain', name: 'Nail Polish Only', price: 10.00, group: 'Add-on', ghlId: '68bd04d395406dff94d1686f' }, { id: 'temp_test', name: 'Temperament Test', price: 0.00, group: 'Add-on', ghlId: '68bf7aeba177e83e178ba619' }, /* Fees */ { id: 'late_pickup', name: 'Late Pickup (per 15 min)',price: 10.00, group: 'Fee', ghlId: '69d695553b93df491268fa12' }, { id: 'cancel_fee', name: 'Cancellation Fee (<12hr)',price: 25.00, group: 'Fee', ghlId: '68bd0efd2ed799355a30c2e5' }, { id: 'noshow_fee', name: 'No Show Fee', price: 38.15, group: 'Fee', ghlId: '68bd0ea8e91a39e75153398e' }, { id: 'bite_fee', name: 'Bite Fee (per puncture)', price: 50.00, group: 'Fee', ghlId: '68bd0f50e91a39ef6753485f' }, { id: 'rebathe_fee', name: 'Re-Bathe Fee', price: 10.00, group: 'Groom Fee',ghlId: '68bd0ebd2ed7998b0c30be4b' }, { id: 'flea_bath', name: 'Flea Bath + Shop Cleaning',price: 50.00,group: 'Groom Fee',ghlId: '68bd0588e91a396cf25180ef' }, { id: 'demat_5min', name: 'De-matting (per 5-min block)',price: 5.00,group: 'Groom Fee',ghlId: '68bd062295406d25bad1b1d0' }, { id: 'depelt_fee', name: 'De-Pelting Fee (Severe)', price: 50.00, group: 'Groom Fee',ghlId: '68bd066095406dce67d1c405' }, { id: 'skunk_bath', name: 'Skunk / Strong Odor Bath', price: 30.00,group: 'Groom Fee',ghlId: '68bd04fc2ed79944922edd41' } ]; var productPriceMap = {}; function populateProductSelect() { var sel = document.getElementById('productSelect'); if (!sel) return; while (sel.options.length > 1) sel.remove(1); productPriceMap = {}; var lastGroup = ''; for (var i = 0; i < PRODUCT_CATALOG.length; i++) { var p = PRODUCT_CATALOG[i]; productPriceMap[p.id] = { name: p.name, price: p.price, ghlId: p.ghlId || '', group: p.group }; if (p.group !== lastGroup) { var optGroup = document.createElement('optgroup'); optGroup.label = p.group; sel.appendChild(optGroup); lastGroup = p.group; } var opt = document.createElement('option'); opt.value = p.id; opt.textContent = p.name + ' \u2014 $' + p.price.toFixed(2); (sel.lastElementChild.tagName === 'OPTGROUP' ? sel.lastElementChild : sel).appendChild(opt); } } /* UPDATE STATS — v5 inline header format */ function updateStats() { var board = state.boardData || {}; var sched = (state.scheduleData && state.scheduleData.schedule) ? state.scheduleData.schedule : {}; var loc = state.location; var c = board.counts || {}; /* per-service counts from board API v2 */ /* Helper: sum a count field across locations based on current filter */ function sumCount(service, field) { var total = 0; if (loc === 'east' || loc === 'both') total += (c.east && c.east[service]) ? (c.east[service][field] || 0) : 0; if (loc === 'west' || loc === 'both') total += (c.west && c.west[service]) ? (c.west[service][field] || 0) : 0; return total; } /* --- Dashboard location label + date --- */ var locNames = { east: 'East \u2014 975 Thomas Ave', west: 'West \u2014 360 Keewatin St', both: 'Both Locations' }; var dashLocEl = document.getElementById('dashLocName'); if (dashLocEl) dashLocEl.textContent = locNames[loc] || 'Both Locations'; var now = new Date(); var days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; var months = ['January','February','March','April','May','June','July','August','September','October','November','December']; var dashDateEl = document.getElementById('dashDate'); if (dashDateEl) { var dateLabel = days[now.getDay()] + ', ' + months[now.getMonth()] + ' ' + now.getDate() + ', ' + now.getFullYear(); if (state.boardDate) { var bd = new Date(state.boardDate + 'T12:00:00'); dateLabel = days[bd.getDay()] + ', ' + months[bd.getMonth()] + ' ' + bd.getDate() + ', ' + bd.getFullYear() + ' (viewing different date)'; } dashDateEl.textContent = dateLabel; dashDateEl.style.cursor = 'pointer'; dashDateEl.title = 'Click to view a different date'; /* Add hidden date input if not already present */ if (!document.getElementById('spkDatePicker')) { var picker = document.createElement('input'); picker.type = 'date'; picker.id = 'spkDatePicker'; picker.style.cssText = 'position:absolute;opacity:0;width:0;height:0;pointer-events:none;'; picker.value = getBoardDate(); dashDateEl.parentNode.style.position = 'relative'; dashDateEl.parentNode.appendChild(picker); dashDateEl.addEventListener('click', function() { picker.style.pointerEvents = 'auto'; picker.showPicker ? picker.showPicker() : picker.click(); }); picker.addEventListener('change', function() { picker.style.pointerEvents = 'none'; if (this.value) setBoardDate(this.value); }); } } /* --- Per-service counts from board API v3 --- */ var dcArriving = sumCount('daycamp', 'arriving'); var dcIn = sumCount('daycamp', 'checked_in'); var dcOut = sumCount('daycamp', 'checked_out'); var grArriving = sumCount('grooming', 'arriving'); var grIn = sumCount('grooming', 'checked_in'); var grOut = sumCount('grooming', 'checked_out'); var trIn = sumCount('training', 'checked_in'); var trOut = sumCount('training', 'checked_out'); var bdIn = sumCount('boarding', 'checked_in'); var bdOut = sumCount('boarding', 'checked_out'); /* Expected = arriving (from board) + already checked in */ var dcExpected = dcArriving; /* Grooming booked = arriving (from board) + in progress */ var grBooked = grArriving; /* Training from schedule */ var trClasses = 0; var trEnrolled = 0; var training = sched.training || {}; var trArr = []; if (loc === 'east' || loc === 'both') trArr = trArr.concat(training.east || []); if (loc === 'west' || loc === 'both') trArr = trArr.concat(training.west || []); trClasses = trArr.length; for (var t = 0; t < trArr.length; t++) { var en = trArr[t].enrollment_count !== undefined ? trArr[t].enrollment_count : (trArr[t].enrolled || 0); trEnrolled += (parseInt(en, 10) || 0); } /* Boarding stays from schedule — split by status */ var bdStays = 0; var bdArriving = 0; var bdDeparting = 0; var boarding = sched.boarding || {}; if (boarding.east) { for (var bi = 0; bi < boarding.east.length; bi++) { var bStatus = boarding.east[bi].status || ''; if (bStatus === 'arriving') bdArriving++; else if (bStatus === 'departing') { bdDeparting++; bdStays++; } else { bdStays++; } /* checked_in */ } } /* --- HERE NOW rollup = all checked-in across all services --- */ var hereNow = dcIn + grIn + bdIn; var checkedOutTotal = dcOut + grOut + trOut + bdOut; /* --- Populate dashboard --- */ setEl('dashHereNow', hereNow); setEl('dashCheckedOut', checkedOutTotal); /* Daycare: expected = arriving + already checked in */ setEl('dashDcExpected', dcExpected + dcIn); setEl('dashDcIn', dcIn); setEl('dashDcOut', dcOut); /* Grooming: booked (arriving + in progress) */ setEl('dashGrBooked', grBooked + grIn); setEl('dashGrIn', grIn); setEl('dashGrDone', grOut); /* Training */ setEl('dashTrClasses', trClasses); setEl('dashTrEnrolled', trEnrolled); /* Boarding */ setEl('dashBdStays', bdStays > 0 ? bdStays : bdIn); setEl('dashBdArriving', bdArriving); setEl('dashBdDeparting', bdDeparting); /* Section counts */ setCount('daycareCount', dcIn); setCount('groomingCount', grBooked + grIn); setCount('trainingCount', trClasses); setCount('boardingCount', bdStays + bdArriving); } function setEl(id, val) { var el = document.getElementById(id); if (el) el.textContent = val; } /* SVC PILL HELPERS */ function svcPillClass(svc, cat) { svc = (svc || '').toLowerCase(); cat = (cat || '').toLowerCase(); if (cat === 'grooming' || svc.indexOf('groom') > -1) return 'grooming'; if (cat === 'training' || svc.indexOf('train') > -1) return 'training'; if (cat === 'boarding' || svc.indexOf('board') > -1) return 'boarding'; if (svc.indexOf('full') > -1) return 'daycare-full'; if (svc.indexOf('half') > -1) return 'daycare-half'; if (svc.indexOf('power') > -1 || svc.indexOf('hour') > -1) return 'daycare-power'; if (cat === 'daycare' || cat === 'daycamp') return 'daycare-full'; return 'default'; } function svcPillLabel(svc, cat) { svc = (svc || '').toLowerCase(); cat = (cat || '').toLowerCase(); if (cat === 'grooming' || svc.indexOf('groom') > -1) return 'Grooming'; if (cat === 'training' || svc.indexOf('train') > -1) return 'Training'; if (cat === 'boarding' || svc.indexOf('board') > -1) return 'Boarding'; if (svc.indexOf('full') > -1) return 'Full Day'; if (svc.indexOf('half') > -1) return 'Half Day'; if (svc.indexOf('power') > -1 || svc.indexOf('hour') > -1) return 'Power Hour'; if (cat === 'daycare' || cat === 'daycamp') return 'Daycare'; return svc || 'Day'; } /* TIME PARSE */ function parseTime(raw) { if (!raw) return { display: '\u2014', hour: '\u2014', min: '00', ampm: '' }; var str = '' + raw; if (str.indexOf('T') > -1) { var d = new Date(str); if (!isNaN(d.getTime())) { var h = d.getHours(); var m = ('0' + d.getMinutes()).slice(-2); var ampm = h >= 12 ? 'PM' : 'AM'; h = h % 12 || 12; return { display: h + ':' + m + ' ' + ampm, hour: h, min: m, ampm: ampm }; } } var match = str.match(/(\d+):(\d+)/); if (!match) return { display: str, hour: str, min: '00', ampm: '' }; var h = parseInt(match[1], 10); var m = match[2]; var ampm = h >= 12 ? 'PM' : 'AM'; h = h % 12 || 12; return { display: h + ':' + m + ' ' + ampm, hour: h, min: m, ampm: ampm }; } /* SAFETY BADGE HELPER */ function safetyBadge(status) { var el = document.createElement('span'); el.setAttribute('role', 'status'); var s = (status || '').toLowerCase(); if (s === 'pass' || s === 'passed') { el.className = 'safety-badge pass'; el.textContent = 'PASS'; el.setAttribute('aria-label', 'Safety status: passed'); } else if (s === 'review' || s === 'conditional') { el.className = 'safety-badge review'; el.textContent = 'REVIEW'; el.setAttribute('aria-label', 'Safety status: review required'); } else if (s === 'hold' || s === 'failed') { el.className = 'safety-badge hold'; el.textContent = 'HOLD'; el.setAttribute('aria-label', 'Safety status: hold — do not check in'); } else { el.className = 'safety-badge unknown'; el.textContent = 'PENDING'; el.setAttribute('aria-label', 'Safety status: pending evaluation'); } return el; } /* DOCS BADGE HELPER */ function docsBadge(dog) { if (!dog.has_unsigned_contracts) return null; var el = document.createElement('span'); el.className = 'docs-badge'; el.textContent = 'DOCS'; el.title = 'Unsigned contracts on file'; return el; } /* GENDER LABEL HELPER */ function genderLabel(g) { if (Array.isArray(g)) g = g[0] || ''; var s = (g || '').toLowerCase(); if (s === 'male' || s === 'm') return 'Good Boy'; if (s === 'female' || s === 'f') return 'Good Girl'; return ''; } /* WEIGHT CLASS HELPER */ function weightClass(lbs) { if (!lbs) return ''; var w = parseFloat(lbs); if (isNaN(w)) return ''; if (w < 15) return 'XS'; if (w < 30) return 'S'; if (w < 55) return 'M'; if (w < 90) return 'L'; return 'XL'; } function lbsToKg(lbs) { var n = parseFloat(lbs); return isNaN(n) ? '' : (Math.round(n * 0.453592 * 10) / 10).toString(); } function formatWeight(lbs) { if (lbs === null || lbs === undefined || lbs === '') return ''; var kg = lbsToKg(lbs); return kg ? lbs + ' lbs (' + kg + ' kg)' : lbs + ' lbs'; } /* CARE FLAGS HELPER */ function careFlags(dog) { var flags = []; var isTruthy = function(v) { return v === true || v === 'true' || v === 'Yes' || v === 'yes' || v === '1'; }; var isFalsy = function(v) { return v === false || v === 'false' || v === 'No' || v === 'no'; }; if (isTruthy(dog.medication_required)) flags.push({ cls: 'meds', label: 'MEDS' }); if (isTruthy(dog.feeding_required)) flags.push({ cls: 'feed', label: 'FEED' }); if (isFalsy(dog.allowed_treats)) flags.push({ cls: 'notreats', label: 'NO TREATS' }); if (dog.lunch_nap && dog.lunch_nap !== '' && dog.lunch_nap !== 'No' && dog.lunch_nap !== 'no' && dog.lunch_nap !== 'false') { flags.push({ cls: 'nap', label: 'NAP' }); } if (flags.length === 0) return null; var row = document.createElement('div'); row.className = 'care-flags'; for (var i = 0; i < flags.length; i++) { var f = document.createElement('span'); f.className = 'care-flag ' + flags[i].cls; f.textContent = flags[i].label; row.appendChild(f); } return row; } /* PACKAGE BADGE HELPER */ function packageBadge(dog) { var el = document.createElement('span'); var pkgStatus = (dog.package_status || '').toLowerCase(); var remaining = dog.package_remaining; if (pkgStatus === 'unlimited') { el.className = 'pkg-pill unlimited'; el.textContent = 'Unlimited'; } else if (pkgStatus === 'package' && remaining != null) { el.className = 'pkg-pill passes'; el.textContent = remaining + (remaining == 1 ? ' pass' : ' passes'); } else { el.className = 'pkg-pill payg'; el.textContent = 'Pay-as-you-go'; } return el; } /* FORMAT PHONE FOR DISPLAY */ function formatPhoneDisplay(raw) { if (!raw) return ''; var d = ('' + raw).replace(/[^0-9]/g, ''); if (d.length === 11 && d[0] === '1') d = d.slice(1); if (d.length === 10) return '(' + d.slice(0,3) + ') ' + d.slice(3,6) + '-' + d.slice(6); return raw; } /* Format a combined "Name — phone" emergency contact string. Customer portal Update Profile saves emergency_contact as `ecName + ' — ' + ecPhone` (raw digits). On read, split and format the phone half. */ function formatEmergencyContactDisplay(value) { if (!value) return ''; var s = String(value); var m = s.match(/^(.+?)\s*—\s*(.+)$/); if (!m) return s; var name = m[1].trim(); var phonePart = m[2].trim(); var formatted = formatPhoneDisplay(phonePart); return name + ' — ' + formatted; } function formatPhoneInput(v) { var d = v.replace(/\D/g, ''); if (d.length > 10) d = d.slice(0, 10); if (d.length >= 7) return '(' + d.slice(0,3) + ') ' + d.slice(3,6) + '-' + d.slice(6); if (d.length >= 4) return '(' + d.slice(0,3) + ') ' + d.slice(3); if (d.length > 0) return '(' + d; return ''; } function bindPhoneField(id) { var el = document.getElementById(id); if (!el) return; el.addEventListener('input', function() { var pos = this.selectionStart; var before = this.value.length; this.value = formatPhoneInput(this.value); var after = this.value.length; this.setSelectionRange(pos + (after - before), pos + (after - before)); }); if (el.value) el.value = formatPhoneInput(el.value); } /* BUILD DOG CARD — v5 stacked layout */ function buildDogCard(dog, statusGroup, pipelineId) { dog = parseDog(dog); var isCheckedIn = (statusGroup === 'checked_in'); var safetyStatus = (dog.safety_status || '').toLowerCase(); var card = document.createElement('div'); var cardClass = 'dog-card ' + (isCheckedIn ? 'checked-in' : 'arriving'); if (safetyStatus === 'hold' || safetyStatus === 'failed') cardClass += ' safety-hold'; else if (safetyStatus === 'review' || safetyStatus === 'conditional') cardClass += ' safety-review'; card.className = cardClass; /* Top row: safety badge LEFT, action button RIGHT */ var topRow = document.createElement('div'); topRow.className = 'dog-card-top'; var topLeft = document.createElement('div'); topLeft.className = 'dog-card-top-left'; topLeft.appendChild(safetyBadge(dog.safety_status)); var db = docsBadge(dog); if (db) topLeft.appendChild(db); topRow.appendChild(topLeft); var actions = document.createElement('div'); actions.className = 'dog-actions'; if (!isCheckedIn) { var btn = document.createElement('button'); btn.className = 'btn btn-checkin'; btn.textContent = 'Check In'; btn.setAttribute('data-state', 'idle'); btn.onclick = (function(b, d, pid) { return function() { handleCheckin(b, d, pid); }; })(btn, dog, pipelineId); actions.appendChild(btn); var nsBtn = document.createElement('button'); nsBtn.className = 'btn btn-noshow'; nsBtn.textContent = 'No Show'; nsBtn.setAttribute('data-state', 'idle'); nsBtn.onclick = (function(b, d, pid) { return function() { handleNoShow(b, d, pid); }; })(nsBtn, dog, pipelineId); actions.appendChild(nsBtn); } else { var btn2 = document.createElement('button'); btn2.className = 'btn btn-checkout'; btn2.textContent = 'Check Out'; btn2.setAttribute('data-pipeline-id', pipelineId || ''); btn2.onclick = (function(d, pid) { return function() { d.pipeline_id = pid; openCheckoutPanel(d); }; })(dog, pipelineId); actions.appendChild(btn2); var odBtn = document.createElement('button'); odBtn.className = 'btn btn-overdue'; odBtn.textContent = 'Overdue'; odBtn.setAttribute('data-state', 'idle'); odBtn.onclick = (function(b, d, pid) { return function() { handleOverdue(b, d, pid); }; })(odBtn, dog, pipelineId); actions.appendChild(odBtn); } topRow.appendChild(actions); card.appendChild(topRow); /* Body row: photo + info — clickable for modal */ var bodyRow = document.createElement('div'); bodyRow.className = 'dog-card-body clickable'; bodyRow.setAttribute('role', 'button'); bodyRow.setAttribute('tabindex', '0'); bodyRow.setAttribute('aria-label', 'View details for ' + (dog._parsedName || 'this dog')); (function(d) { bodyRow.onclick = function() { openPetModal(d); }; bodyRow.onkeydown = function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openPetModal(d); } }; })(dog); var photo = document.createElement('img'); photo.className = 'dog-photo'; photo.alt = dog._parsedName; photo.src = dog.photo_url || DOG_FALLBACK; photo.onerror = function() { this.src = DOG_FALLBACK; }; bodyRow.appendChild(photo); var info = document.createElement('div'); info.className = 'dog-info'; /* Name */ var nameEl = document.createElement('div'); nameEl.className = 'dog-name'; nameEl.textContent = dog._parsedName || 'Unknown'; info.appendChild(nameEl); /* Birthday check — flag if today is the dog's birthday */ if (dog.birthday) { var today = new Date(); var bParts = dog.birthday.split('-'); if (bParts.length === 3 && parseInt(bParts[1]) === (today.getMonth() + 1) && parseInt(bParts[2]) === today.getDate()) { card.classList.add('birthday-today'); var bdBadge = document.createElement('span'); bdBadge.className = 'birthday-badge'; var birthYear = parseInt(bParts[0]); var turningAge = today.getFullYear() - birthYear; bdBadge.textContent = 'Birthday! Turning ' + turningAge; info.appendChild(bdBadge); } } /* Owner + phone */ var ownerRow = document.createElement('div'); ownerRow.className = 'dog-owner-row'; var ownerName = document.createElement('span'); ownerName.textContent = dog.owner_name || dog.owner_last_name || ''; ownerRow.appendChild(ownerName); var ownerPhone = dog.owner_phone || ''; if (ownerPhone) { var phoneSep = document.createTextNode(' \u00b7 '); ownerRow.appendChild(phoneSep); var phoneLink = document.createElement('a'); phoneLink.className = 'owner-phone-link'; phoneLink.href = 'tel:' + ownerPhone; phoneLink.textContent = formatPhoneDisplay(ownerPhone); phoneLink.onclick = function(e) { e.stopPropagation(); }; ownerRow.appendChild(phoneLink); } info.appendChild(ownerRow); /* Meta: service pill + package pill + time */ var metaEl = document.createElement('div'); metaEl.className = 'dog-meta'; var svcEl = document.createElement('span'); svcEl.className = 'svc-pill ' + svcPillClass(dog._parsedService, dog.service_category); svcEl.textContent = svcPillLabel(dog._parsedService, dog.service_category); metaEl.appendChild(svcEl); /* Package badge (daycare only) */ var cat = (dog.service_category || '').toLowerCase(); if (cat === 'daycamp' || cat === 'daycare') { metaEl.appendChild(packageBadge(dog)); } /* Gender + weight class pills */ var gl = genderLabel(dog.gender); if (gl) { var gPill = document.createElement('span'); gPill.className = 'weight-pill'; gPill.textContent = gl; metaEl.appendChild(gPill); } var wc = weightClass(dog.weight_lbs); if (wc) { var wPill = document.createElement('span'); wPill.className = 'weight-pill'; wPill.textContent = wc; metaEl.appendChild(wPill); } /* Time — show check-in time for checked-in dogs, appointment time for arriving */ if (isCheckedIn) { var ciTimeVal = dog.check_in_time || dog.checkin_time || ''; if (ciTimeVal) { var ciPt = parseTime(ciTimeVal); if (ciPt.hour !== '\u2014') { var ciEl = document.createElement('span'); ciEl.className = 'dog-time checked-in-time'; ciEl.textContent = '\u2705 In ' + ciPt.display; metaEl.appendChild(ciEl); } } } else { var apptTime = dog.appt_time || dog.check_in_time || dog.checkin_time || ''; if (apptTime) { var pt = parseTime(apptTime); if (pt.hour !== '\u2014') { var timeEl = document.createElement('span'); timeEl.className = 'dog-time'; timeEl.textContent = '\u23f0 ' + pt.display; metaEl.appendChild(timeEl); } } } info.appendChild(metaEl); /* Play area pills (compact, max 3 + overflow) */ var playAreas = dog.play_areas || dog.assigned_yards || []; if (typeof playAreas === 'string' && playAreas) { playAreas = playAreas.split(',').map(function(s) { return s.trim(); }).filter(Boolean); } if (playAreas && playAreas.length > 0) { var playRow = document.createElement('div'); playRow.className = 'play-area-row'; var maxShow = 3; for (var pa = 0; pa < Math.min(playAreas.length, maxShow); pa++) { var pill = document.createElement('span'); var areaName = playAreas[pa]; var isWestArea = /west|w$/i.test(areaName); pill.className = 'play-pill ' + (isWestArea ? 'west' : 'east'); pill.textContent = abbreviateArea(areaName); playRow.appendChild(pill); } if (playAreas.length > maxShow) { var morePill = document.createElement('span'); morePill.className = 'play-pill more'; morePill.textContent = '+' + (playAreas.length - maxShow) + ' more'; playRow.appendChild(morePill); } info.appendChild(playRow); } /* Care flags (MEDS / FEED / NO TREATS / NAP) */ var cf = careFlags(dog); if (cf) info.appendChild(cf); /* Special note one-liner */ if (dog.special_notes) { var noteEl = document.createElement('div'); noteEl.className = 'dog-special-note'; var noteText = dog.special_notes; noteEl.textContent = noteText.length > 70 ? noteText.substring(0, 70) + '\u2026' : noteText; noteEl.title = noteText; info.appendChild(noteEl); } bodyRow.appendChild(info); card.appendChild(bodyRow); return card; } /* RENDER DAYCARE */ function renderDaycare() { var board = state.boardData || {}; /* Filter to daycare only — grooming/boarding/training have their own sections */ function isDaycare(dog) { var cat = (dog.service_category || '').toLowerCase(); return !cat || cat === 'daycare' || cat === 'daycamp'; } var eastDogs = (board.east && board.east.dogs) ? board.east.dogs.map(parseDog).filter(isDaycare) : []; var westDogs = (board.west && board.west.dogs) ? board.west.dogs.map(parseDog).filter(isDaycare) : []; var loc = state.location; var body = document.getElementById('daycareBody'); if (!body) return; var total = 0; if (loc === 'east' || loc === 'both') total += eastDogs.length; if (loc === 'west' || loc === 'both') total += westDogs.length; setCount('daycareCount', total); while (body.firstChild) body.removeChild(body.firstChild); if (loc === 'both') { var cols = document.createElement('div'); cols.className = 'loc-columns'; var ec = document.createElement('div'); renderLocCol(ec, 'East', '975 Thomas Ave', eastDogs, 'fs0z78GJdzzgH9ih5GTK'); cols.appendChild(ec); var wc = document.createElement('div'); renderLocCol(wc, 'West', '360 Keewatin St', westDogs, 'UqA1X5l5M44bmR4AHMUq'); cols.appendChild(wc); body.appendChild(cols); } else { var col = document.createElement('div'); var isEast = (loc === 'east'); renderLocCol(col, isEast ? 'East' : 'West', isEast ? '975 Thomas Ave' : '360 Keewatin St', isEast ? eastDogs : westDogs, isEast ? 'fs0z78GJdzzgH9ih5GTK' : 'UqA1X5l5M44bmR4AHMUq'); body.appendChild(col); } showBody('daycare'); } function renderLocCol(container, name, addr, dogs, pipelineId) { var header = document.createElement('div'); header.className = 'loc-col-header'; var nameSpan = document.createElement('span'); nameSpan.textContent = name; header.appendChild(nameSpan); var addrSpan = document.createElement('span'); addrSpan.className = 'loc-col-addr'; addrSpan.textContent = addr; header.appendChild(addrSpan); container.appendChild(header); if (!dogs || dogs.length === 0) { var empty = document.createElement('div'); empty.className = 'section-empty'; empty.textContent = 'No dogs at ' + name + ' today.'; container.appendChild(empty); return; } var arriving = []; var checkedIn = []; for (var i = 0; i < dogs.length; i++) { if (dogs[i].status === 'checked_in' || dogs[i].is_checked_in) { checkedIn.push(dogs[i]); } else { arriving.push(dogs[i]); } } if (arriving.length > 0) { var ag = document.createElement('div'); ag.className = 'sub-group'; var al = document.createElement('div'); al.className = 'sub-group-label'; al.textContent = 'Arriving (' + arriving.length + ')'; ag.appendChild(al); var agGrid = document.createElement('div'); agGrid.className = 'dog-card-grid'; for (var j = 0; j < arriving.length; j++) agGrid.appendChild(buildDogCard(arriving[j], 'arriving', pipelineId)); ag.appendChild(agGrid); container.appendChild(ag); } if (checkedIn.length > 0) { var cig = document.createElement('div'); cig.className = 'sub-group'; var cil = document.createElement('div'); cil.className = 'sub-group-label'; cil.textContent = 'Checked In (' + checkedIn.length + ')'; cig.appendChild(cil); var ciGrid = document.createElement('div'); ciGrid.className = 'dog-card-grid'; for (var k = 0; k < checkedIn.length; k++) ciGrid.appendChild(buildDogCard(checkedIn[k], 'checked_in', pipelineId)); cig.appendChild(ciGrid); container.appendChild(cig); } } /* CHECK IN (two-tap) */ function handleCheckin(btn, dog, pipelineId) { var s = btn.getAttribute('data-state'); var oppId = dog.opp_id || ''; if (s === 'idle') { btn.textContent = 'Confirm?'; btn.className = 'btn btn-confirm'; btn.setAttribute('data-state', 'confirm'); var tid = setTimeout(function() { if (btn.getAttribute('data-state') === 'confirm') { btn.textContent = 'Check In'; btn.className = 'btn btn-checkin'; btn.setAttribute('data-state', 'idle'); } }, 4000); state.confirmTimers[oppId] = tid; } else if (s === 'confirm') { clearTimeout(state.confirmTimers[oppId]); btn.disabled = true; btn.textContent = '...'; btn.className = 'btn'; callWebhook('/webhook/staff-checkin', { opp_id: oppId, pipeline_id: pipelineId, checked_in_stage_id: dog.checked_in_stage_id || '' }, function(err) { if (err) { btn.disabled = false; btn.textContent = 'Check In'; btn.className = 'btn btn-checkin'; btn.setAttribute('data-state', 'idle'); showToast('Check-in failed \u2014 try again.', 'error'); return; } /* Optimistic UI: update local state + re-render section instantly */ var now = new Date(); var h = now.getHours(); var m = ('0' + now.getMinutes()).slice(-2); var ampm = h >= 12 ? 'PM' : 'AM'; h = h % 12 || 12; var checkinTimeStr = h + ':' + m + ' ' + ampm; var found = findDogInState(oppId, pipelineId); if (found) { found.dog.status = 'checked_in'; found.dog.is_checked_in = true; found.dog.check_in_time = checkinTimeStr; } rerenderSection(pipelineId); showToast((dog._parsedName || 'Dog') + ' checked in!', 'success'); scheduleBgSync(); }); } } /* NO SHOW (two-tap) */ function handleNoShow(btn, dog, pipelineId) { var s = btn.getAttribute('data-state'); var oppId = dog.opp_id || ''; var noShowStageId = (pipelineId && NO_SHOW_STAGES[pipelineId]) ? NO_SHOW_STAGES[pipelineId] : ''; if (s === 'idle') { btn.textContent = 'Confirm?'; btn.className = 'btn btn-confirm'; btn.setAttribute('data-state', 'confirm'); var tid = setTimeout(function() { if (btn.getAttribute('data-state') === 'confirm') { btn.textContent = 'No Show'; btn.className = 'btn btn-noshow'; btn.setAttribute('data-state', 'idle'); } }, 4000); state.confirmTimers['ns_' + oppId] = tid; } else if (s === 'confirm') { clearTimeout(state.confirmTimers['ns_' + oppId]); btn.disabled = true; btn.textContent = '...'; btn.className = 'btn'; callWebhook('/webhook/staff-checkin', { opp_id: oppId, pipeline_id: pipelineId, checked_in_stage_id: noShowStageId, action: 'no_show' }, function(err) { if (err) { btn.disabled = false; btn.textContent = 'No Show'; btn.className = 'btn btn-noshow'; btn.setAttribute('data-state', 'idle'); showToast('No Show failed \u2014 try again.', 'error'); return; } /* Optimistic UI: remove dog from local state + re-render */ var found = findDogInState(oppId, pipelineId); if (found) { found.array.splice(found.index, 1); } rerenderSection(pipelineId); showToast((dog._parsedName || 'Dog') + ' marked as No Show.', 'warning'); scheduleBgSync(); }); } } /* OVERDUE (two-tap) */ function handleOverdue(btn, dog, pipelineId) { var s = btn.getAttribute('data-state'); var oppId = dog.opp_id || ''; var overdueStageId = (pipelineId && OVERDUE_STAGES[pipelineId]) ? OVERDUE_STAGES[pipelineId] : ''; if (s === 'idle') { btn.textContent = 'Confirm?'; btn.className = 'btn btn-confirm'; btn.setAttribute('data-state', 'confirm'); var tid = setTimeout(function() { if (btn.getAttribute('data-state') === 'confirm') { btn.textContent = 'Overdue'; btn.className = 'btn btn-overdue'; btn.setAttribute('data-state', 'idle'); } }, 4000); state.confirmTimers['od_' + oppId] = tid; } else if (s === 'confirm') { clearTimeout(state.confirmTimers['od_' + oppId]); btn.disabled = true; btn.textContent = '...'; btn.className = 'btn'; callWebhook('/webhook/staff-checkin', { opp_id: oppId, pipeline_id: pipelineId, checked_in_stage_id: overdueStageId, action: 'overdue' }, function(err) { if (err) { btn.disabled = false; btn.textContent = 'Overdue'; btn.className = 'btn btn-overdue'; btn.setAttribute('data-state', 'idle'); showToast('Overdue failed \u2014 try again.', 'error'); return; } /* Optimistic UI: update local state + re-render */ var found = findDogInState(oppId, pipelineId); if (found) { found.dog.status = 'overdue'; } rerenderSection(pipelineId); showToast((dog._parsedName || 'Dog') + ' marked as Overdue. Owner notified.', 'warning'); scheduleBgSync(); }); } } /* RENDER GROOMING */ function renderGrooming() { var body = document.getElementById('groomingBody'); if (!body) return; var sched = (state.scheduleData && state.scheduleData.schedule) ? state.scheduleData.schedule : {}; var grooming = sched.grooming || {}; var loc = state.location; var all = []; if (loc === 'east' || loc === 'both') { var ea = grooming.east || []; for (var i = 0; i < ea.length; i++) { ea[i]._loc = 'East'; all.push(ea[i]); } } if (loc === 'west' || loc === 'both') { var wa = grooming.west || []; for (var j = 0; j < wa.length; j++) { wa[j]._loc = 'West'; all.push(wa[j]); } } all.sort(function(a, b) { return (a.appt_time || a.appointment_time || a.time || '').localeCompare(b.appt_time || b.appointment_time || b.time || ''); }); setCount('groomingCount', all.length); while (body.firstChild) body.removeChild(body.firstChild); if (all.length === 0) { var empty = document.createElement('div'); empty.className = 'section-empty'; empty.textContent = 'No grooming appointments today.'; body.appendChild(empty); showBody('grooming'); return; } var timeline = document.createElement('div'); timeline.className = 'grooming-timeline'; for (var k = 0; k < all.length; k++) timeline.appendChild(buildGroomingRow(all[k])); body.appendChild(timeline); showBody('grooming'); } function buildGroomingRow(appt) { appt = parseDog(appt); var isCheckedIn = appt.status === 'checked_in' || appt.is_checked_in; var pipelineId = appt._loc === 'West' ? 'rLD6NbyZqDU1pucQ9a3l' : 'ZHjUiZ0vY2hfoS8bjFN4'; var row = document.createElement('div'); row.className = 'grooming-row' + (isCheckedIn ? ' checked-in' : ''); var rawTime = isCheckedIn ? (appt.check_in_time || appt.appt_time || appt.appointment_time || appt.time || '') : (appt.appt_time || appt.appointment_time || appt.time || ''); var pt = parseTime(rawTime); var timeEl = document.createElement('div'); timeEl.className = 'grooming-time'; if (pt.hour !== '\u2014') { var timeNum = document.createTextNode(pt.hour + ':' + pt.min); timeEl.appendChild(timeNum); var ampmEl = document.createElement('span'); ampmEl.className = 'grooming-time-ampm'; ampmEl.textContent = pt.ampm; timeEl.appendChild(ampmEl); if (isCheckedIn && (appt.check_in_time || !appt.appt_time)) { var inLabel = document.createElement('div'); inLabel.style.cssText = 'font-size:.6rem;color:var(--green);font-weight:700;text-transform:uppercase;'; inLabel.textContent = 'In'; timeEl.appendChild(inLabel); } } else { timeEl.textContent = isCheckedIn ? '\u2705' : '\u2014'; } row.appendChild(timeEl); var divider = document.createElement('div'); divider.className = 'grooming-divider'; row.appendChild(divider); var photo = document.createElement('img'); photo.className = 'dog-photo'; photo.style.cssText = 'width:40px;height:40px;cursor:pointer'; photo.alt = appt._parsedName; photo.src = appt.photo_url || DOG_FALLBACK; photo.onerror = function() { this.src = DOG_FALLBACK; }; (function(d) { photo.onclick = function(e) { e.stopPropagation(); openPetModal(d); }; })(appt); row.appendChild(photo); var info = document.createElement('div'); info.className = 'grooming-info'; info.style.cursor = 'pointer'; (function(d) { info.onclick = function(e) { if (e.target.tagName !== 'BUTTON' && e.target.tagName !== 'A') openPetModal(d); }; })(appt); var nameRow = document.createElement('div'); nameRow.className = 'dog-name-row'; var nameEl = document.createElement('span'); nameEl.className = 'grooming-dog'; nameEl.textContent = appt._parsedName || 'Unknown'; nameRow.appendChild(nameEl); nameRow.appendChild(safetyBadge(appt.safety_status)); var grDb = docsBadge(appt); if (grDb) nameRow.appendChild(grDb); info.appendChild(nameRow); var detail = document.createElement('div'); detail.className = 'grooming-detail'; var parts = []; if (appt._parsedService || appt.service) parts.push(appt._parsedService || appt.service); if (appt.groomer_name || appt.staff_name) parts.push('with ' + (appt.groomer_name || appt.staff_name)); var ownerPart = appt.owner_last_name || appt.owner_name || ''; if (ownerPart) { var phonePart = appt.owner_phone ? ' ' + formatPhoneDisplay(appt.owner_phone) : ''; parts.push('\u2014 ' + ownerPart + phonePart); } if (appt._loc && state.location === 'both') parts.push('(' + appt._loc + ')'); detail.textContent = parts.join(' '); info.appendChild(detail); row.appendChild(info); var actions = document.createElement('div'); actions.className = 'dog-actions'; if (!isCheckedIn) { var btn = document.createElement('button'); btn.className = 'btn btn-checkin'; btn.textContent = 'Check In'; btn.setAttribute('data-state', 'idle'); btn.onclick = (function(b, d, pid) { return function() { handleCheckin(b, d, pid); }; })(btn, appt, pipelineId); actions.appendChild(btn); var nsBtn = document.createElement('button'); nsBtn.className = 'btn btn-noshow'; nsBtn.textContent = 'No Show'; nsBtn.setAttribute('data-state', 'idle'); nsBtn.onclick = (function(b, d, pid) { return function() { handleNoShow(b, d, pid); }; })(nsBtn, appt, pipelineId); actions.appendChild(nsBtn); } else { var btn2 = document.createElement('button'); btn2.className = 'btn btn-checkout'; btn2.textContent = 'Check Out'; btn2.onclick = (function(d, pid) { return function() { d.pipeline_id = pid; openCheckoutPanel(d); }; })(appt, pipelineId); actions.appendChild(btn2); var odBtn = document.createElement('button'); odBtn.className = 'btn btn-overdue'; odBtn.textContent = 'Overdue'; odBtn.setAttribute('data-state', 'idle'); odBtn.onclick = (function(b, d, pid) { return function() { handleOverdue(b, d, pid); }; })(odBtn, appt, pipelineId); actions.appendChild(odBtn); } row.appendChild(actions); return row; } /* RENDER TRAINING */ function renderTraining() { var body = document.getElementById('trainingBody'); if (!body) return; var sched = (state.scheduleData && state.scheduleData.schedule) ? state.scheduleData.schedule : {}; var training = sched.training || {}; var loc = state.location; var all = []; if (loc === 'east' || loc === 'both') { var ea = training.east || []; for (var i = 0; i < ea.length; i++) { ea[i]._loc = 'East'; all.push(ea[i]); } } if (loc === 'west' || loc === 'both') { var wa = training.west || []; for (var j = 0; j < wa.length; j++) { wa[j]._loc = 'West'; all.push(wa[j]); } } all.sort(function(a, b) { return (a.class_time || a.time || '').localeCompare(b.class_time || b.time || ''); }); setCount('trainingCount', all.length); while (body.firstChild) body.removeChild(body.firstChild); if (all.length === 0) { var empty = document.createElement('div'); empty.className = 'section-empty'; empty.textContent = 'No training classes today.'; body.appendChild(empty); showBody('training'); return; } var grid = document.createElement('div'); grid.className = 'training-grid'; for (var k = 0; k < all.length; k++) grid.appendChild(buildTrainingCard(all[k])); body.appendChild(grid); showBody('training'); } function buildTrainingCard(cls) { var card = document.createElement('div'); card.className = 'training-class-card'; var pt = parseTime(cls.class_time || cls.time || ''); var timeEl = document.createElement('div'); timeEl.className = 'training-class-time'; timeEl.textContent = pt.display; card.appendChild(timeEl); var divider = document.createElement('div'); divider.className = 'grooming-divider'; card.appendChild(divider); var info = document.createElement('div'); info.className = 'training-class-info'; var nameEl = document.createElement('div'); nameEl.className = 'training-class-name'; nameEl.textContent = cls.class_name || cls.name || 'Training Class'; info.appendChild(nameEl); var detail = document.createElement('div'); detail.className = 'training-class-detail'; var parts = []; if (cls._loc && state.location === 'both') parts.push(cls._loc); if (cls.trainer_name || cls.instructor) parts.push('Trainer: ' + (cls.trainer_name || cls.instructor)); detail.textContent = parts.join(' \u00b7 '); info.appendChild(detail); card.appendChild(info); var enCount = cls.enrollment_count !== undefined ? cls.enrollment_count : cls.enrolled; if (enCount !== undefined) { var enroll = document.createElement('div'); enroll.className = 'training-class-enroll'; enroll.textContent = '\ud83d\udc65 ' + enCount + ' enrolled'; card.appendChild(enroll); } return card; } /* RENDER BOARDING */ function renderBoarding() { var body = document.getElementById('boardingBody'); if (!body) return; var sched = (state.scheduleData && state.scheduleData.schedule) ? state.scheduleData.schedule : {}; var boarding = sched.boarding || {}; var all = boarding.east || []; while (body.firstChild) body.removeChild(body.firstChild); /* Split into subgroups by status */ var arriving = []; var staying = []; var departing = []; for (var i = 0; i < all.length; i++) { var s = all[i].status || ''; if (s === 'departing') departing.push(all[i]); else if (s === 'checked_in') staying.push(all[i]); else arriving.push(all[i]); } var totalCount = arriving.length + staying.length + departing.length; setCount('boardingCount', totalCount); if (totalCount === 0) { var empty = document.createElement('div'); empty.className = 'section-empty'; empty.textContent = 'No boarding stays right now.'; body.appendChild(empty); showBody('boarding'); return; } var pipelineId = 'EkADsEd5y64yx33if2Mz'; if (arriving.length > 0) { var ag = document.createElement('div'); ag.className = 'sub-group'; var al = document.createElement('div'); al.className = 'sub-group-label'; al.textContent = 'Arriving Today (' + arriving.length + ')'; ag.appendChild(al); var agGrid = document.createElement('div'); agGrid.className = 'boarding-grid'; for (var a = 0; a < arriving.length; a++) agGrid.appendChild(buildBoardingCard(arriving[a], 'arriving', pipelineId)); ag.appendChild(agGrid); body.appendChild(ag); } if (staying.length > 0) { var sg = document.createElement('div'); sg.className = 'sub-group'; var sl = document.createElement('div'); sl.className = 'sub-group-label'; sl.textContent = 'Currently Staying (' + staying.length + ')'; sg.appendChild(sl); var sgGrid = document.createElement('div'); sgGrid.className = 'boarding-grid'; for (var st = 0; st < staying.length; st++) sgGrid.appendChild(buildBoardingCard(staying[st], 'staying', pipelineId)); sg.appendChild(sgGrid); body.appendChild(sg); } if (departing.length > 0) { var dg = document.createElement('div'); dg.className = 'sub-group'; var dl = document.createElement('div'); dl.className = 'sub-group-label departing'; dl.textContent = 'Departing Today (' + departing.length + ')'; dg.appendChild(dl); var dgGrid = document.createElement('div'); dgGrid.className = 'boarding-grid'; for (var d = 0; d < departing.length; d++) dgGrid.appendChild(buildBoardingCard(departing[d], 'departing', pipelineId)); dg.appendChild(dgGrid); body.appendChild(dg); } showBody('boarding'); } function buildBoardingCard(stay, boardingState, pipelineId) { stay = parseDog(stay); var isArriving = (boardingState === 'arriving'); var isDeparting = (boardingState === 'departing'); var isStaying = (boardingState === 'staying'); var card = document.createElement('div'); card.className = 'boarding-card' + (isDeparting ? ' departing' : '') + (isArriving ? ' arriving' : ''); /* TOP: photo + name + safety + stay badge */ var top = document.createElement('div'); top.className = 'boarding-card-top'; top.style.cursor = 'pointer'; (function(d) { top.onclick = function(e) { if (e.target.tagName !== 'BUTTON' && e.target.tagName !== 'A') openPetModal(d); }; })(stay); var photo = document.createElement('img'); photo.className = 'dog-photo'; photo.style.flexShrink = '0'; photo.alt = stay._parsedName; photo.src = stay.photo_url || DOG_FALLBACK; photo.onerror = function() { this.src = DOG_FALLBACK; }; top.appendChild(photo); var info = document.createElement('div'); info.className = 'boarding-info'; /* Name row with safety badge */ var nameRow = document.createElement('div'); nameRow.className = 'dog-name-row'; var nameEl = document.createElement('span'); nameEl.className = 'boarding-dog-name'; nameEl.textContent = stay._parsedName || 'Unknown'; nameRow.appendChild(nameEl); nameRow.appendChild(safetyBadge(stay.safety_status)); info.appendChild(nameRow); /* Owner row with phone for arriving/departing */ var ownerRow = document.createElement('div'); ownerRow.className = 'boarding-owner'; ownerRow.textContent = stay.owner_name || stay.owner_last_name || ''; if ((isArriving || isDeparting) && stay.owner_phone) { var phoneSep = document.createTextNode(' \u00b7 '); ownerRow.appendChild(phoneSep); var phoneLink = document.createElement('a'); phoneLink.className = 'owner-phone-link'; phoneLink.href = 'tel:' + stay.owner_phone; phoneLink.textContent = formatPhoneDisplay(stay.owner_phone); phoneLink.onclick = function(e) { e.stopPropagation(); }; ownerRow.appendChild(phoneLink); } info.appendChild(ownerRow); /* Stay duration badge */ var stayBadge = document.createElement('div'); stayBadge.className = 'boarding-stay'; if (isDeparting) { stayBadge.textContent = 'Last night \u2014 checkout today'; stayBadge.classList.add('departing-badge'); } else if (isStaying && stay.night_current !== undefined && stay.night_total !== undefined) { stayBadge.textContent = 'Night ' + stay.night_current + ' of ' + stay.night_total; } else if (isArriving && stay.night_total) { stayBadge.textContent = stay.night_total + ' night' + (stay.night_total !== 1 ? 's' : ''); } if (stayBadge.textContent) info.appendChild(stayBadge); /* Pickup date (all states) */ if (stay.pickup_date || stay.boarding_end_date) { var pEl = document.createElement('div'); pEl.className = 'boarding-pickup'; pEl.textContent = '\ud83d\udcc5 Pickup: ' + formatDate(stay.pickup_date || stay.boarding_end_date); info.appendChild(pEl); } top.appendChild(info); card.appendChild(top); /* DETAILS: care info */ var details = document.createElement('div'); details.className = 'boarding-card-details'; /* Feeding 1-liner */ var feedText = stay.feeding_instructions || stay.food_type || ''; if (feedText) { var feedEl = document.createElement('div'); feedEl.className = 'boarding-care-line'; feedEl.textContent = '\ud83c\udf7d\ufe0f ' + (feedText.length > 60 ? feedText.substring(0, 60) + '\u2026' : feedText); feedEl.title = feedText; details.appendChild(feedEl); } /* Meds 1-liner */ var medText = stay.medication_details || stay.medications || ''; if (medText) { var medEl = document.createElement('div'); medEl.className = 'boarding-care-line med'; medEl.textContent = '\ud83d\udc8a ' + (medText.length > 60 ? medText.substring(0, 60) + '\u2026' : medText); medEl.title = medText; details.appendChild(medEl); } /* Allergies warning */ if (stay.allergies) { var allergyEl = document.createElement('div'); allergyEl.className = 'boarding-care-line allergy'; allergyEl.textContent = '\u26a0\ufe0f Allergies: ' + stay.allergies; details.appendChild(allergyEl); } /* Special notes (arriving + staying only) */ if ((isArriving || isStaying) && stay.special_notes) { var noteEl = document.createElement('div'); noteEl.className = 'boarding-care-line'; var noteText = stay.special_notes; noteEl.textContent = noteText.length > 70 ? noteText.substring(0, 70) + '\u2026' : noteText; noteEl.title = noteText; details.appendChild(noteEl); } /* Emergency contact (staying only) */ if (isStaying) { var emerName = stay.boarding_emergency_name || stay.emergency_contact || ''; var emerPhone = stay.boarding_emergency_phone || ''; if (emerName || emerPhone) { var emerEl = document.createElement('div'); emerEl.className = 'boarding-care-line'; var emerText = '\ud83d\udcde Emergency: ' + emerName; if (emerPhone) { emerEl.textContent = emerText + ' '; var emerLink = document.createElement('a'); emerLink.className = 'owner-phone-link'; emerLink.href = 'tel:' + emerPhone; emerLink.textContent = formatPhoneDisplay(emerPhone); emerLink.onclick = function(e) { e.stopPropagation(); }; emerEl.appendChild(emerLink); } else { emerEl.textContent = emerText; } details.appendChild(emerEl); } } /* Pickup person + password (departing only) */ if (isDeparting) { if (stay.alt_pickup_person) { var pupEl = document.createElement('div'); pupEl.className = 'boarding-care-line'; pupEl.textContent = '\ud83d\udc64 Pickup: ' + stay.alt_pickup_person; if (stay.pickup_password) pupEl.textContent += ' (PW: ' + stay.pickup_password + ')'; details.appendChild(pupEl); } } /* Last note (staying only) */ if (isStaying && stay.last_note) { var ns = document.createElement('div'); ns.className = 'boarding-notes-section'; var nl = document.createElement('div'); nl.className = 'boarding-note-label'; nl.textContent = 'Last Note'; ns.appendChild(nl); var nt = document.createElement('div'); nt.className = 'boarding-note-text'; nt.textContent = stay.last_note; ns.appendChild(nt); details.appendChild(ns); } card.appendChild(details); /* FOOTER: actions */ var footer = document.createElement('div'); footer.className = 'boarding-card-footer'; if (isArriving) { /* Check In + No Show buttons (same two-tap pattern as daycare) */ var ciBtn = document.createElement('button'); ciBtn.className = 'btn btn-checkin'; ciBtn.textContent = 'Check In'; ciBtn.setAttribute('data-state', 'idle'); ciBtn.onclick = (function(b, d, pid) { return function() { handleCheckin(b, d, pid); }; })(ciBtn, stay, pipelineId); footer.appendChild(ciBtn); var nsBtn = document.createElement('button'); nsBtn.className = 'btn btn-noshow'; nsBtn.textContent = 'No Show'; nsBtn.setAttribute('data-state', 'idle'); nsBtn.onclick = (function(b, d, pid) { return function() { handleNoShow(b, d, pid); }; })(nsBtn, stay, pipelineId); footer.appendChild(nsBtn); } if (isStaying) { /* Add Note button (inline note) */ var addNoteInline = document.createElement('div'); addNoteInline.className = 'add-note-inline'; var textarea = document.createElement('textarea'); textarea.placeholder = 'Enter note for this stay...'; addNoteInline.appendChild(textarea); var noteBtns = document.createElement('div'); noteBtns.style.cssText = 'display:flex;gap:6px;margin-top:6px;'; var saveBtn = document.createElement('button'); saveBtn.className = 'btn btn-primary'; saveBtn.style.fontSize = '.78rem'; saveBtn.textContent = 'Save Note'; saveBtn.onclick = (function(ta, d) { return function() { saveBoardingNote(ta, d); }; })(textarea, stay); noteBtns.appendChild(saveBtn); var cancelBtn = document.createElement('button'); cancelBtn.className = 'btn'; cancelBtn.style.fontSize = '.78rem'; cancelBtn.textContent = 'Cancel'; cancelBtn.onclick = (function(el) { return function() { el.style.display = 'none'; }; })(addNoteInline); noteBtns.appendChild(cancelBtn); addNoteInline.appendChild(noteBtns); card.appendChild(addNoteInline); var addNoteBtn = document.createElement('button'); addNoteBtn.className = 'btn'; addNoteBtn.style.fontSize = '.78rem'; addNoteBtn.textContent = '+ Add Note'; addNoteBtn.onclick = (function(el) { return function() { el.style.display = el.style.display === 'block' ? 'none' : 'block'; }; })(addNoteInline); footer.appendChild(addNoteBtn); } if (isDeparting) { /* Check Out button */ var coBtn = document.createElement('button'); coBtn.className = 'btn btn-checkout'; coBtn.textContent = 'Check Out'; coBtn.onclick = (function(d) { return function() { d.pipeline_id = 'EkADsEd5y64yx33if2Mz'; openCheckoutPanel(d); }; })(stay); footer.appendChild(coBtn); } card.appendChild(footer); return card; } function saveBoardingNote(textarea, stay) { var note = textarea.value.trim(); if (!note) { showToast('Note cannot be empty.', 'warning'); return; } callWebhook('/webhook/staff-checkout', { action: 'add_note', opp_id: stay.opp_id || '', note: note, note_type: 'boarding' }, function(err) { if (err) { showToast('Failed to save note.', 'error'); return; } showToast('Note saved.', 'success'); textarea.value = ''; textarea.parentNode.style.display = 'none'; }); } /* RESOURCES */ function renderResources() { var body = document.getElementById('resourcesBody'); if (!body) return; var resources = [ { icon: '\ud83d\udea8', title: 'Emergency Procedures', desc: 'First aid, fire evacuation, medical emergency protocols', href: 'https://doghouse.sprockettsddc.ca/resources/comingsoon' }, { icon: '\ud83d\udc8a', title: 'Medication Guide', desc: 'How to administer, log, and store boarding medications', href: 'https://doghouse.sprockettsddc.ca/resources/comingsoon' }, { icon: '\ud83d\udcdd', title: 'Incident Reporting', desc: 'Step-by-step guide for logging bites, injuries, and incidents', href: 'https://doghouse.sprockettsddc.ca/resources/comingsoon' }, { icon: '\ud83c\udf93', title: 'New Staff Onboarding', desc: 'First week checklist, system walkthrough, role training', href: 'https://doghouse.sprockettsddc.ca/resources/comingsoon' }, { icon: '\ud83d\udcde', title: 'Contact Theresa', desc: 'Owner direct line, urgent escalation contacts', href: 'tel:+12042337332' }, { icon: '\u2753', title: 'System Help', desc: 'Platform guides, troubleshooting, and support', href: 'https://www.notion.so/324853d109f28124bb8cffdc90f9beb8' } ]; var grid = document.createElement('div'); grid.className = 'resources-grid'; for (var i = 0; i < resources.length; i++) { var r = resources[i]; var card = document.createElement('a'); card.className = 'resource-card'; card.href = r.href; card.target = '_blank'; card.rel = 'noopener noreferrer'; var iconEl = document.createElement('div'); iconEl.className = 'resource-icon'; iconEl.textContent = r.icon; card.appendChild(iconEl); var infoEl = document.createElement('div'); infoEl.className = 'resource-info'; var titleEl = document.createElement('div'); titleEl.className = 'resource-title'; titleEl.textContent = r.title; infoEl.appendChild(titleEl); var descEl = document.createElement('div'); descEl.className = 'resource-desc'; descEl.textContent = r.desc; infoEl.appendChild(descEl); card.appendChild(infoEl); grid.appendChild(card); } body.appendChild(grid); } /* Detect checkout mode based on service + package status */ function detectCheckoutMode(dog) { var cat = (dog.service_category || '').toLowerCase(); var isDaycare = (cat === 'daycamp' || cat === 'daycare'); if (!isDaycare) return { mode: 'standard', usePackage: false }; var pkgStatus = (dog.package_status || '').toLowerCase(); var remaining = parseInt(dog.package_remaining, 10) || 0; var unlExpiry = dog.unlimited_expiry || ''; var unlFirstUse = dog.unlimited_first_use || ''; /* Day passes consumed first, even if unlimited exists */ if (pkgStatus === 'package' && remaining > 0) { return { mode: 'package', usePackage: true, packageType: 'day_pass', packageLabel: 'Package Pass', packageDetail: (remaining - 1) + ' remaining after this visit', packageRemainingBefore: remaining }; } if (pkgStatus === 'unlimited') { var isDormant = !unlFirstUse; var expiryDate = unlExpiry ? new Date(unlExpiry) : null; var isExpired = expiryDate && expiryDate < new Date(); if (isExpired) return { mode: 'standard', usePackage: false }; if (isDormant) { var calcExpiry = new Date(); calcExpiry.setDate(calcExpiry.getDate() + 29); return { mode: 'package', usePackage: true, packageType: 'unlimited', packageLabel: 'Monthly Unlimited starts today', packageDetail: 'Expires ' + formatDate(calcExpiry.toISOString()), unlimitedExpiry: calcExpiry.toISOString().split('T')[0], isDormantActivation: true }; } return { mode: 'package', usePackage: true, packageType: 'unlimited', packageLabel: 'Monthly Unlimited', packageDetail: 'Expires ' + formatDate(unlExpiry), unlimitedExpiry: unlExpiry, isDormantActivation: false }; } return { mode: 'standard', usePackage: false }; } /* CHECKOUT PANEL */ function openCheckoutPanel(dog) { dog = parseDog(dog); state.checkoutDog = dog; state.checkoutItems = []; state.paymentMethod = null; var photo = document.getElementById('coDogPhoto'); photo.src = dog.photo_url || DOG_FALLBACK; photo.onerror = function() { this.src = DOG_FALLBACK; }; photo.alt = dog._parsedName || ''; document.getElementById('coDogName').textContent = dog._parsedName || 'Unknown'; document.getElementById('coDogDetail').textContent = (dog.owner_name || '') + (dog._parsedService ? ' \u00b7 ' + svcPillLabel(dog._parsedService, dog.service_category) : ''); var ciTime = dog.check_in_time || dog.checkin_time; if (ciTime) { var pt = parseTime(ciTime); document.getElementById('coDuration').textContent = 'In at ' + pt.display; } else { document.getElementById('coDuration').textContent = '\u2014'; } var pkgInfo = detectCheckoutMode(dog); state.checkoutMode = pkgInfo.mode; state.usePackage = pkgInfo.usePackage; state.packageInfo = pkgInfo; state.originalPackageInfo = pkgInfo.mode === 'package' ? pkgInfo : null; if (pkgInfo.mode === 'package') { var pkgDefault = getDefaultLineItem(dog._parsedService); state.checkoutItems = pkgDefault ? [{ name: pkgDefault.name + ' (Package)', price: 0, id: pkgDefault.id, ghlId: '' }] : []; } else { var defaultItem = getDefaultLineItem(dog._parsedService); if (defaultItem) state.checkoutItems = [defaultItem]; } document.getElementById('coNotes').value = ''; state.receiptEmail = true; state.receiptSms = true; updateReceiptToggles(); var payOpts = document.querySelectorAll('.payment-option'); for (var i = 0; i < payOpts.length; i++) payOpts[i].classList.remove('selected'); renderLineItems(); renderCheckoutMode(); document.getElementById('checkoutOverlay').classList.add('open'); document.getElementById('checkoutPanel').classList.add('open'); document.body.style.overflow = 'hidden'; } function renderCheckoutMode() { var banner = document.getElementById('coPackageBanner'); var paymentParent = document.getElementById('paymentGrid').parentElement; var lineSection = document.getElementById('lineItemsTable').parentElement; var sectionLabels = lineSection.querySelectorAll('.panel-section-label'); var lineLabel = sectionLabels.length > 0 ? sectionLabels[0] : null; if (state.checkoutMode === 'package') { while (banner.firstChild) banner.removeChild(banner.firstChild); var card = document.createElement('div'); card.className = 'co-package-banner'; var title = document.createElement('div'); title.className = 'co-package-banner-title'; title.textContent = state.packageInfo.packageLabel; card.appendChild(title); var detail = document.createElement('div'); detail.className = 'co-package-banner-detail'; detail.textContent = state.packageInfo.packageDetail; card.appendChild(detail); var price = document.createElement('div'); price.className = 'co-package-banner-price'; price.textContent = '$0.00'; card.appendChild(price); var pocketLink = document.createElement('button'); pocketLink.className = 'co-pocket-link'; pocketLink.textContent = 'Pay out of pocket instead'; pocketLink.onclick = switchToOutOfPocket; card.appendChild(pocketLink); banner.appendChild(card); banner.style.display = ''; if (lineLabel) lineLabel.textContent = 'Add-ons'; updatePackagePaymentVisibility(); } else { /* Standard mode — show "Use package" link if package is available */ while (banner.firstChild) banner.removeChild(banner.firstChild); if (state.originalPackageInfo) { var backLink = document.createElement('button'); backLink.className = 'co-pocket-link'; backLink.style.cssText = 'margin-bottom:8px;color:#15803d'; backLink.textContent = 'Use package instead (' + state.originalPackageInfo.packageLabel + ')'; backLink.onclick = switchToPackage; banner.appendChild(backLink); banner.style.display = ''; } else { banner.style.display = 'none'; } if (lineLabel) lineLabel.textContent = 'Line Items'; paymentParent.style.display = ''; } } function updatePackagePaymentVisibility() { var paymentParent = document.getElementById('paymentGrid').parentElement; var btn = document.getElementById('btnCompleteCheckout'); if (state.checkoutMode !== 'package') { paymentParent.style.display = ''; return; } var addonSubtotal = 0; var addonGst = 0; var addonPst = 0; for (var i = 0; i < state.checkoutItems.length; i++) { var p = Number(state.checkoutItems[i].price) || 0; addonSubtotal += p; var tx = TAX_BY_GROUP[state.checkoutItems[i].group] || { gst: 0.05, pst: 0 }; addonGst += p * tx.gst; addonPst += p * tx.pst; } var addonTotal = addonSubtotal + Math.round(addonGst * 100) / 100 + Math.round(addonPst * 100) / 100; var hasPayableAddons = addonSubtotal > 0; if (hasPayableAddons) { paymentParent.style.display = ''; btn.textContent = '\u2713 Complete Checkout \u2014 $' + addonTotal.toFixed(2) + ' due'; } else { paymentParent.style.display = 'none'; state.paymentMethod = 'package'; btn.textContent = '\u2713 Complete Checkout \u2014 Package'; } } function switchToOutOfPocket() { state.checkoutMode = 'standard'; state.usePackage = false; state.paymentMethod = null; var payOpts = document.querySelectorAll('.payment-option'); for (var i = 0; i < payOpts.length; i++) payOpts[i].classList.remove('selected'); var defaultItem = getDefaultLineItem(state.checkoutDog._parsedService); if (defaultItem) state.checkoutItems = [defaultItem]; renderLineItems(); renderCheckoutMode(); } function switchToPackage() { if (!state.originalPackageInfo) return; state.checkoutMode = 'package'; state.usePackage = true; state.packageInfo = state.originalPackageInfo; state.paymentMethod = null; var pkgDefault = getDefaultLineItem(state.checkoutDog._parsedService); state.checkoutItems = pkgDefault ? [{ name: pkgDefault.name + ' (Package)', price: 0, id: pkgDefault.id, ghlId: '' }] : []; renderLineItems(); renderCheckoutMode(); } function closeCheckoutPanel() { document.getElementById('checkoutOverlay').classList.remove('open'); document.getElementById('checkoutPanel').classList.remove('open'); document.body.style.overflow = ''; state.checkoutDog = null; } function getDefaultLineItem(svc) { svc = (svc || '').toLowerCase(); if (svc.indexOf('full') > -1) return { name: 'Full Day Daycamp', price: 38.15, id: 'full_day' }; if (svc.indexOf('half') > -1) return { name: 'Half Day Daycamp', price: 28.00, id: 'half_day' }; if (svc.indexOf('power') > -1 || svc.indexOf('hour') > -1) return { name: 'Power Hour', price: 19.60, id: 'power_hour' }; return null; } function renderLineItems() { var table = document.getElementById('lineItemsTable'); while (table.children.length > 1) table.removeChild(table.lastChild); for (var i = 0; i < state.checkoutItems.length; i++) { var item = state.checkoutItems[i]; var row = document.createElement('div'); row.className = 'line-item-row'; var nameEl = document.createElement('span'); nameEl.className = 'li-name'; nameEl.textContent = item.name; row.appendChild(nameEl); var priceEl = document.createElement('span'); priceEl.className = 'li-price'; priceEl.textContent = '$' + Number(item.price).toFixed(2); row.appendChild(priceEl); var removeBtn = document.createElement('button'); removeBtn.className = 'li-remove'; removeBtn.textContent = '\u00d7'; removeBtn.title = 'Remove item'; removeBtn.onclick = (function(idx) { return function() { removeLineItem(idx); }; })(i); row.appendChild(removeBtn); table.appendChild(row); } updateTotal(); } function addLineItem() { var sel = document.getElementById('productSelect'); if (sel.selectedIndex <= 0) { showToast('Select an item to add.', 'warning'); return; } var id = sel.value; var item = productPriceMap[id]; if (!item) { showToast('Unknown product.', 'error'); return; } state.checkoutItems.push({ name: item.name, price: item.price, id: id, ghlId: item.ghlId || '', group: item.group || '' }); sel.selectedIndex = 0; renderLineItems(); if (state.checkoutMode === 'package') updatePackagePaymentVisibility(); } function addCustomLineItem() { var nameInput = document.getElementById('customItemName'); var priceInput = document.getElementById('customItemPrice'); var name = (nameInput.value || '').trim(); var price = parseFloat(priceInput.value) || 0; if (!name) { showToast('Enter an item name.', 'warning'); nameInput.focus(); return; } if (price <= 0) { showToast('Enter a price.', 'warning'); priceInput.focus(); return; } var groomCb = document.getElementById('customItemGroom'); var isGroom = groomCb && groomCb.checked; state.checkoutItems.push({ name: name, price: price, id: 'custom_' + Date.now(), ghlId: '', group: isGroom ? 'Groom A-la-carte' : '' }); nameInput.value = ''; priceInput.value = ''; if (groomCb) groomCb.checked = false; renderLineItems(); if (state.checkoutMode === 'package') updatePackagePaymentVisibility(); } function removeLineItem(idx) { state.checkoutItems.splice(idx, 1); renderLineItems(); if (state.checkoutMode === 'package') updatePackagePaymentVisibility(); } function updateTotal() { var subtotal = 0; var gstTotal = 0; var pstTotal = 0; for (var i = 0; i < state.checkoutItems.length; i++) { var price = Number(state.checkoutItems[i].price) || 0; subtotal += price; var tax = TAX_BY_GROUP[state.checkoutItems[i].group] || { gst: 0.05, pst: 0 }; gstTotal += price * tax.gst; pstTotal += price * tax.pst; } gstTotal = Math.round(gstTotal * 100) / 100; pstTotal = Math.round(pstTotal * 100) / 100; var total = subtotal + gstTotal + pstTotal; document.getElementById('coSubtotal').textContent = '$' + subtotal.toFixed(2); document.getElementById('coGst').textContent = '$' + gstTotal.toFixed(2); var pstRow = document.getElementById('coPstRow'); if (pstTotal > 0) { pstRow.style.display = 'flex'; document.getElementById('coPst').textContent = '$' + pstTotal.toFixed(2); } else { pstRow.style.display = 'none'; } document.getElementById('coTotal').textContent = '$' + total.toFixed(2); } function selectPayment(el) { var opts = document.querySelectorAll('.payment-option'); for (var i = 0; i < opts.length; i++) opts[i].classList.remove('selected'); el.classList.add('selected'); state.paymentMethod = el.getAttribute('data-method'); } function toggleReceipt(type) { if (type === 'email') state.receiptEmail = !state.receiptEmail; else state.receiptSms = !state.receiptSms; updateReceiptToggles(); } function updateReceiptToggles() { var emailEl = document.getElementById('receiptEmail'); var smsEl = document.getElementById('receiptSms'); var emailT = document.getElementById('receiptEmailToggle'); var smsT = document.getElementById('receiptSmsToggle'); emailEl.classList[state.receiptEmail ? 'add' : 'remove']('on'); emailT.classList[state.receiptEmail ? 'add' : 'remove']('on'); smsEl.classList[state.receiptSms ? 'add' : 'remove']('on'); smsT.classList[state.receiptSms ? 'add' : 'remove']('on'); } function toggleIncident() { state.incidentOn = !state.incidentOn; document.getElementById('incidentToggle').className = 'toggle-switch' + (state.incidentOn ? ' on' : ''); document.getElementById('incidentFields').classList[state.incidentOn ? 'add' : 'remove']('visible'); } /* COMPLETE CHECKOUT */ function completeCheckout() { var dog = state.checkoutDog; if (!dog) return; var isPackageVisit = state.usePackage && state.checkoutMode === 'package'; var hasAddons = state.checkoutItems.length > 0; if (!isPackageVisit && state.checkoutItems.length === 0) { showToast('Add at least one line item.', 'warning'); return; } if (!isPackageVisit && !state.paymentMethod) { showToast('Select a payment method.', 'warning'); return; } var addonTotal = 0; for (var at = 0; at < state.checkoutItems.length; at++) addonTotal += Number(state.checkoutItems[at].price) || 0; if (isPackageVisit && addonTotal > 0 && !state.paymentMethod) { showToast('Select a payment method for add-ons.', 'warning'); return; } var incidentData = null; var btn = document.getElementById('btnCompleteCheckout'); btn.disabled = true; btn.textContent = 'Processing...'; var total = 0; for (var i = 0; i < state.checkoutItems.length; i++) total += Number(state.checkoutItems[i].price) || 0; var pipelineId = dog.pipeline_id || ''; var checkedOutStageId = (pipelineId && CHECKED_OUT_STAGES[pipelineId]) ? CHECKED_OUT_STAGES[pipelineId] : ''; /* Map checkout items to n8n format: price (dollars) → amount (cents) */ var lineItems = []; for (var li = 0; li < state.checkoutItems.length; li++) { lineItems.push({ name: state.checkoutItems[li].name, amount: Math.round((Number(state.checkoutItems[li].price) || 0) * 100), qty: 1, description: '', productId: state.checkoutItems[li].ghlId || '' }); } var totalCents = Math.round(total * 100); callWebhook('/webhook/staff-checkout', { opp_id: dog.opp_id || '', pipeline_id: pipelineId, checked_out_stage_id: checkedOutStageId, dog_name: dog._parsedName, owner_name: dog.owner_name || '', contact_id: dog.contact_id || '', pet_id: dog.pet_id || '', service: dog._parsedService, line_items: lineItems, total: totalCents, payment_method: state.paymentMethod, send_receipt_email: state.receiptEmail, send_receipt_sms: state.receiptSms, notes: document.getElementById('coNotes').value.trim(), incident: incidentData, use_package: isPackageVisit, package_type: isPackageVisit && state.packageInfo ? (state.packageInfo.packageType || '') : '', package_remaining_before: isPackageVisit && state.packageInfo ? (state.packageInfo.packageRemainingBefore || 0) : 0, unlimited_expiry: isPackageVisit && state.packageInfo ? (state.packageInfo.unlimitedExpiry || '') : '' }, function(err) { btn.disabled = false; btn.textContent = '\u2713 Complete Checkout'; if (err) { showToast('Checkout failed \u2014 please try again.', 'error', 4000); return; } showToast((dog._parsedName || 'Dog') + ' checked out!', 'success'); closeCheckoutPanel(); /* Optimistic UI: remove dog from local state + re-render */ var coOppId = dog.opp_id || ''; var coPipelineId = dog.pipeline_id || ''; var coFound = findDogInState(coOppId, coPipelineId); if (coFound) { coFound.array.splice(coFound.index, 1); } rerenderSection(coPipelineId); scheduleBgSync(); }); } /* REFRESH */ function refreshBoard() { showLoading('daycare'); fetchBoard(function(err) { if (err) { showError('daycare', 'Could not load board. Check your connection.'); return; } updateStats(); renderDaycare(); }); } function refreshAll() { var btn = document.getElementById('refreshBtn'); if (btn) btn.classList.add('spinning'); var done = { board: false, schedule: false }; function check() { if (done.board && done.schedule) { if (btn) btn.classList.remove('spinning'); } } showLoading('daycare'); fetchBoard(function(err) { done.board = true; if (err) { showError('daycare', 'Could not load board.'); } else { renderDaycare(); updateStats(); updateIncidentBadge(); if (currentView === 'incidents') renderIncidentLog(); } check(); }); showLoading('grooming'); showLoading('training'); showLoading('boarding'); fetchSchedule(function(err) { done.schedule = true; if (err) { showError('grooming', 'Could not load schedule.'); showError('training', 'Could not load schedule.'); showError('boarding', 'Could not load schedule.'); } else { renderGrooming(); renderTraining(); renderBoarding(); updateStats(); updateIncidentBadge(); if (currentView === 'incidents') renderIncidentLog(); } check(); }); } function startAutoRefresh() { if (state.refreshTimer) clearInterval(state.refreshTimer); state.refreshTimer = setInterval(refreshAll, 300000); /* 5 minutes */ } /* LOCATION VISIBILITY */ function updateLocationVisibility() { var loc = state.location; var isWest = (loc === 'west'); /* Hide boarding section when West selected (East only) */ var boardingBlock = document.getElementById('boardingBlock'); if (boardingBlock) boardingBlock.style.display = isWest ? 'none' : ''; /* Hide boarding card in dashboard when West selected */ var dashBoarding = document.getElementById('dashBoarding'); if (dashBoarding) dashBoarding.style.display = isWest ? 'none' : ''; /* Adjust dashboard grid: 3 columns when boarding hidden */ var dashServices = document.getElementById('dashServices'); if (dashServices) dashServices.style.gridTemplateColumns = isWest ? 'repeat(3,1fr)' : 'repeat(4,1fr)'; } /* BOOK A DOG PANEL */ var BOOKING_URLS = { daycamp: 'https://book.sprockettsddc.ca/doggydaycampall', grooming: 'https://book.sprockettsddc.ca/grooming', boarding: 'https://book.sprockettsddc.ca/boarding', training: 'https://book.sprockettsddc.ca/training_classes' }; var SVC_CONTRACTS = { daycamp: ['general_release', 'daycare_waiver'], grooming: ['general_release', 'grooming_agreement'], boarding: ['general_release', 'daycare_waiver', 'boarding_waiver'], training: ['general_release', 'training_waiver'] }; var SVC_CONTRACT_LABELS = { general_release: 'General Release', daycare_waiver: 'Daycare Waiver', grooming_agreement: 'Grooming Agreement', boarding_waiver: 'Boarding Waiver', training_waiver: 'Training Waiver' }; var bookSearchTimer = null; var bookSelectedPets = []; /* { contact: {...}, pet: {...} } */ var bookLastResults = []; /* cached search results for checkbox access */ /* Profile view inside Search panel */ var bookProfileData = null; var bookProfileEditMode = false; function showProfileInPanel(searchResult) { var contactId = searchResult.contact_id || ''; var searchPets = searchResult.pets || []; if (!contactId) return; /* Hide search + action bar, show back button */ document.getElementById('bookSearchWrap').style.display = 'none'; document.getElementById('bookActionBar').style.display = 'none'; document.getElementById('bookBackBtn').classList.add('visible'); document.getElementById('bookPanelTitle').textContent = (searchResult.first_name || '') + ' ' + (searchResult.last_name || ''); var container = document.getElementById('bookResults'); while (container.firstChild) container.removeChild(container.firstChild); var loading = document.createElement('div'); loading.className = 'book-empty'; var loadIcon = document.createElement('div'); loadIcon.className = 'book-empty-icon'; loadIcon.style.animation = 'spin .7s linear infinite'; loadIcon.textContent = '\u21BB'; var loadText = document.createElement('div'); loadText.className = 'book-empty-text'; loadText.textContent = 'Loading profile...'; loading.appendChild(loadIcon); loading.appendChild(loadText); container.appendChild(loading); callWebhook('/webhook/PetPortal', { action: 'portal_list_pets', contact_id: contactId }, function(err, data) { /* If PetPortal fails or returns empty, fall back to search result data */ if (err || !data || !data.ok) { data = { ok: true, first_name: searchResult.first_name || '', last_name: searchResult.last_name || '', phone: searchResult.phone || '', contact_email: searchResult.email || '', preferred_daycare_location: '', address1: '', city: '', state: '', postalCode: '', contracts: searchResult.contracts || {}, pets: searchPets, package_balance: null }; } bookProfileData = data; bookProfileData._contactId = contactId; if (!bookProfileData.pets || bookProfileData.pets.length === 0) { bookProfileData.pets = searchPets; } bookProfileEditMode = false; renderProfileInPanel(data); }); } function renderProfileInPanel(data) { var container = document.getElementById('bookResults'); while (container.firstChild) container.removeChild(container.firstChild); var fullName = ((data.first_name || '') + ' ' + (data.last_name || '')).trim(); var contracts = data.contracts || {}; /* Header */ var header = document.createElement('div'); header.className = 'cl-profile-header'; var avatar = document.createElement('div'); avatar.className = 'cl-profile-avatar'; avatar.textContent = '\ud83d\udc64'; header.appendChild(avatar); var headerInfo = document.createElement('div'); var nameEl = document.createElement('div'); nameEl.className = 'cl-profile-name'; nameEl.textContent = fullName || 'Unknown'; headerInfo.appendChild(nameEl); var subEl = document.createElement('div'); subEl.className = 'cl-profile-sub'; var locPref = (data.preferred_daycare_location || '').toLowerCase(); subEl.textContent = locPref === 'west' ? 'West \u2014 360 Keewatin St' : 'East \u2014 975 Thomas Ave'; headerInfo.appendChild(subEl); header.appendChild(headerInfo); container.appendChild(header); if (!bookProfileEditMode) { /* READ MODE */ function addReadField(parent, label, value, linkType) { var row = document.createElement('div'); row.className = 'cl-field'; var lbl = document.createElement('span'); lbl.className = 'cl-field-label'; lbl.textContent = label; row.appendChild(lbl); if (value) { if (linkType === 'tel') { var a = document.createElement('a'); a.href = 'tel:' + value; a.textContent = formatPhoneDisplay(value); row.appendChild(a); } else if (linkType === 'email') { var a2 = document.createElement('a'); a2.href = 'mailto:' + value; a2.textContent = value; row.appendChild(a2); } else { row.appendChild(document.createTextNode(value)); } } else { var na = document.createElement('span'); na.style.color = '#aaa'; na.textContent = 'Not on file'; row.appendChild(na); } parent.appendChild(row); } /* Contact Info */ var contactSection = document.createElement('div'); contactSection.className = 'cl-section'; var contactTitle = document.createElement('div'); contactTitle.className = 'cl-section-title'; contactTitle.textContent = 'Contact Info '; var editLink = document.createElement('span'); editLink.className = 'cl-section-edit'; editLink.textContent = 'Edit'; editLink.onclick = function() { bookProfileEditMode = true; renderProfileInPanel(bookProfileData); }; contactTitle.appendChild(editLink); contactSection.appendChild(contactTitle); addReadField(contactSection, 'Phone', data.phone, 'tel'); addReadField(contactSection, 'Email', data.contact_email, 'email'); var addrParts = [data.address1, data.city, data.state, data.postalCode].filter(function(x) { return x; }); addReadField(contactSection, 'Address', addrParts.length > 0 ? addrParts.join(', ') : ''); container.appendChild(contactSection); /* Agreements */ var ctSection = document.createElement('div'); ctSection.className = 'cl-section'; var ctTitle = document.createElement('div'); ctTitle.className = 'cl-section-title'; ctTitle.textContent = 'Agreements'; ctSection.appendChild(ctTitle); var contractDefs = [ { key: 'general_release', name: 'General Release', url: 'https://links.kanameplatform.com/documents/doc-form/68ded260ce7fb708e1ac6e04?locale=en-US' }, { key: 'daycare_waiver', name: 'Daycare Waiver', url: 'https://links.kanameplatform.com/documents/doc-form/68decaeb7549b5170a4709a6?locale=en-US' }, { key: 'grooming_agreement', name: 'Grooming Agreement', url: 'https://links.kanameplatform.com/documents/doc-form/68ded2cb7549b50ccf479d06?locale=en-US' }, { key: 'boarding_waiver', name: 'Boarding Waiver', url: 'https://links.kanameplatform.com/documents/doc-form/68ded336e784bbdb9ea46e0b?locale=en-US' }, { key: 'training_waiver', name: 'Training Waiver', url: 'https://links.kanameplatform.com/documents/doc-form/68ded3ab79fcb912a77b45f4?locale=en-US' } ]; for (var c = 0; c < contractDefs.length; c++) { var crow = document.createElement('div'); crow.className = 'cl-contract-row'; var signed = !!(contracts[contractDefs[c].key]); var cIcon = document.createElement('span'); cIcon.style.cssText = 'color:' + (signed ? 'var(--green)' : 'var(--red)') + ';flex-shrink:0'; cIcon.textContent = signed ? '\u2713' : '\u2717'; crow.appendChild(cIcon); crow.appendChild(document.createTextNode(' ' + contractDefs[c].name + ' \u2014 ')); if (signed) { var ss = document.createElement('span'); ss.className = 'cl-contract-signed'; ss.textContent = 'Signed'; crow.appendChild(ss); } else { var us = document.createElement('span'); us.className = 'cl-contract-unsigned'; var sl = document.createElement('a'); sl.href = contractDefs[c].url; sl.target = '_blank'; sl.rel = 'noopener'; sl.textContent = 'Sign Now \u2192'; us.appendChild(sl); crow.appendChild(us); } ctSection.appendChild(crow); } container.appendChild(ctSection); /* Packages */ var pkgSection = document.createElement('div'); pkgSection.className = 'cl-section'; var pkgTitle = document.createElement('div'); pkgTitle.className = 'cl-section-title'; pkgTitle.textContent = 'Packages'; pkgSection.appendChild(pkgTitle); var pkg = data.package_balance; if (pkg && (pkg.remaining > 0 || pkg.remaining === 'Unlimited')) { var pkgCard = document.createElement('div'); pkgCard.className = 'cl-package-card'; var pkgLabel = document.createElement('div'); pkgLabel.className = 'cl-package-label'; pkgLabel.textContent = pkg.name || 'Daycamp Pass'; pkgCard.appendChild(pkgLabel); var pkgBal = document.createElement('div'); pkgBal.className = 'cl-package-balance'; pkgBal.textContent = pkg.remaining; pkgCard.appendChild(pkgBal); pkgSection.appendChild(pkgCard); } else { var noPkg = document.createElement('div'); noPkg.style.cssText = 'font-size:.85rem;color:#aaa'; noPkg.textContent = 'No active packages'; pkgSection.appendChild(noPkg); } var addPkgBtn = document.createElement('button'); addPkgBtn.className = 'pp-add-btn'; addPkgBtn.textContent = '+ Add Package'; addPkgBtn.onclick = openPackageModal; pkgSection.appendChild(addPkgBtn); container.appendChild(pkgSection); /* Dogs */ var dogsSection = document.createElement('div'); dogsSection.className = 'cl-section'; var dogsTitle = document.createElement('div'); dogsTitle.className = 'cl-section-title'; dogsTitle.textContent = 'Dogs'; dogsSection.appendChild(dogsTitle); var pets = (data.pets || []).filter(function(p) { return (p.status || '').toUpperCase() !== 'INACTIVE'; }); for (var d = 0; d < pets.length; d++) { var dogCard = document.createElement('div'); dogCard.className = 'cl-dog-card'; (function(pet) { dogCard.onclick = function() { pet.owner_name = fullName; pet.owner_phone = data.phone || ''; pet.owner_email = data.contact_email || ''; pet.contact_id = bookProfileData._contactId; pet.contracts = contracts; pet.has_unsigned_contracts = Object.values(contracts).some(function(v) { return !v; }); openPetModal(pet); }; })(pets[d]); var dogPhoto = document.createElement('img'); dogPhoto.className = 'cl-dog-photo'; dogPhoto.src = pets[d].profile_photo || pets[d].pet_photo || DOG_FALLBACK; dogPhoto.onerror = function() { this.src = DOG_FALLBACK; }; dogCard.appendChild(dogPhoto); var dogInfo = document.createElement('div'); dogInfo.className = 'cl-dog-info'; var dogName2 = document.createElement('div'); dogName2.className = 'cl-dog-name'; dogName2.textContent = cleanPetDisplayName(pets[d].name || pets[d].pet_name || 'Dog'); dogInfo.appendChild(dogName2); var dogMeta = document.createElement('div'); dogMeta.className = 'cl-dog-meta'; var metaParts = []; if (pets[d].breed) metaParts.push(pets[d].breed); if (pets[d].birthday) { var ageStr2 = calcAge(pets[d].birthday); if (ageStr2) metaParts.push(ageStr2); } var wc2 = weightClass(pets[d].weight_lbs || pets[d].weight); if (wc2) metaParts.push(wc2); var gl2 = genderLabel(pets[d].gender); if (gl2) metaParts.push(gl2); dogMeta.textContent = metaParts.join(' \u00b7 '); dogInfo.appendChild(dogMeta); var badgeRow = document.createElement('div'); badgeRow.style.cssText = 'display:flex;flex-wrap:wrap;gap:4px;margin-top:4px;align-items:center'; badgeRow.appendChild(safetyBadge(pets[d].safety_status)); var petDocs = docsBadge({ has_unsigned_contracts: Object.values(contracts).some(function(v) { return !v; }) }); if (petDocs) badgeRow.appendChild(petDocs); var petFlags = careFlags(pets[d]); if (petFlags) { var fc = petFlags.querySelectorAll('.care-flag'); for (var f = 0; f < fc.length; f++) badgeRow.appendChild(fc[f].cloneNode(true)); } dogInfo.appendChild(badgeRow); dogCard.appendChild(dogInfo); dogsSection.appendChild(dogCard); } container.appendChild(dogsSection); } else { /* EDIT MODE */ var editSection = document.createElement('div'); editSection.className = 'cl-section'; var editTitle = document.createElement('div'); editTitle.className = 'cl-section-title'; editTitle.textContent = 'Edit Contact Info'; editSection.appendChild(editTitle); function makeEditField(label, id, value, type) { var wrap = document.createElement('div'); var lbl = document.createElement('label'); lbl.className = 'cl-edit-label'; lbl.textContent = label; wrap.appendChild(lbl); var inp = document.createElement('input'); inp.className = 'cl-edit-input'; inp.type = type || 'text'; inp.id = id; inp.value = value || ''; wrap.appendChild(inp); return wrap; } var row1 = document.createElement('div'); row1.className = 'cl-edit-row'; row1.appendChild(makeEditField('Phone', 'clEditPhone', data.phone, 'tel')); row1.appendChild(makeEditField('Email', 'clEditEmail', data.contact_email, 'email')); editSection.appendChild(row1); editSection.appendChild(makeEditField('Street Address', 'clEditAddress', data.address1)); var row2 = document.createElement('div'); row2.className = 'cl-edit-row'; row2.appendChild(makeEditField('City', 'clEditCity', data.city)); row2.appendChild(makeEditField('Province', 'clEditState', data.state)); editSection.appendChild(row2); var row3 = document.createElement('div'); row3.className = 'cl-edit-row'; row3.appendChild(makeEditField('Postal Code', 'clEditPostal', data.postalCode)); var locWrap = document.createElement('div'); var locLabel = document.createElement('label'); locLabel.className = 'cl-edit-label'; locLabel.textContent = 'Preferred Location'; locWrap.appendChild(locLabel); var locSelect = document.createElement('select'); locSelect.className = 'cl-edit-input'; locSelect.id = 'clEditLoc'; var optEast = document.createElement('option'); optEast.value = 'east'; optEast.textContent = 'East'; if ((data.preferred_daycare_location || '').toLowerCase() !== 'west') optEast.selected = true; var optWest = document.createElement('option'); optWest.value = 'west'; optWest.textContent = 'West'; if ((data.preferred_daycare_location || '').toLowerCase() === 'west') optWest.selected = true; locSelect.appendChild(optEast); locSelect.appendChild(optWest); locWrap.appendChild(locSelect); row3.appendChild(locWrap); editSection.appendChild(row3); var actions = document.createElement('div'); actions.className = 'cl-edit-actions'; var saveBtn = document.createElement('button'); saveBtn.className = 'cl-save-btn'; saveBtn.id = 'clSaveBtn'; saveBtn.textContent = 'Save Changes'; saveBtn.onclick = saveProfileInPanel; var cancelBtn = document.createElement('button'); cancelBtn.className = 'cl-cancel-btn'; cancelBtn.textContent = 'Cancel'; cancelBtn.onclick = function() { bookProfileEditMode = false; renderProfileInPanel(bookProfileData); }; actions.appendChild(saveBtn); actions.appendChild(cancelBtn); editSection.appendChild(actions); var msgEl = document.createElement('div'); msgEl.id = 'clSaveMsg'; msgEl.style.cssText = 'margin-top:10px;font-size:.85rem;text-align:center'; editSection.appendChild(msgEl); container.appendChild(editSection); } } function saveProfileInPanel() { if (!bookProfileData) return; var btn = document.getElementById('clSaveBtn'); var msg = document.getElementById('clSaveMsg'); btn.disabled = true; btn.textContent = 'Saving...'; var fields = {}; var v; v = (document.getElementById('clEditPhone') || {}).value; if (v && v.trim()) fields.phone = v.trim(); v = (document.getElementById('clEditEmail') || {}).value; if (v && v.trim()) fields.email = v.trim(); v = (document.getElementById('clEditAddress') || {}).value; if (v !== undefined) fields.address1 = (v || '').trim(); v = (document.getElementById('clEditCity') || {}).value; if (v !== undefined) fields.city = (v || '').trim(); v = (document.getElementById('clEditState') || {}).value; if (v) fields.state = v; v = (document.getElementById('clEditPostal') || {}).value; if (v !== undefined) fields.postalCode = (v || '').trim(); v = (document.getElementById('clEditLoc') || {}).value; if (v) fields.preferred_location = v; callWebhook('/webhook/PetPortal', { action: 'portal_update_contact', contact_id: bookProfileData._contactId, fields: fields }, function(err, data) { btn.disabled = false; btn.textContent = 'Save Changes'; if (err || !data || !data.ok) { if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Save failed. Try again.'; } return; } if (fields.phone) bookProfileData.phone = fields.phone; if (fields.email) bookProfileData.contact_email = fields.email; if (fields.address1 !== undefined) bookProfileData.address1 = fields.address1; if (fields.city !== undefined) bookProfileData.city = fields.city; if (fields.state) bookProfileData.state = fields.state; if (fields.postalCode !== undefined) bookProfileData.postalCode = fields.postalCode; if (fields.preferred_location) bookProfileData.preferred_daycare_location = fields.preferred_location; bookProfileEditMode = false; renderProfileInPanel(bookProfileData); showToast('Client profile updated', 'success'); }); } function bookBackToResults() { document.getElementById('bookSearchWrap').style.display = ''; document.getElementById('bookBackBtn').classList.remove('visible'); document.getElementById('bookPanelTitle').textContent = 'Search'; bookProfileData = null; bookProfileEditMode = false; updateBookActionBar(); renderBookResults(bookLastResults); } function openBookPanel() { document.getElementById('bookOverlay').classList.add('open'); document.getElementById('bookPanel').classList.add('open'); document.body.style.overflow = 'hidden'; var input = document.getElementById('bookSearchInput'); input.value = ''; document.getElementById('bookSearchClear').classList.remove('visible'); document.getElementById('bookBackBtn').classList.remove('visible'); document.getElementById('bookSearchWrap').style.display = ''; document.getElementById('bookPanelTitle').textContent = 'Search'; bookSelectedPets = []; bookLastResults = []; bookProfileData = null; bookProfileEditMode = false; updateBookActionBar(); showBookEmpty('\ud83d\udd0d', 'Type at least 2 characters to search'); setTimeout(function() { input.focus(); }, 300); } function closeBookPanel() { document.getElementById('bookOverlay').classList.remove('open'); document.getElementById('bookPanel').classList.remove('open'); document.body.style.overflow = ''; } function clearBookSearch() { var input = document.getElementById('bookSearchInput'); input.value = ''; document.getElementById('bookSearchClear').classList.remove('visible'); showBookEmpty('\ud83d\udd0d', 'Type at least 2 characters to search'); input.focus(); } /* Safe empty state — no user input in DOM writes */ function showBookEmpty(icon, text) { var container = document.getElementById('bookResults'); while (container.firstChild) container.removeChild(container.firstChild); var wrap = document.createElement('div'); wrap.className = 'book-empty'; var iconEl = document.createElement('div'); iconEl.className = 'book-empty-icon'; iconEl.textContent = icon; wrap.appendChild(iconEl); var textEl = document.createElement('div'); textEl.className = 'book-empty-text'; textEl.textContent = text; wrap.appendChild(textEl); container.appendChild(wrap); } function showBookLoading() { var container = document.getElementById('bookResults'); while (container.firstChild) container.removeChild(container.firstChild); var wrap = document.createElement('div'); wrap.className = 'book-loading'; var spinner = document.createElement('div'); spinner.className = 'spinner'; spinner.style.cssText = 'width:20px;height:20px;border:3px solid var(--border);border-top-color:var(--blue);border-radius:50%;animation:spin .7s linear infinite;flex-shrink:0;margin-right:10px'; wrap.appendChild(spinner); wrap.appendChild(document.createTextNode('Searching...')); container.appendChild(wrap); } function showBookError() { var container = document.getElementById('bookResults'); while (container.firstChild) container.removeChild(container.firstChild); var wrap = document.createElement('div'); wrap.className = 'book-empty'; var iconEl = document.createElement('div'); iconEl.className = 'book-empty-icon'; iconEl.textContent = '\u26a0\ufe0f'; wrap.appendChild(iconEl); var textEl = document.createElement('div'); textEl.className = 'book-empty-text'; textEl.textContent = 'Search failed. '; var retryBtn = document.createElement('button'); retryBtn.className = 'book-empty-link'; retryBtn.style.cssText = 'border:none;background:none;cursor:pointer;font-size:inherit;font-weight:inherit;color:inherit;text-decoration:underline'; retryBtn.textContent = 'Try again'; retryBtn.onclick = handleBookSearch; textEl.appendChild(retryBtn); wrap.appendChild(textEl); container.appendChild(wrap); } /* Check if a contract is signed (truthy string value) */ function isSigned(val) { if (!val) return false; var s = ('' + val).toLowerCase().trim(); return s !== '' && s !== 'false' && s !== '0' && s !== 'null' && s !== 'undefined'; } /* Get missing contracts for a service */ function getMissingContracts(contracts, service) { var required = SVC_CONTRACTS[service] || []; var missing = []; for (var i = 0; i < required.length; i++) { if (!contracts || !isSigned(contracts[required[i]])) { missing.push(SVC_CONTRACT_LABELS[required[i]] || required[i]); } } return missing; } /* Build booking URL with all params */ function buildBookingUrl(service, contact, pet) { if (service === 'grooming') return BOOKING_URLS.grooming; var base = BOOKING_URLS[service]; if (!base) return '#'; var p = { cid: contact.contact_id || '', pet_id: pet.pet_id || '', pet_name: pet.name || '', contact_email: contact.email || '', first_name: contact.first_name || '', last_name: contact.last_name || '', contact_phone: contact.phone || '', safety_status: pet.safety_status || '', safety_reasons: pet.safety_reasons || '', vax_status: pet.vax_status || '', vax_expiring_soon: pet.vax_expiring_soon || 'no', profile_photo: pet.profile_photo || '', location_pref: pet.location_pref || 'east' }; if (service === 'daycamp' || service === 'training') { p.birthday = pet.birthday || ''; } if (service === 'boarding') { p.boarding_allowed = pet.boarding_allowed || ''; } var qs = []; for (var key in p) { if (p.hasOwnProperty(key) && p[key]) { qs.push(encodeURIComponent(key) + '=' + encodeURIComponent(p[key])); } } return base + '?' + qs.join('&'); } /* Render search results — all DOM built safely via createElement */ function renderBookResults(results) { var container = document.getElementById('bookResults'); while (container.firstChild) container.removeChild(container.firstChild); bookSelectedPets = []; bookLastResults = results || []; updateBookActionBar(); if (!results || results.length === 0) { showBookEmpty('\ud83d\udeab', 'No matches found.'); return; } /* Sort: contacts with pets first, then alphabetical by last name */ results.sort(function(a, b) { var aPets = (a.pets || []).length; var bPets = (b.pets || []).length; if (aPets > 0 && bPets === 0) return -1; if (bPets > 0 && aPets === 0) return 1; var aName = ((a.last_name || '') + ' ' + (a.first_name || '')).toLowerCase(); var bName = ((b.last_name || '') + ' ' + (b.first_name || '')).toLowerCase(); return aName < bName ? -1 : aName > bName ? 1 : 0; }); for (var i = 0; i < results.length; i++) { var r = results[i]; var card = document.createElement('div'); card.className = 'book-result-card'; /* Owner row */ var ownerRow = document.createElement('div'); ownerRow.className = 'book-owner-row'; var ownerName = document.createElement('span'); ownerName.className = 'book-owner-name'; ownerName.textContent = (r.first_name || '') + ' ' + (r.last_name || ''); ownerRow.appendChild(ownerName); /* Profile button */ var profileBtn = document.createElement('button'); profileBtn.style.cssText = 'background:none;border:1.5px solid var(--blue);color:var(--blue);padding:4px 10px;border-radius:var(--r-full);font-family:\'DM Sans\',sans-serif;font-size:.72rem;font-weight:700;cursor:pointer;white-space:nowrap;margin-left:auto;flex-shrink:0;transition:all .15s'; profileBtn.textContent = '\ud83d\udc64 Profile'; profileBtn.onmouseover = function() { this.style.background = 'var(--blue-pale)'; }; profileBtn.onmouseout = function() { this.style.background = 'none'; }; (function(contact) { profileBtn.onclick = function(e) { e.stopPropagation(); showProfileInPanel(contact); }; })(r); ownerRow.appendChild(profileBtn); if (r.phone) { var phoneLink = document.createElement('a'); phoneLink.className = 'book-owner-phone'; phoneLink.href = 'tel:' + r.phone; phoneLink.textContent = formatPhoneDisplay(r.phone); phoneLink.onclick = function(e) { e.stopPropagation(); }; ownerRow.appendChild(phoneLink); } if (r.email) { var emailEl = document.createElement('span'); emailEl.className = 'book-owner-email'; emailEl.textContent = r.email; ownerRow.appendChild(emailEl); } card.appendChild(ownerRow); /* Pets */ var pets = r.pets || []; if (pets.length === 0) { var noPets = document.createElement('div'); noPets.style.cssText = 'padding:10px 14px;'; var noPetsText = document.createElement('span'); noPetsText.style.cssText = 'color:var(--ink-soft);font-size:.82rem;'; noPetsText.textContent = 'No dogs on file. '; noPets.appendChild(noPetsText); var addDogLink = document.createElement('a'); addDogLink.className = 'book-add-dog'; addDogLink.href = 'https://book.sprockettsddc.ca/members/mydogs?cid=' + encodeURIComponent(r.contact_id || ''); addDogLink.target = '_blank'; addDogLink.rel = 'noopener noreferrer'; addDogLink.textContent = 'Add a Dog \u2192'; noPets.appendChild(addDogLink); card.appendChild(noPets); } else { for (var j = 0; j < pets.length; j++) { card.appendChild(buildBookPetRow(r, pets[j])); } } container.appendChild(card); } } function buildBookPetRow(contact, pet) { var row = document.createElement('div'); row.className = 'book-pet-row'; /* Checkbox for multi-select */ var safetyLowerCheck = (pet.safety_status || '').toLowerCase(); var isHoldCheck = (safetyLowerCheck === 'hold' || safetyLowerCheck === 'failed' || safetyLowerCheck === ''); if (!isHoldCheck) { var checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'book-pet-check'; checkbox.setAttribute('aria-label', 'Select ' + (pet.name || 'this dog') + ' for booking'); checkbox.setAttribute('data-pet-id', pet.pet_id || ''); (function(cb, c, p, r) { cb.onchange = function() { if (cb.checked) { bookSelectedPets.push({ contact: c, pet: p }); r.classList.add('selected'); } else { bookSelectedPets = bookSelectedPets.filter(function(s) { return s.pet.pet_id !== p.pet_id; }); r.classList.remove('selected'); } updateBookActionBar(); }; })(checkbox, contact, pet, row); row.appendChild(checkbox); } /* Photo */ var photo = document.createElement('img'); photo.className = 'book-pet-photo'; photo.alt = cleanPetDisplayName(pet.name) || ''; photo.src = (pet.profile_photo && pet.profile_photo.indexOf('http') === 0) ? pet.profile_photo : DOG_FALLBACK; photo.onerror = function() { this.src = DOG_FALLBACK; }; row.appendChild(photo); /* Info */ var info = document.createElement('div'); info.className = 'book-pet-info'; var nameEl = document.createElement('div'); nameEl.className = 'book-pet-name'; nameEl.textContent = cleanPetDisplayName(pet.name) || 'Unknown'; info.appendChild(nameEl); /* Breed */ if (pet.breed) { var breedEl = document.createElement('div'); breedEl.className = 'book-pet-breed'; breedEl.textContent = pet.breed; info.appendChild(breedEl); } /* Birthday check in search results */ if (pet.birthday) { var bToday = new Date(); var bP = pet.birthday.split('-'); if (bP.length === 3 && parseInt(bP[1]) === (bToday.getMonth() + 1) && parseInt(bP[2]) === bToday.getDate()) { var bdBadge2 = document.createElement('span'); bdBadge2.className = 'birthday-badge'; bdBadge2.textContent = 'Birthday! Turning ' + (bToday.getFullYear() - parseInt(bP[0])); info.appendChild(bdBadge2); } } var meta = document.createElement('div'); meta.className = 'book-pet-meta'; meta.appendChild(safetyBadge(pet.safety_status)); /* Vax status badge */ if (pet.vax_status) { var vaxBadge = document.createElement('span'); var vs = (pet.vax_status || '').toUpperCase(); if (vs === 'ALL_CURRENT' || vs === 'CURRENT') { vaxBadge.className = 'book-vax-badge current'; vaxBadge.textContent = 'VAX OK'; } else if (vs.indexOf('EXPIR') > -1 && vs.indexOf('EXPIRED') === -1) { vaxBadge.className = 'book-vax-badge expiring'; vaxBadge.textContent = 'VAX EXPIRING'; } else if (vs.indexOf('EXPIRED') > -1 || vs.indexOf('MISSING') > -1) { vaxBadge.className = 'book-vax-badge expired'; vaxBadge.textContent = 'VAX EXPIRED'; } else { vaxBadge.className = 'book-vax-badge unknown'; vaxBadge.textContent = 'VAX ?'; } meta.appendChild(vaxBadge); } if (pet.location_pref) { var locPill = document.createElement('span'); locPill.className = 'book-pet-loc'; locPill.textContent = pet.location_pref; meta.appendChild(locPill); } info.appendChild(meta); /* HOLD message */ if (isHoldCheck) { var holdMsg = document.createElement('div'); holdMsg.className = 'book-hold-msg'; holdMsg.textContent = safetyLowerCheck === '' ? 'Pending evaluation \u2014 cannot book yet' : 'On hold \u2014 must resolve before booking'; info.appendChild(holdMsg); } row.appendChild(info); /* Service buttons */ var btns = document.createElement('div'); btns.className = 'book-svc-btns'; var services = ['daycamp', 'grooming', 'boarding', 'training']; var svcLabels = { daycamp: 'Daycamp', grooming: 'Grooming', boarding: 'Boarding', training: 'Training' }; for (var s = 0; s < services.length; s++) { var svc = services[s]; var btn = document.createElement('a'); btn.className = 'book-svc-btn svc-' + svc; btn.textContent = svcLabels[svc]; btn.setAttribute('role', 'button'); if (isHoldCheck) { btn.classList.add('disabled'); btn.setAttribute('aria-disabled', 'true'); btn.setAttribute('aria-label', svcLabels[svc] + ' \u2014 on hold'); } else { var missing = getMissingContracts(contact.contracts, svc); if (missing.length > 0) { btn.classList.add('needs-contract'); btn.setAttribute('aria-disabled', 'true'); btn.setAttribute('aria-label', svcLabels[svc] + ' \u2014 needs ' + missing.join(', ')); btn.setAttribute('title', 'Needs: ' + missing.join(', ')); btn.onclick = (function(m, svcName) { return function(e) { e.preventDefault(); showToast('Before booking ' + svcName + ': needs ' + m.join(', '), 'warning', 4000); }; })(missing, svcLabels[svc]); } else { btn.href = buildBookingUrl(svc, contact, pet); btn.target = '_blank'; btn.rel = 'noopener noreferrer'; btn.setAttribute('aria-label', 'Book ' + svcLabels[svc] + ' for ' + (pet.name || 'this dog')); } } btns.appendChild(btn); } /* Show a visible tip if ANY service needs contracts */ if (!isHoldCheck) { var allMissing = {}; for (var ms = 0; ms < services.length; ms++) { var mList = getMissingContracts(contact.contracts, services[ms]); for (var mi = 0; mi < mList.length; mi++) allMissing[mList[mi]] = true; } var missingNames = Object.keys(allMissing); if (missingNames.length > 0) { var tip = document.createElement('div'); tip.className = 'book-contract-tip'; tip.textContent = 'Needs: ' + missingNames.join(', '); row.appendChild(btns); row.appendChild(tip); return row; } } row.appendChild(btns); return row; } /* Update action bar visibility + count */ function updateBookActionBar() { var bar = document.getElementById('bookActionBar'); var count = bookSelectedPets.length; if (count >= 1) { bar.classList.add('visible'); var label = count === 1 ? '1 dog selected' : count + ' dogs selected'; document.getElementById('bookActionCount').textContent = label; } else { bar.classList.remove('visible'); } } /* Book all selected dogs for a service — opens one tab per dog */ function bookSelectedDogs(service) { if (bookSelectedPets.length === 0) return; var totalDogs = bookSelectedPets.length; var isMulti = totalDogs > 1; for (var i = 0; i < bookSelectedPets.length; i++) { var sel = bookSelectedPets[i]; var contact = sel.contact; var pet = sel.pet; /* Check contracts for this service */ var missing = getMissingContracts(contact.contracts, service); if (missing.length > 0) { showToast((pet.name || 'Dog') + ' needs: ' + missing.join(', '), 'warning', 4000); continue; } /* Build URL with multi-dog params */ var url = buildBookingUrl(service, contact, pet); if (isMulti && service !== 'grooming') { url += (url.indexOf('?') >= 0 ? '&' : '?') + 'is_multi_dog=yes&sibling_count=' + totalDogs + '&additional_dogs=' + (totalDogs - 1); } window.open(url, '_blank', 'noopener,noreferrer'); } } /* Search handler with debounce */ function handleBookSearch() { var input = document.getElementById('bookSearchInput'); var query = input.value.trim(); var clearBtn = document.getElementById('bookSearchClear'); clearBtn.classList[query.length > 0 ? 'add' : 'remove']('visible'); if (bookSearchTimer) clearTimeout(bookSearchTimer); if (query.length < 2) { showBookEmpty('\ud83d\udd0d', 'Type at least 2 characters to search'); return; } bookSearchTimer = setTimeout(function() { showBookLoading(); callWebhook('/webhook/staff-board', { action: 'search_client', query: query }, function(err, data) { if (err) { showBookError(); return; } var results = (data && data.results) ? data.results : []; renderBookResults(results); }); }, 300); } /* Old Client Lookup removed — merged into Search panel above */ /* Keeping cl-* CSS classes for profile view rendering */ /* Keyboard: Escape to close panels/modal */ document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { var petOverlay = document.getElementById('petModalOverlay'); if (petOverlay && petOverlay.classList.contains('open')) { closePetModal(); e.preventDefault(); return; } var bookPanel = document.getElementById('bookPanel'); if (bookPanel && bookPanel.classList.contains('open')) { closeBookPanel(); e.preventDefault(); } } /* Focus trap for pet modal */ if (e.key === 'Tab') { var overlay = document.getElementById('petModalOverlay'); if (overlay && overlay.classList.contains('open')) { trapFocusInModal(e); } } }); /* ABBREVIATE PLAY AREA NAME */ function abbreviateArea(name) { if (!name) return ''; /* Shorten common long names */ var n = name.replace(/\s+/g, ' ').trim(); /* e.g. "Big Camper East" → "Big Camper E", "Serenity West" → "Serenity W" */ n = n.replace(/\bEast\b/gi, 'E').replace(/\bWest\b/gi, 'W'); return n; } /* PET DETAIL MODAL */ var petModalTrigger = null; /* element that opened modal — return focus on close */ var petModalData = null; /* current dog data for edit mode */ var petModalEditMode = false; /* Play area options for edit mode */ var PLAY_AREA_OPTIONS = [ { key: 'big_camper_east', label: 'Big Camper Yard', loc: 'east' }, { key: 'lil_camper_east', label: 'Lil Camper Yard', loc: 'east' }, { key: 'serenity_east', label: 'Serenity Yard', loc: 'east' }, { key: 'quiet_east', label: 'Quiet Yard', loc: 'east' }, { key: 'bunkhouse_east', label: 'Bunkhouse', loc: 'east' }, { key: 'the_spaw_east', label: 'The Spaw', loc: 'east' }, { key: 'grub_room_east', label: 'Grub Room', loc: 'east' }, { key: 'camp_office_east', label: 'Camp Office', loc: 'east' }, { key: 'big_camper_west', label: 'Big Camper Yard', loc: 'west' }, { key: 'lil_camper_west', label: 'Lil Camper Yard', loc: 'west' }, { key: 'serenity_west', label: 'Serenity Yard', loc: 'west' }, { key: 'quiet_west', label: 'Quiet Yard', loc: 'west' }, { key: 'bunkhouse_west', label: 'Bunkhouse', loc: 'west' }, { key: 'the_spaw_west', label: 'The Spaw', loc: 'west' }, { key: 'grub_room_west', label: 'Grub Room', loc: 'west' }, { key: 'camp_office_west', label: 'Camp Office', loc: 'west' }, { key: 'the_lodge', label: 'The Lodge', loc: '' } ]; function openPetModal(dog) { dog = parseDog(dog); petModalData = dog; petModalEditMode = false; petModalTrigger = document.activeElement; document.getElementById('pmEditBtn').classList.remove('active'); document.getElementById('pmEditFooter').style.display = 'none'; var saveMsg = document.getElementById('pmSaveMsg'); if (saveMsg) saveMsg.remove(); /* Header */ var photo = document.getElementById('pmPhoto'); photo.src = dog.photo_url || DOG_FALLBACK; photo.onerror = function() { this.src = DOG_FALLBACK; }; photo.alt = dog._parsedName || ''; document.getElementById('pmName').textContent = dog._parsedName || 'Unknown'; var breedAge = []; if (dog.breed) breedAge.push(dog.breed); if (dog.gender) breedAge.push(dog.gender); if (dog.age_display) breedAge.push(dog.age_display); else if (dog.birthday) { var ageStr = calcAge(dog.birthday); if (ageStr) breedAge.push(ageStr); } if (dog.weight) breedAge.push(formatWeight(dog.weight)); document.getElementById('pmBreedAge').textContent = breedAge.join(' \u00b7 ') || '\u2014'; /* Safety badge in header */ var badgeContainer = document.getElementById('pmSafetyBadge'); while (badgeContainer.firstChild) badgeContainer.removeChild(badgeContainer.firstChild); badgeContainer.appendChild(safetyBadge(dog.safety_status)); /* Build body content */ var body = document.getElementById('pmBody'); while (body.firstChild) body.removeChild(body.firstChild); /* 0. TODAY'S VISIT — service-aware context */ var svcCat = (dog.service_category || '').toLowerCase(); var svcType = dog.service_type || ''; if (svcCat || svcType) { var visitSection = buildModalSection("Today's Visit"); var visitGrid = document.createElement('div'); visitGrid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px;'; function addVisitItem(label, value, color) { if (!value) return; var item = document.createElement('div'); item.style.cssText = 'padding:8px 12px;background:' + (color || '#f7f7f8') + ';border-radius:8px;'; var l = document.createElement('div'); l.style.cssText = 'font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#999;margin-bottom:2px;'; l.textContent = label; item.appendChild(l); var v = document.createElement('div'); v.style.cssText = 'font-size:.88rem;font-weight:600;color:#222;'; v.textContent = value; item.appendChild(v); visitGrid.appendChild(item); } if (svcCat === 'daycare') { addVisitItem('Service', svcType || 'Daycamp', '#E3F2FD'); addVisitItem('Location', dog.location_display || dog.pipeline_name || '', '#E3F2FD'); if (dog.dropoff_window) addVisitItem('Drop-off', dog.dropoff_window); if (dog.pickup_window) addVisitItem('Pickup', dog.pickup_window); addVisitItem('Payment', dog.package_label || 'Pay-as-you-go'); } else if (svcCat === 'grooming') { addVisitItem('Service', svcType || 'Grooming', '#F3E5F5'); addVisitItem('Location', dog.location_display || dog.pipeline_name || '', '#F3E5F5'); if (dog.groomer_name || dog.assigned_to_name) addVisitItem('Groomer', dog.groomer_name || dog.assigned_to_name); if (dog.monetary_value) addVisitItem('Quoted', '$' + parseFloat(dog.monetary_value).toFixed(2)); if (dog.grooming_notes) { var gNotes = document.createElement('div'); gNotes.style.cssText = 'grid-column:1/-1;padding:8px 12px;background:#faf5ff;border-radius:8px;font-size:.85rem;color:#555;'; gNotes.textContent = dog.grooming_notes; visitGrid.appendChild(gNotes); } } else if (svcCat === 'boarding') { addVisitItem('Service', svcType || 'Boarding', '#FFE0B2'); addVisitItem('Location', 'East \u2014 975 Thomas Ave', '#FFE0B2'); /* Stay progress — always show */ if (dog.night_current && dog.night_total) { addVisitItem('Stay', 'Night ' + dog.night_current + ' of ' + dog.night_total, '#FFE0B2'); } else if (dog.boarding_nights_booked || dog.night_total) { addVisitItem('Nights', (dog.boarding_nights_booked || dog.night_total) + ' booked', '#FFE0B2'); } else { addVisitItem('Nights', '\u2014', '#FFE0B2'); } addVisitItem('Check-in', dog.boarding_start_date ? formatDate(dog.boarding_start_date) : '\u2014'); addVisitItem('Checkout', dog.boarding_end_date ? formatDate(dog.boarding_end_date) : '\u2014'); /* Boarding care details — always show section */ var bdCareItems = [ { icon: '\ud83c\udf7d\ufe0f', label: 'Feeding', value: dog.feeding_instructions || dog.food_type || '', bg: '#FFF8E1' }, { icon: '\ud83d\udc8a', label: 'Meds', value: dog.medication_details || dog.medications || '', bg: '#FEF2F2', bold: true }, { icon: '\u26a0\ufe0f', label: 'Allergies', value: dog.allergies || '', bg: '#FEF2F2', bold: true }, { icon: '\ud83d\udcde', label: 'Emergency', value: (dog.boarding_emergency_name || dog.emergency_contact || '') + (dog.boarding_emergency_phone ? ' ' + formatPhoneDisplay(dog.boarding_emergency_phone) : ''), bg: '#f7f7f8' }, { icon: '\ud83d\udc64', label: 'Alt Pickup', value: (dog.alt_pickup_person || '') + (dog.pickup_password ? ' (PW: ' + dog.pickup_password + ')' : ''), bg: '#f7f7f8' } ]; for (var bci = 0; bci < bdCareItems.length; bci++) { var ci = bdCareItems[bci]; var ciEl = document.createElement('div'); ciEl.style.cssText = 'grid-column:1/-1;padding:8px 12px;background:' + ci.bg + ';border-radius:8px;font-size:.85rem;color:' + (ci.bold ? '#B91C1C;font-weight:600' : '#555') + ';'; ciEl.textContent = ci.icon + ' ' + ci.label + ': ' + (ci.value || 'None on file'); visitGrid.appendChild(ciEl); } } else if (svcCat === 'training') { addVisitItem('Service', svcType || 'Training', '#E8F5E9'); addVisitItem('Location', dog.location_display || dog.pipeline_name || '', '#E8F5E9'); } visitSection.appendChild(visitGrid); body.appendChild(visitSection); } /* 1. ALERTS — Medical/Meds/Allergies shown first if present (high priority for staff) */ var hasMedAlert = dog.medications || dog.has_medication || dog.allergies; if (hasMedAlert) { var alertSection = buildModalSection('Alerts'); var alertBox = document.createElement('div'); alertBox.style.cssText = 'background:#FEF2F2;border:1.5px solid #FECACA;border-radius:10px;padding:12px 14px;'; if (dog.medications || dog.has_medication) { var medLine = document.createElement('div'); medLine.style.cssText = 'font-size:.88rem;font-weight:600;color:#B91C1C;margin-bottom:4px;'; medLine.textContent = '\ud83d\udc8a Medications: ' + (dog.medications || dog.medication_details || 'Yes \u2014 check pet record for details'); alertBox.appendChild(medLine); } if (dog.allergies) { var allergyLine = document.createElement('div'); allergyLine.style.cssText = 'font-size:.88rem;font-weight:600;color:#B91C1C;margin-bottom:4px;'; allergyLine.textContent = '\u26a0\ufe0f Allergies: ' + dog.allergies; alertBox.appendChild(allergyLine); } alertSection.appendChild(alertBox); body.appendChild(alertSection); } /* 1b. SPECIAL NOTES — shown early if present */ var hasNotes = dog.special_notes || dog.medical_notes || dog.behaviour_notes; if (hasNotes) { var notesSection = buildModalSection('Notes'); if (dog.special_notes || dog.medical_notes) { var noteBox = document.createElement('div'); noteBox.className = 'pm-notes-box'; noteBox.textContent = dog.special_notes || dog.medical_notes; notesSection.appendChild(noteBox); } if (dog.behaviour_notes && dog.behaviour_notes !== (dog.special_notes || dog.medical_notes)) { var bNoteBox = document.createElement('div'); bNoteBox.className = 'pm-notes-box'; bNoteBox.style.marginTop = '6px'; bNoteBox.textContent = '\ud83d\udcdd Behaviour: ' + dog.behaviour_notes; notesSection.appendChild(bNoteBox); } body.appendChild(notesSection); } /* 2. PLAY AREAS */ var playAreas = dog.play_areas || dog.assigned_yards || []; if (typeof playAreas === 'string' && playAreas) { playAreas = playAreas.split(',').map(function(s) { return s.trim(); }).filter(Boolean); } if (playAreas && playAreas.length > 0) { var playSection = buildModalSection('Play Areas'); var pillWrap = document.createElement('div'); pillWrap.className = 'pm-play-pills'; for (var pa = 0; pa < playAreas.length; pa++) { var pill = document.createElement('span'); var isWestArea = /west|w$/i.test(playAreas[pa]); pill.className = 'pm-play-pill ' + (isWestArea ? 'west' : 'east'); pill.textContent = playAreas[pa]; pillWrap.appendChild(pill); } playSection.appendChild(pillWrap); body.appendChild(playSection); } /* 3. CARE PROFILE */ var hasCareData = dog.feeding_instructions || dog.food_type || dog.lunch_nap || dog.allowed_treats || dog.kennel_trained; if (hasCareData) { var careSection = buildModalSection('Care Profile'); /* Feeding info */ if (dog.feeding_instructions || dog.food_type) { var feedBox = document.createElement('div'); feedBox.className = 'pm-notes-box'; feedBox.style.cssText = 'background:#FFFDE7;border:1px solid #FFF9C4;'; feedBox.textContent = '\ud83c\udf7d\ufe0f ' + (dog.feeding_instructions || dog.food_type); careSection.appendChild(feedBox); } /* Flags */ var flags = document.createElement('div'); flags.className = 'pm-flags'; if (dog.lunch_nap !== undefined && dog.lunch_nap !== null && dog.lunch_nap !== '') { var napFlag = document.createElement('span'); var napVal = isTruthy(dog.lunch_nap); napFlag.className = 'pm-flag ' + (napVal ? 'yes' : 'no'); napFlag.textContent = napVal ? 'Lunch Nap: Yes' : 'Lunch Nap: No'; flags.appendChild(napFlag); } if (dog.allowed_treats !== undefined && dog.allowed_treats !== null && dog.allowed_treats !== '') { var treatFlag = document.createElement('span'); var treatVal = isTruthy(dog.allowed_treats); treatFlag.className = 'pm-flag ' + (treatVal ? 'yes' : 'no'); treatFlag.textContent = treatVal ? 'Treats: OK' : 'Treats: No'; flags.appendChild(treatFlag); } if (dog.kennel_trained !== undefined && dog.kennel_trained !== null && dog.kennel_trained !== '') { var kennelFlag = document.createElement('span'); var kennelVal = isTruthy(dog.kennel_trained); kennelFlag.className = 'pm-flag ' + (kennelVal ? 'yes' : 'no'); kennelFlag.textContent = kennelVal ? 'Kennel Trained' : 'Not Kennel Trained'; flags.appendChild(kennelFlag); } if (flags.childNodes.length > 0) careSection.appendChild(flags); body.appendChild(careSection); } /* 3. MEDICAL — now shown as Alerts section at top; vet info remains here */ var hasVetInfo = dog.vet_clinic || dog.vet_phone; if (hasVetInfo) { var vetSection = buildModalSection('Vet'); if (dog.vet_clinic) vetSection.appendChild(buildModalRow('\ud83c\udfe5', 'Clinic', dog.vet_clinic)); if (dog.vet_phone) vetSection.appendChild(buildModalRow('\ud83d\udcde', 'Vet Phone', dog.vet_phone)); body.appendChild(vetSection); } /* 4. STATUS — Vax + Temperament */ var hasVaxData = dog.rabies_expiry || dog.distemper_expiry || dog.parvo_expiry || dog.bordetella_expiry; var hasTemperament = dog.temperament_status || dog.temperament_test_result_date; if (hasVaxData || hasTemperament) { var statusSection = buildModalSection('Status'); if (hasVaxData) { var vaxGrid = document.createElement('div'); vaxGrid.className = 'pm-vax-grid'; var vaxTypes = [ { name: 'Rabies', date: dog.rabies_expiry }, { name: 'Distemper', date: dog.distemper_expiry }, { name: 'Parvovirus', date: dog.parvo_expiry }, { name: 'Bordetella', date: dog.bordetella_expiry } ]; var now = new Date(); var soon = new Date(); soon.setDate(soon.getDate() + 30); for (var v = 0; v < vaxTypes.length; v++) { if (!vaxTypes[v].date) continue; var vItem = document.createElement('div'); vItem.className = 'pm-vax-item'; var vDate = new Date(vaxTypes[v].date); if (!isNaN(vDate.getTime())) { if (vDate < now) vItem.classList.add('expired'); else if (vDate < soon) vItem.classList.add('expiring'); } var vName = document.createElement('span'); vName.className = 'vax-name'; vName.textContent = vaxTypes[v].name; vItem.appendChild(vName); var vDateEl = document.createElement('span'); vDateEl.className = 'vax-date'; vDateEl.textContent = formatDate(vaxTypes[v].date); if (!isNaN(vDate.getTime()) && vDate < now) vDateEl.textContent += ' (EXPIRED)'; else if (!isNaN(vDate.getTime()) && vDate < soon) vDateEl.textContent += ' (EXPIRING)'; vItem.appendChild(vDateEl); vaxGrid.appendChild(vItem); } if (vaxGrid.childNodes.length > 0) statusSection.appendChild(vaxGrid); } if (hasTemperament) { var tempWrap = document.createElement('div'); tempWrap.style.marginTop = hasVaxData ? '8px' : '0'; var tempBadge = document.createElement('span'); var ts = (dog.temperament_status || '').toLowerCase(); if (ts === 'passed' || ts === 'pass') tempBadge.className = 'pm-temp-badge passed'; else if (ts === 'conditional') tempBadge.className = 'pm-temp-badge conditional'; else if (ts === 'not suitable' || ts === 'not_suitable' || ts === 'failed') tempBadge.className = 'pm-temp-badge not-suitable'; else tempBadge.className = 'pm-temp-badge'; tempBadge.textContent = 'Temperament: ' + (dog.temperament_status || 'Pending'); if (dog.temperament_test_result_date) { tempBadge.textContent += ' (' + formatDate(dog.temperament_test_result_date) + ')'; } tempWrap.appendChild(tempBadge); statusSection.appendChild(tempWrap); } body.appendChild(statusSection); } /* 5. BOARDING (conditional) */ var hasBoarding = dog.emergency_contact || dog.alt_pickup_person || dog.pickup_password || dog.owner_away_contact; if (hasBoarding) { var bdSection = buildModalSection('Boarding'); var bdGrid = document.createElement('div'); bdGrid.className = 'pm-boarding-grid'; if (dog.emergency_contact) bdGrid.appendChild(buildBoardingItem('Emergency Contact', formatEmergencyContactDisplay(dog.emergency_contact))); if (dog.alt_pickup_person) bdGrid.appendChild(buildBoardingItem('Alt Pickup', dog.alt_pickup_person)); if (dog.pickup_password) bdGrid.appendChild(buildBoardingItem('Pickup Password', dog.pickup_password)); if (dog.owner_away_contact) bdGrid.appendChild(buildBoardingItem('Owner Away Contact', dog.owner_away_contact)); bdSection.appendChild(bdGrid); body.appendChild(bdSection); } /* 5b. BOARDING CARE PROFILE LINK — only for boarding visits */ if (svcCat === 'boarding' || hasBoarding) { var careLink = document.createElement('div'); careLink.style.cssText = 'margin-bottom:16px;text-align:center;'; var careBtn = document.createElement('a'); careBtn.href = 'https://book.sprockettsddc.ca/members/boarding-care?pet_name=' + encodeURIComponent(dog._parsedName || '') + '&pet_id=' + encodeURIComponent(dog.pet_object_id || dog.pet_id || '') + '&cid=' + encodeURIComponent(dog.contact_id || ''); careBtn.target = '_blank'; careBtn.rel = 'noopener'; careBtn.style.cssText = 'display:inline-flex;align-items:center;gap:6px;padding:10px 18px;background:#f0f7ff;border:2px solid #1974CE;border-radius:10px;color:#1974CE;font-weight:600;font-size:0.88rem;text-decoration:none;transition:all 0.2s;'; careBtn.onmouseover = function() { this.style.background = '#e0efff'; }; careBtn.onmouseout = function() { this.style.background = '#f0f7ff'; }; careBtn.textContent = '\ud83d\udccb Boarding Care Profile' + (dog.boarding_care_profile_updated_on ? ' \u2714' : ''); careLink.appendChild(careBtn); body.appendChild(careLink); } /* 6. OWNER */ var ownerSection = buildModalSection('Owner'); var ownerRow = document.createElement('div'); ownerRow.className = 'pm-owner-row'; var ownerContact = document.createElement('div'); ownerContact.className = 'pm-owner-contact'; var owName = document.createElement('div'); owName.className = 'pm-owner-name'; owName.textContent = dog.owner_name || dog.owner_last_name || '\u2014'; ownerContact.appendChild(owName); var owPhone = dog.owner_phone || ''; if (owPhone) { var owPhoneLink = document.createElement('a'); owPhoneLink.className = 'pm-owner-link'; owPhoneLink.href = 'tel:' + owPhone; owPhoneLink.textContent = '\ud83d\udcde ' + formatPhoneDisplay(owPhone); ownerContact.appendChild(owPhoneLink); } var owEmail = dog.owner_email || ''; if (owEmail) { var owEmailLink = document.createElement('a'); owEmailLink.className = 'pm-owner-link'; owEmailLink.href = 'mailto:' + owEmail; owEmailLink.textContent = '\u2709\ufe0f ' + owEmail; ownerContact.appendChild(owEmailLink); } /* Emergency contact */ var ecName = dog.emergency_contact || dog.ec_name || ''; var ecPhone = dog.emergency_contact_phone || dog.ec_phone || ''; if (ecName || ecPhone) { var ecWrap = document.createElement('div'); ecWrap.style.marginTop = '8px'; var ecLabel = document.createElement('div'); ecLabel.style.cssText = 'font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#999;margin-bottom:3px'; ecLabel.textContent = 'Emergency Contact'; ecWrap.appendChild(ecLabel); if (ecName) { var ecNameEl = document.createElement('div'); ecNameEl.style.cssText = 'font-size:.9rem;font-weight:600;color:var(--ink)'; ecNameEl.textContent = ecName; ecWrap.appendChild(ecNameEl); } if (ecPhone) { var ecPhLink = document.createElement('a'); ecPhLink.className = 'pm-owner-link'; ecPhLink.href = 'tel:' + ecPhone; ecPhLink.textContent = '\ud83d\udcde ' + formatPhoneDisplay(ecPhone); ecWrap.appendChild(ecPhLink); } ownerContact.appendChild(ecWrap); } ownerRow.appendChild(ownerContact); ownerSection.appendChild(ownerRow); body.appendChild(ownerSection); /* Care Scorecard button (v1.13) — only for active visit cards (need opp_id) */ if (typeof injectCareScorecardButton === 'function' && dog.opp_id) { injectCareScorecardButton(body, dog); } /* Show modal */ var overlay = document.getElementById('petModalOverlay'); overlay.classList.add('open'); document.body.style.overflow = 'hidden'; /* Focus close button */ setTimeout(function() { document.getElementById('pmClose').focus(); }, 100); } function closePetModal() { var overlay = document.getElementById('petModalOverlay'); overlay.classList.remove('open'); document.body.style.overflow = ''; /* Return focus to trigger element */ if (petModalTrigger && petModalTrigger.focus) { try { petModalTrigger.focus(); } catch(e) {} } petModalTrigger = null; } /* Close on overlay click (not modal content) */ (function() { var overlay = document.getElementById('petModalOverlay'); if (overlay) { overlay.addEventListener('click', function(e) { if (e.target === overlay) closePetModal(); }); } var closeBtn = document.getElementById('pmClose'); if (closeBtn) closeBtn.addEventListener('click', closePetModal); var editBtn = document.getElementById('pmEditBtn'); if (editBtn) editBtn.addEventListener('click', function() { if (petModalEditMode) exitEditMode(); else enterEditMode(); }); var saveBtn = document.getElementById('pmSaveBtn'); if (saveBtn) saveBtn.addEventListener('click', savePetEdits); var cancelBtn = document.getElementById('pmCancelBtn'); if (cancelBtn) cancelBtn.addEventListener('click', exitEditMode); })(); /* Focus trap */ function trapFocusInModal(e) { var modal = document.getElementById('petModal'); if (!modal) return; var focusable = modal.querySelectorAll('a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'); if (focusable.length === 0) return; var first = focusable[0]; var last = focusable[focusable.length - 1]; if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); last.focus(); } } else { if (document.activeElement === last) { e.preventDefault(); first.focus(); } } } /* EDIT MODE */ function enterEditMode() { if (!petModalData) return; petModalEditMode = true; document.getElementById('pmEditBtn').classList.add('active'); document.getElementById('pmEditFooter').style.display = 'flex'; var saveMsg = document.getElementById('pmSaveMsg'); if (saveMsg) saveMsg.remove(); var dog = petModalData; var body = document.getElementById('pmBody'); while (body.firstChild) body.removeChild(body.firstChild); /* -- Play Areas -- */ var playSection = buildModalSection('Play Areas'); var currentAreas = dog.play_areas || dog.assigned_play_areas || dog.assigned_yards || []; if (typeof currentAreas === 'string') currentAreas = currentAreas.split(',').map(function(s){return s.trim();}).filter(Boolean); var areaGrid = document.createElement('div'); areaGrid.className = 'pm-play-checkbox-grid'; areaGrid.id = 'editPlayAreas'; /* Group by location */ var eastLabel = document.createElement('div'); eastLabel.className = 'pm-edit-field-label'; eastLabel.textContent = 'EAST'; eastLabel.style.width = '100%'; areaGrid.appendChild(eastLabel); PLAY_AREA_OPTIONS.forEach(function(opt) { if (opt.loc !== 'east') return; var isSelected = currentAreas.indexOf(opt.key) !== -1; var chip = document.createElement('label'); chip.className = 'pm-play-checkbox east' + (isSelected ? ' selected' : ''); var cb = document.createElement('input'); cb.type = 'checkbox'; cb.value = opt.key; cb.checked = isSelected; cb.onchange = function() { chip.classList.toggle('selected', this.checked); }; chip.appendChild(cb); chip.appendChild(document.createTextNode(opt.label)); areaGrid.appendChild(chip); }); var westLabel = document.createElement('div'); westLabel.className = 'pm-edit-field-label'; westLabel.textContent = 'WEST'; westLabel.style.width = '100%'; westLabel.style.marginTop = '8px'; areaGrid.appendChild(westLabel); PLAY_AREA_OPTIONS.forEach(function(opt) { if (opt.loc !== 'west') return; var isSelected = currentAreas.indexOf(opt.key) !== -1; var chip = document.createElement('label'); chip.className = 'pm-play-checkbox west' + (isSelected ? ' selected' : ''); var cb = document.createElement('input'); cb.type = 'checkbox'; cb.value = opt.key; cb.checked = isSelected; cb.onchange = function() { chip.classList.toggle('selected', this.checked); }; chip.appendChild(cb); chip.appendChild(document.createTextNode(opt.label)); areaGrid.appendChild(chip); }); /* Lodge */ var lodgeOpt = PLAY_AREA_OPTIONS.find(function(o){return o.key === 'the_lodge';}); if (lodgeOpt) { var isLodge = currentAreas.indexOf('the_lodge') !== -1; var lodgeChip = document.createElement('label'); lodgeChip.className = 'pm-play-checkbox' + (isLodge ? ' selected' : ''); lodgeChip.style.marginTop = '8px'; var lodgeCb = document.createElement('input'); lodgeCb.type = 'checkbox'; lodgeCb.value = 'the_lodge'; lodgeCb.checked = isLodge; lodgeCb.onchange = function() { lodgeChip.classList.toggle('selected', this.checked); }; lodgeChip.appendChild(lodgeCb); lodgeChip.appendChild(document.createTextNode('The Lodge')); areaGrid.appendChild(lodgeChip); } playSection.appendChild(areaGrid); body.appendChild(playSection); /* -- Care Profile -- */ var careSection = buildModalSection('Care Profile'); careSection.appendChild(buildEditTextarea('Feeding Instructions', 'editFeeding', dog.feeding_instructions || dog.feeding_am || '')); careSection.appendChild(buildEditToggle('Lunch Nap', 'editLunchNap', isTruthy(dog.lunch_nap || dog.feeding_schedule))); careSection.appendChild(buildEditToggle('Allowed Treats', 'editTreats', isTruthy(dog.allowed_treats))); careSection.appendChild(buildEditToggle('Kennel Trained', 'editKennel', isTruthy(dog.kennel_trained || dog.crate_when_sleeping))); body.appendChild(careSection); /* -- Medical -- */ var medSection = buildModalSection('Medical'); medSection.appendChild(buildEditDate('Rabies expiry', 'editRabies', dog.rabies_vax_expire || dog.rabies_expiry || '')); medSection.appendChild(buildEditDate('Distemper expiry', 'editDistemper', dog.distemper_vax_expire || dog.distemper_expiry || '')); medSection.appendChild(buildEditDate('Parvo expiry', 'editParvo', dog.parvovirus_vax_expire || dog.parvo_expiry || '')); medSection.appendChild(buildEditDate('Bordetella expiry', 'editBordetella', dog.bordetella_vax_expire || dog.bordetella_expiry || '')); medSection.appendChild(buildEditToggle('Medication Required', 'editMedRequired', isTruthy(dog.medication_required || dog.has_medication))); medSection.appendChild(buildEditTextarea('Medication Details', 'editMedDetails', dog.medication_details || dog.medications || '')); medSection.appendChild(buildEditText('Allergies', 'editAllergies', dog.allergies || dog.allergies_or_sensitivities || '')); medSection.appendChild(buildEditTextarea('Special Notes', 'editSpecialNotes', dog.special_notes || '')); medSection.appendChild(buildEditTextarea('Behaviour Notes', 'editBehaviourNotes', dog.behaviour_notes || '')); body.appendChild(medSection); /* -- Status -- */ var statusSection = buildModalSection('Status'); var tempSelect = buildEditSelect('Temperament Status', 'editTempStatus', [ { v: '', l: '-- Not Evaluated --' }, { v: 'pass', l: 'Pass' }, { v: 'conditional', l: 'Conditional' }, { v: 'not_suitable', l: 'Not Suitable' } ], (dog.temperament_status || '').toLowerCase() ); statusSection.appendChild(tempSelect); var playgroupSelect = buildEditSelect('Temperament Playgroup', 'editPlaygroup', [ { v: '', l: '-- None --' }, { v: 'standard_group', l: 'Standard Group' }, { v: 'small__calm', l: 'Small & Calm' }, { v: 'highenergy', l: 'High-Energy' }, { v: 'soloplay', l: 'Solo-Play' }, { v: 'staffonly', l: 'Staff-Only (supervised)' } ], Array.isArray(dog.temperament_playgroup) ? (dog.temperament_playgroup[0] || '') : (dog.temperament_playgroup || '') ); statusSection.appendChild(playgroupSelect); statusSection.appendChild(buildEditTextarea('Temperament Notes', 'editTempNotes', dog.temperament_notes || '')); statusSection.appendChild(buildEditText('Weight (lbs)', 'editWeight', dog.weight_lbs || dog.weight || '')); statusSection.appendChild(buildEditText('Vet Clinic', 'editVet', dog.vet_clinic || dog.vet_name || '')); statusSection.appendChild(buildEditText('Vet Phone', 'editVetPhone', dog.vet_phone || '')); body.appendChild(statusSection); /* -- Description -- */ var descSection = buildModalSection('Description'); descSection.appendChild(buildEditTextarea('Pet Description', 'editDescription', dog.pet_description || dog.description || '')); body.appendChild(descSection); /* -- Owner -- */ var ownerEditSection = buildModalSection('Owner'); ownerEditSection.appendChild(buildEditText('Phone', 'editOwnerPhone', dog.owner_phone || '')); ownerEditSection.appendChild(buildEditText('Email', 'editOwnerEmail', dog.owner_email || '')); ownerEditSection.appendChild(buildEditText('Emergency Contact Name', 'editEcName', dog.emergency_contact || dog.ec_name || '')); ownerEditSection.appendChild(buildEditText('Emergency Contact Phone', 'editEcPhone', dog.emergency_contact_phone || dog.ec_phone || '')); body.appendChild(ownerEditSection); /* Scroll to top */ body.scrollTop = 0; /* Phone formatting on edit fields */ bindPhoneField('editOwnerPhone'); bindPhoneField('editVetPhone'); bindPhoneField('editEcPhone'); } function exitEditMode() { petModalEditMode = false; document.getElementById('pmEditBtn').classList.remove('active'); document.getElementById('pmEditFooter').style.display = 'none'; /* Re-render read-only view */ if (petModalData) openPetModal(petModalData); } function savePetEdits() { if (!petModalData) return; var btn = document.getElementById('pmSaveBtn'); btn.disabled = true; btn.textContent = 'Saving...'; /* Gather values */ var fields = {}; /* Play areas */ var areaCheckboxes = document.querySelectorAll('#editPlayAreas input[type="checkbox"]:checked'); var areas = []; areaCheckboxes.forEach(function(cb) { areas.push(cb.value); }); fields.assigned_play_areas = areas; /* Care */ var feedVal = (document.getElementById('editFeeding') || {}).value || ''; if (feedVal.trim()) fields.feeding_instructions = feedVal.trim(); fields.feeding_schedule = document.getElementById('editLunchNap').checked ? ['Lunch Nap'] : []; fields.allowed_treats = document.getElementById('editTreats').checked ? ['Yes'] : []; fields.crate_when_sleeping = document.getElementById('editKennel').checked ? 'Yes' : 'No'; /* Medical — vax dates (empty = don't send, preserves existing value) */ var rabVal = (document.getElementById('editRabies') || {}).value || ''; if (rabVal) fields.rabies_vax_expire = rabVal; var disVal = (document.getElementById('editDistemper') || {}).value || ''; if (disVal) fields.distemper_vax_expire = disVal; var parVal = (document.getElementById('editParvo') || {}).value || ''; if (parVal) fields.parvovirus_vax_expire = parVal; var borVal = (document.getElementById('editBordetella') || {}).value || ''; if (borVal) fields.bordetella_vax_expire = borVal; /* Medical */ fields.medication_required = document.getElementById('editMedRequired').checked ? 'yes' : 'no'; var medDet = (document.getElementById('editMedDetails') || {}).value || ''; if (medDet.trim()) fields.medication_details = medDet.trim(); var allergy = (document.getElementById('editAllergies') || {}).value || ''; if (allergy.trim()) fields.allergies_or_sensitivities = allergy.trim(); var specNotes = (document.getElementById('editSpecialNotes') || {}).value || ''; if (specNotes.trim()) fields.special_notes = specNotes.trim(); var behNotes = (document.getElementById('editBehaviourNotes') || {}).value || ''; if (behNotes.trim()) fields.behaviour_notes = behNotes.trim(); /* Status */ var tempVal = (document.getElementById('editTempStatus') || {}).value || ''; if (tempVal) fields.temperament_status = tempVal; var playgroupVal = (document.getElementById('editPlaygroup') || {}).value || ''; fields.temperament_playgroup = playgroupVal ? [playgroupVal] : []; var tempNotesVal = (document.getElementById('editTempNotes') || {}).value || ''; if (tempNotesVal.trim()) fields.temperament_notes = tempNotesVal.trim(); var weightVal = (document.getElementById('editWeight') || {}).value || ''; if (weightVal) fields.weight_lbs = weightVal; var vetVal = (document.getElementById('editVet') || {}).value || ''; if (vetVal.trim()) fields.vet_clinic = vetVal.trim(); var vetPhVal = (document.getElementById('editVetPhone') || {}).value || ''; if (vetPhVal.trim()) fields.vet_phone = vetPhVal.trim(); /* Description */ var descVal = (document.getElementById('editDescription') || {}).value || ''; if (descVal.trim()) fields.pet_description = descVal.trim(); /* Gather owner fields */ var ownerFields = {}; var ownerPhoneVal = (document.getElementById('editOwnerPhone') || {}).value || ''; var ownerEmailVal = (document.getElementById('editOwnerEmail') || {}).value || ''; var ecNameVal = (document.getElementById('editEcName') || {}).value || ''; var ecPhoneVal = (document.getElementById('editEcPhone') || {}).value || ''; if (ownerPhoneVal.trim()) ownerFields.phone = ownerPhoneVal.trim(); if (ownerEmailVal.trim()) ownerFields.email = ownerEmailVal.trim(); if (ecNameVal.trim()) ownerFields.emergency_contact_name = ecNameVal.trim(); if (ecPhoneVal.trim()) ownerFields.emergency_contact_phone = ecPhoneVal.trim(); /* Get pet_object_id */ var petId = petModalData.pet_object_id || petModalData.id || ''; var contactId = petModalData.contact_id || petModalData.primary_contact_id || ''; var petPromise = fetch(N8N_BASE + '/webhook/PetPortal', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-kaname-webhook-secret': 'kaname_pet_registry_v1_9f3a7c2d' }, body: JSON.stringify({ action: 'portal_update_pet', contact_id: contactId, pet_object_id: petId, fields: fields, source: 'staff_ops' }) }).then(function(r) { return r.json(); }); var savePromises = [petPromise]; if (Object.keys(ownerFields).length > 0 && contactId) { var contactPromise = fetch(N8N_BASE + '/webhook/PetPortal', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-kaname-webhook-secret': 'kaname_pet_registry_v1_9f3a7c2d' }, body: JSON.stringify({ action: 'portal_update_contact', contact_id: contactId, fields: ownerFields }) }).then(function(r) { return r.json(); }); savePromises.push(contactPromise); } Promise.all(savePromises) .then(function(results) { btn.disabled = false; btn.textContent = 'Save Changes'; var petResult = results[0]; if (petResult.ok) { if (petResult.pet) Object.assign(petModalData, petResult.pet); /* Reflect saved owner fields locally */ if (results[1] && results[1].ok) { if (ownerFields.phone) petModalData.owner_phone = ownerFields.phone; if (ownerFields.email) petModalData.owner_email = ownerFields.email; if (ownerFields.emergency_contact_name) petModalData.emergency_contact = ownerFields.emergency_contact_name; if (ownerFields.emergency_contact_phone) petModalData.emergency_contact_phone = ownerFields.emergency_contact_phone; } showSaveMsg('Saved — safety recalculated', 'success'); setTimeout(function() { exitEditMode(); }, 800); } else { showSaveMsg(petResult.error || 'Save failed', 'error'); } }) .catch(function(err) { btn.disabled = false; btn.textContent = 'Save Changes'; showSaveMsg('Network error: ' + err.message, 'error'); }); } function showSaveMsg(text, type) { var existing = document.getElementById('pmSaveMsg'); if (existing) existing.remove(); var msg = document.createElement('div'); msg.id = 'pmSaveMsg'; msg.className = 'pm-save-msg ' + type; msg.textContent = text; var footer = document.getElementById('pmEditFooter'); footer.parentNode.insertBefore(msg, footer); } /* Edit mode form builders */ function buildEditText(label, id, value) { var wrap = document.createElement('div'); wrap.className = 'pm-edit-field'; var lbl = document.createElement('div'); lbl.className = 'pm-edit-field-label'; lbl.textContent = label; wrap.appendChild(lbl); var inp = document.createElement('input'); inp.type = 'text'; inp.className = 'pm-edit-input'; inp.id = id; inp.value = value || ''; wrap.appendChild(inp); return wrap; } function buildEditDate(label, id, value) { var wrap = document.createElement('div'); wrap.className = 'pm-edit-field'; var lbl = document.createElement('div'); lbl.className = 'pm-edit-field-label'; lbl.textContent = label; wrap.appendChild(lbl); var inp = document.createElement('input'); inp.type = 'date'; inp.className = 'pm-edit-input'; inp.id = id; inp.value = (value || '').slice(0, 10); wrap.appendChild(inp); return wrap; } function buildEditTextarea(label, id, value) { var wrap = document.createElement('div'); wrap.className = 'pm-edit-field'; var lbl = document.createElement('div'); lbl.className = 'pm-edit-field-label'; lbl.textContent = label; wrap.appendChild(lbl); var ta = document.createElement('textarea'); ta.className = 'pm-edit-input'; ta.id = id; ta.value = value || ''; ta.rows = 3; wrap.appendChild(ta); return wrap; } function buildEditToggle(label, id, checked) { var wrap = document.createElement('div'); wrap.className = 'pm-edit-toggle'; var cb = document.createElement('input'); cb.type = 'checkbox'; cb.id = id; cb.checked = !!checked; wrap.appendChild(cb); var lbl = document.createElement('label'); lbl.htmlFor = id; lbl.textContent = label; wrap.appendChild(lbl); return wrap; } function buildEditSelect(label, id, options, selected) { var wrap = document.createElement('div'); wrap.className = 'pm-edit-field'; var lbl = document.createElement('div'); lbl.className = 'pm-edit-field-label'; lbl.textContent = label; wrap.appendChild(lbl); var sel = document.createElement('select'); sel.className = 'pm-edit-input'; sel.id = id; options.forEach(function(opt) { var o = document.createElement('option'); o.value = opt.v; o.textContent = opt.l; if (opt.v === selected) o.selected = true; sel.appendChild(o); }); wrap.appendChild(sel); return wrap; } /* Modal builder helpers */ function buildModalSection(title) { var section = document.createElement('div'); section.className = 'pm-section'; var titleEl = document.createElement('div'); titleEl.className = 'pm-section-title'; titleEl.textContent = title; section.appendChild(titleEl); return section; } function buildModalRow(icon, label, value, valueClass) { var row = document.createElement('div'); row.className = 'pm-row'; if (icon) { var iconEl = document.createElement('span'); iconEl.className = 'pm-row-icon'; iconEl.textContent = icon; row.appendChild(iconEl); } var labelEl = document.createElement('span'); labelEl.className = 'pm-row-label'; labelEl.textContent = label; row.appendChild(labelEl); var valEl = document.createElement('span'); valEl.className = 'pm-row-value' + (valueClass ? ' ' + valueClass : ''); valEl.textContent = value || '\u2014'; row.appendChild(valEl); return row; } function buildBoardingItem(label, value) { var item = document.createElement('div'); item.className = 'pm-boarding-item'; var l = document.createElement('div'); l.className = 'pm-boarding-item-label'; l.textContent = label; item.appendChild(l); var v = document.createElement('div'); v.className = 'pm-boarding-item-value'; v.textContent = value || '\u2014'; item.appendChild(v); return item; } function isTruthy(val) { if (!val) return false; var s = ('' + val).toLowerCase().trim(); return s === 'true' || s === 'yes' || s === '1' || s === 'on'; } function calcAge(birthday) { if (!birthday) return ''; var bDate = new Date(birthday); if (isNaN(bDate.getTime())) return ''; var now = new Date(); var totalMonths = (now.getFullYear() - bDate.getFullYear()) * 12 + now.getMonth() - bDate.getMonth(); if (now.getDate() < bDate.getDate()) totalMonths--; if (totalMonths < 0) totalMonths = 0; if (totalMonths < 24) return totalMonths + (totalMonths === 1 ? ' month' : ' months'); return Math.floor(totalMonths / 12) + ' yrs'; } function formatDate(raw) { if (!raw) return '\u2014'; var d = new Date(raw); if (isNaN(d.getTime())) return raw; var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; return months[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear(); } /* ===================== INCIDENT REPORT ===================== */ var irSelectedDogs = []; function getAllCheckedInDogs() { var dogs = []; var board = state.boardData || {}; var seen = {}; function addDogs(arr) { if (!arr) return; for (var i = 0; i < arr.length; i++) { var d = parseDog(arr[i]); var id = d.pet_object_id || d.pet_id || d.opp_id || d._parsedName; if (!id || seen[id]) continue; seen[id] = true; dogs.push({ pet_id: d.pet_object_id || d.pet_id || '', pet_name: d._parsedName || d.pet_name || d.dog_name || '', photo_url: d.photo_url || '', location: d.location || d._loc || '' }); } } /* Daycare — all dogs on-site today (any status) */ if (board.east && board.east.dogs) { addDogs(board.east.dogs); } if (board.west && board.west.dogs) { addDogs(board.west.dogs); } /* Grooming + Training + Boarding from schedule */ var sched = (state.scheduleData && state.scheduleData.schedule) ? state.scheduleData.schedule : {}; if (sched.grooming) { addDogs(sched.grooming.east); addDogs(sched.grooming.west); } if (sched.training) { addDogs(sched.training.east); addDogs(sched.training.west); } if (sched.boarding) { addDogs(sched.boarding.east); } dogs.sort(function(a, b) { return (a.pet_name || '').localeCompare(b.pet_name || ''); }); return dogs; } function openIncidentModal() { irSelectedDogs = []; /* Reset form */ var radios = document.querySelectorAll('#irOverlay .ir-radio'); for (var r = 0; r < radios.length; r++) { radios[r].classList.remove('selected'); radios[r].querySelector('input').checked = false; } document.getElementById('irCategory').value = ''; document.getElementById('irDescription').value = ''; document.getElementById('irPeople').value = ''; document.getElementById('irDogManual').value = ''; document.getElementById('irAction').value = ''; /* Pre-fill location from current filter */ if (state.location === 'east' || state.location === 'west') { var locVal = state.location.charAt(0).toUpperCase() + state.location.slice(1); var locLabels = document.querySelectorAll('#irLocation .ir-radio'); for (var ll = 0; ll < locLabels.length; ll++) { if (locLabels[ll].getAttribute('data-val') === locVal) { locLabels[ll].classList.add('selected'); locLabels[ll].querySelector('input').checked = true; } } } /* Pre-fill reporter from localStorage */ var saved = ''; try { saved = localStorage.getItem('spk_reporter_name') || ''; } catch(e) {} document.getElementById('irReporter').value = saved; /* Populate dog grid */ populateIncidentDogGrid(); /* Show */ document.getElementById('irOverlay').classList.add('open'); document.getElementById('irSubmitBtn').disabled = false; document.getElementById('irSubmitBtn').textContent = 'Submit Report'; } function closeIncidentModal() { document.getElementById('irOverlay').classList.remove('open'); irSelectedDogs = []; } function populateIncidentDogGrid() { var grid = document.getElementById('irDogGrid'); while (grid.firstChild) grid.removeChild(grid.firstChild); var dogs = getAllCheckedInDogs(); if (dogs.length === 0) { var empty = document.createElement('div'); empty.style.cssText = 'color:var(--ink-pale);font-size:.82rem'; empty.textContent = 'No dogs currently on site.'; grid.appendChild(empty); return; } for (var i = 0; i < dogs.length; i++) { (function(dog) { var chip = document.createElement('label'); chip.className = 'ir-dog-chip'; var cb = document.createElement('input'); cb.type = 'checkbox'; chip.appendChild(cb); var img = document.createElement('img'); img.src = (dog.photo_url && dog.photo_url.indexOf('http') === 0) ? dog.photo_url : DOG_FALLBACK; img.alt = dog.pet_name; img.onerror = function() { this.src = DOG_FALLBACK; }; chip.appendChild(img); var span = document.createElement('span'); span.textContent = dog.pet_name; chip.appendChild(span); cb.onchange = function() { if (cb.checked) { chip.classList.add('selected'); irSelectedDogs.push({ pet_id: dog.pet_id, pet_name: dog.pet_name }); } else { chip.classList.remove('selected'); irSelectedDogs = irSelectedDogs.filter(function(s) { return s.pet_id !== dog.pet_id; }); } }; grid.appendChild(chip); })(dogs[i]); } } /* Wire up radio toggles */ (function() { document.addEventListener('click', function(e) { var radio = e.target.closest('.ir-radio'); if (!radio) return; var group = radio.parentElement; if (!group || !group.classList.contains('ir-radio-group')) return; var siblings = group.querySelectorAll('.ir-radio'); for (var s = 0; s < siblings.length; s++) { siblings[s].classList.remove('selected'); siblings[s].querySelector('input').checked = false; } radio.classList.add('selected'); radio.querySelector('input').checked = true; }); })(); function getRadioValue(groupId) { var checked = document.querySelector('#' + groupId + ' input:checked'); return checked ? checked.value : ''; } function submitIncidentReport() { var location = getRadioValue('irLocation'); var category = document.getElementById('irCategory').value; var severity = getRadioValue('irSeverity'); var description = document.getElementById('irDescription').value.trim(); var reporter = document.getElementById('irReporter').value.trim(); /* Validate required fields */ if (!location) { showToast('Select a location.', 'error'); return; } if (!category) { showToast('Select a category.', 'error'); return; } if (!severity) { showToast('Select a severity.', 'error'); return; } if (!description) { showToast('Describe what happened.', 'error'); return; } if (!reporter) { showToast('Enter your name.', 'error'); return; } /* Save reporter name */ try { localStorage.setItem('spk_reporter_name', reporter); } catch(e) {} var btn = document.getElementById('irSubmitBtn'); btn.disabled = true; btn.textContent = 'Submitting...'; /* Add manually typed dog name if provided */ var dogsInvolved = irSelectedDogs.slice(); var manualDog = document.getElementById('irDogManual').value.trim(); if (manualDog) { dogsInvolved.push({ pet_id: '', pet_name: manualDog }); } var payload = { location: location, category: category, severity: severity, description: description, dogs_involved: dogsInvolved, people_involved: document.getElementById('irPeople').value.trim(), action_taken: document.getElementById('irAction').value.trim(), reporter_name: reporter, reported_at: new Date().toISOString() }; fetch(N8N_BASE + '/webhook/incident-report', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-kaname-staff-secret': STAFF_SECRET }, body: JSON.stringify(payload) }) .then(function(res) { return res.json(); }) .then(function(data) { if (data && data.status === 'error') { showToast('Failed: ' + (data.message || 'Unknown error'), 'error', 4000); btn.disabled = false; btn.textContent = 'Submit Report'; return; } showToast('Incident report submitted.', 'success', 3000); closeIncidentModal(); }) .catch(function(err) { showToast('Network error \u2014 try again.', 'error', 4000); btn.disabled = false; btn.textContent = 'Submit Report'; }); } /* Escape key closes incident modal */ (function() { var origKeydown = document.onkeydown; document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { var irOverlay = document.getElementById('irOverlay'); if (irOverlay && irOverlay.classList.contains('open')) { closeIncidentModal(); e.stopPropagation(); } } }); })(); /* Click overlay to close */ (function() { var overlay = document.getElementById('irOverlay'); if (overlay) { overlay.addEventListener('click', function(e) { if (e.target === overlay) closeIncidentModal(); }); } })(); /* ===== PACKAGE PURCHASE ===== */ var PACKAGE_CONFIG = { half_day_10: { name: 'Half Day 10-Pass', days: 10, price: 252.80, unlimited: false }, half_day_20: { name: 'Half Day 20-Pass', days: 20, price: 477.55, unlimited: false }, full_day_10: { name: 'Full Day 10-Pass', days: 10, price: 343.45, unlimited: false }, full_day_20: { name: 'Full Day 20-Pass', days: 20, price: 648.70, unlimited: false }, puppy_10: { name: 'Puppy Full Day 10-Pass', days: 10, price: 286.20, unlimited: false }, puppy_20: { name: 'Puppy Full Day 20-Pass', days: 20, price: 540.60, unlimited: false }, multi_dog_10: { name: 'Full Day Multi-Dog 10-Pass', days: 10, price: 295.75, unlimited: false }, multi_dog_20: { name: 'Full Day Multi-Dog 20-Pass', days: 20, price: 558.60, unlimited: false }, monthly_unlimited: { name: 'Monthly Unlimited', days: 0, price: 556.50, unlimited: true } }; function openPackageModal() { if (!bookProfileData || !bookProfileData._contactId) { showToast('Search for a client first.', 'warning'); return; } var firstName = bookProfileData.first_name || ''; var lastName = bookProfileData.last_name || ''; document.getElementById('ppClientName').textContent = (firstName + ' ' + lastName).trim() || 'Unknown Client'; document.getElementById('ppPackage').selectedIndex = 0; document.getElementById('ppPayment').selectedIndex = 0; document.getElementById('ppNotes').value = ''; document.getElementById('ppPriceTag').textContent = ''; document.getElementById('ppSubmitBtn').disabled = true; document.getElementById('ppSubmitBtn').textContent = 'Record Sale'; document.getElementById('ppOverlay').classList.add('open'); } function closePackageModal() { document.getElementById('ppOverlay').classList.remove('open'); } function updatePackagePrice() { var key = document.getElementById('ppPackage').value; var tag = document.getElementById('ppPriceTag'); if (key && PACKAGE_CONFIG[key]) { var pkg = PACKAGE_CONFIG[key]; tag.textContent = pkg.unlimited ? 'Unlimited for 29 days from first use' : '+' + pkg.days + ' days added to balance'; } else { tag.textContent = ''; } validatePackageForm(); } function validatePackageForm() { var pkg = document.getElementById('ppPackage').value; var pay = document.getElementById('ppPayment').value; document.getElementById('ppSubmitBtn').disabled = !(pkg && pay); } function submitPackagePurchase() { var packageKey = document.getElementById('ppPackage').value; var paymentMethod = document.getElementById('ppPayment').value; var notes = document.getElementById('ppNotes').value.trim(); if (!packageKey || !paymentMethod) { showToast('Select a package and payment method.', 'warning'); return; } var config = PACKAGE_CONFIG[packageKey]; if (!config) { showToast('Invalid package.', 'error'); return; } var btn = document.getElementById('ppSubmitBtn'); btn.disabled = true; btn.textContent = 'Processing...'; var staffName = ''; try { staffName = localStorage.getItem('spk_reporter_name') || 'Front Desk'; } catch(e) { staffName = 'Front Desk'; } var payload = { contactId: bookProfileData._contactId, contactName: ((bookProfileData.first_name || '') + ' ' + (bookProfileData.last_name || '')).trim(), packageKey: packageKey, paymentMethod: paymentMethod, staffName: staffName, notes: notes }; fetch(N8N_BASE + '/webhook/package-purchase', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-kaname-staff-secret': STAFF_SECRET }, body: JSON.stringify(payload) }) .then(function(res) { return res.json(); }) .then(function(data) { if (Array.isArray(data)) data = data[0] || {}; if (data.success) { var msg = config.unlimited ? 'Monthly Unlimited activated!' : 'Package added \u2014 ' + data.newBalance + ' days credited'; showToast(msg, 'success', 4000); bookProfileData.package_balance = { name: config.name, remaining: config.unlimited ? 'Unlimited' : (data.newBalance || config.days) }; renderProfileInPanel(bookProfileData); closePackageModal(); } else { showToast('Something went wrong \u2014 try again.', 'error', 4000); btn.disabled = false; btn.textContent = 'Record Sale'; } }) .catch(function(err) { showToast('Something went wrong \u2014 try again.', 'error', 4000); btn.disabled = false; btn.textContent = 'Record Sale'; }); } /* Close on overlay click */ document.getElementById('ppOverlay').addEventListener('click', function(e) { if (e.target === this) closePackageModal(); }); /* Close on Escape */ document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && document.getElementById('ppOverlay').classList.contains('open')) { closePackageModal(); } }); /* Validate on payment change */ document.getElementById('ppPayment').addEventListener('change', validatePackageForm); /* ===================== VIEW TOGGLE ===================== */ var currentView = 'board'; function switchView(view) { currentView = view; var boardView = document.getElementById('boardView'); var locDashboard = document.getElementById('locDashboard'); var incidentView = document.getElementById('incidentLogView'); var meetGreetView = document.getElementById('meetGreetView'); var boardBtn = document.getElementById('viewBoardBtn'); var incidentBtn = document.getElementById('viewIncidentsBtn'); var meetGreetBtn = document.getElementById('viewMeetGreetsBtn'); /* Hide everything first */ boardView.style.display = 'none'; locDashboard.style.display = 'none'; incidentView.classList.remove('visible'); if (meetGreetView) meetGreetView.classList.remove('visible'); boardBtn.classList.remove('active'); boardBtn.setAttribute('aria-selected', 'false'); incidentBtn.classList.remove('active'); incidentBtn.setAttribute('aria-selected', 'false'); if (meetGreetBtn) { meetGreetBtn.classList.remove('active'); meetGreetBtn.setAttribute('aria-selected', 'false'); } if (view === 'board') { boardView.style.display = ''; locDashboard.style.display = ''; boardBtn.classList.add('active'); boardBtn.setAttribute('aria-selected', 'true'); } else if (view === 'incidents') { incidentView.classList.add('visible'); incidentBtn.classList.add('active'); incidentBtn.setAttribute('aria-selected', 'true'); renderIncidentLog(); } else if (view === 'meetgreets') { if (meetGreetView) meetGreetView.classList.add('visible'); if (meetGreetBtn) { meetGreetBtn.classList.add('active'); meetGreetBtn.setAttribute('aria-selected', 'true'); } loadMeetGreets(); } } /* ===================== INCIDENT LOG ===================== */ function getIncidentDogs() { var dogs = []; var board = state.boardData || {}; var sched = (state.scheduleData && state.scheduleData.schedule) ? state.scheduleData.schedule : {}; var seen = {}; function addDog(d) { d = parseDog(d); var status = (d.incident_status || '').toLowerCase(); if (status !== 'open' && status !== 'following_up') return; var id = d.pet_object_id || d.pet_id || d.opp_id || ''; if (!id || seen[id]) return; seen[id] = true; dogs.push(d); } /* Board data (daycare) */ if (board.east && board.east.dogs) { for (var i = 0; i < board.east.dogs.length; i++) addDog(board.east.dogs[i]); } if (board.west && board.west.dogs) { for (var j = 0; j < board.west.dogs.length; j++) addDog(board.west.dogs[j]); } /* Schedule data */ if (sched.grooming) { if (sched.grooming.east) { for (var ge = 0; ge < sched.grooming.east.length; ge++) addDog(sched.grooming.east[ge]); } if (sched.grooming.west) { for (var gw = 0; gw < sched.grooming.west.length; gw++) addDog(sched.grooming.west[gw]); } } if (sched.training) { if (sched.training.east) { for (var te = 0; te < sched.training.east.length; te++) addDog(sched.training.east[te]); } if (sched.training.west) { for (var tw = 0; tw < sched.training.west.length; tw++) addDog(sched.training.west[tw]); } } if (sched.boarding && sched.boarding.east) { for (var be = 0; be < sched.boarding.east.length; be++) addDog(sched.boarding.east[be]); } /* Sort: severity (high first), then date (oldest first) */ var sevOrder = { high: 0, medium: 1, low: 2 }; dogs.sort(function(a, b) { var sevA = sevOrder[(a.last_incident_severity || 'low').toLowerCase()] || 2; var sevB = sevOrder[(b.last_incident_severity || 'low').toLowerCase()] || 2; if (sevA !== sevB) return sevA - sevB; var dateA = a.last_incident_date ? new Date(a.last_incident_date).getTime() : Infinity; var dateB = b.last_incident_date ? new Date(b.last_incident_date).getTime() : Infinity; return dateA - dateB; }); return dogs; } function updateIncidentBadge() { var dogs = getIncidentDogs(); var badge = document.getElementById('incidentCountBadge'); if (dogs.length > 0) { badge.textContent = dogs.length; badge.classList.remove('hidden'); } else { badge.classList.add('hidden'); } } function daysSince(dateStr) { if (!dateStr) return null; var d = new Date(dateStr); if (isNaN(d.getTime())) return null; var now = new Date(); var diff = Math.floor((now.getTime() - d.getTime()) / (1000 * 60 * 60 * 24)); return diff >= 0 ? diff : 0; } function getFollowUpStatus(dog) { var days = daysSince(dog.last_incident_date); if (days === null) return null; var sev = (dog.last_incident_severity || 'low').toLowerCase(); if (sev === 'high' && days >= 7) return { label: 'Resolution due', cls: 'overdue' }; if ((sev === 'medium' || sev === 'high') && days >= 3) return { label: 'Follow-up overdue', cls: 'overdue' }; if (days >= 1) return { label: 'Follow-up due', cls: 'due' }; return null; } function renderIncidentLog() { var grid = document.getElementById('incidentLogGrid'); while (grid.firstChild) grid.removeChild(grid.firstChild); var dogs = getIncidentDogs(); document.getElementById('incidentLogCount').textContent = dogs.length; if (dogs.length === 0) { var empty = document.createElement('div'); empty.className = 'incident-log-empty'; var emptyIcon = document.createElement('div'); emptyIcon.className = 'incident-log-empty-icon'; emptyIcon.textContent = '\u2705'; empty.appendChild(emptyIcon); var emptyText = document.createElement('div'); emptyText.className = 'incident-log-empty-text'; emptyText.textContent = 'No open incidents. All clear!'; empty.appendChild(emptyText); grid.appendChild(empty); return; } for (var i = 0; i < dogs.length; i++) { grid.appendChild(buildIncidentCard(dogs[i])); } } function buildIncidentCard(dog) { var sev = (dog.last_incident_severity || 'low').toLowerCase(); var card = document.createElement('div'); card.className = 'incident-card severity-' + sev; /* Photo */ var photo = document.createElement('img'); photo.className = 'incident-card-photo'; photo.src = dog.photo_url || DOG_FALLBACK; photo.alt = dog._parsedName || ''; photo.onerror = function() { this.src = DOG_FALLBACK; }; card.appendChild(photo); /* Body */ var body = document.createElement('div'); body.className = 'incident-card-body'; /* Top: name + breed */ var top = document.createElement('div'); top.className = 'incident-card-top'; var nameEl = document.createElement('span'); nameEl.className = 'incident-card-name'; nameEl.textContent = dog._parsedName || 'Unknown'; top.appendChild(nameEl); if (dog.breed) { var breedEl = document.createElement('span'); breedEl.className = 'incident-card-breed'; breedEl.textContent = dog.breed; top.appendChild(breedEl); } body.appendChild(top); /* Meta row: severity + type + days + followup status + incident status */ var meta = document.createElement('div'); meta.className = 'incident-card-meta'; /* Severity badge */ var sevBadge = document.createElement('span'); sevBadge.className = 'incident-severity-badge ' + sev; sevBadge.textContent = sev.charAt(0).toUpperCase() + sev.slice(1); meta.appendChild(sevBadge); /* Type badge */ if (dog.last_incident_type) { var typeBadge = document.createElement('span'); typeBadge.className = 'incident-type-badge'; typeBadge.textContent = dog.last_incident_type; meta.appendChild(typeBadge); } /* Days since incident */ var days = daysSince(dog.last_incident_date); if (days !== null) { var daysBadge = document.createElement('span'); daysBadge.className = 'incident-days-badge'; daysBadge.textContent = days === 0 ? 'Today' : days === 1 ? '1 day ago' : days + ' days ago'; meta.appendChild(daysBadge); } /* Follow-up status */ var fuStatus = getFollowUpStatus(dog); if (fuStatus) { var fuBadge = document.createElement('span'); fuBadge.className = 'incident-followup-badge ' + fuStatus.cls; fuBadge.textContent = fuStatus.label; meta.appendChild(fuBadge); } /* Incident status badge */ var incStatus = (dog.incident_status || 'open').toLowerCase(); var statusBadge = document.createElement('span'); statusBadge.className = 'incident-status-badge ' + incStatus; statusBadge.textContent = incStatus === 'following_up' ? 'Following Up' : 'Open'; meta.appendChild(statusBadge); body.appendChild(meta); /* Owner + phone */ var ownerRow = document.createElement('div'); ownerRow.className = 'incident-card-owner'; var ownerName = dog.owner_name || dog.owner_last_name || ''; if (ownerName) { var ownerSpan = document.createElement('span'); ownerSpan.textContent = ownerName; ownerRow.appendChild(ownerSpan); } var ownerPhone = dog.owner_phone || ''; if (ownerPhone) { var sep = document.createTextNode(' \u00b7 '); ownerRow.appendChild(sep); var phoneLink = document.createElement('a'); phoneLink.href = 'tel:' + ownerPhone; phoneLink.textContent = formatPhoneDisplay(ownerPhone); ownerRow.appendChild(phoneLink); } body.appendChild(ownerRow); card.appendChild(body); /* Actions */ var actions = document.createElement('div'); actions.className = 'incident-card-actions'; var fuBtn = document.createElement('button'); fuBtn.className = 'btn btn-followup'; fuBtn.textContent = 'Follow Up'; (function(d) { fuBtn.onclick = function() { openFollowUpModal(d); }; })(dog); actions.appendChild(fuBtn); var histBtn = document.createElement('button'); histBtn.className = 'btn btn-history'; histBtn.textContent = 'View Details'; (function(d) { histBtn.onclick = function() { openPetModal(d); }; })(dog); actions.appendChild(histBtn); card.appendChild(actions); return card; } /* ===================== FOLLOW-UP MODAL ===================== */ var fuCurrentDog = null; function openFollowUpModal(dog) { fuCurrentDog = dog; /* Set dog summary */ var photo = document.getElementById('fuDogPhoto'); photo.src = dog.photo_url || DOG_FALLBACK; photo.onerror = function() { this.src = DOG_FALLBACK; }; photo.alt = dog._parsedName || ''; document.getElementById('fuDogName').textContent = dog._parsedName || 'Unknown'; var detail = []; if (dog.breed) detail.push(dog.breed); if (dog.last_incident_severity) detail.push('Severity: ' + dog.last_incident_severity); if (dog.last_incident_date) detail.push('Incident: ' + formatDate(dog.last_incident_date)); document.getElementById('fuDogDetail').textContent = detail.join(' \u00b7 '); /* Reset form */ var radios = document.querySelectorAll('#fuActionGroup .ir-radio'); for (var r = 0; r < radios.length; r++) { radios[r].classList.remove('selected'); radios[r].querySelector('input').checked = false; } /* Default to follow_up */ radios[0].classList.add('selected'); radios[0].querySelector('input').checked = true; document.getElementById('fuNotes').value = ''; document.getElementById('fuResolutionNotes').value = ''; document.getElementById('fuResolutionSection').classList.remove('visible'); document.getElementById('fuSubmitBtn').classList.remove('resolve'); document.getElementById('fuSubmitBtn').textContent = 'Submit Follow-Up'; document.getElementById('fuSubmitBtn').disabled = false; document.getElementById('fuTitle').textContent = 'Follow Up \u2014 ' + (dog._parsedName || ''); /* Pre-fill staff name from localStorage */ var saved = ''; try { saved = localStorage.getItem('spk_reporter_name') || ''; } catch(e) {} document.getElementById('fuStaffName').value = saved; /* Show */ document.getElementById('fuOverlay').classList.add('open'); document.body.style.overflow = 'hidden'; } function closeFollowUpModal() { document.getElementById('fuOverlay').classList.remove('open'); document.body.style.overflow = ''; fuCurrentDog = null; } /* Wire up action radio toggles for follow-up modal */ (function() { document.addEventListener('click', function(e) { var radio = e.target.closest('#fuActionGroup .ir-radio'); if (!radio) return; var group = radio.parentElement; var siblings = group.querySelectorAll('.ir-radio'); for (var s = 0; s < siblings.length; s++) { siblings[s].classList.remove('selected'); siblings[s].querySelector('input').checked = false; } radio.classList.add('selected'); radio.querySelector('input').checked = true; var val = radio.querySelector('input').value; var resSection = document.getElementById('fuResolutionSection'); var submitBtn = document.getElementById('fuSubmitBtn'); if (val === 'resolve') { resSection.classList.add('visible'); submitBtn.classList.add('resolve'); submitBtn.textContent = 'Resolve Incident'; } else { resSection.classList.remove('visible'); submitBtn.classList.remove('resolve'); submitBtn.textContent = 'Submit Follow-Up'; } }); })(); function submitFollowUp() { if (!fuCurrentDog) return; var action = ''; var checkedAction = document.querySelector('#fuActionGroup input:checked'); if (checkedAction) action = checkedAction.value; var notes = document.getElementById('fuNotes').value.trim(); var resolutionNotes = document.getElementById('fuResolutionNotes').value.trim(); var staffName = document.getElementById('fuStaffName').value.trim(); if (!action) { showToast('Select an action.', 'error'); return; } if (!notes) { showToast('Enter follow-up notes.', 'error'); return; } if (!staffName) { showToast('Enter your name.', 'error'); return; } if (action === 'resolve' && !resolutionNotes) { showToast('Enter resolution notes.', 'error'); return; } /* Save staff name */ try { localStorage.setItem('spk_reporter_name', staffName); } catch(e) {} var btn = document.getElementById('fuSubmitBtn'); btn.disabled = true; btn.textContent = 'Submitting...'; var payload = { pet_id: fuCurrentDog.pet_object_id || fuCurrentDog.pet_id || '', action: action, notes: notes, resolution_notes: resolutionNotes || '', staff_name: staffName }; fetch(N8N_BASE + '/webhook/incident-resolve', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-kaname-staff-secret': STAFF_SECRET }, body: JSON.stringify(payload) }) .then(function(res) { return res.json(); }) .then(function(data) { if (data && data.status === 'error') { showToast('Failed: ' + (data.message || 'Unknown error'), 'error', 4000); btn.disabled = false; btn.textContent = action === 'resolve' ? 'Resolve Incident' : 'Submit Follow-Up'; return; } var msg = action === 'resolve' ? (fuCurrentDog._parsedName || 'Incident') + ' resolved.' : 'Follow-up submitted for ' + (fuCurrentDog._parsedName || 'incident') + '.'; showToast(msg, 'success', 3000); closeFollowUpModal(); /* Refresh data and re-render */ setTimeout(function() { refreshAll(); }, 800); }) .catch(function(err) { showToast('Network error \u2014 try again.', 'error', 4000); btn.disabled = false; btn.textContent = action === 'resolve' ? 'Resolve Incident' : 'Submit Follow-Up'; }); } /* Escape key closes follow-up modal */ (function() { document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { var fuOverlay = document.getElementById('fuOverlay'); if (fuOverlay && fuOverlay.classList.contains('open')) { closeFollowUpModal(); e.stopPropagation(); } } }); })(); /* Click overlay to close follow-up modal */ (function() { var overlay = document.getElementById('fuOverlay'); if (overlay) { overlay.addEventListener('click', function(e) { if (e.target === overlay) closeFollowUpModal(); }); } })(); /* DAILY PUN / FACT — rotates by day of year */ var DAILY_PUNS = [ '"I\'m not arguing, I\'m just explaining why I\'m right." — Every Golden Retriever, probably', 'Fun fact: A dog\'s nose print is as unique as a human fingerprint.', '"You want the ball? You can\'t handle the ball!" — A Few Good Boys', 'Fun fact: Dogs can smell up to 100,000 times better than humans.', '"After all this time?" "Always." — Severus Snape\'s dog, waiting by the door', 'Fun fact: Dalmatians are born completely white — spots develop as they grow.', '"I\'ll be bark." — The Terrier-nator', 'Fun fact: A wagging tail to the right means happy; to the left means anxious.', '"To sit, or not to sit — that is the question." — Hamlet\'s Great Dane', 'Fun fact: Greyhounds can run up to 72 km/h — faster than a cheetah over distance.', '"You had me at woof." — Jerry Maguire\'s Labrador', 'Fun fact: Three dogs survived the sinking of the Titanic — two Pomeranians and a Pekingese.', '"Life is like a box of treats — you always know what you\'re gonna get." — Forrest Pup', 'Fun fact: Dogs dream just like humans — small dogs dream more often than big ones.', '"I see dead squirrels." — The Sixth Scents' ]; function setDailyPun() { var el = document.getElementById('dailyPun'); if (!el) return; var dayOfYear = Math.floor((Date.now() - new Date(new Date().getFullYear(),0,0)) / 86400000); el.textContent = DAILY_PUNS[dayOfYear % DAILY_PUNS.length]; } /* ===================== MEET & GREETS ===================== */ var meetGreetState = { list: [], loading: false, loaded: false }; function loadMeetGreets() { var grid = document.getElementById('meetGreetGrid'); var loading = document.getElementById('meetGreetLoading'); var error = document.getElementById('meetGreetError'); var empty = document.getElementById('meetGreetEmpty'); if (!grid) return; meetGreetState.loading = true; grid.innerHTML = ''; if (loading) loading.style.display = 'flex'; if (error) error.style.display = 'none'; if (empty) empty.style.display = 'none'; callWebhook('/webhook/staff-board', { action: 'list_pending_temp_tests' }, function(err, data) { meetGreetState.loading = false; if (loading) loading.style.display = 'none'; if (err) { if (error) { error.style.display = 'block'; error.textContent = '\u26a0\ufe0f ' + (err.message || 'Failed to load'); } return; } var pets = (data && data.pets) || []; meetGreetState.list = pets; meetGreetState.loaded = true; setCount('meetGreetListCount', pets.length); updateMeetGreetBadge(pets.length); if (pets.length === 0) { if (empty) empty.style.display = 'flex'; return; } renderMeetGreetList(pets); }); } function updateMeetGreetBadge(n) { var badge = document.getElementById('meetGreetCountBadge'); if (!badge) return; badge.textContent = n; if (n > 0) { badge.classList.remove('hidden'); } else { badge.classList.add('hidden'); } } function appendReqLabel(parent, text) { var label = document.createElement('div'); label.className = 'meetgreet-card-label'; label.appendChild(document.createTextNode(text + ' ')); var req = document.createElement('span'); req.className = 'req'; req.textContent = '*'; label.appendChild(req); parent.appendChild(label); return label; } function appendPlainLabel(parent, text, extraClass) { var label = document.createElement('div'); label.className = 'meetgreet-card-label'; label.textContent = text; if (extraClass) label.classList.add(extraClass); parent.appendChild(label); return label; } function renderMeetGreetList(pets) { var grid = document.getElementById('meetGreetGrid'); if (!grid) return; grid.innerHTML = ''; pets.forEach(function(pet, idx) { var card = document.createElement('div'); card.className = 'meetgreet-card'; card.setAttribute('data-pet-id', pet.pet_object_id || pet.id || ''); var petName = cleanPetDisplayName(pet.pet_name || pet.dog_name || 'Unnamed'); var breed = pet.breed || ''; var ownerName = [pet.owner_first_name, pet.owner_last_name].filter(Boolean).join(' ').trim(); var ownerPhone = pet.owner_phone || ''; var ownerEmail = pet.owner_email || ''; var currentStatus = (pet.temperament_status || '').toLowerCase() || 'not_evaluated'; var location = pet.location || ''; var vaxStatus = pet.pet_vax_expire_status || ''; var registeredAt = pet.created_at ? formatDate(pet.created_at) : ''; var photoUrl = pet.photo_url || pet.photo || DOG_FALLBACK; /* HEADER */ var header = document.createElement('div'); header.className = 'meetgreet-card-header'; var photo = document.createElement('img'); photo.className = 'meetgreet-card-photo'; photo.src = photoUrl; photo.alt = petName; photo.onerror = function() { this.src = DOG_FALLBACK; }; header.appendChild(photo); var headInfo = document.createElement('div'); headInfo.className = 'meetgreet-card-info'; var nameEl = document.createElement('div'); nameEl.className = 'meetgreet-card-name'; nameEl.textContent = petName; headInfo.appendChild(nameEl); if (breed) { var breedEl = document.createElement('div'); breedEl.className = 'meetgreet-card-breed'; breedEl.textContent = breed + (pet.weight_lbs ? ' \u2022 ' + formatWeight(pet.weight_lbs) : ''); headInfo.appendChild(breedEl); } var statusPill = document.createElement('span'); statusPill.className = 'meetgreet-card-status status-' + currentStatus; statusPill.textContent = currentStatus === 'not_evaluated' ? 'Not Evaluated' : currentStatus; headInfo.appendChild(statusPill); header.appendChild(headInfo); card.appendChild(header); /* OWNER CONTACT */ if (ownerName || ownerPhone || ownerEmail) { var ownerBlock = document.createElement('div'); ownerBlock.className = 'meetgreet-card-owner'; appendPlainLabel(ownerBlock, 'Owner'); if (ownerName) { var ownerNameEl = document.createElement('div'); ownerNameEl.className = 'meetgreet-card-owner-name'; ownerNameEl.textContent = ownerName; ownerBlock.appendChild(ownerNameEl); } if (ownerPhone) { var phoneA = document.createElement('a'); phoneA.className = 'meetgreet-card-contact'; phoneA.href = 'tel:' + ownerPhone; phoneA.textContent = '\u260E ' + ownerPhone; ownerBlock.appendChild(phoneA); } if (ownerEmail) { var emailA = document.createElement('a'); emailA.className = 'meetgreet-card-contact'; emailA.href = 'mailto:' + ownerEmail; emailA.textContent = '\u2709 ' + ownerEmail; ownerBlock.appendChild(emailA); } card.appendChild(ownerBlock); } /* META */ if (location || vaxStatus || registeredAt) { var meta = document.createElement('div'); meta.className = 'meetgreet-card-meta'; if (location) meta.appendChild(buildMetaChip('Location', location)); if (vaxStatus) meta.appendChild(buildMetaChip('Vax', vaxStatus.replace(/_/g, ' '))); if (registeredAt) meta.appendChild(buildMetaChip('Registered', registeredAt)); card.appendChild(meta); } /* EVAL FORM */ var form = document.createElement('div'); form.className = 'meetgreet-card-form'; appendReqLabel(form, 'Evaluation Result'); var statusGrp = document.createElement('div'); statusGrp.className = 'meetgreet-radio-group'; ['pass', 'conditional', 'not_suitable'].forEach(function(val) { var label = document.createElement('label'); label.className = 'meetgreet-radio meetgreet-radio-' + val; label.setAttribute('data-val', val); var labels = { pass: 'Pass', conditional: 'Conditional', not_suitable: 'Not Suitable' }; var radio = document.createElement('input'); radio.type = 'radio'; radio.name = 'mg_status_' + idx; radio.value = val; radio.addEventListener('change', function() { onMeetGreetStatusChange(card, val); }); label.appendChild(radio); label.appendChild(document.createTextNode(' ' + labels[val])); statusGrp.appendChild(label); }); form.appendChild(statusGrp); appendPlainLabel(form, 'Assigned Playgroup'); var playSelect = document.createElement('select'); playSelect.className = 'meetgreet-select'; playSelect.setAttribute('data-field', 'playgroup'); [ { v: '', l: '-- Select playgroup --' }, { v: 'standard_group', l: 'Standard Group' }, { v: 'small__calm', l: 'Small & Calm' }, { v: 'highenergy', l: 'High-Energy' }, { v: 'soloplay', l: 'Solo-Play' }, { v: 'staffonly', l: 'Staff-Only (supervised)' } ].forEach(function(opt) { var o = document.createElement('option'); o.value = opt.v; o.textContent = opt.l; playSelect.appendChild(o); }); form.appendChild(playSelect); var notesLabelEl = document.createElement('div'); notesLabelEl.className = 'meetgreet-card-label'; notesLabelEl.appendChild(document.createTextNode('Evaluation Notes ')); var notesReqEl = document.createElement('span'); notesReqEl.className = 'meetgreet-notes-req'; notesReqEl.style.display = 'none'; notesReqEl.textContent = '*'; notesLabelEl.appendChild(notesReqEl); form.appendChild(notesLabelEl); var notesTa = document.createElement('textarea'); notesTa.className = 'meetgreet-textarea'; notesTa.setAttribute('data-field', 'notes'); notesTa.placeholder = 'Behavior, temperament observations, any restrictions...'; notesTa.rows = 3; form.appendChild(notesTa); appendReqLabel(form, 'Your Name'); var staffInput = document.createElement('input'); staffInput.type = 'text'; staffInput.className = 'meetgreet-input'; staffInput.setAttribute('data-field', 'staff_name'); staffInput.placeholder = 'Staff name...'; form.appendChild(staffInput); var actions = document.createElement('div'); actions.className = 'meetgreet-card-actions'; var submitBtn = document.createElement('button'); submitBtn.className = 'meetgreet-submit-btn'; submitBtn.textContent = 'Submit Evaluation'; submitBtn.addEventListener('click', function() { submitMeetGreet(card, pet); }); actions.appendChild(submitBtn); form.appendChild(actions); card.appendChild(form); grid.appendChild(card); }); } function buildMetaChip(label, value) { var chip = document.createElement('div'); chip.className = 'meetgreet-meta-chip'; var l = document.createElement('span'); l.className = 'meetgreet-meta-label'; l.textContent = label + ':'; var v = document.createElement('span'); v.className = 'meetgreet-meta-value'; v.textContent = ' ' + value; chip.appendChild(l); chip.appendChild(v); return chip; } function onMeetGreetStatusChange(card, status) { /* Notes required for conditional (so staff can document restrictions) */ var notesReq = card.querySelector('.meetgreet-notes-req'); if (notesReq) { notesReq.style.display = (status === 'conditional') ? 'inline' : 'none'; } /* Visual state on card */ card.setAttribute('data-selected-status', status); } function submitMeetGreet(card, pet) { var selectedRadio = card.querySelector('input[type="radio"]:checked'); if (!selectedRadio) { toast('error', 'Select an evaluation result (Pass / Conditional / Not Suitable)'); return; } var status = selectedRadio.value; var playgroup = (card.querySelector('[data-field="playgroup"]') || {}).value || ''; var notes = ((card.querySelector('[data-field="notes"]') || {}).value || '').trim(); var staffName = ((card.querySelector('[data-field="staff_name"]') || {}).value || '').trim(); if (!staffName) { toast('error', 'Enter your name'); return; } if (status === 'conditional' && !notes) { toast('error', 'Notes are required when marking Conditional \u2014 describe any restrictions or special care'); return; } var submitBtn = card.querySelector('.meetgreet-submit-btn'); if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Submitting...'; } var payload = { pet_object_id: pet.pet_object_id || pet.id || '', contact_id: pet.contact_id || pet.primary_contact_id || '', temperament_status: status, temperament_playgroup: playgroup, temperament_notes: notes, evaluator: staffName, evaluated_at: new Date().toISOString() }; callWebhook('/webhook/temp-test-result', payload, function(err, data) { if (err) { if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Submit Evaluation'; } toast('error', 'Submit failed: ' + (err.message || 'try again')); return; } /* Success \u2014 remove card + refresh count */ var notified = (status === 'pass' || status === 'conditional') ? '. Client notified.' : '.'; toast('success', cleanPetDisplayName(pet.pet_name) + ' marked as ' + status + notified); card.style.opacity = '0.5'; setTimeout(function() { if (card.parentNode) card.parentNode.removeChild(card); meetGreetState.list = meetGreetState.list.filter(function(p) { return (p.pet_object_id || p.id) !== (pet.pet_object_id || pet.id); }); setCount('meetGreetListCount', meetGreetState.list.length); updateMeetGreetBadge(meetGreetState.list.length); if (meetGreetState.list.length === 0) { var empty = document.getElementById('meetGreetEmpty'); if (empty) empty.style.display = 'flex'; } }, 500); }); } /* Poll for pending count on load so badge shows even before tab is opened */ function refreshMeetGreetBadge() { callWebhook('/webhook/staff-board', { action: 'list_pending_temp_tests' }, function(err, data) { if (err) return; var pets = (data && data.pets) || []; updateMeetGreetBadge(pets.length); }); } /* INIT */ function init() { initLocation(); updateClock(); updateLocationVisibility(); setInterval(updateClock, 15000); renderResources(); populateProductSelect(); setDailyPun(); refreshAll(); startAutoRefresh(); /* Book a Dog search input */ var bookInput = document.getElementById('bookSearchInput'); if (bookInput) bookInput.addEventListener('input', handleBookSearch); /* Meet & Greets pending count */ refreshMeetGreetBadge(); } init(); /* ============================================================================ * CARE SCORECARD (v1.13 — May 6 2026) * Digital replacement for PetExec's printed Scorecard sheet. * 3-zone modal (pre-stay / daily action grid / departure). * Boarding: N-day grid × {Breakfast,Lunch,Dinner,AM Med,Noon Med,PM Med}. * Daycare: simple 4-row card (Feeding / Medications / Playgroup / Extras). * Greyed cells driven by pet's intake schedule (feeding_times_*, medication_times_*). * All actions append timestamped entries to opportunity.care_log via * staff-board webhook (action=staff_care_log_append). Single source of truth. * Per-cell state derived from latest matching log entry on read. * ============================================================================ */ var SCORECARD_INITIALS_KEY = 'spk_staff_initials_2026'; function getStaffInitials() { var v = localStorage.getItem(SCORECARD_INITIALS_KEY); return v && /^[A-Z]{2,3}$/.test(v) ? v : ''; } function setStaffInitials(v) { v = (v || '').toString().trim().toUpperCase(); if (!/^[A-Z]{2,3}$/.test(v)) return false; localStorage.setItem(SCORECARD_INITIALS_KEY, v); return true; } function promptStaffInitials(cb) { /* Modal: 2-3 letter initials capture */ var existing = document.getElementById('scInitialsModal'); if (existing) existing.remove(); var ov = document.createElement('div'); ov.id = 'scInitialsModal'; ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:10000;'; var box = document.createElement('div'); box.style.cssText = 'background:#fff;padding:24px;border-radius:14px;width:90%;max-width:340px;'; box.innerHTML = '

Quick — your initials?

' + '

2 or 3 letters. Used on the care log so we know who did what. Saved on this device.

' + '' + '' + ''; ov.appendChild(box); document.body.appendChild(ov); var inp = document.getElementById('scInitialsInput'); inp.focus(); function save() { var ok = setStaffInitials(inp.value); if (!ok) { document.getElementById('scInitialsErr').style.display = 'block'; return; } ov.remove(); if (cb) cb(getStaffInitials()); } document.getElementById('scInitialsSave').addEventListener('click', save); inp.addEventListener('keydown', function(e) { if (e.key === 'Enter') save(); }); } function ensureStaffInitials(cb) { var i = getStaffInitials(); if (i) { cb(i); return; } promptStaffInitials(cb); } /* Inject Care Scorecard button into openPetModal body — called from the * pet modal section builder. Adds a prominent button at top of body. */ function injectCareScorecardButton(body, dog) { if (!dog || !dog.opp_id) return; var section = document.createElement('div'); section.style.cssText = 'margin:8px 0 16px;display:flex;justify-content:center;'; var btn = document.createElement('button'); btn.type = 'button'; btn.style.cssText = 'display:flex;align-items:center;gap:10px;padding:14px 22px;background:linear-gradient(135deg,#1974CE 0%,#1461ad 100%);color:#fff;border:none;border-radius:12px;font-weight:700;font-size:1rem;cursor:pointer;font-family:Outfit,sans-serif;box-shadow:0 4px 12px rgba(25,116,206,0.25);'; btn.innerHTML = '📋 Care Scorecard'; btn.addEventListener('click', function() { if (typeof closePetModal === 'function') closePetModal(); setTimeout(function() { openCareScorecardModal(dog); }, 80); }); section.appendChild(btn); body.insertBefore(section, body.firstChild); } /* Build day list for boarding grid: array of YYYY-MM-DD between start and end (inclusive). */ function scorecardBoardingDays(dog) { var startStr = dog.boarding_start_date || ''; var endStr = dog.boarding_end_date || ''; if (!startStr || !endStr) { /* Fallback: today only */ var t = new Date(); var ts = t.getFullYear() + '-' + String(t.getMonth() + 1).padStart(2, '0') + '-' + String(t.getDate()).padStart(2, '0'); return [ts]; } /* Normalize to YYYY-MM-DD */ function norm(s) { if (typeof s === 'number') { var d = new Date(s); return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); } return ('' + s).substring(0, 10); } var start = norm(startStr); var end = norm(endStr); var days = []; var d = new Date(start + 'T12:00:00Z'); var endD = new Date(end + 'T12:00:00Z'); while (d.getTime() <= endD.getTime()) { days.push(d.toISOString().substring(0, 10)); d.setUTCDate(d.getUTCDate() + 1); } return days.length ? days : [start]; } function scorecardDayLabel(dateStr) { var d = new Date(dateStr + 'T12:00:00Z'); var dows = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; return dows[d.getUTCDay()] + ' ' + (d.getUTCMonth() + 1) + '/' + d.getUTCDate(); } function scorecardCellGreyed(dog, slotKey) { /* slotKey: 'breakfast' | 'lunch' | 'dinner' | 'am_med' | 'noon_med' | 'pm_med' * * Returns TRUE only when this pet has an EXPLICIT "no" on this slot. * Empty/null/unknown → returns FALSE (cell stays active = "tap any time"). * * Schema fields (added 2026-05-05): * custom_objects.pets.feeding_times_am/lunch/pm * custom_objects.pets.medication_times_am/lunch/pm * SINGLE_OPTIONS yes/no, projected by n8n staff board as pet_feeding_times_* * and pet_medication_times_*. Until intake form captures structured slot * data, all pets read empty here → cells render active by default. */ function explicitlyNo(v) { if (v == null || v === '') return false; var s = ('' + v).toLowerCase(); return s === 'no' || s === 'false' || s === '0' || s === 'off'; } switch (slotKey) { case 'breakfast': return explicitlyNo(dog.pet_feeding_times_am); case 'lunch': return explicitlyNo(dog.pet_feeding_times_lunch); case 'dinner': return explicitlyNo(dog.pet_feeding_times_pm); case 'am_med': return explicitlyNo(dog.pet_medication_times_am); case 'noon_med': return explicitlyNo(dog.pet_medication_times_lunch); case 'pm_med': return explicitlyNo(dog.pet_medication_times_pm); default: return false; } } function scorecardSlotLabel(slotKey, dayLabel) { var labels = { breakfast: 'Breakfast', lunch: 'Lunch', dinner: 'Dinner', am_med: 'AM Med', noon_med: 'Noon Med', pm_med: 'PM Med' }; return dayLabel + ' ' + (labels[slotKey] || slotKey); } function scorecardParseEntries(rawLog) { if (!rawLog) return []; var lines = rawLog.split('\n'); var entries = []; for (var i = 0; i < lines.length; i++) { var line = lines[i]; /* Format: "YYYY-MM-DD HH:MM — [TYPE] payload — INITIALS" (em-dash u+2014) */ var m = line.match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}) — \[(\w+)\]\s*(.*?)\s*—\s*([A-Z]{2,3})\s*$/); if (!m) continue; entries.push({ date: m[1], time: m[2], type: m[3], payload: m[4].trim(), initials: m[5] }); } return entries; } function scorecardCellState(entries, dayDateStr, dayLabel, slotKey) { /* Look for latest MEDS or FOOD entry whose payload starts with the slot label. */ var slotLabel = scorecardSlotLabel(slotKey, dayLabel); for (var i = 0; i < entries.length; i++) { var e = entries[i]; if (e.type !== 'MEDS' && e.type !== 'FOOD') continue; if (e.date !== dayDateStr) continue; var payloadStart = e.payload.split(' — ')[0].trim(); if (payloadStart === slotLabel) { return { done: true, time: e.time, initials: e.initials }; } } return { done: false }; } /* === The scorecard modal itself === */ var SC_MODAL_DOG = null; var SC_MODAL_ENTRIES = []; function openCareScorecardModal(dog) { SC_MODAL_DOG = dog; var existing = document.getElementById('scModal'); if (existing) existing.remove(); var ov = document.createElement('div'); ov.id = 'scModal'; ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.55);z-index:9999;display:flex;justify-content:center;align-items:flex-start;overflow-y:auto;padding:20px;'; var box = document.createElement('div'); box.style.cssText = 'background:#fff;border-radius:14px;width:100%;max-width:780px;margin:auto;'; box.id = 'scModalBox'; ov.appendChild(box); ov.addEventListener('click', function(e) { if (e.target === ov) ov.remove(); }); document.body.appendChild(ov); scorecardRender(); } function scorecardClose() { var m = document.getElementById('scModal'); if (m) m.remove(); } function scorecardRender() { var dog = SC_MODAL_DOG; var box = document.getElementById('scModalBox'); if (!box || !dog) return; box.innerHTML = ''; var svc = (dog.service_category || '').toLowerCase(); var isBoarding = svc === 'boarding'; var initials = getStaffInitials(); /* Header */ var hdr = document.createElement('div'); hdr.style.cssText = 'padding:18px 22px;border-bottom:2px solid #e5e5e5;display:flex;justify-content:space-between;align-items:center;background:#1974CE;color:#fff;border-radius:14px 14px 0 0;'; hdr.innerHTML = '
' + (dog._parsedName || dog.dog_name || 'Care Scorecard') + '
Care Scorecard · ' + (isBoarding ? 'Boarding' : 'Daycare') + ' · ' + (initials ? 'Staff: ' + initials : 'initials not set') + '
'; var closeBtn = document.createElement('button'); closeBtn.style.cssText = 'background:rgba(255,255,255,0.2);color:#fff;border:none;border-radius:50%;width:36px;height:36px;font-size:1.2rem;cursor:pointer;font-weight:700;'; closeBtn.textContent = '×'; closeBtn.addEventListener('click', scorecardClose); hdr.appendChild(closeBtn); box.appendChild(hdr); /* Body */ var body = document.createElement('div'); body.style.cssText = 'padding:18px 22px;'; box.appendChild(body); /* Loading state for read */ var status = document.createElement('div'); status.style.cssText = 'text-align:center;padding:30px;color:#999;'; status.textContent = 'Loading care log...'; body.appendChild(status); /* Fetch fresh care_log */ callWebhook('/webhook/staff-board', { action: 'staff_care_log_read', opp_id: dog.opp_id }, function(err, data) { body.innerHTML = ''; if (err) { var errBox = document.createElement('div'); errBox.style.cssText = 'padding:16px;background:#fee2e2;color:#991b1b;border-radius:10px;'; errBox.textContent = 'Could not load care log: ' + err.message; body.appendChild(errBox); return; } SC_MODAL_ENTRIES = (data && data.entries) || []; /* === Zone 1: Pre-stay === */ body.appendChild(scorecardZonePreStay(dog)); /* === Zone 2: Daily action grid === */ if (isBoarding) body.appendChild(scorecardZoneBoardingGrid(dog)); else body.appendChild(scorecardZoneDaycareCard(dog)); /* === Zone 3: Departure === */ body.appendChild(scorecardZoneDeparture(dog)); /* === Zone 4: Notes / handoff === */ body.appendChild(scorecardZoneNotes(dog)); /* === Zone 5: Activity log === */ body.appendChild(scorecardZoneActivity(dog)); }); } function scorecardZoneTitle(text, color) { var t = document.createElement('div'); t.style.cssText = 'font-family:Outfit,sans-serif;font-weight:700;text-transform:uppercase;letter-spacing:.06em;font-size:.78rem;color:' + (color || '#1974CE') + ';margin:18px 0 8px;border-bottom:2px solid ' + (color || '#1974CE') + ';padding-bottom:4px;'; t.textContent = text; return t; } function scorecardZonePreStay(dog) { var z = document.createElement('div'); z.appendChild(scorecardZoneTitle('1. Pre-stay')); var box = document.createElement('div'); box.style.cssText = 'background:#f0f7ff;border:1px solid #cfe5ff;border-radius:10px;padding:14px;font-size:.92rem;color:#1a1a2e;'; /* Care profile flag */ var careOnFile = (dog.boarding_care_on_file || '').toLowerCase(); if (careOnFile && careOnFile !== 'yes') { var warn = document.createElement('div'); warn.style.cssText = 'background:#fef3c7;border-left:3px solid #f59e0b;color:#7c2d12;padding:8px 12px;border-radius:6px;margin-bottom:10px;font-weight:600;'; warn.textContent = '⚠️ Care profile incomplete — customer hasn’t finished the questionnaire.'; box.appendChild(warn); } /* Items received text (read-only display + button to add note) */ if (dog.other_items_received_at_check_in) { var ir = document.createElement('div'); ir.style.cssText = 'margin-bottom:6px;'; ir.innerHTML = 'Items received: ' + escapeHtml(dog.other_items_received_at_check_in); box.appendChild(ir); } /* Confirmation button */ var confirmed = SC_MODAL_ENTRIES.some(function(e) { return e.type === 'CONFIRM'; }); var confirmBtn = document.createElement('button'); confirmBtn.type = 'button'; if (confirmed) { var confEntry = SC_MODAL_ENTRIES.find(function(e) { return e.type === 'CONFIRM'; }); confirmBtn.style.cssText = 'padding:10px 14px;background:#dcfce7;color:#166534;border:1px solid #86efac;border-radius:8px;font-weight:700;width:100%;cursor:default;'; confirmBtn.textContent = '✓ Care profile reviewed (' + (confEntry ? confEntry.initials + ' · ' + confEntry.date + ' ' + confEntry.time : '') + ')'; } else { confirmBtn.style.cssText = 'padding:10px 14px;background:#1974CE;color:#fff;border:none;border-radius:8px;font-weight:700;width:100%;cursor:pointer;'; confirmBtn.textContent = 'I’ve reviewed this dog’s care setup'; confirmBtn.addEventListener('click', function() { ensureStaffInitials(function(initials) { scorecardPost({ entry_type: 'CONFIRM', staff_initials: initials }); }); }); } box.appendChild(confirmBtn); z.appendChild(box); return z; } function scorecardZoneBoardingGrid(dog) { var z = document.createElement('div'); z.appendChild(scorecardZoneTitle('2. Daily Care Grid')); var days = scorecardBoardingDays(dog); var slots = ['breakfast', 'lunch', 'dinner', 'am_med', 'noon_med', 'pm_med']; var slotHeaders = ['Breakfast', 'Lunch', 'Dinner', 'AM Med', 'Noon Med', 'PM Med']; var wrap = document.createElement('div'); wrap.style.cssText = 'overflow-x:auto;'; var tbl = document.createElement('table'); tbl.style.cssText = 'border-collapse:collapse;width:100%;font-size:.85rem;min-width:520px;'; /* Header */ var thead = document.createElement('tr'); var firstTh = document.createElement('th'); firstTh.style.cssText = 'text-align:left;padding:8px 6px;border-bottom:2px solid #1974CE;font-weight:700;color:#1974CE;'; firstTh.textContent = 'Day'; thead.appendChild(firstTh); for (var s = 0; s < slots.length; s++) { var th = document.createElement('th'); th.style.cssText = 'text-align:center;padding:8px 4px;border-bottom:2px solid #1974CE;font-weight:700;color:#1974CE;font-size:.78rem;'; th.textContent = slotHeaders[s]; thead.appendChild(th); } tbl.appendChild(thead); /* Rows */ for (var d = 0; d < days.length; d++) { var row = document.createElement('tr'); var lblCell = document.createElement('td'); lblCell.style.cssText = 'padding:8px 6px;border-bottom:1px solid #e5e5e5;font-weight:700;font-size:.85rem;color:#1a1a2e;'; var dayLabel = scorecardDayLabel(days[d]); lblCell.textContent = dayLabel; row.appendChild(lblCell); for (var s2 = 0; s2 < slots.length; s2++) { var cell = scorecardBuildCell(dog, days[d], dayLabel, slots[s2]); row.appendChild(cell); } tbl.appendChild(row); } wrap.appendChild(tbl); z.appendChild(wrap); /* Legend */ var legend = document.createElement('div'); legend.style.cssText = 'font-size:.75rem;color:#666;margin-top:6px;text-align:center;'; legend.innerHTML = 'Tap a cell when food/meds given. Greyed cells = not on this dog’s schedule.'; z.appendChild(legend); return z; } function scorecardBuildCell(dog, dayDateStr, dayLabel, slotKey) { var cell = document.createElement('td'); cell.style.cssText = 'border-bottom:1px solid #e5e5e5;padding:4px;text-align:center;'; var greyed = scorecardCellGreyed(dog, slotKey); var state = scorecardCellState(SC_MODAL_ENTRIES, dayDateStr, dayLabel, slotKey); if (greyed) { var grey = document.createElement('div'); grey.style.cssText = 'background:#e5e5e5;color:#999;padding:8px 4px;border-radius:6px;font-size:.78rem;min-height:38px;display:flex;align-items:center;justify-content:center;'; grey.textContent = '—'; cell.appendChild(grey); return cell; } var btn = document.createElement('button'); btn.type = 'button'; if (state.done) { btn.style.cssText = 'background:#dcfce7;color:#166534;border:1px solid #86efac;padding:8px 4px;border-radius:6px;font-weight:700;font-size:.78rem;width:100%;cursor:pointer;min-height:38px;'; btn.innerHTML = '✓ ' + state.initials + '
' + state.time + ''; } else { btn.style.cssText = 'background:#fff;color:#1974CE;border:2px dashed #cfe5ff;padding:8px 4px;border-radius:6px;font-weight:600;font-size:.78rem;width:100%;cursor:pointer;min-height:38px;'; btn.textContent = 'Tap'; } btn.addEventListener('click', function() { if (state.done) { /* For v1: reclick allowed only if explicit confirm — simpler to just ignore. */ if (!confirm('Already marked done by ' + state.initials + ' at ' + state.time + '. Re-mark anyway?')) return; } ensureStaffInitials(function(initials) { var entryType = (slotKey.indexOf('med') >= 0) ? 'MEDS' : 'FOOD'; scorecardPost({ entry_type: entryType, slot_label: scorecardSlotLabel(slotKey, dayLabel), staff_initials: initials }); }); }); cell.appendChild(btn); return cell; } function scorecardZoneDaycareCard(dog) { var z = document.createElement('div'); z.appendChild(scorecardZoneTitle('2. Today’s Care')); var today = new Date(); var todayStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0'); var todayLabel = scorecardDayLabel(todayStr); var rows = [ { label: 'Feeding', slot: 'breakfast', type: 'FOOD' }, /* daycare uses single feeding */ { label: 'Medications', slot: 'am_med', type: 'MEDS' }, { label: 'Playgroup', slot: 'playgroup', type: 'NOTES', alwaysActive: true }, { label: 'Extras', slot: 'extras', type: 'NOTES', alwaysActive: true } ]; var tbl = document.createElement('table'); tbl.style.cssText = 'border-collapse:collapse;width:100%;font-size:.92rem;'; for (var r = 0; r < rows.length; r++) { var row = rows[r]; var greyed = !row.alwaysActive && scorecardCellGreyed(dog, row.slot); var tr = document.createElement('tr'); var lblCell = document.createElement('td'); lblCell.style.cssText = 'padding:10px 6px;border-bottom:1px solid #e5e5e5;font-weight:700;width:35%;'; lblCell.textContent = row.label; tr.appendChild(lblCell); var actCell = document.createElement('td'); actCell.style.cssText = 'padding:8px 6px;border-bottom:1px solid #e5e5e5;text-align:center;'; if (greyed) { var greyDiv = document.createElement('div'); greyDiv.style.cssText = 'color:#999;font-size:.85rem;'; greyDiv.textContent = '— Not on schedule —'; actCell.appendChild(greyDiv); } else { var slotForCheck = row.slot; var state = (row.type === 'NOTES') ? { done: SC_MODAL_ENTRIES.some(function(e) { return e.type === 'NOTES' && e.payload.indexOf(row.label) >= 0; }) } : scorecardCellState(SC_MODAL_ENTRIES, todayStr, todayLabel, slotForCheck); var btn = document.createElement('button'); btn.type = 'button'; btn.style.cssText = state.done ? 'background:#dcfce7;color:#166534;border:1px solid #86efac;padding:10px 14px;border-radius:8px;font-weight:700;cursor:pointer;' : 'background:#1974CE;color:#fff;border:none;padding:10px 14px;border-radius:8px;font-weight:700;cursor:pointer;'; btn.textContent = state.done ? ('✓ Done' + (state.initials ? ' · ' + state.initials : '')) : 'Mark Done'; btn.addEventListener('click', function(rowCopy) { return function() { ensureStaffInitials(function(initials) { if (rowCopy.type === 'NOTES') { var note = prompt(rowCopy.label + ' note (optional details):'); if (note === null) return; scorecardPost({ entry_type: 'NOTES', text: rowCopy.label + (note ? ' — ' + note : ''), staff_initials: initials }); } else { scorecardPost({ entry_type: rowCopy.type, slot_label: scorecardSlotLabel(rowCopy.slot, todayLabel), staff_initials: initials }); } }); }; }(row)); actCell.appendChild(btn); } tr.appendChild(actCell); tbl.appendChild(tr); } z.appendChild(tbl); return z; } function scorecardZoneNotes(dog) { var z = document.createElement('div'); z.appendChild(scorecardZoneTitle('3. Shift Notes & Handoff')); var notes = SC_MODAL_ENTRIES.filter(function(e) { return e.type === 'NOTES'; }); var box = document.createElement('div'); box.style.cssText = 'background:#fafafa;border:1px solid #e5e5e5;border-radius:10px;padding:12px;'; if (notes.length === 0) { var empty = document.createElement('div'); empty.style.cssText = 'color:#999;font-style:italic;text-align:center;padding:8px;font-size:.88rem;'; empty.textContent = 'No notes yet.'; box.appendChild(empty); } else { for (var i = 0; i < Math.min(notes.length, 8); i++) { var n = notes[i]; var line = document.createElement('div'); line.style.cssText = 'padding:6px 0;border-bottom:1px solid #f0f0f0;font-size:.88rem;'; line.innerHTML = '' + n.date + ' ' + n.time + ' (' + n.initials + '): ' + escapeHtml(n.payload); box.appendChild(line); } } var addBtn = document.createElement('button'); addBtn.type = 'button'; addBtn.style.cssText = 'margin-top:10px;width:100%;padding:10px;background:#1974CE;color:#fff;border:none;border-radius:8px;font-weight:700;cursor:pointer;'; addBtn.textContent = '+ Add a Note'; addBtn.addEventListener('click', function() { var note = prompt('Shift note:'); if (!note) return; ensureStaffInitials(function(initials) { scorecardPost({ entry_type: 'NOTES', text: note, staff_initials: initials }); }); }); box.appendChild(addBtn); z.appendChild(box); return z; } function scorecardZoneDeparture(dog) { var z = document.createElement('div'); z.appendChild(scorecardZoneTitle('4. Departure', '#10b981')); var box = document.createElement('div'); box.style.cssText = 'background:#f0fdf4;border:1px solid #86efac;border-radius:10px;padding:14px;'; var checkedOut = SC_MODAL_ENTRIES.some(function(e) { return e.type === 'CHECKOUT'; }); if (checkedOut) { var coEntry = SC_MODAL_ENTRIES.find(function(e) { return e.type === 'CHECKOUT'; }); var coLine = document.createElement('div'); coLine.style.cssText = 'color:#166534;font-weight:700;margin-bottom:10px;'; coLine.textContent = '✓ Checked out ' + (coEntry ? '(' + coEntry.initials + ' · ' + coEntry.date + ' ' + coEntry.time + ')' : ''); box.appendChild(coLine); } /* Stay summary input */ var lbl = document.createElement('label'); lbl.style.cssText = 'display:block;font-weight:700;font-size:.82rem;text-transform:uppercase;color:#166534;margin-bottom:4px;letter-spacing:.04em;'; lbl.textContent = 'Stay summary (for owner at pickup)'; box.appendChild(lbl); var ta = document.createElement('textarea'); ta.id = 'scStaySummaryInput'; ta.rows = 3; ta.value = dog.stay_summary__note || ''; ta.style.cssText = 'width:100%;padding:10px;border:1px solid #e5e5e5;border-radius:8px;font-family:inherit;font-size:.92rem;resize:vertical;box-sizing:border-box;'; ta.placeholder = 'e.g. Charlie was a gem this stay. Took meds without fuss, played mostly in Quiet Yard with his buddy Mowgli.'; box.appendChild(ta); var ta2lbl = document.createElement('label'); ta2lbl.style.cssText = 'display:block;font-weight:700;font-size:.82rem;text-transform:uppercase;color:#166534;margin:10px 0 4px;letter-spacing:.04em;'; ta2lbl.textContent = 'Items returned (notes)'; box.appendChild(ta2lbl); var ta2 = document.createElement('input'); ta2.id = 'scItemsReturnedInput'; ta2.type = 'text'; ta2.value = dog.other_items_returned_to_customer__notes || ''; ta2.style.cssText = 'width:100%;padding:10px;border:1px solid #e5e5e5;border-radius:8px;font-family:inherit;font-size:.92rem;box-sizing:border-box;'; ta2.placeholder = 'e.g. All meds, blue paw bowl, magenta bag'; box.appendChild(ta2); var saveBtn = document.createElement('button'); saveBtn.type = 'button'; saveBtn.style.cssText = 'margin-top:12px;width:100%;padding:12px;background:#10b981;color:#fff;border:none;border-radius:8px;font-weight:700;cursor:pointer;font-size:.95rem;'; saveBtn.textContent = checkedOut ? 'Update Departure Notes' : 'Mark Checked Out & Save Summary'; saveBtn.addEventListener('click', function() { ensureStaffInitials(function(initials) { var summary = ta.value.trim(); var itemsReturned = ta2.value.trim(); var extras = {}; if (summary) extras.stay_summary__note = summary; if (itemsReturned) extras.other_items_returned_to_customer__notes = itemsReturned; scorecardPost({ entry_type: 'CHECKOUT', text: 'Departure notes saved', staff_initials: initials, extra_field_writes: extras }); }); }); box.appendChild(saveBtn); z.appendChild(box); return z; } function scorecardZoneActivity(dog) { var z = document.createElement('div'); z.appendChild(scorecardZoneTitle('5. Activity Log', '#666')); var box = document.createElement('div'); box.style.cssText = 'background:#fafafa;border:1px solid #e5e5e5;border-radius:8px;padding:10px 14px;font-family:Menlo,monospace;font-size:.78rem;color:#444;max-height:240px;overflow-y:auto;'; if (SC_MODAL_ENTRIES.length === 0) { box.textContent = '(no entries yet)'; } else { for (var i = 0; i < SC_MODAL_ENTRIES.length; i++) { var e = SC_MODAL_ENTRIES[i]; var line = document.createElement('div'); line.style.cssText = 'padding:3px 0;border-bottom:1px dotted #eee;'; line.textContent = e.date + ' ' + e.time + ' — [' + e.type + '] ' + e.payload + ' — ' + e.initials; box.appendChild(line); } } z.appendChild(box); return z; } function scorecardPost(payload) { if (!SC_MODAL_DOG || !SC_MODAL_DOG.opp_id) return; payload.opp_id = SC_MODAL_DOG.opp_id; payload.action = 'staff_care_log_append'; callWebhook('/webhook/staff-board', payload, function(err, data) { if (err) { alert('Save failed: ' + err.message); return; } if (data && data.ok === false) { alert('Save failed: ' + (data.error || 'unknown error')); return; } /* Re-render to show latest state */ scorecardRender(); }); } /* Helper: HTML escape */ function escapeHtml(s) { if (s == null) return ''; return ('' + s).replace(/[&<>"']/g, function(c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); }