// --- Utilities --- const $ = (sel, el = document) => el.querySelector(sel); const $$ = (sel, el = document) => Array.from(el.querySelectorAll(sel)); function getTodayStr() { return new Date().toISOString().split('T')[0]; } function formatDate(dateStr) { const d = new Date(dateStr + 'T00:00:00'); return d.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); } function formatDateTime() { return new Date().toLocaleString('en-US', { dateStyle: 'full', timeStyle: 'short' }); } // --- Storage --- const STORAGE_KEY = 'poolChecklist_v1'; function loadData() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {}; } catch { return {}; } } function saveData() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state.data)); } // --- Schedule Data --- const SCHEDULE = [ { key: 'morning', label: 'Morning Schedule', icon: 'sun', periods: [ { time: '5:00AM - 5:30AM', tasks: ['Open Umbrella','Check Filters','Check Pumps','Check Water Level','Check Water Temperature','Check Water Clarity (Clear or Cloudy)','Open sprinkler for landscape'] }, { time: '5:30AM - 6:00AM', tasks: ['Test Water pH Level (ideal 7.4 - 7.6)','Test Chlorine level (ideal 1-3 ppm)','Add Chlorine (if needed)','Add Acid (if needed)','Add algaecide (if needed)'] }, { time: '6:00AM - 8:00AM', tasks: ['Leaf Skimming (surface)','Check, clean & empty skimmer baskets','Vacuum Pool','Clean/scrub pool walls & floors','Unclog and clean foot baths'] }, { time: '8:00AM - 9:00AM', tasks: ['Backwash filter (if needed)','Polish pool ladders','Scrub pool walls & decks'] }, { time: '9:00AM - 10:30AM', tasks: ['Doors & door locks','Lavatory Sinks','Faucets & fittings','Shower & fittings','Bidets','Mirrors','Toilet bowls & fittings','Walls','Floors','Light, Switches & Outlets','Drains','Exhaust Fans','Garbage Cans'] }, { time: '10:30AM - 12:00NN', tasks: ['Gardening Works Outside Pool'] }, { time: '1:00PM - 2:00PM', tasks: ['Final leaf skimming','Endorse left work or issue upon duty'] } ] }, { key: 'afternoon', label: 'Afternoon Schedule', icon: 'moon', periods: [ { time: '1:00PM - 2:00PM', tasks: ['Inspect pumps','Inspect filters','Check overall cleanliness of pool and surroundings'] }, { time: '2:00PM - 6:00PM', tasks: ['Leaf Skimming (as needed)','Inspect and clean skimmer baskets if debris is heavy','Spot Cleaning of pool decks and walls','Check furniture alignments','Ensure umbrellas remain folded when not in use','Observe water clarity and circulation'] }, { time: '6:00PM - 7:30PM', tasks: ['Clean toilet bowls & fittings','Clean lavatory sinks','Wipe mirrors','Spot clean floor and drains','Check exhaust fans','Clean garbage bins'] }, { time: '7:30PM - 8:00PM', tasks: ['Final light leaf skimming','Visual Clarity and water level check'] }, { time: '8:00PM - 9:00PM', tasks: ['Final pump and filter inspection','Water level and clarity check','Overall cleanliness inspection (pool, CR, Common areas)','Secure equipment and area'] }, { time: '9:00PM - 10:00PM', tasks: ['Watering of landscape','Fold all umbrella'] } ] } ]; const TOTAL_TASKS = SCHEDULE.reduce((sum, shift) => sum + shift.periods.reduce((s, p) => s + p.tasks.length, 0), 0); // --- State --- const state = { view: 'dashboard', selectedDate: getTodayStr(), data: loadData(), attendantName: localStorage.getItem('poolAttendantName') || '' }; // --- Init --- function init() { navigate('dashboard'); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeReport(); }); } // --- Navigation --- function updateNav() { $$('.nav-btn').forEach(btn => { const isActive = btn.id === `nav-${state.view}`; btn.className = `nav-btn px-4 py-1.5 rounded-md text-sm font-medium transition ${isActive ? 'bg-white text-cyan-700 shadow-sm' : 'text-slate-500 hover:text-slate-700'}`; }); } function navigate(view, date = null) { if (date) state.selectedDate = date; state.view = view; updateNav(); if (view === 'dashboard') renderDashboard(); else if (view === 'checklist') renderChecklist(); lucide.createIcons(); } function setMainContent(html) { $('#app').innerHTML = `
${html}
`; } // --- Stats --- function getDayData(date) { return state.data[date] || {}; } function calculateStats(date) { const day = getDayData(date); let completed = 0; let withPhotos = 0; SCHEDULE.forEach(shift => { shift.periods.forEach((period, pi) => { period.tasks.forEach((_, ti) => { const tid = `${shift.key}-${pi}-${ti}`; const t = day[tid]; if (t?.completed) completed++; if (t?.photo) withPhotos++; }); }); }); return { total: TOTAL_TASKS, completed, pending: TOTAL_TASKS - completed, withPhotos, percent: Math.round((completed / TOTAL_TASKS) * 100) }; } // --- Dashboard --- function renderDashboard() { const today = getTodayStr(); const todayStats = calculateStats(today); const dates = Object.keys(state.data).sort((a, b) => new Date(b + 'T00:00:00') - new Date(a + 'T00:00:00')); let historyHtml = dates.slice(0, 14).map(date => { const s = calculateStats(date); return `
${s.percent}%
${formatDate(date)}
${s.completed}/${s.total} tasks • ${s.withPhotos} photos
`; }).join(''); if (dates.length === 0) { historyHtml = `
No records yet. Open the checklist to start tracking.
`; } const html = `

