// ============================================================ // Checklists Module // Views: Templates, Instances, Builder, Fill-Out, Job Types // ============================================================ let clState = { tab: 'templates', tPage: 1, tSearch: '', tType: '', tActive: '1', iPage: 1, iStatus: '', iType: '', iDateFrom: '', iDateTo: '', }; let _clJobTypes = null; let _clVehicles = null; let _clUsers = null; // ── Entry Point ────────────────────────────────────────────── async function renderChecklists(params = {}) { if (params.instance_id) return renderInstanceFillOut(params.instance_id); const content = document.getElementById('page-content'); const canManage = Auth.can('checklists', 'create') || Auth.can('checklists', 'edit'); content.innerHTML = `

Checklists

Templates, schedules and submissions

${canManage ? `` : ''}
`; await _loadClCaches(); renderChecklistsTab(clState.tab); } async function _loadClCaches() { const [jt, veh, usr] = await Promise.all([ _clJobTypes ? Promise.resolve({ success: true, data: { types: _clJobTypes } }) : API.post('jobcard_types/list', {}), _clVehicles ? Promise.resolve({ success: true, data: { vehicles: _clVehicles } }) : API.post('fleet/vehicles', { action: 'list' }), _clUsers ? Promise.resolve({ success: true, data: { users: _clUsers } }) : API.post('auth/users_list', {}), ]); if (jt.success) _clJobTypes = jt.data.types || []; if (veh.success) _clVehicles = veh.data.vehicles || []; if (usr.success) _clUsers = usr.data.users || []; } function renderChecklistsTab(tab) { clState.tab = tab; document.querySelectorAll('#cl-tabs .tab-btn').forEach(b => { b.classList.toggle('active', b.dataset.tab === tab); }); const wrap = document.getElementById('cl-tab-content'); if (!wrap) return; wrap.innerHTML = '
'; if (tab === 'templates') loadTemplatesList(); if (tab === 'instances') loadInstancesList(); } // ── TEMPLATES LIST ─────────────────────────────────────────── async function loadTemplatesList() { const wrap = document.getElementById('cl-tab-content'); if (!wrap) return; const canManage = Auth.can('checklists', 'create') || Auth.can('checklists', 'edit'); wrap.innerHTML = `
`; await _fetchTemplates(); } const clTSearchDebounced = debounce(val => { clState.tSearch = val; _fetchTemplates(); }, 350); async function _fetchTemplates() { const body = document.getElementById('cl-templates-body'); if (!body) return; const canManage = Auth.can('checklists', 'create') || Auth.can('checklists', 'edit'); const res = await API.post('checklists/template_list', { page: clState.tPage, limit: 20, search: clState.tSearch, linked_type: clState.tType, is_active: clState.tActive, }); if (!res.success) { body.innerHTML = `
${res.message}
`; return; } const templates = res.data.templates || []; if (!templates.length) { body.innerHTML = `
📋
No templates found

Create your first checklist template.

