Subversion Repositories SmartDukaan

Rev

Rev 36618 | Rev 36644 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
36618 ranu 1
package com.spice.profitmandi.web.controller;
2
 
3
import com.google.gson.Gson;
4
import com.google.gson.reflect.TypeToken;
5
import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
6
import com.spice.profitmandi.common.model.CustomRetailer;
7
import com.spice.profitmandi.common.web.util.ResponseSender;
8
import com.spice.profitmandi.dao.entity.auth.AuthUser;
9
import com.spice.profitmandi.dao.entity.fofo.FofoStore;
10
import com.spice.profitmandi.dao.entity.logistics.PublicHolidays;
11
import com.spice.profitmandi.dao.entity.user.AuthUserLocation;
12
import com.spice.profitmandi.dao.entity.user.BeatPlan;
13
import com.spice.profitmandi.dao.entity.user.BeatPlanDay;
14
import com.spice.profitmandi.dao.enumuration.cs.EscalationType;
15
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
16
import com.spice.profitmandi.dao.repository.cs.CsService;
17
import com.spice.profitmandi.dao.repository.dtr.*;
18
import com.spice.profitmandi.dao.repository.logistics.PublicHolidaysRepository;
19
import com.spice.profitmandi.service.user.RetailerService;
20
import com.spice.profitmandi.web.model.LoginDetails;
21
import com.spice.profitmandi.web.util.CookiesProcessor;
22
import org.apache.logging.log4j.LogManager;
23
import org.apache.logging.log4j.Logger;
24
import org.springframework.beans.factory.annotation.Autowired;
25
import org.springframework.http.ResponseEntity;
26
import org.springframework.stereotype.Controller;
27
import org.springframework.transaction.annotation.Transactional;
28
import org.springframework.ui.Model;
29
import org.springframework.web.bind.annotation.GetMapping;
30
import org.springframework.web.bind.annotation.PostMapping;
31
import org.springframework.web.bind.annotation.RequestParam;
32
 
33
import javax.servlet.http.HttpServletRequest;
34
import java.lang.reflect.Type;
35
import java.time.DayOfWeek;
36
import java.time.LocalDate;
37
import java.time.LocalDateTime;
38
import java.time.YearMonth;
39
import java.time.format.DateTimeFormatter;
40
import java.util.*;
41
import java.util.stream.Collectors;
42
 
