import { api } from '../../lib/api.mjs'; const PHASES = ['all', 'before', 'during', 'after']; class WoPhotoPanel extends HTMLElement { #woId = null; #photos = []; #phase = 'all'; #loading = true; #lightboxIdx = -1; static get observedAttributes() { return ['wo-id']; } connectedCallback() { if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); if (this.#woId) this.#load(); } attributeChangedCallback(_, __, val) { this.#woId = val ? +val : null; if (this.shadowRoot && this.#woId) this.#load(); } async #load() { this.#loading = true; this.#render(); try { this.#photos = await api.get(`/work-orders/${this.#woId}/attachments`) || []; } catch { this.#photos = []; } this.#loading = false; this.#render(); } async #upload(files, phase, caption) { const uploads = Array.from(files).map(async file => { const form = new FormData(); form.append('file', file); form.append('phase', phase); form.append('caption', caption || ''); if (navigator.geolocation) { await new Promise(res => navigator.geolocation.getCurrentPosition( pos => { form.append('lat', pos.coords.latitude); form.append('lng', pos.coords.longitude); res(); }, () => res(), { timeout: 3000 } )); } return api.upload(`/work-orders/${this.#woId}/attachments`, form); }); try { await Promise.all(uploads); window.dispatchEvent(new CustomEvent('wo:toast', { detail: { message: `${files.length} photo(s) uploaded`, type: 'success' } })); await this.#load(); } catch (err) { window.dispatchEvent(new CustomEvent('wo:toast', { detail: { message: err.message, type: 'error' } })); } } async #deletePhoto(id) { try { await api.delete(`/work-orders/${this.#woId}/attachments/${id}`); await this.#load(); } catch (err) { window.dispatchEvent(new CustomEvent('wo:toast', { detail: { message: err.message, type: 'error' } })); } } #filteredPhotos() { if (this.#phase === 'all') return this.#photos; return this.#photos.filter(p => p.phase === this.#phase); } #render() { const s = this.shadowRoot; if (this.#loading) { s.innerHTML = ``; return; } const filtered = this.#filteredPhotos(); const phaseCounts = { all: this.#photos.length, before: 0, during: 0, after: 0 }; this.#photos.forEach(p => { if (phaseCounts[p.phase] !== undefined) phaseCounts[p.phase]++; }); s.innerHTML = `
${PHASES.map(ph => ` `).join('')}
${filtered.length === 0 ? `

${this.#phase === 'all' ? 'No photos yet' : `No ${this.#phase} photos yet`}

` : `
${filtered.map((p, i) => `
${this.#esc(p.caption)}
${p.caption ? `
${this.#esc(p.caption)}
` : ''}
${p.phase ? `
${p.phase}
` : ''}
`).join('')}
`}
Add Photos
Click to select photos or drag & drop here
`; if (window.lucide) lucide.createIcons({ root: s }); this.#bindEvents(filtered); } #bindEvents(filtered) { const s = this.shadowRoot; s.querySelectorAll('.phase-tab').forEach(tab => tab.addEventListener('click', () => { this.#phase = tab.dataset.phase; this.#render(); })); s.querySelectorAll('.photo-tile').forEach(tile => tile.addEventListener('click', e => { if (e.target.closest('.del-btn')) return; this.#openLightbox(+tile.dataset.idx, filtered); })); s.querySelectorAll('.del-btn').forEach(btn => btn.addEventListener('click', e => { e.stopPropagation(); this.#deletePhoto(+btn.dataset.id); })); const dialog = s.querySelector('#upload-dialog'); const fileInput = s.querySelector('#file-input'); const dropZone = s.querySelector('#drop-zone'); const preview = s.querySelector('#preview-list'); const uploadBtn = s.querySelector('#dlg-upload'); s.querySelector('#upload-btn').addEventListener('click', () => { s.querySelector('#phase-select').value = this.#phase !== 'all' ? this.#phase : ''; s.querySelector('#caption-input').value = ''; preview.innerHTML = ''; uploadBtn.disabled = true; dialog.showModal(); }); s.querySelector('#dlg-close').addEventListener('click', () => dialog.close()); s.querySelector('#dlg-cancel').addEventListener('click', () => dialog.close()); dropZone.addEventListener('click', () => fileInput.click()); dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over')); dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); this.#setFiles(e.dataTransfer.files, preview, uploadBtn); }); fileInput.addEventListener('change', () => this.#setFiles(fileInput.files, preview, uploadBtn)); s.querySelector('#dlg-upload').addEventListener('click', () => { const files = fileInput.files; const phase = s.querySelector('#phase-select').value; const caption = s.querySelector('#caption-input').value; dialog.close(); this.#upload(files, phase, caption); }); } #setFiles(files, preview, btn) { preview.innerHTML = Array.from(files).map(f => `
${this.#esc(f.name)}
` ).join(''); if (window.lucide) lucide.createIcons({ root: preview }); btn.disabled = files.length === 0; } #openLightbox(idx, photos) { const s = this.shadowRoot; const show = (i) => { const existing = s.querySelector('.lightbox'); if (existing) existing.remove(); if (i < 0 || i >= photos.length) return; const p = photos[i]; const lb = document.createElement('div'); lb.className = 'lightbox'; lb.innerHTML = ` ${this.#esc(p.caption)} ${photos.length > 1 ? ` ` : ''} ${p.caption ? `` : ''}`; s.appendChild(lb); if (window.lucide) lucide.createIcons({ root: lb }); lb.querySelector('.lightbox-close').addEventListener('click', () => lb.remove()); lb.addEventListener('click', e => { if (e.target === lb) lb.remove(); }); lb.querySelector('.lightbox-prev')?.addEventListener('click', e => { e.stopPropagation(); show(i - 1); }); lb.querySelector('.lightbox-next')?.addEventListener('click', e => { e.stopPropagation(); show(i + 1); }); }; show(idx); } #esc(s) { return (s || '').replace(/&/g,'&').replace(//g,'>'); } } customElements.define('wo-photo-panel', WoPhotoPanel);