${canManage ? `` : ''}
`; document.getElementById('cl-t-pagination').innerHTML = ''; return; } const typeIcon = { general: '📂', fleet: '🚗', job_card: '📋' }; const typeLabel = { general: 'General', fleet: 'Fleet', job_card: 'Job Card' }; const freqLabel = { once: 'Once', daily: 'Daily', weekly: 'Weekly', monthly: 'Monthly', custom: 'Custom' }; if (window.innerWidth < 768) { body.innerHTML = `
${templates.map(t => `
${clH(t.name)}
${t.description ? `
${clH(t.description)}
` : ''} ${t.vehicle_reg ? `
🚗 ${clH(t.vehicle_reg)} — ${clH(t.vehicle_make || '')} ${clH(t.vehicle_model || '')}
` : ''}
${typeIcon[t.linked_type] || ''} ${typeLabel[t.linked_type] || t.linked_type} ${freqLabel[t.frequency] || t.frequency || 'Once'} ${t.item_count || 0} Qs · ${t.instance_count || 0} submissions
${t.is_active ? 'Active' : 'Inactive'} ${t.pending_count > 0 ? `${t.pending_count} pending` : ''}
${canManage ? `` : ''} ${t.linked_type !== 'job_card' ? `📄 PDF` : ''}
`).join('')}
`; } else { body.innerHTML = `
${templates.map(t => ` `).join('')}
NameTypeFrequencyQuestionsSubmissionsPendingStatus
${clH(t.name)}
${t.description ? `
${clH(t.description)}
` : ''} ${t.vehicle_reg ? `
🚗 ${clH(t.vehicle_reg)} — ${clH(t.vehicle_make || '')} ${clH(t.vehicle_model || '')}
` : ''}
${typeIcon[t.linked_type] || ''} ${typeLabel[t.linked_type] || t.linked_type} ${freqLabel[t.frequency] || t.frequency || 'Once'} ${t.item_count || 0} ${t.instance_count || 0} ${t.pending_count > 0 ? `${t.pending_count}` : '0'} ${t.is_active ? 'Active' : 'Inactive'} ${canManage ? ` ` : ''} ${t.linked_type !== 'job_card' ? `📄 Blank PDF` : ''}
`; } renderPagination('cl-t-pagination', res.data.pagination, `p => { clState.tPage=p; _fetchTemplates(); }`); } // ── INSTANCES LIST ─────────────────────────────────────────── async function loadInstancesList() { const wrap = document.getElementById('cl-tab-content'); if (!wrap) return; wrap.innerHTML = `
`; await _fetchInstances(); } async function _fetchInstances() { const body = document.getElementById('cl-instances-body'); if (!body) return; const res = await API.post('checklists/instance_list', { page: clState.iPage, limit: 25, status: clState.iStatus, linked_type: clState.iType, date_from: clState.iDateFrom, date_to: clState.iDateTo, }); if (!res.success) { body.innerHTML = `
${res.message}
`; return; } const instances = res.data.instances || []; if (!instances.length) { body.innerHTML = `
No submissions found
`; document.getElementById('cl-i-pagination').innerHTML = ''; return; } const statusCls = { pending: 'badge-warning', in_progress: 'badge-info', completed: 'badge-success', missed: 'badge-danger' }; if (window.innerWidth < 768) { body.innerHTML = `
${instances.map(i => { const pct = i.total_items > 0 ? Math.round((i.answered_items / i.total_items) * 100) : 0; return `
${clH(i.template_name || '—')}
${Fmt.date(i.due_date)} · ${clH(i.assigned_to_name || 'Unassigned')}
${i.vehicle_reg ? `
🚗 ${clH(i.vehicle_reg)}
` : ''}
${Fmt.capitalize((i.status || '').replace('_', ' '))}
${i.answered_items}/${i.total_items}
${i.status !== 'completed' ? `` : `📄 PDF`}
`; }).join('')}
`; } else { body.innerHTML = `
${instances.map(i => { const pct = i.total_items > 0 ? Math.round((i.answered_items / i.total_items) * 100) : 0; return ` `; }).join('')}
ChecklistTypeDue DateAssigned ToVehicleProgressStatus
${clH(i.template_name || '—')} ${i.linked_type || '—'} ${Fmt.date(i.due_date)}${i.due_time ? ' ' + i.due_time.slice(0, 5) : ''} ${clH(i.assigned_to_name || '—')} ${i.vehicle_reg ? clH(i.vehicle_reg) + ' ' + clH(i.vehicle_make || '') : '—'}
${i.answered_items}/${i.total_items}
${Fmt.capitalize((i.status || '').replace('_', ' '))} ${i.status !== 'completed' ? `` : `📄 PDF`}
`; } renderPagination('cl-i-pagination', res.data.pagination, `p => { clState.iPage=p; _fetchInstances(); }`); } // ── INSTANCE FILL-OUT ──────────────────────────────────────── async function renderInstanceFillOut(instanceId) { const content = document.getElementById('page-content'); content.innerHTML = `
`; const res = await API.post('checklists/instance_get', { id: instanceId }); if (!res.success) { Toast.show(res.message, 'error'); Router.navigate('checklists'); return; } const inst = res.data.instance; const items = inst.items || []; const isCompleted = inst.status === 'completed'; // Set BEFORE items.map so _renderImageSection can read it during HTML build window._clInstanceId = instanceId; window._clInstanceAnswers = {}; items.forEach(it => { window._clInstanceAnswers[it.id] = { answer_bool: it.answer_bool !== null && it.answer_bool !== undefined ? parseInt(it.answer_bool) : null, answer_text: it.answer_text || '', note: it.note || '', images: it.images || [], // array of {id, filename} }; }); const statusCls = { pending: 'badge-warning', in_progress: 'badge-info', completed: 'badge-success', missed: 'badge-danger' }; content.innerHTML = `

