// ─── 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 = `
`;
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(/