43
@Controller
44
@Transactional(rollbackFor = Throwable.class)
45
public class BeatPlanController {
46
	private static final Logger LOGGER = LogManager.getLogger(BeatPlanController.class);
47
	private static final String[] BEAT_COLORS = {
48
			"#3498DB", "#E74C3C", "#2ECC71", "#9B59B6", "#F39C12",
49
			"#1ABC9C", "#E67E22", "#34495E", "#16A085", "#C0392B"
50
	};
51
	@Autowired
52
	private CsService csService;
53
	@Autowired
54
	private AuthRepository authRepository;
55
	@Autowired
56
	private FofoStoreRepository fofoStoreRepository;
57
	@Autowired
58
	private RetailerService retailerService;
59
	@Autowired
60
	private BeatPlanRepository beatPlanRepository;
61
	@Autowired
62
	private BeatPlanDayRepository beatPlanDayRepository;
63
	@Autowired
64
	private AuthUserLocationRepository authUserLocationRepository;
65
	@Autowired
66
	private LeadRepository leadRepository;
67
	@Autowired
68
	private PublicHolidaysRepository publicHolidaysRepository;
69
	@Autowired
70
	private com.spice.profitmandi.service.GeocodingService geocodingService;
71
	@Autowired
72
	private CookiesProcessor cookiesProcessor;
73
	@Autowired
74
	private ResponseSender responseSender;
75
 
76
	@GetMapping(value = "/beatPlan")
77
	public String beatPlan(HttpServletRequest request, Model model) {
78
		EscalationType[] escalationTypes = EscalationType.values();
79
		model.addAttribute("escalationTypes", escalationTypes);
80
		return "beat-plan";
81
	}
82
 
83
	@GetMapping(value = "/beatPlanWindow")
84
	public String beatPlanWindow(HttpServletRequest request, Model model) {
85
		EscalationType[] escalationTypes = EscalationType.values();
86
		model.addAttribute("escalationTypes", escalationTypes);
87
		return "beat-plan-window";
88
	}
89
 
90
	@GetMapping(value = "/beatPlan/getAuthUsers")
91
	public ResponseEntity<?> getAuthUsers(
92
			@RequestParam int categoryId,
93
			@RequestParam EscalationType escalationType) {
94
 
95
		List<AuthUser> authUsers = csService.getAuthUserByCategoryId(categoryId, escalationType);
96
		List<Map<String, Object>> result = authUsers.stream()
97
				.filter(au -> au.getActive())
98
				.map(au -> {
99
					Map<String, Object> map = new HashMap<>();
100
					map.put("id", au.getId());
101
					map.put("name", au.getFirstName() + " " + au.getLastName());
102
					return map;
103
				})
104
				.collect(Collectors.toList());
105
 
106
		return responseSender.ok(result);
107
	}
108
 
36632 ranu 109
	@GetMapping(value = "/beatPlan/getBeatVisits")
110
	public ResponseEntity<?> getBeatVisits(@RequestParam String planGroupId) {
111
		List<BeatPlan> visits = beatPlanRepository.selectByPlanGroupId(planGroupId);
112
		List<Map<String, Object>> result = visits.stream().map(v -> {
113
			Map<String, Object> map = new HashMap<>();
114
			map.put("fofoId", v.getFofoId());
115
			map.put("dayNumber", v.getDayNumber());
116
			map.put("sequenceOrder", v.getSequenceOrder());
117
			map.put("visitType", v.getVisitType());
118
			return map;
119
		}).collect(Collectors.toList());
120
		return responseSender.ok(result);
121
	}
122
 
36618 ranu 123
	@GetMapping(value = "/beatPlan/getBaseLocation")
124
	public ResponseEntity<?> getBaseLocation(@RequestParam int authUserId) {
125
		AuthUserLocation baseLoc = authUserLocationRepository.selectLatestByAuthUserIdAndType(authUserId, "BASE");
126
		if (baseLoc == null) {
127
			return responseSender.ok(new HashMap<>());
128
		}
129
		Map<String, Object> result = new HashMap<>();
130
		result.put("id", baseLoc.getId());
131
		result.put("locationName", baseLoc.getLocationName());
132
		result.put("latitude", baseLoc.getLatitude());
133
		result.put("longitude", baseLoc.getLongitude());
134
		result.put("address", baseLoc.getAddress());
135
		return responseSender.ok(result);
136
	}
137
 
138
	@PostMapping(value = "/beatPlan/saveBaseLocation")
139
	public ResponseEntity<?> saveBaseLocation(
140
			@RequestParam int authUserId,
141
			@RequestParam String locationName,
142
			@RequestParam String latitude,
143
			@RequestParam String longitude,
144
			@RequestParam(required = false) String address) {
145
 
146
		AuthUserLocation loc = new AuthUserLocation();
147
		loc.setAuthUserId(authUserId);
148
		loc.setLocationType("BASE");
149
		loc.setLocationName(locationName);
150
		loc.setLatitude(latitude);
151
		loc.setLongitude(longitude);
152
		loc.setAddress(address);
153
		loc.setCreatedTimestamp(LocalDateTime.now());
154
		authUserLocationRepository.persist(loc);
155
 
156
		Map<String, Object> result = new HashMap<>();
157
		result.put("status", true);
158
		result.put("id", loc.getId());
159
		return responseSender.ok(result);
160
	}
161
 
162
	@GetMapping(value = "/beatPlan/getPartners")
163
	public ResponseEntity<?> getPartners(
164
			@RequestParam int authUserId,
165
			@RequestParam int categoryId,
166
			@RequestParam(required = false) String startLat,
167
			@RequestParam(required = false) String startLng) throws ProfitMandiBusinessException {
168
 
169
		Map<Integer, List<Integer>> pp = csService.getAuthUserIdPartnerIdMapping();
170
		List<Integer> fofoIds = pp.get(authUserId);
171
 
172
 
173
		if (fofoIds.isEmpty()) {
174
			Map<String, Object> empty = new HashMap<>();
175
			empty.put("partners", new ArrayList<>());
176
			return responseSender.ok(empty);
177
		}
178
 
179
		List<FofoStore> fofoStores = fofoStoreRepository.selectByRetailerIds(fofoIds);
180
		Map<Integer, CustomRetailer> retailerMap = retailerService.getFofoRetailers(fofoIds);
181
 
182
		// Build partner data list with addresses
183
		List<Map<String, Object>> partners = new ArrayList<>();
184
		List<String> addressesToGeocode = new ArrayList<>();
185
		List<FofoStore> activeStores = new ArrayList<>();
186
 
187
		for (FofoStore store : fofoStores) {
188
			if (!store.isActive() || store.isClosed()) continue;
189
			activeStores.add(store);
190
 
191
			CustomRetailer retailer = retailerMap.get(store.getId());
192
 
193
			Map<String, Object> partnerData = new HashMap<>();
194
			partnerData.put("fofoId", store.getId());
195
			partnerData.put("code", store.getCode());
196
			partnerData.put("outletName", store.getOutletName());
197
			partnerData.put("type", "partner");
198
 
36632 ranu 199
			String displayAddress = null;
200
			String geoAddress = null;
36618 ranu 201
			if (retailer != null) {
202
				partnerData.put("businessName", retailer.getBusinessName());
203
				if (retailer.getAddress() != null) {
36632 ranu 204
					displayAddress = retailer.getAddress().getAddressString();
205
					partnerData.put("address", displayAddress);
206
					// Build clean address for geocoding: city, state, pincode, India
207
					geoAddress = com.spice.profitmandi.service.GeocodingService.buildGeoAddress(
208
							retailer.getAddress().getLine1(),
209
							retailer.getAddress().getCity(),
210
							retailer.getAddress().getState(),
211
							retailer.getAddress().getPinCode());
36618 ranu 212
				}
213
			}
214
 
36632 ranu 215
			addressesToGeocode.add(geoAddress);
36618 ranu 216
			partners.add(partnerData);
217
		}
218
 
219
		// Geocode all addresses in parallel (cached eternally in Redis, so only first call hits Google)
220
		java.util.concurrent.ExecutorService executor = java.util.concurrent.Executors.newFixedThreadPool(
221
				Math.min(10, partners.size()));
222
		List<java.util.concurrent.Future<double[]>> futures = new ArrayList<>();
223
 
224
		for (String addr : addressesToGeocode) {
225
			futures.add(executor.submit(() -> {
226
				if (addr != null && !addr.isEmpty()) {
227
					try {
228
						return geocodingService.geocodeAddress(addr);
229
					} catch (Exception e) {
230
						return null;
231
					}
232
				}
233
				return null;
234
			}));
235
		}
236
 
237
		// Collect results
238
		for (int i = 0; i < partners.size(); i++) {
239
			Map<String, Object> partnerData = partners.get(i);
240
			String lat = null, lng = null;
241
 
242
			try {
243
				double[] coords = futures.get(i).get(10, java.util.concurrent.TimeUnit.SECONDS);
244
				if (coords != null) {
245
					lat = String.valueOf(coords[0]);
246
					lng = String.valueOf(coords[1]);
247
				}
248
			} catch (Exception e) {
249
				LOGGER.warn("Geocoding timeout/error for partner {}", partnerData.get("code"));
250
			}
251
 
252
			partnerData.put("latitude", lat);
253
			partnerData.put("longitude", lng);
254
		}
255
 
256
		executor.shutdown();
257
 
258
		// Sort by nearest neighbor from start location if provided, otherwise from first partner
259
		if (startLat != null && startLng != null && !startLat.isEmpty() && !startLng.isEmpty()) {
260
			partners = sortByNearestNeighborFromStart(partners, Double.parseDouble(startLat), Double.parseDouble(startLng));
261
		} else {
262
			partners = sortByNearestNeighbor(partners);
263
		}
264
 
265
		Map<String, Object> response = new HashMap<>();
266
		response.put("partners", partners);
267
		return responseSender.ok(response);
268
	}
269
 
270
	@PostMapping(value = "/beatPlan/submitPlan")
271
	public ResponseEntity<?> submitPlan(
272
			HttpServletRequest request,
273
			@RequestParam int authUserId,
274
			@RequestParam String planData) throws Exception {
275
 
276
		LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);
277
		AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());
