| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
| let loadedSchedule = null; |
|
|
| |
| let currentProblemId = null; |
|
|
| |
| let autoRefreshIntervalId = null; |
|
|
| |
| let lastScore = null; |
|
|
| |
| let distances = new Map(); |
|
|
| |
| let userRequestedSolving = false; |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| $(document).ready(function() { |
| |
| replaceQuickstartSolverForgeAutoHeaderFooter(); |
|
|
| |
| initWarehouseCanvas(); |
|
|
| |
| loadDemoData(); |
|
|
| |
| $("#solveButton").click(solve); |
| $("#stopSolvingButton").click(stopSolving); |
| $("#analyzeButton").click(analyze); |
| $("#generateButton").click(generateNewData); |
|
|
| |
| $("#ordersCountSlider").on("input", function() { |
| $("#ordersCountValue").text($(this).val()); |
| }); |
| $("#trolleysCountSlider").on("input", function() { |
| $("#trolleysCountValue").text($(this).val()); |
| }); |
| $("#bucketsCountSlider").on("input", function() { |
| $("#bucketsCountValue").text($(this).val()); |
| }); |
|
|
| |
| window.addEventListener('resize', () => { |
| initWarehouseCanvas(); |
| if (loadedSchedule) { |
| renderWarehouse(loadedSchedule); |
| } |
| }); |
| }); |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| function loadDemoData() { |
| fetch('/demo-data/DEFAULT') |
| .then(r => r.json()) |
| .then(solution => { |
| loadedSchedule = solution; |
| currentProblemId = null; |
| updateUI(solution, false); |
| }) |
| .catch(error => { |
| showError("Failed to load demo data", error); |
| }); |
| } |
|
|
| |
| |
| |
| |
| function generateNewData() { |
| |
| const config = { |
| ordersCount: parseInt($("#ordersCountSlider").val()), |
| trolleysCount: parseInt($("#trolleysCountSlider").val()), |
| bucketCount: parseInt($("#bucketsCountSlider").val()) |
| }; |
|
|
| |
| const btn = $("#generateButton"); |
| btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Generating...'); |
|
|
| fetch('/demo-data/generate', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(config) |
| }) |
| .then(r => { |
| if (!r.ok) { |
| return r.text().then(text => { |
| throw new Error(`Server error ${r.status}: ${text}`); |
| }); |
| } |
| return r.json(); |
| }) |
| .then(solution => { |
| loadedSchedule = solution; |
| currentProblemId = null; |
| distances.clear(); |
| updateUI(solution, false); |
| $("#settingsPanel").collapse('hide'); |
| showSuccess(`Generated ${config.ordersCount} orders with ${config.trolleysCount} trolleys`); |
| }) |
| .catch(error => { |
| console.error("Generate error:", error); |
| showError("Failed to generate data: " + error.message, error); |
| }) |
| .finally(() => { |
| btn.prop('disabled', false).html('<i class="fas fa-sync-alt"></i> Generate New'); |
| }); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function solve() { |
| lastScore = null; |
| userRequestedSolving = true; |
|
|
| |
| setSolving(true); |
|
|
| |
| fetch('/schedules', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(loadedSchedule) |
| }) |
| .then(r => r.text()) |
| .then(problemId => { |
| |
| currentProblemId = problemId.replace(/"/g, ''); |
|
|
| |
| ISO.isSolving = true; |
|
|
| |
| autoRefreshIntervalId = setInterval(refreshSchedule, 250); |
|
|
| |
| refreshSchedule(); |
| }) |
| .catch(error => { |
| showError("Failed to start solving", error); |
| setSolving(false); |
| stopWarehouseAnimation(); |
| }); |
| } |
|
|
| |
| |
| |
| |
| function stopSolving() { |
| if (!currentProblemId) return; |
| userRequestedSolving = false; |
|
|
| |
| if (autoRefreshIntervalId) { |
| clearInterval(autoRefreshIntervalId); |
| autoRefreshIntervalId = null; |
| } |
|
|
| |
| setSolving(false); |
| stopWarehouseAnimation(); |
|
|
| |
| fetch(`/schedules/${currentProblemId}`, { method: 'DELETE' }) |
| .then(r => r.ok ? r.json() : Promise.reject(`HTTP ${r.status}`)) |
| .then(solution => { |
| loadedSchedule = solution; |
| updateUI(solution, false); |
| }) |
| .catch(error => showError("Failed to stop solving", error)); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function refreshSchedule() { |
| if (!currentProblemId) return; |
| if (!userRequestedSolving) return; |
|
|
| |
| Promise.all([ |
| fetch(`/schedules/${currentProblemId}`).then(r => r.json()), |
| fetch(`/schedules/${currentProblemId}/status`).then(r => r.json()) |
| ]) |
| .then(([solution, status]) => { |
| |
| if (!userRequestedSolving) return; |
|
|
| |
| |
| if (typeof ISO !== 'undefined') { |
| ISO.currentSolution = solution; |
| } |
|
|
| |
| distances = new Map(Object.entries(status.distances || {})); |
|
|
| |
| const newScoreStr = `${status.score.hardScore}hard/${status.score.softScore}soft`; |
| if (lastScore && newScoreStr !== lastScore) { |
| flashScoreImprovement(); |
| } |
| lastScore = newScoreStr; |
|
|
| |
| loadedSchedule = solution; |
| const isSolving = status.solverStatus !== 'NOT_SOLVING' && status.solverStatus != null; |
| updateUI(solution, isSolving); |
|
|
| |
| if (userRequestedSolving) { |
| if (ISO.trolleyAnimations.size === 0) { |
| startWarehouseAnimation(solution); |
| } |
| updateWarehouseAnimation(solution); |
| } |
|
|
| |
| const solverSaysNotSolving = status.solverStatus === 'NOT_SOLVING'; |
| const solverActuallyFinished = solverSaysNotSolving && solution.score !== null; |
| const shouldStop = !userRequestedSolving || solverActuallyFinished; |
|
|
| if (shouldStop) { |
| if (autoRefreshIntervalId) { |
| clearInterval(autoRefreshIntervalId); |
| autoRefreshIntervalId = null; |
| } |
| userRequestedSolving = false; |
| setSolving(false); |
| stopWarehouseAnimation(); |
| } |
| }) |
| .catch(error => { |
| console.error("Refresh error:", error); |
| }); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| function updateUI(solution, solving) { |
| updateScore(solution); |
| updateStats(solution); |
| updateLegend(solution, distances); |
| updateTrolleyCards(solution); |
| renderWarehouse(solution); |
| setSolving(solving && solution.solverStatus !== 'NOT_SOLVING'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function updateScore(solution) { |
| const score = solution.score; |
| if (!score) { |
| $("#score").text("?"); |
| } else if (typeof score === 'string') { |
| $("#score").text(score); |
| } else { |
| $("#score").text(`${score.hardScore}hard/${score.softScore}soft`); |
| } |
| } |
|
|
| |
| |
| |
| |
| function updateStats(solution) { |
| const orderIds = new Set(); |
| let totalItems = 0; |
| let activeTrolleys = 0; |
| let totalDistance = 0; |
|
|
| |
| const stepLookup = new Map(); |
| for (const step of solution.trolleySteps || []) { |
| stepLookup.set(step.id, step); |
| } |
|
|
| |
| for (const trolley of solution.trolleys || []) { |
| |
| const steps = (trolley.steps || []).map(ref => |
| typeof ref === 'string' ? stepLookup.get(ref) : ref |
| ).filter(s => s); |
|
|
| if (steps.length > 0) { |
| activeTrolleys++; |
| totalItems += steps.length; |
|
|
| |
| for (const step of steps) { |
| if (step.orderItem) { |
| orderIds.add(step.orderItem.orderId); |
| } |
| } |
| } |
|
|
| |
| const dist = distances.get(trolley.id) || 0; |
| totalDistance += dist; |
| } |
|
|
| |
| animateValue("#totalOrders", orderIds.size); |
| animateValue("#totalItems", totalItems); |
| animateValue("#activeTrolleys", activeTrolleys); |
| animateValue("#totalDistance", Math.round(totalDistance / 100)); |
| } |
|
|
| |
| |
| |
| function animateValue(selector, newValue) { |
| const el = $(selector); |
| const oldValue = parseInt(el.text()) || 0; |
| if (oldValue !== newValue) { |
| el.text(newValue); |
| el.addClass('value-changed'); |
| setTimeout(() => el.removeClass('value-changed'), 500); |
| } |
| } |
|
|
| |
| |
| |
| |
| function updateTrolleyCards(solution) { |
| const container = $("#trolleyCardsContainer"); |
|
|
| |
| const stepLookup = new Map(); |
| for (const step of solution.trolleySteps || []) { |
| stepLookup.set(step.id, step); |
| } |
|
|
| const trolleys = solution.trolleys || []; |
|
|
| |
| if (container.children().length !== trolleys.length) { |
| container.empty(); |
| for (const trolley of trolleys) { |
| const color = getTrolleyColor(trolley.id); |
| const card = $(` |
| <div class="trolley-card" data-trolley-id="${trolley.id}"> |
| <div class="trolley-card-header"> |
| <div class="trolley-color-badge" style="background: ${color}"></div> |
| <div class="trolley-card-info"> |
| <div class="trolley-card-title">Trolley ${trolley.id}</div> |
| <div class="trolley-card-stats"></div> |
| </div> |
| <div class="trolley-capacity-bar"> |
| <div class="trolley-capacity-fill"></div> |
| </div> |
| </div> |
| <div class="trolley-card-body"></div> |
| </div> |
| `); |
| container.append(card); |
| } |
| } |
|
|
| |
| for (const trolley of trolleys) { |
| const card = container.find(`[data-trolley-id="${trolley.id}"]`); |
| if (!card.length) continue; |
|
|
| const steps = (trolley.steps || []).map(ref => |
| typeof ref === 'string' ? stepLookup.get(ref) : ref |
| ).filter(s => s); |
|
|
| const itemCount = steps.length; |
|
|
| |
| let totalVolume = 0; |
| const bucketCapacity = 50000; |
| const bucketCount = trolley.bucketCount || 6; |
| const maxCapacity = bucketCapacity * bucketCount; |
| for (const step of steps) { |
| if (step.orderItem?.product?.volume) { |
| totalVolume += step.orderItem.product.volume; |
| } |
| } |
| const capacityPercent = Math.min(100, Math.round((totalVolume / maxCapacity) * 100)); |
| const capacityClass = capacityPercent > 90 ? 'high' : capacityPercent > 70 ? 'medium' : 'low'; |
|
|
| |
| card.find('.trolley-card-stats').text(`${itemCount} items`); |
|
|
| |
| const fill = card.find('.trolley-capacity-fill'); |
| fill.css('width', `${capacityPercent}%`); |
| fill.removeClass('low medium high').addClass(capacityClass); |
|
|
| |
| const body = card.find('.trolley-card-body'); |
| if (itemCount > 0) { |
| body.html(` |
| <div class="trolley-items-list"> |
| ${steps.map((step, i) => ` |
| <div class="trolley-item"> |
| <span class="trolley-item-number">${i + 1}</span> |
| ${step.orderItem?.product?.name?.substring(0, 15) || 'Item'} |
| </div> |
| `).join('')} |
| </div> |
| `); |
| } else { |
| body.html('<div class="trolley-empty">No items assigned</div>'); |
| } |
| } |
| } |
|
|
| |
| |
| |
| function setSolving(solving) { |
| if (solving) { |
| $("#solveButton").hide(); |
| $("#stopSolvingButton").show(); |
| $("#solvingIndicator").show(); |
| $("#generateButton").prop('disabled', true); |
| } else { |
| $("#solveButton").show(); |
| $("#stopSolvingButton").hide(); |
| $("#solvingIndicator").hide(); |
| $("#generateButton").prop('disabled', false); |
| } |
| } |
|
|
| |
| |
| |
| function flashScoreImprovement() { |
| const display = $("#scoreDisplay"); |
| display.addClass('improved'); |
| setTimeout(() => display.removeClass('improved'), 500); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| function analyze() { |
| if (!currentProblemId) { |
| showError("No active solution to analyze"); |
| return; |
| } |
|
|
| |
| const btn = $("#analyzeButton"); |
| btn.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i>'); |
|
|
| fetch(`/schedules/${currentProblemId}/score-analysis`) |
| .then(r => r.json()) |
| .then(analysis => { |
| showScoreAnalysis(analysis); |
| }) |
| .catch(error => { |
| showError("Failed to load score analysis", error); |
| }) |
| .finally(() => { |
| btn.prop('disabled', false).html('<i class="fas fa-chart-bar"></i>'); |
| }); |
| } |
|
|
| |
| |
| |
| function showScoreAnalysis(analysis) { |
| const content = $("#scoreAnalysisModalContent"); |
| content.empty(); |
|
|
| if (!analysis || !analysis.constraints) { |
| content.html('<p>No constraint data available.</p>'); |
| } else { |
| for (const constraint of analysis.constraints) { |
| const score = constraint.score || '0'; |
| const isHard = score.includes('hard'); |
|
|
| const group = $(` |
| <div class="constraint-group"> |
| <div class="constraint-header"> |
| <span class="constraint-name">${constraint.name}</span> |
| <span class="constraint-score ${isHard ? 'hard' : 'soft'}">${score}</span> |
| </div> |
| </div> |
| `); |
| content.append(group); |
| } |
| } |
|
|
| |
| const modalEl = document.getElementById('scoreAnalysisModal'); |
| bootstrap.Modal.getOrCreateInstance(modalEl).show(); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| function showError(message, error) { |
| console.error(message, error); |
| const alert = $(` |
| <div class="alert alert-danger alert-dismissible fade show"> |
| <i class="fas fa-exclamation-circle me-2"></i> |
| <strong>Error:</strong> ${message} |
| <button type="button" class="btn-close" data-bs-dismiss="alert"></button> |
| </div> |
| `); |
| $("#notificationPanel").append(alert); |
| setTimeout(() => alert.alert('close'), 5000); |
| } |
|
|
| |
| |
| |
| function showSuccess(message) { |
| const alert = $(` |
| <div class="alert alert-success alert-dismissible fade show"> |
| <i class="fas fa-check-circle me-2"></i>${message} |
| <button type="button" class="btn-close" data-bs-dismiss="alert"></button> |
| </div> |
| `); |
| $("#notificationPanel").append(alert); |
| setTimeout(() => alert.alert('close'), 3000); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| function replaceQuickstartSolverForgeAutoHeaderFooter() { |
| const header = $("header#solverforge-auto-header"); |
| if (header.length) { |
| header.css("background-color", "#ffffff"); |
| header.append($(` |
| <div class="container-fluid"> |
| <nav class="navbar sticky-top navbar-expand-lg shadow-sm mb-3" style="background-color: #ffffff;"> |
| <a class="navbar-brand" href="https://www.solverforge.org"> |
| <img src="/webjars/solverforge/img/solverforge-horizontal.svg" alt="SolverForge logo" width="400"> |
| </a> |
| </nav> |
| </div> |
| `)); |
| } |
|
|
| const footer = $("footer#solverforge-auto-footer"); |
| if (footer.length) { |
| footer.append($(` |
| <footer class="bg-black text-white-50"> |
| <div class="container"> |
| <div class="hstack gap-3 p-4"> |
| <div class="ms-auto"><a class="text-white" href="https://www.solverforge.org">SolverForge</a></div> |
| <div class="vr"></div> |
| <div><a class="text-white" href="https://www.solverforge.org/docs">Documentation</a></div> |
| <div class="vr"></div> |
| <div><a class="text-white" href="https://github.com/SolverForge/solverforge-legacy">Code</a></div> |
| <div class="vr"></div> |
| <div class="me-auto"><a class="text-white" href="mailto:info@solverforge.org">Support</a></div> |
| </div> |
| </div> |
| </footer> |
| `)); |
| } |
| } |
|
|