Dashboard

Monitor daily pool operations at a glance.

Today
${todayStats.percent}%
Completion Rate
${todayStats.completed}
Tasks Done
${todayStats.pending}
Tasks Pending
${todayStats.withPhotos}
Photos Uploaded

Recent History

${historyHtml}

Daily Reminder

Maintain water pH at 7.4 - 7.6 and chlorine at 1-3 ppm. Fold umbrellas by 10:00PM and secure all equipment.

Scheduled Tasks
${TOTAL_TASKS}
across morning & afternoon shifts
`; setMainContent(html); lucide.createIcons(); } // --- Checklist --- function renderChecklist() { const dayData = getDayData(state.selectedDate); const stats = calculateStats(state.selectedDate); const shiftHtml = SCHEDULE.map(shift => { const periodHtml = shift.periods.map((period, pi) => { const tasksHtml = period.tasks.map((task, ti) => { const tid = `${shift.key}-${pi}-${ti}`; const tdata = dayData[tid] || {}; const isDone = !!tdata.completed; const photo = tdata.photo || null; const notes = tdata.notes || ''; return `
${task}
${photo ? `
` : ''}
`; }).join(''); return `
${tasksHtml}
`; }).join(''); return `

${shift.label}

${periodHtml}
`; }).join(''); const html = `

Daily Checklist

Complete tasks, attach photos, and add notes for verification.