278
 
279
		Gson gson = new Gson();
280
		Type type = new TypeToken<Map<String, Object>>() {
281
		}.getType();
282
		Map<String, Object> plan = gson.fromJson(planData, type);
283
 
284
		List<Map<String, Object>> days = (List<Map<String, Object>>) plan.get("days");
285
		List<String> dates = (List<String>) plan.get("dates");
286
 
287
		// Build visit fingerprint to detect duplicates
288
		List<Integer> newVisitIds = new ArrayList<>();
289
		for (Map<String, Object> day : days) {
290
			List<Map<String, Object>> visits = (List<Map<String, Object>>) day.get("visits");
291
			if (visits != null) {
292
				for (Map<String, Object> v : visits) {
293
					newVisitIds.add(((Number) v.get("id")).intValue());
294
				}
295
			}
296
		}
297
		Collections.sort(newVisitIds);
298
 
299
		// Check existing beats for this auth user
300
		List<BeatPlanDay> existingBeatDays = beatPlanDayRepository.selectByAuthUserIdAndDateRange(
301
				authUserId, LocalDate.of(2000, 1, 1), LocalDate.of(2099, 12, 31));
302
		Set<String> existingGroups = existingBeatDays.stream()
303
				.map(BeatPlanDay::getPlanGroupId).filter(g -> g != null).collect(Collectors.toSet());
304
 
305
		for (String existingGroup : existingGroups) {
306
			List<BeatPlan> existingVisits = beatPlanRepository.selectByPlanGroupId(existingGroup);
307
			List<Integer> existingIds = existingVisits.stream()
308
					.map(BeatPlan::getFofoId).sorted().collect(Collectors.toList());
309
			if (existingIds.equals(newVisitIds)) {
310
				// Duplicate found — return existing planGroupId
311
				Map<String, Object> response = new HashMap<>();
312
				response.put("status", true);
313
				response.put("planGroupId", existingGroup);
314
				response.put("duplicate", true);
315
				response.put("message", "Beat already exists with same visits");
316
				return responseSender.ok(response);
317
			}
318
		}
319
 
320
		String planGroupId = UUID.randomUUID().toString();
321
		String beatName = plan.get("beatName") != null ? (String) plan.get("beatName") : "Beat";
322
		String beatColor = BEAT_COLORS[Math.abs(planGroupId.hashCode()) % BEAT_COLORS.length];
323
 
324
		for (int d = 0; d < days.size(); d++) {
325
			Map<String, Object> day = days.get(d);
326
			int dayNumber = d + 1;
327
			LocalDate planDate = (dates != null && d < dates.size() && dates.get(d) != null)
328
					? LocalDate.parse(dates.get(d), DateTimeFormatter.ISO_DATE)
329
					: null;
330
 
331
			// Save day-level info
332
			BeatPlanDay bpDay = new BeatPlanDay();
333
			bpDay.setPlanGroupId(planGroupId);
334
			bpDay.setAuthUserId(authUserId);
335
			bpDay.setDayNumber(dayNumber);
336
			bpDay.setPlanDate(planDate);
337
			bpDay.setBeatName(beatName);
338
			bpDay.setBeatColor(beatColor);
339
			bpDay.setStartLocationName((String) day.get("startLocationName"));
340
			bpDay.setStartLatitude((String) day.get("startLatitude"));
341
			bpDay.setStartLongitude((String) day.get("startLongitude"));
342
			bpDay.setEndAction((String) day.get("endAction"));
343
			bpDay.setStayLocationName((String) day.get("stayLocationName"));
344
			bpDay.setStayLatitude((String) day.get("stayLatitude"));
345
			bpDay.setStayLongitude((String) day.get("stayLongitude"));
346
			if (day.get("totalDistanceKm") != null) {
347
				bpDay.setTotalDistanceKm(((Number) day.get("totalDistanceKm")).doubleValue());
348
			}
349
			if (day.get("totalTimeMins") != null) {
350
				bpDay.setTotalTimeMins(((Number) day.get("totalTimeMins")).intValue());
351
			}
352
			bpDay.setCreatedBy(currentUser.getId());
353
			bpDay.setCreatedTimestamp(LocalDateTime.now());
354
			bpDay.setActive(true);
355
			beatPlanDayRepository.persist(bpDay);
356
 
357
			// Save visits for this day
358
			List<Map<String, Object>> visits = (List<Map<String, Object>>) day.get("visits");
359
			if (visits != null) {
360
				for (int i = 0; i < visits.size(); i++) {
361
					Map<String, Object> visit = visits.get(i);
362
					BeatPlan bp = new BeatPlan();
363
					bp.setAuthUserId(authUserId);
364
					bp.setFofoId(((Number) visit.get("id")).intValue());
365
					bp.setVisitType((String) visit.get("type"));
366
					bp.setDayNumber(dayNumber);
367
					bp.setPlanGroupId(planGroupId);
368
					bp.setPlanDate(planDate);
369
					bp.setSequenceOrder(i);
370
					bp.setCreatedBy(currentUser.getId());
371
					bp.setCreatedTimestamp(LocalDateTime.now());
372
					bp.setUpdatedTimestamp(LocalDateTime.now());
373
					bp.setActive(true);
374
					beatPlanRepository.persist(bp);
375
				}
376
			}
377
		}
378
 
