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 === 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('')}
`;
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);