import { api } from '../../lib/api.mjs'; const PRIORITIES = ['low', 'normal', 'high', 'urgent']; const STEP_TYPES = [ { value: 'work_step', label: 'Work Step', icon: 'check-square', color: '#0A7EA4', hint: '' }, { value: 'photo', label: 'Photo', icon: 'camera', color: '#8B5CF6', hint: 'Requires a photo to complete' }, { value: 'inspection', label: 'Inspect', icon: 'clipboard-check', color: '#E07B39', hint: 'Pass / Fail / N/A' }, { value: 'note', label: 'Note', icon: 'file-text', color: '#64748B', hint: 'Free-text entry' }, ]; class ProfileForm extends HTMLElement { #onSave = null; connectedCallback() { if (!this.shadowRoot) { this.attachShadow({ mode: 'open' }); this.shadowRoot.innerHTML = ``; } } open(profile, onSave) { this.#onSave = onSave; const isEdit = !!profile; const d = this.shadowRoot.querySelector('dialog'); d.innerHTML = `
${isEdit ? 'Edit Profile' : 'New Profile'}
${isEdit ? `
Active
` : ''}
Default Steps
${(profile?.steps || []).length === 0 ? '
No steps yet — add steps below
' : (profile?.steps || []).map((s, i) => this.#stepRowHTML(s, i)).join('')}
${STEP_TYPES.map(t => ` `).join('')}
`; if (window.lucide) lucide.createIcons({ root: d }); const profileId = profile?.id || null; let pendingSteps = (profile?.steps || []).map(s => ({ ...s })); let selectedType = 'work_step'; // ── Step list rendering ─────────────────────────────────────────────── const renderSteps = () => { const list = d.querySelector('#step-list'); list.innerHTML = pendingSteps.length === 0 ? '
No steps yet — add steps below
' : pendingSteps.map((s, i) => this.#stepRowHTML(s, i)).join(''); if (window.lucide) lucide.createIcons({ root: list }); list.querySelectorAll('.step-title-input').forEach((inp, i) => { inp.addEventListener('input', () => { pendingSteps[i].title = inp.value; }); inp.addEventListener('change', () => { pendingSteps[i].title = inp.value; }); }); list.querySelectorAll('.step-req-check').forEach((cb, i) => { cb.addEventListener('change', () => { pendingSteps[i].required = cb.checked; }); }); list.querySelectorAll('.step-del').forEach((btn, i) => { btn.addEventListener('click', () => { pendingSteps.splice(i, 1); renderSteps(); }); }); }; renderSteps(); // ── Type picker ─────────────────────────────────────────────────────── const cfgDiv = d.querySelector('#cfg-fields'); const renderCfgFields = (type) => { if (type === 'work_step') { cfgDiv.style.display = 'none'; cfgDiv.innerHTML = ''; return; } cfgDiv.style.display = 'block'; if (type === 'photo') { cfgDiv.innerHTML = `
`; } else if (type === 'inspection') { cfgDiv.innerHTML = `
`; } else if (type === 'note') { cfgDiv.innerHTML = `
`; } }; const getCfgJSON = (type) => { if (type === 'photo') { const phase = cfgDiv.querySelector('#cfg-phase')?.value || 'during'; const caption = cfgDiv.querySelector('#cfg-caption')?.value.trim() || ''; return JSON.stringify({ phase, caption_prompt: caption }); } if (type === 'inspection') { const criteria = cfgDiv.querySelector('#cfg-criteria')?.value.trim() || ''; return criteria ? JSON.stringify({ criteria }) : ''; } if (type === 'note') { const prompt = cfgDiv.querySelector('#cfg-prompt')?.value.trim() || ''; return prompt ? JSON.stringify({ prompt }) : ''; } return ''; }; d.querySelectorAll('.type-btn').forEach(btn => { btn.addEventListener('click', () => { selectedType = btn.dataset.type; d.querySelectorAll('.type-btn').forEach(b => b.classList.toggle('active', b === btn)); renderCfgFields(selectedType); }); }); // ── Add step ────────────────────────────────────────────────────────── d.querySelector('#add-step-btn').addEventListener('click', () => { const inp = d.querySelector('#new-step-title'); const title = inp.value.trim(); if (!title) { inp.focus(); return; } pendingSteps.push({ id: null, step_order: pendingSteps.length + 1, title, description: '', required: true, step_type: selectedType, type_config: getCfgJSON(selectedType), }); inp.value = ''; // Reset type picker to work_step after adding selectedType = 'work_step'; d.querySelectorAll('.type-btn').forEach(b => b.classList.toggle('active', b.dataset.type === 'work_step')); renderCfgFields('work_step'); renderSteps(); }); d.querySelector('#new-step-title').addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); d.querySelector('#add-step-btn').click(); } }); // ── Dialog controls ─────────────────────────────────────────────────── d.querySelector('#dlg-close').addEventListener('click', () => d.close()); d.querySelector('#dlg-cancel').addEventListener('click', () => d.close()); d.querySelector('#dlg-save').addEventListener('click', async () => { const name = d.querySelector('#p-name').value.trim(); const errEl = d.querySelector('#form-error'); if (!name) { errEl.textContent = 'Name is required'; d.querySelector('#p-name').focus(); return; } errEl.textContent = ''; const saveBtn = d.querySelector('#dlg-save'); saveBtn.disabled = true; saveBtn.textContent = 'Saving…'; const payload = { name, description: d.querySelector('#p-desc').value.trim(), category: d.querySelector('#p-cat').value.trim(), default_priority: d.querySelector('#p-pri').value, default_duration_hours: d.querySelector('#p-dur').value ? +d.querySelector('#p-dur').value : null, default_instructions: d.querySelector('#p-instr').value.trim(), active: isEdit ? d.querySelector('#p-active').checked : true, }; try { let savedProfile; if (isEdit) { savedProfile = await api.put(`/profiles/${profile.id}`, payload); } else { savedProfile = await api.post('/profiles', payload); } const savedId = savedProfile.id; const origIds = new Set((profile?.steps || []).map(s => s.id)); // Delete removed steps for (const orig of (profile?.steps || [])) { if (!pendingSteps.find(s => s.id === orig.id)) { await api.delete(`/profiles/${savedId}/steps/${orig.id}`); } } // Update existing / create new for (let i = 0; i < pendingSteps.length; i++) { const s = pendingSteps[i]; const stepPayload = { title: s.title, description: s.description || '', required: s.required !== false, step_order: i + 1, step_type: s.step_type || 'work_step', type_config: s.type_config || '', }; if (s.id && origIds.has(s.id)) { await api.put(`/profiles/${savedId}/steps/${s.id}`, stepPayload); } else { await api.post(`/profiles/${savedId}/steps`, stepPayload); } } d.close(); window.dispatchEvent(new CustomEvent('wo:toast', { detail: { message: isEdit ? 'Profile updated' : 'Profile created', type: 'success' } })); this.#onSave?.(); } catch (err) { errEl.textContent = err.message; saveBtn.disabled = false; saveBtn.textContent = isEdit ? 'Save Changes' : 'Create Profile'; } }); d.showModal(); d.querySelector('#p-name').focus(); } #stepRowHTML(s, i) { const type = s.step_type || 'work_step'; const tdef = { work_step: { color: '#0A7EA4', label: 'Step' }, photo: { color: '#8B5CF6', label: 'Photo' }, inspection: { color: '#E07B39', label: 'Inspect' }, note: { color: '#64748B', label: 'Note' } }; const td = tdef[type] || tdef.work_step; const cfg = this.#parseCfg(s.type_config); const cfgSummary = type === 'photo' ? (cfg.phase ? cfg.phase + (cfg.caption_prompt ? ' · ' + cfg.caption_prompt : '') : '') : type === 'inspection' ? (cfg.criteria || '') : type === 'note' ? (cfg.prompt || '') : ''; return `
${i + 1} ${td.label} ${cfgSummary ? `${this.#esc(cfgSummary)}` : ''}
`; } #parseCfg(str) { try { return JSON.parse(str || '{}'); } catch { return {}; } } #esc(s) { return (s || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } } customElements.define('profile-form', ProfileForm);