// ─── Savuki Drilling — Rich Select Component ───────────────────────────── // Usage: // richSelect('my-container-id', { // placeholder: 'Search jobcards…', // items: [ { value, label, sub, badge, badge2 }, … ], // onSelect: (value, item) => {} // }) // // Returns { getValue(), setValue(v), destroy() } export function richSelect(containerId, { placeholder = 'Search…', items = [], onSelect = () => { } } = {}) { const container = document.getElementById(containerId); if (!container) return null; let selected = null; let open = false; let filtered = items; // ── Build DOM ───────────────────────────────────────────────────────── container.innerHTML = `
${placeholder}
`; const wrap = container.querySelector('.rs-wrap'); const trigger = container.querySelector('.rs-trigger'); const dropdown = container.querySelector('.rs-dropdown'); const search = container.querySelector('.rs-search'); const list = container.querySelector('.rs-list'); const label = container.querySelector('.rs-selected-label'); // ── Render list ─────────────────────────────────────────────────────── function renderList(itemsToShow) { if (!itemsToShow.length) { list.innerHTML = `
No results found
`; return; } list.innerHTML = itemsToShow.map(item => `
${item.label} ${item.badge ? `${item.badge}` : ''} ${item.badge2 ? `${item.badge2}` : ''}
${item.sub ? `
${item.sub}
` : ''}
`).join(''); list.querySelectorAll('.rs-item').forEach(el => { el.addEventListener('mousedown', e => { e.preventDefault(); const val = el.dataset.value; const item = items.find(i => String(i.value) === val); select(item); }); }); } function select(item) { selected = item; label.textContent = item ? item.label : placeholder; label.style.color = item ? '' : 'var(--text-muted)'; close(); onSelect(item?.value ?? '', item); renderList(filtered); } function openDropdown() { open = true; dropdown.style.display = ''; search.value = ''; filtered = items; renderList(filtered); search.focus(); // Position above if not enough space below const rect = wrap.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; dropdown.style.top = spaceBelow < 280 ? 'auto' : '100%'; dropdown.style.bottom = spaceBelow < 280 ? '100%' : 'auto'; } function close() { open = false; dropdown.style.display = 'none'; } // ── Events ──────────────────────────────────────────────────────────── trigger.addEventListener('click', () => open ? close() : openDropdown()); search.addEventListener('input', () => { const q = search.value.toLowerCase().trim(); filtered = q ? items.filter(i => (i.label + ' ' + (i.sub || '') + ' ' + (i.badge || '')).toLowerCase().includes(q) ) : items; renderList(filtered); }); // Close on outside click document.addEventListener('mousedown', function handler(e) { if (!container.contains(e.target)) { close(); } }); // ── Public API ──────────────────────────────────────────────────────── return { getValue: () => selected?.value ?? '', setValue: (v) => { const item = items.find(i => String(i.value) === String(v)); if (item) select(item); }, destroy: () => { container.innerHTML = ''; }, }; } // ── Jobcard items builder ───────────────────────────────────────────────── export function jobcardItems(rows) { return rows.map(j => ({ value: j.jc_no, label: `#${j.jc_no} ${j.client_name || j.address || '—'}`, sub: [j.address, j.contact_number].filter(Boolean).join(' · '), badge: j.jc_current_status || '', _raw: j, })); } // ── Team items builder ──────────────────────────────────────────────────── export function teamItems(rows) { return rows.map(t => ({ value: t.record_id, label: t.name, sub: t.team_members ? `👥 ${t.team_members}` : '', badge: t.assigned_assets || '', })); } function esc(v) { return (v || '').toString().replace(/"/g, '"').replace(/