import $ from 'jquery'; import axios from 'axios'; const CELL_SIZE = 80; const CELL_GAP = 16; const MIN_ZOOM = 0.05; const MAX_ZOOM = 3; const ZOOM_FACTOR = 0.1; const SCROLL_SENSITIVITY = 0.6; const DAY_IN_MS = 24 * 60 * 60 * 1000; const ACTIONS_REQUIRING_DATE = new Set(['watering', 'pruning', 'fertilisation', 'pesticide', 'harvest']); function showToast(message, type = 'error') { let container = document.querySelector('[data-role="toast-container"]'); if (!container) { container = document.createElement('div'); container.dataset.role = 'toast-container'; container.style.position = 'fixed'; container.style.top = '1rem'; container.style.right = '1rem'; container.style.zIndex = '9999'; container.style.display = 'flex'; container.style.flexDirection = 'column'; container.style.gap = '0.5rem'; document.body.appendChild(container); } const toast = document.createElement('div'); toast.textContent = message; toast.style.padding = '0.6rem 0.9rem'; toast.style.borderRadius = '6px'; toast.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)'; toast.style.color = '#0f172a'; toast.style.fontSize = '0.9rem'; toast.style.backgroundColor = type === 'success' ? '#bbf7d0' : '#fee2e2'; container.appendChild(toast); setTimeout(() => { toast.remove(); if (!container.children.length) { container.remove(); } }, 3000); } function parseData(elementId, fallback) { const el = document.getElementById(elementId); if (!el) { return fallback; } try { return JSON.parse(el.textContent ?? 'null') ?? fallback; } catch (error) { console.error(`Failed to parse payload for ${elementId}`, error); return fallback; } } function computeContentDimensions(rows) { const maxX = Math.max(0, ...rows.map((row) => row.location.x)); const maxY = Math.max(0, ...rows.map((row) => row.location.y)); const width = Math.max(1, maxX + 1) * (CELL_SIZE + CELL_GAP); const height = Math.max(1, maxY + 1) * (CELL_SIZE + CELL_GAP); return { width, height }; } function applyStatusClass(element, status) { const targetStatus = status ?? 'unknown'; element.classList.forEach((name) => { if (name.startsWith('status-')) { element.classList.remove(name); } }); element.classList.add(`status-${targetStatus}`); } function createCell(row) { const cell = document.createElement('div'); cell.classList.add('map-cell'); cell.dataset.rowId = String(row.id); cell.style.left = `${row.location.x * (CELL_SIZE + CELL_GAP)}px`; cell.style.top = `${row.location.y * (CELL_SIZE + CELL_GAP)}px`; cell.style.width = `${CELL_SIZE}px`; cell.style.height = `${CELL_SIZE}px`; applyStatusClass(cell, row.status); const countBadge = document.createElement('div'); countBadge.classList.add('map-cell-count'); countBadge.dataset.role = 'vine-count'; countBadge.textContent = String(row.vine_count ?? 0); const varietyLabel = document.createElement('div'); varietyLabel.classList.add('map-cell-variety'); varietyLabel.dataset.role = 'variety-label'; varietyLabel.textContent = row.variety?.label ?? 'Unassigned'; const badgeRow = document.createElement('div'); badgeRow.classList.add('map-cell-badge-row'); badgeRow.appendChild(countBadge); const warning = document.createElement('div'); warning.classList.add('map-cell-warning'); warning.dataset.role = 'restriction-warning'; warning.style.display = 'none'; const skull = document.createElement('span'); skull.dataset.role = 'skull-icon'; skull.textContent = '☠'; const warningText = document.createElement('span'); warningText.dataset.role = 'restriction-text'; warning.appendChild(skull); warning.appendChild(warningText); badgeRow.appendChild(warning); cell.appendChild(badgeRow); cell.appendChild(varietyLabel); return cell; } function parseISODate(value) { if (!value || typeof value !== 'string') { return null; } const parts = value.split('-').map((segment) => Number(segment)); if (parts.length !== 3 || parts.some((number) => Number.isNaN(number))) { return null; } const [year, month, day] = parts; return new Date(Date.UTC(year, month - 1, day)); } function withinDays(laterDate, earlierDate, windowDays) { if (!laterDate || !earlierDate) { return false; } const diffDays = (laterDate.getTime() - earlierDate.getTime()) / DAY_IN_MS; return diffDays >= 0 && diffDays <= windowDays; } function daysBetween(laterDate, earlierDate) { if (!laterDate || !earlierDate) { return null; } return Math.round((laterDate.getTime() - earlierDate.getTime()) / DAY_IN_MS); } function updateNextState($nextButton, selectedAction, selectedRows, actionDate) { const requiresDate = selectedAction ? ACTIONS_REQUIRING_DATE.has(selectedAction) : false; const hasDate = !requiresDate || Boolean(actionDate); const enable = Boolean(selectedAction) && selectedRows.size > 0 && hasDate; $nextButton.prop('disabled', !enable); } function activateActionButton($sidebar, actionKey) { $sidebar.find('[data-action]').removeClass('active'); if (actionKey) { $sidebar.find(`[data-action="${actionKey}"]`).addClass('active'); } } export default function initMapIndex() { const $root = $('[data-page="vineyard-map-index"]'); if (!$root.length) { return; } const rows = parseData('vineyard-map-rows', []); const routes = parseData('vineyard-map-routes', {}); const selectedRows = new Set(); const rowElements = new Map(); const rowDataById = new Map(); let selectedAction = null; let actionPromptTimeout = null; let selectedActionDate = ''; const $sidebar = $root.find('[data-role="sidebar"]'); const $mapPanel = $root.find('[data-role="map-panel"]'); const $nextButton = $root.find('[data-control="next-action"]'); const $clearButton = $root.find('[data-control="clear-selection"]'); const mapViewport = $root.find('[data-role="map-container"]')[0]; const actionDetailsSection = $root.find('[data-role="action-details"]')[0] ?? null; const actionDateInput = $root.find('[data-control="action-date"]')[0] ?? null; const actionTimelineRoot = actionDetailsSection?.querySelector('[data-role="action-timeline"]') ?? null; const actionTimelineGroups = actionTimelineRoot ? { pesticide: { root: actionTimelineRoot.querySelector('[data-action="pesticide"]') ?? null, completed: actionTimelineRoot.querySelector('[data-timeline="pesticide-completed"]') ?? null, planned: actionTimelineRoot.querySelector('[data-timeline="pesticide-planned"]') ?? null, }, harvest: { root: actionTimelineRoot.querySelector('[data-action="harvest"]') ?? null, completed: actionTimelineRoot.querySelector('[data-timeline="harvest-completed"]') ?? null, planned: actionTimelineRoot.querySelector('[data-timeline="harvest-planned"]') ?? null, }, } : null; if (!mapViewport) { return; } const $helpBtn1 = $sidebar.find('button[aria-label="Help"]'); const $tooltip1 = $sidebar.find('.action-tooltip'); if ($helpBtn1.length && $tooltip1.length) { $helpBtn1.on('mouseenter focus', function() { $tooltip1.show(); }); $helpBtn1.on('mouseleave blur', function() { $tooltip1.hide(); }); } const $helpBtn2 = $('button[aria-label="Help2"]'); const $tooltip2 = $('.action-tooltip2'); if ($helpBtn2.length && $tooltip2.length) { console.log($helpBtn2, $tooltip2, $helpBtn1); $helpBtn2.on('mouseenter focus', function() { $tooltip2.show(); }); $helpBtn2.on('mouseleave blur', function() { $tooltip2.hide(); }); } const actionRequiresDate = (actionKey) => { if (!actionKey) { return false; } return ACTIONS_REQUIRING_DATE.has(actionKey); }; const toggleActionDetails = () => { if (!actionDetailsSection || !actionDateInput) { return; } const requiresDate = actionRequiresDate(selectedAction); if (requiresDate) { actionDetailsSection.classList.add('visible'); actionDetailsSection.setAttribute('aria-hidden', 'false'); actionDateInput.disabled = false; actionDateInput.value = selectedActionDate || ''; } else { actionDetailsSection.classList.remove('visible'); actionDetailsSection.setAttribute('aria-hidden', 'true'); actionDateInput.disabled = true; actionDateInput.value = ''; selectedActionDate = ''; } if (actionTimelineRoot) { const timelineVisible = selectedAction === 'harvest' || selectedAction === 'pesticide'; actionTimelineRoot.classList.toggle('visible', timelineVisible); actionTimelineRoot.setAttribute('aria-hidden', timelineVisible ? 'false' : 'true'); } }; toggleActionDetails(); if (actionDateInput) { const today = new Date(); const year = today.getFullYear(); const month = String(today.getMonth() + 1).padStart(2, '0'); const day = String(today.getDate()).padStart(2, '0'); actionDateInput.min = `${year}-${month}-${day}`; } const updateTimelineDisplay = () => { if (!actionTimelineGroups) { return; } const aggregated = { pesticide: { completed: new Set(), planned: new Set(), }, harvest: { completed: new Set(), planned: new Set(), }, }; selectedRows.forEach((rowId) => { const row = rowDataById.get(rowId); if (!row) { return; } ['pesticide', 'harvest'].forEach((category) => { const timeline = row.timeline?.[category]; if (!timeline) { return; } (timeline.completed ?? []).forEach((date) => { if (date) { aggregated[category].completed.add(date); } }); (timeline.planned ?? []).forEach((date) => { if (date) { aggregated[category].planned.add(date); } }); }); }); const timelineVisible = actionTimelineRoot?.classList.contains('visible'); ['pesticide', 'harvest'].forEach((category) => { const group = actionTimelineGroups[category]; if (!group) { return; } const completed = Array.from(aggregated[category].completed).sort(); const planned = Array.from(aggregated[category].planned).sort(); if (group.completed) { group.completed.textContent = completed.length ? completed.join(', ') : 'None'; } if (group.planned) { group.planned.textContent = planned.length ? planned.join(', ') : 'None'; } if (group.root) { const isRelevant = actionRequiresDate(selectedAction) && (selectedAction === category || selectedAction === 'harvest' || selectedAction === 'pesticide'); group.root.classList.toggle('active', Boolean(timelineVisible && isRelevant)); } }); }; const requireActionBeforeSelection = () => { if (selectedAction) { return true; } mapViewport.classList.add('requires-action'); window.clearTimeout(actionPromptTimeout); actionPromptTimeout = window.setTimeout(() => { mapViewport.classList.remove('requires-action'); }, 420); return false; }; const { width: contentWidth, height: contentHeight } = computeContentDimensions(rows); mapViewport.innerHTML = ''; const mapContent = document.createElement('div'); mapContent.classList.add('vineyard-map-grid'); mapContent.style.width = `${contentWidth}px`; mapContent.style.height = `${contentHeight}px`; mapViewport.appendChild(mapContent); const selectionLayer = document.createElement('div'); selectionLayer.classList.add('vineyard-map-selection-layer'); mapViewport.appendChild(selectionLayer); rows.forEach((row) => { const cell = createCell(row); mapContent.appendChild(cell); const rowId = Number(row.id); rowElements.set(rowId, cell); const timeline = row.timeline ?? {}; rowDataById.set(rowId, { ...row, timeline: { pesticide: { completed: Array.isArray(timeline?.pesticide?.completed) ? timeline.pesticide.completed : [], planned: Array.isArray(timeline?.pesticide?.planned) ? timeline.pesticide.planned : [], }, harvest: { completed: Array.isArray(timeline?.harvest?.completed) ? timeline.harvest.completed : [], planned: Array.isArray(timeline?.harvest?.planned) ? timeline.harvest.planned : [], }, }, }); }); const viewportState = { scale: 1, translateX: 0, translateY: 0, }; let eligibleRows = null; const isRowEligibleForAction = (actionKey, row) => { if (!row) { return false; } const hasVarietyAssigned = Boolean(row.variety); if (['discard-plants', 'harvest', 'watering', 'pruning', 'fertilisation', 'pesticide'].includes(actionKey)) { return hasVarietyAssigned; } if (actionKey === 'add-plants') { return !hasVarietyAssigned; } return true; }; const isRowEligibleOnDate = (actionKey, row, targetDate) => { if (!targetDate) { return true; } const timeline = row.timeline ?? {}; const pesticideDates = [ ...(timeline.pesticide?.completed ?? []), ...(timeline.pesticide?.planned ?? []), ] .map((value) => parseISODate(value)) .filter(Boolean); const harvestDates = [ ...(timeline.harvest?.completed ?? []), ...(timeline.harvest?.planned ?? []), ] .map((value) => parseISODate(value)) .filter(Boolean); if (actionKey === 'harvest') { return !pesticideDates.some((pesticideDate) => withinDays(targetDate, pesticideDate, 14)); } if (actionKey === 'pesticide') { return !harvestDates.some((harvestDate) => withinDays(harvestDate, targetDate, 14)); } return true; }; const computeEligibleRows = (actionKey, actionDate) => { if (!actionKey) { return null; } const eligible = new Set(); const requiresDate = actionRequiresDate(actionKey); const targetDate = requiresDate && actionDate ? parseISODate(actionDate) : null; rowDataById.forEach((row, rowId) => { if (!isRowEligibleForAction(actionKey, row)) { return; } if (requiresDate && targetDate && !isRowEligibleOnDate(actionKey, row, targetDate)) { return; } eligible.add(rowId); }); return eligible; }; const applyRowEligibility = () => { let selectionChanged = false; rowElements.forEach((element, rowId) => { const eligible = !eligibleRows || eligibleRows.has(rowId); element.classList.toggle('disabled', !eligible); const warning = element.querySelector('[data-role="restriction-warning"]'); const warningText = element.querySelector('[data-role="restriction-text"]'); if (warning && warningText) { warning.style.display = 'none'; warning.classList.remove('map-cell-warning--pesticide'); const row = rowDataById.get(rowId); const timeline = row?.timeline ?? {}; const targetDate = selectedActionDate ? parseISODate(selectedActionDate) : null; if (!eligible && selectedAction === 'harvest' && targetDate) { const pesticideDates = [ ...(timeline.pesticide?.completed ?? []), ...(timeline.pesticide?.planned ?? []), ] .map((value) => parseISODate(value)) .filter(Boolean); if (pesticideDates.length > 0) { let maxRemaining = 0; pesticideDates.forEach((pesticideDate) => { const diff = 14 - daysBetween(targetDate, pesticideDate); if (Number.isFinite(diff) && diff > maxRemaining) { maxRemaining = diff; } }); if (maxRemaining > 0) { warningText.textContent = `${maxRemaining}d`; const icon = element.querySelector('[data-role="skull-icon"]'); if (icon) { icon.textContent = '☠'; } warning.style.display = 'flex'; } } } if (!eligible && selectedAction === 'pesticide' && targetDate) { const harvestDates = [ ...(timeline.harvest?.completed ?? []), ...(timeline.harvest?.planned ?? []), ] .map((value) => parseISODate(value)) .filter(Boolean); if (harvestDates.length > 0) { let minUntilHarvest = Infinity; harvestDates.forEach((harvestDate) => { const diff = daysBetween(harvestDate, targetDate); if (Number.isFinite(diff) && diff >= 0 && diff < minUntilHarvest) { minUntilHarvest = diff; } }); if (Number.isFinite(minUntilHarvest) && minUntilHarvest !== Infinity) { warningText.textContent = `${minUntilHarvest}d`; const icon = element.querySelector('[data-role="skull-icon"]'); if (icon) { icon.textContent = '🍇'; } warning.classList.add('map-cell-warning--pesticide'); warning.style.display = 'flex'; } } } } if (!eligible && selectedRows.has(rowId)) { selectedRows.delete(rowId); selectionChanged = true; } }); return selectionChanged; }; eligibleRows = computeEligibleRows(selectedAction, selectedActionDate); applyRowEligibility(); applyRowEligibility(); const clampTransform = () => { const viewportWidth = mapViewport.clientWidth; const viewportHeight = mapViewport.clientHeight; const scaledWidth = contentWidth * viewportState.scale; const scaledHeight = contentHeight * viewportState.scale; if (scaledWidth <= viewportWidth) { viewportState.translateX = (viewportWidth - scaledWidth) / 2; } else { const minX = viewportWidth - scaledWidth; viewportState.translateX = Math.min(0, Math.max(minX, viewportState.translateX)); } if (scaledHeight <= viewportHeight) { viewportState.translateY = (viewportHeight - scaledHeight) / 2; } else { const minY = viewportHeight - scaledHeight; viewportState.translateY = Math.min(0, Math.max(minY, viewportState.translateY)); } }; const applyTransform = () => { clampTransform(); mapContent.style.transform = `translate(${viewportState.translateX}px, ${viewportState.translateY}px) scale(${viewportState.scale})`; }; const fitContentToViewport = () => { if (!contentWidth || !contentHeight) { viewportState.scale = 1; viewportState.translateX = 0; viewportState.translateY = 0; applyTransform(); return; } const viewportWidth = mapViewport.clientWidth || 1; const viewportHeight = mapViewport.clientHeight || 1; const scaleX = viewportWidth / contentWidth; const scaleY = viewportHeight / contentHeight; const fittedScale = Math.min(1, Math.min(scaleX, scaleY)); viewportState.scale = Math.min(MAX_ZOOM, Math.max(fittedScale, MIN_ZOOM)); viewportState.translateX = (viewportWidth - contentWidth * viewportState.scale) / 2; viewportState.translateY = (viewportHeight - contentHeight * viewportState.scale) / 2; applyTransform(); }; const updateSelectionStyles = () => { rowElements.forEach((element, rowId) => { if (selectedRows.has(rowId)) { element.classList.add('selected'); } else { element.classList.remove('selected'); } }); }; const syncState = () => { updateSelectionStyles(); updateNextState($nextButton, selectedAction, selectedRows, selectedActionDate); updateTimelineDisplay(); }; if (actionDateInput) { actionDateInput.addEventListener('input', (event) => { selectedActionDate = event.target.value; if (selectedActionDate) { const today = new Date(); today.setHours(0, 0, 0, 0); const chosen = parseISODate(selectedActionDate); if (chosen && chosen < today) { showToast('Planned date cannot be in the past.', 'error'); } } eligibleRows = computeEligibleRows(selectedAction, selectedActionDate); applyRowEligibility(); syncState(); }); } const clearSelection = () => { if (selectedRows.size === 0) { return; } selectedRows.clear(); syncState(); }; const selectRow = (rowId, multi) => { if (eligibleRows && !eligibleRows.has(rowId)) { return; } if (!multi) { selectedRows.clear(); selectedRows.add(rowId); } else if (selectedRows.has(rowId)) { selectedRows.delete(rowId); } else { selectedRows.add(rowId); } syncState(); }; const areaSelect = (rowIds, merge) => { const candidates = eligibleRows ? rowIds.filter((rowId) => eligibleRows.has(rowId)) : rowIds; if (candidates.length === 0) { if (!merge && rowIds.length === 0) { clearSelection(); } return; } if (!merge) { selectedRows.clear(); } candidates.forEach((rowId) => selectedRows.add(rowId)); syncState(); }; const handleWheel = (event) => { if (event.ctrlKey) { event.preventDefault(); const previousScale = viewportState.scale; const direction = -event.deltaY; const scaleDelta = direction > 0 ? 1 + ZOOM_FACTOR : 1 - ZOOM_FACTOR; viewportState.scale = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, viewportState.scale * scaleDelta)); const zoomRatio = viewportState.scale / previousScale; const rect = mapViewport.getBoundingClientRect(); const offsetX = event.clientX - rect.left; const offsetY = event.clientY - rect.top; viewportState.translateX = offsetX - (offsetX - viewportState.translateX) * zoomRatio; viewportState.translateY = offsetY - (offsetY - viewportState.translateY) * zoomRatio; applyTransform(); return; } event.preventDefault(); if (event.shiftKey) { viewportState.translateX -= event.deltaY * SCROLL_SENSITIVITY; viewportState.translateX -= event.deltaX * SCROLL_SENSITIVITY; } else { viewportState.translateY -= event.deltaY * SCROLL_SENSITIVITY; viewportState.translateX -= event.deltaX * SCROLL_SENSITIVITY; } applyTransform(); }; let isSelectingArea = false; let selectionStartPoint = null; let selectionBox = null; let selectionMerge = false; let selectionMoved = false; let skipClickClear = false; const beginSelection = (event) => { if (event.button !== 0) { return; } const cell = event.target.closest('[data-row-id]'); if (cell && !cell.classList.contains('disabled')) { return; } if (!requireActionBeforeSelection()) { return; } isSelectingArea = true; selectionMerge = event.shiftKey; selectionMoved = false; selectionStartPoint = { x: event.clientX, y: event.clientY }; selectionBox = document.createElement('div'); selectionBox.classList.add('selection-box'); selectionLayer.appendChild(selectionBox); event.preventDefault(); }; const updateSelectionBox = (event) => { if (!isSelectingArea || !selectionBox || !selectionStartPoint) { return; } const rect = mapViewport.getBoundingClientRect(); const startX = selectionStartPoint.x - rect.left; const startY = selectionStartPoint.y - rect.top; const currentX = event.clientX - rect.left; const currentY = event.clientY - rect.top; const left = Math.min(startX, currentX); const top = Math.min(startY, currentY); const width = Math.abs(currentX - startX); const height = Math.abs(currentY - startY); if (!selectionMoved && (width > 3 || height > 3)) { selectionMoved = true; } Object.assign(selectionBox.style, { left: `${left}px`, top: `${top}px`, width: `${width}px`, height: `${height}px`, }); }; const finishSelection = (moved) => { if (!isSelectingArea) { return; } const boxRect = selectionBox?.getBoundingClientRect() ?? null; selectionBox?.remove(); selectionBox = null; selectionStartPoint = null; isSelectingArea = false; if (!moved) { skipClickClear = true; if (!selectionMerge) { clearSelection(); } selectionMerge = false; return; } if (!boxRect) { selectionMerge = false; return; } const intersectingRows = []; rowElements.forEach((element, rowId) => { const cellRect = element.getBoundingClientRect(); const fullyContained = cellRect.left >= boxRect.left && cellRect.right <= boxRect.right && cellRect.top >= boxRect.top && cellRect.bottom <= boxRect.bottom; if (fullyContained) { intersectingRows.push(rowId); } }); skipClickClear = true; areaSelect(intersectingRows, selectionMerge); selectionMerge = false; }; mapContent.addEventListener('click', (event) => { const cell = event.target.closest('[data-row-id]'); if (!cell) { return; } if (!requireActionBeforeSelection()) { return; } if (cell.classList.contains('disabled')) { skipClickClear = true; event.stopPropagation(); return; } skipClickClear = true; event.stopPropagation(); const rowId = Number(cell.dataset.rowId); if (!Number.isFinite(rowId) || rowId <= 0) { return; } selectRow(rowId, event.shiftKey); }); mapViewport.addEventListener('click', (event) => { if (skipClickClear) { skipClickClear = false; return; } if (event.target.closest('[data-row-id]')) { return; } if (!event.shiftKey) { clearSelection(); } }); mapViewport.addEventListener('mousedown', beginSelection); window.addEventListener('mousemove', updateSelectionBox); window.addEventListener('mouseup', () => { const wasSelecting = isSelectingArea; const moved = selectionMoved; finishSelection(moved); if (wasSelecting && moved) { skipClickClear = true; } selectionMoved = false; }); mapViewport.addEventListener('wheel', handleWheel, { passive: false }); window.addEventListener('resize', applyTransform); fitContentToViewport(); $sidebar.on('click', '[data-action]', function () { const $button = $(this); const action = $button.data('action'); if (selectedAction === action) { selectedAction = null; activateActionButton($sidebar, null); } else { selectedAction = action; activateActionButton($sidebar, action); } if (selectedAction) { mapViewport.classList.remove('requires-action'); } toggleActionDetails(); eligibleRows = computeEligibleRows(selectedAction, selectedActionDate); applyRowEligibility(); syncState(); }); $clearButton.on('click', () => { clearSelection(); }); $nextButton.on('click', async (event) => { event.preventDefault(); if (!selectedAction || selectedRows.size === 0) { return; } const rowList = Array.from(selectedRows.values()); if (selectedAction === 'discard-plants') { const endpoint = routes['discard-plants']; if (!endpoint) { return; } try { await axios.post(endpoint, { rows: rowList }); rowList.forEach((rowId) => { const element = rowElements.get(rowId); if (!element) { return; } element.classList.remove('selected'); // Backend now marks rows inactive but keeps vine counts intact. applyStatusClass(element, 'inactive'); const data = rowDataById.get(rowId) ?? {}; rowDataById.set(rowId, { ...data, status: 'inactive', variety: null, variety_variation_id: null, }); const varietyLabel = element.querySelector('[data-role="variety-label"]'); if (varietyLabel) { varietyLabel.textContent = 'Unassigned'; } }); eligibleRows = computeEligibleRows(selectedAction, selectedActionDate); applyRowEligibility(); selectedRows.clear(); syncState(); } catch (error) { console.error('Failed to discard plants', error); alert('Discard action failed. Please try again.'); } return; } const endpoint = routes[selectedAction]; if (!endpoint) { return; } if (actionRequiresDate(selectedAction) && !selectedActionDate) { return; } const url = new URL(endpoint, window.location.origin); url.searchParams.set('rows', rowList.join(',')); if (actionRequiresDate(selectedAction)) { url.searchParams.set('date', selectedActionDate); } window.location.href = url.toString(); }); syncState(); }