/** * GoingsOn - Day Planning Paint Module * Timeline painting to create events, painted event modal, time block types. */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; const escAttr = GoingsOn.utils.escapeAttr; // ============ Utility ============ /** * Convert a 15-minute slot index (0-95) to a Date object on the current day plan date. * @param {number} slotIndex - Slot index (0 = 00:00, 95 = 23:45) * @returns {Date} Date object for the slot */ function slotToTime(slotIndex) { const hour = Math.floor(slotIndex / 4); const minute = (slotIndex % 4) * 15; const date = new Date(GoingsOn.state.dayPlanDate); date.setHours(hour, minute, 0, 0); return date; } const toLocalISOString = GoingsOn.utils.toLocalISOString; // ============ Painting to Create Events ============ /** * Begin painting a time range on mousedown. * @param {MouseEvent} event * @param {number} slotIndex - Starting slot index * @param {string} slotTime - ISO timestamp of the slot */ function onPaintStart(event, slotIndex, slotTime) { if (event.button !== 0) return; if (event.target.closest('.timeline-item')) return; // Mobile UI doesn't expose drag-paint — tap-to-add is the touch path. if (GoingsOn.viewport?.isMobile()) return; event.preventDefault(); const container = document.getElementById('timeline-container'); container.classList.add('is-painting'); const preview = document.createElement('div'); preview.className = 'timeline-paint-preview'; document.getElementById('timeline-items').appendChild(preview); GoingsOn.state.set('paintingState', { startSlot: slotIndex, startTime: slotTime, endSlot: slotIndex, endTime: slotTime, preview }); updatePaintPreview(); document.addEventListener('mouseup', onPaintEnd); } /** * Extend the paint selection on mousemove. * @param {MouseEvent} event * @param {number} slotIndex - Current slot index * @param {string} slotTime - ISO timestamp of the slot */ function onPaintMove(event, slotIndex, slotTime) { if (!GoingsOn.state.paintingState) return; GoingsOn.state.paintingState.endSlot = slotIndex; GoingsOn.state.paintingState.endTime = slotTime; updatePaintPreview(); } function onPaintEnd() { if (!GoingsOn.state.paintingState) return; document.removeEventListener('mouseup', onPaintEnd); document.getElementById('timeline-container').classList.remove('is-painting'); GoingsOn.state.paintingState.preview.remove(); const startSlot = Math.min(GoingsOn.state.paintingState.startSlot, GoingsOn.state.paintingState.endSlot); const endSlot = Math.max(GoingsOn.state.paintingState.startSlot, GoingsOn.state.paintingState.endSlot); const startTime = slotToTime(startSlot); const endTime = slotToTime(endSlot + 1); GoingsOn.state.set('paintingState', null); openPaintedEventModal(startTime, endTime); } function updatePaintPreview() { if (!GoingsOn.state.paintingState) return; const slotHeight = GoingsOn.dayPlanRender.getSlotHeight(); const startSlot = Math.min(GoingsOn.state.paintingState.startSlot, GoingsOn.state.paintingState.endSlot); const endSlot = Math.max(GoingsOn.state.paintingState.startSlot, GoingsOn.state.paintingState.endSlot); GoingsOn.state.paintingState.preview.style.top = `${startSlot * slotHeight}px`; GoingsOn.state.paintingState.preview.style.height = `${(endSlot - startSlot + 1) * slotHeight}px`; } /** * Open the create modal for a painted time range (event, block, or task link). * @param {Date} startTime - Start of the painted range * @param {Date} endTime - End of the painted range */ function openPaintedEventModal(startTime, endTime) { const startISO = toLocalISOString(startTime); const endISO = toLocalISOString(endTime); const unscheduledTasks = GoingsOn.state.dayPlanData?.unscheduledTasks || []; const taskOptions = unscheduledTasks.map(t => `` ).join(''); const content = `
`; GoingsOn.ui.openModal('Create Event', content); // Show initial time preview updatePaintTimePreview(); // Default to "Link to Task" when there are unscheduled tasks if (unscheduledTasks.length > 0) { const modeSelect = document.querySelector('#painted-event-form select[name="item_mode"]'); if (modeSelect) { modeSelect.value = 'task'; GoingsOn.dayPlan.togglePaintMode(modeSelect); } } } /** * Toggle visibility of form fields based on the selected paint mode. * @param {HTMLSelectElement} select - The mode selector element */ function togglePaintMode(select) { const mode = select.value; const taskFields = document.getElementById('paint-task-fields'); const blockFields = document.getElementById('paint-block-fields'); const eventFields = document.getElementById('standalone-event-fields'); const descGroup = document.getElementById('paint-description-group'); const locationGroup = document.getElementById('paint-location-group'); taskFields.classList.toggle('hidden', mode !== 'task'); blockFields.classList.toggle('hidden', mode !== 'block'); eventFields.classList.toggle('hidden', mode === 'task'); // Hide description and location for blocks if (descGroup) descGroup.classList.toggle('hidden', mode === 'block'); if (locationGroup) locationGroup.classList.toggle('hidden', mode === 'block'); } /** * Update the human-readable time preview above the datetime inputs. */ function updatePaintTimePreview() { const form = document.getElementById('painted-event-form'); const preview = document.getElementById('paint-time-preview'); if (!form || !preview) return; const startVal = form.start_time?.value; const endVal = form.end_time?.value; if (!startVal || !endVal) { preview.textContent = ''; return; } const start = new Date(startVal); const end = new Date(endVal); const timeOpts = { hour: 'numeric', minute: '2-digit' }; const duration = Math.round((end - start) / 60000); let durationLabel = ''; if (duration > 0) { const hours = Math.floor(duration / 60); const mins = duration % 60; durationLabel = hours > 0 ? (mins > 0 ? ` (${hours}h ${mins}m)` : ` (${hours}h)`) : ` (${mins}m)`; } preview.textContent = `${start.toLocaleTimeString('en-US', timeOpts)} \u2013 ${end.toLocaleTimeString('en-US', timeOpts)}${durationLabel}`; } async function submitPaintedEvent() { const form = document.getElementById('painted-event-form'); const mode = form.item_mode.value; const startTime = new Date(form.start_time.value).toISOString(); const endTime = new Date(form.end_time.value).toISOString(); const duration = Math.round((new Date(form.end_time.value) - new Date(form.start_time.value)) / 60000); try { const timeOpts = { hour: 'numeric', minute: '2-digit' }; const startLabel = new Date(form.start_time.value).toLocaleTimeString('en-US', timeOpts); const endLabel = new Date(form.end_time.value).toLocaleTimeString('en-US', timeOpts); const timeRange = `${startLabel} \u2013 ${endLabel}`; if (mode === 'task') { const linkedTaskId = form.linked_task_id.value; if (!linkedTaskId) { GoingsOn.ui.showToast('Please select a task', 'error'); return; } await GoingsOn.api.dayPlanning.scheduleTask(linkedTaskId, { startTime, duration }); GoingsOn.ui.showToast(`Task scheduled for ${timeRange}`, 'success'); } else if (mode === 'block') { const blockType = form.block_type.value; const title = form.title.value.trim() || GoingsOn.dayPlanRender.BLOCK_TYPE_LABELS[blockType] || blockType; await GoingsOn.api.events.create({ title, description: '', startTime, endTime, location: null, projectId: null, blockType, }); GoingsOn.ui.showToast(`Time block created: ${timeRange}`, 'success'); } else { if (!form.title.value.trim()) { GoingsOn.ui.showToast('Title is required for standalone events', 'error'); return; } await GoingsOn.api.events.create({ title: form.title.value, description: form.description.value || '', startTime, endTime, location: form.location.value || null, projectId: null, }); GoingsOn.ui.showToast(`Event created: ${timeRange}`, 'success'); } GoingsOn.ui.closeModal(); await GoingsOn.dayPlan.load(); } catch (err) { GoingsOn.ui.showToast(GoingsOn.utils.getErrorMessage(err, 'Failed to create'), 'error'); } } // ============ Populate GoingsOn.dayPlanPaint Namespace ============ GoingsOn.dayPlanPaint = { slotToTime, onPaintStart, onPaintMove, onPaintEnd, openPaintedEventModal, togglePaintMode, updatePaintTimePreview, submitPaintedEvent, }; })();