379
		Map<String, Object> response = new HashMap<>();
380
		response.put("status", true);
381
		response.put("planGroupId", planGroupId);
382
		response.put("message", "Beat plan submitted successfully");
383
		return responseSender.ok(response);
384
	}
385
 
36632 ranu 386
	// ============ BULK UPLOAD ============
387
 
388
	@GetMapping(value = "/beatPlan/bulkUpload")
389
	public String bulkUploadPage(HttpServletRequest request, Model model) {
390
		return "beat-plan-bulk";
391
	}
392
 
393
	@GetMapping(value = "/beatPlan/downloadTemplate")
394
	public ResponseEntity<?> downloadTemplate() {
395
		String csv = "beat_name,auth_user_id,start_date,day_number,partner_codes\n";
396
		csv += "Jaipur East Route,280,2026-06-02,1,\"RJKAI1478,RJBUN1449,RJDEG1443\"\n";
397
		csv += ",280,,2,\"RJALR1362,RJBTR1388\"\n";
398
		csv += ",280,,3,\"RJRSD1518,RJSML356\"\n";
399
		csv += "Agra Circuit,145,2026-06-05,1,\"UPAGR101,UPAGR102\"\n";
400
 
401
		org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
402
		headers.add("Content-Disposition", "attachment; filename=beat_plan_template.csv");
403
		headers.add("Content-Type", "text/csv");
404
 
405
		return new ResponseEntity<>(csv, headers, org.springframework.http.HttpStatus.OK);
406
	}
407
 
408
	@PostMapping(value = "/beatPlan/bulkUploadProcess")
409
	public ResponseEntity<?> bulkUploadProcess(
410
			HttpServletRequest request,
411
			@RequestParam("file") org.springframework.web.multipart.MultipartFile file,
412
			@RequestParam(value = "includeSundays", defaultValue = "false") boolean includeSundays) throws Exception {
413
 
414
		LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);
415
		AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());
416
 
417
		java.io.Reader reader = new java.io.InputStreamReader(file.getInputStream());
418
		org.apache.commons.csv.CSVParser parser = new org.apache.commons.csv.CSVParser(reader,
419
				org.apache.commons.csv.CSVFormat.DEFAULT.withFirstRecordAsHeader().withTrim());
420
 
421
		// Each row = one day: beat_name, auth_user_id, start_date (only on day 1), day_number, partner_codes
422
		// First pass: collect all records, normalize beat_name
423
		List<org.apache.commons.csv.CSVRecord> allRecords = parser.getRecords();
424
		parser.close();
425
 
426
		// Track last beat_name per auth_user_id for blank name rows
427
		Map<String, String> lastBeatNameByUser = new HashMap<>();
428
 
429
		Map<String, List<org.apache.commons.csv.CSVRecord>> beatGroups = new LinkedHashMap<>();
430
		// Also store resolved beat names since CSVRecord is immutable
431
		Map<Long, String> resolvedBeatNames = new HashMap<>();
432
 
433
		for (org.apache.commons.csv.CSVRecord record : allRecords) {
434
			String authId = record.get("auth_user_id").trim();
435
			String rawName = record.get("beat_name").trim();
436
			// Normalize: collapse multiple spaces, trim
437
			String beatName = rawName.replaceAll("\\s+", " ").trim();
438
 
439
			// If blank, use last beat_name for this auth_user_id
440
			if (beatName.isEmpty()) {
441
				beatName = lastBeatNameByUser.getOrDefault(authId, "Beat");
442
			} else {
443
				lastBeatNameByUser.put(authId, beatName);
444
			}
445
 
446
			resolvedBeatNames.put(record.getRecordNumber(), beatName);
447
			String key = beatName + "|" + authId;
448
			beatGroups.computeIfAbsent(key, k -> new ArrayList<>()).add(record);
449
		}
450
 
451
		// Build FofoStore code → id lookup
452
		List<FofoStore> allStores = fofoStoreRepository.selectAll();
453
		Map<String, Integer> codeToId = new HashMap<>();
454
		for (FofoStore store : allStores) {
455
			codeToId.put(store.getCode(), store.getId());
456
		}
457
 
458
		// Load holidays for validation
459
		LocalDate holidayStart = LocalDate.now();
460
		LocalDate holidayEnd = holidayStart.plusMonths(6);
461
		List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(holidayStart, holidayEnd);
462
		Set<LocalDate> holidayDates = holidays.stream().map(PublicHolidays::getDate).collect(Collectors.toSet());
463
 
464
		int beatsCreated = 0;
465
		int errors = 0;
466
		List<String> errorMessages = new ArrayList<>();
467
 