${stats.percent}%
${shiftHtml}
`; setMainContent(html); setupChecklistListeners(); lucide.createIcons(); } function setupChecklistListeners() { const app = $('#app'); const dp = $('#date-picker', app); if (dp) { dp.addEventListener('change', (e) => { state.selectedDate = e.target.value; renderChecklist(); showToast(`Switched to ${formatDate(state.selectedDate)}`); }); } const an = $('#attendant-name', app); if (an) { an.addEventListener('input', (e) => { state.attendantName = e.target.value; localStorage.setItem('poolAttendantName', state.attendantName); }); } $$('.task-checkbox', app).forEach(cb => { cb.addEventListener('change', (e) => { const tid = e.target.dataset.tid; if (!state.data[state.selectedDate]) state.data[state.selectedDate] = {}; if (!state.data[state.selectedDate][tid]) state.data[state.selectedDate][tid] = {}; state.data[state.selectedDate][tid].completed = e.target.checked; state.data[state.selectedDate][tid].updatedAt = new Date().toISOString(); saveData(); const row = e.target.closest('.task-row'); const text = row.querySelector('.task-text'); if (e.target.checked) { text.classList.add('line-through', 'text-slate-400'); row.classList.add('bg-slate-50/50'); } else { text.classList.remove('line-through', 'text-slate-400'); row.classList.remove('bg-slate-50/50'); } showToast(e.target.checked ? 'Task completed' : 'Task unchecked'); }); }); $$('.photo-trigger', app).forEach(btn => { btn.addEventListener('click', () => { const row = btn.closest('.task-row'); const input = row.querySelector('.photo-input'); input.click(); }); }); $$('.photo-input', app).forEach(input => { input.addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; const tid = input.dataset.tid; resizeImage(file, 800, (base64) => { if (!state.data[state.selectedDate]) state.data[state.selectedDate] = {}; if (!state.data[state.selectedDate][tid]) state.data[state.selectedDate][tid] = {}; state.data[state.selectedDate][tid].photo = base64; state.data[state.selectedDate][tid].updatedAt = new Date().toISOString(); saveData(); renderChecklist(); showToast('Photo uploaded'); }); }); }); $$('.remove-photo', app).forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const tid = btn.dataset.tid; if (state.data[state.selectedDate]?.[tid]) { delete state.data[state.selectedDate][tid].photo; if (!state.data[state.selectedDate][tid].completed && !state.data[state.selectedDate][tid].notes) { delete state.data[state.selectedDate][tid]; } saveData(); renderChecklist(); showToast('Photo removed'); } }); }); $$('.task-notes', app).forEach(input => { let timeout; input.addEventListener('input', (e) => { const tid = e.target.dataset.tid; clearTimeout(timeout); timeout = setTimeout(() => { if (!state.data[state.selectedDate]) state.data[state.selectedDate] = {}; if (!state.data[state.selectedDate][tid]) state.data[state.selectedDate][tid] = {}; state.data[state.selectedDate][tid].notes = e.target.value; saveData(); }, 600); }); }); } // --- Image Resizing --- function resizeImage(file, maxWidth, callback) { const reader = new FileReader(); reader.onload = (evt) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); const scale = Math.min(1, maxWidth / img.width); canvas.width = img.width * scale; canvas.height = img.height * scale; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); callback(canvas.toDataURL('image/jpeg', 0.8)); }; img.src = evt.target.result; }; reader.readAsDataURL(file); } // --- Report --- function generateReport(date) { const dayData = getDayData(date); const stats = calculateStats(date); const generatedOn = formatDateTime(); const shiftsHtml = SCHEDULE.map(shift => { const periodsHtml = shift.periods.map((period, pi) => { const tasksHtml = period.tasks.map((task, ti) => { const tid = `${shift.key}-${pi}-${ti}`; const t = dayData[tid] || {}; const done = !!t.completed; return `
${done ? '
' : '
'}
${task}
${t.notes ? `
Note: ${t.notes}
` : ''} ${t.photo ? `` : ''}
`; }).join(''); return `
${period.time}
${tasksHtml}
`; }).join(''); return `

${shift.label}

${periodsHtml}
`; }).join(''); const html = `

Pool Attendant Checklist Report

${formatDate(date)}
${state.attendantName || 'Unassigned'}
${stats.completed} / ${stats.total} Completed
${shiftsHtml}
Report generated on ${generatedOn} PoolCheck Pro System
Prepared By
Supervisor Sign-off
`; $('#report-content').innerHTML = html; $('#report-modal').classList.remove('hidden'); lucide.createIcons(); } function closeReport() { $('#report-modal').classList.add('hidden'); } // --- Toast --- let toastTimer; function showToast(msg) { const toast = $('#toast'); $('#toast-msg').textContent = msg; toast.classList.remove('translate-y-20', 'opacity-0'); clearTimeout(toastTimer); toastTimer = setTimeout(() => { toast.classList.add('translate-y-20', 'opacity-0'); }, 2200); } // --- Expose Globals --- window.navigate = navigate; window.generateReport = generateReport; window.closeReport = closeReport; window.goToDate = (date) => navigate('checklist', date); // Start init();