| Line 56... |
Line 56... |
| 56 |
state.authUserId = parseInt(uid);
|
56 |
state.authUserId = parseInt(uid);
|
| 57 |
state.categoryId = parseInt($('#bp-category').val());
|
57 |
state.categoryId = parseInt($('#bp-category').val());
|
| 58 |
state.mode = 'list'; // 'list' or 'create' or 'view'
|
58 |
state.mode = 'list'; // 'list' or 'create' or 'view'
|
| 59 |
$('#bp-user-label').text($('#bp-auth-user option:selected').text());
|
59 |
$('#bp-user-label').text($('#bp-auth-user option:selected').text());
|
| 60 |
|
60 |
|
| - |
|
61 |
// Load all saved base locations — default goes in state.home*, the rest is
|
| 61 |
// Load home location
|
62 |
// exposed via a picker in the route panel so the planner can pick a
|
| - |
|
63 |
// different starting point for this one beat without changing the default.
|
| 62 |
$.ajax({
|
64 |
$.ajax({
|
| 63 |
url: context + "/beatPlan/getBaseLocation", type: "GET", dataType: "json",
|
65 |
url: context + "/beatPlan/listBaseLocations", type: "GET", dataType: "json",
|
| 64 |
data: {authUserId: uid},
|
66 |
data: {authUserId: uid},
|
| 65 |
success: function (r) {
|
67 |
success: function (r) {
|
| 66 |
var data = r.response || r;
|
68 |
var data = r.response || r;
|
| - |
|
69 |
state.baseLocations = data.locations || [];
|
| - |
|
70 |
var def = state.baseLocations.find(function (l) {
|
| - |
|
71 |
return l.isDefault;
|
| 67 |
if (data.locationName) {
|
72 |
}) || state.baseLocations[0];
|
| - |
|
73 |
if (def) {
|
| 68 |
state.homeLat = parseFloat(data.latitude);
|
74 |
state.homeLat = parseFloat(def.latitude);
|
| 69 |
state.homeLng = parseFloat(data.longitude);
|
75 |
state.homeLng = parseFloat(def.longitude);
|
| 70 |
state.homeName = data.locationName;
|
76 |
state.homeName = def.locationName;
|
| - |
|
77 |
state.activeBaseLocationId = def.id;
|
| - |
|
78 |
} else {
|
| - |
|
79 |
state.activeBaseLocationId = null;
|
| 71 |
}
|
80 |
}
|
| 72 |
showBeatList();
|
81 |
showBeatList();
|
| 73 |
}
|
82 |
}
|
| 74 |
});
|
83 |
});
|
| 75 |
});
|
84 |
});
|
| 76 |
|
85 |
|
| - |
|
86 |
// Called from the picker → swap the start location for the beat being
|
| - |
|
87 |
// created/edited. Updates state.home* + day 1's start point, then re-draws.
|
| - |
|
88 |
function renderBaseLocationPicker() {
|
| - |
|
89 |
var locs = state.baseLocations || [];
|
| - |
|
90 |
if (locs.length === 0) return '';
|
| - |
|
91 |
var activeId = state.activeBaseLocationId;
|
| - |
|
92 |
if (locs.length === 1) {
|
| - |
|
93 |
var only = locs[0];
|
| - |
|
94 |
return '<div style="font-size:11px;color:#94a3b8;margin-bottom:10px;">'
|
| - |
|
95 |
+ 'Starting from: <strong style="color:#e2e8f0;">' + (only.locationName || 'Home') + '</strong>'
|
| - |
|
96 |
+ (only.isDefault ? ' <span style="color:#22c55e;">(default)</span>' : '')
|
| - |
|
97 |
+ '</div>';
|
| - |
|
98 |
}
|
| - |
|
99 |
var html = '<div style="margin-bottom:10px;">'
|
| - |
|
100 |
+ '<label style="font-size:11px;color:#94a3b8;display:block;margin-bottom:4px;">Start this beat from:</label>'
|
| - |
|
101 |
+ '<select id="bp-base-picker" style="width:100%;padding:6px 8px;border:1px solid #475569;border-radius:4px;background:#0f172a;color:#e2e8f0;font-size:12px;font-family:inherit;">';
|
| - |
|
102 |
locs.forEach(function (l) {
|
| - |
|
103 |
var sel = (l.id === activeId) ? ' selected' : '';
|
| - |
|
104 |
var label = (l.locationName || 'Location ' + l.id) + (l.isDefault ? ' (default)' : '');
|
| - |
|
105 |
html += '<option value="' + l.id + '"' + sel + '>' + label + '</option>';
|
| - |
|
106 |
});
|
| - |
|
107 |
html += '</select></div>';
|
| - |
|
108 |
return html;
|
| - |
|
109 |
}
|
| - |
|
110 |
|
| - |
|
111 |
$(document).on('change', '#bp-base-picker', function () {
|
| - |
|
112 |
var id = parseInt($(this).val());
|
| - |
|
113 |
if (!id) return;
|
| - |
|
114 |
applyBaseLocation(id);
|
| - |
|
115 |
});
|
| - |
|
116 |
|
| - |
|
117 |
function applyBaseLocation(id) {
|
| - |
|
118 |
var loc = (state.baseLocations || []).find(function (l) {
|
| - |
|
119 |
return l.id === id;
|
| - |
|
120 |
});
|
| - |
|
121 |
if (!loc) return;
|
| - |
|
122 |
state.homeLat = parseFloat(loc.latitude);
|
| - |
|
123 |
state.homeLng = parseFloat(loc.longitude);
|
| - |
|
124 |
state.homeName = loc.locationName;
|
| - |
|
125 |
state.activeBaseLocationId = id;
|
| - |
|
126 |
|
| - |
|
127 |
// Only day 1 starts from the chosen base; later days start where the
|
| - |
|
128 |
// previous day ended (existing behavior unchanged).
|
| - |
|
129 |
if (state.days && state.days.length > 0) {
|
| - |
|
130 |
state.days[0].startLat = state.homeLat;
|
| - |
|
131 |
state.days[0].startLng = state.homeLng;
|
| - |
|
132 |
state.days[0].startName = state.homeName;
|
| - |
|
133 |
if (typeof recalcDay === 'function') {
|
| - |
|
134 |
var saved = state.currentDay;
|
| - |
|
135 |
state.currentDay = 1;
|
| - |
|
136 |
recalcDay();
|
| - |
|
137 |
state.currentDay = saved;
|
| - |
|
138 |
}
|
| - |
|
139 |
}
|
| - |
|
140 |
|
| - |
|
141 |
// Move the home marker on the map + recenter so the user actually sees it.
|
| - |
|
142 |
if (typeof google !== 'undefined' && google.maps && map) {
|
| - |
|
143 |
var pos = new google.maps.LatLng(state.homeLat, state.homeLng);
|
| - |
|
144 |
if (homeMarker) {
|
| - |
|
145 |
homeMarker.setPosition(pos);
|
| - |
|
146 |
homeMarker.setTitle('Home: ' + state.homeName);
|
| - |
|
147 |
} else {
|
| - |
|
148 |
homeMarker = new google.maps.Marker({
|
| - |
|
149 |
map: map, position: pos, title: 'Home: ' + state.homeName,
|
| - |
|
150 |
icon: {
|
| - |
|
151 |
url: 'https://maps.google.com/mapfiles/kml/shapes/homegardenbusiness.png',
|
| - |
|
152 |
scaledSize: new google.maps.Size(32, 32)
|
| - |
|
153 |
},
|
| - |
|
154 |
zIndex: 100
|
| - |
|
155 |
});
|
| - |
|
156 |
}
|
| - |
|
157 |
map.panTo(pos);
|
| - |
|
158 |
}
|
| - |
|
159 |
|
| - |
|
160 |
if (typeof drawRoute === 'function') drawRoute();
|
| - |
|
161 |
if (typeof renderRoutePanel === 'function') renderRoutePanel();
|
| - |
|
162 |
if (typeof updateBottomBar === 'function') updateBottomBar();
|
| - |
|
163 |
}
|
| - |
|
164 |
|
| 77 |
// ============ BEAT LIST VIEW ============
|
165 |
// ============ BEAT LIST VIEW ============
|
| 78 |
function showBeatList() {
|
166 |
function showBeatList() {
|
| 79 |
state.mode = 'list';
|
167 |
state.mode = 'list';
|
| 80 |
$('#bottom-bar').hide();
|
168 |
$('#bottom-bar').hide();
|
| 81 |
|
169 |
|
| Line 248... |
Line 336... |
| 248 |
totalKm: 0, totalMins: 0
|
336 |
totalKm: 0, totalMins: 0
|
| 249 |
});
|
337 |
});
|
| 250 |
});
|
338 |
});
|
| 251 |
state.currentDay = 1;
|
339 |
state.currentDay = 1;
|
| 252 |
|
340 |
|
| - |
|
341 |
// Snapshot the originals — used during save to detect day-count
|
| - |
|
342 |
// increases (blocked) and lead removals (popup with cancel/reschedule).
|
| - |
|
343 |
state.originalDayCount = state.days.length;
|
| - |
|
344 |
state.originalLeads = [];
|
| - |
|
345 |
state.days.forEach(function (d) {
|
| - |
|
346 |
d.visits.forEach(function (v) {
|
| - |
|
347 |
if (v.type === 'lead' || v.isLead) {
|
| - |
|
348 |
state.originalLeads.push({
|
| - |
|
349 |
leadId: v.id,
|
| - |
|
350 |
dayNumber: d.dayNumber,
|
| - |
|
351 |
name: v.name || ('Lead #' + v.id)
|
| - |
|
352 |
});
|
| - |
|
353 |
}
|
| - |
|
354 |
});
|
| - |
|
355 |
});
|
| - |
|
356 |
|
| 253 |
// Async enrich each lead with name + geo
|
357 |
// Async enrich each lead with name + geo
|
| 254 |
var leadPromises = [];
|
358 |
var leadPromises = [];
|
| 255 |
state.days.forEach(function (day) {
|
359 |
state.days.forEach(function (day) {
|
| 256 |
day.visits.forEach(function (v) {
|
360 |
day.visits.forEach(function (v) {
|
| 257 |
if (!v.isLead) return;
|
361 |
if (!v.isLead) return;
|
| Line 263... |
Line 367... |
| 263 |
v.lng = g.longitude;
|
367 |
v.lng = g.longitude;
|
| 264 |
}
|
368 |
}
|
| 265 |
}),
|
369 |
}),
|
| 266 |
$.get(context + '/getLead?leadId=' + v.id).then(function (rr) {
|
370 |
$.get(context + '/getLead?leadId=' + v.id).then(function (rr) {
|
| 267 |
var l = rr.response || rr;
|
371 |
var l = rr.response || rr;
|
| - |
|
372 |
if (l && l.firstName) {
|
| 268 |
if (l && l.firstName) v.name = 'LEAD - ' + l.firstName + ' ' + (l.lastName || '');
|
373 |
var nm = 'LEAD - ' + l.firstName + ' ' + (l.lastName || '');
|
| - |
|
374 |
v.name = nm;
|
| - |
|
375 |
// Keep the originalLeads snapshot label in sync so the
|
| - |
|
376 |
// removed-leads popup shows the real name, not 'LEAD #123'.
|
| - |
|
377 |
(state.originalLeads || []).forEach(function (ol) {
|
| - |
|
378 |
if (ol.leadId === v.id) ol.name = nm;
|
| - |
|
379 |
});
|
| - |
|
380 |
}
|
| 269 |
})
|
381 |
})
|
| 270 |
);
|
382 |
);
|
| 271 |
});
|
383 |
});
|
| 272 |
});
|
384 |
});
|
| 273 |
|
385 |
|
| 274 |
$.when.apply($, leadPromises).always(function () {
|
386 |
$.when.apply($, leadPromises).always(function () {
|
| - |
|
387 |
// Pre-select whichever saved base location matches the beat's
|
| - |
|
388 |
// existing day-1 start (lat/lng), so the picker reflects current state.
|
| - |
|
389 |
var day1 = state.days[0];
|
| - |
|
390 |
if (day1 && state.baseLocations) {
|
| - |
|
391 |
var match = state.baseLocations.find(function (l) {
|
| - |
|
392 |
return Math.abs(parseFloat(l.latitude) - day1.startLat) < 1e-4
|
| - |
|
393 |
&& Math.abs(parseFloat(l.longitude) - day1.startLng) < 1e-4;
|
| - |
|
394 |
});
|
| - |
|
395 |
if (match) state.activeBaseLocationId = match.id;
|
| - |
|
396 |
}
|
| - |
|
397 |
|
| 275 |
renderRoutePanel();
|
398 |
renderRoutePanel();
|
| 276 |
var dateLabel = planDate
|
399 |
var dateLabel = planDate
|
| 277 |
? '<div style="font-size:11px;color:#f59e0b;margin-bottom:8px;">Editing run on: ' + planDate + ' (leads on this date can be removed)</div>'
|
400 |
? '<div style="font-size:11px;color:#f59e0b;margin-bottom:8px;">Editing run on: ' + planDate + ' (leads on this date can be removed)</div>'
|
| 278 |
: '<div style="font-size:11px;color:#94a3b8;margin-bottom:8px;">Editing beat template (partners only)</div>';
|
401 |
: '<div style="font-size:11px;color:#94a3b8;margin-bottom:8px;">Editing beat template (partners only)</div>';
|
| 279 |
$('#route-list').prepend(
|
402 |
$('#route-list').prepend(
|
| 280 |
'<div style="margin-bottom:8px;"><button class="btn-back-to-list" '
|
403 |
'<div style="margin-bottom:8px;"><button class="btn-back-to-list" '
|
| 281 |
+ 'style="font-size:11px;padding:4px 10px;border:1px solid #475569;'
|
404 |
+ 'style="font-size:11px;padding:4px 10px;border:1px solid #475569;'
|
| 282 |
+ 'border-radius:4px;background:none;color:#94a3b8;cursor:pointer;'
|
405 |
+ 'border-radius:4px;background:none;color:#94a3b8;cursor:pointer;'
|
| 283 |
+ 'font-family:inherit;">← Back to list</button></div>'
|
406 |
+ 'font-family:inherit;">← Back to list</button></div>'
|
| 284 |
+ '<div style="font-size:14px;font-weight:600;color:#f59e0b;margin-bottom:4px;">'
|
407 |
+ '<div style="font-size:14px;font-weight:600;color:#f59e0b;margin-bottom:4px;">'
|
| 285 |
+ 'EDITING: ' + state.beatTitle + '</div>' + dateLabel);
|
408 |
+ 'EDITING: ' + state.beatTitle + '</div>' + dateLabel
|
| - |
|
409 |
+ renderBaseLocationPicker());
|
| 286 |
|
410 |
|
| 287 |
initMap();
|
411 |
initMap();
|
| 288 |
// Add lead markers (orange) on the map
|
412 |
// Add lead markers (orange) on the map
|
| 289 |
state.days.forEach(function (day) {
|
413 |
state.days.forEach(function (day) {
|
| 290 |
day.visits.forEach(function (v) {
|
414 |
day.visits.forEach(function (v) {
|
| Line 982... |
Line 1106... |
| 982 |
function renderRoutePanel() {
|
1106 |
function renderRoutePanel() {
|
| 983 |
var html = '';
|
1107 |
var html = '';
|
| 984 |
if (state.mode === 'create' && state.beatTitle) {
|
1108 |
if (state.mode === 'create' && state.beatTitle) {
|
| 985 |
html += '<div style="margin-bottom:8px;"><button class="btn-back-to-list" style="font-size:11px;padding:4px 10px;border:1px solid #475569;border-radius:4px;background:none;color:#94a3b8;cursor:pointer;font-family:inherit;">← Back</button></div>';
|
1109 |
html += '<div style="margin-bottom:8px;"><button class="btn-back-to-list" style="font-size:11px;padding:4px 10px;border:1px solid #475569;border-radius:4px;background:none;color:#94a3b8;cursor:pointer;font-family:inherit;">← Back</button></div>';
|
| 986 |
html += '<div style="font-size:14px;font-weight:600;color:#60a5fa;margin-bottom:10px;">' + state.beatTitle + '</div>';
|
1110 |
html += '<div style="font-size:14px;font-weight:600;color:#60a5fa;margin-bottom:10px;">' + state.beatTitle + '</div>';
|
| - |
|
1111 |
html += renderBaseLocationPicker();
|
| 987 |
}
|
1112 |
}
|
| 988 |
state.days.forEach(function (day) {
|
1113 |
state.days.forEach(function (day) {
|
| 989 |
var isCurrent = day.dayNumber === state.currentDay;
|
1114 |
var isCurrent = day.dayNumber === state.currentDay;
|
| 990 |
var isLastDay = day.dayNumber === state.days.length;
|
1115 |
var isLastDay = day.dayNumber === state.days.length;
|
| 991 |
html += '<div class="route-day">';
|
1116 |
html += '<div class="route-day">';
|
| Line 1184... |
Line 1309... |
| 1184 |
})) {
|
1309 |
})) {
|
| 1185 |
alert('No visits added');
|
1310 |
alert('No visits added');
|
| 1186 |
return;
|
1311 |
return;
|
| 1187 |
}
|
1312 |
}
|
| 1188 |
|
1313 |
|
| 1189 |
var beatTitle = state.beatTitle || 'Beat';
|
1314 |
var beatTitle = state.beatTitle || 'Beat';
|
| - |
|
1315 |
var isEdit = state.mode === 'edit' && state.editingBeatId;
|
| - |
|
1316 |
|
| - |
|
1317 |
// EDIT RULE: cannot grow day count. Caught client-side so the modal
|
| - |
|
1318 |
// doesn't even open; server enforces the same rule.
|
| - |
|
1319 |
if (isEdit && state.originalDayCount && state.days.length > state.originalDayCount) {
|
| - |
|
1320 |
alert('Cannot increase the number of days while editing.\n\n'
|
| - |
|
1321 |
+ 'Original: ' + state.originalDayCount + ' day(s)\n'
|
| - |
|
1322 |
+ 'Current: ' + state.days.length + ' day(s)\n\n'
|
| - |
|
1323 |
+ 'Please create a new beat for additional days.');
|
| - |
|
1324 |
return;
|
| - |
|
1325 |
}
|
| 1190 |
|
1326 |
|
| 1191 |
// Build plan data and save to DB first
|
1327 |
// Build plan data
|
| 1192 |
var daysData = [];
|
1328 |
var daysData = [];
|
| 1193 |
state.days.forEach(function (day) {
|
1329 |
state.days.forEach(function (day) {
|
| 1194 |
if (day.visits.length === 0) return;
|
1330 |
if (day.visits.length === 0) return;
|
| 1195 |
daysData.push({
|
1331 |
daysData.push({
|
| 1196 |
dayNumber: day.dayNumber,
|
1332 |
dayNumber: day.dayNumber,
|
| Line 1207... |
Line 1343... |
| 1207 |
return {id: v.id, type: v.type || 'partner'};
|
1343 |
return {id: v.id, type: v.type || 'partner'};
|
| 1208 |
})
|
1344 |
})
|
| 1209 |
});
|
1345 |
});
|
| 1210 |
});
|
1346 |
});
|
| 1211 |
|
1347 |
|
| 1212 |
// Dates will be assigned later via calendar — pass nulls for now
|
1348 |
// EDIT RULE: detect removed leads → show popup with cancel/reschedule per lead
|
| - |
|
1349 |
var removedLeads = [];
|
| - |
|
1350 |
if (isEdit && state.originalLeads && state.originalLeads.length) {
|
| - |
|
1351 |
var currentLeadIds = {};
|
| - |
|
1352 |
state.days.forEach(function (d) {
|
| - |
|
1353 |
d.visits.forEach(function (v) {
|
| - |
|
1354 |
if (v.type === 'lead' || v.isLead) currentLeadIds[v.id] = true;
|
| - |
|
1355 |
});
|
| - |
|
1356 |
});
|
| - |
|
1357 |
removedLeads = state.originalLeads.filter(function (l) {
|
| - |
|
1358 |
return !currentLeadIds[l.leadId];
|
| - |
|
1359 |
});
|
| - |
|
1360 |
}
|
| - |
|
1361 |
|
| - |
|
1362 |
if (removedLeads.length > 0) {
|
| - |
|
1363 |
openRemovedLeadsModal(removedLeads, function (actions) {
|
| - |
|
1364 |
doFinishSubmit(daysData, beatTitle, isEdit, actions);
|
| - |
|
1365 |
});
|
| - |
|
1366 |
return;
|
| - |
|
1367 |
}
|
| - |
|
1368 |
|
| - |
|
1369 |
doFinishSubmit(daysData, beatTitle, isEdit, null);
|
| - |
|
1370 |
});
|
| - |
|
1371 |
|
| - |
|
1372 |
function doFinishSubmit(daysData, beatTitle, isEdit, removedLeadActions) {
|
| - |
|
1373 |
var planPayload = {
|
| - |
|
1374 |
days: daysData,
|
| 1213 |
var dates = daysData.map(function () {
|
1375 |
dates: daysData.map(function () {
|
| 1214 |
return null;
|
1376 |
return null;
|
| 1215 |
});
|
1377 |
}),
|
| 1216 |
var planPayload = {days: daysData, dates: dates, beatName: beatTitle.trim()};
|
1378 |
beatName: beatTitle.trim()
|
| - |
|
1379 |
};
|
| 1217 |
// When editing a specific scheduled date, pass it so leads removed from that date get cancelled
|
1380 |
if (state.mode === 'edit' && state.editingDate) planPayload.planDate = state.editingDate;
|
| 1218 |
if (state.mode === 'edit' && state.editingDate) {
|
1381 |
if (removedLeadActions && removedLeadActions.length) {
|
| 1219 |
planPayload.planDate = state.editingDate;
|
1382 |
planPayload.removedLeadActions = JSON.stringify(removedLeadActions);
|
| 1220 |
}
|
1383 |
}
|
| 1221 |
var planData = JSON.stringify(planPayload);
|
1384 |
var planData = JSON.stringify(planPayload);
|
| 1222 |
|
1385 |
|
| 1223 |
isSubmittingBeat = true;
|
1386 |
isSubmittingBeat = true;
|
| 1224 |
var isEdit = state.mode === 'edit' && state.editingBeatId;
|
- |
|
| 1225 |
$('#btn-finish').prop('disabled', true).text(isEdit ? 'Updating...' : 'Saving...');
|
1387 |
$('#btn-finish').prop('disabled', true).text(isEdit ? 'Updating...' : 'Saving...');
|
| 1226 |
|
1388 |
|
| 1227 |
var url = isEdit ? '/beatPlan/updateBeat' : '/beatPlan/submitPlan';
|
1389 |
var url = isEdit ? '/beatPlan/updateBeat' : '/beatPlan/submitPlan';
|
| 1228 |
var ajaxData = isEdit
|
1390 |
var ajaxData = isEdit
|
| 1229 |
? {beatId: state.editingBeatId, planData: planData}
|
1391 |
? {beatId: state.editingBeatId, planData: planData}
|
| Line 1236... |
Line 1398... |
| 1236 |
success: function (response) {
|
1398 |
success: function (response) {
|
| 1237 |
var data = (typeof response === 'string' ? JSON.parse(response) : response);
|
1399 |
var data = (typeof response === 'string' ? JSON.parse(response) : response);
|
| 1238 |
var result = data.response || data;
|
1400 |
var result = data.response || data;
|
| 1239 |
|
1401 |
|
| 1240 |
if (result.status) {
|
1402 |
if (result.status) {
|
| 1241 |
// Mark as saved — finish button stays disabled, no re-submit possible
|
- |
|
| 1242 |
state.savedPlanGroupId = result.planGroupId;
|
1403 |
state.savedPlanGroupId = result.planGroupId;
|
| 1243 |
$('#bottom-bar').hide();
|
1404 |
$('#bottom-bar').hide();
|
| 1244 |
|
1405 |
|
| 1245 |
if (result.duplicate) {
|
1406 |
if (result.duplicate) {
|
| 1246 |
alert('This beat already exists.');
|
1407 |
alert('This beat already exists.');
|
| 1247 |
} else if (isEdit) {
|
1408 |
} else if (isEdit) {
|
| - |
|
1409 |
var extras = [];
|
| - |
|
1410 |
if (result.leadsCancelled) extras.push(result.leadsCancelled + ' lead(s) cancelled');
|
| - |
|
1411 |
if (result.leadsRescheduled) extras.push(result.leadsRescheduled + ' lead(s) rescheduled');
|
| 1248 |
alert('Beat "' + beatTitle + '" updated successfully!');
|
1412 |
alert('Beat "' + beatTitle + '" updated successfully'
|
| - |
|
1413 |
+ (extras.length ? '\n\n' + extras.join('\n') : '') + '.');
|
| 1249 |
} else {
|
1414 |
} else {
|
| 1250 |
alert('Beat "' + beatTitle + '" saved successfully!');
|
1415 |
alert('Beat "' + beatTitle + '" saved successfully!');
|
| 1251 |
}
|
1416 |
}
|
| 1252 |
|
1417 |
|
| 1253 |
// Reset edit state, go back to beat list
|
- |
|
| 1254 |
state.editingBeatId = null;
|
1418 |
state.editingBeatId = null;
|
| 1255 |
state.mode = 'list';
|
1419 |
state.mode = 'list';
|
| 1256 |
showBeatList();
|
1420 |
showBeatList();
|
| 1257 |
} else {
|
1421 |
} else {
|
| 1258 |
$('#btn-finish').prop('disabled', false).text(isEdit ? 'Save Changes' : 'Finish Plan');
|
1422 |
$('#btn-finish').prop('disabled', false).text(isEdit ? 'Save Changes' : 'Finish Plan');
|
| 1259 |
alert('Error saving beat plan');
|
1423 |
alert('Error saving beat plan');
|
| 1260 |
}
|
1424 |
}
|
| 1261 |
isSubmittingBeat = false;
|
1425 |
isSubmittingBeat = false;
|
| 1262 |
},
|
1426 |
},
|
| 1263 |
error: function (xhr) {
|
1427 |
error: function (xhr) {
|
| 1264 |
isSubmittingBeat = false;
|
1428 |
isSubmittingBeat = false;
|
| 1265 |
$('#btn-finish').prop('disabled', false).text(isEdit ? 'Save Changes' : 'Finish Plan');
|
1429 |
$('#btn-finish').prop('disabled', false).text(isEdit ? 'Save Changes' : 'Finish Plan');
|
| - |
|
1430 |
// Surface a clean server message if present (e.g., day-increase block, missing beat on reschedule date)
|
| 1266 |
alert('Error: ' + (xhr.responseText || xhr.statusText));
|
1431 |
var msg = xhr.responseText || xhr.statusText;
|
| - |
|
1432 |
try {
|
| - |
|
1433 |
var err = JSON.parse(xhr.responseText);
|
| - |
|
1434 |
if (err && err.response) {
|
| - |
|
1435 |
if (typeof err.response === 'string') msg = err.response;
|
| - |
|
1436 |
else if (err.response.message) msg = err.response.message;
|
| - |
|
1437 |
}
|
| - |
|
1438 |
} catch (_) {
|
| - |
|
1439 |
}
|
| - |
|
1440 |
alert(msg);
|
| 1267 |
}
|
1441 |
}
|
| 1268 |
});
|
1442 |
});
|
| - |
|
1443 |
}
|
| - |
|
1444 |
|
| - |
|
1445 |
// ============ REMOVED LEADS POPUP ============
|
| - |
|
1446 |
// Shown when an edit removes one or more leads. Per lead, the user picks:
|
| - |
|
1447 |
// - Cancel → mark CANCELLED
|
| - |
|
1448 |
// - Reschedule + date → move to whichever beat the same user has on that date
|
| - |
|
1449 |
// The date input is validated against /beatPlan/userBeatsOnDate so the user
|
| - |
|
1450 |
// sees "No beat on this date — pick another" before submit.
|
| - |
|
1451 |
function openRemovedLeadsModal(removedLeads, onConfirm) {
|
| - |
|
1452 |
$('#modal-removed-leads').remove(); // clean any prior instance
|
| - |
|
1453 |
|
| - |
|
1454 |
var rowsHtml = removedLeads.map(function (l) {
|
| - |
|
1455 |
return ''
|
| - |
|
1456 |
+ '<div class="rl-row" data-leadid="' + l.leadId + '" style="border:1px solid #334155;border-radius:6px;padding:8px;margin-bottom:8px;">'
|
| - |
|
1457 |
+ ' <div style="font-weight:600;color:#f1f5f9;margin-bottom:6px;">'
|
| - |
|
1458 |
+ ' ' + (l.name || ('Lead #' + l.leadId)) + ' <span style="color:#64748b;font-weight:400;font-size:11px;">(was on day ' + l.dayNumber + ')</span>'
|
| - |
|
1459 |
+ ' </div>'
|
| - |
|
1460 |
+ ' <label style="display:inline-flex;align-items:center;gap:6px;margin-right:14px;cursor:pointer;font-size:12px;">'
|
| - |
|
1461 |
+ ' <input type="radio" name="rl-act-' + l.leadId + '" value="cancel" checked> Cancel this lead'
|
| - |
|
1462 |
+ ' </label>'
|
| - |
|
1463 |
+ ' <label style="display:inline-flex;align-items:center;gap:6px;cursor:pointer;font-size:12px;">'
|
| - |
|
1464 |
+ ' <input type="radio" name="rl-act-' + l.leadId + '" value="reschedule"> Reschedule to another date'
|
| - |
|
1465 |
+ ' </label>'
|
| - |
|
1466 |
+ ' <div class="rl-date-wrap" style="display:none;margin-top:6px;">'
|
| - |
|
1467 |
+ ' <input type="date" class="rl-date" min="' + fmtDate(new Date()) + '" style="width:auto;display:inline-block;margin-bottom:0;">'
|
| - |
|
1468 |
+ ' <span class="rl-date-status" style="font-size:11px;margin-left:8px;"></span>'
|
| - |
|
1469 |
+ ' </div>'
|
| - |
|
1470 |
+ '</div>';
|
| - |
|
1471 |
}).join('');
|
| - |
|
1472 |
|
| - |
|
1473 |
var html = ''
|
| - |
|
1474 |
+ '<div class="modal-overlay show" id="modal-removed-leads">'
|
| - |
|
1475 |
+ ' <div class="modal-box" style="min-width:520px;max-width:680px;">'
|
| - |
|
1476 |
+ ' <h3>You removed ' + removedLeads.length + ' lead(s) from this beat</h3>'
|
| - |
|
1477 |
+ ' <p style="font-size:12px;color:#94a3b8;margin-bottom:12px;">'
|
| - |
|
1478 |
+ ' Choose what to do with each lead before saving. Reschedule moves the lead to whichever beat this user has on the selected date.'
|
| - |
|
1479 |
+ ' </p>'
|
| - |
|
1480 |
+ ' <div id="rl-list" style="max-height:55vh;overflow-y:auto;">' + rowsHtml + '</div>'
|
| - |
|
1481 |
+ ' <div class="modal-actions">'
|
| - |
|
1482 |
+ ' <button class="modal-cancel" id="rl-cancel">Cancel (keep editing)</button>'
|
| - |
|
1483 |
+ ' <button class="modal-confirm" id="rl-confirm">Save with these actions</button>'
|
| - |
|
1484 |
+ ' </div>'
|
| - |
|
1485 |
+ ' </div>'
|
| - |
|
1486 |
+ '</div>';
|
| - |
|
1487 |
$('body').append(html);
|
| - |
|
1488 |
|
| - |
|
1489 |
// Show/hide date picker per row
|
| - |
|
1490 |
$('#modal-removed-leads').on('change', 'input[type="radio"]', function () {
|
| - |
|
1491 |
var $row = $(this).closest('.rl-row');
|
| - |
|
1492 |
var isResched = $row.find('input[type="radio"]:checked').val() === 'reschedule';
|
| - |
|
1493 |
$row.find('.rl-date-wrap').toggle(isResched);
|
| - |
|
1494 |
$row.find('.rl-date-status').text('');
|
| - |
|
1495 |
});
|
| - |
|
1496 |
|
| - |
|
1497 |
// Validate the date as the user types it — show "no beat on this date" warning
|
| - |
|
1498 |
$('#modal-removed-leads').on('change', '.rl-date', function () {
|
| - |
|
1499 |
var $row = $(this).closest('.rl-row');
|
| - |
|
1500 |
var $status = $row.find('.rl-date-status');
|
| - |
|
1501 |
var d = $(this).val();
|
| - |
|
1502 |
if (!d) {
|
| - |
|
1503 |
$status.text('');
|
| - |
|
1504 |
return;
|
| - |
|
1505 |
}
|
| - |
|
1506 |
$status.text('Checking...').css('color', '#94a3b8');
|
| - |
|
1507 |
$.ajax({
|
| - |
|
1508 |
url: context + '/beatPlan/userBeatsOnDate', type: 'GET', dataType: 'json',
|
| - |
|
1509 |
data: {authUserId: state.authUserId, date: d},
|
| - |
|
1510 |
success: function (r) {
|
| - |
|
1511 |
var dd = r.response || r;
|
| - |
|
1512 |
var beats = dd.beats || [];
|
| - |
|
1513 |
if (beats.length === 0) {
|
| - |
|
1514 |
$status.text('No beat on this date — pick another date, or Cancel this lead instead.').css('color', '#ef4444');
|
| - |
|
1515 |
} else {
|
| - |
|
1516 |
var names = beats.map(function (b) {
|
| - |
|
1517 |
return '"' + b.beatName + '" (day ' + b.dayNumber + ')';
|
| - |
|
1518 |
}).join(', ');
|
| - |
|
1519 |
$status.text('Will attach to: ' + names).css('color', '#22c55e');
|
| - |
|
1520 |
}
|
| - |
|
1521 |
},
|
| - |
|
1522 |
error: function () {
|
| - |
|
1523 |
$status.text('Could not verify date').css('color', '#f59e0b');
|
| - |
|
1524 |
}
|
| - |
|
1525 |
});
|
| - |
|
1526 |
});
|
| - |
|
1527 |
|
| - |
|
1528 |
$('#modal-removed-leads').on('click', '#rl-cancel', function () {
|
| - |
|
1529 |
$('#modal-removed-leads').remove();
|
| - |
|
1530 |
// abort save — user goes back to editing
|
| - |
|
1531 |
});
|
| - |
|
1532 |
|
| - |
|
1533 |
$('#modal-removed-leads').on('click', '#rl-confirm', function () {
|
| - |
|
1534 |
var actions = [];
|
| - |
|
1535 |
var problems = [];
|
| - |
|
1536 |
$('#modal-removed-leads .rl-row').each(function () {
|
| - |
|
1537 |
var $row = $(this);
|
| - |
|
1538 |
var leadId = parseInt($row.data('leadid'));
|
| - |
|
1539 |
var mode = $row.find('input[type="radio"]:checked').val();
|
| - |
|
1540 |
if (mode === 'reschedule') {
|
| - |
|
1541 |
var d = $row.find('.rl-date').val();
|
| - |
|
1542 |
if (!d) {
|
| - |
|
1543 |
problems.push('Lead ' + leadId + ': pick a date or switch to Cancel');
|
| - |
|
1544 |
return;
|
| - |
|
1545 |
}
|
| - |
|
1546 |
actions.push({leadId: leadId, action: 'reschedule', toDate: d});
|
| - |
|
1547 |
} else {
|
| - |
|
1548 |
actions.push({leadId: leadId, action: 'cancel'});
|
| - |
|
1549 |
}
|
| - |
|
1550 |
});
|
| - |
|
1551 |
if (problems.length) {
|
| - |
|
1552 |
alert(problems.join('\n'));
|
| - |
|
1553 |
return;
|
| - |
|
1554 |
}
|
| - |
|
1555 |
$('#modal-removed-leads').remove();
|
| - |
|
1556 |
onConfirm(actions);
|
| 1269 |
});
|
1557 |
});
|
| - |
|
1558 |
}
|
| 1270 |
|
1559 |
|
| 1271 |
// ============ PANEL TABS ============
|
1560 |
// ============ PANEL TABS ============
|
| 1272 |
$('.panel-tab').on('click', function () {
|
1561 |
$('.panel-tab').on('click', function () {
|
| 1273 |
var tab = $(this).data('tab');
|
1562 |
var tab = $(this).data('tab');
|
| 1274 |
$('.panel-tab').removeClass('active');
|
1563 |
$('.panel-tab').removeClass('active');
|