468
		for (Map.Entry<String, List<org.apache.commons.csv.CSVRecord>> entry : beatGroups.entrySet()) {
469
			try {
470
				String[] keyParts = entry.getKey().split("\\|");
471
				String beatName = keyParts[0];
472
				int authUserId = Integer.parseInt(keyParts[1]);
473
				List<org.apache.commons.csv.CSVRecord> rows = entry.getValue();
474
 
475
				// Sort rows by day_number
476
				rows.sort((a, b) -> Integer.parseInt(a.get("day_number").trim()) - Integer.parseInt(b.get("day_number").trim()));
477
 
478
				// Get start_date from first row
479
				String startDateStr = rows.get(0).get("start_date").trim();
480
				LocalDate startDate = null;
481
				if (!startDateStr.isEmpty()) {
482
					startDate = LocalDate.parse(startDateStr, DateTimeFormatter.ISO_DATE);
483
				}
484
 
485
				// Validate start date
486
				if (startDate != null && startDate.isBefore(LocalDate.now())) {
487
					errorMessages.add("Beat '" + beatName + "': start_date " + startDate + " is in the past. Skipped.");
488
					errors++;
489
					continue;
490
				}
491
 
492
				// Auto-calculate dates for each day, skipping Sundays and holidays
493
				int totalDays = rows.size();
494
				List<LocalDate> scheduleDates = new ArrayList<>();
495
				if (startDate != null) {
496
					LocalDate d = startDate;
497
					while (scheduleDates.size() < totalDays) {
498
						boolean isSunday = d.getDayOfWeek() == DayOfWeek.SUNDAY;
499
						boolean isHoliday = holidayDates.contains(d);
500
 
501
						if (isHoliday) {
502
							// Always skip holidays
503
							errorMessages.add("Beat '" + beatName + "': skipping " + d + " (Holiday)");
504
						} else if (isSunday && !includeSundays) {
505
							// Skip Sunday only if not including Sundays
506
							errorMessages.add("Beat '" + beatName + "': skipping " + d + " (Sunday)");
507
						} else {
508
							scheduleDates.add(d);
509
							if (isSunday) {
510
								errorMessages.add("Beat '" + beatName + "': including Sunday " + d);
511
							}
512
						}
513
						d = d.plusDays(1);
514
					}
515
				}
516
 
517
				String planGroupId = UUID.randomUUID().toString();
518
				String beatColor = BEAT_COLORS[Math.abs(planGroupId.hashCode()) % BEAT_COLORS.length];
519
 
520
				// Get home location for this auth user
521
				AuthUserLocation homeLoc = authUserLocationRepository.selectLatestByAuthUserIdAndType(authUserId, "BASE");
522
				String homeName = homeLoc != null ? homeLoc.getLocationName() : "Home";
523
				String homeLat = homeLoc != null ? homeLoc.getLatitude() : null;
524
				String homeLng = homeLoc != null ? homeLoc.getLongitude() : null;
525
 
526
				int totalRows = rows.size();
527
 
528
				for (int rowIdx = 0; rowIdx < totalRows; rowIdx++) {
529
					org.apache.commons.csv.CSVRecord row = rows.get(rowIdx);
530
					int dayNumber = Integer.parseInt(row.get("day_number").trim());
531
					boolean isFirstDay = (rowIdx == 0);
532
					boolean isLastDay = (rowIdx == totalRows - 1);
533
 
534
					LocalDate planDate = (rowIdx < scheduleDates.size()) ? scheduleDates.get(rowIdx) : null;
535
 
536
					// Save BeatPlanDay
537
					BeatPlanDay bpDay = new BeatPlanDay();
538
					bpDay.setPlanGroupId(planGroupId);
539
					bpDay.setAuthUserId(authUserId);
540
					bpDay.setDayNumber(dayNumber);
541
					bpDay.setPlanDate(planDate);
542
					bpDay.setBeatName(beatName);
543
					bpDay.setBeatColor(beatColor);
544
 
545
					// Start location: Day 1 starts from home, other days start from home too (each day starts fresh)
546
					bpDay.setStartLocationName(homeName);
547
					bpDay.setStartLatitude(homeLat);
548
					bpDay.setStartLongitude(homeLng);
549
 
550
					// End action: last day = HOME (return home), other days = DAYBREAK
551
					if (isLastDay) {
552
						bpDay.setEndAction("HOME");
553
					} else {
554
						bpDay.setEndAction("DAYBREAK");
555
					}
556
 
557
					bpDay.setCreatedBy(currentUser.getId());
558
					bpDay.setCreatedTimestamp(LocalDateTime.now());
559
					bpDay.setActive(true);
560
					beatPlanDayRepository.persist(bpDay);
561
 
562
					// Parse comma-separated partner codes
563
					String partnerCodesStr = row.get("partner_codes").trim();
564
					String[] partnerCodes = partnerCodesStr.split(",");
565
 
566
					for (int i = 0; i < partnerCodes.length; i++) {
567
						String partnerCode = partnerCodes[i].trim();
568
						if (partnerCode.isEmpty()) continue;
569
 
570
						Integer fofoId = codeToId.get(partnerCode);
571
						if (fofoId == null) {
572
							errorMessages.add("Partner code not found: " + partnerCode + " in beat '" + beatName + "'");
573
							errors++;
574
							continue;
575
						}
576
 
577
						BeatPlan bp = new BeatPlan();
578
						bp.setAuthUserId(authUserId);
579
						bp.setFofoId(fofoId);
580
						bp.setVisitType("partner");
581
						bp.setDayNumber(dayNumber);
582
						bp.setPlanGroupId(planGroupId);
583
						bp.setPlanDate(planDate);
584
						bp.setSequenceOrder(i);
585
						bp.setCreatedBy(currentUser.getId());
586
						bp.setCreatedTimestamp(LocalDateTime.now());
587
						bp.setUpdatedTimestamp(LocalDateTime.now());
588
						bp.setActive(true);
589
						beatPlanRepository.persist(bp);
590
					}
591
				}
592
				beatsCreated++;
593
 
594
				// Log scheduled dates
595
				if (!scheduleDates.isEmpty()) {
596
					errorMessages.add("Beat '" + beatName + "' for user " + authUserId + " scheduled on: " +
597
							scheduleDates.stream().map(LocalDate::toString).collect(Collectors.joining(", ")));
598
				}
599
 
600
			} catch (Exception e) {
601
				errors++;
602
				errorMessages.add("Error processing beat: " + entry.getKey() + " - " + e.getMessage());
603
				LOGGER.error("Bulk upload error for {}: {}", entry.getKey(), e.getMessage());
604
			}
605
		}
606
 
607
		Map<String, Object> response = new HashMap<>();
608
		response.put("status", true);
609
		response.put("beatsCreated", beatsCreated);
610
		response.put("errors", errors);
611
		response.put("errorMessages", errorMessages);
612
		return responseSender.ok(response);
613
	}
614
 
36618 ranu 615
	// ============ CALENDAR ENDPOINTS ============
616
 
617
	@PostMapping(value = "/beatPlan/delete")
618
	public ResponseEntity<?> deleteBeat(@RequestParam String planGroupId) {
619
		beatPlanDayRepository.deleteByPlanGroupId(planGroupId);
620
		beatPlanRepository.deleteByPlanGroupId(planGroupId);
621
 
622
		Map<String, Object> response = new HashMap<>();
623
		response.put("status", true);
624
		response.put("message", "Beat deleted");
625
		return responseSender.ok(response);
626
	}
