import { api } from '../../lib/api.mjs'; const SECTIONS = [ { type: 'person', label: 'People', icon: 'users', endpoint: 'people' }, { type: 'vehicle', label: 'Vehicles', icon: 'truck', endpoint: 'vehicles' }, { type: 'equipment', label: 'Equipment', icon: 'wrench', endpoint: 'equipment' }, { type: 'material', label: 'Materials', icon: 'package', endpoint: 'materials' }, ]; class WoResourcePanel extends HTMLElement { #woId = null; #resources = []; #registry = {}; #loading = true; #pickerType = null; 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 { const [resources, people, vehicles, equipment, materials] = await Promise.all([ api.get(`/work-orders/${this.#woId}/resources`), api.get('/registry/people'), api.get('/registry/vehicles'), api.get('/registry/equipment'), api.get('/registry/materials'), ]); this.#resources = resources || []; this.#registry = { person: people || [], vehicle: vehicles || [], equipment: equipment || [], material: materials || [] }; } catch { this.#resources = []; this.#registry = {}; } this.#loading = false; this.#render(); } async #assign(type, resourceId, quantity) { try { await api.post(`/work-orders/${this.#woId}/resources`, { resource_type: type, resource_id: +resourceId, quantity: quantity ? +quantity : null, }); await this.#load(); } catch (err) { window.dispatchEvent(new CustomEvent('wo:toast', { detail: { message: err.message, type: 'error' } })); } } async #remove(assignId) { try { await api.delete(`/work-orders/${this.#woId}/resources/${assignId}`); await this.#load(); } catch (err) { window.dispatchEvent(new CustomEvent('wo:toast', { detail: { message: err.message, type: 'error' } })); } } #resourceName(item, type) { if (type === 'vehicle') return `${item.unit_number}${item.description ? ' – ' + item.description : ''}`; return item.name || item.unit_number || '—'; } #sectionHTML(section) { const assigned = this.#resources.filter(r => r.resource_type === section.type); const count = assigned.length; return `
${count ? `${count}` : ''}
${count === 0 ? `
No ${section.label.toLowerCase()} assigned
` : assigned.map(r => `
${this.#esc(r.name)} ${r.resource_type === 'material' && r.quantity != null ? `${r.quantity} ${this.#unitFor(r.resource_id)}` : ''}
`).join('')}
`; } #unitFor(resourceId) { const item = (this.#registry['material'] || []).find(m => m.id === resourceId); return item ? item.unit : ''; } #render() { const s = this.shadowRoot; if (this.#loading) { s.innerHTML = ``; return; } s.innerHTML = `
${SECTIONS.map(sec => this.#sectionHTML(sec)).join('')}
Add Resource
`; if (window.lucide) lucide.createIcons({ root: s }); this.#bindEvents(); } #bindEvents() { const s = this.shadowRoot; s.querySelectorAll('[data-rid]').forEach(btn => btn.addEventListener('click', () => this.#remove(+btn.dataset.rid))); s.querySelectorAll('.add-resource-btn').forEach(btn => btn.addEventListener('click', () => this.#openPicker(btn.dataset.type))); const dialog = s.querySelector('#picker-dialog'); const closeBtn = s.querySelector('#picker-close'); const cancelBtn = s.querySelector('#picker-cancel'); const addBtn = s.querySelector('#picker-add'); const search = s.querySelector('#picker-search'); closeBtn.addEventListener('click', () => dialog.close()); cancelBtn.addEventListener('click', () => dialog.close()); dialog.addEventListener('click', e => { if (e.target === dialog) dialog.close(); }); search.addEventListener('input', () => this.#filterPicker(search.value)); addBtn.addEventListener('click', () => { const selected = s.querySelector('.picker-item.selected'); if (!selected) return; const qty = s.querySelector('#qty-input')?.value || null; this.#assign(this.#pickerType, selected.dataset.id, qty); dialog.close(); }); } #openPicker(type) { const s = this.shadowRoot; const section = SECTIONS.find(sec => sec.type === type); this.#pickerType = type; s.querySelector('#picker-title').textContent = `Add ${section.label}`; s.querySelector('#picker-search').value = ''; s.querySelector('#qty-row').style.display = type === 'material' ? 'flex' : 'none'; s.querySelector('#picker-add').disabled = true; this.#filterPicker(''); s.querySelector('#picker-dialog').showModal(); s.querySelector('#picker-search').focus(); } #filterPicker(query) { const s = this.shadowRoot; const type = this.#pickerType; const list = this.#registry[type] || []; const assigned = this.#resources.filter(r => r.resource_type === type).map(r => r.resource_id); const q = query.toLowerCase(); const available = list.filter(item => { if (assigned.includes(item.id)) return false; const name = (item.name || item.unit_number || '').toLowerCase(); const sub = (item.role || item.vehicle_type || item.category || item.part_number || '').toLowerCase(); return !q || name.includes(q) || sub.includes(q); }); const container = s.querySelector('#picker-list'); if (available.length === 0) { container.innerHTML = '
No items available
'; return; } container.innerHTML = available.map(item => { const name = type === 'vehicle' ? `${item.unit_number}${item.description ? ' – ' + item.description : ''}` : item.name; const sub = item.role || item.vehicle_type || item.category || (item.part_number ? `#${item.part_number}` : '') || ''; return `
${this.#esc(name)}
${sub ? `
${this.#esc(sub)}
` : ''}
`; }).join(''); container.querySelectorAll('.picker-item').forEach(item => { item.addEventListener('click', () => { container.querySelectorAll('.picker-item').forEach(i => i.classList.remove('selected')); item.classList.add('selected'); this.shadowRoot.querySelector('#picker-add').disabled = false; }); }); } #esc(s) { return (s || '').replace(/&/g,'&').replace(//g,'>'); } } customElements.define('wo-resource-panel', WoResourcePanel);