/** * Recommended Fit functionality for adding new visits with recommendations. * * This module provides: * - Modal form for adding new visits * - Integration with the recommendation API * - Application of selected recommendations */ // Customer type configurations (must match CUSTOMER_TYPES in app.js and demo_data.py) const VISIT_CUSTOMER_TYPES = { RESIDENTIAL: { label: "Residential", icon: "fa-home", color: "#10b981", windowStart: "17:00", windowEnd: "20:00", minDemand: 1, maxDemand: 2, minService: 5, maxService: 10 }, BUSINESS: { label: "Business", icon: "fa-building", color: "#3b82f6", windowStart: "09:00", windowEnd: "17:00", minDemand: 3, maxDemand: 6, minService: 15, maxService: 30 }, RESTAURANT: { label: "Restaurant", icon: "fa-utensils", color: "#f59e0b", windowStart: "06:00", windowEnd: "10:00", minDemand: 5, maxDemand: 10, minService: 20, maxService: 40 }, }; function addNewVisit(id, lat, lng, map, marker) { $('#newVisitModal').modal('show'); const visitModalContent = $("#newVisitModalContent"); visitModalContent.children().remove(); let visitForm = ""; // Customer Type Selection (prominent at the top) visitForm += "
" + " " + "
"; Object.entries(VISIT_CUSTOMER_TYPES).forEach(([type, config]) => { const isDefault = type === 'RESIDENTIAL'; visitForm += `
`; }); visitForm += "
" + "
"; // Name and Location row visitForm += "
" + "
" + "
" + " " + ` ` + "
Field is required
" + "
" + "
" + " " + ` ` + "
" + "
" + " " + ` ` + "
" + "
" + "
"; // Cargo and Duration row visitForm += "
" + "
" + "
" + " " + " " + "
Field is required
" + "
" + "
" + " " + " " + "
Field is required
" + "
" + "
" + "
"; // Time window row visitForm += "
" + "
" + "
" + " " + " " + "
Field is required
" + "
" + "
" + " " + " " + "
Field is required
" + "
" + "
" + "
"; visitModalContent.append(visitForm); // Initialize with Residential defaults const defaultType = VISIT_CUSTOMER_TYPES.RESIDENTIAL; const tomorrow = JSJoda.LocalDate.now().plusDays(1); function parseTimeToDateTime(timeStr) { const [hours, minutes] = timeStr.split(':').map(Number); return tomorrow.atTime(JSJoda.LocalTime.of(hours, minutes)); } let minStartPicker = flatpickr("#inputMinStartTime", { enableTime: true, dateFormat: "Y-m-d H:i", defaultDate: parseTimeToDateTime(defaultType.windowStart).format(JSJoda.DateTimeFormatter.ofPattern('yyyy-M-d HH:mm')) }); let maxEndPicker = flatpickr("#inputMaxStartTime", { enableTime: true, dateFormat: "Y-m-d H:i", defaultDate: parseTimeToDateTime(defaultType.windowEnd).format(JSJoda.DateTimeFormatter.ofPattern('yyyy-M-d HH:mm')) }); // Customer type button click handler $(".customer-type-btn").click(function() { const selectedType = $(this).data('type'); const config = VISIT_CUSTOMER_TYPES[selectedType]; // Update button styles $(".customer-type-btn").each(function() { const btnType = $(this).data('type'); const btnConfig = VISIT_CUSTOMER_TYPES[btnType]; $(this).removeClass('active'); $(this).css({ 'background-color': 'transparent', 'color': btnConfig.color }); }); $(this).addClass('active'); $(this).css({ 'background-color': config.color, 'color': 'white' }); // Update time windows minStartPicker.setDate(parseTimeToDateTime(config.windowStart).format(JSJoda.DateTimeFormatter.ofPattern('yyyy-M-d HH:mm'))); maxEndPicker.setDate(parseTimeToDateTime(config.windowEnd).format(JSJoda.DateTimeFormatter.ofPattern('yyyy-M-d HH:mm'))); // Update demand hint and value $("#demandHint").text(`(${config.minDemand}-${config.maxDemand} typical)`); $("#inputDemand").val(config.minDemand); // Update service duration hint and value (use midpoint of range) const avgService = Math.round((config.minService + config.maxService) / 2); $("#durationHint").text(`(${config.minService}-${config.maxService} min typical)`); $("#inputDuration").val(avgService); }); const visitModalFooter = $("#newVisitModalFooter"); visitModalFooter.children().remove(); visitModalFooter.append(""); $("#recommendationButton").click(getRecommendationsModal); } function requestRecommendations(visitId, solution, endpointPath) { $.post(endpointPath, JSON.stringify({solution, visitId}), function (recommendations) { const visitModalContent = $("#newVisitModalContent"); visitModalContent.children().remove(); if (!recommendations || recommendations.length === 0) { visitModalContent.append("
No recommendations available. The recommendation API may not be fully implemented.
"); const visitModalFooter = $("#newVisitModalFooter"); visitModalFooter.children().remove(); visitModalFooter.append(""); return; } let visitOptions = ""; const visit = solution.visits.find(c => c.id === visitId); recommendations.forEach((recommendation, index) => { const scoreDiffDisplay = recommendation.scoreDiff || "N/A"; visitOptions += "
" + ` ` + ` " + "
"; }); visitModalContent.append(visitOptions); const visitModalFooter = $("#newVisitModalFooter"); visitModalFooter.children().remove(); visitModalFooter.append(""); $("#applyRecommendationButton").click(_ => applyRecommendationModal(recommendations)); }).fail(function (xhr, ajaxOptions, thrownError) { showError("Recommendations request failed.", xhr); $('#newVisitModal').modal('hide'); }); } function applyRecommendation(solution, visitId, vehicleId, index, endpointPath) { $.post(endpointPath, JSON.stringify({solution, visitId, vehicleId, index}), function (updatedSolution) { updateSolutionWithNewVisit(updatedSolution); }).fail(function (xhr, ajaxOptions, thrownError) { showError("Apply recommendation request failed.", xhr); $('#newVisitModal').modal('hide'); }); }