627
 
628
	@GetMapping(value = "/beatPlan/calendar")
629
	public ResponseEntity<?> getCalendar(
630
			@RequestParam int authUserId,
631
			@RequestParam String month) {
632
 
633
		YearMonth ym = YearMonth.parse(month);
634
		LocalDate startDate = ym.atDay(1);
635
		LocalDate endDate = ym.atEndOfMonth();
636
 
637
		// Get holidays
638
		List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(startDate, endDate);
639
		List<Map<String, String>> holidayList = holidays.stream().map(h -> {
640
			Map<String, String> m = new HashMap<>();
641
			m.put("date", h.getDate().toString());
642
			m.put("occasion", h.getOccasion());
643
			return m;
644
		}).collect(Collectors.toList());
645
 
36632 ranu 646
		// Get ALL active beats for this auth user (not just this month)
647
		List<BeatPlanDay> allBeatDays = beatPlanDayRepository.selectByAuthUserIdAndDateRange(
648
				authUserId, LocalDate.of(2000, 1, 1), LocalDate.of(2099, 12, 31));
36618 ranu 649
 
650
		// Group by planGroupId
36632 ranu 651
		Map<String, List<BeatPlanDay>> grouped = allBeatDays.stream()
36618 ranu 652
				.filter(d -> d.getPlanGroupId() != null)
653
				.collect(Collectors.groupingBy(BeatPlanDay::getPlanGroupId));
654
 
655
		LocalDate today = LocalDate.now();
656
		List<Map<String, Object>> scheduledBeats = new ArrayList<>();
657
 
658
		for (Map.Entry<String, List<BeatPlanDay>> entry : grouped.entrySet()) {
659
			List<BeatPlanDay> days = entry.getValue();
660
			BeatPlanDay first = days.get(0);
661
 
662
			// Determine status
663
			boolean allNullDates = days.stream().allMatch(d -> d.getPlanDate() == null);
664
			boolean hasToday = days.stream().anyMatch(d -> d.getPlanDate() != null && d.getPlanDate().equals(today));
665
			boolean allPast = !allNullDates && days.stream().filter(d -> d.getPlanDate() != null).allMatch(d -> d.getPlanDate().isBefore(today));
666
			boolean allFuture = !allNullDates && days.stream().filter(d -> d.getPlanDate() != null).allMatch(d -> d.getPlanDate().isAfter(today));
667
 
668
			String status;
669
			if (allNullDates) status = "unscheduled";
670
			else if (hasToday) status = "running";
671
			else if (allPast) status = "completed";
672
			else if (allFuture) status = "scheduled";
673
			else status = "scheduled"; // mixed past/future
674
 
675
			// Get visit counts per day
676
			Map<String, Object> beat = new HashMap<>();
677
			beat.put("planGroupId", entry.getKey());
678
			beat.put("beatName", first.getBeatName() != null ? first.getBeatName() : "Beat");
679
			beat.put("beatColor", first.getBeatColor() != null ? first.getBeatColor() : "#3498DB");
680
			beat.put("status", status);
681
 
682
			List<Map<String, Object>> dayInfoList = new ArrayList<>();
683
			for (BeatPlanDay d : days) {
684
				Map<String, Object> dayInfo = new HashMap<>();
685
				dayInfo.put("dayNumber", d.getDayNumber());
686
				dayInfo.put("planDate", d.getPlanDate() != null ? d.getPlanDate().toString() : null);
687
				dayInfo.put("totalKm", d.getTotalDistanceKm());
688
				dayInfo.put("totalMins", d.getTotalTimeMins());
689
 
690
				// Count visits for this day
691
				List<BeatPlan> visits = beatPlanRepository.selectByPlanGroupId(entry.getKey()).stream()
692
						.filter(bp -> bp.getDayNumber() == d.getDayNumber())
693
						.collect(Collectors.toList());
694
				dayInfo.put("visitCount", visits.size());
695
				dayInfoList.add(dayInfo);
696
			}
697
			beat.put("days", dayInfoList);
698
			scheduledBeats.add(beat);
699
		}
700
 
701
		// Build blocked dates (Sundays + holidays)
702
		Set<String> blockedDates = new HashSet<>();
703
		for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
704
			if (d.getDayOfWeek() == DayOfWeek.SUNDAY) {
705
				blockedDates.add(d.toString());
706
			}
707
		}
708
		for (PublicHolidays h : holidays) {
709
			blockedDates.add(h.getDate().toString());
710
		}
711
 
712
		Map<String, Object> response = new HashMap<>();
713
		response.put("holidays", holidayList);
714
		response.put("scheduledBeats", scheduledBeats);
715
		response.put("blockedDates", blockedDates);
716
 
717
		return responseSender.ok(response);
718
	}
719
 
720
	@PostMapping(value = "/beatPlan/scheduleOnCalendar")
