// Cost & splitting subsection for the day editor. Self-contained so dayEditor // stays small. read() returns the cost fields for the entry payload: // { price: null } — no cost // { price, paid_by, split_mode, participants } — cost set // { error } — validation message import { el } from '../dom.js'; import { SPLIT_MODES } from '../format.js'; export function createCostForm({ members, currency }) { const priceInput = el('input', { class: 'input', type: 'number', min: '0', step: '0.01', placeholder: '0.00' }); const payerSelect = el( 'select', { class: 'input' }, el('option', { value: '' }, '— unassigned —'), ...members.map((m) => el('option', { value: String(m.id) }, m.display_name)), ); const modeSelect = el( 'select', { class: 'input' }, ...SPLIT_MODES.map((m) => el('option', { value: m.value }, m.label)), ); const participantChecks = members.map((m) => el('input', { type: 'checkbox', class: 'part-check', value: String(m.id), checked: true }), ); const participantsBox = el( 'div', { class: 'participants' }, ...members.map((m, i) => el('label', { class: 'part-item' }, participantChecks[i], el('span', {}, m.display_name)), ), ); const costDetail = el( 'div', { class: 'cost-detail' }, el('div', { class: 'form-row' }, el('label', { class: 'field' }, el('span', { class: 'field-label' }, 'Paid by'), payerSelect), el('label', { class: 'field field-grow' }, el('span', { class: 'field-label' }, 'Split'), modeSelect)), el('div', { class: 'field' }, el('span', { class: 'field-label' }, 'Participants'), participantsBox), ); const node = el( 'div', { class: 'cost-section' }, el('div', { class: 'form-row' }, el('label', { class: 'field field-price' }, el('span', { class: 'field-label' }, `Price (${currency})`), priceInput)), costDetail, ); // Dim the payer/split/participants controls until a price is entered. function syncVisibility() { costDetail.classList.toggle('disabled', priceInput.value.trim() === ''); } priceInput.addEventListener('input', syncVisibility); function read() { const priceRaw = priceInput.value.trim(); if (priceRaw === '') return { price: null }; const price = Number(priceRaw); if (!Number.isFinite(price) || price < 0) return { error: 'Price must be a number ≥ 0.' }; const split_mode = modeSelect.value; const paid_by = payerSelect.value ? Number(payerSelect.value) : null; if (split_mode === 'payer' && paid_by == null) { return { error: "Choose who paid for a payer's own expense." }; } const checked = participantChecks.filter((c) => c.checked).map((c) => Number(c.value)); if (checked.length === 0) { return { error: 'Select at least one participant (or clear the price to drop the cost).' }; } // [] means "all trip members"; only send an explicit list for a subset. const participants = checked.length === members.length ? [] : checked; return { price, paid_by, split_mode, participants }; } function prefill(entry) { priceInput.value = entry.price != null ? String(entry.price) : ''; payerSelect.value = entry.paid_by != null ? String(entry.paid_by) : ''; modeSelect.value = entry.split_mode || 'equal'; const parts = Array.isArray(entry.participants) ? entry.participants : []; for (const cb of participantChecks) { cb.checked = parts.length === 0 || parts.includes(Number(cb.value)); } syncVisibility(); } syncVisibility(); return { node, read, prefill }; }