${clH(inst.template_name || 'Checklist')}

Due: ${Fmt.date(inst.due_date)}${inst.due_time ? ' ' + inst.due_time.slice(0, 5) : ''} ${inst.assigned_to_name ? ` · Assigned to ${clH(inst.assigned_to_name)}` : ''} ${inst.vehicle_reg ? ` · 🚗 ${clH(inst.vehicle_reg)} ${clH(inst.vehicle_make || '')} ${clH(inst.vehicle_model || '')}` : ''}

${Fmt.capitalize((inst.status || '').replace(/_/g, ' '))} ${isCompleted ? `📄 Download PDF` : ''}
${inst.template_description ? `
${clH(inst.template_description)}
` : ''}
${items.length ? items.map((it, idx) => _renderQuestionCard(it, idx, isCompleted)).join('') : '

This checklist has no questions.

'}
${!isCompleted && items.length ? `
` : ''} ${isCompleted ? `

✓ Submitted by ${clH(inst.completed_by_name || '—')} on ${Fmt.datetime(inst.submitted_at)}

${inst.instance_notes ? `

📝 ${clH(inst.instance_notes)}

` : ''}
` : ''}
`; } function _renderQuestionCard(it, idx, isReadOnly) { const ans = window._clInstanceAnswers?.[it.id] || {}; const isYes = ans.answer_bool === 1; const isNo = ans.answer_bool === 0; const images = ans.images || []; const needsImage = it.requires_image === 'always' || (it.requires_image === 'on_yes' && isYes) || (it.requires_image === 'on_no' && isNo); const answerHtml = (() => { if (it.answer_type === 'yes_no') { if (isReadOnly) { return `
${isYes ? '✓ Yes' : isNo ? '✕ No' : '— Unanswered'}
`; } return `
`; } if (it.answer_type === 'text') { if (isReadOnly) return `
${ans.answer_text ? clH(ans.answer_text) : 'No answer'}
`; return ``; } if (it.answer_type === 'number') { if (isReadOnly) return `
${ans.answer_text || 'No answer'}
`; return `
`; } return `
Photo submission required
`; })(); const showImgSection = needsImage || images.length > 0 || it.requires_image !== 'never'; const noteHtml = it.answer_type !== 'text' ? ( isReadOnly && ans.note ? `
📝 ${clH(ans.note)}
` : !isReadOnly ? `
📝 Add note
` : '' ) : ''; return `
${idx + 1}
${clH(it.item_text)}${it.is_required ? ' *' : ' (optional)'}
${answerHtml}
${_renderImageSection(it.id, images, isReadOnly, needsImage)}
${noteHtml}
`; } function _renderImageSection(itemId, images, isReadOnly, needsImage) { const instanceId = window._clInstanceId || 0; const thumbs = images.map(img => `
${!isReadOnly ? `` : ''}
`).join(''); const addBtn = !isReadOnly ? ` ` : (images.length === 0 ? 'No images attached' : ''); return `
${thumbs}${addBtn}
`; } function clAnswerYN(itemId, val) { if (!window._clInstanceAnswers) window._clInstanceAnswers = {}; if (!window._clInstanceAnswers[itemId]) window._clInstanceAnswers[itemId] = {}; window._clInstanceAnswers[itemId].answer_bool = val; const card = document.getElementById(`cl-q-${itemId}`); if (!card) return; card.querySelectorAll('.cl-yn-btn').forEach(b => b.classList.remove('active-yes', 'active-no')); const btns = card.querySelectorAll('.cl-yn-btn'); if (val === 1) btns[0]?.classList.add('active-yes'); else btns[1]?.classList.add('active-no'); card.style.outline = ''; const req = card.dataset.requiresImage; const imgSec = document.getElementById(`cl-img-section-${itemId}`); if (imgSec) { const show = req === 'always' || (req === 'on_yes' && val === 1) || (req === 'on_no' && val === 0); const hasImages = (window._clInstanceAnswers[itemId]?.images || []).length > 0; imgSec.style.display = (show || hasImages || req !== 'never') ? '' : 'none'; // Refresh the add button required label const needsNow = req === 'always' || (req === 'on_yes' && val === 1) || (req === 'on_no' && val === 0); const lbl = document.getElementById(`cl-img-label-${itemId}`); if (lbl) { const span = lbl.querySelector('span:last-child'); if (span) span.textContent = (needsNow && !hasImages) ? 'Required' : 'Add'; } } } function clAnswerText(itemId, val) { if (!window._clInstanceAnswers) window._clInstanceAnswers = {}; if (!window._clInstanceAnswers[itemId]) window._clInstanceAnswers[itemId] = {}; window._clInstanceAnswers[itemId].answer_text = val; const card = document.getElementById(`cl-q-${itemId}`); if (card) card.style.outline = ''; } function clAnswerNote(itemId, val) { if (!window._clInstanceAnswers) window._clInstanceAnswers = {}; if (!window._clInstanceAnswers[itemId]) window._clInstanceAnswers[itemId] = {}; window._clInstanceAnswers[itemId].note = val; } async function clUploadImage(itemId, instanceId, input) { const file = input.files[0]; if (!file) return; // Reset input so the same file can be re-selected if needed input.value = ''; const prog = document.getElementById(`cl-img-progress-${itemId}`); const label = document.getElementById(`cl-img-label-${itemId}`); if (prog) prog.style.display = 'flex'; if (label) label.style.display = 'none'; const fd = new FormData(); fd.append('image', file); fd.append('instance_id', instanceId); fd.append('item_id', itemId); const res = await API.postForm('checklists/image_upload', fd); if (prog) prog.style.display = 'none'; if (label) label.style.display = ''; if (res.success) { if (!window._clInstanceAnswers[itemId]) window._clInstanceAnswers[itemId] = { images: [] }; if (!window._clInstanceAnswers[itemId].images) window._clInstanceAnswers[itemId].images = []; window._clInstanceAnswers[itemId].images.push({ id: res.data.id, filename: res.data.filename }); // Re-render image grid const card = document.getElementById(`cl-q-${itemId}`); const req = card?.dataset.requiresImage; const needsImage = req === 'always' || (req === 'on_yes' && window._clInstanceAnswers[itemId]?.answer_bool === 1) || (req === 'on_no' && window._clInstanceAnswers[itemId]?.answer_bool === 0); const grid = document.getElementById(`cl-img-grid-${itemId}`); if (grid) grid.outerHTML = _renderImageSection(itemId, window._clInstanceAnswers[itemId].images, false, needsImage); // Update required label const lbl = document.getElementById(`cl-img-label-${itemId}`); if (lbl) { const span = lbl.querySelector('span:last-child'); if (span) span.textContent = 'Add'; } Toast.show('Image uploaded.', 'success'); } else { Toast.show(res.message, 'error'); } } async function clDeleteImage(itemId, imageId, filename) { if (!window._clInstanceAnswers?.[itemId]) return; const res = imageId ? await API.post('checklists/image_delete', { image_id: imageId }) : { success: true }; // legacy fallback with no DB id if (res.success) { window._clInstanceAnswers[itemId].images = (window._clInstanceAnswers[itemId].images || []) .filter(img => img.filename !== filename); const card = document.getElementById(`cl-q-${itemId}`); const req = card?.dataset.requiresImage; const needsImage = req === 'always' || (req === 'on_yes' && window._clInstanceAnswers[itemId]?.answer_bool === 1) || (req === 'on_no' && window._clInstanceAnswers[itemId]?.answer_bool === 0); const grid = document.getElementById(`cl-img-grid-${itemId}`); if (grid) grid.outerHTML = _renderImageSection(itemId, window._clInstanceAnswers[itemId].images, false, needsImage); Toast.show('Image removed.', 'success'); } else Toast.show(res.message, 'error'); } function _buildAnswerPayload() { const answers = []; const state = window._clInstanceAnswers || {}; for (const [itemId, ans] of Object.entries(state)) { const card = document.getElementById(`cl-q-${itemId}`); if (!card) continue; const type = card.dataset.answerType; answers.push({ item_id: parseInt(itemId), answer_bool: type === 'yes_no' && ans.answer_bool !== null ? ans.answer_bool : null, answer_text: (type === 'text' || type === 'number') ? (ans.answer_text || '') : null, note: ans.note || null, }); } return answers; } async function clSaveProgress(instanceId) { const btn = document.querySelector('.cl-submission-area .btn-outline'); if (btn) btn.classList.add('loading'); const notes = document.getElementById('cl-inst-notes')?.value || ''; const res = await API.post('checklists/instance_submit', { instance_id: instanceId, answers: _buildAnswerPayload(), notes, action: 'save' }); if (btn) btn.classList.remove('loading'); if (res.success) Toast.show('Progress saved.', 'success'); else Toast.show(res.message, 'error'); } async function clSubmitChecklist(instanceId) { const missing = []; document.querySelectorAll('[data-item-id]').forEach(card => { if (card.dataset.required !== '1') return; const id = parseInt(card.dataset.itemId), type = card.dataset.answerType; const ans = window._clInstanceAnswers?.[id] || {}; if (type === 'yes_no' && ans.answer_bool === null) missing.push(card); if ((type === 'text' || type === 'number') && !ans.answer_text) missing.push(card); if (type === 'photo_only' && (!ans.images || ans.images.length === 0)) missing.push(card); if (card.dataset.requiresImage !== 'never' && (!ans.images || ans.images.length === 0) && needsImageForCard(card, ans)) missing.push(card); }); if (missing.length) { missing.forEach(c => c.style.outline = '2px solid var(--danger)'); Toast.show(`${missing.length} required question(s) not answered.`, 'error'); missing[0].scrollIntoView({ behavior: 'smooth', block: 'center' }); return; } const confirmed = await Modal.confirm('Submit this checklist? You cannot edit it after submission.', 'Submit Checklist'); if (!confirmed) return; const btn = document.querySelector('.cl-submission-area .btn-primary'); if (btn) btn.classList.add('loading'); const notes = document.getElementById('cl-inst-notes')?.value || ''; const res = await API.post('checklists/instance_submit', { instance_id: instanceId, answers: _buildAnswerPayload(), notes, action: 'submit' }); if (btn) btn.classList.remove('loading'); if (res.success) { Toast.show('Checklist submitted!', 'success'); renderInstanceFillOut(instanceId); } else Toast.show(res.message, 'error'); } // ── TEMPLATE DETAIL ────────────────────────────────────────── async function openTemplateDetail(id) { const res = await API.post('checklists/template_get', { id }); if (!res.success) { Toast.show(res.message, 'error'); return; } const t = res.data.template, items = t.items || []; const canManage = Auth.isDev() || Auth.isAdmin() || Auth.isHR(); const ansLabel = { yes_no: 'Yes/No', text: 'Text', number: 'Number', photo_only: 'Photo Only' }; const imgLabel = { never: 'No Image', always: 'Always', on_yes: 'If Yes', on_no: 'If No' }; Modal.open({ id: 'cl-detail', title: t.name, size: 'modal-lg', body: `
Type
${clH(t.linked_type)}
Frequency
${Fmt.capitalize(t.frequency || '')}
${t.starts_at ? `
Starts
${Fmt.date(t.starts_at)}
` : ''} ${t.ends_at ? `
Ends
${Fmt.date(t.ends_at)}
` : ''} ${t.assigned_to_name ? `
Assigned To
${clH(t.assigned_to_name)}
` : ''} ${t.vehicle_reg ? `
Vehicle
🚗 ${clH(t.vehicle_reg)} ${clH(t.vehicle_make || '')} ${clH(t.vehicle_model || '')}
` : ''}
${t.description ? `

${clH(t.description)}

` : ''}
${items.length} Question${items.length !== 1 ? 's' : ''}
${items.map((it, i) => `
${i + 1}${clH(it.item_text)}${it.is_required ? ' *' : ''}
${ansLabel[it.answer_type] || it.answer_type} ${it.requires_image !== 'never' ? `📷 ${imgLabel[it.requires_image] || it.requires_image}` : ''}
`).join('')}
`, footer: ` ${canManage ? `` : ''} `, }); } // ── START INSTANCE MODAL ───────────────────────────────────── async function openInstanceCreateModal(templateId, templateName, linkedType) { const vehicles = _clVehicles || [], users = _clUsers || []; Modal.open({ id: 'cl-start', title: `Start: ${templateName}`, body: `
${linkedType !== 'job_card' ? `
` : ''}
`, footer: ` `, }); } async function submitCreateInstance(templateId) { const btn = document.querySelector('#modal-cl-start .btn-primary'); if (btn) btn.classList.add('loading'); const data = getFormData('cl-start-form'); const res = await API.post('checklists/instance_create', { ...data, template_id: templateId }); if (btn) btn.classList.remove('loading'); if (res.success) { Toast.show('Checklist started!', 'success'); Modal.close(); Router.navigate('checklists', { instance_id: res.data.id }); } else Toast.show(res.message, 'error'); } // ── TEMPLATE BUILDER ───────────────────────────────────────── let _builderItems = []; let _builderEditId = 0; let _bUid = 5000; async function openTemplateBuilder(editId = 0) { _builderItems = []; _builderEditId = editId; let template = null; if (editId) { const res = await API.post('checklists/template_get', { id: editId }); if (!res.success) { Toast.show(res.message, 'error'); return; } template = res.data.template; _builderItems = (template.items || []).map(it => ({ _uid: it.id, item_text: it.item_text, answer_type: it.answer_type, requires_image: it.requires_image, is_required: it.is_required, })); } const vehicles = _clVehicles || [], users = _clUsers || [], jobTypes = _clJobTypes || []; const v = (k, def = '') => template ? (template[k] ?? def) : def; Modal.open({ id: 'cl-builder', title: editId ? `Edit: ${template.name}` : 'New Checklist Template', size: 'modal-xl', body: `
Checklist Info

Schedule
Questions ${_builderItems.length}
${_builderItems.length ? '' : '
No questions yet. Click “+ Add Question” to start.
'}
`, footer: ` `, }); _builderItems.forEach(it => _renderBuilderItem(it)); } function clBuilderTypeChange(val) { document.getElementById('cl-b-fleet-opts').style.display = val === 'fleet' ? '' : 'none'; } function clBuilderAddItem() { const item = { _uid: 'n' + (++_bUid), item_text: '', answer_type: 'yes_no', requires_image: 'never', is_required: 1 }; _builderItems.push(item); const empty = document.getElementById('cl-bi-empty'); if (empty) empty.remove(); _renderBuilderItem(item); _updateBuilderCount(); const list = document.getElementById('cl-builder-items'); if (list) setTimeout(() => { list.scrollTop = list.scrollHeight; const inp = document.getElementById(`cl-bi-${item._uid}`)?.querySelector('input[type=text]'); if (inp) inp.focus(); }, 60); } function _renderBuilderItem(item) { const list = document.getElementById('cl-builder-items'); if (!list) return; const div = document.createElement('div'); div.className = 'cl-builder-item'; div.id = `cl-bi-${item._uid}`; div.innerHTML = `
`; list.appendChild(div); } function clBuUpdate(uid, field, value) { const item = _builderItems.find(i => String(i._uid) === String(uid)); if (item) item[field] = value; } function clBuilderRemoveItem(uid) { _builderItems = _builderItems.filter(i => String(i._uid) !== String(uid)); const el = document.getElementById(`cl-bi-${uid}`); if (el) el.remove(); _updateBuilderCount(); const list = document.getElementById('cl-builder-items'); if (list && !list.children.length) list.innerHTML = '
No questions yet. Click “+ Add Question” to start.
'; } function _updateBuilderCount() { const el = document.getElementById('cl-b-cnt'); if (el) el.textContent = _builderItems.length; } async function submitTemplateBuilder() { const btn = document.querySelector('#modal-cl-builder .btn-primary'); if (btn) btn.classList.add('loading'); // Sync text inputs document.querySelectorAll('.cl-builder-item').forEach((el, i) => { const inp = el.querySelector('input[type=text]'); if (inp && _builderItems[i]) _builderItems[i].item_text = inp.value; }); const data = getFormData('cl-builder-form'); const items = _builderItems.filter(it => (it.item_text || '').trim()); if (!data.name) { Toast.show('Checklist name is required.', 'error'); if (btn) btn.classList.remove('loading'); return; } const res = await API.post('checklists/template_save', { ...data, id: _builderEditId || 0, is_active: data.is_active ? 1 : 0, items }); if (btn) btn.classList.remove('loading'); if (res.success) { Toast.show(_builderEditId ? 'Template updated.' : 'Template created.', 'success'); Modal.close(); _clJobTypes = null; await _loadClCaches(); renderChecklistsTab('templates'); } else Toast.show(res.message, 'error'); } async function deleteTemplate(id, name) { if (!await Modal.confirm(`Delete checklist "${name}"?`, 'Delete Template', true)) return; const res = await API.post('checklists/template_delete', { id }); if (res.success) { Toast.show(res.message, 'success'); renderChecklistsTab('templates'); } else Toast.show(res.message, 'error'); } // ── Utility ─────────────────────────────────────────────────── function needsImageForCard(card, ans) { const req = card.dataset.requiresImage; if (req === 'always') return true; if (req === 'on_yes' && ans.answer_bool === 1) return true; if (req === 'on_no' && ans.answer_bool === 0) return true; return false; } function clH(s) { return String(s ?? '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } // ── CSS for this module ─────────────────────────────────────── (function injectChecklistStyles() { if (document.getElementById('cl-styles')) return; const style = document.createElement('style'); style.id = 'cl-styles'; style.textContent = ` .cl-question-card { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:1rem 1.25rem; transition:outline .15s; } .cl-q-header { display:flex; align-items:flex-start; gap:.75rem; } .cl-q-num { width:26px; min-width:26px; height:26px; background:var(--primary); color:#fff; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:.75rem; font-weight:700; flex-shrink:0; } .cl-q-text { font-weight:500; line-height:1.5; } .cl-yn-btn { border:2px solid var(--border); background:transparent; padding:.4rem 1.25rem; border-radius:var(--radius); font-weight:600; cursor:pointer; transition:all .15s; } .cl-yn-btn:hover { border-color:var(--primary); } .cl-yn-btn.active-yes { background:#dcfce7; border-color:#16a34a; color:#15803d; } .cl-yn-btn.active-no { background:#fee2e2; border-color:#dc2626; color:#b91c1c; } .cl-img-grid { display:flex; flex-wrap:wrap; gap:.5rem; align-items:flex-start; margin-top:.5rem; } .cl-img-thumb { position:relative; flex-shrink:0; } .cl-img-thumb-del { position:absolute; top:-6px; right:-6px; width:18px; height:18px; border-radius:50%; background:var(--danger); color:#fff; border:none; cursor:pointer; font-size:.6rem; display:flex; align-items:center; justify-content:center; line-height:1; padding:0; } .cl-img-thumb-del:hover { background:#b91c1c; } .cl-img-add-btn { width:80px; height:80px; display:inline-flex; flex-direction:column; align-items:center; justify-content:center; gap:2px; border:2px dashed var(--border); border-radius:6px; cursor:pointer; font-size:.85rem; color:var(--text-muted); background:var(--bg); transition:border-color .15s; flex-shrink:0; } .cl-img-add-btn:hover { border-color:var(--primary); color:var(--primary); } .cl-submission-area { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:1.25rem; } .cl-readonly-text { background:var(--bg); border:1px solid var(--border); border-radius:var(--radius); padding:.5rem .75rem; font-size:.9rem; white-space:pre-wrap; } .cl-note-readonly { background:rgba(0,0,0,.03); border-radius:var(--radius); padding:.4rem .75rem; font-size:.85rem; color:var(--text-muted); } .cl-builder-grid { display:grid; grid-template-columns:1fr 1fr; gap:1.5rem; } @media(max-width:720px) { .cl-builder-grid { grid-template-columns:1fr; } } .cl-builder-section-title { font-family:var(--font-display); font-size:.8rem; text-transform:uppercase; letter-spacing:.06em; color:var(--text-muted); margin-bottom:.75rem; } .cl-builder-item { display:flex; align-items:flex-start; gap:.5rem; background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:.6rem; } .cl-bi-drag { cursor:grab; color:var(--text-muted); padding:.15rem .1rem; font-size:1.1rem; margin-top:.15rem; } .cl-bi-body { flex:1; display:flex; flex-direction:column; gap:.4rem; } .cl-bi-opts { display:flex; align-items:center; gap:.4rem; flex-wrap:wrap; } .cl-bi-opts select { flex:1; min-width:90px; } .cl-bi-del { background:none; border:none; cursor:pointer; color:var(--text-muted); padding:.2rem; border-radius:4px; display:flex; align-items:center; } .cl-bi-del:hover { color:var(--danger); background:var(--danger-light,#fee2e2); } .badge-lg { font-size:.9rem; padding:.35em .9em; } `; document.head.appendChild(style); })();