721
	public ResponseEntity<?> scheduleOnCalendar(
722
			HttpServletRequest request,
723
			@RequestParam String planGroupId,
724
			@RequestParam String dates,
725
			@RequestParam(required = false) String beatName,
726
			@RequestParam(required = false) String beatColor) throws Exception {
727
 
728
		Gson gson = new Gson();
729
		List<String> dateList = gson.fromJson(dates, new TypeToken<List<String>>() {
730
		}.getType());
731
 
732
		List<BeatPlanDay> beatDays = beatPlanDayRepository.selectByPlanGroupId(planGroupId);
733
		if (beatDays.isEmpty()) {
734
			return responseSender.badRequest("No beat found for this plan group");
735
		}
736
 
737
		// Check running beat — can't reschedule
738
		LocalDate today = LocalDate.now();
739
		boolean isRunning = beatDays.stream().anyMatch(d -> d.getPlanDate() != null && d.getPlanDate().equals(today));
740
		if (isRunning) {
741
			return responseSender.badRequest("Cannot reschedule a running beat");
742
		}
743
 
744
		// Auto-assign color if not provided
745
		if (beatColor == null || beatColor.isEmpty()) {
746
			beatColor = BEAT_COLORS[Math.abs(planGroupId.hashCode()) % BEAT_COLORS.length];
747
		}
748
 
749
		// Update dates on beat_plan_day
750
		beatDays.sort(Comparator.comparingInt(BeatPlanDay::getDayNumber));
751
		for (int i = 0; i < beatDays.size() && i < dateList.size(); i++) {
752
			BeatPlanDay day = beatDays.get(i);
753
			day.setPlanDate(LocalDate.parse(dateList.get(i)));
754
			if (beatName != null) day.setBeatName(beatName);
755
			day.setBeatColor(beatColor);
756
			beatPlanDayRepository.persist(day);
757
		}
758
 
759
		// Update dates on beat_plan records too
760
		List<BeatPlan> beatPlans = beatPlanRepository.selectByPlanGroupId(planGroupId);
761
		for (BeatPlan bp : beatPlans) {
762
			for (int i = 0; i < beatDays.size() && i < dateList.size(); i++) {
763
				if (bp.getDayNumber() == beatDays.get(i).getDayNumber()) {
764
					bp.setPlanDate(LocalDate.parse(dateList.get(i)));
765
					beatPlanRepository.persist(bp);
766
				}
767
			}
768
		}
769
 
770
		Map<String, Object> response = new HashMap<>();
771
		response.put("status", true);
772
		response.put("message", "Beat scheduled successfully");
773
		return responseSender.ok(response);
774
	}
775
 
776
	@PostMapping(value = "/beatPlan/repeatBeat")
777
	public ResponseEntity<?> repeatBeat(
778
			HttpServletRequest request,
779
			@RequestParam String sourcePlanGroupId,
780
			@RequestParam int authUserId,
781
			@RequestParam String dates) throws Exception {
782
 
783
		LoginDetails loginDetails = cookiesProcessor.getCookiesObject(request);
784
		AuthUser currentUser = authRepository.selectByEmailOrMobile(loginDetails.getEmailId());
785
 
786
		Gson gson = new Gson();
787
		List<String> dateList = gson.fromJson(dates, new TypeToken<List<String>>() {
788
		}.getType());
789
 
790
		// Get source beat days and visits
791
		List<BeatPlanDay> sourceDays = beatPlanDayRepository.selectByPlanGroupId(sourcePlanGroupId);
792
		List<BeatPlan> sourceVisits = beatPlanRepository.selectByPlanGroupId(sourcePlanGroupId);
793
 
794
		if (sourceDays.isEmpty()) {
795
			return responseSender.badRequest("Source beat not found");
796
		}
797
 
798
		String newPlanGroupId = UUID.randomUUID().toString();
799
		String beatName = sourceDays.get(0).getBeatName();
800
		String beatColor = sourceDays.get(0).getBeatColor();
801
 
802
		// Copy days with new dates
803
		sourceDays.sort(Comparator.comparingInt(BeatPlanDay::getDayNumber));
804
		for (int i = 0; i < sourceDays.size() && i < dateList.size(); i++) {
805
			BeatPlanDay src = sourceDays.get(i);
806
			BeatPlanDay newDay = new BeatPlanDay();
807
			newDay.setPlanGroupId(newPlanGroupId);
808
			newDay.setAuthUserId(authUserId);
809
			newDay.setDayNumber(src.getDayNumber());
810
			newDay.setPlanDate(LocalDate.parse(dateList.get(i)));
811
			newDay.setBeatName(beatName);
812
			newDay.setBeatColor(beatColor);
813
			newDay.setStartLocationName(src.getStartLocationName());
814
			newDay.setStartLatitude(src.getStartLatitude());
815
			newDay.setStartLongitude(src.getStartLongitude());
816
			newDay.setEndAction(src.getEndAction());
817
			newDay.setStayLocationName(src.getStayLocationName());
818
			newDay.setStayLatitude(src.getStayLatitude());
819
			newDay.setStayLongitude(src.getStayLongitude());
820
			newDay.setTotalDistanceKm(src.getTotalDistanceKm());
821
			newDay.setTotalTimeMins(src.getTotalTimeMins());
822
			newDay.setCreatedBy(currentUser.getId());
823
			newDay.setCreatedTimestamp(LocalDateTime.now());
824
			newDay.setActive(true);
825
			beatPlanDayRepository.persist(newDay);
826
		}
827
 
828
		// Copy visits with new plan group and dates
829
		for (BeatPlan srcVisit : sourceVisits) {
830
			BeatPlan newVisit = new BeatPlan();
831
			newVisit.setAuthUserId(authUserId);
832
			newVisit.setFofoId(srcVisit.getFofoId());
833
			newVisit.setVisitType(srcVisit.getVisitType());
834
			newVisit.setDayNumber(srcVisit.getDayNumber());
835
			newVisit.setPlanGroupId(newPlanGroupId);
836
			newVisit.setSequenceOrder(srcVisit.getSequenceOrder());
837
			newVisit.setCreatedBy(currentUser.getId());
838
			newVisit.setCreatedTimestamp(LocalDateTime.now());
839
			newVisit.setUpdatedTimestamp(LocalDateTime.now());
840
			newVisit.setActive(true);
841
 
842
			// Set date from new dateList
843
			int dayIdx = srcVisit.getDayNumber() - 1;
844
			if (dayIdx >= 0 && dayIdx < dateList.size()) {
845
				newVisit.setPlanDate(LocalDate.parse(dateList.get(dayIdx)));
846
			}
847
			beatPlanRepository.persist(newVisit);
848
		}
849
 
850
		Map<String, Object> response = new HashMap<>();
851
		response.put("status", true);
852
		response.put("planGroupId", newPlanGroupId);
853
		response.put("message", "Beat repeated successfully");
854
		return responseSender.ok(response);
855
	}
856
 
