Projects/3BIT/winter-semester/IIS/xnecasr00/resources/js/vineyard/map-index.js
2026-04-14 19:28:46 +02:00

1000 lines
32 KiB
JavaScript

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();
}