Subversion Repositories SmartDukaan

Rev

Rev 36632 | Rev 36650 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

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