857
	@GetMapping(value = "/beatPlan/availableSlots")
858
	public ResponseEntity<?> getAvailableSlots(
859
			@RequestParam int authUserId,
860
			@RequestParam String month,
861
			@RequestParam int daysNeeded) {
862
 
863
		YearMonth ym = YearMonth.parse(month);
864
		LocalDate startDate = ym.atDay(1);
865
		LocalDate endDate = ym.atEndOfMonth();
866
		LocalDate today = LocalDate.now();
867
 
868
		// Blocked: Sundays + holidays + already scheduled dates
869
		Set<LocalDate> blocked = new HashSet<>();
870
 
871
		for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
872
			if (d.getDayOfWeek() == DayOfWeek.SUNDAY) blocked.add(d);
873
			if (!d.isAfter(today)) blocked.add(d); // past dates blocked
874
		}
875
 
876
		List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(startDate, endDate);
877
		for (PublicHolidays h : holidays) blocked.add(h.getDate());
878
 
879
		List<BeatPlanDay> existingBeats = beatPlanDayRepository.selectByAuthUserIdAndDateRange(authUserId, startDate, endDate);
880
		for (BeatPlanDay bd : existingBeats) {
881
			if (bd.getPlanDate() != null) blocked.add(bd.getPlanDate());
882
		}
883
 
884
		// Find earliest available slots
885
		List<String> available = new ArrayList<>();
886
		for (LocalDate d = startDate.isAfter(today) ? startDate : today.plusDays(1);
887
			 !d.isAfter(endDate) && available.size() < daysNeeded;
888
			 d = d.plusDays(1)) {
889
			if (!blocked.contains(d)) {
890
				available.add(d.toString());
891
			}
892
		}
893
 
894
		Map<String, Object> response = new HashMap<>();
895
		response.put("suggestedDates", available);
896
		response.put("totalAvailable", available.size());
897
		return responseSender.ok(response);
898
	}
899
 
900
	// --- Sorting helpers ---
901
 
902
	private List<Map<String, Object>> sortByNearestNeighborFromStart(
903
			List<Map<String, Object>> partners, double startLat, double startLng) {
904
 
905
		List<Map<String, Object>> withCoords = new ArrayList<>();
906
		List<Map<String, Object>> withoutCoords = new ArrayList<>();
907
 
908
		for (Map<String, Object> p : partners) {
909
			if (hasValidCoords(p)) {
910
				withCoords.add(p);
911
			} else {
912
				withoutCoords.add(p);
913
			}
914
		}
915
 
916
		List<Map<String, Object>> sorted = new ArrayList<>();
917
		double currentLat = startLat;
918
		double currentLng = startLng;
919
 
920
		while (!withCoords.isEmpty()) {
921
			int nearestIdx = 0;
922
			double nearestDist = Double.MAX_VALUE;
923
			for (int i = 0; i < withCoords.size(); i++) {
924
				double lat = Double.parseDouble(withCoords.get(i).get("latitude").toString());
925
				double lng = Double.parseDouble(withCoords.get(i).get("longitude").toString());
926
				double dist = haversine(currentLat, currentLng, lat, lng);
927
				if (dist < nearestDist) {
928
					nearestDist = dist;
929
					nearestIdx = i;
930
				}
931
			}
932
			Map<String, Object> nearest = withCoords.remove(nearestIdx);
933
			sorted.add(nearest);
934
			currentLat = Double.parseDouble(nearest.get("latitude").toString());
935
			currentLng = Double.parseDouble(nearest.get("longitude").toString());
936
		}
937
 
938
		sorted.addAll(withoutCoords);
939
		return sorted;
940
	}
941
 
942
	private List<Map<String, Object>> sortByNearestNeighbor(List<Map<String, Object>> partners) {
943
		List<Map<String, Object>> withCoords = new ArrayList<>();
944
		List<Map<String, Object>> withoutCoords = new ArrayList<>();
945
 
946
		for (Map<String, Object> p : partners) {
947
			if (hasValidCoords(p)) {
948
				withCoords.add(p);
949
			} else {
950
				withoutCoords.add(p);
951
			}
952
		}
953
 
954
		List<Map<String, Object>> sorted = new ArrayList<>();
955
		if (!withCoords.isEmpty()) {
956
			sorted.add(withCoords.remove(0));
957
			while (!withCoords.isEmpty()) {
958
				Map<String, Object> last = sorted.get(sorted.size() - 1);
959
				double lastLat = Double.parseDouble(last.get("latitude").toString());
960
				double lastLng = Double.parseDouble(last.get("longitude").toString());
961
 
962
				int nearestIdx = 0;
963
				double nearestDist = Double.MAX_VALUE;
964
				for (int i = 0; i < withCoords.size(); i++) {
965
					double lat = Double.parseDouble(withCoords.get(i).get("latitude").toString());
966
					double lng = Double.parseDouble(withCoords.get(i).get("longitude").toString());
967
					double dist = haversine(lastLat, lastLng, lat, lng);
968
					if (dist < nearestDist) {
969
						nearestDist = dist;
970
						nearestIdx = i;
971
					}
972
				}
973
				sorted.add(withCoords.remove(nearestIdx));
974
			}
975
		}
976
		sorted.addAll(withoutCoords);
977
		return sorted;
978
	}
979
 
980
	private boolean hasValidCoords(Map<String, Object> p) {
981
		Object lat = p.get("latitude");
982
		Object lng = p.get("longitude");
983
		return lat != null && lng != null
984
				&& !lat.toString().isEmpty() && !lng.toString().isEmpty();
985
	}
986
 
987
	private double haversine(double lat1, double lng1, double lat2, double lng2) {
988
		double R = 6371;
989
		double dLat = Math.toRadians(lat2 - lat1);
990
		double dLng = Math.toRadians(lng2 - lng1);
991
		double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
992
				+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
993
				* Math.sin(dLng / 2) * Math.sin(dLng / 2);
994
		double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
995
		return R * c;
996
	}
997
}