Rev 36618 | Rev 36644 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
package com.spice.profitmandi.web.controller;import com.google.gson.Gson;import com.google.gson.reflect.TypeToken;import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;import com.spice.profitmandi.common.model.CustomRetailer;import com.spice.profitmandi.common.web.util.ResponseSender;import com.spice.profitmandi.dao.entity.auth.AuthUser;import com.spice.profitmandi.dao.entity.fofo.FofoStore;import com.spice.profitmandi.dao.entity.logistics.PublicHolidays;import com.spice.profitmandi.dao.entity.user.AuthUserLocation;import com.spice.profitmandi.dao.entity.user.BeatPlan;import com.spice.profitmandi.dao.entity.user.BeatPlanDay;import com.spice.profitmandi.dao.enumuration.cs.EscalationType;import com.spice.profitmandi.dao.repository.auth.AuthRepository;import com.spice.profitmandi.dao.repository.cs.CsService;import com.spice.profitmandi.dao.repository.dtr.*;import com.spice.profitmandi.dao.repository.logistics.PublicHolidaysRepository;import com.spice.profitmandi.service.user.RetailerService;import com.spice.profitmandi.web.model.LoginDetails;import com.spice.profitmandi.web.util.CookiesProcessor;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Controller;import org.springframework.transaction.annotation.Transactional;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestParam;import javax.servlet.http.HttpServletRequest;import java.lang.reflect.Type;import java.time.DayOfWeek;import java.time.LocalDate;import java.time.LocalDateTime;import java.time.YearMonth;import java.time.format.DateTimeFormatter;import java.util.*;import java.util.stream.Collectors;@Controller@Transactional(rollbackFor = Throwable.class)public class BeatPlanController {private static final Logger LOGGER = LogManager.getLogger(BeatPlanController.class);private static final String[] BEAT_COLORS = {"#3498DB", "#E74C3C", "#2ECC71", "#9B59B6", "#F39C12","#1ABC9C", "#E67E22", "#34495E", "#16A085", "#C0392B"};@Autowiredprivate CsService csService;@Autowiredprivate AuthRepository authRepository;@Autowiredprivate FofoStoreRepository fofoStoreRepository;@Autowiredprivate RetailerService retailerService;@Autowiredprivate BeatPlanRepository beatPlanRepository;@Autowiredprivate BeatPlanDayRepository beatPlanDayRepository;@Autowiredprivate AuthUserLocationRepository authUserLocationRepository;@Autowiredprivate LeadRepository leadRepository;@Autowiredprivate PublicHolidaysRepository publicHolidaysRepository;@Autowiredprivate com.spice.profitmandi.service.GeocodingService geocodingService;@Autowiredprivate CookiesProcessor cookiesProcessor;@Autowiredprivate ResponseSender responseSender;@GetMapping(value = "/beatPlan")public String beatPlan(HttpServletRequest request, Model model) {EscalationType[] escalationTypes = EscalationType.values();model.addAttribute("escalationTypes", escalationTypes);return "beat-plan";}@GetMapping(value = "/beatPlanWindow")public String beatPlanWindow(HttpServletRequest request, Model model) {EscalationType[] escalationTypes = EscalationType.values();model.addAttribute("escalationTypes", escalationTypes);return "beat-plan-window";}@GetMapping(value = "/beatPlan/getAuthUsers")public ResponseEntity<?> getAuthUsers(@RequestParam int categoryId,@RequestParam EscalationType escalationType) {List<AuthUser> authUsers = csService.getAuthUserByCategoryId(categoryId, escalationType);List<Map<String, Object>> result = authUsers.stream().filter(au -> au.getActive()).map(au -> {Map<String, Object> map = new HashMap<>();map.put("id", au.getId());map.put("name", au.getFirstName() + " " + au.getLastName());return map;}).collect(Collectors.toList());return responseSender.ok(result);}@GetMapping(value = "/beatPlan/getBeatVisits")public ResponseEntity<?> getBeatVisits(@RequestParam String planGroupId) {List<BeatPlan> visits = beatPlanRepository.selectByPlanGroupId(planGroupId);List<Map<String, Object>> result = visits.stream().map(v -> {Map<String, Object> map = new HashMap<>();map.put("fofoId", v.getFofoId());map.put("dayNumber", v.getDayNumber());map.put("sequenceOrder", v.getSequenceOrder());map.put("visitType", v.getVisitType());return map;}).collect(Collectors.toList());return responseSender.ok(result);}@GetMapping(value = "/beatPlan/getBaseLocation")public ResponseEntity<?> getBaseLocation(@RequestParam int authUserId) {AuthUserLocation baseLoc = authUserLocationRepository.selectLatestByAuthUserIdAndType(authUserId, "BASE");if (baseLoc == null) {return responseSender.ok(new HashMap<>());}Map<String, Object> result = new HashMap<>();result.put("id", baseLoc.getId());result.put("locationName", baseLoc.getLocationName());result.put("latitude", baseLoc.getLatitude());result.put("longitude", baseLoc.getLongitude());result.put("address", baseLoc.getAddress());return responseSender.ok(result);}@PostMapping(value = "/beatPlan/saveBaseLocation")public ResponseEntity<?> saveBaseLocation(@RequestParam int authUserId,@RequestParam String locationName,@RequestParam String latitude,@RequestParam String longitude,@RequestParam(required = false) String address) {AuthUserLocation loc = new AuthUserLocation();loc.setAuthUserId(authUserId);loc.setLocationType("BASE");loc.setLocationName(locationName);loc.setLatitude(latitude);loc.setLongitude(longitude);loc.setAddress(address);loc.setCreatedTimestamp(LocalDateTime.now());authUserLocationRepository.persist(loc);Map<String, Object> result = new HashMap<>();result.put("status", true);result.put("id", loc.getId());return responseSender.ok(result);}@GetMapping(value = "/beatPlan/getPartners")public ResponseEntity<?> getPartners(@RequestParam int authUserId,@RequestParam int categoryId,@RequestParam(required = false) String startLat,@RequestParam(required = false) String startLng) throws ProfitMandiBusinessException {Map<Integer, List<Integer>> pp = csService.getAuthUserIdPartnerIdMapping();List<Integer> fofoIds = pp.get(authUserId);if (fofoIds.isEmpty()) {Map<String, Object> empty = new HashMap<>();empty.put("partners", new ArrayList<>());return responseSender.ok(empty);}List<FofoStore> fofoStores = fofoStoreRepository.selectByRetailerIds(fofoIds);Map<Integer, CustomRetailer> retailerMap = retailerService.getFofoRetailers(fofoIds);// Build partner data list with addressesList<Map<String, Object>> partners = new ArrayList<>();List<String> addressesToGeocode = new ArrayList<>();List<FofoStore> activeStores = new ArrayList<>();for (FofoStore store : fofoStores) {if (!store.isActive() || store.isClosed()) continue;activeStores.add(store);CustomRetailer retailer = retailerMap.get(store.getId());Map<String, Object> partnerData = new HashMap<>();partnerData.put("fofoId", store.getId());partnerData.put("code", store.getCode());partnerData.put("outletName", store.getOutletName());partnerData.put("type", "partner");String displayAddress = null;String geoAddress = null;if (retailer != null) {partnerData.put("businessName", retailer.getBusinessName());if (retailer.getAddress() != null) {displayAddress = retailer.getAddress().getAddressString();partnerData.put("address", displayAddress);// Build clean address for geocoding: city, state, pincode, IndiageoAddress = com.spice.profitmandi.service.GeocodingService.buildGeoAddress(retailer.getAddress().getLine1(),retailer.getAddress().getCity(),retailer.getAddress().getState(),retailer.getAddress().getPinCode());}}addressesToGeocode.add(geoAddress);partners.add(partnerData);}// Geocode all addresses in parallel (cached eternally in Redis, so only first call hits Google)java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(Math.min(10, partners.size()));List<java.util.concurrent.Future<double[]>> futures = new ArrayList<>();for (String addr : addressesToGeocode) {futures.add(executor.submit(() -> {if (addr != null && !addr.isEmpty()) {try {return geocodingService.geocodeAddress(addr);} catch (Exception e) {return null;}}return null;}));}// Collect resultsfor (int i = 0; i < partners.size(); i++) {Map<String, Object> partnerData = partners.get(i);String lat = null, lng = null;try {double[] coords = futures.get(i).get(10, java.util.concurrent.TimeUnit.SECONDS);if (coords != null) {lat = String.valueOf(coords[0]);lng = String.valueOf(coords[1]);}} catch (Exception e) {LOGGER.warn("Geocoding timeout/error for partner {}", partnerData.get("code"));}partnerData.put("latitude", lat);partnerData.put("longitude", lng);}executor.shutdown();// Sort by nearest neighbor from start location if provided, otherwise from first partnerif (startLat != null && startLng != null && !startLat.isEmpty() && !startLng.isEmpty()) {partners = sortByNearestNeighborFromStart(partners, Double.parseDouble(startLat), Double.parseDouble(startLng));} else {partners = sortByNearestNeighbor(partners);}Map<String, Object> response = new HashMap<>();response.put("partners", partners);return responseSender.ok(response);}@PostMapping(value = "/beatPlan/submitPlan")public ResponseEntity<?> submitPlan(HttpServletRequest request,@RequestParam int authUserId,@RequestParam String planData) throws Exception {LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());Gson gson = new Gson();Type type = new TypeToken<Map<String, Object>>() {}.getType();Map<String, Object> plan = gson.fromJson(planData, type);List<Map<String, Object>> days = (List<Map<String, Object>>) plan.get("days");List<String> dates = (List<String>) plan.get("dates");// Build visit fingerprint to detect duplicatesList<Integer> newVisitIds = new ArrayList<>();for (Map<String, Object> day : days) {List<Map<String, Object>> visits = (List<Map<String, Object>>) day.get("visits");if (visits != null) {for (Map<String, Object> v : visits) {newVisitIds.add(((Number) v.get("id")).intValue());}}}Collections.sort(newVisitIds);// Check existing beats for this auth userList<BeatPlanDay> existingBeatDays = beatPlanDayRepository.selectByAuthUserIdAndDateRange(authUserId, LocalDate.of(2000, 1, 1), LocalDate.of(2099, 12, 31));Set<String> existingGroups = existingBeatDays.stream().map(BeatPlanDay::getPlanGroupId).filter(g -> g != null).collect(Collectors.toSet());for (String existingGroup : existingGroups) {List<BeatPlan> existingVisits = beatPlanRepository.selectByPlanGroupId(existingGroup);List<Integer> existingIds = existingVisits.stream().map(BeatPlan::getFofoId).sorted().collect(Collectors.toList());if (existingIds.equals(newVisitIds)) {// Duplicate found — return existing planGroupIdMap<String, Object> response = new HashMap<>();response.put("status", true);response.put("planGroupId", existingGroup);response.put("duplicate", true);response.put("message", "Beat already exists with same visits");return responseSender.ok(response);}}String planGroupId = UUID.randomUUID().toString();String beatName = plan.get("beatName") != null ? (String) plan.get("beatName") : "Beat";String beatColor = BEAT_COLORS[Math.abs(planGroupId.hashCode()) % BEAT_COLORS.length];for (int d = 0; d < days.size(); d++) {Map<String, Object> day = days.get(d);int dayNumber = d + 1;LocalDate planDate = (dates != null && d < dates.size() && dates.get(d) != null)? LocalDate.parse(dates.get(d), DateTimeFormatter.ISO_DATE): null;// Save day-level infoBeatPlanDay bpDay = new BeatPlanDay();bpDay.setPlanGroupId(planGroupId);bpDay.setAuthUserId(authUserId);bpDay.setDayNumber(dayNumber);bpDay.setPlanDate(planDate);bpDay.setBeatName(beatName);bpDay.setBeatColor(beatColor);bpDay.setStartLocationName((String) day.get("startLocationName"));bpDay.setStartLatitude((String) day.get("startLatitude"));bpDay.setStartLongitude((String) day.get("startLongitude"));bpDay.setEndAction((String) day.get("endAction"));bpDay.setStayLocationName((String) day.get("stayLocationName"));bpDay.setStayLatitude((String) day.get("stayLatitude"));bpDay.setStayLongitude((String) day.get("stayLongitude"));if (day.get("totalDistanceKm") != null) {bpDay.setTotalDistanceKm(((Number) day.get("totalDistanceKm")).doubleValue());}if (day.get("totalTimeMins") != null) {bpDay.setTotalTimeMins(((Number) day.get("totalTimeMins")).intValue());}bpDay.setCreatedBy(currentUser.getId());bpDay.setCreatedTimestamp(LocalDateTime.now());bpDay.setActive(true);beatPlanDayRepository.persist(bpDay);// Save visits for this dayList<Map<String, Object>> visits = (List<Map<String, Object>>) day.get("visits");if (visits != null) {for (int i = 0; i < visits.size(); i++) {Map<String, Object> visit = visits.get(i);BeatPlan bp = new BeatPlan();bp.setAuthUserId(authUserId);bp.setFofoId(((Number) visit.get("id")).intValue());bp.setVisitType((String) visit.get("type"));bp.setDayNumber(dayNumber);bp.setPlanGroupId(planGroupId);bp.setPlanDate(planDate);bp.setSequenceOrder(i);bp.setCreatedBy(currentUser.getId());bp.setCreatedTimestamp(LocalDateTime.now());bp.setUpdatedTimestamp(LocalDateTime.now());bp.setActive(true);beatPlanRepository.persist(bp);}}}Map<String, Object> response = new HashMap<>();response.put("status", true);response.put("planGroupId", planGroupId);response.put("message", "Beat plan submitted successfully");return responseSender.ok(response);}// ============ BULK UPLOAD ============@GetMapping(value = "/beatPlan/bulkUpload")public String bulkUploadPage(HttpServletRequest request, Model model) {return "beat-plan-bulk";}@GetMapping(value = "/beatPlan/downloadTemplate")public ResponseEntity<?> downloadTemplate() {String csv = "beat_name,auth_user_id,start_date,day_number,partner_codes\n";csv += "Jaipur East Route,280,2026-06-02,1,\"RJKAI1478,RJBUN1449,RJDEG1443\"\n";csv += ",280,,2,\"RJALR1362,RJBTR1388\"\n";csv += ",280,,3,\"RJRSD1518,RJSML356\"\n";csv += "Agra Circuit,145,2026-06-05,1,\"UPAGR101,UPAGR102\"\n";org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();headers.add("Content-Disposition", "attachment; filename=beat_plan_template.csv");headers.add("Content-Type", "text/csv");return new ResponseEntity<>(csv, headers, org.springframework.http.HttpStatus.OK);}@PostMapping(value = "/beatPlan/bulkUploadProcess")public ResponseEntity<?> bulkUploadProcess(HttpServletRequest request,@RequestParam("file") org.springframework.web.multipart.MultipartFile file,@RequestParam(value = "includeSundays", defaultValue = "false") boolean includeSundays) throws Exception {LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());java.io.Reader reader = new java.io.InputStreamReader(file.getInputStream());org.apache.commons.csv.CSVParser parser = new org.apache.commons.csv.CSVParser(reader,org.apache.commons.csv.CSVFormat.DEFAULT.withFirstRecordAsHeader().withTrim());// Each row = one day: beat_name, auth_user_id, start_date (only on day 1), day_number, partner_codes// First pass: collect all records, normalize beat_nameList<org.apache.commons.csv.CSVRecord> allRecords = parser.getRecords();parser.close();// Track last beat_name per auth_user_id for blank name rowsMap<String, String> lastBeatNameByUser = new HashMap<>();Map<String, List<org.apache.commons.csv.CSVRecord>> beatGroups = new LinkedHashMap<>();// Also store resolved beat names since CSVRecord is immutableMap<Long, String> resolvedBeatNames = new HashMap<>();for (org.apache.commons.csv.CSVRecord record : allRecords) {String authId = record.get("auth_user_id").trim();String rawName = record.get("beat_name").trim();// Normalize: collapse multiple spaces, trimString beatName = rawName.replaceAll("\\s+", " ").trim();// If blank, use last beat_name for this auth_user_idif (beatName.isEmpty()) {beatName = lastBeatNameByUser.getOrDefault(authId, "Beat");} else {lastBeatNameByUser.put(authId, beatName);}resolvedBeatNames.put(record.getRecordNumber(), beatName);String key = beatName + "|" + authId;beatGroups.computeIfAbsent(key, k -> new ArrayList<>()).add(record);}// Build FofoStore code → id lookupList<FofoStore> allStores = fofoStoreRepository.selectAll();Map<String, Integer> codeToId = new HashMap<>();for (FofoStore store : allStores) {codeToId.put(store.getCode(), store.getId());}// Load holidays for validationLocalDate holidayStart = LocalDate.now();LocalDate holidayEnd = holidayStart.plusMonths(6);List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(holidayStart, holidayEnd);Set<LocalDate> holidayDates = holidays.stream().map(PublicHolidays::getDate).collect(Collectors.toSet());int beatsCreated = 0;int errors = 0;List<String> errorMessages = new ArrayList<>();for (Map.Entry<String, List<org.apache.commons.csv.CSVRecord>> entry : beatGroups.entrySet()) {try {String[] keyParts = entry.getKey().split("\\|");String beatName = keyParts[0];int authUserId = Integer.parseInt(keyParts[1]);List<org.apache.commons.csv.CSVRecord> rows = entry.getValue();// Sort rows by day_numberrows.sort((a, b) -> Integer.parseInt(a.get("day_number").trim()) - Integer.parseInt(b.get("day_number").trim()));// Get start_date from first rowString startDateStr = rows.get(0).get("start_date").trim();LocalDate startDate = null;if (!startDateStr.isEmpty()) {startDate = LocalDate.parse(startDateStr, DateTimeFormatter.ISO_DATE);}// Validate start dateif (startDate != null && startDate.isBefore(LocalDate.now())) {errorMessages.add("Beat '" + beatName + "': start_date " + startDate + " is in the past. Skipped.");errors++;continue;}// Auto-calculate dates for each day, skipping Sundays and holidaysint totalDays = rows.size();List<LocalDate> scheduleDates = new ArrayList<>();if (startDate != null) {LocalDate d = startDate;while (scheduleDates.size() < totalDays) {boolean isSunday = d.getDayOfWeek() == DayOfWeek.SUNDAY;boolean isHoliday = holidayDates.contains(d);if (isHoliday) {// Always skip holidayserrorMessages.add("Beat '" + beatName + "': skipping " + d + " (Holiday)");} else if (isSunday && !includeSundays) {// Skip Sunday only if not including SundayserrorMessages.add("Beat '" + beatName + "': skipping " + d + " (Sunday)");} else {scheduleDates.add(d);if (isSunday) {errorMessages.add("Beat '" + beatName + "': including Sunday " + d);}}d = d.plusDays(1);}}String planGroupId = UUID.randomUUID().toString();String beatColor = BEAT_COLORS[Math.abs(planGroupId.hashCode()) % BEAT_COLORS.length];// Get home location for this auth userAuthUserLocation homeLoc = authUserLocationRepository.selectLatestByAuthUserIdAndType(authUserId, "BASE");String homeName = homeLoc != null ? homeLoc.getLocationName() : "Home";String homeLat = homeLoc != null ? homeLoc.getLatitude() : null;String homeLng = homeLoc != null ? homeLoc.getLongitude() : null;int totalRows = rows.size();for (int rowIdx = 0; rowIdx < totalRows; rowIdx++) {org.apache.commons.csv.CSVRecord row = rows.get(rowIdx);int dayNumber = Integer.parseInt(row.get("day_number").trim());boolean isFirstDay = (rowIdx == 0);boolean isLastDay = (rowIdx == totalRows - 1);LocalDate planDate = (rowIdx < scheduleDates.size()) ? scheduleDates.get(rowIdx) : null;// Save BeatPlanDayBeatPlanDay bpDay = new BeatPlanDay();bpDay.setPlanGroupId(planGroupId);bpDay.setAuthUserId(authUserId);bpDay.setDayNumber(dayNumber);bpDay.setPlanDate(planDate);bpDay.setBeatName(beatName);bpDay.setBeatColor(beatColor);// Start location: Day 1 starts from home, other days start from home too (each day starts fresh)bpDay.setStartLocationName(homeName);bpDay.setStartLatitude(homeLat);bpDay.setStartLongitude(homeLng);// End action: last day = HOME (return home), other days = DAYBREAKif (isLastDay) {bpDay.setEndAction("HOME");} else {bpDay.setEndAction("DAYBREAK");}bpDay.setCreatedBy(currentUser.getId());bpDay.setCreatedTimestamp(LocalDateTime.now());bpDay.setActive(true);beatPlanDayRepository.persist(bpDay);// Parse comma-separated partner codesString partnerCodesStr = row.get("partner_codes").trim();String[] partnerCodes = partnerCodesStr.split(",");for (int i = 0; i < partnerCodes.length; i++) {String partnerCode = partnerCodes[i].trim();if (partnerCode.isEmpty()) continue;Integer fofoId = codeToId.get(partnerCode);if (fofoId == null) {errorMessages.add("Partner code not found: " + partnerCode + " in beat '" + beatName + "'");errors++;continue;}BeatPlan bp = new BeatPlan();bp.setAuthUserId(authUserId);bp.setFofoId(fofoId);bp.setVisitType("partner");bp.setDayNumber(dayNumber);bp.setPlanGroupId(planGroupId);bp.setPlanDate(planDate);bp.setSequenceOrder(i);bp.setCreatedBy(currentUser.getId());bp.setCreatedTimestamp(LocalDateTime.now());bp.setUpdatedTimestamp(LocalDateTime.now());bp.setActive(true);beatPlanRepository.persist(bp);}}beatsCreated++;// Log scheduled datesif (!scheduleDates.isEmpty()) {errorMessages.add("Beat '" + beatName + "' for user " + authUserId + " scheduled on: " +scheduleDates.stream().map(LocalDate::toString).collect(Collectors.joining(", ")));}} catch (Exception e) {errors++;errorMessages.add("Error processing beat: " + entry.getKey() + " - " + e.getMessage());LOGGER.error("Bulk upload error for {}: {}", entry.getKey(), e.getMessage());}}Map<String, Object> response = new HashMap<>();response.put("status", true);response.put("beatsCreated", beatsCreated);response.put("errors", errors);response.put("errorMessages", errorMessages);return responseSender.ok(response);}// ============ CALENDAR ENDPOINTS ============@PostMapping(value = "/beatPlan/delete")public ResponseEntity<?> deleteBeat(@RequestParam String planGroupId) {beatPlanDayRepository.deleteByPlanGroupId(planGroupId);beatPlanRepository.deleteByPlanGroupId(planGroupId);Map<String, Object> response = new HashMap<>();response.put("status", true);response.put("message", "Beat deleted");return responseSender.ok(response);}@GetMapping(value = "/beatPlan/calendar")public ResponseEntity<?> getCalendar(@RequestParam int authUserId,@RequestParam String month) {YearMonth ym = YearMonth.parse(month);LocalDate startDate = ym.atDay(1);LocalDate endDate = ym.atEndOfMonth();// Get holidaysList<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(startDate, endDate);List<Map<String, String>> holidayList = holidays.stream().map(h -> {Map<String, String> m = new HashMap<>();m.put("date", h.getDate().toString());m.put("occasion", h.getOccasion());return m;}).collect(Collectors.toList());// Get ALL active beats for this auth user (not just this month)List<BeatPlanDay> allBeatDays = beatPlanDayRepository.selectByAuthUserIdAndDateRange(authUserId, LocalDate.of(2000, 1, 1), LocalDate.of(2099, 12, 31));// Group by planGroupIdMap<String, List<BeatPlanDay>> grouped = allBeatDays.stream().filter(d -> d.getPlanGroupId() != null).collect(Collectors.groupingBy(BeatPlanDay::getPlanGroupId));LocalDate today = LocalDate.now();List<Map<String, Object>> scheduledBeats = new ArrayList<>();for (Map.Entry<String, List<BeatPlanDay>> entry : grouped.entrySet()) {List<BeatPlanDay> days = entry.getValue();BeatPlanDay first = days.get(0);// Determine statusboolean allNullDates = days.stream().allMatch(d -> d.getPlanDate() == null);boolean hasToday = days.stream().anyMatch(d -> d.getPlanDate() != null && d.getPlanDate().equals(today));boolean allPast = !allNullDates && days.stream().filter(d -> d.getPlanDate() != null).allMatch(d -> d.getPlanDate().isBefore(today));boolean allFuture = !allNullDates && days.stream().filter(d -> d.getPlanDate() != null).allMatch(d -> d.getPlanDate().isAfter(today));String status;if (allNullDates) status = "unscheduled";else if (hasToday) status = "running";else if (allPast) status = "completed";else if (allFuture) status = "scheduled";else status = "scheduled"; // mixed past/future// Get visit counts per dayMap<String, Object> beat = new HashMap<>();beat.put("planGroupId", entry.getKey());beat.put("beatName", first.getBeatName() != null ? first.getBeatName() : "Beat");beat.put("beatColor", first.getBeatColor() != null ? first.getBeatColor() : "#3498DB");beat.put("status", status);List<Map<String, Object>> dayInfoList = new ArrayList<>();for (BeatPlanDay d : days) {Map<String, Object> dayInfo = new HashMap<>();dayInfo.put("dayNumber", d.getDayNumber());dayInfo.put("planDate", d.getPlanDate() != null ? d.getPlanDate().toString() : null);dayInfo.put("totalKm", d.getTotalDistanceKm());dayInfo.put("totalMins", d.getTotalTimeMins());// Count visits for this dayList<BeatPlan> visits = beatPlanRepository.selectByPlanGroupId(entry.getKey()).stream().filter(bp -> bp.getDayNumber() == d.getDayNumber()).collect(Collectors.toList());dayInfo.put("visitCount", visits.size());dayInfoList.add(dayInfo);}beat.put("days", dayInfoList);scheduledBeats.add(beat);}// Build blocked dates (Sundays + holidays)Set<String> blockedDates = new HashSet<>();for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {if (d.getDayOfWeek() == DayOfWeek.SUNDAY) {blockedDates.add(d.toString());}}for (PublicHolidays h : holidays) {blockedDates.add(h.getDate().toString());}Map<String, Object> response = new HashMap<>();response.put("holidays", holidayList);response.put("scheduledBeats", scheduledBeats);response.put("blockedDates", blockedDates);return responseSender.ok(response);}@PostMapping(value = "/beatPlan/scheduleOnCalendar")public ResponseEntity<?> scheduleOnCalendar(HttpServletRequest request,@RequestParam String planGroupId,@RequestParam String dates,@RequestParam(required = false) String beatName,@RequestParam(required = false) String beatColor) throws Exception {Gson gson = new Gson();List<String> dateList = gson.fromJson(dates, new TypeToken<List<String>>() {}.getType());List<BeatPlanDay> beatDays = beatPlanDayRepository.selectByPlanGroupId(planGroupId);if (beatDays.isEmpty()) {return responseSender.badRequest("No beat found for this plan group");}// Check running beat — can't rescheduleLocalDate today = LocalDate.now();boolean isRunning = beatDays.stream().anyMatch(d -> d.getPlanDate() != null && d.getPlanDate().equals(today));if (isRunning) {return responseSender.badRequest("Cannot reschedule a running beat");}// Auto-assign color if not providedif (beatColor == null || beatColor.isEmpty()) {beatColor = BEAT_COLORS[Math.abs(planGroupId.hashCode()) % BEAT_COLORS.length];}// Update dates on beat_plan_daybeatDays.sort(Comparator.comparingInt(BeatPlanDay::getDayNumber));for (int i = 0; i < beatDays.size() && i < dateList.size(); i++) {BeatPlanDay day = beatDays.get(i);day.setPlanDate(LocalDate.parse(dateList.get(i)));if (beatName != null) day.setBeatName(beatName);day.setBeatColor(beatColor);beatPlanDayRepository.persist(day);}// Update dates on beat_plan records tooList<BeatPlan> beatPlans = beatPlanRepository.selectByPlanGroupId(planGroupId);for (BeatPlan bp : beatPlans) {for (int i = 0; i < beatDays.size() && i < dateList.size(); i++) {if (bp.getDayNumber() == beatDays.get(i).getDayNumber()) {bp.setPlanDate(LocalDate.parse(dateList.get(i)));beatPlanRepository.persist(bp);}}}Map<String, Object> response = new HashMap<>();response.put("status", true);response.put("message", "Beat scheduled successfully");return responseSender.ok(response);}@PostMapping(value = "/beatPlan/repeatBeat")public ResponseEntity<?> repeatBeat(HttpServletRequest request,@RequestParam String sourcePlanGroupId,@RequestParam int authUserId,@RequestParam String dates) throws Exception {LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());Gson gson = new Gson();List<String> dateList = gson.fromJson(dates, new TypeToken<List<String>>() {}.getType());// Get source beat days and visitsList<BeatPlanDay> sourceDays = beatPlanDayRepository.selectByPlanGroupId(sourcePlanGroupId);List<BeatPlan> sourceVisits = beatPlanRepository.selectByPlanGroupId(sourcePlanGroupId);if (sourceDays.isEmpty()) {return responseSender.badRequest("Source beat not found");}String newPlanGroupId = UUID.randomUUID().toString();String beatName = sourceDays.get(0).getBeatName();String beatColor = sourceDays.get(0).getBeatColor();// Copy days with new datessourceDays.sort(Comparator.comparingInt(BeatPlanDay::getDayNumber));for (int i = 0; i < sourceDays.size() && i < dateList.size(); i++) {BeatPlanDay src = sourceDays.get(i);BeatPlanDay newDay = new BeatPlanDay();newDay.setPlanGroupId(newPlanGroupId);newDay.setAuthUserId(authUserId);newDay.setDayNumber(src.getDayNumber());newDay.setPlanDate(LocalDate.parse(dateList.get(i)));newDay.setBeatName(beatName);newDay.setBeatColor(beatColor);newDay.setStartLocationName(src.getStartLocationName());newDay.setStartLatitude(src.getStartLatitude());newDay.setStartLongitude(src.getStartLongitude());newDay.setEndAction(src.getEndAction());newDay.setStayLocationName(src.getStayLocationName());newDay.setStayLatitude(src.getStayLatitude());newDay.setStayLongitude(src.getStayLongitude());newDay.setTotalDistanceKm(src.getTotalDistanceKm());newDay.setTotalTimeMins(src.getTotalTimeMins());newDay.setCreatedBy(currentUser.getId());newDay.setCreatedTimestamp(LocalDateTime.now());newDay.setActive(true);beatPlanDayRepository.persist(newDay);}// Copy visits with new plan group and datesfor (BeatPlan srcVisit : sourceVisits) {BeatPlan newVisit = new BeatPlan();newVisit.setAuthUserId(authUserId);newVisit.setFofoId(srcVisit.getFofoId());newVisit.setVisitType(srcVisit.getVisitType());newVisit.setDayNumber(srcVisit.getDayNumber());newVisit.setPlanGroupId(newPlanGroupId);newVisit.setSequenceOrder(srcVisit.getSequenceOrder());newVisit.setCreatedBy(currentUser.getId());newVisit.setCreatedTimestamp(LocalDateTime.now());newVisit.setUpdatedTimestamp(LocalDateTime.now());newVisit.setActive(true);// Set date from new dateListint dayIdx = srcVisit.getDayNumber() - 1;if (dayIdx >= 0 && dayIdx < dateList.size()) {newVisit.setPlanDate(LocalDate.parse(dateList.get(dayIdx)));}beatPlanRepository.persist(newVisit);}Map<String, Object> response = new HashMap<>();response.put("status", true);response.put("planGroupId", newPlanGroupId);response.put("message", "Beat repeated successfully");return responseSender.ok(response);}@GetMapping(value = "/beatPlan/availableSlots")public ResponseEntity<?> getAvailableSlots(@RequestParam int authUserId,@RequestParam String month,@RequestParam int daysNeeded) {YearMonth ym = YearMonth.parse(month);LocalDate startDate = ym.atDay(1);LocalDate endDate = ym.atEndOfMonth();LocalDate today = LocalDate.now();// Blocked: Sundays + holidays + already scheduled datesSet<LocalDate> blocked = new HashSet<>();for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {if (d.getDayOfWeek() == DayOfWeek.SUNDAY) blocked.add(d);if (!d.isAfter(today)) blocked.add(d); // past dates blocked}List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(startDate, endDate);for (PublicHolidays h : holidays) blocked.add(h.getDate());List<BeatPlanDay> existingBeats = beatPlanDayRepository.selectByAuthUserIdAndDateRange(authUserId, startDate, endDate);for (BeatPlanDay bd : existingBeats) {if (bd.getPlanDate() != null) blocked.add(bd.getPlanDate());}// Find earliest available slotsList<String> available = new ArrayList<>();for (LocalDate d = startDate.isAfter(today) ? startDate : today.plusDays(1);!d.isAfter(endDate) && available.size() < daysNeeded;d = d.plusDays(1)) {if (!blocked.contains(d)) {available.add(d.toString());}}Map<String, Object> response = new HashMap<>();response.put("suggestedDates", available);response.put("totalAvailable", available.size());return responseSender.ok(response);}// --- Sorting helpers ---private List<Map<String, Object>> sortByNearestNeighborFromStart(List<Map<String, Object>> partners, double startLat, double startLng) {List<Map<String, Object>> withCoords = new ArrayList<>();List<Map<String, Object>> withoutCoords = new ArrayList<>();for (Map<String, Object> p : partners) {if (hasValidCoords(p)) {withCoords.add(p);} else {withoutCoords.add(p);}}List<Map<String, Object>> sorted = new ArrayList<>();double currentLat = startLat;double currentLng = startLng;while (!withCoords.isEmpty()) {int nearestIdx = 0;double nearestDist = Double.MAX_VALUE;for (int i = 0; i < withCoords.size(); i++) {double lat = Double.parseDouble(withCoords.get(i).get("latitude").toString());double lng = Double.parseDouble(withCoords.get(i).get("longitude").toString());double dist = haversine(currentLat, currentLng, lat, lng);if (dist < nearestDist) {nearestDist = dist;nearestIdx = i;}}Map<String, Object> nearest = withCoords.remove(nearestIdx);sorted.add(nearest);currentLat = Double.parseDouble(nearest.get("latitude").toString());currentLng = Double.parseDouble(nearest.get("longitude").toString());}sorted.addAll(withoutCoords);return sorted;}private List<Map<String, Object>> sortByNearestNeighbor(List<Map<String, Object>> partners) {List<Map<String, Object>> withCoords = new ArrayList<>();List<Map<String, Object>> withoutCoords = new ArrayList<>();for (Map<String, Object> p : partners) {if (hasValidCoords(p)) {withCoords.add(p);} else {withoutCoords.add(p);}}List<Map<String, Object>> sorted = new ArrayList<>();if (!withCoords.isEmpty()) {sorted.add(withCoords.remove(0));while (!withCoords.isEmpty()) {Map<String, Object> last = sorted.get(sorted.size() - 1);double lastLat = Double.parseDouble(last.get("latitude").toString());double lastLng = Double.parseDouble(last.get("longitude").toString());int nearestIdx = 0;double nearestDist = Double.MAX_VALUE;for (int i = 0; i < withCoords.size(); i++) {double lat = Double.parseDouble(withCoords.get(i).get("latitude").toString());double lng = Double.parseDouble(withCoords.get(i).get("longitude").toString());double dist = haversine(lastLat, lastLng, lat, lng);if (dist < nearestDist) {nearestDist = dist;nearestIdx = i;}}sorted.add(withCoords.remove(nearestIdx));}}sorted.addAll(withoutCoords);return sorted;}private boolean hasValidCoords(Map<String, Object> p) {Object lat = p.get("latitude");Object lng = p.get("longitude");return lat != null && lng != null&& !lat.toString().isEmpty() && !lng.toString().isEmpty();}private double haversine(double lat1, double lng1, double lat2, double lng2) {double R = 6371;double dLat = Math.toRadians(lat2 - lat1);double dLng = Math.toRadians(lng2 - lng1);double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))* Math.sin(dLng / 2) * Math.sin(dLng / 2);double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));return R * c;}}