| Line 1781... |
Line 1781... |
| 1781 |
}
|
1781 |
}
|
| 1782 |
|
1782 |
|
| 1783 |
String beatColor = BEAT_COLORS[Math.abs(beatName.hashCode()) % BEAT_COLORS.length];
|
1783 |
String beatColor = BEAT_COLORS[Math.abs(beatName.hashCode()) % BEAT_COLORS.length];
|
| 1784 |
int totalDays = days.size();
|
1784 |
int totalDays = days.size();
|
| 1785 |
|
1785 |
|
| - |
|
1786 |
// One-beat-per-day guard: reject if any of the requested dates already
|
| - |
|
1787 |
// has a beat scheduled for this user.
|
| - |
|
1788 |
if (dates != null) {
|
| - |
|
1789 |
List<LocalDate> candidateDates = new ArrayList<>();
|
| - |
|
1790 |
for (String dStr : dates) {
|
| - |
|
1791 |
if (dStr != null && !dStr.isEmpty()) {
|
| - |
|
1792 |
try {
|
| - |
|
1793 |
candidateDates.add(LocalDate.parse(dStr, DateTimeFormatter.ISO_DATE));
|
| - |
|
1794 |
} catch (Exception ignored) {
|
| - |
|
1795 |
}
|
| - |
|
1796 |
}
|
| - |
|
1797 |
}
|
| - |
|
1798 |
Map<String, Object> conflict = findScheduleConflict(authUserId, candidateDates, 0);
|
| - |
|
1799 |
if (conflict != null) return responseSender.badRequest(scheduleConflictMessage(conflict));
|
| - |
|
1800 |
}
|
| - |
|
1801 |
|
| 1786 |
// Create Beat master
|
1802 |
// Create Beat master
|
| 1787 |
Beat beat = new Beat();
|
1803 |
Beat beat = new Beat();
|
| 1788 |
beat.setName(beatName);
|
1804 |
beat.setName(beatName);
|
| 1789 |
beat.setAuthUserId(authUserId);
|
1805 |
beat.setAuthUserId(authUserId);
|
| 1790 |
beat.setBeatColor(beatColor);
|
1806 |
beat.setBeatColor(beatColor);
|
| Line 2073... |
Line 2089... |
| 2073 |
|
2089 |
|
| 2074 |
LocalDate holidayStart = LocalDate.now();
|
2090 |
LocalDate holidayStart = LocalDate.now();
|
| 2075 |
List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(holidayStart, holidayStart.plusMonths(6));
|
2091 |
List<PublicHolidays> holidays = publicHolidaysRepository.selectAllBetweenDates(holidayStart, holidayStart.plusMonths(6));
|
| 2076 |
Set<LocalDate> holidayDates = holidays.stream().map(PublicHolidays::getDate).collect(Collectors.toSet());
|
2092 |
Set<LocalDate> holidayDates = holidays.stream().map(PublicHolidays::getDate).collect(Collectors.toSet());
|
| 2077 |
|
2093 |
|
| - |
|
2094 |
// =====================================================================
|
| - |
|
2095 |
// All-or-nothing import: validate every group first; only persist if
|
| 2078 |
int beatsCreated = 0, errors = 0;
|
2096 |
// the entire file passes. A single bad row blocks the whole upload
|
| - |
|
2097 |
// so the user can fix and re-upload without partial creations.
|
| - |
|
2098 |
// =====================================================================
|
| - |
|
2099 |
|
| 2079 |
List<String> errorMessages = new ArrayList<>();
|
2100 |
List<String> errorMessages = new ArrayList<>();
|
| - |
|
2101 |
List<ValidatedBulkBeat> ready = new ArrayList<>();
|
| 2080 |
|
2102 |
|
| - |
|
2103 |
// ----- Phase 1: validate every group, collect ALL errors -----
|
| 2081 |
for (BulkBeatGroup g : beatGroups.values()) {
|
2104 |
for (BulkBeatGroup g : beatGroups.values()) {
|
| - |
|
2105 |
String beatName = g.beatName;
|
| - |
|
2106 |
|
| - |
|
2107 |
int authUserId;
|
| 2082 |
try {
|
2108 |
try {
|
| 2083 |
String beatName = g.beatName;
|
- |
|
| 2084 |
int authUserId;
|
- |
|
| 2085 |
try {
|
- |
|
| 2086 |
authUserId = Integer.parseInt(g.authUserId);
|
2109 |
authUserId = Integer.parseInt(g.authUserId);
|
| 2087 |
} catch (Exception e) {
|
2110 |
} catch (Exception e) {
|
| 2088 |
errorMessages.add("Beat '" + beatName + "': invalid auth_user_id '" + g.authUserId + "'. Skipped.");
|
2111 |
errorMessages.add("Beat '" + beatName + "': invalid auth_user_id '" + g.authUserId + "'.");
|
| 2089 |
errors++;
|
- |
|
| 2090 |
continue;
|
2112 |
continue;
|
| 2091 |
}
|
2113 |
}
|
| 2092 |
|
2114 |
|
| - |
|
2115 |
LocalDate startDate;
|
| - |
|
2116 |
try {
|
| - |
|
2117 |
startDate = (g.startDate == null || g.startDate.isEmpty())
|
| 2093 |
LocalDate startDate = (g.startDate == null) ? null : LocalDate.parse(g.startDate, DateTimeFormatter.ISO_DATE);
|
2118 |
? null : LocalDate.parse(g.startDate, DateTimeFormatter.ISO_DATE);
|
| - |
|
2119 |
} catch (Exception e) {
|
| - |
|
2120 |
errorMessages.add("Beat '" + beatName + "': invalid start_date '" + g.startDate + "'.");
|
| - |
|
2121 |
continue;
|
| - |
|
2122 |
}
|
| 2094 |
if (startDate != null && startDate.isBefore(LocalDate.now())) {
|
2123 |
if (startDate != null && startDate.isBefore(LocalDate.now())) {
|
| 2095 |
errorMessages.add("Beat '" + beatName + "': start_date in past. Skipped.");
|
2124 |
errorMessages.add("Beat '" + beatName + "': start_date in past.");
|
| 2096 |
errors++;
|
- |
|
| 2097 |
continue;
|
2125 |
continue;
|
| 2098 |
}
|
2126 |
}
|
| 2099 |
|
2127 |
|
| 2100 |
List<Integer> sortedDays = new ArrayList<>(g.dayToPartners.keySet());
|
2128 |
List<Integer> sortedDays = new ArrayList<>(g.dayToPartners.keySet());
|
| 2101 |
Collections.sort(sortedDays);
|
2129 |
Collections.sort(sortedDays);
|
| 2102 |
|
2130 |
|
| 2103 |
List<LocalDate> scheduleDates = new ArrayList<>();
|
2131 |
List<LocalDate> scheduleDates = new ArrayList<>();
|
| 2104 |
if (startDate != null) {
|
2132 |
if (startDate != null) {
|
| 2105 |
LocalDate d = startDate;
|
2133 |
LocalDate d = startDate;
|
| 2106 |
while (scheduleDates.size() < sortedDays.size()) {
|
2134 |
while (scheduleDates.size() < sortedDays.size()) {
|
| 2107 |
if (holidayDates.contains(d) || (d.getDayOfWeek() == DayOfWeek.SUNDAY && !includeSundays)) {
|
2135 |
if (holidayDates.contains(d) || (d.getDayOfWeek() == DayOfWeek.SUNDAY && !includeSundays)) {
|
| 2108 |
d = d.plusDays(1);
|
- |
|
| 2109 |
continue;
|
- |
|
| 2110 |
}
|
- |
|
| 2111 |
scheduleDates.add(d);
|
- |
|
| 2112 |
d = d.plusDays(1);
|
2136 |
d = d.plusDays(1);
|
| - |
|
2137 |
continue;
|
| 2113 |
}
|
2138 |
}
|
| - |
|
2139 |
scheduleDates.add(d);
|
| - |
|
2140 |
d = d.plusDays(1);
|
| 2114 |
}
|
2141 |
}
|
| - |
|
2142 |
}
|
| 2115 |
|
2143 |
|
| 2116 |
// Duplicate check — ACTIVE beats only (soft-deleted names are reusable)
|
2144 |
// Duplicate beat-name check (ACTIVE only; soft-deleted names are reusable).
|
| 2117 |
boolean isDuplicate = beatRepository.selectActiveByAuthUserId(authUserId).stream()
|
2145 |
boolean isDuplicate = beatRepository.selectActiveByAuthUserId(authUserId).stream()
|
| 2118 |
.anyMatch(b -> b.getName() != null && beatName.equalsIgnoreCase(b.getName().trim()));
|
2146 |
.anyMatch(b -> b.getName() != null && beatName.equalsIgnoreCase(b.getName().trim()));
|
| 2119 |
if (isDuplicate) {
|
2147 |
if (isDuplicate) {
|
| 2120 |
errorMessages.add("Beat '" + beatName + "' already exists for user " + authUserId + ". Skipped.");
|
2148 |
errorMessages.add("Beat '" + beatName + "': already exists for user " + authUserId + ".");
|
| 2121 |
errors++;
|
- |
|
| 2122 |
continue;
|
2149 |
continue;
|
| 2123 |
}
|
2150 |
}
|
| 2124 |
|
2151 |
|
| 2125 |
String beatColor = BEAT_COLORS[Math.abs(beatName.hashCode()) % BEAT_COLORS.length];
|
2152 |
// One-beat-per-day guard (against existing beats).
|
| 2126 |
AuthUserLocation homeLoc = authUserLocationRepository.selectDefaultByAuthUserIdAndType(authUserId, "BASE");
|
2153 |
Map<String, Object> bulkConflict = findScheduleConflict(authUserId, scheduleDates, 0);
|
| 2127 |
|
- |
|
| 2128 |
Beat beat = new Beat();
|
2154 |
if (bulkConflict != null) {
|
| 2129 |
beat.setName(beatName);
|
- |
|
| 2130 |
beat.setAuthUserId(authUserId);
|
- |
|
| 2131 |
beat.setBeatColor(beatColor);
|
- |
|
| 2132 |
beat.setTotalDays(sortedDays.size());
|
- |
|
| 2133 |
beat.setStartLocationName(homeLoc != null ? homeLoc.getLocationName() : "Home");
|
2155 |
errorMessages.add("Beat '" + beatName + "': " + scheduleConflictMessage(bulkConflict));
|
| 2134 |
beat.setStartLatitude(homeLoc != null ? homeLoc.getLatitude() : null);
|
- |
|
| 2135 |
beat.setStartLongitude(homeLoc != null ? homeLoc.getLongitude() : null);
|
- |
|
| 2136 |
beat.setActive(true);
|
2156 |
continue;
|
| 2137 |
beat.setCreatedBy(currentUser.getId());
|
- |
|
| 2138 |
beat.setCreatedTimestamp(LocalDateTime.now());
|
- |
|
| 2139 |
beatRepository.persist(beat);
|
- |
|
| 2140 |
|
2157 |
}
|
| 2141 |
LocalDate bulkEndDate = scheduleDates.isEmpty() ? null : scheduleDates.get(scheduleDates.size() - 1);
|
- |
|
| 2142 |
|
2158 |
|
| 2143 |
for (int dayIdx = 0; dayIdx < sortedDays.size(); dayIdx++) {
|
2159 |
// Validate partner codes upfront so we don't half-persist.
|
| 2144 |
int dayNumber = sortedDays.get(dayIdx);
|
- |
|
| 2145 |
LocalDate planDate = (dayIdx < scheduleDates.size()) ? scheduleDates.get(dayIdx) : null;
|
- |
|
| 2146 |
|
- |
|
| 2147 |
BeatSchedule schedule = new BeatSchedule();
|
2160 |
List<String> badCodes = new ArrayList<>();
|
| 2148 |
schedule.setBeatId(beat.getId());
|
- |
|
| 2149 |
schedule.setStartDate(planDate != null ? planDate : LocalDate.of(9999, 12, 31));
|
- |
|
| 2150 |
schedule.setEndDate(bulkEndDate);
|
- |
|
| 2151 |
schedule.setDayNumber(dayNumber);
|
- |
|
| 2152 |
schedule.setEndAction(dayIdx == sortedDays.size() - 1 ? "HOME" : "DAYBREAK");
|
- |
|
| 2153 |
schedule.setCreatedTimestamp(LocalDateTime.now());
|
- |
|
| 2154 |
beatScheduleRepository.persist(schedule);
|
- |
|
| 2155 |
|
- |
|
| 2156 |
List<BulkPartner> partners = g.dayToPartners.get(dayNumber);
|
2161 |
for (List<BulkPartner> ps : g.dayToPartners.values()) {
|
| 2157 |
// Sort by explicit sequence_order when present, else by row order
|
- |
|
| 2158 |
partners.sort((a, b) -> {
|
- |
|
| 2159 |
if (a.seq >= 0 && b.seq >= 0) return Integer.compare(a.seq, b.seq);
|
- |
|
| 2160 |
if (a.seq >= 0) return -1;
|
- |
|
| 2161 |
if (b.seq >= 0) return 1;
|
- |
|
| 2162 |
return Integer.compare(a.rowNum, b.rowNum);
|
- |
|
| 2163 |
});
|
- |
|
| 2164 |
|
- |
|
| 2165 |
int autoSeq = 0;
|
- |
|
| 2166 |
for (BulkPartner p : partners) {
|
2162 |
for (BulkPartner p : ps) {
|
| 2167 |
Integer fofoId = codeToId.get(p.code);
|
2163 |
if (!codeToId.containsKey(p.code)) {
|
| 2168 |
if (fofoId == null) {
|
- |
|
| 2169 |
errorMessages.add("Row " + p.rowNum + ": code not found '" + p.code + "'");
|
2164 |
badCodes.add(p.code + " (row " + p.rowNum + ")");
|
| 2170 |
errors++;
|
- |
|
| 2171 |
continue;
|
- |
|
| 2172 |
}
|
- |
|
| 2173 |
BeatRoute route = new BeatRoute();
|
- |
|
| 2174 |
route.setBeatId(beat.getId());
|
- |
|
| 2175 |
route.setFofoId(fofoId);
|
- |
|
| 2176 |
route.setSequenceOrder(p.seq >= 0 ? p.seq : autoSeq);
|
- |
|
| 2177 |
route.setDayNumber(dayNumber);
|
- |
|
| 2178 |
route.setActive(true);
|
- |
|
| 2179 |
beatRouteRepository.persist(route);
|
- |
|
| 2180 |
autoSeq++;
|
- |
|
| 2181 |
}
|
2165 |
}
|
| 2182 |
}
|
2166 |
}
|
| 2183 |
beatsCreated++;
|
- |
|
| 2184 |
} catch (Exception e) {
|
- |
|
| 2185 |
errors++;
|
- |
|
| 2186 |
errorMessages.add("Error: " + g.beatName + " - " + e.getMessage());
|
- |
|
| 2187 |
}
|
2167 |
}
|
| - |
|
2168 |
if (!badCodes.isEmpty()) {
|
| - |
|
2169 |
errorMessages.add("Beat '" + beatName + "': unknown partner code(s) — " + String.join(", ", badCodes) + ".");
|
| - |
|
2170 |
continue;
|
| - |
|
2171 |
}
|
| - |
|
2172 |
|
| - |
|
2173 |
ready.add(new ValidatedBulkBeat(g, authUserId, sortedDays, scheduleDates));
|
| - |
|
2174 |
}
|
| - |
|
2175 |
|
| - |
|
2176 |
// Intra-file conflict: two beats in the same upload requesting the same
|
| - |
|
2177 |
// user + date. Caught here so the user fixes the file before re-uploading.
|
| - |
|
2178 |
Set<String> seenUserDates = new HashSet<>();
|
| - |
|
2179 |
for (ValidatedBulkBeat v : ready) {
|
| - |
|
2180 |
for (LocalDate sd : v.scheduleDates) {
|
| - |
|
2181 |
String key = v.authUserId + "|" + sd;
|
| - |
|
2182 |
if (!seenUserDates.add(key)) {
|
| - |
|
2183 |
errorMessages.add("Beat '" + v.g.beatName + "': date " + sd + " is also claimed by another beat in this file for the same user.");
|
| - |
|
2184 |
break;
|
| - |
|
2185 |
}
|
| - |
|
2186 |
}
|
| - |
|
2187 |
}
|
| - |
|
2188 |
|
| - |
|
2189 |
// All-or-nothing: any error → return without persisting anything.
|
| - |
|
2190 |
if (!errorMessages.isEmpty()) {
|
| - |
|
2191 |
Map<String, Object> response = new HashMap<>();
|
| - |
|
2192 |
response.put("status", false);
|
| - |
|
2193 |
response.put("beatsCreated", 0);
|
| - |
|
2194 |
response.put("errors", errorMessages.size());
|
| - |
|
2195 |
response.put("errorMessages", errorMessages);
|
| - |
|
2196 |
response.put("message", "No beats created. Fix the issues below and re-upload the file.");
|
| - |
|
2197 |
return responseSender.ok(response);
|
| - |
|
2198 |
}
|
| - |
|
2199 |
|
| - |
|
2200 |
// ----- Phase 2: persist (only reached when every row was clean) -----
|
| - |
|
2201 |
int beatsCreated = 0;
|
| - |
|
2202 |
for (ValidatedBulkBeat v : ready) {
|
| - |
|
2203 |
BulkBeatGroup g = v.g;
|
| - |
|
2204 |
String beatName = g.beatName;
|
| - |
|
2205 |
int authUserId = v.authUserId;
|
| - |
|
2206 |
List<Integer> sortedDays = v.sortedDays;
|
| - |
|
2207 |
List<LocalDate> scheduleDates = v.scheduleDates;
|
| - |
|
2208 |
|
| - |
|
2209 |
String beatColor = BEAT_COLORS[Math.abs(beatName.hashCode()) % BEAT_COLORS.length];
|
| - |
|
2210 |
AuthUserLocation homeLoc = authUserLocationRepository.selectDefaultByAuthUserIdAndType(authUserId, "BASE");
|
| - |
|
2211 |
|
| - |
|
2212 |
Beat beat = new Beat();
|
| - |
|
2213 |
beat.setName(beatName);
|
| - |
|
2214 |
beat.setAuthUserId(authUserId);
|
| - |
|
2215 |
beat.setBeatColor(beatColor);
|
| - |
|
2216 |
beat.setTotalDays(sortedDays.size());
|
| - |
|
2217 |
beat.setStartLocationName(homeLoc != null ? homeLoc.getLocationName() : "Home");
|
| - |
|
2218 |
beat.setStartLatitude(homeLoc != null ? homeLoc.getLatitude() : null);
|
| - |
|
2219 |
beat.setStartLongitude(homeLoc != null ? homeLoc.getLongitude() : null);
|
| - |
|
2220 |
beat.setActive(true);
|
| - |
|
2221 |
beat.setCreatedBy(currentUser.getId());
|
| - |
|
2222 |
beat.setCreatedTimestamp(LocalDateTime.now());
|
| - |
|
2223 |
beatRepository.persist(beat);
|
| - |
|
2224 |
|
| - |
|
2225 |
LocalDate bulkEndDate = scheduleDates.isEmpty() ? null : scheduleDates.get(scheduleDates.size() - 1);
|
| - |
|
2226 |
|
| - |
|
2227 |
for (int dayIdx = 0; dayIdx < sortedDays.size(); dayIdx++) {
|
| - |
|
2228 |
int dayNumber = sortedDays.get(dayIdx);
|
| - |
|
2229 |
LocalDate planDate = (dayIdx < scheduleDates.size()) ? scheduleDates.get(dayIdx) : null;
|
| - |
|
2230 |
|
| - |
|
2231 |
BeatSchedule schedule = new BeatSchedule();
|
| - |
|
2232 |
schedule.setBeatId(beat.getId());
|
| - |
|
2233 |
schedule.setStartDate(planDate != null ? planDate : LocalDate.of(9999, 12, 31));
|
| - |
|
2234 |
schedule.setEndDate(bulkEndDate);
|
| - |
|
2235 |
schedule.setDayNumber(dayNumber);
|
| - |
|
2236 |
schedule.setEndAction(dayIdx == sortedDays.size() - 1 ? "HOME" : "DAYBREAK");
|
| - |
|
2237 |
schedule.setCreatedTimestamp(LocalDateTime.now());
|
| - |
|
2238 |
beatScheduleRepository.persist(schedule);
|
| - |
|
2239 |
|
| - |
|
2240 |
List<BulkPartner> partners = g.dayToPartners.get(dayNumber);
|
| - |
|
2241 |
partners.sort((a, b) -> {
|
| - |
|
2242 |
if (a.seq >= 0 && b.seq >= 0) return Integer.compare(a.seq, b.seq);
|
| - |
|
2243 |
if (a.seq >= 0) return -1;
|
| - |
|
2244 |
if (b.seq >= 0) return 1;
|
| - |
|
2245 |
return Integer.compare(a.rowNum, b.rowNum);
|
| - |
|
2246 |
});
|
| - |
|
2247 |
|
| - |
|
2248 |
int autoSeq = 0;
|
| - |
|
2249 |
for (BulkPartner p : partners) {
|
| - |
|
2250 |
Integer fofoId = codeToId.get(p.code);
|
| - |
|
2251 |
// Codes were validated in Phase 1, this is just a safety net.
|
| - |
|
2252 |
if (fofoId == null) continue;
|
| - |
|
2253 |
BeatRoute route = new BeatRoute();
|
| - |
|
2254 |
route.setBeatId(beat.getId());
|
| - |
|
2255 |
route.setFofoId(fofoId);
|
| - |
|
2256 |
route.setSequenceOrder(p.seq >= 0 ? p.seq : autoSeq);
|
| - |
|
2257 |
route.setDayNumber(dayNumber);
|
| - |
|
2258 |
route.setActive(true);
|
| - |
|
2259 |
beatRouteRepository.persist(route);
|
| - |
|
2260 |
autoSeq++;
|
| - |
|
2261 |
}
|
| - |
|
2262 |
}
|
| - |
|
2263 |
beatsCreated++;
|
| 2188 |
}
|
2264 |
}
|
| 2189 |
|
2265 |
|
| 2190 |
Map<String, Object> response = new HashMap<>();
|
2266 |
Map<String, Object> response = new HashMap<>();
|
| 2191 |
response.put("status", true);
|
2267 |
response.put("status", true);
|
| 2192 |
response.put("beatsCreated", beatsCreated);
|
2268 |
response.put("beatsCreated", beatsCreated);
|
| 2193 |
response.put("errors", errors);
|
2269 |
response.put("errors", 0);
|
| 2194 |
response.put("errorMessages", errorMessages);
|
2270 |
response.put("errorMessages", errorMessages);
|
| - |
|
2271 |
response.put("message", beatsCreated + " beat(s) created.");
|
| - |
|
2272 |
return responseSender.ok(response);
|
| - |
|
2273 |
}
|
| - |
|
2274 |
|
| - |
|
2275 |
// Move a beat from one date to another — used by calendar drag-and-drop.
|
| - |
|
2276 |
// Behaviour: if the target date already has ANOTHER beat scheduled (for the
|
| - |
|
2277 |
// same sales user), the two schedules swap — the other beat slides onto
|
| - |
|
2278 |
// the source date. If the target date is empty, the source date becomes empty.
|
| - |
|
2279 |
@PostMapping(value = "/beatPlan/moveScheduleDate")
|
| - |
|
2280 |
public ResponseEntity<?> moveScheduleDate(
|
| - |
|
2281 |
@RequestParam String planGroupId,
|
| - |
|
2282 |
@RequestParam String fromDate,
|
| - |
|
2283 |
@RequestParam String toDate) {
|
| - |
|
2284 |
int beatId = Integer.parseInt(planGroupId);
|
| - |
|
2285 |
LocalDate from = LocalDate.parse(fromDate);
|
| - |
|
2286 |
LocalDate to = LocalDate.parse(toDate);
|
| - |
|
2287 |
|
| - |
|
2288 |
if (from.equals(to)) {
|
| - |
|
2289 |
Map<String, Object> ok = new HashMap<>();
|
| - |
|
2290 |
ok.put("status", true);
|
| - |
|
2291 |
ok.put("message", "Same date — no change");
|
| - |
|
2292 |
return responseSender.ok(ok);
|
| - |
|
2293 |
}
|
| - |
|
2294 |
|
| - |
|
2295 |
// Today is the live/running slot — a beat already running today can't be
|
| - |
|
2296 |
// bumped, and a future beat can't be moved onto today.
|
| - |
|
2297 |
LocalDate today = LocalDate.now();
|
| - |
|
2298 |
if (to.equals(today) || from.equals(today) || from.isBefore(today) || to.isBefore(today)) {
|
| - |
|
2299 |
return responseSender.badRequest("Cannot move to or from today / a past date — today's beat is live. Use future dates only.");
|
| - |
|
2300 |
}
|
| - |
|
2301 |
|
| - |
|
2302 |
Beat beat = beatRepository.selectById(beatId);
|
| - |
|
2303 |
if (beat == null) return responseSender.badRequest("Beat not found");
|
| - |
|
2304 |
|
| - |
|
2305 |
List<BeatSchedule> schedules = beatScheduleRepository.selectByBeatId(beatId);
|
| - |
|
2306 |
|
| - |
|
2307 |
// Reject if THIS beat already has a different schedule row on the target date
|
| - |
|
2308 |
// (it would create two schedule rows of the same beat on one day).
|
| - |
|
2309 |
boolean selfConflict = schedules.stream()
|
| - |
|
2310 |
.anyMatch(s -> s.getStartDate() != null && s.getStartDate().equals(to));
|
| - |
|
2311 |
if (selfConflict) return responseSender.badRequest("Beat is already scheduled on " + toDate);
|
| - |
|
2312 |
|
| - |
|
2313 |
BeatSchedule match = schedules.stream()
|
| - |
|
2314 |
.filter(s -> s.getStartDate() != null && s.getStartDate().equals(from))
|
| - |
|
2315 |
.findFirst().orElse(null);
|
| - |
|
2316 |
if (match == null) return responseSender.badRequest("No schedule found for " + fromDate);
|
| - |
|
2317 |
|
| - |
|
2318 |
// Look for ANY OTHER beat (same sales user) whose schedule sits on the target
|
| - |
|
2319 |
// date — if found we'll swap it onto the source date.
|
| - |
|
2320 |
BeatSchedule otherSchedule = null;
|
| - |
|
2321 |
List<BeatSchedule> otherSchedules = null;
|
| - |
|
2322 |
Beat otherBeat = null;
|
| - |
|
2323 |
for (Beat ub : beatRepository.selectActiveByAuthUserId(beat.getAuthUserId())) {
|
| - |
|
2324 |
if (ub.getId() == beatId) continue;
|
| - |
|
2325 |
List<BeatSchedule> ubSchedules = beatScheduleRepository.selectByBeatId(ub.getId());
|
| - |
|
2326 |
BeatSchedule hit = ubSchedules.stream()
|
| - |
|
2327 |
.filter(s -> s.getStartDate() != null && s.getStartDate().equals(to))
|
| - |
|
2328 |
.findFirst().orElse(null);
|
| - |
|
2329 |
if (hit != null) {
|
| - |
|
2330 |
otherSchedule = hit;
|
| - |
|
2331 |
otherSchedules = ubSchedules;
|
| - |
|
2332 |
otherBeat = ub;
|
| - |
|
2333 |
break;
|
| - |
|
2334 |
}
|
| - |
|
2335 |
}
|
| - |
|
2336 |
|
| - |
|
2337 |
// Move the dragged beat onto the target date.
|
| - |
|
2338 |
match.setStartDate(to);
|
| - |
|
2339 |
// If another beat occupied the target, slide it onto the source date (swap).
|
| - |
|
2340 |
if (otherSchedule != null) {
|
| - |
|
2341 |
otherSchedule.setStartDate(from);
|
| - |
|
2342 |
}
|
| - |
|
2343 |
|
| - |
|
2344 |
// Recompute endDate as the max across each affected beat's schedules so
|
| - |
|
2345 |
// the [startDate, endDate] envelope stays consistent.
|
| - |
|
2346 |
recomputeEndDate(schedules);
|
| - |
|
2347 |
if (otherSchedules != null) {
|
| - |
|
2348 |
recomputeEndDate(otherSchedules);
|
| - |
|
2349 |
}
|
| - |
|
2350 |
|
| - |
|
2351 |
Map<String, Object> response = new HashMap<>();
|
| - |
|
2352 |
response.put("status", true);
|
| - |
|
2353 |
response.put("message", otherBeat != null
|
| - |
|
2354 |
? "Swapped with \"" + (otherBeat.getName() != null ? otherBeat.getName() : "beat") + "\" on " + toDate
|
| - |
|
2355 |
: "Moved from " + fromDate + " to " + toDate);
|
| - |
|
2356 |
response.put("swapped", otherBeat != null);
|
| 2195 |
return responseSender.ok(response);
|
2357 |
return responseSender.ok(response);
|
| 2196 |
}
|
2358 |
}
|
| 2197 |
|
2359 |
|
| 2198 |
private static class BulkBeatGroup {
|
2360 |
private static class BulkBeatGroup {
|
| 2199 |
final String beatName;
|
2361 |
final String beatName;
|
| Line 2293... |
Line 2455... |
| 2293 |
response.put("status", true);
|
2455 |
response.put("status", true);
|
| 2294 |
response.put("message", "Unscheduled from " + date);
|
2456 |
response.put("message", "Unscheduled from " + date);
|
| 2295 |
return responseSender.ok(response);
|
2457 |
return responseSender.ok(response);
|
| 2296 |
}
|
2458 |
}
|
| 2297 |
|
2459 |
|
| 2298 |
// Move a beat from one date to another — used by calendar drag-and-drop.
|
- |
|
| 2299 |
// Just updates the start_date of the matching beat_schedule row.
|
2460 |
private void recomputeEndDate(List<BeatSchedule> schedules) {
|
| 2300 |
@PostMapping(value = "/beatPlan/moveScheduleDate")
|
2461 |
LocalDate newEnd = schedules.stream()
|
| 2301 |
public ResponseEntity<?> moveScheduleDate(
|
2462 |
.map(BeatSchedule::getStartDate)
|
| 2302 |
@RequestParam String planGroupId,
|
2463 |
.filter(d -> d != null && d.getYear() != 9999)
|
| 2303 |
@RequestParam String fromDate,
|
2464 |
.max(LocalDate::compareTo).orElse(null);
|
| 2304 |
@RequestParam String toDate) {
|
2465 |
if (newEnd == null) return;
|
| 2305 |
int beatId = Integer.parseInt(planGroupId);
|
2466 |
for (BeatSchedule s : schedules) {
|
| 2306 |
LocalDate from = LocalDate.parse(fromDate);
|
2467 |
if (s.getStartDate() != null && s.getStartDate().getYear() != 9999) {
|
| 2307 |
LocalDate to = LocalDate.parse(toDate);
|
2468 |
s.setEndDate(newEnd);
|
| - |
|
2469 |
}
|
| - |
|
2470 |
}
|
| - |
|
2471 |
}
|
| 2308 |
|
2472 |
|
| - |
|
2473 |
/**
|
| - |
|
2474 |
* Per-user one-beat-per-day guard. Walks every active beat the sales user
|
| - |
|
2475 |
* already has and looks for a schedule row on any of the candidate dates.
|
| - |
|
2476 |
* Returns null if the candidate dates are clear, otherwise a {date,beatName}
|
| - |
|
2477 |
* map describing the first collision so the caller can surface a clean error.
|
| - |
|
2478 |
* Pass `excludeBeatId` so callers that are re-scheduling an existing beat
|
| - |
|
2479 |
* don't trip on their own pre-existing schedule rows; pass 0 for new beats.
|
| - |
|
2480 |
*/
|
| - |
|
2481 |
private Map<String, Object> findScheduleConflict(int authUserId, java.util.Collection<LocalDate> candidates, int excludeBeatId) {
|
| - |
|
2482 |
if (candidates == null || candidates.isEmpty()) return null;
|
| - |
|
2483 |
Set<LocalDate> ds = new HashSet<>();
|
| - |
|
2484 |
for (LocalDate d : candidates) {
|
| - |
|
2485 |
if (d != null && d.getYear() != 9999) ds.add(d);
|
| - |
|
2486 |
}
|
| 2309 |
if (from.equals(to)) {
|
2487 |
if (ds.isEmpty()) return null;
|
| - |
|
2488 |
for (Beat ub : beatRepository.selectActiveByAuthUserId(authUserId)) {
|
| - |
|
2489 |
if (ub.getId() == excludeBeatId) continue;
|
| - |
|
2490 |
for (BeatSchedule s : beatScheduleRepository.selectByBeatId(ub.getId())) {
|
| - |
|
2491 |
if (s.getStartDate() != null && ds.contains(s.getStartDate())) {
|
| 2310 |
Map<String, Object> ok = new HashMap<>();
|
2492 |
Map<String, Object> conflict = new HashMap<>();
|
| 2311 |
ok.put("status", true);
|
2493 |
conflict.put("date", s.getStartDate().toString());
|
| 2312 |
ok.put("message", "Same date — no change");
|
2494 |
conflict.put("beatName", ub.getName() != null ? ub.getName() : "Beat #" + ub.getId());
|
| 2313 |
return responseSender.ok(ok);
|
2495 |
return conflict;
|
| - |
|
2496 |
}
|
| - |
|
2497 |
}
|
| 2314 |
}
|
2498 |
}
|
| - |
|
2499 |
return null;
|
| - |
|
2500 |
}
|
| 2315 |
|
2501 |
|
| - |
|
2502 |
private String scheduleConflictMessage(Map<String, Object> conflict) {
|
| 2316 |
Beat beat = beatRepository.selectById(beatId);
|
2503 |
return "Cannot schedule on " + conflict.get("date")
|
| 2317 |
if (beat == null) return responseSender.badRequest("Beat not found");
|
2504 |
+ " — \"" + conflict.get("beatName") + "\" is already scheduled for this user on that day.";
|
| - |
|
2505 |
}
|
| 2318 |
|
2506 |
|
| - |
|
2507 |
@PostMapping(value = "/beatPlan/scheduleOnCalendar")
|
| - |
|
2508 |
public ResponseEntity<?> scheduleOnCalendar(
|
| - |
|
2509 |
HttpServletRequest request,
|
| - |
|
2510 |
@RequestParam String planGroupId,
|
| - |
|
2511 |
@RequestParam String dates,
|
| - |
|
2512 |
@RequestParam(required = false) String beatName,
|
| 2319 |
List<BeatSchedule> schedules = beatScheduleRepository.selectByBeatId(beatId);
|
2513 |
@RequestParam(required = false) String beatColor) throws Exception {
|
| 2320 |
|
2514 |
|
| 2321 |
// Reject if the beat is already on the target date
|
2515 |
int beatId = Integer.parseInt(planGroupId);
|
| 2322 |
boolean conflict = schedules.stream()
|
2516 |
Gson gson = new Gson();
|
| 2323 |
.anyMatch(s -> s.getStartDate() != null && s.getStartDate().equals(to));
|
2517 |
List<String> dateList = gson.fromJson(dates, new TypeToken<List<String>>() {
|
| 2324 |
if (conflict) return responseSender.badRequest("Beat is already scheduled on " + toDate);
|
2518 |
}.getType());
|
| 2325 |
|
2519 |
|
| 2326 |
BeatSchedule match = schedules.stream()
|
2520 |
Beat beat = beatRepository.selectById(beatId);
|
| 2327 |
.filter(s -> s.getStartDate() != null && s.getStartDate().equals(from))
|
- |
|
| 2328 |
.findFirst().orElse(null);
|
- |
|
| 2329 |
if (match == null) return responseSender.badRequest("No schedule found for " + fromDate);
|
2521 |
if (beat == null) return responseSender.badRequest("Beat not found");
|
| 2330 |
|
2522 |
|
| 2331 |
match.setStartDate(to);
|
2523 |
if (beatName != null) beat.setName(beatName);
|
| 2332 |
// Recompute endDate as the max across all (post-update) schedules
|
2524 |
if (beatColor != null && !beatColor.isEmpty()) beat.setBeatColor(beatColor);
|
| 2333 |
LocalDate newEnd = schedules.stream()
|
- |
|
| - |
|
2525 |
|
| 2334 |
.map(s -> s == match ? to : s.getStartDate())
|
2526 |
// One-beat-per-day guard: reject if any of the requested dates already
|
| 2335 |
.filter(d -> d != null && d.getYear() != 9999)
|
2527 |
// has another beat scheduled for this user (excluding this beat itself).
|
| 2336 |
.max(LocalDate::compareTo).orElse(to);
|
2528 |
List<LocalDate> requested = new ArrayList<>();
|
| 2337 |
for (BeatSchedule s : schedules) {
|
2529 |
for (String s : dateList) {
|
| - |
|
2530 |
try {
|
| 2338 |
if (s.getStartDate() != null && s.getStartDate().getYear() != 9999) {
|
2531 |
requested.add(LocalDate.parse(s));
|
| 2339 |
s.setEndDate(newEnd);
|
2532 |
} catch (Exception ignored) {
|
| 2340 |
}
|
2533 |
}
|
| 2341 |
}
|
2534 |
}
|
| - |
|
2535 |
Map<String, Object> conflict = findScheduleConflict(beat.getAuthUserId(), requested, beatId);
|
| - |
|
2536 |
if (conflict != null) return responseSender.badRequest(scheduleConflictMessage(conflict));
|
| - |
|
2537 |
|
| - |
|
2538 |
// Delete old schedules and create new
|
| - |
|
2539 |
beatScheduleRepository.deleteByBeatId(beatId);
|
| - |
|
2540 |
LocalDate schEndDate = dateList.isEmpty() ? null : LocalDate.parse(dateList.get(dateList.size() - 1));
|
| - |
|
2541 |
for (int i = 0; i < dateList.size() && i < beat.getTotalDays(); i++) {
|
| - |
|
2542 |
int dayNumber = i + 1;
|
| - |
|
2543 |
String endAction = (i == dateList.size() - 1) ? "HOME" : "DAYBREAK";
|
| - |
|
2544 |
BeatSchedule schedule = new BeatSchedule();
|
| - |
|
2545 |
schedule.setBeatId(beatId);
|
| - |
|
2546 |
schedule.setStartDate(LocalDate.parse(dateList.get(i)));
|
| - |
|
2547 |
schedule.setEndDate(schEndDate);
|
| - |
|
2548 |
schedule.setDayNumber(dayNumber);
|
| - |
|
2549 |
schedule.setEndAction(endAction);
|
| - |
|
2550 |
// Fill total_distance_km / total_time_mins from beat_route so the new
|
| - |
|
2551 |
// schedule row isn't NULL (this was the bug — these were left unset).
|
| - |
|
2552 |
double[] totals = computeDayTotals(beatId, dayNumber, endAction);
|
| - |
|
2553 |
schedule.setTotalDistanceKm(totals[0]);
|
| - |
|
2554 |
schedule.setTotalTimeMins((int) totals[1]);
|
| - |
|
2555 |
schedule.setCreatedTimestamp(LocalDateTime.now());
|
| - |
|
2556 |
beatScheduleRepository.persist(schedule);
|
| - |
|
2557 |
}
|
| 2342 |
|
2558 |
|
| 2343 |
Map<String, Object> response = new HashMap<>();
|
2559 |
Map<String, Object> response = new HashMap<>();
|
| 2344 |
response.put("status", true);
|
2560 |
response.put("status", true);
|
| 2345 |
response.put("message", "Moved from " + fromDate + " to " + toDate);
|
2561 |
response.put("message", "Beat scheduled successfully");
|
| 2346 |
return responseSender.ok(response);
|
2562 |
return responseSender.ok(response);
|
| 2347 |
}
|
2563 |
}
|
| 2348 |
|
2564 |
|
| 2349 |
@GetMapping(value = "/beatPlan/calendar")
|
2565 |
@GetMapping(value = "/beatPlan/calendar")
|
| 2350 |
public ResponseEntity<?> getCalendar(
|
2566 |
public ResponseEntity<?> getCalendar(
|
| Line 2433... |
Line 2649... |
| 2433 |
response.put("scheduledBeats", scheduledBeats);
|
2649 |
response.put("scheduledBeats", scheduledBeats);
|
| 2434 |
response.put("blockedDates", blockedDates);
|
2650 |
response.put("blockedDates", blockedDates);
|
| 2435 |
return responseSender.ok(response);
|
2651 |
return responseSender.ok(response);
|
| 2436 |
}
|
2652 |
}
|
| 2437 |
|
2653 |
|
| 2438 |
@PostMapping(value = "/beatPlan/scheduleOnCalendar")
|
- |
|
| 2439 |
public ResponseEntity<?> scheduleOnCalendar(
|
- |
|
| 2440 |
HttpServletRequest request,
|
- |
|
| 2441 |
@RequestParam String planGroupId,
|
- |
|
| 2442 |
@RequestParam String dates,
|
- |
|
| 2443 |
@RequestParam(required = false) String beatName,
|
- |
|
| 2444 |
@RequestParam(required = false) String beatColor) throws Exception {
|
- |
|
| 2445 |
|
- |
|
| 2446 |
int beatId = Integer.parseInt(planGroupId);
|
- |
|
| 2447 |
Gson gson = new Gson();
|
- |
|
| 2448 |
List<String> dateList = gson.fromJson(dates, new TypeToken<List<String>>() {
|
- |
|
| 2449 |
}.getType());
|
- |
|
| 2450 |
|
- |
|
| 2451 |
Beat beat = beatRepository.selectById(beatId);
|
- |
|
| 2452 |
if (beat == null) return responseSender.badRequest("Beat not found");
|
- |
|
| 2453 |
|
- |
|
| 2454 |
if (beatName != null) beat.setName(beatName);
|
- |
|
| 2455 |
if (beatColor != null && !beatColor.isEmpty()) beat.setBeatColor(beatColor);
|
- |
|
| 2456 |
|
- |
|
| 2457 |
// Delete old schedules and create new
|
- |
|
| 2458 |
beatScheduleRepository.deleteByBeatId(beatId);
|
- |
|
| 2459 |
LocalDate schEndDate = dateList.isEmpty() ? null : LocalDate.parse(dateList.get(dateList.size() - 1));
|
- |
|
| 2460 |
for (int i = 0; i < dateList.size() && i < beat.getTotalDays(); i++) {
|
- |
|
| 2461 |
int dayNumber = i + 1;
|
- |
|
| 2462 |
String endAction = (i == dateList.size() - 1) ? "HOME" : "DAYBREAK";
|
- |
|
| 2463 |
BeatSchedule schedule = new BeatSchedule();
|
- |
|
| 2464 |
schedule.setBeatId(beatId);
|
- |
|
| 2465 |
schedule.setStartDate(LocalDate.parse(dateList.get(i)));
|
- |
|
| 2466 |
schedule.setEndDate(schEndDate);
|
- |
|
| 2467 |
schedule.setDayNumber(dayNumber);
|
- |
|
| 2468 |
schedule.setEndAction(endAction);
|
- |
|
| 2469 |
// Fill total_distance_km / total_time_mins from beat_route so the new
|
- |
|
| 2470 |
// schedule row isn't NULL (this was the bug — these were left unset).
|
- |
|
| 2471 |
double[] totals = computeDayTotals(beatId, dayNumber, endAction);
|
- |
|
| 2472 |
schedule.setTotalDistanceKm(totals[0]);
|
- |
|
| 2473 |
schedule.setTotalTimeMins((int) totals[1]);
|
- |
|
| 2474 |
schedule.setCreatedTimestamp(LocalDateTime.now());
|
- |
|
| 2475 |
beatScheduleRepository.persist(schedule);
|
- |
|
| 2476 |
}
|
- |
|
| 2477 |
|
- |
|
| 2478 |
Map<String, Object> response = new HashMap<>();
|
- |
|
| 2479 |
response.put("status", true);
|
- |
|
| 2480 |
response.put("message", "Beat scheduled successfully");
|
- |
|
| 2481 |
return responseSender.ok(response);
|
- |
|
| 2482 |
}
|
- |
|
| 2483 |
|
- |
|
| 2484 |
// Drag-drop scheduling — adds schedule dates to the EXISTING beat (no new beat created)
|
2654 |
// Drag-drop scheduling — adds schedule dates to the EXISTING beat (no new beat created)
|
| 2485 |
@PostMapping(value = "/beatPlan/repeatBeat")
|
2655 |
@PostMapping(value = "/beatPlan/repeatBeat")
|
| 2486 |
public ResponseEntity<?> repeatBeat(
|
2656 |
public ResponseEntity<?> repeatBeat(
|
| 2487 |
HttpServletRequest request,
|
2657 |
HttpServletRequest request,
|
| 2488 |
@RequestParam String sourcePlanGroupId,
|
2658 |
@RequestParam String sourcePlanGroupId,
|
| Line 2495... |
Line 2665... |
| 2495 |
}.getType());
|
2665 |
}.getType());
|
| 2496 |
|
2666 |
|
| 2497 |
Beat beat = beatRepository.selectById(beatId);
|
2667 |
Beat beat = beatRepository.selectById(beatId);
|
| 2498 |
if (beat == null) return responseSender.badRequest("Beat not found");
|
2668 |
if (beat == null) return responseSender.badRequest("Beat not found");
|
| 2499 |
|
2669 |
|
| - |
|
2670 |
// One-beat-per-day guard: reject if any of the new dates already has
|
| - |
|
2671 |
// another beat scheduled for this user (excluding this beat itself).
|
| - |
|
2672 |
List<LocalDate> repeatDates = new ArrayList<>();
|
| - |
|
2673 |
for (String s : dateList) {
|
| - |
|
2674 |
try {
|
| - |
|
2675 |
repeatDates.add(LocalDate.parse(s));
|
| - |
|
2676 |
} catch (Exception ignored) {
|
| - |
|
2677 |
}
|
| - |
|
2678 |
}
|
| - |
|
2679 |
Map<String, Object> repeatConflict = findScheduleConflict(beat.getAuthUserId(), repeatDates, beatId);
|
| - |
|
2680 |
if (repeatConflict != null) return responseSender.badRequest(scheduleConflictMessage(repeatConflict));
|
| - |
|
2681 |
|
| 2500 |
// Remove placeholder (unscheduled) schedule rows
|
2682 |
// Remove placeholder (unscheduled) schedule rows
|
| 2501 |
List<BeatSchedule> existing = beatScheduleRepository.selectByBeatId(beatId);
|
2683 |
List<BeatSchedule> existing = beatScheduleRepository.selectByBeatId(beatId);
|
| 2502 |
for (BeatSchedule s : existing) {
|
2684 |
for (BeatSchedule s : existing) {
|
| 2503 |
if (s.getStartDate() != null && s.getStartDate().getYear() == 9999) {
|
2685 |
if (s.getStartDate() != null && s.getStartDate().getYear() == 9999) {
|
| 2504 |
beatScheduleRepository.delete(s);
|
2686 |
beatScheduleRepository.delete(s);
|
| Line 2529... |
Line 2711... |
| 2529 |
response.put("planGroupId", String.valueOf(beatId));
|
2711 |
response.put("planGroupId", String.valueOf(beatId));
|
| 2530 |
response.put("message", "Beat scheduled successfully");
|
2712 |
response.put("message", "Beat scheduled successfully");
|
| 2531 |
return responseSender.ok(response);
|
2713 |
return responseSender.ok(response);
|
| 2532 |
}
|
2714 |
}
|
| 2533 |
|
2715 |
|
| - |
|
2716 |
private static class ValidatedBulkBeat {
|
| - |
|
2717 |
final BulkBeatGroup g;
|
| - |
|
2718 |
final int authUserId;
|
| - |
|
2719 |
final List<Integer> sortedDays;
|
| - |
|
2720 |
final List<LocalDate> scheduleDates;
|
| - |
|
2721 |
|
| - |
|
2722 |
ValidatedBulkBeat(BulkBeatGroup g, int authUserId, List<Integer> sortedDays, List<LocalDate> scheduleDates) {
|
| - |
|
2723 |
this.g = g;
|
| - |
|
2724 |
this.authUserId = authUserId;
|
| - |
|
2725 |
this.sortedDays = sortedDays;
|
| - |
|
2726 |
this.scheduleDates = scheduleDates;
|
| - |
|
2727 |
}
|
| - |
|
2728 |
}
|
| - |
|
2729 |
|
| 2534 |
@GetMapping(value = "/beatPlan/availableSlots")
|
2730 |
@GetMapping(value = "/beatPlan/availableSlots")
|
| 2535 |
public ResponseEntity<?> getAvailableSlots(
|
2731 |
public ResponseEntity<?> getAvailableSlots(
|
| 2536 |
@RequestParam int authUserId,
|
2732 |
@RequestParam int authUserId,
|
| 2537 |
@RequestParam String month,
|
2733 |
@RequestParam String month,
|
| 2538 |
@RequestParam int daysNeeded) {
|
2734 |
@RequestParam int daysNeeded) {
|