| 36621 |
ranu |
1 |
// ============================================================
|
|
|
2 |
// BEAT PLAN APP — Map-First Route Planner
|
|
|
3 |
// ============================================================
|
|
|
4 |
|
|
|
5 |
var map, infoWindow, homeMarker, previewLine;
|
|
|
6 |
var markers = {};
|
|
|
7 |
var routeLines = [];
|
|
|
8 |
var distLabels = [];
|
|
|
9 |
|
|
|
10 |
var state = {
|
|
|
11 |
authUserId: 0,
|
|
|
12 |
categoryId: 4,
|
|
|
13 |
homeLat: null, homeLng: null, homeName: '',
|
|
|
14 |
currentDay: 1,
|
|
|
15 |
days: [], // [{dayNumber, startLat, startLng, startName, visits:[], endAction, stayName, stayLat, stayLng, totalKm, totalMins}]
|
|
|
16 |
partners: [],
|
|
|
17 |
calMonth: null,
|
|
|
18 |
calData: null,
|
|
|
19 |
calSelectedBeat: null,
|
| 36962 |
vikas |
20 |
calPickedDates: [],
|
|
|
21 |
// L4+ operators may schedule a beat on today's date (set from the page).
|
|
|
22 |
canScheduleToday: (typeof canScheduleToday !== 'undefined' && !!canScheduleToday)
|
| 36621 |
ranu |
23 |
};
|
|
|
24 |
|
|
|
25 |
var VISIT_MINS = 30;
|
|
|
26 |
var AVG_SPEED = 30;
|
|
|
27 |
var DAY_LIMIT = 540;
|
|
|
28 |
var ROAD_FACTOR = 1.3;
|
| 36644 |
ranu |
29 |
var isSubmittingBeat = false; // guard against double-submit
|
| 36621 |
ranu |
30 |
var COLORS = ['#3b82f6', '#ef4444', '#22c55e', '#a855f7', '#f59e0b', '#06b6d4', '#ec4899', '#334155', '#14b8a6', '#dc2626'];
|
|
|
31 |
|
|
|
32 |
// ============ TOP BAR ============
|
|
|
33 |
$('#bp-level').on('change', function () {
|
|
|
34 |
var level = $(this).val();
|
|
|
35 |
$('#bp-auth-user').html('<option value="">Select User</option>');
|
|
|
36 |
if (!level) return;
|
|
|
37 |
|
|
|
38 |
$.ajax({
|
|
|
39 |
url: context + "/beatPlan/getAuthUsers", type: "GET", dataType: "json",
|
|
|
40 |
data: {categoryId: $('#bp-category').val(), escalationType: level},
|
|
|
41 |
success: function (r) {
|
|
|
42 |
var data = r.response || r;
|
|
|
43 |
var html = '<option value="">Select User</option>';
|
| 36788 |
ranu |
44 |
// Show "Name (#authUserId)" so the head can quickly match identities
|
|
|
45 |
// (useful when grabbing an id for the Visit-Request Reassign prompt etc).
|
| 36621 |
ranu |
46 |
data.forEach(function (u) {
|
| 36788 |
ranu |
47 |
html += '<option value="' + u.id + '">' + u.name + ' (#' + u.id + ')</option>';
|
| 36621 |
ranu |
48 |
});
|
|
|
49 |
$('#bp-auth-user').html(html);
|
|
|
50 |
}
|
|
|
51 |
});
|
|
|
52 |
});
|
|
|
53 |
|
|
|
54 |
$('#bp-load').on('click', function () {
|
|
|
55 |
var uid = $('#bp-auth-user').val();
|
|
|
56 |
if (!uid) {
|
|
|
57 |
alert('Select a user');
|
|
|
58 |
return;
|
|
|
59 |
}
|
| 36785 |
ranu |
60 |
|
|
|
61 |
// Hard reset of any previous user's cached state so calendar chips,
|
|
|
62 |
// route lines, partner markers, and the route-detail view don't leak.
|
| 36621 |
ranu |
63 |
state.authUserId = parseInt(uid);
|
|
|
64 |
state.categoryId = parseInt($('#bp-category').val());
|
| 36785 |
ranu |
65 |
state.mode = 'list';
|
|
|
66 |
state.days = [];
|
|
|
67 |
state.partners = [];
|
|
|
68 |
state.currentDay = 1;
|
|
|
69 |
state.calMonth = null;
|
|
|
70 |
state.calData = null;
|
|
|
71 |
state.calSelectedBeat = null;
|
|
|
72 |
state.calPickedDates = [];
|
|
|
73 |
state.homeLat = null;
|
|
|
74 |
state.homeLng = null;
|
|
|
75 |
state.homeName = '';
|
|
|
76 |
state.activeBaseLocationId = null;
|
|
|
77 |
state.baseLocations = [];
|
|
|
78 |
state.viewDate = null;
|
|
|
79 |
state.originalDayCount = 0;
|
| 36787 |
ranu |
80 |
state.visitRequests = [];
|
|
|
81 |
state.assignVisitRequest = null;
|
| 37120 |
vikas |
82 |
state.assignedLeads = [];
|
|
|
83 |
state.assignLead = null;
|
| 36792 |
ranu |
84 |
state.deferredItems = [];
|
| 36787 |
ranu |
85 |
$('#visit-request-panel').hide();
|
| 37120 |
vikas |
86 |
$('#assigned-leads-panel').hide();
|
|
|
87 |
// A fresh load starts both collapsibles expanded.
|
|
|
88 |
$('#vr-body, #al-body').show();
|
|
|
89 |
$('#vr-caret, #al-caret').html('▾');
|
| 36792 |
ranu |
90 |
// #deferred-items-panel now sits inside its own tab pane (#panel-deferred)
|
|
|
91 |
// which controls visibility — no need to hide the inner panel here.
|
|
|
92 |
$('#di-list').empty();
|
|
|
93 |
$('#di-count').text('');
|
|
|
94 |
$('#di-tab-count').text('');
|
| 36785 |
ranu |
95 |
// Clear the rendered calendar grid + route map / panel so nothing from
|
|
|
96 |
// the previous user is briefly visible while the new fetch is in flight.
|
|
|
97 |
$('#cal-grid-content').empty();
|
|
|
98 |
$('#route-list').empty();
|
|
|
99 |
if (typeof clearRoute === 'function') {
|
|
|
100 |
try {
|
|
|
101 |
clearRoute();
|
|
|
102 |
} catch (e) {
|
|
|
103 |
}
|
|
|
104 |
}
|
|
|
105 |
if (typeof markers === 'object' && markers) {
|
|
|
106 |
for (var k in markers) {
|
|
|
107 |
try {
|
|
|
108 |
markers[k].setMap(null);
|
|
|
109 |
} catch (e) {
|
|
|
110 |
}
|
|
|
111 |
}
|
|
|
112 |
markers = {};
|
|
|
113 |
}
|
|
|
114 |
if (typeof homeMarker !== 'undefined' && homeMarker) {
|
|
|
115 |
try {
|
|
|
116 |
homeMarker.setMap(null);
|
|
|
117 |
} catch (e) {
|
|
|
118 |
}
|
|
|
119 |
homeMarker = null;
|
|
|
120 |
}
|
|
|
121 |
|
| 36621 |
ranu |
122 |
$('#bp-user-label').text($('#bp-auth-user option:selected').text());
|
|
|
123 |
|
| 36681 |
ranu |
124 |
// Load all saved base locations — default goes in state.home*, the rest is
|
|
|
125 |
// exposed via a picker in the route panel so the planner can pick a
|
|
|
126 |
// different starting point for this one beat without changing the default.
|
| 36621 |
ranu |
127 |
$.ajax({
|
| 36681 |
ranu |
128 |
url: context + "/beatPlan/listBaseLocations", type: "GET", dataType: "json",
|
| 36621 |
ranu |
129 |
data: {authUserId: uid},
|
|
|
130 |
success: function (r) {
|
|
|
131 |
var data = r.response || r;
|
| 36681 |
ranu |
132 |
state.baseLocations = data.locations || [];
|
|
|
133 |
var def = state.baseLocations.find(function (l) {
|
|
|
134 |
return l.isDefault;
|
|
|
135 |
}) || state.baseLocations[0];
|
|
|
136 |
if (def) {
|
|
|
137 |
state.homeLat = parseFloat(def.latitude);
|
|
|
138 |
state.homeLng = parseFloat(def.longitude);
|
|
|
139 |
state.homeName = def.locationName;
|
|
|
140 |
state.activeBaseLocationId = def.id;
|
|
|
141 |
} else {
|
|
|
142 |
state.activeBaseLocationId = null;
|
| 36632 |
ranu |
143 |
}
|
|
|
144 |
showBeatList();
|
| 36785 |
ranu |
145 |
// If the user is currently on the Calendar tab when Load is clicked,
|
|
|
146 |
// re-fetch immediately. Otherwise the calendar grid stays blank
|
|
|
147 |
// until they click the tab (which would auto-fetch on its own).
|
|
|
148 |
if ($('.panel-tab[data-tab="calendar"]').hasClass('active')) {
|
|
|
149 |
var now = new Date();
|
|
|
150 |
state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
|
|
|
151 |
loadCalendarData();
|
|
|
152 |
}
|
| 36787 |
ranu |
153 |
// Pending visit requests for this user's hierarchy. Hidden when empty.
|
|
|
154 |
if (typeof loadVisitRequests === 'function') {
|
|
|
155 |
loadVisitRequests();
|
|
|
156 |
}
|
| 37120 |
vikas |
157 |
// This user's active assigned leads (routable onto a beat). Hidden when empty.
|
|
|
158 |
if (typeof loadAssignedLeads === 'function') {
|
|
|
159 |
loadAssignedLeads();
|
|
|
160 |
}
|
| 36792 |
ranu |
161 |
// Deferred items (partners + leads) for this user. Hidden when empty.
|
|
|
162 |
if (typeof loadDeferredItems === 'function') {
|
|
|
163 |
loadDeferredItems();
|
|
|
164 |
}
|
| 36632 |
ranu |
165 |
}
|
|
|
166 |
});
|
| 36621 |
ranu |
167 |
});
|
|
|
168 |
|
| 36681 |
ranu |
169 |
// Called from the picker → swap the start location for the beat being
|
|
|
170 |
// created/edited. Updates state.home* + day 1's start point, then re-draws.
|
| 36698 |
ranu |
171 |
// Inline typeahead inside the route sidebar — same data source as the
|
|
|
172 |
// Partners tab (state.partners, fed by /beatPlan/getPartners, which mirrors
|
|
|
173 |
// the /getPartnerReadonlyInfo pattern used by the Partner Access tab).
|
|
|
174 |
// User types 2+ chars → suggestions appear → click adds to the current day.
|
|
|
175 |
function renderPartnerQuickAdd() {
|
|
|
176 |
if (!state.partners || state.partners.length === 0) return '';
|
|
|
177 |
return ''
|
|
|
178 |
+ '<div style="position:relative; margin-bottom:10px;">'
|
|
|
179 |
+ ' <input type="text" id="bp-partner-quick" autocomplete="off"'
|
|
|
180 |
+ ' placeholder="Search & add a partner..."'
|
|
|
181 |
+ ' style="width:100%; padding:8px 10px; border:1px solid #475569; border-radius:6px;'
|
|
|
182 |
+ ' background:#0f172a; color:#e2e8f0; font-size:12px; font-family:inherit;">'
|
|
|
183 |
+ ' <div id="bp-partner-suggest"'
|
|
|
184 |
+ ' style="display:none; position:absolute; left:0; right:0; top:100%; z-index:50;'
|
|
|
185 |
+ ' background:#1e293b; border:1px solid #334155; border-radius:6px;'
|
|
|
186 |
+ ' margin-top:4px; max-height:260px; overflow-y:auto;'
|
|
|
187 |
+ ' box-shadow:0 6px 16px rgba(0,0,0,0.4);"></div>'
|
|
|
188 |
+ '</div>';
|
|
|
189 |
}
|
|
|
190 |
|
|
|
191 |
function refreshPartnerSuggestions() {
|
|
|
192 |
var $input = $('#bp-partner-quick');
|
|
|
193 |
if (!$input.length) return;
|
|
|
194 |
var q = ($input.val() || '').trim().toLowerCase();
|
|
|
195 |
var $sug = $('#bp-partner-suggest');
|
|
|
196 |
if (q.length < 2) {
|
|
|
197 |
$sug.hide().empty();
|
|
|
198 |
return;
|
|
|
199 |
}
|
|
|
200 |
|
|
|
201 |
// Lookup of fofoId → day number for partners already on the route
|
|
|
202 |
var inRoute = {};
|
|
|
203 |
(state.days || []).forEach(function (d) {
|
|
|
204 |
(d.visits || []).forEach(function (v) {
|
|
|
205 |
if (!v.isLead && v.type !== 'lead') inRoute[v.id] = d.dayNumber;
|
|
|
206 |
});
|
|
|
207 |
});
|
|
|
208 |
|
|
|
209 |
var matches = (state.partners || []).filter(function (p) {
|
|
|
210 |
var hay = ((p.code || '') + ' '
|
|
|
211 |
+ (p.outletName || '') + ' '
|
|
|
212 |
+ (p.businessName || '') + ' '
|
|
|
213 |
+ (p.city || '')).toLowerCase();
|
|
|
214 |
return hay.indexOf(q) !== -1;
|
|
|
215 |
}).slice(0, 10);
|
|
|
216 |
|
|
|
217 |
if (matches.length === 0) {
|
|
|
218 |
$sug.html('<div style="padding:10px; color:#94a3b8; font-size:11px; text-align:center;">No partners found</div>').show();
|
|
|
219 |
return;
|
|
|
220 |
}
|
|
|
221 |
|
|
|
222 |
var html = '';
|
|
|
223 |
matches.forEach(function (p) {
|
|
|
224 |
var day = inRoute[p.fofoId];
|
|
|
225 |
var nameLine = (p.outletName || p.businessName || '');
|
|
|
226 |
if (p.city) nameLine += (nameLine ? ' - ' : '') + p.city;
|
| 36802 |
ranu |
227 |
var inactiveBadge = (p.active === false)
|
|
|
228 |
? ' <span style="background:#7c2d12;color:#fed7aa;padding:1px 6px;border-radius:3px;font-size:10px;font-weight:600;">Inactive</span>'
|
|
|
229 |
: '';
|
| 36698 |
ranu |
230 |
if (day) {
|
|
|
231 |
html += '<div class="bp-partner-row in-route" style="padding:8px 10px; font-size:11px;'
|
|
|
232 |
+ ' color:#94a3b8; border-bottom:1px solid #0f172a; cursor:not-allowed;">'
|
| 36802 |
ranu |
233 |
+ '<div style="font-weight:600;">' + (p.code || '') + inactiveBadge + '</div>'
|
| 36698 |
ranu |
234 |
+ '<div>' + nameLine + ' <span style="color:#22c55e;">(in D' + day + ')</span></div>'
|
|
|
235 |
+ '</div>';
|
|
|
236 |
} else {
|
|
|
237 |
html += '<div class="bp-partner-row" data-fofoid="' + p.fofoId + '"'
|
|
|
238 |
+ ' style="padding:8px 10px; font-size:12px; color:#e2e8f0;'
|
|
|
239 |
+ ' border-bottom:1px solid #0f172a; cursor:pointer;">'
|
| 36802 |
ranu |
240 |
+ '<div style="font-weight:600;">' + (p.code || '') + inactiveBadge + '</div>'
|
| 36698 |
ranu |
241 |
+ '<div style="font-size:11px; color:#94a3b8;">' + nameLine + '</div>'
|
|
|
242 |
+ '</div>';
|
|
|
243 |
}
|
|
|
244 |
});
|
|
|
245 |
$sug.html(html).show();
|
|
|
246 |
}
|
|
|
247 |
|
|
|
248 |
$(document).on('input', '#bp-partner-quick', refreshPartnerSuggestions);
|
|
|
249 |
|
|
|
250 |
$(document).on('focus', '#bp-partner-quick', function () {
|
|
|
251 |
if (($(this).val() || '').trim().length >= 2) refreshPartnerSuggestions();
|
|
|
252 |
});
|
|
|
253 |
|
|
|
254 |
// Hide on outside click
|
|
|
255 |
$(document).on('click', function (e) {
|
|
|
256 |
if (!$(e.target).closest('#bp-partner-quick, #bp-partner-suggest').length) {
|
|
|
257 |
$('#bp-partner-suggest').hide();
|
|
|
258 |
}
|
|
|
259 |
});
|
|
|
260 |
|
|
|
261 |
$(document).on('mousedown', '#bp-partner-suggest .bp-partner-row', function (e) {
|
|
|
262 |
if ($(this).hasClass('in-route')) return;
|
|
|
263 |
e.preventDefault(); // keep input from losing focus before we read fofoid
|
|
|
264 |
var fofoId = parseInt($(this).data('fofoid'));
|
|
|
265 |
if (!fofoId) return;
|
|
|
266 |
if (typeof addVisit === 'function') addVisit(fofoId, 'next');
|
|
|
267 |
$('#bp-partner-quick').val('').focus();
|
|
|
268 |
$('#bp-partner-suggest').hide().empty();
|
|
|
269 |
});
|
|
|
270 |
|
| 36681 |
ranu |
271 |
function renderBaseLocationPicker() {
|
|
|
272 |
var locs = state.baseLocations || [];
|
|
|
273 |
if (locs.length === 0) return '';
|
|
|
274 |
var activeId = state.activeBaseLocationId;
|
|
|
275 |
if (locs.length === 1) {
|
|
|
276 |
var only = locs[0];
|
|
|
277 |
return '<div style="font-size:11px;color:#94a3b8;margin-bottom:10px;">'
|
|
|
278 |
+ 'Starting from: <strong style="color:#e2e8f0;">' + (only.locationName || 'Home') + '</strong>'
|
|
|
279 |
+ (only.isDefault ? ' <span style="color:#22c55e;">(default)</span>' : '')
|
|
|
280 |
+ '</div>';
|
|
|
281 |
}
|
|
|
282 |
var html = '<div style="margin-bottom:10px;">'
|
|
|
283 |
+ '<label style="font-size:11px;color:#94a3b8;display:block;margin-bottom:4px;">Start this beat from:</label>'
|
|
|
284 |
+ '<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;">';
|
|
|
285 |
locs.forEach(function (l) {
|
|
|
286 |
var sel = (l.id === activeId) ? ' selected' : '';
|
|
|
287 |
var label = (l.locationName || 'Location ' + l.id) + (l.isDefault ? ' (default)' : '');
|
|
|
288 |
html += '<option value="' + l.id + '"' + sel + '>' + label + '</option>';
|
|
|
289 |
});
|
|
|
290 |
html += '</select></div>';
|
|
|
291 |
return html;
|
|
|
292 |
}
|
|
|
293 |
|
|
|
294 |
$(document).on('change', '#bp-base-picker', function () {
|
|
|
295 |
var id = parseInt($(this).val());
|
|
|
296 |
if (!id) return;
|
|
|
297 |
applyBaseLocation(id);
|
|
|
298 |
});
|
|
|
299 |
|
|
|
300 |
function applyBaseLocation(id) {
|
|
|
301 |
var loc = (state.baseLocations || []).find(function (l) {
|
|
|
302 |
return l.id === id;
|
|
|
303 |
});
|
|
|
304 |
if (!loc) return;
|
|
|
305 |
state.homeLat = parseFloat(loc.latitude);
|
|
|
306 |
state.homeLng = parseFloat(loc.longitude);
|
|
|
307 |
state.homeName = loc.locationName;
|
|
|
308 |
state.activeBaseLocationId = id;
|
|
|
309 |
|
|
|
310 |
// Only day 1 starts from the chosen base; later days start where the
|
|
|
311 |
// previous day ended (existing behavior unchanged).
|
|
|
312 |
if (state.days && state.days.length > 0) {
|
|
|
313 |
state.days[0].startLat = state.homeLat;
|
|
|
314 |
state.days[0].startLng = state.homeLng;
|
|
|
315 |
state.days[0].startName = state.homeName;
|
|
|
316 |
if (typeof recalcDay === 'function') {
|
|
|
317 |
var saved = state.currentDay;
|
|
|
318 |
state.currentDay = 1;
|
|
|
319 |
recalcDay();
|
|
|
320 |
state.currentDay = saved;
|
|
|
321 |
}
|
|
|
322 |
}
|
|
|
323 |
|
|
|
324 |
// Move the home marker on the map + recenter so the user actually sees it.
|
|
|
325 |
if (typeof google !== 'undefined' && google.maps && map) {
|
|
|
326 |
var pos = new google.maps.LatLng(state.homeLat, state.homeLng);
|
|
|
327 |
if (homeMarker) {
|
|
|
328 |
homeMarker.setPosition(pos);
|
|
|
329 |
homeMarker.setTitle('Home: ' + state.homeName);
|
|
|
330 |
} else {
|
|
|
331 |
homeMarker = new google.maps.Marker({
|
|
|
332 |
map: map, position: pos, title: 'Home: ' + state.homeName,
|
|
|
333 |
icon: {
|
|
|
334 |
url: 'https://maps.google.com/mapfiles/kml/shapes/homegardenbusiness.png',
|
|
|
335 |
scaledSize: new google.maps.Size(32, 32)
|
|
|
336 |
},
|
|
|
337 |
zIndex: 100
|
|
|
338 |
});
|
|
|
339 |
}
|
|
|
340 |
map.panTo(pos);
|
|
|
341 |
}
|
|
|
342 |
|
|
|
343 |
if (typeof drawRoute === 'function') drawRoute();
|
|
|
344 |
if (typeof renderRoutePanel === 'function') renderRoutePanel();
|
|
|
345 |
if (typeof updateBottomBar === 'function') updateBottomBar();
|
|
|
346 |
}
|
|
|
347 |
|
| 36632 |
ranu |
348 |
// ============ BEAT LIST VIEW ============
|
|
|
349 |
function showBeatList() {
|
|
|
350 |
state.mode = 'list';
|
|
|
351 |
$('#bottom-bar').hide();
|
|
|
352 |
|
|
|
353 |
// Load all beats for this user
|
|
|
354 |
var now = new Date();
|
|
|
355 |
var month = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
|
|
|
356 |
|
|
|
357 |
$.ajax({
|
|
|
358 |
url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
|
|
|
359 |
data: {authUserId: state.authUserId, month: month},
|
|
|
360 |
success: function (r) {
|
|
|
361 |
var data = r.response || r;
|
|
|
362 |
var beats = data.scheduledBeats || [];
|
|
|
363 |
|
|
|
364 |
var html = '<div style="margin-bottom:10px;">';
|
|
|
365 |
html += '<button id="btn-new-beat" style="width:100%;padding:10px;border:2px dashed #475569;border-radius:8px;background:none;color:#60a5fa;cursor:pointer;font-size:13px;font-weight:600;font-family:inherit;">+ New Beat</button>';
|
|
|
366 |
html += '</div>';
|
|
|
367 |
|
|
|
368 |
if (beats.length === 0) {
|
|
|
369 |
html += '<p style="color:#64748b;font-size:12px;text-align:center;padding:20px;">No beats created yet.</p>';
|
|
|
370 |
}
|
|
|
371 |
|
|
|
372 |
// Deduplicate by beatName
|
|
|
373 |
var seen = {};
|
|
|
374 |
beats.forEach(function (b) {
|
|
|
375 |
var key = b.beatName || b.planGroupId;
|
|
|
376 |
if (!seen[key]) {
|
|
|
377 |
seen[key] = {beat: b, count: 0};
|
|
|
378 |
}
|
|
|
379 |
seen[key].count++;
|
|
|
380 |
});
|
|
|
381 |
|
|
|
382 |
Object.keys(seen).forEach(function (key) {
|
|
|
383 |
var item = seen[key];
|
|
|
384 |
var b = item.beat;
|
|
|
385 |
var totalV = 0, totalKm = 0;
|
|
|
386 |
b.days.forEach(function (d) {
|
|
|
387 |
totalV += d.visitCount || 0;
|
|
|
388 |
totalKm += d.totalKm || 0;
|
|
|
389 |
});
|
|
|
390 |
|
|
|
391 |
var statusCls = b.status === 'running' ? 'status-running' : b.status === 'completed' ? 'status-completed' : 'status-scheduled';
|
|
|
392 |
var statusLabel = b.status === 'running' ? 'Live' : b.status === 'completed' ? 'Done' : b.status === 'unscheduled' ? 'Unscheduled' : 'Scheduled';
|
|
|
393 |
|
| 36651 |
ranu |
394 |
html += '<div class="beat-card beat-list-item" data-plangroup="' + b.planGroupId + '" style="cursor:pointer; position:relative;">';
|
| 36632 |
ranu |
395 |
html += '<div class="beat-name"><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:' + b.beatColor + ';"></span> ' + b.beatName + ' <span class="status-badge ' + statusCls + '">' + statusLabel + '</span></div>';
|
|
|
396 |
html += '<div class="beat-meta">' + b.days.length + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km';
|
|
|
397 |
if (item.count > 1) html += ' | ' + item.count + 'x scheduled';
|
|
|
398 |
html += '</div>';
|
| 36651 |
ranu |
399 |
html += '<button class="beat-edit-btn" data-plangroup="' + b.planGroupId + '" '
|
|
|
400 |
+ 'style="position:absolute;top:8px;right:8px;background:#475569;color:#e2e8f0;'
|
|
|
401 |
+ 'border:none;border-radius:4px;padding:3px 8px;font-size:10px;cursor:pointer;'
|
|
|
402 |
+ 'font-family:inherit;">Edit</button>';
|
| 36632 |
ranu |
403 |
html += '</div>';
|
|
|
404 |
});
|
|
|
405 |
|
|
|
406 |
$('#route-list').html(html);
|
|
|
407 |
}
|
|
|
408 |
});
|
|
|
409 |
}
|
|
|
410 |
|
|
|
411 |
// Click on existing beat → view its route on map
|
|
|
412 |
$(document).on('click', '.beat-list-item', function () {
|
|
|
413 |
var pg = $(this).data('plangroup');
|
|
|
414 |
viewBeat(pg);
|
|
|
415 |
});
|
|
|
416 |
|
| 36651 |
ranu |
417 |
// Edit button on a beat card → load in EDIT mode (modifiable)
|
|
|
418 |
$(document).on('click', '.beat-edit-btn', function (e) {
|
|
|
419 |
e.stopPropagation(); // don't also trigger beat-list-item view
|
|
|
420 |
var pg = $(this).data('plangroup');
|
|
|
421 |
var date = $(this).data('date') || null;
|
|
|
422 |
editBeat(String(pg), date ? String(date) : null);
|
|
|
423 |
});
|
|
|
424 |
|
|
|
425 |
// Loads a beat into edit mode — same UI as create but Finish becomes Save.
|
|
|
426 |
// When planDate is provided: leads scheduled on that date are loaded too and
|
|
|
427 |
// can be removed (template = partners stays the same across runs).
|
|
|
428 |
function editBeat(planGroupId, planDate) {
|
|
|
429 |
state.mode = 'edit';
|
|
|
430 |
state.editingBeatId = planGroupId;
|
|
|
431 |
state.editingDate = planDate || null;
|
|
|
432 |
state.viewDate = null;
|
|
|
433 |
state.savedPlanGroupId = null;
|
|
|
434 |
isSubmittingBeat = false;
|
|
|
435 |
|
|
|
436 |
$.ajax({
|
|
|
437 |
url: context + '/beatPlan/getPartners', type: 'GET', dataType: 'json',
|
|
|
438 |
data: {
|
|
|
439 |
authUserId: state.authUserId, categoryId: state.categoryId,
|
|
|
440 |
startLat: state.homeLat, startLng: state.homeLng
|
|
|
441 |
},
|
|
|
442 |
success: function (r) {
|
|
|
443 |
var pData = r.response || r;
|
|
|
444 |
state.partners = pData.partners || [];
|
|
|
445 |
var partnerMap = {};
|
|
|
446 |
state.partners.forEach(function (p) {
|
|
|
447 |
partnerMap[p.fofoId] = p;
|
|
|
448 |
});
|
|
|
449 |
|
|
|
450 |
$.ajax({
|
|
|
451 |
url: context + '/beatPlan/calendar', type: 'GET', dataType: 'json',
|
|
|
452 |
data: {authUserId: state.authUserId, month: '2020-01'},
|
|
|
453 |
success: function (r2) {
|
|
|
454 |
var calData = r2.response || r2;
|
|
|
455 |
var beat = (calData.scheduledBeats || []).find(function (b) {
|
|
|
456 |
return String(b.planGroupId) === String(planGroupId);
|
|
|
457 |
});
|
|
|
458 |
if (!beat) {
|
|
|
459 |
alert('Beat not found');
|
|
|
460 |
return;
|
|
|
461 |
}
|
|
|
462 |
state.beatTitle = beat.beatName || 'Beat';
|
|
|
463 |
|
|
|
464 |
// Pass planDate to getBeatVisits so leads for that date are included
|
|
|
465 |
var visitsData = planDate
|
|
|
466 |
? {planGroupId: planGroupId, planDate: planDate}
|
|
|
467 |
: {planGroupId: planGroupId};
|
|
|
468 |
|
|
|
469 |
$.ajax({
|
|
|
470 |
url: context + '/beatPlan/getBeatVisits', type: 'GET', dataType: 'json',
|
|
|
471 |
data: visitsData,
|
|
|
472 |
success: function (r3) {
|
|
|
473 |
var visits = (r3.response || r3) || [];
|
|
|
474 |
var dayVisitsMap = {};
|
|
|
475 |
visits.forEach(function (v) {
|
|
|
476 |
if (!dayVisitsMap[v.dayNumber]) dayVisitsMap[v.dayNumber] = [];
|
|
|
477 |
dayVisitsMap[v.dayNumber].push(v);
|
|
|
478 |
});
|
|
|
479 |
|
|
|
480 |
// Collect lead IDs so we can fetch their name/geo in parallel
|
|
|
481 |
var leadIdsToFetch = [];
|
|
|
482 |
visits.forEach(function (v) {
|
|
|
483 |
if (v.visitType === 'lead') leadIdsToFetch.push(v.fofoId);
|
|
|
484 |
});
|
|
|
485 |
|
|
|
486 |
state.days = [];
|
|
|
487 |
var seen = {};
|
|
|
488 |
beat.days.forEach(function (d) {
|
|
|
489 |
if (seen[d.dayNumber]) return;
|
|
|
490 |
seen[d.dayNumber] = true;
|
|
|
491 |
var dayVisits = (dayVisitsMap[d.dayNumber] || []).sort(function (a, b) {
|
|
|
492 |
return a.sequenceOrder - b.sequenceOrder;
|
|
|
493 |
});
|
|
|
494 |
var builtVisits = [];
|
|
|
495 |
dayVisits.forEach(function (v) {
|
|
|
496 |
if (v.visitType === 'lead') {
|
|
|
497 |
builtVisits.push({
|
|
|
498 |
id: v.fofoId, type: 'lead',
|
|
|
499 |
name: 'LEAD #' + v.fofoId, code: 'LEAD',
|
|
|
500 |
lat: null, lng: null, isLead: true
|
|
|
501 |
});
|
| 36811 |
ranu |
502 |
} else if (v.visitType === 'office') {
|
|
|
503 |
// Office stops are enriched server-side because the partner
|
|
|
504 |
// map doesn't cover them. Lat/lng come from logistics.company_office.
|
|
|
505 |
builtVisits.push({
|
|
|
506 |
id: v.fofoId, type: 'office',
|
|
|
507 |
name: (v.code ? v.code + ' - ' : '') + (v.name || 'Office #' + v.fofoId),
|
|
|
508 |
code: v.code || 'OFFICE',
|
|
|
509 |
lat: v.latitude ? parseFloat(v.latitude) : null,
|
|
|
510 |
lng: v.longitude ? parseFloat(v.longitude) : null,
|
|
|
511 |
isOffice: true
|
|
|
512 |
});
|
| 36651 |
ranu |
513 |
} else {
|
|
|
514 |
var p = partnerMap[v.fofoId];
|
|
|
515 |
if (p) builtVisits.push({
|
|
|
516 |
id: p.fofoId, type: 'partner',
|
|
|
517 |
name: p.code + ' - ' + (p.outletName || p.businessName || ''),
|
|
|
518 |
code: p.code,
|
|
|
519 |
lat: p.latitude ? parseFloat(p.latitude) : null,
|
|
|
520 |
lng: p.longitude ? parseFloat(p.longitude) : null
|
|
|
521 |
});
|
|
|
522 |
}
|
|
|
523 |
});
|
| 37060 |
vikas |
524 |
// Day 1 starts at the beat's own saved start location when set;
|
|
|
525 |
// otherwise fall back to the user's default base. Later days
|
|
|
526 |
// start where the prior day ended (handled by the Day Break flow).
|
|
|
527 |
var dayStartLat = state.homeLat, dayStartLng = state.homeLng, dayStartName = state.homeName;
|
|
|
528 |
if (d.dayNumber === 1) {
|
|
|
529 |
var bLat = beat.startLatitude != null ? parseFloat(beat.startLatitude) : NaN;
|
|
|
530 |
var bLng = beat.startLongitude != null ? parseFloat(beat.startLongitude) : NaN;
|
|
|
531 |
if (!isNaN(bLat) && !isNaN(bLng)) {
|
|
|
532 |
dayStartLat = bLat;
|
|
|
533 |
dayStartLng = bLng;
|
|
|
534 |
dayStartName = beat.startLocationName || state.homeName;
|
|
|
535 |
}
|
|
|
536 |
}
|
| 36651 |
ranu |
537 |
state.days.push({
|
|
|
538 |
dayNumber: d.dayNumber,
|
| 37060 |
vikas |
539 |
startLat: dayStartLat, startLng: dayStartLng,
|
|
|
540 |
startName: dayStartName,
|
| 36651 |
ranu |
541 |
visits: builtVisits,
|
| 36711 |
ranu |
542 |
endAction: d.endAction || (d.dayNumber < beat.days.length ? 'DAYBREAK' : 'HOME'),
|
| 36651 |
ranu |
543 |
totalKm: 0, totalMins: 0
|
|
|
544 |
});
|
|
|
545 |
});
|
|
|
546 |
state.currentDay = 1;
|
|
|
547 |
|
| 36681 |
ranu |
548 |
// Snapshot the originals — used during save to detect day-count
|
|
|
549 |
// increases (blocked) and lead removals (popup with cancel/reschedule).
|
|
|
550 |
state.originalDayCount = state.days.length;
|
|
|
551 |
state.originalLeads = [];
|
|
|
552 |
state.days.forEach(function (d) {
|
|
|
553 |
d.visits.forEach(function (v) {
|
|
|
554 |
if (v.type === 'lead' || v.isLead) {
|
|
|
555 |
state.originalLeads.push({
|
|
|
556 |
leadId: v.id,
|
|
|
557 |
dayNumber: d.dayNumber,
|
|
|
558 |
name: v.name || ('Lead #' + v.id)
|
|
|
559 |
});
|
|
|
560 |
}
|
|
|
561 |
});
|
|
|
562 |
});
|
|
|
563 |
|
| 36651 |
ranu |
564 |
// Async enrich each lead with name + geo
|
|
|
565 |
var leadPromises = [];
|
|
|
566 |
state.days.forEach(function (day) {
|
|
|
567 |
day.visits.forEach(function (v) {
|
|
|
568 |
if (!v.isLead) return;
|
|
|
569 |
leadPromises.push(
|
|
|
570 |
$.get(context + '/lead-geo/check/' + v.id).then(function (rr) {
|
|
|
571 |
var g = rr.response || rr;
|
|
|
572 |
if (g && g.hasApprovedGeo) {
|
|
|
573 |
v.lat = g.latitude;
|
|
|
574 |
v.lng = g.longitude;
|
|
|
575 |
}
|
|
|
576 |
}),
|
|
|
577 |
$.get(context + '/getLead?leadId=' + v.id).then(function (rr) {
|
|
|
578 |
var l = rr.response || rr;
|
| 36681 |
ranu |
579 |
if (l && l.firstName) {
|
|
|
580 |
var nm = 'LEAD - ' + l.firstName + ' ' + (l.lastName || '');
|
|
|
581 |
v.name = nm;
|
|
|
582 |
// Keep the originalLeads snapshot label in sync so the
|
|
|
583 |
// removed-leads popup shows the real name, not 'LEAD #123'.
|
|
|
584 |
(state.originalLeads || []).forEach(function (ol) {
|
|
|
585 |
if (ol.leadId === v.id) ol.name = nm;
|
|
|
586 |
});
|
|
|
587 |
}
|
| 36651 |
ranu |
588 |
})
|
|
|
589 |
);
|
|
|
590 |
});
|
|
|
591 |
});
|
|
|
592 |
|
|
|
593 |
$.when.apply($, leadPromises).always(function () {
|
| 36681 |
ranu |
594 |
// Pre-select whichever saved base location matches the beat's
|
|
|
595 |
// existing day-1 start (lat/lng), so the picker reflects current state.
|
|
|
596 |
var day1 = state.days[0];
|
|
|
597 |
if (day1 && state.baseLocations) {
|
|
|
598 |
var match = state.baseLocations.find(function (l) {
|
|
|
599 |
return Math.abs(parseFloat(l.latitude) - day1.startLat) < 1e-4
|
|
|
600 |
&& Math.abs(parseFloat(l.longitude) - day1.startLng) < 1e-4;
|
|
|
601 |
});
|
|
|
602 |
if (match) state.activeBaseLocationId = match.id;
|
|
|
603 |
}
|
|
|
604 |
|
| 36700 |
ranu |
605 |
// renderRoutePanel now builds the edit header (back btn, EDITING
|
|
|
606 |
// title, date label, base picker, partner search) inline — so we
|
|
|
607 |
// just call it without the one-time prepend that used to live here.
|
| 36651 |
ranu |
608 |
renderRoutePanel();
|
|
|
609 |
|
|
|
610 |
initMap();
|
|
|
611 |
// Add lead markers (orange) on the map
|
|
|
612 |
state.days.forEach(function (day) {
|
|
|
613 |
day.visits.forEach(function (v) {
|
|
|
614 |
if (v.isLead && v.lat && v.lng) {
|
|
|
615 |
var m = new google.maps.Marker({
|
|
|
616 |
map: map,
|
|
|
617 |
position: {lat: v.lat, lng: v.lng},
|
|
|
618 |
title: v.name,
|
|
|
619 |
label: {text: 'L', color: '#fff', fontSize: '11px', fontWeight: '700'},
|
|
|
620 |
icon: {
|
|
|
621 |
path: google.maps.SymbolPath.CIRCLE, scale: 16,
|
|
|
622 |
fillColor: '#e67e22', fillOpacity: 0.95,
|
|
|
623 |
strokeColor: '#fff', strokeWeight: 2
|
|
|
624 |
}
|
|
|
625 |
});
|
|
|
626 |
markers[v.id] = m;
|
|
|
627 |
}
|
|
|
628 |
});
|
|
|
629 |
});
|
|
|
630 |
resetMarkerColors();
|
|
|
631 |
drawRoute();
|
|
|
632 |
updateBottomBar();
|
|
|
633 |
$('#bottom-bar').show(); // make Save bar visible
|
|
|
634 |
$('#btn-finish').text('Save Changes').prop('disabled', false);
|
|
|
635 |
});
|
|
|
636 |
}
|
|
|
637 |
});
|
|
|
638 |
}
|
|
|
639 |
});
|
|
|
640 |
}
|
|
|
641 |
});
|
|
|
642 |
}
|
|
|
643 |
|
| 36644 |
ranu |
644 |
function viewBeat(planGroupId, planDate) {
|
| 36632 |
ranu |
645 |
state.mode = 'view';
|
| 36644 |
ranu |
646 |
state.viewDate = planDate || null; // when set, show that run's leads
|
| 36632 |
ranu |
647 |
$('#bottom-bar').hide();
|
|
|
648 |
|
|
|
649 |
// Load partners first (for coordinates), then load beat visits
|
|
|
650 |
$.ajax({
|
|
|
651 |
url: context + "/beatPlan/getPartners", type: "GET", dataType: "json",
|
|
|
652 |
data: {
|
|
|
653 |
authUserId: state.authUserId,
|
|
|
654 |
categoryId: state.categoryId,
|
|
|
655 |
startLat: state.homeLat,
|
|
|
656 |
startLng: state.homeLng
|
|
|
657 |
},
|
|
|
658 |
success: function (r) {
|
|
|
659 |
var pData = r.response || r;
|
|
|
660 |
state.partners = pData.partners || [];
|
|
|
661 |
|
|
|
662 |
// Build partner lookup by fofoId
|
|
|
663 |
var partnerMap = {};
|
|
|
664 |
state.partners.forEach(function (p) {
|
|
|
665 |
partnerMap[p.fofoId] = p;
|
|
|
666 |
});
|
|
|
667 |
|
|
|
668 |
// Load beat visits
|
|
|
669 |
$.ajax({
|
|
|
670 |
url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
|
|
|
671 |
data: {authUserId: state.authUserId, month: '2020-01'},
|
|
|
672 |
success: function (r2) {
|
|
|
673 |
var calData = r2.response || r2;
|
|
|
674 |
var beat = (calData.scheduledBeats || []).find(function (b) {
|
| 36644 |
ranu |
675 |
return String(b.planGroupId) === String(planGroupId);
|
| 36632 |
ranu |
676 |
});
|
|
|
677 |
if (!beat) {
|
|
|
678 |
alert('Beat not found');
|
|
|
679 |
return;
|
|
|
680 |
}
|
|
|
681 |
|
|
|
682 |
// Reconstruct state.days from beat data for map drawing
|
|
|
683 |
// We need actual visit fofoIds — fetch from beat_plan records
|
|
|
684 |
$.ajax({
|
|
|
685 |
url: context + "/beatPlan/getBeatVisits", type: "GET", dataType: "json",
|
| 36644 |
ranu |
686 |
data: planDate
|
|
|
687 |
? {planGroupId: planGroupId, planDate: planDate}
|
|
|
688 |
: {planGroupId: planGroupId},
|
| 36632 |
ranu |
689 |
success: function (r3) {
|
|
|
690 |
var visits = (r3.response || r3) || [];
|
|
|
691 |
|
|
|
692 |
// Group visits by dayNumber
|
|
|
693 |
var dayVisitsMap = {};
|
|
|
694 |
visits.forEach(function (v) {
|
|
|
695 |
if (!dayVisitsMap[v.dayNumber]) dayVisitsMap[v.dayNumber] = [];
|
|
|
696 |
dayVisitsMap[v.dayNumber].push(v);
|
|
|
697 |
});
|
|
|
698 |
|
| 36644 |
ranu |
699 |
// Build days for route display.
|
|
|
700 |
// A beat scheduled on multiple dates has multiple schedule
|
|
|
701 |
// rows per day_number — dedupe so each day appears once.
|
| 36632 |
ranu |
702 |
state.days = [];
|
| 36644 |
ranu |
703 |
var seenDayNumbers = {};
|
| 36632 |
ranu |
704 |
beat.days.forEach(function (d) {
|
| 36644 |
ranu |
705 |
if (seenDayNumbers[d.dayNumber]) return; // skip duplicate day
|
|
|
706 |
seenDayNumbers[d.dayNumber] = true;
|
|
|
707 |
|
| 36632 |
ranu |
708 |
var dayVisits = (dayVisitsMap[d.dayNumber] || []).sort(function (a, b) {
|
|
|
709 |
return a.sequenceOrder - b.sequenceOrder;
|
|
|
710 |
});
|
|
|
711 |
var startLat = state.homeLat, startLng = state.homeLng, startName = state.homeName;
|
|
|
712 |
|
|
|
713 |
// If not first day, start from previous day's last visit
|
|
|
714 |
if (state.days.length > 0) {
|
|
|
715 |
var prevDay = state.days[state.days.length - 1];
|
|
|
716 |
if (prevDay.visits.length > 0 && prevDay.endAction === 'DAYBREAK') {
|
|
|
717 |
var lastV = prevDay.visits[prevDay.visits.length - 1];
|
|
|
718 |
startLat = lastV.lat;
|
|
|
719 |
startLng = lastV.lng;
|
|
|
720 |
startName = lastV.code || lastV.name;
|
|
|
721 |
}
|
|
|
722 |
}
|
|
|
723 |
|
|
|
724 |
var builtVisits = [];
|
|
|
725 |
dayVisits.forEach(function (v) {
|
| 36642 |
ranu |
726 |
if (v.visitType === 'lead') {
|
| 36644 |
ranu |
727 |
builtVisits.push({
|
| 36642 |
ranu |
728 |
id: v.fofoId, type: 'lead',
|
|
|
729 |
name: 'LEAD #' + v.fofoId,
|
|
|
730 |
code: 'LEAD',
|
|
|
731 |
lat: null, lng: null,
|
|
|
732 |
isLead: true
|
| 36632 |
ranu |
733 |
});
|
| 36811 |
ranu |
734 |
} else if (v.visitType === 'office') {
|
|
|
735 |
builtVisits.push({
|
|
|
736 |
id: v.fofoId, type: 'office',
|
|
|
737 |
name: (v.code ? v.code + ' - ' : '') + (v.name || 'Office #' + v.fofoId),
|
|
|
738 |
code: v.code || 'OFFICE',
|
|
|
739 |
lat: v.latitude ? parseFloat(v.latitude) : null,
|
|
|
740 |
lng: v.longitude ? parseFloat(v.longitude) : null,
|
|
|
741 |
isOffice: true
|
|
|
742 |
});
|
| 36642 |
ranu |
743 |
} else {
|
|
|
744 |
var p = partnerMap[v.fofoId];
|
|
|
745 |
if (p) {
|
|
|
746 |
builtVisits.push({
|
|
|
747 |
id: p.fofoId, type: 'partner',
|
|
|
748 |
name: p.code + ' - ' + (p.outletName || p.businessName || ''),
|
|
|
749 |
code: p.code,
|
|
|
750 |
lat: p.latitude ? parseFloat(p.latitude) : null,
|
|
|
751 |
lng: p.longitude ? parseFloat(p.longitude) : null
|
|
|
752 |
});
|
|
|
753 |
}
|
| 36632 |
ranu |
754 |
}
|
|
|
755 |
});
|
|
|
756 |
|
|
|
757 |
state.days.push({
|
|
|
758 |
dayNumber: d.dayNumber,
|
|
|
759 |
startLat: startLat,
|
|
|
760 |
startLng: startLng,
|
|
|
761 |
startName: startName,
|
|
|
762 |
visits: builtVisits,
|
| 36711 |
ranu |
763 |
// Use the actual end_action stored on this schedule row.
|
|
|
764 |
// The dayNumber-based inference is wrong for multi-instance
|
|
|
765 |
// beats (same beat scheduled on several dates) — every
|
|
|
766 |
// instance would have day N but only one was treated HOME.
|
|
|
767 |
endAction: d.endAction || (d.dayNumber < beat.days.length ? 'DAYBREAK' : 'HOME'),
|
| 36632 |
ranu |
768 |
totalKm: d.totalKm || 0,
|
|
|
769 |
totalMins: d.totalMins || 0
|
|
|
770 |
});
|
|
|
771 |
});
|
|
|
772 |
|
|
|
773 |
state.currentDay = 1;
|
|
|
774 |
|
| 36644 |
ranu |
775 |
// Collect all lead data promises across all days
|
|
|
776 |
var allLeadPromises = [];
|
|
|
777 |
state.days.forEach(function (d) {
|
|
|
778 |
d.visits.forEach(function (v) {
|
|
|
779 |
if (v.isLead) {
|
|
|
780 |
allLeadPromises.push(
|
|
|
781 |
$.get(context + '/lead-geo/check/' + v.id).then(function (resp) {
|
|
|
782 |
var geoData = resp.response || resp;
|
|
|
783 |
if (geoData && geoData.hasApprovedGeo) {
|
|
|
784 |
v.lat = geoData.latitude;
|
|
|
785 |
v.lng = geoData.longitude;
|
|
|
786 |
}
|
|
|
787 |
})
|
|
|
788 |
);
|
|
|
789 |
allLeadPromises.push(
|
|
|
790 |
$.get(context + '/getLead?leadId=' + v.id).then(function (resp) {
|
|
|
791 |
var lead = resp.response || resp;
|
|
|
792 |
if (lead && lead.firstName) {
|
|
|
793 |
v.name = 'LEAD - ' + lead.firstName + ' ' + (lead.lastName || '');
|
|
|
794 |
}
|
|
|
795 |
})
|
|
|
796 |
);
|
|
|
797 |
}
|
|
|
798 |
});
|
|
|
799 |
});
|
| 36632 |
ranu |
800 |
|
| 36644 |
ranu |
801 |
// Wait for all lead data, then render
|
|
|
802 |
$.when.apply($, allLeadPromises).always(function () {
|
|
|
803 |
// Render left panel
|
|
|
804 |
state.mode = 'view';
|
|
|
805 |
renderRoutePanel();
|
|
|
806 |
var dateLabel = state.viewDate
|
|
|
807 |
? '<div style="font-size:11px;color:#f59e0b;margin-bottom:8px;">Run date: ' + state.viewDate + ' (showing this day\'s leads)</div>'
|
|
|
808 |
: '<div style="font-size:11px;color:#94a3b8;margin-bottom:8px;">Beat template (partners only)</div>';
|
|
|
809 |
$('#route-list').prepend('<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 to list</button></div><div style="font-size:14px;font-weight:600;color:#60a5fa;margin-bottom:4px;">' + beat.beatName + '</div>' + dateLabel);
|
| 36632 |
ranu |
810 |
|
| 36644 |
ranu |
811 |
// Init map with all partners + lead markers
|
|
|
812 |
initMap();
|
|
|
813 |
state.days.forEach(function (d) {
|
|
|
814 |
d.visits.forEach(function (v) {
|
|
|
815 |
if (v.isLead && v.lat && v.lng) {
|
|
|
816 |
var m = new google.maps.Marker({
|
|
|
817 |
map: map,
|
|
|
818 |
position: {lat: v.lat, lng: v.lng},
|
|
|
819 |
title: v.name,
|
|
|
820 |
label: {text: 'L', color: '#fff', fontSize: '11px', fontWeight: '700'},
|
|
|
821 |
icon: {
|
|
|
822 |
path: google.maps.SymbolPath.CIRCLE,
|
|
|
823 |
scale: 16,
|
|
|
824 |
fillColor: '#e67e22',
|
|
|
825 |
fillOpacity: 0.95,
|
|
|
826 |
strokeColor: '#fff',
|
|
|
827 |
strokeWeight: 2
|
|
|
828 |
}
|
|
|
829 |
});
|
|
|
830 |
markers[v.id] = m;
|
|
|
831 |
}
|
|
|
832 |
});
|
|
|
833 |
});
|
|
|
834 |
resetMarkerColors();
|
|
|
835 |
drawRoute();
|
|
|
836 |
});
|
| 36632 |
ranu |
837 |
}
|
|
|
838 |
});
|
|
|
839 |
}
|
|
|
840 |
});
|
|
|
841 |
}
|
|
|
842 |
});
|
|
|
843 |
}
|
|
|
844 |
|
|
|
845 |
// Back to list
|
|
|
846 |
$(document).on('click', '.btn-back-to-list', function () {
|
|
|
847 |
showBeatList();
|
|
|
848 |
// Clear map
|
|
|
849 |
if (map) {
|
|
|
850 |
for (var key in markers) {
|
|
|
851 |
markers[key].setMap(null);
|
|
|
852 |
}
|
|
|
853 |
markers = {};
|
|
|
854 |
clearRoute();
|
|
|
855 |
}
|
|
|
856 |
});
|
|
|
857 |
|
|
|
858 |
// New Beat button
|
| 37120 |
vikas |
859 |
// Start the full map-first new-beat planner (shared by the big "+ New Beat"
|
|
|
860 |
// button and the "+ New beat" buttons on the visit-request / assigned-lead cards).
|
|
|
861 |
function startNewBeat() {
|
| 36632 |
ranu |
862 |
var beatTitle = prompt('Enter beat name:', '');
|
|
|
863 |
if (!beatTitle || !beatTitle.trim()) return;
|
|
|
864 |
|
|
|
865 |
state.beatTitle = beatTitle.trim();
|
|
|
866 |
state.mode = 'create';
|
| 36644 |
ranu |
867 |
state.savedPlanGroupId = null; // reset — this is a fresh beat
|
|
|
868 |
state.days = []; // clear any leftover route data
|
|
|
869 |
isSubmittingBeat = false;
|
| 36632 |
ranu |
870 |
|
| 37120 |
vikas |
871 |
// Cards live on the Route tab already, but ensure we're there when invoked.
|
|
|
872 |
$('.panel-tab[data-tab="route"]').click();
|
|
|
873 |
|
| 36632 |
ranu |
874 |
if (!state.homeLat) {
|
|
|
875 |
showHomeModal();
|
|
|
876 |
} else {
|
|
|
877 |
loadPartners();
|
|
|
878 |
}
|
| 37120 |
vikas |
879 |
}
|
| 36632 |
ranu |
880 |
|
| 37120 |
vikas |
881 |
$(document).on('click', '#btn-new-beat', startNewBeat);
|
|
|
882 |
|
| 36621 |
ranu |
883 |
// ============ HOME LOCATION MODAL ============
|
|
|
884 |
var homeMap, homeMapMarker;
|
|
|
885 |
|
|
|
886 |
function showHomeModal() {
|
|
|
887 |
$('#modal-home').addClass('show');
|
|
|
888 |
setTimeout(function () {
|
|
|
889 |
if (!homeMap) {
|
|
|
890 |
homeMap = new google.maps.Map(document.getElementById('home-map'), {
|
|
|
891 |
center: {lat: 28.6, lng: 77.2},
|
|
|
892 |
zoom: 6
|
|
|
893 |
});
|
|
|
894 |
var ac = new google.maps.places.Autocomplete(document.getElementById('home-search'), {componentRestrictions: {country: 'in'}});
|
|
|
895 |
homeMapMarker = new google.maps.Marker({map: homeMap, draggable: true});
|
|
|
896 |
|
|
|
897 |
ac.addListener('place_changed', function () {
|
|
|
898 |
var p = ac.getPlace();
|
|
|
899 |
if (!p.geometry) return;
|
|
|
900 |
homeMap.setCenter(p.geometry.location);
|
|
|
901 |
homeMap.setZoom(14);
|
|
|
902 |
homeMapMarker.setPosition(p.geometry.location);
|
|
|
903 |
state.homeLat = p.geometry.location.lat();
|
|
|
904 |
state.homeLng = p.geometry.location.lng();
|
|
|
905 |
state.homeName = p.name || p.formatted_address || '';
|
|
|
906 |
});
|
|
|
907 |
homeMapMarker.addListener('dragend', function () {
|
|
|
908 |
state.homeLat = homeMapMarker.getPosition().lat();
|
|
|
909 |
state.homeLng = homeMapMarker.getPosition().lng();
|
|
|
910 |
});
|
|
|
911 |
homeMap.addListener('click', function (e) {
|
|
|
912 |
homeMapMarker.setPosition(e.latLng);
|
|
|
913 |
state.homeLat = e.latLng.lat();
|
|
|
914 |
state.homeLng = e.latLng.lng();
|
|
|
915 |
});
|
|
|
916 |
}
|
|
|
917 |
}, 200);
|
|
|
918 |
}
|
|
|
919 |
|
|
|
920 |
$('#home-cancel').on('click', function () {
|
|
|
921 |
$('#modal-home').removeClass('show');
|
|
|
922 |
});
|
|
|
923 |
$('#home-confirm').on('click', function () {
|
|
|
924 |
if (!state.homeLat) {
|
|
|
925 |
alert('Please select a location');
|
|
|
926 |
return;
|
|
|
927 |
}
|
|
|
928 |
$('#modal-home').removeClass('show');
|
|
|
929 |
$.ajax({
|
|
|
930 |
url: context + "/beatPlan/saveBaseLocation", type: "POST",
|
|
|
931 |
data: {
|
|
|
932 |
authUserId: state.authUserId,
|
|
|
933 |
locationName: state.homeName || 'Home',
|
|
|
934 |
latitude: state.homeLat,
|
|
|
935 |
longitude: state.homeLng,
|
|
|
936 |
address: $('#home-search').val()
|
|
|
937 |
}
|
|
|
938 |
});
|
|
|
939 |
loadPartners();
|
|
|
940 |
});
|
|
|
941 |
|
|
|
942 |
// ============ LOAD PARTNERS + INIT MAP ============
|
|
|
943 |
function loadPartners() {
|
|
|
944 |
$.ajax({
|
|
|
945 |
url: context + "/beatPlan/getPartners", type: "GET", dataType: "json",
|
|
|
946 |
data: {
|
|
|
947 |
authUserId: state.authUserId,
|
|
|
948 |
categoryId: state.categoryId,
|
|
|
949 |
startLat: state.homeLat,
|
|
|
950 |
startLng: state.homeLng
|
|
|
951 |
},
|
|
|
952 |
success: function (r) {
|
|
|
953 |
var data = r.response || r;
|
|
|
954 |
state.partners = data.partners || [];
|
|
|
955 |
initDay1();
|
|
|
956 |
initMap();
|
|
|
957 |
}
|
|
|
958 |
});
|
|
|
959 |
}
|
|
|
960 |
|
|
|
961 |
function initDay1() {
|
|
|
962 |
state.days = [{
|
|
|
963 |
dayNumber: 1, startLat: state.homeLat, startLng: state.homeLng, startName: state.homeName,
|
|
|
964 |
visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
|
|
|
965 |
}];
|
|
|
966 |
state.currentDay = 1;
|
| 36651 |
ranu |
967 |
if (state.mode === 'create' || state.mode === 'edit') $('#bottom-bar').show();
|
| 36621 |
ranu |
968 |
renderRoutePanel();
|
|
|
969 |
}
|
|
|
970 |
|
|
|
971 |
function initMap() {
|
|
|
972 |
var center = {lat: state.homeLat, lng: state.homeLng};
|
|
|
973 |
map = new google.maps.Map(document.getElementById('beat-map'), {
|
|
|
974 |
center: center, zoom: 9,
|
|
|
975 |
styles: [
|
|
|
976 |
{elementType: 'geometry', stylers: [{color: '#1d2c4d'}]},
|
|
|
977 |
{elementType: 'labels.text.fill', stylers: [{color: '#8ec3b9'}]},
|
|
|
978 |
{elementType: 'labels.text.stroke', stylers: [{color: '#1a3646'}]},
|
|
|
979 |
{featureType: 'road', elementType: 'geometry', stylers: [{color: '#304a7d'}]},
|
|
|
980 |
{featureType: 'water', elementType: 'geometry', stylers: [{color: '#0e1626'}]}
|
|
|
981 |
]
|
|
|
982 |
});
|
|
|
983 |
infoWindow = new google.maps.InfoWindow();
|
|
|
984 |
|
|
|
985 |
// Home marker
|
|
|
986 |
homeMarker = new google.maps.Marker({
|
|
|
987 |
map: map, position: center, title: 'Home: ' + state.homeName,
|
|
|
988 |
icon: {
|
|
|
989 |
url: 'https://maps.google.com/mapfiles/kml/shapes/homegardenbusiness.png',
|
|
|
990 |
scaledSize: new google.maps.Size(32, 32)
|
|
|
991 |
},
|
|
|
992 |
zIndex: 100
|
|
|
993 |
});
|
|
|
994 |
|
|
|
995 |
// Partner markers with name labels
|
|
|
996 |
for (var key in markers) {
|
|
|
997 |
markers[key].setMap(null);
|
|
|
998 |
}
|
|
|
999 |
markers = {};
|
|
|
1000 |
|
|
|
1001 |
state.partners.forEach(function (p) {
|
|
|
1002 |
if (!p.latitude || !p.longitude) return;
|
|
|
1003 |
var lat = parseFloat(p.latitude), lng = parseFloat(p.longitude);
|
|
|
1004 |
if (isNaN(lat) || isNaN(lng)) return;
|
|
|
1005 |
|
|
|
1006 |
var m = new google.maps.Marker({
|
|
|
1007 |
map: map, position: {lat: lat, lng: lng},
|
|
|
1008 |
title: p.code + ' - ' + (p.outletName || p.businessName || ''),
|
|
|
1009 |
label: {text: p.code || '', color: '#fff', fontSize: '10px', fontWeight: '600'},
|
|
|
1010 |
icon: {
|
|
|
1011 |
path: google.maps.SymbolPath.CIRCLE,
|
|
|
1012 |
scale: 14,
|
|
|
1013 |
fillColor: '#ef4444',
|
|
|
1014 |
fillOpacity: 0.9,
|
|
|
1015 |
strokeColor: '#fff',
|
|
|
1016 |
strokeWeight: 1
|
|
|
1017 |
}
|
|
|
1018 |
});
|
|
|
1019 |
|
|
|
1020 |
m.addListener('click', function () {
|
|
|
1021 |
showMarkerPopup(p, m);
|
|
|
1022 |
});
|
|
|
1023 |
m.addListener('mouseover', function () {
|
|
|
1024 |
showPreviewLine(p);
|
|
|
1025 |
});
|
|
|
1026 |
m.addListener('mouseout', function () {
|
|
|
1027 |
hidePreviewLine();
|
|
|
1028 |
});
|
|
|
1029 |
|
|
|
1030 |
markers[p.fofoId] = m;
|
|
|
1031 |
});
|
|
|
1032 |
|
|
|
1033 |
fitBounds();
|
|
|
1034 |
}
|
|
|
1035 |
|
|
|
1036 |
// ============ HOVER PREVIEW ============
|
|
|
1037 |
function showPreviewLine(partner) {
|
|
|
1038 |
if (!partner.latitude || !partner.longitude) return;
|
|
|
1039 |
var day = curDay();
|
|
|
1040 |
var lastPt = day.visits.length > 0
|
|
|
1041 |
? {lat: day.visits[day.visits.length - 1].lat, lng: day.visits[day.visits.length - 1].lng}
|
|
|
1042 |
: {lat: day.startLat, lng: day.startLng};
|
|
|
1043 |
if (!lastPt.lat) return;
|
|
|
1044 |
|
|
|
1045 |
var endPt = {lat: parseFloat(partner.latitude), lng: parseFloat(partner.longitude)};
|
|
|
1046 |
if (previewLine) previewLine.setMap(null);
|
|
|
1047 |
previewLine = new google.maps.Polyline({
|
|
|
1048 |
path: [lastPt, endPt],
|
|
|
1049 |
geodesic: true,
|
|
|
1050 |
strokeColor: '#60a5fa',
|
|
|
1051 |
strokeOpacity: 0.5,
|
|
|
1052 |
strokeWeight: 2,
|
|
|
1053 |
strokePattern: [10, 5],
|
|
|
1054 |
icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
|
|
|
1055 |
map: map
|
|
|
1056 |
});
|
|
|
1057 |
}
|
|
|
1058 |
|
|
|
1059 |
function hidePreviewLine() {
|
|
|
1060 |
if (previewLine) {
|
|
|
1061 |
previewLine.setMap(null);
|
|
|
1062 |
previewLine = null;
|
|
|
1063 |
}
|
|
|
1064 |
}
|
|
|
1065 |
|
|
|
1066 |
// ============ MARKER CLICK POPUP ============
|
|
|
1067 |
function showMarkerPopup(partner, marker) {
|
|
|
1068 |
var day = curDay();
|
|
|
1069 |
var alreadyAdded = day.visits.some(function (v) {
|
|
|
1070 |
return v.id === partner.fofoId;
|
|
|
1071 |
});
|
|
|
1072 |
if (alreadyAdded) {
|
|
|
1073 |
infoWindow.setContent('<div class="marker-popup"><h4>' + partner.code + '</h4><p>Already in today\'s route</p></div>');
|
|
|
1074 |
infoWindow.open(map, marker);
|
|
|
1075 |
return;
|
|
|
1076 |
}
|
|
|
1077 |
|
|
|
1078 |
var lastPt = day.visits.length > 0
|
|
|
1079 |
? {lat: day.visits[day.visits.length - 1].lat, lng: day.visits[day.visits.length - 1].lng}
|
|
|
1080 |
: {lat: day.startLat, lng: day.startLng};
|
|
|
1081 |
|
|
|
1082 |
var dist = 0, travelMins = 0;
|
|
|
1083 |
if (lastPt.lat && partner.latitude) {
|
|
|
1084 |
dist = haversine(lastPt.lat, lastPt.lng, parseFloat(partner.latitude), parseFloat(partner.longitude)) * ROAD_FACTOR;
|
|
|
1085 |
travelMins = (dist / AVG_SPEED) * 60;
|
|
|
1086 |
}
|
|
|
1087 |
|
|
|
1088 |
var name = partner.outletName || partner.businessName || '';
|
|
|
1089 |
var addr = partner.address || '';
|
|
|
1090 |
|
|
|
1091 |
var html = '<div class="marker-popup">';
|
|
|
1092 |
html += '<h4>' + partner.code + ' - ' + name + '</h4>';
|
|
|
1093 |
html += '<div class="popup-meta">' + addr + '</div>';
|
| 36711 |
ranu |
1094 |
html += '<div class="popup-distance">~' + dist.toFixed(1) + ' km | ~' + fmtMins(travelMins) + ' travel + ' + VISIT_MINS + ' Minutes Discussion Time</div>';
|
| 36621 |
ranu |
1095 |
html += '<button class="popup-next" onclick="addVisit(' + partner.fofoId + ',\'next\')">Next Visit</button>';
|
|
|
1096 |
html += '<button class="popup-last" onclick="addVisit(' + partner.fofoId + ',\'last\')">Last Visit</button>';
|
|
|
1097 |
html += '</div>';
|
|
|
1098 |
|
|
|
1099 |
infoWindow.setContent(html);
|
|
|
1100 |
infoWindow.open(map, marker);
|
|
|
1101 |
}
|
|
|
1102 |
|
|
|
1103 |
// ============ ADD VISIT ============
|
|
|
1104 |
function addVisit(fofoId, action) {
|
|
|
1105 |
infoWindow.close();
|
|
|
1106 |
var p = state.partners.find(function (x) {
|
|
|
1107 |
return x.fofoId === fofoId;
|
|
|
1108 |
});
|
|
|
1109 |
if (!p) return;
|
|
|
1110 |
|
|
|
1111 |
var day = curDay();
|
|
|
1112 |
var visit = {
|
|
|
1113 |
id: p.fofoId, type: 'partner',
|
|
|
1114 |
name: p.code + ' - ' + (p.outletName || p.businessName || ''),
|
|
|
1115 |
code: p.code,
|
|
|
1116 |
lat: p.latitude ? parseFloat(p.latitude) : null,
|
|
|
1117 |
lng: p.longitude ? parseFloat(p.longitude) : null
|
|
|
1118 |
};
|
|
|
1119 |
|
|
|
1120 |
day.visits.push(visit);
|
|
|
1121 |
|
| 37060 |
vikas |
1122 |
// Auto-order the day into a nearest-neighbor route. The just-added stop may
|
|
|
1123 |
// no longer be last, so recolour + renumber every current-day marker by its
|
|
|
1124 |
// new index (resetMarkerColors handles both) rather than labelling this one.
|
|
|
1125 |
sortDayByNearestNeighbor(day);
|
|
|
1126 |
resetMarkerColors();
|
| 36621 |
ranu |
1127 |
|
|
|
1128 |
recalcDay();
|
|
|
1129 |
drawRoute();
|
|
|
1130 |
renderRoutePanel();
|
|
|
1131 |
|
|
|
1132 |
if (action === 'last') {
|
|
|
1133 |
// Last visit — end day, go home
|
|
|
1134 |
day.endAction = 'HOME';
|
|
|
1135 |
day.stayLat = state.homeLat;
|
|
|
1136 |
day.stayLng = state.homeLng;
|
|
|
1137 |
day.stayName = state.homeName;
|
|
|
1138 |
drawReturnHome();
|
|
|
1139 |
renderRoutePanel();
|
|
|
1140 |
} else if (action === 'nextday') {
|
|
|
1141 |
// Continue next day — end current day, start new day from here
|
|
|
1142 |
day.endAction = 'CONTINUE';
|
|
|
1143 |
day.stayLat = visit.lat;
|
|
|
1144 |
day.stayLng = visit.lng;
|
|
|
1145 |
day.stayName = visit.name;
|
|
|
1146 |
startNextDay(visit.lat, visit.lng, visit.name);
|
|
|
1147 |
}
|
|
|
1148 |
// 'next' — just added, continue on same day
|
|
|
1149 |
}
|
|
|
1150 |
|
|
|
1151 |
function drawReturnHome() {
|
|
|
1152 |
var day = curDay();
|
|
|
1153 |
if (day.visits.length === 0) return;
|
|
|
1154 |
var lastVisit = day.visits[day.visits.length - 1];
|
|
|
1155 |
if (!lastVisit.lat || !state.homeLat) return;
|
|
|
1156 |
|
|
|
1157 |
var line = new google.maps.Polyline({
|
|
|
1158 |
path: [{lat: lastVisit.lat, lng: lastVisit.lng}, {lat: state.homeLat, lng: state.homeLng}],
|
|
|
1159 |
geodesic: true, strokeColor: '#22c55e', strokeOpacity: 0.6, strokeWeight: 2,
|
|
|
1160 |
icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
|
|
|
1161 |
map: map
|
|
|
1162 |
});
|
|
|
1163 |
routeLines.push(line);
|
|
|
1164 |
|
|
|
1165 |
// Add return distance to day total
|
|
|
1166 |
var retDist = haversine(lastVisit.lat, lastVisit.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
|
|
|
1167 |
day.totalKm += retDist;
|
|
|
1168 |
day.totalMins += (retDist / AVG_SPEED) * 60;
|
|
|
1169 |
updateBottomBar();
|
|
|
1170 |
}
|
|
|
1171 |
|
|
|
1172 |
function startNextDay(startLat, startLng, startName) {
|
|
|
1173 |
var nextNum = state.days.length + 1;
|
|
|
1174 |
state.days.push({
|
|
|
1175 |
dayNumber: nextNum, startLat: startLat, startLng: startLng, startName: startName,
|
|
|
1176 |
visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
|
|
|
1177 |
});
|
|
|
1178 |
state.currentDay = nextNum;
|
|
|
1179 |
updateBottomBar();
|
|
|
1180 |
renderRoutePanel();
|
|
|
1181 |
// Reset marker colors for unvisited
|
|
|
1182 |
resetMarkerColors();
|
|
|
1183 |
}
|
|
|
1184 |
|
|
|
1185 |
function resetMarkerColors() {
|
|
|
1186 |
var allVisited = {};
|
|
|
1187 |
state.days.forEach(function (d) {
|
|
|
1188 |
d.visits.forEach(function (v) {
|
|
|
1189 |
allVisited[v.id] = true;
|
|
|
1190 |
});
|
|
|
1191 |
});
|
|
|
1192 |
|
|
|
1193 |
state.partners.forEach(function (p) {
|
|
|
1194 |
if (!markers[p.fofoId]) return;
|
|
|
1195 |
if (allVisited[p.fofoId]) {
|
|
|
1196 |
markers[p.fofoId].setIcon({
|
|
|
1197 |
path: google.maps.SymbolPath.CIRCLE,
|
|
|
1198 |
scale: 14,
|
|
|
1199 |
fillColor: '#22c55e',
|
|
|
1200 |
fillOpacity: 0.9,
|
|
|
1201 |
strokeColor: '#fff',
|
|
|
1202 |
strokeWeight: 1
|
|
|
1203 |
});
|
|
|
1204 |
} else {
|
|
|
1205 |
markers[p.fofoId].setIcon({
|
|
|
1206 |
path: google.maps.SymbolPath.CIRCLE,
|
|
|
1207 |
scale: 14,
|
|
|
1208 |
fillColor: '#ef4444',
|
|
|
1209 |
fillOpacity: 0.9,
|
|
|
1210 |
strokeColor: '#fff',
|
|
|
1211 |
strokeWeight: 1
|
|
|
1212 |
});
|
|
|
1213 |
markers[p.fofoId].setLabel({text: p.code || '', color: '#fff', fontSize: '10px', fontWeight: '600'});
|
|
|
1214 |
}
|
|
|
1215 |
});
|
|
|
1216 |
|
|
|
1217 |
// Re-label current day visits
|
|
|
1218 |
var day = curDay();
|
|
|
1219 |
day.visits.forEach(function (v, i) {
|
|
|
1220 |
if (markers[v.id]) markers[v.id].setLabel({
|
|
|
1221 |
text: '' + (i + 1),
|
|
|
1222 |
color: '#fff',
|
|
|
1223 |
fontSize: '11px',
|
|
|
1224 |
fontWeight: '700'
|
|
|
1225 |
});
|
|
|
1226 |
});
|
|
|
1227 |
}
|
|
|
1228 |
|
|
|
1229 |
// ============ ROUTE DRAWING ============
|
|
|
1230 |
function clearRoute() {
|
|
|
1231 |
routeLines.forEach(function (l) {
|
|
|
1232 |
l.setMap(null);
|
|
|
1233 |
});
|
|
|
1234 |
routeLines = [];
|
|
|
1235 |
distLabels.forEach(function (l) {
|
|
|
1236 |
l.setMap(null);
|
|
|
1237 |
});
|
|
|
1238 |
distLabels = [];
|
|
|
1239 |
}
|
|
|
1240 |
|
| 36632 |
ranu |
1241 |
var DAY_ROUTE_COLORS = ['#3b82f6', '#a855f7', '#06b6d4', '#ec4899', '#f59e0b', '#14b8a6'];
|
|
|
1242 |
|
| 36621 |
ranu |
1243 |
function drawRoute() {
|
|
|
1244 |
clearRoute();
|
|
|
1245 |
|
| 36632 |
ranu |
1246 |
// Draw routes for ALL days, not just current
|
|
|
1247 |
state.days.forEach(function (day, dayIdx) {
|
|
|
1248 |
var isCurrent = day.dayNumber === state.currentDay;
|
|
|
1249 |
var routeColor = DAY_ROUTE_COLORS[dayIdx % DAY_ROUTE_COLORS.length];
|
|
|
1250 |
var opacity = isCurrent ? 0.9 : 0.5;
|
|
|
1251 |
var weight = isCurrent ? 3 : 2;
|
| 36621 |
ranu |
1252 |
|
| 36632 |
ranu |
1253 |
var pts = [{lat: day.startLat, lng: day.startLng}];
|
|
|
1254 |
day.visits.forEach(function (v) {
|
|
|
1255 |
if (v.lat && v.lng) pts.push({lat: v.lat, lng: v.lng});
|
|
|
1256 |
});
|
|
|
1257 |
|
|
|
1258 |
// Draw visit route lines for this day
|
|
|
1259 |
for (var i = 0; i < pts.length - 1; i++) {
|
|
|
1260 |
var line = new google.maps.Polyline({
|
|
|
1261 |
path: [pts[i], pts[i + 1]], geodesic: true,
|
|
|
1262 |
strokeColor: routeColor, strokeOpacity: opacity, strokeWeight: weight, map: map
|
|
|
1263 |
});
|
|
|
1264 |
routeLines.push(line);
|
|
|
1265 |
|
|
|
1266 |
// Only show distance labels on current day
|
|
|
1267 |
if (isCurrent) {
|
|
|
1268 |
var d = haversine(pts[i].lat, pts[i].lng, pts[i + 1].lat, pts[i + 1].lng) * ROAD_FACTOR;
|
|
|
1269 |
var mid = {lat: (pts[i].lat + pts[i + 1].lat) / 2, lng: (pts[i].lng + pts[i + 1].lng) / 2};
|
|
|
1270 |
var lbl = new google.maps.Marker({
|
|
|
1271 |
position: mid, map: map, icon: {path: google.maps.SymbolPath.CIRCLE, scale: 0},
|
|
|
1272 |
label: {text: d.toFixed(1) + 'km', color: '#93c5fd', fontSize: '10px', fontWeight: '600'}
|
|
|
1273 |
});
|
|
|
1274 |
distLabels.push(lbl);
|
|
|
1275 |
}
|
|
|
1276 |
}
|
|
|
1277 |
|
|
|
1278 |
// Draw return-to-home line (not on DAYBREAK days)
|
|
|
1279 |
if (day.visits.length > 0 && day.endAction !== 'DAYBREAK') {
|
|
|
1280 |
var lastV = day.visits[day.visits.length - 1];
|
|
|
1281 |
if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
|
|
|
1282 |
var retLine = new google.maps.Polyline({
|
|
|
1283 |
path: [{lat: lastV.lat, lng: lastV.lng}, {lat: state.homeLat, lng: state.homeLng}],
|
|
|
1284 |
geodesic: true, strokeColor: '#22c55e', strokeOpacity: isCurrent ? 0.4 : 0.2, strokeWeight: 2,
|
|
|
1285 |
icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
|
|
|
1286 |
map: map
|
|
|
1287 |
});
|
|
|
1288 |
routeLines.push(retLine);
|
|
|
1289 |
}
|
|
|
1290 |
}
|
|
|
1291 |
});
|
| 36621 |
ranu |
1292 |
}
|
|
|
1293 |
|
|
|
1294 |
function recalcDay() {
|
|
|
1295 |
var day = curDay();
|
|
|
1296 |
var pts = [{lat: day.startLat, lng: day.startLng}];
|
|
|
1297 |
day.visits.forEach(function (v) {
|
|
|
1298 |
if (v.lat && v.lng) pts.push({lat: v.lat, lng: v.lng});
|
|
|
1299 |
});
|
|
|
1300 |
|
|
|
1301 |
var km = 0;
|
|
|
1302 |
for (var i = 0; i < pts.length - 1; i++) km += haversine(pts[i].lat, pts[i].lng, pts[i + 1].lat, pts[i + 1].lng) * ROAD_FACTOR;
|
|
|
1303 |
day.totalKm = km;
|
|
|
1304 |
day.totalMins = (km / AVG_SPEED) * 60 + day.visits.length * VISIT_MINS;
|
|
|
1305 |
updateBottomBar();
|
|
|
1306 |
}
|
|
|
1307 |
|
| 37060 |
vikas |
1308 |
// Auto-order a day's stops into a nearest-neighbor route from the day's start
|
|
|
1309 |
// point (beat start / base for day 1, prior day's end for later days). Reorders
|
|
|
1310 |
// day.visits in place; sequence = array index downstream. Stops without valid
|
|
|
1311 |
// coords (e.g. leads with no captured geo) are parked at the end in their
|
|
|
1312 |
// existing relative order rather than dropped.
|
|
|
1313 |
function sortDayByNearestNeighbor(day) {
|
|
|
1314 |
if (!day || !day.visits || day.visits.length < 2) return;
|
|
|
1315 |
|
|
|
1316 |
var routable = [], parked = [];
|
|
|
1317 |
day.visits.forEach(function (v) {
|
|
|
1318 |
if (typeof v.lat === 'number' && typeof v.lng === 'number' && !isNaN(v.lat) && !isNaN(v.lng)) routable.push(v);
|
|
|
1319 |
else parked.push(v);
|
|
|
1320 |
});
|
|
|
1321 |
|
|
|
1322 |
var haveStart = (typeof day.startLat === 'number' && typeof day.startLng === 'number' && !isNaN(day.startLat) && !isNaN(day.startLng));
|
|
|
1323 |
var ordered = [], curLat, curLng;
|
|
|
1324 |
if (haveStart) {
|
|
|
1325 |
curLat = day.startLat;
|
|
|
1326 |
curLng = day.startLng;
|
|
|
1327 |
} else if (routable.length > 0) {
|
|
|
1328 |
var first = routable.shift();
|
|
|
1329 |
ordered.push(first);
|
|
|
1330 |
curLat = first.lat;
|
|
|
1331 |
curLng = first.lng;
|
|
|
1332 |
}
|
|
|
1333 |
|
|
|
1334 |
while (routable.length > 0) {
|
|
|
1335 |
var bestIdx = 0, bestD = Infinity;
|
|
|
1336 |
for (var i = 0; i < routable.length; i++) {
|
|
|
1337 |
var d = haversine(curLat, curLng, routable[i].lat, routable[i].lng);
|
|
|
1338 |
if (d < bestD) { bestD = d; bestIdx = i; }
|
|
|
1339 |
}
|
|
|
1340 |
var next = routable.splice(bestIdx, 1)[0];
|
|
|
1341 |
ordered.push(next);
|
|
|
1342 |
curLat = next.lat;
|
|
|
1343 |
curLng = next.lng;
|
|
|
1344 |
}
|
|
|
1345 |
|
|
|
1346 |
day.visits.length = 0;
|
|
|
1347 |
ordered.forEach(function (v) { day.visits.push(v); });
|
|
|
1348 |
parked.forEach(function (v) { day.visits.push(v); });
|
|
|
1349 |
}
|
|
|
1350 |
|
| 36621 |
ranu |
1351 |
function updateBottomBar() {
|
|
|
1352 |
var day = curDay();
|
|
|
1353 |
$('#bb-day-label').text('Day ' + day.dayNumber);
|
|
|
1354 |
$('#bb-stops').text(day.visits.length + ' stops');
|
|
|
1355 |
$('#bb-distance').text(day.totalKm.toFixed(1) + ' km');
|
|
|
1356 |
$('#bb-time').text(fmtMins(day.totalMins));
|
|
|
1357 |
|
|
|
1358 |
var pct = Math.min((day.totalMins / DAY_LIMIT) * 100, 100);
|
|
|
1359 |
var col = pct < 78 ? '#22c55e' : pct < 89 ? '#f59e0b' : '#ef4444';
|
|
|
1360 |
$('#bb-progress').css({width: pct + '%', background: col});
|
|
|
1361 |
}
|
|
|
1362 |
|
|
|
1363 |
// ============ ROUTE PANEL ============
|
|
|
1364 |
function renderRoutePanel() {
|
|
|
1365 |
var html = '';
|
| 36632 |
ranu |
1366 |
if (state.mode === 'create' && state.beatTitle) {
|
|
|
1367 |
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>';
|
|
|
1368 |
html += '<div style="font-size:14px;font-weight:600;color:#60a5fa;margin-bottom:10px;">' + state.beatTitle + '</div>';
|
| 36681 |
ranu |
1369 |
html += renderBaseLocationPicker();
|
| 36698 |
ranu |
1370 |
html += renderPartnerQuickAdd();
|
| 36700 |
ranu |
1371 |
} else if (state.mode === 'edit' && state.beatTitle) {
|
|
|
1372 |
// Edit header used to be injected via a one-time prepend() in editBeat's
|
|
|
1373 |
// success callback — that meant any addVisit-triggered re-render wiped the
|
|
|
1374 |
// back button / title / base picker / search box. Building it here keeps
|
|
|
1375 |
// them present across every re-render so users can keep adding partners.
|
|
|
1376 |
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 to list</button></div>';
|
|
|
1377 |
html += '<div style="font-size:14px;font-weight:600;color:#f59e0b;margin-bottom:4px;">EDITING: ' + state.beatTitle + '</div>';
|
|
|
1378 |
html += state.editingDate
|
|
|
1379 |
? '<div style="font-size:11px;color:#f59e0b;margin-bottom:8px;">Editing run on: ' + state.editingDate + ' (leads on this date can be removed)</div>'
|
|
|
1380 |
: '<div style="font-size:11px;color:#94a3b8;margin-bottom:8px;">Editing beat template (partners only)</div>';
|
|
|
1381 |
html += renderBaseLocationPicker();
|
|
|
1382 |
html += renderPartnerQuickAdd();
|
| 36632 |
ranu |
1383 |
}
|
| 36621 |
ranu |
1384 |
state.days.forEach(function (day) {
|
|
|
1385 |
var isCurrent = day.dayNumber === state.currentDay;
|
| 36632 |
ranu |
1386 |
var isLastDay = day.dayNumber === state.days.length;
|
| 36621 |
ranu |
1387 |
html += '<div class="route-day">';
|
|
|
1388 |
html += '<div class="route-day-header">';
|
|
|
1389 |
html += '<span>Day ' + day.dayNumber + (isCurrent ? ' (current)' : '') + '</span>';
|
|
|
1390 |
html += '<span>' + fmtMins(day.totalMins) + '</span>';
|
|
|
1391 |
html += '</div>';
|
|
|
1392 |
|
|
|
1393 |
// Start point
|
| 36632 |
ranu |
1394 |
html += '<div class="route-stop route-stop-home"><span class="stop-num">H</span><div class="stop-info"><div class="stop-name">' + (day.startName || 'Home') + '</div></div></div>';
|
| 36621 |
ranu |
1395 |
|
|
|
1396 |
// Visits with distance between each
|
|
|
1397 |
var prevLat = day.startLat, prevLng = day.startLng;
|
| 36632 |
ranu |
1398 |
var isLastStop;
|
| 36621 |
ranu |
1399 |
day.visits.forEach(function (v, i) {
|
| 36632 |
ranu |
1400 |
isLastStop = (i === day.visits.length - 1);
|
|
|
1401 |
|
| 36621 |
ranu |
1402 |
var segDist = 0, segTime = 0;
|
|
|
1403 |
if (prevLat && prevLng && v.lat && v.lng) {
|
|
|
1404 |
segDist = haversine(prevLat, prevLng, v.lat, v.lng) * ROAD_FACTOR;
|
|
|
1405 |
segTime = (segDist / AVG_SPEED) * 60;
|
|
|
1406 |
}
|
|
|
1407 |
if (segDist > 0) {
|
| 36711 |
ranu |
1408 |
// Show drive ETA and discussion-time dwell separately so the
|
|
|
1409 |
// planner can see where the time goes.
|
|
|
1410 |
html += '<div class="route-segment"><span class="seg-line"></span><span class="seg-dist">'
|
|
|
1411 |
+ segDist.toFixed(1) + ' km | '
|
|
|
1412 |
+ fmtMins(segTime) + ' drive '
|
|
|
1413 |
+ '<span style="color:#94a3b8;">+ ' + VISIT_MINS + ' Minutes Discussion Time</span>'
|
|
|
1414 |
+ '</span></div>';
|
| 36621 |
ranu |
1415 |
}
|
|
|
1416 |
|
| 36642 |
ranu |
1417 |
var isLeadVisit = v.isLead || v.type === 'lead';
|
| 36711 |
ranu |
1418 |
// Outlet/business name is the primary label; code is the muted subtitle.
|
|
|
1419 |
// v.name is stored as "CODE - Outlet Name" — strip the code prefix.
|
|
|
1420 |
var outletLabel = v.name || '';
|
|
|
1421 |
if (v.code && outletLabel.indexOf(v.code + ' - ') === 0) {
|
|
|
1422 |
outletLabel = outletLabel.substring(v.code.length + 3);
|
|
|
1423 |
}
|
|
|
1424 |
if (!outletLabel) outletLabel = v.code || '';
|
| 36642 |
ranu |
1425 |
html += '<div class="route-stop route-stop-clickable" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" style="cursor:pointer;' + (isLeadVisit ? 'background:#fff3cd;border-left:3px solid #e67e22;' : '') + '">';
|
|
|
1426 |
html += '<span class="stop-num" style="' + (isLeadVisit ? 'background:#e67e22;' : '') + '">' + (i + 1) + '</span>';
|
| 36865 |
ranu |
1427 |
html += '<div class="stop-info"><div class="stop-name">' + outletLabel + (isLeadVisit ? ' <span style="background:#e67e22;color:#fff;font-size:10px;font-weight:700;padding:2px 6px;border-radius:3px;margin-left:4px;letter-spacing:0.3px;">LEAD VISIT</span>' : '') + '</div>';
|
| 36711 |
ranu |
1428 |
html += '<div class="stop-meta">' + (v.code || '') + '</div>';
|
| 36632 |
ranu |
1429 |
|
|
|
1430 |
// Show "Last Visit" + "Day Break" buttons on last stop of current day
|
|
|
1431 |
if (isLastStop && isCurrent && !day.endAction) {
|
|
|
1432 |
html += '<div style="margin-top:4px;display:flex;gap:4px;">';
|
|
|
1433 |
html += '<button class="btn-last-visit" data-daynum="' + day.dayNumber + '" style="font-size:10px;padding:2px 8px;border:none;border-radius:3px;background:#22c55e;color:#fff;cursor:pointer;font-weight:600;">Last Visit</button>';
|
|
|
1434 |
html += '<button class="btn-day-break" data-daynum="' + day.dayNumber + '" style="font-size:10px;padding:2px 8px;border:none;border-radius:3px;background:#f59e0b;color:#0f172a;cursor:pointer;font-weight:600;">Day Break</button>';
|
|
|
1435 |
html += '</div>';
|
|
|
1436 |
}
|
|
|
1437 |
|
|
|
1438 |
html += '</div>';
|
| 36651 |
ranu |
1439 |
if (state.mode === 'create' || state.mode === 'edit') {
|
| 36632 |
ranu |
1440 |
html += '<span class="stop-remove" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" title="Remove">×</span>';
|
|
|
1441 |
}
|
| 36621 |
ranu |
1442 |
html += '</div>';
|
|
|
1443 |
|
|
|
1444 |
prevLat = v.lat;
|
|
|
1445 |
prevLng = v.lng;
|
|
|
1446 |
});
|
|
|
1447 |
|
| 36632 |
ranu |
1448 |
// Show return-to-home only when day is complete (Last Visit) or still in progress
|
|
|
1449 |
if (day.visits.length > 0 && day.endAction !== 'DAYBREAK') {
|
| 36621 |
ranu |
1450 |
var lastV = day.visits[day.visits.length - 1];
|
|
|
1451 |
var retDist = 0, retTime = 0;
|
|
|
1452 |
if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
|
|
|
1453 |
retDist = haversine(lastV.lat, lastV.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
|
|
|
1454 |
retTime = (retDist / AVG_SPEED) * 60;
|
|
|
1455 |
}
|
|
|
1456 |
if (retDist > 0) {
|
| 36711 |
ranu |
1457 |
// Return-home leg = travel time only (no discussion at home).
|
|
|
1458 |
html += '<div class="route-segment"><span class="seg-line seg-line-home"></span><span class="seg-dist">'
|
|
|
1459 |
+ retDist.toFixed(1) + ' km | ' + fmtMins(retTime) + ' drive (return)'
|
|
|
1460 |
+ '</span></div>';
|
| 36621 |
ranu |
1461 |
}
|
|
|
1462 |
html += '<div class="route-stop route-stop-home"><span class="stop-num">H</span><div class="stop-info"><div class="stop-name">Home</div></div></div>';
|
|
|
1463 |
}
|
|
|
1464 |
|
| 36632 |
ranu |
1465 |
// Day break indicator
|
|
|
1466 |
if (day.endAction === 'DAYBREAK' && day.visits.length > 0) {
|
|
|
1467 |
html += '<div style="margin-left:12px;padding:4px 8px;font-size:11px;color:#f59e0b;font-weight:600;">-- Day Break (stays near last visit) --</div>';
|
|
|
1468 |
}
|
|
|
1469 |
|
| 36621 |
ranu |
1470 |
html += '<div class="day-summary">' + day.visits.length + ' stops | ' + day.totalKm.toFixed(1) + ' km</div>';
|
|
|
1471 |
html += '</div>';
|
|
|
1472 |
});
|
|
|
1473 |
|
|
|
1474 |
$('#route-list').html(html);
|
|
|
1475 |
}
|
|
|
1476 |
|
|
|
1477 |
// Click on stop name in route panel → show popup on map
|
|
|
1478 |
$(document).on('click', '.route-stop-clickable', function (e) {
|
|
|
1479 |
if ($(e.target).hasClass('stop-remove')) return; // let remove handler handle it
|
|
|
1480 |
var fofoId = parseInt($(this).data('fofoid'));
|
|
|
1481 |
if (!fofoId || !markers[fofoId]) return;
|
|
|
1482 |
|
|
|
1483 |
var marker = markers[fofoId];
|
|
|
1484 |
var p = state.partners.find(function (x) {
|
|
|
1485 |
return x.fofoId === fofoId;
|
|
|
1486 |
});
|
|
|
1487 |
if (!p) return;
|
|
|
1488 |
|
|
|
1489 |
map.panTo(marker.getPosition());
|
|
|
1490 |
map.setZoom(12);
|
|
|
1491 |
|
|
|
1492 |
var name = p.outletName || p.businessName || '';
|
|
|
1493 |
var addr = p.address || '';
|
|
|
1494 |
infoWindow.setContent('<div class="marker-popup"><h4>' + p.code + ' - ' + name + '</h4><div class="popup-meta">' + addr + '</div></div>');
|
|
|
1495 |
infoWindow.open(map, marker);
|
|
|
1496 |
});
|
|
|
1497 |
|
|
|
1498 |
// Remove stop from route panel
|
|
|
1499 |
$(document).on('click', '.stop-remove', function (e) {
|
|
|
1500 |
e.stopPropagation();
|
|
|
1501 |
var fofoId = parseInt($(this).data('fofoid'));
|
|
|
1502 |
var dayNum = parseInt($(this).data('daynum'));
|
|
|
1503 |
|
|
|
1504 |
var day = state.days[dayNum - 1];
|
|
|
1505 |
if (!day) return;
|
|
|
1506 |
|
|
|
1507 |
// Remove visit from day
|
|
|
1508 |
day.visits = day.visits.filter(function (v) {
|
|
|
1509 |
return v.id !== fofoId;
|
|
|
1510 |
});
|
|
|
1511 |
|
| 37060 |
vikas |
1512 |
// Re-route the remaining stops (nearest-neighbor) before re-numbering below.
|
|
|
1513 |
sortDayByNearestNeighbor(day);
|
|
|
1514 |
|
| 36621 |
ranu |
1515 |
// Reset marker back to red (unvisited)
|
|
|
1516 |
if (markers[fofoId]) {
|
|
|
1517 |
var p = state.partners.find(function (x) {
|
|
|
1518 |
return x.fofoId === fofoId;
|
|
|
1519 |
});
|
|
|
1520 |
markers[fofoId].setIcon({
|
|
|
1521 |
path: google.maps.SymbolPath.CIRCLE, scale: 14,
|
|
|
1522 |
fillColor: '#ef4444', fillOpacity: 0.9, strokeColor: '#fff', strokeWeight: 1
|
|
|
1523 |
});
|
|
|
1524 |
markers[fofoId].setLabel({text: (p ? p.code : '') || '', color: '#fff', fontSize: '10px', fontWeight: '600'});
|
|
|
1525 |
}
|
|
|
1526 |
|
|
|
1527 |
// Re-number remaining markers for this day
|
|
|
1528 |
day.visits.forEach(function (v, i) {
|
|
|
1529 |
if (markers[v.id]) {
|
|
|
1530 |
markers[v.id].setLabel({text: '' + (i + 1), color: '#fff', fontSize: '11px', fontWeight: '700'});
|
|
|
1531 |
}
|
|
|
1532 |
});
|
|
|
1533 |
|
|
|
1534 |
recalcDay();
|
|
|
1535 |
drawRoute();
|
|
|
1536 |
renderRoutePanel();
|
|
|
1537 |
updateBottomBar();
|
|
|
1538 |
});
|
|
|
1539 |
|
| 36632 |
ranu |
1540 |
// ============ LAST VISIT (from sidebar) — marks beat as complete, return home ============
|
|
|
1541 |
$(document).on('click', '.btn-last-visit', function (e) {
|
|
|
1542 |
e.stopPropagation();
|
|
|
1543 |
var dayNum = parseInt($(this).data('daynum'));
|
|
|
1544 |
var day = state.days[dayNum - 1];
|
|
|
1545 |
if (!day || day.visits.length === 0) return;
|
| 36621 |
ranu |
1546 |
|
| 36632 |
ranu |
1547 |
var lastVisit = day.visits[day.visits.length - 1];
|
|
|
1548 |
if (!confirm('Mark ' + (lastVisit.code || lastVisit.name) + ' as last visit? Beat will end here and return home.')) return;
|
| 36621 |
ranu |
1549 |
|
| 36632 |
ranu |
1550 |
day.endAction = 'HOME';
|
|
|
1551 |
drawRoute();
|
|
|
1552 |
renderRoutePanel();
|
|
|
1553 |
updateBottomBar();
|
| 36621 |
ranu |
1554 |
});
|
|
|
1555 |
|
| 36632 |
ranu |
1556 |
// ============ DAY BREAK (from sidebar) — end day, next day starts from last visit ============
|
|
|
1557 |
$(document).on('click', '.btn-day-break', function (e) {
|
|
|
1558 |
e.stopPropagation();
|
|
|
1559 |
var dayNum = parseInt($(this).data('daynum'));
|
|
|
1560 |
var day = state.days[dayNum - 1];
|
|
|
1561 |
if (!day || day.visits.length === 0) return;
|
| 36621 |
ranu |
1562 |
|
| 36632 |
ranu |
1563 |
var lastVisit = day.visits[day.visits.length - 1];
|
|
|
1564 |
if (!confirm('Day Break at ' + (lastVisit.code || lastVisit.name) + '? Next day will start from this location.')) return;
|
|
|
1565 |
|
|
|
1566 |
day.endAction = 'DAYBREAK';
|
|
|
1567 |
|
|
|
1568 |
// Next day starts from LAST VISIT location (not home)
|
|
|
1569 |
var nextNum = state.days.length + 1;
|
|
|
1570 |
state.days.push({
|
|
|
1571 |
dayNumber: nextNum,
|
|
|
1572 |
startLat: lastVisit.lat, startLng: lastVisit.lng, startName: lastVisit.code || lastVisit.name,
|
|
|
1573 |
visits: [], endAction: null,
|
|
|
1574 |
stayName: null, stayLat: null, stayLng: null,
|
|
|
1575 |
totalKm: 0, totalMins: 0
|
|
|
1576 |
});
|
|
|
1577 |
state.currentDay = nextNum;
|
|
|
1578 |
|
|
|
1579 |
resetMarkerColors();
|
|
|
1580 |
drawRoute();
|
| 36621 |
ranu |
1581 |
renderRoutePanel();
|
| 36632 |
ranu |
1582 |
updateBottomBar();
|
| 36621 |
ranu |
1583 |
});
|
|
|
1584 |
|
|
|
1585 |
$('#btn-finish').on('click', function () {
|
| 36644 |
ranu |
1586 |
// Guard 1: already submitting — ignore duplicate clicks
|
|
|
1587 |
if (isSubmittingBeat) {
|
|
|
1588 |
return;
|
|
|
1589 |
}
|
|
|
1590 |
// Guard 2: this beat was already saved — don't re-submit
|
|
|
1591 |
if (state.savedPlanGroupId) {
|
|
|
1592 |
alert('This beat is already saved.');
|
|
|
1593 |
showBeatList();
|
|
|
1594 |
return;
|
|
|
1595 |
}
|
| 36621 |
ranu |
1596 |
if (state.days.every(function (d) {
|
|
|
1597 |
return d.visits.length === 0;
|
|
|
1598 |
})) {
|
|
|
1599 |
alert('No visits added');
|
|
|
1600 |
return;
|
|
|
1601 |
}
|
|
|
1602 |
|
| 36681 |
ranu |
1603 |
var beatTitle = state.beatTitle || 'Beat';
|
|
|
1604 |
var isEdit = state.mode === 'edit' && state.editingBeatId;
|
| 36621 |
ranu |
1605 |
|
| 36681 |
ranu |
1606 |
// EDIT RULE: cannot grow day count. Caught client-side so the modal
|
|
|
1607 |
// doesn't even open; server enforces the same rule.
|
|
|
1608 |
if (isEdit && state.originalDayCount && state.days.length > state.originalDayCount) {
|
|
|
1609 |
alert('Cannot increase the number of days while editing.\n\n'
|
|
|
1610 |
+ 'Original: ' + state.originalDayCount + ' day(s)\n'
|
|
|
1611 |
+ 'Current: ' + state.days.length + ' day(s)\n\n'
|
|
|
1612 |
+ 'Please create a new beat for additional days.');
|
|
|
1613 |
return;
|
|
|
1614 |
}
|
|
|
1615 |
|
| 36711 |
ranu |
1616 |
// Build plan data — recompute every day's totals + per-leg distance/time
|
|
|
1617 |
// fresh from the current state. recalcDay() only refreshes the CURRENT day,
|
|
|
1618 |
// so older days could carry stale values; doing it here guarantees DB rows
|
|
|
1619 |
// match what the user sees in the panel.
|
| 36621 |
ranu |
1620 |
var daysData = [];
|
|
|
1621 |
state.days.forEach(function (day) {
|
|
|
1622 |
if (day.visits.length === 0) return;
|
| 36711 |
ranu |
1623 |
|
|
|
1624 |
// Walk the visits in order, computing each leg from the previous point.
|
|
|
1625 |
// Day always starts at (startLat, startLng); for the LAST stop, if
|
|
|
1626 |
// endAction === 'HOME' we ALSO add the return-to-home leg into the day total
|
|
|
1627 |
// (but not stored per-visit, just rolled into totalDistanceKm/totalTimeMins).
|
|
|
1628 |
var prevLat = day.startLat, prevLng = day.startLng;
|
|
|
1629 |
var dayKm = 0, dayMins = 0;
|
|
|
1630 |
var visitsOut = [];
|
|
|
1631 |
day.visits.forEach(function (v) {
|
|
|
1632 |
var legKm = null, legMins = null;
|
|
|
1633 |
if (prevLat != null && prevLng != null && v.lat != null && v.lng != null) {
|
|
|
1634 |
legKm = haversine(prevLat, prevLng, v.lat, v.lng) * ROAD_FACTOR;
|
|
|
1635 |
legMins = Math.round((legKm / AVG_SPEED) * 60);
|
|
|
1636 |
dayKm += legKm;
|
|
|
1637 |
dayMins += legMins;
|
|
|
1638 |
}
|
|
|
1639 |
visitsOut.push({
|
|
|
1640 |
id: v.id,
|
|
|
1641 |
type: v.type || 'partner',
|
|
|
1642 |
distanceFromPrevKm: legKm != null ? Number(legKm.toFixed(3)) : null,
|
|
|
1643 |
timeFromPrevMins: legMins
|
|
|
1644 |
});
|
|
|
1645 |
if (v.lat != null && v.lng != null) {
|
|
|
1646 |
prevLat = v.lat;
|
|
|
1647 |
prevLng = v.lng;
|
|
|
1648 |
}
|
|
|
1649 |
});
|
|
|
1650 |
// Return-home leg if applicable
|
|
|
1651 |
if (day.endAction === 'HOME' && day.visits.length > 0
|
|
|
1652 |
&& state.homeLat != null && state.homeLng != null) {
|
|
|
1653 |
var last = day.visits[day.visits.length - 1];
|
|
|
1654 |
if (last.lat != null && last.lng != null) {
|
|
|
1655 |
var retKm = haversine(last.lat, last.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
|
|
|
1656 |
dayKm += retKm;
|
|
|
1657 |
dayMins += (retKm / AVG_SPEED) * 60;
|
|
|
1658 |
}
|
|
|
1659 |
}
|
|
|
1660 |
// Add per-stop dwell time
|
|
|
1661 |
dayMins += day.visits.length * VISIT_MINS;
|
|
|
1662 |
|
| 36621 |
ranu |
1663 |
daysData.push({
|
|
|
1664 |
dayNumber: day.dayNumber,
|
|
|
1665 |
startLocationName: day.startName,
|
|
|
1666 |
startLatitude: day.startLat ? day.startLat.toString() : null,
|
|
|
1667 |
startLongitude: day.startLng ? day.startLng.toString() : null,
|
|
|
1668 |
endAction: day.endAction,
|
|
|
1669 |
stayLocationName: day.stayName,
|
|
|
1670 |
stayLatitude: day.stayLat ? day.stayLat.toString() : null,
|
|
|
1671 |
stayLongitude: day.stayLng ? day.stayLng.toString() : null,
|
| 36711 |
ranu |
1672 |
totalDistanceKm: Number(dayKm.toFixed(3)),
|
|
|
1673 |
totalTimeMins: Math.round(dayMins),
|
|
|
1674 |
visits: visitsOut
|
| 36621 |
ranu |
1675 |
});
|
|
|
1676 |
});
|
|
|
1677 |
|
| 36681 |
ranu |
1678 |
// EDIT RULE: detect removed leads → show popup with cancel/reschedule per lead
|
|
|
1679 |
var removedLeads = [];
|
|
|
1680 |
if (isEdit && state.originalLeads && state.originalLeads.length) {
|
|
|
1681 |
var currentLeadIds = {};
|
|
|
1682 |
state.days.forEach(function (d) {
|
|
|
1683 |
d.visits.forEach(function (v) {
|
|
|
1684 |
if (v.type === 'lead' || v.isLead) currentLeadIds[v.id] = true;
|
|
|
1685 |
});
|
|
|
1686 |
});
|
|
|
1687 |
removedLeads = state.originalLeads.filter(function (l) {
|
|
|
1688 |
return !currentLeadIds[l.leadId];
|
|
|
1689 |
});
|
| 36651 |
ranu |
1690 |
}
|
| 36681 |
ranu |
1691 |
|
|
|
1692 |
if (removedLeads.length > 0) {
|
|
|
1693 |
openRemovedLeadsModal(removedLeads, function (actions) {
|
|
|
1694 |
doFinishSubmit(daysData, beatTitle, isEdit, actions);
|
|
|
1695 |
});
|
|
|
1696 |
return;
|
|
|
1697 |
}
|
|
|
1698 |
|
|
|
1699 |
doFinishSubmit(daysData, beatTitle, isEdit, null);
|
|
|
1700 |
});
|
|
|
1701 |
|
|
|
1702 |
function doFinishSubmit(daysData, beatTitle, isEdit, removedLeadActions) {
|
|
|
1703 |
var planPayload = {
|
|
|
1704 |
days: daysData,
|
|
|
1705 |
dates: daysData.map(function () {
|
|
|
1706 |
return null;
|
|
|
1707 |
}),
|
|
|
1708 |
beatName: beatTitle.trim()
|
|
|
1709 |
};
|
|
|
1710 |
if (state.mode === 'edit' && state.editingDate) planPayload.planDate = state.editingDate;
|
|
|
1711 |
if (removedLeadActions && removedLeadActions.length) {
|
|
|
1712 |
planPayload.removedLeadActions = JSON.stringify(removedLeadActions);
|
|
|
1713 |
}
|
| 36651 |
ranu |
1714 |
var planData = JSON.stringify(planPayload);
|
| 36621 |
ranu |
1715 |
|
| 36644 |
ranu |
1716 |
isSubmittingBeat = true;
|
| 36651 |
ranu |
1717 |
$('#btn-finish').prop('disabled', true).text(isEdit ? 'Updating...' : 'Saving...');
|
| 36621 |
ranu |
1718 |
|
| 36651 |
ranu |
1719 |
var url = isEdit ? '/beatPlan/updateBeat' : '/beatPlan/submitPlan';
|
|
|
1720 |
var ajaxData = isEdit
|
|
|
1721 |
? {beatId: state.editingBeatId, planData: planData}
|
|
|
1722 |
: {authUserId: state.authUserId, planData: planData};
|
|
|
1723 |
|
| 36621 |
ranu |
1724 |
$.ajax({
|
| 36651 |
ranu |
1725 |
url: context + url,
|
| 36621 |
ranu |
1726 |
type: "POST",
|
| 36651 |
ranu |
1727 |
data: ajaxData,
|
| 36621 |
ranu |
1728 |
success: function (response) {
|
|
|
1729 |
var data = (typeof response === 'string' ? JSON.parse(response) : response);
|
|
|
1730 |
var result = data.response || data;
|
|
|
1731 |
|
|
|
1732 |
if (result.status) {
|
|
|
1733 |
state.savedPlanGroupId = result.planGroupId;
|
|
|
1734 |
$('#bottom-bar').hide();
|
|
|
1735 |
|
| 36681 |
ranu |
1736 |
if (result.duplicate) {
|
| 36644 |
ranu |
1737 |
alert('This beat already exists.');
|
| 36651 |
ranu |
1738 |
} else if (isEdit) {
|
| 36681 |
ranu |
1739 |
var extras = [];
|
|
|
1740 |
if (result.leadsCancelled) extras.push(result.leadsCancelled + ' lead(s) cancelled');
|
|
|
1741 |
if (result.leadsRescheduled) extras.push(result.leadsRescheduled + ' lead(s) rescheduled');
|
|
|
1742 |
alert('Beat "' + beatTitle + '" updated successfully'
|
|
|
1743 |
+ (extras.length ? '\n\n' + extras.join('\n') : '') + '.');
|
|
|
1744 |
} else {
|
|
|
1745 |
alert('Beat "' + beatTitle + '" saved successfully!');
|
|
|
1746 |
}
|
| 36632 |
ranu |
1747 |
|
| 36698 |
ranu |
1748 |
// Wipe in-progress beat state for this user — the next "+ New Beat"
|
|
|
1749 |
// must start from a clean slate (no leftover days/visits/title/leads).
|
| 36651 |
ranu |
1750 |
state.editingBeatId = null;
|
| 36698 |
ranu |
1751 |
state.editingDate = null;
|
|
|
1752 |
state.beatTitle = null;
|
|
|
1753 |
state.days = [];
|
|
|
1754 |
state.currentDay = 1;
|
|
|
1755 |
state.originalDayCount = null;
|
|
|
1756 |
state.originalLeads = [];
|
|
|
1757 |
state.savedPlanGroupId = null;
|
|
|
1758 |
|
|
|
1759 |
// Clear map artifacts from the just-saved route so the canvas
|
|
|
1760 |
// doesn't show stale polylines/labels behind the beat list.
|
|
|
1761 |
routeLines.forEach(function (l) {
|
|
|
1762 |
l.setMap(null);
|
|
|
1763 |
});
|
|
|
1764 |
routeLines = [];
|
|
|
1765 |
if (previewLine) {
|
|
|
1766 |
previewLine.setMap(null);
|
|
|
1767 |
previewLine = null;
|
|
|
1768 |
}
|
|
|
1769 |
if (typeof resetMarkerColors === 'function') resetMarkerColors();
|
|
|
1770 |
|
| 36651 |
ranu |
1771 |
state.mode = 'list';
|
| 36681 |
ranu |
1772 |
showBeatList();
|
| 36621 |
ranu |
1773 |
} else {
|
| 36651 |
ranu |
1774 |
$('#btn-finish').prop('disabled', false).text(isEdit ? 'Save Changes' : 'Finish Plan');
|
| 36621 |
ranu |
1775 |
alert('Error saving beat plan');
|
|
|
1776 |
}
|
| 36644 |
ranu |
1777 |
isSubmittingBeat = false;
|
| 36621 |
ranu |
1778 |
},
|
|
|
1779 |
error: function (xhr) {
|
| 36644 |
ranu |
1780 |
isSubmittingBeat = false;
|
| 36651 |
ranu |
1781 |
$('#btn-finish').prop('disabled', false).text(isEdit ? 'Save Changes' : 'Finish Plan');
|
| 36681 |
ranu |
1782 |
// Surface a clean server message if present (e.g., day-increase block, missing beat on reschedule date)
|
|
|
1783 |
var msg = xhr.responseText || xhr.statusText;
|
|
|
1784 |
try {
|
|
|
1785 |
var err = JSON.parse(xhr.responseText);
|
|
|
1786 |
if (err && err.response) {
|
|
|
1787 |
if (typeof err.response === 'string') msg = err.response;
|
|
|
1788 |
else if (err.response.message) msg = err.response.message;
|
|
|
1789 |
}
|
|
|
1790 |
} catch (_) {
|
|
|
1791 |
}
|
|
|
1792 |
alert(msg);
|
| 36621 |
ranu |
1793 |
}
|
|
|
1794 |
});
|
| 36681 |
ranu |
1795 |
}
|
| 36621 |
ranu |
1796 |
|
| 36681 |
ranu |
1797 |
// ============ REMOVED LEADS POPUP ============
|
|
|
1798 |
// Shown when an edit removes one or more leads. Per lead, the user picks:
|
|
|
1799 |
// - Cancel → mark CANCELLED
|
|
|
1800 |
// - Reschedule + date → move to whichever beat the same user has on that date
|
|
|
1801 |
// The date input is validated against /beatPlan/userBeatsOnDate so the user
|
|
|
1802 |
// sees "No beat on this date — pick another" before submit.
|
|
|
1803 |
function openRemovedLeadsModal(removedLeads, onConfirm) {
|
|
|
1804 |
$('#modal-removed-leads').remove(); // clean any prior instance
|
|
|
1805 |
|
|
|
1806 |
var rowsHtml = removedLeads.map(function (l) {
|
|
|
1807 |
return ''
|
|
|
1808 |
+ '<div class="rl-row" data-leadid="' + l.leadId + '" style="border:1px solid #334155;border-radius:6px;padding:8px;margin-bottom:8px;">'
|
|
|
1809 |
+ ' <div style="font-weight:600;color:#f1f5f9;margin-bottom:6px;">'
|
|
|
1810 |
+ ' ' + (l.name || ('Lead #' + l.leadId)) + ' <span style="color:#64748b;font-weight:400;font-size:11px;">(was on day ' + l.dayNumber + ')</span>'
|
|
|
1811 |
+ ' </div>'
|
|
|
1812 |
+ ' <label style="display:inline-flex;align-items:center;gap:6px;margin-right:14px;cursor:pointer;font-size:12px;">'
|
|
|
1813 |
+ ' <input type="radio" name="rl-act-' + l.leadId + '" value="cancel" checked> Cancel this lead'
|
|
|
1814 |
+ ' </label>'
|
|
|
1815 |
+ ' <label style="display:inline-flex;align-items:center;gap:6px;cursor:pointer;font-size:12px;">'
|
|
|
1816 |
+ ' <input type="radio" name="rl-act-' + l.leadId + '" value="reschedule"> Reschedule to another date'
|
|
|
1817 |
+ ' </label>'
|
|
|
1818 |
+ ' <div class="rl-date-wrap" style="display:none;margin-top:6px;">'
|
|
|
1819 |
+ ' <input type="date" class="rl-date" min="' + fmtDate(new Date()) + '" style="width:auto;display:inline-block;margin-bottom:0;">'
|
|
|
1820 |
+ ' <span class="rl-date-status" style="font-size:11px;margin-left:8px;"></span>'
|
|
|
1821 |
+ ' </div>'
|
|
|
1822 |
+ '</div>';
|
|
|
1823 |
}).join('');
|
|
|
1824 |
|
|
|
1825 |
var html = ''
|
|
|
1826 |
+ '<div class="modal-overlay show" id="modal-removed-leads">'
|
|
|
1827 |
+ ' <div class="modal-box" style="min-width:520px;max-width:680px;">'
|
|
|
1828 |
+ ' <h3>You removed ' + removedLeads.length + ' lead(s) from this beat</h3>'
|
|
|
1829 |
+ ' <p style="font-size:12px;color:#94a3b8;margin-bottom:12px;">'
|
|
|
1830 |
+ ' Choose what to do with each lead before saving. Reschedule moves the lead to whichever beat this user has on the selected date.'
|
|
|
1831 |
+ ' </p>'
|
|
|
1832 |
+ ' <div id="rl-list" style="max-height:55vh;overflow-y:auto;">' + rowsHtml + '</div>'
|
|
|
1833 |
+ ' <div class="modal-actions">'
|
|
|
1834 |
+ ' <button class="modal-cancel" id="rl-cancel">Cancel (keep editing)</button>'
|
|
|
1835 |
+ ' <button class="modal-confirm" id="rl-confirm">Save with these actions</button>'
|
|
|
1836 |
+ ' </div>'
|
|
|
1837 |
+ ' </div>'
|
|
|
1838 |
+ '</div>';
|
|
|
1839 |
$('body').append(html);
|
|
|
1840 |
|
|
|
1841 |
// Show/hide date picker per row
|
|
|
1842 |
$('#modal-removed-leads').on('change', 'input[type="radio"]', function () {
|
|
|
1843 |
var $row = $(this).closest('.rl-row');
|
|
|
1844 |
var isResched = $row.find('input[type="radio"]:checked').val() === 'reschedule';
|
|
|
1845 |
$row.find('.rl-date-wrap').toggle(isResched);
|
|
|
1846 |
$row.find('.rl-date-status').text('');
|
|
|
1847 |
});
|
|
|
1848 |
|
|
|
1849 |
// Validate the date as the user types it — show "no beat on this date" warning
|
|
|
1850 |
$('#modal-removed-leads').on('change', '.rl-date', function () {
|
|
|
1851 |
var $row = $(this).closest('.rl-row');
|
|
|
1852 |
var $status = $row.find('.rl-date-status');
|
|
|
1853 |
var d = $(this).val();
|
|
|
1854 |
if (!d) {
|
|
|
1855 |
$status.text('');
|
|
|
1856 |
return;
|
|
|
1857 |
}
|
|
|
1858 |
$status.text('Checking...').css('color', '#94a3b8');
|
|
|
1859 |
$.ajax({
|
|
|
1860 |
url: context + '/beatPlan/userBeatsOnDate', type: 'GET', dataType: 'json',
|
|
|
1861 |
data: {authUserId: state.authUserId, date: d},
|
|
|
1862 |
success: function (r) {
|
|
|
1863 |
var dd = r.response || r;
|
|
|
1864 |
var beats = dd.beats || [];
|
|
|
1865 |
if (beats.length === 0) {
|
|
|
1866 |
$status.text('No beat on this date — pick another date, or Cancel this lead instead.').css('color', '#ef4444');
|
|
|
1867 |
} else {
|
|
|
1868 |
var names = beats.map(function (b) {
|
|
|
1869 |
return '"' + b.beatName + '" (day ' + b.dayNumber + ')';
|
|
|
1870 |
}).join(', ');
|
|
|
1871 |
$status.text('Will attach to: ' + names).css('color', '#22c55e');
|
|
|
1872 |
}
|
|
|
1873 |
},
|
|
|
1874 |
error: function () {
|
|
|
1875 |
$status.text('Could not verify date').css('color', '#f59e0b');
|
|
|
1876 |
}
|
|
|
1877 |
});
|
|
|
1878 |
});
|
|
|
1879 |
|
|
|
1880 |
$('#modal-removed-leads').on('click', '#rl-cancel', function () {
|
|
|
1881 |
$('#modal-removed-leads').remove();
|
|
|
1882 |
// abort save — user goes back to editing
|
|
|
1883 |
});
|
|
|
1884 |
|
|
|
1885 |
$('#modal-removed-leads').on('click', '#rl-confirm', function () {
|
|
|
1886 |
var actions = [];
|
|
|
1887 |
var problems = [];
|
|
|
1888 |
$('#modal-removed-leads .rl-row').each(function () {
|
|
|
1889 |
var $row = $(this);
|
|
|
1890 |
var leadId = parseInt($row.data('leadid'));
|
|
|
1891 |
var mode = $row.find('input[type="radio"]:checked').val();
|
|
|
1892 |
if (mode === 'reschedule') {
|
|
|
1893 |
var d = $row.find('.rl-date').val();
|
|
|
1894 |
if (!d) {
|
|
|
1895 |
problems.push('Lead ' + leadId + ': pick a date or switch to Cancel');
|
|
|
1896 |
return;
|
|
|
1897 |
}
|
|
|
1898 |
actions.push({leadId: leadId, action: 'reschedule', toDate: d});
|
|
|
1899 |
} else {
|
|
|
1900 |
actions.push({leadId: leadId, action: 'cancel'});
|
|
|
1901 |
}
|
|
|
1902 |
});
|
|
|
1903 |
if (problems.length) {
|
|
|
1904 |
alert(problems.join('\n'));
|
|
|
1905 |
return;
|
|
|
1906 |
}
|
|
|
1907 |
$('#modal-removed-leads').remove();
|
|
|
1908 |
onConfirm(actions);
|
|
|
1909 |
});
|
|
|
1910 |
}
|
|
|
1911 |
|
| 36621 |
ranu |
1912 |
// ============ PANEL TABS ============
|
|
|
1913 |
$('.panel-tab').on('click', function () {
|
|
|
1914 |
var tab = $(this).data('tab');
|
|
|
1915 |
$('.panel-tab').removeClass('active');
|
|
|
1916 |
$(this).addClass('active');
|
|
|
1917 |
$('#panel-route').toggle(tab === 'route');
|
|
|
1918 |
$('#panel-calendar').toggle(tab === 'calendar');
|
| 36792 |
ranu |
1919 |
$('#panel-deferred').toggle(tab === 'deferred');
|
| 36621 |
ranu |
1920 |
$('#cal-grid-container').toggle(tab === 'calendar');
|
| 36698 |
ranu |
1921 |
$('#bottom-bar').toggle(tab === 'route' && (state.mode === 'create' || state.mode === 'edit') && state.days && state.days.length > 0);
|
| 36621 |
ranu |
1922 |
|
|
|
1923 |
if (tab === 'calendar' && !state.calMonth) {
|
|
|
1924 |
var now = new Date();
|
|
|
1925 |
state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
|
|
|
1926 |
loadCalendarData();
|
|
|
1927 |
}
|
| 36792 |
ranu |
1928 |
if (tab === 'deferred' && typeof loadDeferredItems === 'function') {
|
|
|
1929 |
loadDeferredItems();
|
|
|
1930 |
}
|
| 36621 |
ranu |
1931 |
});
|
|
|
1932 |
|
| 36644 |
ranu |
1933 |
// Click a beat chip on the calendar → view that specific day's route
|
|
|
1934 |
// (partners + only the leads scheduled for that exact date)
|
|
|
1935 |
$(document).on('click', '.cal-chip-view', function (e) {
|
|
|
1936 |
if ($(e.target).hasClass('cal-chip-remove')) return; // handled separately
|
|
|
1937 |
e.stopPropagation();
|
|
|
1938 |
var pg = $(this).data('plangroup');
|
|
|
1939 |
var date = $(this).data('date');
|
|
|
1940 |
if (!pg || !date) return;
|
| 36740 |
ranu |
1941 |
|
|
|
1942 |
// Deferred-assign mode: clicking a beat chip drops the deferred item into
|
|
|
1943 |
// that beat on that date (instead of opening the route).
|
|
|
1944 |
if (state.assignDeferred) {
|
|
|
1945 |
assignDeferredToBeat(String(pg), String(date));
|
|
|
1946 |
return;
|
|
|
1947 |
}
|
|
|
1948 |
|
| 36787 |
ranu |
1949 |
// Visit-request scheduling mode: similar handoff but talks to /visitRequest/{id}/approve-schedule.
|
|
|
1950 |
if (state.assignVisitRequest) {
|
|
|
1951 |
approveVisitRequestOnBeat(String(pg), String(date));
|
|
|
1952 |
return;
|
|
|
1953 |
}
|
|
|
1954 |
|
| 37120 |
vikas |
1955 |
// Assigned-lead scheduling mode: drop the picked lead onto this beat+date.
|
|
|
1956 |
if (state.assignLead) {
|
|
|
1957 |
approveLeadOnBeat(String(pg), String(date));
|
|
|
1958 |
return;
|
|
|
1959 |
}
|
|
|
1960 |
|
| 36644 |
ranu |
1961 |
// Switch to Route tab and load that day's run
|
|
|
1962 |
$('.panel-tab[data-tab="route"]').click();
|
|
|
1963 |
viewBeat(String(pg), String(date));
|
|
|
1964 |
});
|
|
|
1965 |
|
| 36740 |
ranu |
1966 |
// Drop the deferred item (set on entry via URL params) into the chosen beat+date.
|
|
|
1967 |
function assignDeferredToBeat(beatId, date) {
|
|
|
1968 |
var ad = state.assignDeferred;
|
|
|
1969 |
if (!ad) return;
|
|
|
1970 |
// Must be a FUTURE beat — strictly after the deferred day (and not in the past).
|
|
|
1971 |
if (date <= ad.minDate) {
|
|
|
1972 |
alert('Pick a later beat. A deferred ' + (ad.type === 'lead' ? 'lead' : 'partner')
|
|
|
1973 |
+ ' can only move to a date after ' + ad.minDate + ' — not ' + date + '.');
|
|
|
1974 |
return;
|
|
|
1975 |
}
|
|
|
1976 |
if (!confirm('Add deferred ' + (ad.type === 'lead' ? 'lead' : 'partner') + ' "' + ad.name + '" into this beat on ' + date + '?')) return;
|
|
|
1977 |
$.ajax({
|
|
|
1978 |
url: context + '/beatPlan/deferred/assignToBeat',
|
|
|
1979 |
type: 'POST', contentType: 'application/json',
|
|
|
1980 |
data: JSON.stringify({deferredId: ad.id, beatId: parseInt(beatId), date: date}),
|
|
|
1981 |
success: function (r) {
|
|
|
1982 |
var d = r.response || r;
|
| 36761 |
ranu |
1983 |
var label = (ad.type === 'lead' ? 'Lead' : 'Partner') + ' "' + ad.name + '"';
|
|
|
1984 |
var msg = label + ' rescheduled to ' + date + '.';
|
|
|
1985 |
$('#deferred-assign-banner').html('<span style="color:#bbf7d0;">' + msg + '</span>');
|
|
|
1986 |
// Confirm to the head (name included so they don't have to read the banner),
|
|
|
1987 |
// then auto-close the parent's iframe modal. The parent's `hidden.bs.modal`
|
|
|
1988 |
// handler reloads the deferred list (rescheduled row drops off).
|
|
|
1989 |
alert(msg);
|
| 36792 |
ranu |
1990 |
// Clear the assign-mode so subsequent chip clicks don't try to
|
|
|
1991 |
// re-schedule the same (now-resolved) deferred row and trip the
|
|
|
1992 |
// "already scheduled" guard. Refresh the in-page deferred panel and
|
|
|
1993 |
// switch back to the route tab.
|
|
|
1994 |
state.assignDeferred = null;
|
|
|
1995 |
if (typeof loadDeferredItems === 'function') loadDeferredItems();
|
|
|
1996 |
$('.panel-tab[data-tab="route"]').click();
|
| 36761 |
ranu |
1997 |
if (window.parent && window.parent !== window) {
|
|
|
1998 |
try {
|
|
|
1999 |
window.parent.$('#defCalModal').modal('hide');
|
|
|
2000 |
} catch (e) {
|
|
|
2001 |
}
|
|
|
2002 |
}
|
| 36740 |
ranu |
2003 |
},
|
|
|
2004 |
error: function (xhr) {
|
|
|
2005 |
var msg = 'Failed';
|
|
|
2006 |
try {
|
|
|
2007 |
var err = JSON.parse(xhr.responseText);
|
|
|
2008 |
if (err && err.response) msg = (typeof err.response === 'string') ? err.response : (err.response.message || msg);
|
|
|
2009 |
} catch (_) {
|
|
|
2010 |
}
|
|
|
2011 |
alert(msg);
|
|
|
2012 |
}
|
|
|
2013 |
});
|
|
|
2014 |
}
|
|
|
2015 |
|
| 36621 |
ranu |
2016 |
// ============ CALENDAR ============
|
|
|
2017 |
$('#cal-prev').on('click', function () {
|
|
|
2018 |
navMonth(-1);
|
|
|
2019 |
});
|
|
|
2020 |
$('#cal-next').on('click', function () {
|
|
|
2021 |
navMonth(1);
|
|
|
2022 |
});
|
|
|
2023 |
|
|
|
2024 |
function navMonth(delta) {
|
|
|
2025 |
var p = state.calMonth.split('-');
|
|
|
2026 |
var d = new Date(parseInt(p[0]), parseInt(p[1]) - 1 + delta, 1);
|
|
|
2027 |
state.calMonth = d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2);
|
|
|
2028 |
loadCalendarData();
|
|
|
2029 |
}
|
|
|
2030 |
|
|
|
2031 |
function loadCalendarData() {
|
|
|
2032 |
if (!state.authUserId) return;
|
|
|
2033 |
$('#cal-month-label').text(state.calMonth);
|
|
|
2034 |
|
|
|
2035 |
$.ajax({
|
|
|
2036 |
url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
|
|
|
2037 |
data: {authUserId: state.authUserId, month: state.calMonth},
|
|
|
2038 |
success: function (r) {
|
|
|
2039 |
var data = r.response || r;
|
|
|
2040 |
state.calData = data;
|
|
|
2041 |
renderCalGrid(data);
|
|
|
2042 |
renderBeatCards(data.scheduledBeats);
|
|
|
2043 |
}
|
|
|
2044 |
});
|
|
|
2045 |
}
|
|
|
2046 |
|
|
|
2047 |
function renderCalGrid(data) {
|
|
|
2048 |
var p = state.calMonth.split('-');
|
|
|
2049 |
var year = parseInt(p[0]), month = parseInt(p[1]) - 1;
|
|
|
2050 |
var first = new Date(year, month, 1);
|
|
|
2051 |
var last = new Date(year, month + 1, 0);
|
|
|
2052 |
var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
|
2053 |
$('#cal-month-label').text(months[month] + ' ' + year);
|
|
|
2054 |
|
|
|
2055 |
var holidayMap = {};
|
|
|
2056 |
(data.holidays || []).forEach(function (h) {
|
|
|
2057 |
holidayMap[h.date] = h.occasion;
|
|
|
2058 |
});
|
|
|
2059 |
var blockedSet = {};
|
|
|
2060 |
(data.blockedDates || []).forEach(function (d) {
|
|
|
2061 |
blockedSet[d] = true;
|
|
|
2062 |
});
|
|
|
2063 |
|
|
|
2064 |
var beatDateMap = {};
|
|
|
2065 |
(data.scheduledBeats || []).forEach(function (b) {
|
|
|
2066 |
(b.days || []).forEach(function (d) {
|
|
|
2067 |
if (d.planDate) {
|
|
|
2068 |
if (!beatDateMap[d.planDate]) beatDateMap[d.planDate] = [];
|
|
|
2069 |
beatDateMap[d.planDate].push({
|
|
|
2070 |
name: b.beatName,
|
|
|
2071 |
color: b.beatColor,
|
|
|
2072 |
day: d.dayNumber,
|
|
|
2073 |
status: b.status,
|
| 36632 |
ranu |
2074 |
visits: d.visitCount,
|
|
|
2075 |
planGroupId: b.planGroupId
|
| 36621 |
ranu |
2076 |
});
|
|
|
2077 |
}
|
|
|
2078 |
});
|
|
|
2079 |
});
|
|
|
2080 |
|
|
|
2081 |
var today = fmtDate(new Date());
|
|
|
2082 |
var startOff = (first.getDay() + 6) % 7;
|
|
|
2083 |
var calStart = new Date(year, month, 1 - startOff);
|
|
|
2084 |
|
|
|
2085 |
var html = '<table class="cal-grid"><tr><th>Mon</th><th>Tue</th><th>Wed</th><th>Thu</th><th>Fri</th><th>Sat</th><th>Sun</th></tr>';
|
|
|
2086 |
var d = new Date(calStart);
|
|
|
2087 |
for (var w = 0; w < 6; w++) {
|
|
|
2088 |
var hasDay = false;
|
|
|
2089 |
var row = '<tr>';
|
|
|
2090 |
for (var wd = 0; wd < 7; wd++) {
|
|
|
2091 |
var ds = fmtDate(d);
|
|
|
2092 |
var inMonth = d.getMonth() === month;
|
|
|
2093 |
var sun = d.getDay() === 0;
|
|
|
2094 |
var hol = !!holidayMap[ds];
|
|
|
2095 |
var isToday = ds === today;
|
|
|
2096 |
var beats = beatDateMap[ds] || [];
|
|
|
2097 |
var isPicked = state.calPickedDates.indexOf(ds) > -1;
|
|
|
2098 |
|
| 36632 |
ranu |
2099 |
var isPast = ds < today;
|
|
|
2100 |
var hasBeats = beats.length > 0;
|
|
|
2101 |
|
| 36621 |
ranu |
2102 |
var cls = [];
|
| 36632 |
ranu |
2103 |
if (hol && !sun) cls.push('holiday', 'blocked');
|
|
|
2104 |
else if (isPast) cls.push('blocked', 'past');
|
|
|
2105 |
if (sun && !isPast) cls.push('sunday'); // Sundays styled but not blocked
|
|
|
2106 |
if (sun && isPast) cls.push('blocked', 'past');
|
| 36621 |
ranu |
2107 |
if (isToday) cls.push('today');
|
|
|
2108 |
if (!inMonth) cls.push('blocked');
|
|
|
2109 |
|
|
|
2110 |
var style = inMonth ? '' : 'opacity:0.3;';
|
| 36632 |
ranu |
2111 |
if (isPast && inMonth) style += 'opacity:0.5;';
|
| 36621 |
ranu |
2112 |
|
| 36632 |
ranu |
2113 |
row += '<td class="' + cls.join(' ') + '" data-date="' + ds + '" data-hasbeat="' + (hasBeats ? '1' : '0') + '" style="' + style + '">';
|
| 36621 |
ranu |
2114 |
row += '<div class="cal-date">' + d.getDate() + '</div>';
|
|
|
2115 |
if (hol) row += '<div class="cal-holiday-label">' + holidayMap[ds] + '</div>';
|
|
|
2116 |
|
|
|
2117 |
beats.forEach(function (b) {
|
| 36785 |
ranu |
2118 |
// The "running"/[live] state only applies to TODAY's chip — future
|
|
|
2119 |
// chips of the same beat are still freely editable (drag + ×).
|
|
|
2120 |
// Past chips are always locked.
|
|
|
2121 |
var isLiveChip = b.status === 'running' && isToday;
|
|
|
2122 |
var isLockedChip = isPast || isLiveChip;
|
|
|
2123 |
|
| 36621 |
ranu |
2124 |
var chipCls = 'cal-chip';
|
| 36785 |
ranu |
2125 |
if (isLiveChip) chipCls += ' running';
|
| 36621 |
ranu |
2126 |
if (b.status === 'completed') chipCls += ' completed';
|
| 36785 |
ranu |
2127 |
var removeBtn = !isLockedChip
|
| 36632 |
ranu |
2128 |
? '<span class="cal-chip-remove" data-date="' + ds + '" data-plangroup="' + (b.planGroupId || '') + '" title="Remove">×</span>'
|
|
|
2129 |
: '';
|
| 36785 |
ranu |
2130 |
var draggable = !isLockedChip ? ' draggable="true"' : '';
|
| 36670 |
ranu |
2131 |
var cursor = draggable ? 'grab' : 'pointer';
|
|
|
2132 |
row += '<span class="' + chipCls + ' cal-chip-view"' + draggable + ' data-plangroup="' + (b.planGroupId || '') + '" data-date="' + ds + '" style="background:' + b.color + ';position:relative;cursor:' + cursor + ';" title="Drag to another date to shuffle">'
|
| 36785 |
ranu |
2133 |
+ b.name + ' D' + b.day + (isLiveChip ? ' [live]' : '') + ' (' + b.visits + ')'
|
| 36632 |
ranu |
2134 |
+ removeBtn + '</span>';
|
| 36621 |
ranu |
2135 |
});
|
|
|
2136 |
|
|
|
2137 |
if (isPicked) {
|
|
|
2138 |
var idx = state.calPickedDates.indexOf(ds) + 1;
|
|
|
2139 |
row += '<span class="cal-chip" style="background:#3b82f6;border:1px dashed #93c5fd;">Day ' + idx + '</span>';
|
|
|
2140 |
}
|
|
|
2141 |
|
|
|
2142 |
row += '</td>';
|
|
|
2143 |
if (inMonth) hasDay = true;
|
|
|
2144 |
d.setDate(d.getDate() + 1);
|
|
|
2145 |
}
|
|
|
2146 |
row += '</tr>';
|
|
|
2147 |
if (hasDay) html += row;
|
|
|
2148 |
}
|
|
|
2149 |
html += '</table>';
|
|
|
2150 |
$('#cal-grid-content').html(html);
|
|
|
2151 |
}
|
|
|
2152 |
|
| 36700 |
ranu |
2153 |
// Lists the scheduled dates for a beat (grouped by day number) inside a beat card.
|
|
|
2154 |
// Output looks like: "Schedule: D1 May 27 | D2 May 28 | D3 May 29"
|
|
|
2155 |
// Unscheduled days show as "—"; if NO real dates exist, returns empty string.
|
|
|
2156 |
function renderBeatScheduledDates(instances) {
|
|
|
2157 |
if (!instances || !instances.length) return '';
|
|
|
2158 |
|
|
|
2159 |
// Collect planDates per dayNumber across every instance of this beat
|
|
|
2160 |
var byDay = {};
|
|
|
2161 |
var today = fmtDate(new Date());
|
|
|
2162 |
instances.forEach(function (inst) {
|
|
|
2163 |
(inst.days || []).forEach(function (d) {
|
|
|
2164 |
if (!d.planDate) return; // skip unscheduled placeholders
|
|
|
2165 |
if (!byDay[d.dayNumber]) byDay[d.dayNumber] = [];
|
|
|
2166 |
byDay[d.dayNumber].push(d.planDate);
|
|
|
2167 |
});
|
|
|
2168 |
});
|
|
|
2169 |
|
|
|
2170 |
var dayNums = Object.keys(byDay).map(function (n) {
|
|
|
2171 |
return parseInt(n, 10);
|
|
|
2172 |
}).sort(function (a, b) {
|
|
|
2173 |
return a - b;
|
|
|
2174 |
});
|
|
|
2175 |
if (dayNums.length === 0) {
|
|
|
2176 |
return '<div class="beat-meta" style="color:#475569; margin-top:4px;">Not scheduled yet</div>';
|
|
|
2177 |
}
|
|
|
2178 |
|
|
|
2179 |
var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
|
2180 |
|
|
|
2181 |
function fmt(ds) {
|
|
|
2182 |
// ds = "yyyy-mm-dd"
|
|
|
2183 |
var p = ds.split('-');
|
|
|
2184 |
if (p.length !== 3) return ds;
|
|
|
2185 |
return MONTHS[parseInt(p[1], 10) - 1] + ' ' + parseInt(p[2], 10);
|
|
|
2186 |
}
|
|
|
2187 |
|
|
|
2188 |
var parts = dayNums.map(function (n) {
|
|
|
2189 |
// De-dup + sort dates for this day (could repeat if beat scheduled multiple times)
|
|
|
2190 |
var dates = (byDay[n] || []).filter(function (v, i, a) {
|
|
|
2191 |
return a.indexOf(v) === i;
|
|
|
2192 |
}).sort();
|
|
|
2193 |
var label = dates.map(function (ds) {
|
|
|
2194 |
var color = ds === today ? '#ef4444' : (ds < today ? '#64748b' : '#60a5fa');
|
|
|
2195 |
return '<span style="color:' + color + ';">' + fmt(ds) + '</span>';
|
|
|
2196 |
}).join(', ');
|
|
|
2197 |
return '<span style="color:#94a3b8;">D' + n + '</span> ' + label;
|
|
|
2198 |
});
|
|
|
2199 |
|
|
|
2200 |
return '<div class="beat-meta" style="margin-top:4px; line-height:1.6;">'
|
|
|
2201 |
+ '<span style="color:#64748b;">Schedule:</span> '
|
|
|
2202 |
+ parts.join('<span style="color:#334155;"> | </span>')
|
|
|
2203 |
+ '</div>';
|
|
|
2204 |
}
|
|
|
2205 |
|
| 36621 |
ranu |
2206 |
function renderBeatCards(beats) {
|
|
|
2207 |
var html = '';
|
|
|
2208 |
if (!beats || beats.length === 0) {
|
|
|
2209 |
html = '<p style="color:#64748b;font-size:12px;padding:20px;">No beats scheduled. Create a beat first.</p>';
|
| 36632 |
ranu |
2210 |
$('#cal-beat-cards').html(html);
|
|
|
2211 |
return;
|
| 36621 |
ranu |
2212 |
}
|
| 36632 |
ranu |
2213 |
|
|
|
2214 |
// Deduplicate by beatName — group beats with same name, show count
|
|
|
2215 |
var beatGroups = {};
|
| 36621 |
ranu |
2216 |
(beats || []).forEach(function (b) {
|
| 36632 |
ranu |
2217 |
var key = b.beatName || b.planGroupId;
|
|
|
2218 |
if (!beatGroups[key]) {
|
|
|
2219 |
beatGroups[key] = {
|
|
|
2220 |
beatName: b.beatName,
|
|
|
2221 |
beatColor: b.beatColor,
|
|
|
2222 |
instances: []
|
|
|
2223 |
};
|
|
|
2224 |
}
|
|
|
2225 |
beatGroups[key].instances.push(b);
|
|
|
2226 |
});
|
| 36621 |
ranu |
2227 |
|
| 36632 |
ranu |
2228 |
Object.keys(beatGroups).forEach(function (key) {
|
|
|
2229 |
var group = beatGroups[key];
|
|
|
2230 |
var first = group.instances[0];
|
|
|
2231 |
var scheduledCount = group.instances.filter(function (b) {
|
|
|
2232 |
return b.status === 'scheduled';
|
|
|
2233 |
}).length;
|
|
|
2234 |
var unscheduledCount = group.instances.filter(function (b) {
|
|
|
2235 |
return b.status === 'unscheduled';
|
|
|
2236 |
}).length;
|
|
|
2237 |
var totalInstances = group.instances.length;
|
|
|
2238 |
|
| 36621 |
ranu |
2239 |
var totalV = 0, totalKm = 0;
|
| 36632 |
ranu |
2240 |
first.days.forEach(function (d) {
|
|
|
2241 |
totalV += d.visitCount || 0;
|
|
|
2242 |
totalKm += d.totalKm || 0;
|
|
|
2243 |
});
|
| 36621 |
ranu |
2244 |
|
| 36632 |
ranu |
2245 |
// Use first unscheduled instance for scheduling, or first overall
|
|
|
2246 |
var primaryPg = first.planGroupId;
|
|
|
2247 |
var unscheduledInstance = group.instances.find(function (b) {
|
|
|
2248 |
return b.status === 'unscheduled';
|
|
|
2249 |
});
|
|
|
2250 |
if (unscheduledInstance) primaryPg = unscheduledInstance.planGroupId;
|
|
|
2251 |
|
|
|
2252 |
var statusText = '';
|
|
|
2253 |
if (scheduledCount > 0) statusText += scheduledCount + ' scheduled';
|
|
|
2254 |
if (unscheduledCount > 0) statusText += (statusText ? ', ' : '') + unscheduledCount + ' unscheduled';
|
|
|
2255 |
|
|
|
2256 |
var isScheduling = state.calSelectedBeat === primaryPg && state.calPickedDates.length > 0;
|
|
|
2257 |
|
|
|
2258 |
html += '<div class="beat-card" data-plangroup="' + primaryPg + '" data-beatname="' + (group.beatName || '') + '" draggable="true">';
|
|
|
2259 |
html += '<div class="beat-name"><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:' + group.beatColor + ';"></span> ' + group.beatName + '</div>';
|
| 36728 |
vikas |
2260 |
html += '<div class="beat-meta">' + (first.totalDays || 1) + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km</div>';
|
| 36632 |
ranu |
2261 |
html += '<div class="beat-meta" style="color:#94a3b8;">' + statusText + ' (' + totalInstances + ' total)</div>';
|
| 36700 |
ranu |
2262 |
html += renderBeatScheduledDates(group.instances);
|
| 36632 |
ranu |
2263 |
|
|
|
2264 |
html += '<div class="beat-actions">';
|
|
|
2265 |
if (isScheduling) {
|
| 36728 |
vikas |
2266 |
html += '<button class="btn-sm btn-confirm-schedule" data-pg="' + primaryPg + '" data-days="' + (first.totalDays || 1) + '" style="background:#22c55e;color:#fff;">Confirm (' + state.calPickedDates.length + ' dates)</button>';
|
| 36632 |
ranu |
2267 |
html += '<button class="btn-sm btn-cancel-schedule" style="background:#475569;color:#e2e8f0;">Cancel</button>';
|
|
|
2268 |
} else {
|
| 36728 |
vikas |
2269 |
html += '<button class="btn-sm btn-schedule" data-pg="' + primaryPg + '" data-days="' + (first.totalDays || 1) + '">Schedule</button>';
|
| 36632 |
ranu |
2270 |
}
|
|
|
2271 |
html += '<button class="btn-sm" style="background:#ef4444;color:#fff;" onclick="deleteBeat(\'' + primaryPg + '\')">Delete</button>';
|
|
|
2272 |
html += '</div>';
|
|
|
2273 |
|
|
|
2274 |
if (isScheduling) {
|
|
|
2275 |
html += '<div style="font-size:10px;color:#60a5fa;margin-top:4px;">Dates: ' + state.calPickedDates.join(', ') + '</div>';
|
|
|
2276 |
html += '<div style="font-size:10px;color:#94a3b8;">Click calendar cells to adjust</div>';
|
|
|
2277 |
}
|
|
|
2278 |
|
|
|
2279 |
html += '<div style="font-size:9px;color:#475569;margin-top:4px;">Drag onto calendar to schedule</div>';
|
| 36621 |
ranu |
2280 |
html += '</div>';
|
|
|
2281 |
});
|
| 36632 |
ranu |
2282 |
|
| 36621 |
ranu |
2283 |
$('#cal-beat-cards').html(html);
|
|
|
2284 |
}
|
|
|
2285 |
|
|
|
2286 |
// Schedule button — suggest dates, show Confirm button in beat card
|
|
|
2287 |
$(document).on('click', '.btn-schedule', function (e) {
|
|
|
2288 |
e.stopPropagation();
|
|
|
2289 |
var pg = $(this).data('pg');
|
|
|
2290 |
var days = parseInt($(this).data('days'));
|
|
|
2291 |
state.calSelectedBeat = pg;
|
|
|
2292 |
state.calPickedDates = [];
|
|
|
2293 |
|
|
|
2294 |
$.ajax({
|
|
|
2295 |
url: context + "/beatPlan/availableSlots", type: "GET", dataType: "json",
|
|
|
2296 |
data: {authUserId: state.authUserId, month: state.calMonth, daysNeeded: days},
|
|
|
2297 |
success: function (r) {
|
|
|
2298 |
var data = r.response || r;
|
|
|
2299 |
state.calPickedDates = data.suggestedDates || [];
|
|
|
2300 |
renderCalGrid(state.calData);
|
|
|
2301 |
renderBeatCards(state.calData.scheduledBeats);
|
|
|
2302 |
if (state.calPickedDates.length < days) {
|
|
|
2303 |
alert('Only ' + state.calPickedDates.length + ' slots available this month. Need ' + days + '.');
|
|
|
2304 |
}
|
|
|
2305 |
}
|
|
|
2306 |
});
|
|
|
2307 |
});
|
|
|
2308 |
|
| 36632 |
ranu |
2309 |
// Click calendar cell during Schedule flow — auto-fill consecutive days from clicked date
|
|
|
2310 |
$(document).on('click', '#cal-grid-content td', function () {
|
| 36621 |
ranu |
2311 |
if (!state.calSelectedBeat) return;
|
|
|
2312 |
var ds = $(this).data('date');
|
| 36632 |
ranu |
2313 |
if (!ds || isDateBlocked(ds)) return;
|
|
|
2314 |
|
|
|
2315 |
// Find the beat to get days needed
|
|
|
2316 |
var beat = (state.calData.scheduledBeats || []).find(function (b) {
|
| 36644 |
ranu |
2317 |
return String(b.planGroupId) === String(state.calSelectedBeat);
|
| 36632 |
ranu |
2318 |
});
|
| 36728 |
vikas |
2319 |
var daysNeeded = beat ? (beat.totalDays || 1) : 1;
|
| 36632 |
ranu |
2320 |
|
|
|
2321 |
// Auto-fill consecutive slots from clicked date
|
|
|
2322 |
var slots = findConsecutiveSlots(ds, daysNeeded);
|
|
|
2323 |
if (slots) {
|
|
|
2324 |
state.calPickedDates = slots;
|
|
|
2325 |
renderCalGrid(state.calData);
|
|
|
2326 |
renderBeatCards(state.calData.scheduledBeats);
|
|
|
2327 |
}
|
| 36621 |
ranu |
2328 |
});
|
|
|
2329 |
|
|
|
2330 |
// Cancel scheduling mode
|
|
|
2331 |
$(document).on('click', '.btn-cancel-schedule', function (e) {
|
|
|
2332 |
e.stopPropagation();
|
|
|
2333 |
state.calSelectedBeat = null;
|
|
|
2334 |
state.calPickedDates = [];
|
|
|
2335 |
renderCalGrid(state.calData);
|
|
|
2336 |
renderBeatCards(state.calData.scheduledBeats);
|
|
|
2337 |
});
|
|
|
2338 |
|
|
|
2339 |
// Confirm schedule — save to server
|
|
|
2340 |
$(document).on('click', '.btn-confirm-schedule', function (e) {
|
|
|
2341 |
e.stopPropagation();
|
|
|
2342 |
var pg = $(this).data('pg');
|
|
|
2343 |
var daysNeeded = parseInt($(this).data('days'));
|
|
|
2344 |
|
|
|
2345 |
if (state.calPickedDates.length < daysNeeded) {
|
|
|
2346 |
alert('Need ' + daysNeeded + ' dates but only ' + state.calPickedDates.length + ' selected.');
|
|
|
2347 |
return;
|
|
|
2348 |
}
|
|
|
2349 |
|
|
|
2350 |
var beat = (state.calData.scheduledBeats || []).find(function (b) {
|
| 36644 |
ranu |
2351 |
return String(b.planGroupId) === String(pg);
|
| 36621 |
ranu |
2352 |
});
|
|
|
2353 |
if (!beat) return;
|
|
|
2354 |
|
|
|
2355 |
$.ajax({
|
|
|
2356 |
url: context + "/beatPlan/scheduleOnCalendar", type: "POST",
|
|
|
2357 |
data: {
|
|
|
2358 |
planGroupId: pg,
|
|
|
2359 |
dates: JSON.stringify(state.calPickedDates.slice(0, daysNeeded)),
|
|
|
2360 |
beatName: beat.beatName,
|
|
|
2361 |
beatColor: beat.beatColor
|
|
|
2362 |
},
|
|
|
2363 |
success: function () {
|
|
|
2364 |
state.calSelectedBeat = null;
|
|
|
2365 |
state.calPickedDates = [];
|
|
|
2366 |
loadCalendarData();
|
|
|
2367 |
},
|
|
|
2368 |
error: function (xhr) {
|
|
|
2369 |
alert('Error: ' + (xhr.responseText || xhr.statusText));
|
|
|
2370 |
}
|
|
|
2371 |
});
|
|
|
2372 |
});
|
|
|
2373 |
|
| 36632 |
ranu |
2374 |
// ============ DRAG & DROP BEAT TO CALENDAR ============
|
|
|
2375 |
$(document).on('dragstart', '.beat-card', function (e) {
|
|
|
2376 |
var pg = $(this).data('plangroup');
|
|
|
2377 |
var beatName = $(this).data('beatname');
|
|
|
2378 |
e.originalEvent.dataTransfer.setData('text/plain', pg);
|
|
|
2379 |
e.originalEvent.dataTransfer.setData('beatname', beatName || '');
|
|
|
2380 |
$(this).css('opacity', '0.5');
|
|
|
2381 |
});
|
| 36621 |
ranu |
2382 |
|
| 36632 |
ranu |
2383 |
$(document).on('dragend', '.beat-card', function () {
|
|
|
2384 |
$(this).css('opacity', '1');
|
|
|
2385 |
});
|
| 36621 |
ranu |
2386 |
|
| 36632 |
ranu |
2387 |
// Calendar cells accept drops
|
|
|
2388 |
$(document).on('dragover', '#cal-grid-content td', function (e) {
|
|
|
2389 |
var ds = $(this).data('date');
|
|
|
2390 |
if (!ds) return;
|
|
|
2391 |
if (isDateBlocked(ds)) return; // don't allow drop indicator on blocked dates
|
|
|
2392 |
e.preventDefault();
|
|
|
2393 |
$(this).css('background', '#1e3a5f');
|
|
|
2394 |
});
|
|
|
2395 |
|
|
|
2396 |
$(document).on('dragleave', '#cal-grid-content td', function () {
|
|
|
2397 |
$(this).css('background', '');
|
|
|
2398 |
});
|
|
|
2399 |
|
|
|
2400 |
$(document).on('drop', '#cal-grid-content td', function (e) {
|
|
|
2401 |
e.preventDefault();
|
|
|
2402 |
$(this).css('background', '');
|
|
|
2403 |
|
|
|
2404 |
var pg = e.originalEvent.dataTransfer.getData('text/plain');
|
|
|
2405 |
var dropDate = $(this).data('date');
|
|
|
2406 |
if (!pg || !dropDate) return;
|
|
|
2407 |
|
|
|
2408 |
if (isDateBlocked(dropDate)) {
|
|
|
2409 |
alert('Cannot schedule on this date (past, Sunday, or holiday)');
|
|
|
2410 |
return;
|
|
|
2411 |
}
|
|
|
2412 |
|
|
|
2413 |
var beat = (state.calData.scheduledBeats || []).find(function (b) {
|
| 36644 |
ranu |
2414 |
return String(b.planGroupId) === String(pg);
|
| 36632 |
ranu |
2415 |
});
|
|
|
2416 |
if (!beat) return;
|
|
|
2417 |
var beatName = beat.beatName || 'Beat';
|
| 36728 |
vikas |
2418 |
var daysNeeded = beat.totalDays || 1;
|
| 36632 |
ranu |
2419 |
|
|
|
2420 |
// Auto-fill consecutive available days starting from drop date
|
|
|
2421 |
var scheduleDates = findConsecutiveSlots(dropDate, daysNeeded);
|
|
|
2422 |
|
|
|
2423 |
if (!scheduleDates) return; // alerts already shown inside findConsecutiveSlots
|
|
|
2424 |
|
|
|
2425 |
var dateStr = scheduleDates.map(function (d, i) {
|
|
|
2426 |
return 'Day ' + (i + 1) + ': ' + d;
|
|
|
2427 |
}).join('\n');
|
|
|
2428 |
if (!confirm('Schedule "' + beatName + '" (' + daysNeeded + ' days):\n\n' + dateStr + '\n\nConfirm?')) return;
|
|
|
2429 |
|
|
|
2430 |
$.ajax({
|
|
|
2431 |
url: context + "/beatPlan/repeatBeat", type: "POST",
|
|
|
2432 |
data: {
|
|
|
2433 |
sourcePlanGroupId: pg,
|
|
|
2434 |
authUserId: state.authUserId,
|
|
|
2435 |
dates: JSON.stringify(scheduleDates)
|
|
|
2436 |
},
|
|
|
2437 |
success: function () {
|
|
|
2438 |
loadCalendarData();
|
|
|
2439 |
},
|
|
|
2440 |
error: function (xhr) {
|
|
|
2441 |
alert('Error: ' + (xhr.responseText || xhr.statusText));
|
|
|
2442 |
}
|
|
|
2443 |
});
|
|
|
2444 |
});
|
|
|
2445 |
|
|
|
2446 |
// Find N consecutive available days starting from startDate, skipping Sundays/holidays
|
|
|
2447 |
// Returns array of date strings, or null if can't fit
|
|
|
2448 |
function findConsecutiveSlots(startDateStr, daysNeeded) {
|
|
|
2449 |
var startDate = new Date(startDateStr);
|
|
|
2450 |
var startMonth = startDate.getMonth();
|
|
|
2451 |
var dates = [];
|
|
|
2452 |
var d = new Date(startDate);
|
|
|
2453 |
var sundayAsked = false;
|
|
|
2454 |
var includeSundays = false;
|
|
|
2455 |
|
|
|
2456 |
while (dates.length < daysNeeded) {
|
|
|
2457 |
var ds = fmtDate(d);
|
|
|
2458 |
|
|
|
2459 |
// Rule 3: must stay within same month
|
|
|
2460 |
if (d.getMonth() !== startMonth) {
|
|
|
2461 |
alert('Beat has ' + daysNeeded + ' days but only ' + dates.length + ' available days left in this month starting from ' + startDateStr + '. Try an earlier date.');
|
|
|
2462 |
return null;
|
| 36621 |
ranu |
2463 |
}
|
| 36632 |
ranu |
2464 |
|
| 36962 |
vikas |
2465 |
// Past dates — skip. Today is skipped too, unless this is an L4+ operator.
|
| 36632 |
ranu |
2466 |
var today = fmtDate(new Date());
|
| 36962 |
vikas |
2467 |
if (ds < today || (ds === today && !state.canScheduleToday)) {
|
| 36632 |
ranu |
2468 |
d.setDate(d.getDate() + 1);
|
|
|
2469 |
continue;
|
|
|
2470 |
}
|
|
|
2471 |
|
|
|
2472 |
// Holidays — always skip
|
|
|
2473 |
if (isHoliday(ds)) {
|
|
|
2474 |
d.setDate(d.getDate() + 1);
|
|
|
2475 |
continue;
|
|
|
2476 |
}
|
|
|
2477 |
|
|
|
2478 |
// Sunday — ask once whether to include
|
|
|
2479 |
if (isSunday(ds)) {
|
|
|
2480 |
if (!sundayAsked) {
|
|
|
2481 |
sundayAsked = true;
|
|
|
2482 |
includeSundays = confirm('This beat falls on a Sunday (' + ds + '). Include Sundays in the schedule?');
|
|
|
2483 |
}
|
|
|
2484 |
if (!includeSundays) {
|
|
|
2485 |
d.setDate(d.getDate() + 1);
|
|
|
2486 |
continue;
|
|
|
2487 |
}
|
|
|
2488 |
}
|
|
|
2489 |
|
|
|
2490 |
// Already occupied — block
|
|
|
2491 |
if (getBeatsOnDate(ds).length > 0) {
|
|
|
2492 |
alert('Cannot schedule: ' + ds + ' already has a beat scheduled. Consecutive days not available from ' + startDateStr + '.');
|
|
|
2493 |
return null;
|
|
|
2494 |
}
|
|
|
2495 |
|
|
|
2496 |
dates.push(ds);
|
|
|
2497 |
d.setDate(d.getDate() + 1);
|
|
|
2498 |
}
|
|
|
2499 |
|
|
|
2500 |
return dates;
|
|
|
2501 |
}
|
|
|
2502 |
|
|
|
2503 |
function isDateBlocked(ds) {
|
|
|
2504 |
var today = fmtDate(new Date());
|
| 36962 |
vikas |
2505 |
if (ds < today) return true; // past
|
|
|
2506 |
if (ds === today && !state.canScheduleToday) return true; // today blocked unless L4+
|
| 36632 |
ranu |
2507 |
// Sundays NOT blocked — handled via confirm dialog instead
|
|
|
2508 |
// Only block holidays
|
|
|
2509 |
if (state.calData && state.calData.blockedDates) {
|
|
|
2510 |
var blocked = state.calData.blockedDates;
|
|
|
2511 |
// blockedDates from server includes Sundays+holidays, but we only block holidays now
|
|
|
2512 |
var d = new Date(ds);
|
|
|
2513 |
if (d.getDay() !== 0) { // not Sunday — check holidays
|
|
|
2514 |
if (Array.isArray(blocked) && blocked.indexOf(ds) > -1) return true;
|
|
|
2515 |
if (typeof blocked === 'object' && blocked[ds]) return true;
|
|
|
2516 |
}
|
|
|
2517 |
}
|
|
|
2518 |
return false;
|
|
|
2519 |
}
|
|
|
2520 |
|
|
|
2521 |
function isSunday(ds) {
|
|
|
2522 |
return new Date(ds).getDay() === 0;
|
|
|
2523 |
}
|
|
|
2524 |
|
|
|
2525 |
function isHoliday(ds) {
|
|
|
2526 |
if (!state.calData || !state.calData.holidays) return false;
|
|
|
2527 |
return state.calData.holidays.some(function (h) {
|
|
|
2528 |
return h.date === ds;
|
|
|
2529 |
});
|
|
|
2530 |
}
|
|
|
2531 |
|
|
|
2532 |
function getBeatsOnDate(ds) {
|
|
|
2533 |
var result = [];
|
|
|
2534 |
if (!state.calData || !state.calData.scheduledBeats) return result;
|
|
|
2535 |
state.calData.scheduledBeats.forEach(function (b) {
|
|
|
2536 |
(b.days || []).forEach(function (d) {
|
|
|
2537 |
if (d.planDate === ds) result.push({planGroupId: b.planGroupId, beatName: b.beatName});
|
|
|
2538 |
});
|
|
|
2539 |
});
|
|
|
2540 |
return result;
|
|
|
2541 |
}
|
|
|
2542 |
|
|
|
2543 |
// ============ REMOVE BEAT FROM CALENDAR DATE ============
|
|
|
2544 |
$(document).on('click', '.cal-chip-remove', function (e) {
|
|
|
2545 |
e.stopPropagation();
|
|
|
2546 |
var date = $(this).data('date');
|
|
|
2547 |
var planGroupId = $(this).data('plangroup');
|
|
|
2548 |
if (!planGroupId || !date) return;
|
|
|
2549 |
|
| 36670 |
ranu |
2550 |
if (!confirm('Unschedule this beat from ' + date + '?\n\n(The beat stays in the list — only this date is cleared.)')) return;
|
| 36632 |
ranu |
2551 |
|
| 36670 |
ranu |
2552 |
// Unschedule ONLY this date — the beat itself is preserved
|
| 36632 |
ranu |
2553 |
$.ajax({
|
| 36670 |
ranu |
2554 |
url: context + "/beatPlan/unscheduleDate", type: "POST",
|
|
|
2555 |
data: {planGroupId: String(planGroupId), date: date},
|
| 36632 |
ranu |
2556 |
success: function () {
|
|
|
2557 |
loadCalendarData();
|
|
|
2558 |
},
|
|
|
2559 |
error: function (xhr) {
|
|
|
2560 |
alert('Error: ' + (xhr.responseText || xhr.statusText));
|
|
|
2561 |
}
|
| 36621 |
ranu |
2562 |
});
|
|
|
2563 |
});
|
|
|
2564 |
|
| 36670 |
ranu |
2565 |
// ============ CALENDAR SHUFFLE (drag chip to another date) ============
|
|
|
2566 |
$(document).on('dragstart', '.cal-chip-view', function (e) {
|
|
|
2567 |
var pg = $(this).data('plangroup');
|
|
|
2568 |
var date = $(this).data('date');
|
|
|
2569 |
if (!pg || !date) {
|
|
|
2570 |
e.preventDefault();
|
|
|
2571 |
return;
|
|
|
2572 |
}
|
|
|
2573 |
var payload = JSON.stringify({pg: String(pg), fromDate: String(date)});
|
|
|
2574 |
var dt = e.originalEvent.dataTransfer;
|
|
|
2575 |
dt.effectAllowed = 'move';
|
|
|
2576 |
dt.setData('text/plain', payload);
|
|
|
2577 |
$(this).css('opacity', '0.4');
|
|
|
2578 |
});
|
|
|
2579 |
|
|
|
2580 |
$(document).on('dragend', '.cal-chip-view', function () {
|
|
|
2581 |
$(this).css('opacity', '');
|
|
|
2582 |
});
|
|
|
2583 |
|
|
|
2584 |
$(document).on('dragover', '.cal-grid td', function (e) {
|
|
|
2585 |
var $td = $(this);
|
| 36785 |
ranu |
2586 |
// Today is the live/running slot — nothing can be shuffled onto it. The
|
|
|
2587 |
// shuffle is only valid between future dates.
|
|
|
2588 |
if ($td.hasClass('past') || $td.hasClass('blocked') || $td.hasClass('holiday') || $td.hasClass('sunday') || $td.hasClass('today')) return;
|
| 36670 |
ranu |
2589 |
e.preventDefault();
|
|
|
2590 |
e.originalEvent.dataTransfer.dropEffect = 'move';
|
|
|
2591 |
$td.css('box-shadow', 'inset 0 0 0 2px #60a5fa');
|
|
|
2592 |
});
|
|
|
2593 |
|
|
|
2594 |
$(document).on('dragleave', '.cal-grid td', function () {
|
|
|
2595 |
$(this).css('box-shadow', '');
|
|
|
2596 |
});
|
|
|
2597 |
|
|
|
2598 |
$(document).on('drop', '.cal-grid td', function (e) {
|
|
|
2599 |
var $td = $(this);
|
|
|
2600 |
$td.css('box-shadow', '');
|
| 36785 |
ranu |
2601 |
if ($td.hasClass('past') || $td.hasClass('blocked') || $td.hasClass('holiday') || $td.hasClass('sunday') || $td.hasClass('today')) return;
|
| 36670 |
ranu |
2602 |
e.preventDefault();
|
|
|
2603 |
|
|
|
2604 |
var raw;
|
|
|
2605 |
try {
|
|
|
2606 |
raw = e.originalEvent.dataTransfer.getData('text/plain');
|
|
|
2607 |
} catch (_) {
|
|
|
2608 |
return;
|
|
|
2609 |
}
|
|
|
2610 |
if (!raw) return;
|
|
|
2611 |
var data;
|
|
|
2612 |
try {
|
|
|
2613 |
data = JSON.parse(raw);
|
|
|
2614 |
} catch (_) {
|
|
|
2615 |
return;
|
|
|
2616 |
}
|
|
|
2617 |
if (!data.pg || !data.fromDate) return;
|
|
|
2618 |
|
|
|
2619 |
var toDate = $td.data('date');
|
|
|
2620 |
if (!toDate || toDate === data.fromDate) return;
|
|
|
2621 |
|
|
|
2622 |
$.ajax({
|
|
|
2623 |
url: context + "/beatPlan/moveScheduleDate", type: "POST",
|
|
|
2624 |
data: {planGroupId: data.pg, fromDate: data.fromDate, toDate: String(toDate)},
|
|
|
2625 |
success: function () {
|
|
|
2626 |
loadCalendarData();
|
|
|
2627 |
},
|
|
|
2628 |
error: function (xhr) {
|
|
|
2629 |
var msg = 'Move failed';
|
|
|
2630 |
try {
|
|
|
2631 |
var err = JSON.parse(xhr.responseText);
|
|
|
2632 |
if (err && err.response) {
|
|
|
2633 |
if (typeof err.response === 'string') msg = err.response;
|
|
|
2634 |
else if (err.response.message) msg = err.response.message;
|
|
|
2635 |
}
|
|
|
2636 |
} catch (_) {
|
|
|
2637 |
}
|
|
|
2638 |
alert(msg);
|
|
|
2639 |
}
|
|
|
2640 |
});
|
|
|
2641 |
});
|
|
|
2642 |
|
| 36621 |
ranu |
2643 |
// ============ DELETE BEAT ============
|
|
|
2644 |
function deleteBeat(planGroupId) {
|
|
|
2645 |
if (!confirm('Delete this beat? This cannot be undone.')) return;
|
|
|
2646 |
$.ajax({
|
|
|
2647 |
url: context + "/beatPlan/delete", type: "POST",
|
|
|
2648 |
data: {planGroupId: planGroupId},
|
|
|
2649 |
success: function () {
|
|
|
2650 |
loadCalendarData();
|
|
|
2651 |
},
|
|
|
2652 |
error: function (xhr) {
|
|
|
2653 |
alert('Error: ' + (xhr.responseText || xhr.statusText));
|
|
|
2654 |
}
|
|
|
2655 |
});
|
|
|
2656 |
}
|
|
|
2657 |
|
|
|
2658 |
// ============ HELPERS ============
|
|
|
2659 |
function curDay() {
|
|
|
2660 |
return state.days[state.currentDay - 1];
|
|
|
2661 |
}
|
|
|
2662 |
|
|
|
2663 |
function haversine(lat1, lng1, lat2, lng2) {
|
|
|
2664 |
var R = 6371, dLat = (lat2 - lat1) * Math.PI / 180, dLng = (lng2 - lng1) * Math.PI / 180;
|
|
|
2665 |
var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
|
2666 |
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
2667 |
}
|
|
|
2668 |
|
|
|
2669 |
function fmtMins(m) {
|
|
|
2670 |
return Math.floor(m / 60) + 'h ' + Math.round(m % 60) + 'm';
|
|
|
2671 |
}
|
|
|
2672 |
|
|
|
2673 |
function fmtDate(d) {
|
|
|
2674 |
return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);
|
|
|
2675 |
}
|
|
|
2676 |
|
|
|
2677 |
function fitBounds() {
|
| 36761 |
ranu |
2678 |
var b = new google.maps.LatLngBounds();
|
|
|
2679 |
var any = false;
|
|
|
2680 |
if (homeMarker) {
|
|
|
2681 |
b.extend(homeMarker.getPosition());
|
|
|
2682 |
any = true;
|
| 36621 |
ranu |
2683 |
}
|
| 36761 |
ranu |
2684 |
|
|
|
2685 |
// Prefer the current route's stops — the unselected partners on the map can
|
|
|
2686 |
// span a much wider area than the actual planned visits, which would zoom
|
|
|
2687 |
// the camera all the way out. We want focus on the route the user is viewing.
|
|
|
2688 |
try {
|
|
|
2689 |
var day = curDay();
|
|
|
2690 |
if (day && day.visits && day.visits.length > 0) {
|
|
|
2691 |
day.visits.forEach(function (v) {
|
|
|
2692 |
if (v.lat && v.lng) {
|
|
|
2693 |
b.extend(new google.maps.LatLng(parseFloat(v.lat), parseFloat(v.lng)));
|
|
|
2694 |
any = true;
|
|
|
2695 |
}
|
|
|
2696 |
});
|
|
|
2697 |
}
|
|
|
2698 |
} catch (e) {
|
|
|
2699 |
}
|
|
|
2700 |
|
|
|
2701 |
// No route yet (e.g., still picking partners) → fit all rendered markers.
|
|
|
2702 |
if (!any) {
|
|
|
2703 |
for (var k in markers) {
|
|
|
2704 |
b.extend(markers[k].getPosition());
|
|
|
2705 |
any = true;
|
|
|
2706 |
}
|
|
|
2707 |
}
|
|
|
2708 |
if (any) map.fitBounds(b);
|
| 36621 |
ranu |
2709 |
}
|
| 36650 |
ranu |
2710 |
|
|
|
2711 |
// ============ AUTO-LOAD FROM URL PARAMS ============
|
|
|
2712 |
// When opened with ?autoUserId=X[&autoUserName=Y], skip the user-picker
|
|
|
2713 |
// dropdowns and load that user's beats directly (and jump to Calendar tab).
|
|
|
2714 |
$(function () {
|
|
|
2715 |
// Use a manual parser so we don't depend on URLSearchParams
|
|
|
2716 |
function getParam(name) {
|
|
|
2717 |
var q = window.location.search.substring(1);
|
|
|
2718 |
var pairs = q.split('&');
|
|
|
2719 |
for (var i = 0; i < pairs.length; i++) {
|
|
|
2720 |
var kv = pairs[i].split('=');
|
|
|
2721 |
if (decodeURIComponent(kv[0]) === name) {
|
|
|
2722 |
return kv.length > 1 ? decodeURIComponent(kv[1].replace(/\+/g, ' ')) : '';
|
|
|
2723 |
}
|
|
|
2724 |
}
|
|
|
2725 |
return null;
|
|
|
2726 |
}
|
|
|
2727 |
|
|
|
2728 |
var autoUid = getParam('autoUserId');
|
|
|
2729 |
console.log('[BEAT-AUTO] autoUserId =', autoUid);
|
|
|
2730 |
if (!autoUid) return;
|
|
|
2731 |
|
|
|
2732 |
var autoName = getParam('autoUserName');
|
| 36651 |
ranu |
2733 |
var editBeatId = getParam('editBeatId');
|
|
|
2734 |
var editDate = getParam('editDate');
|
|
|
2735 |
|
| 36740 |
ranu |
2736 |
// Deferred-assign mode (opened from the Deferred Partners panel): the head
|
|
|
2737 |
// picks an upcoming beat on the calendar to drop this deferred item into.
|
|
|
2738 |
var assignDeferredId = getParam('assignDeferredId');
|
|
|
2739 |
if (assignDeferredId) {
|
|
|
2740 |
var d2 = new Date();
|
|
|
2741 |
var todayStr = d2.getFullYear() + '-' + ('0' + (d2.getMonth() + 1)).slice(-2) + '-' + ('0' + d2.getDate()).slice(-2);
|
|
|
2742 |
var deferDate = getParam('deferDate') || '';
|
|
|
2743 |
// A deferral can only move FORWARD — never to the day it was deferred or
|
|
|
2744 |
// earlier. The floor is the later of (deferred date, today).
|
|
|
2745 |
var floor = (deferDate && deferDate > todayStr) ? deferDate : todayStr;
|
|
|
2746 |
state.assignDeferred = {
|
|
|
2747 |
id: parseInt(assignDeferredId),
|
|
|
2748 |
type: getParam('deferType') || 'visit',
|
|
|
2749 |
name: getParam('deferName') || 'item',
|
|
|
2750 |
deferDate: deferDate,
|
|
|
2751 |
minDate: floor // target date must be strictly AFTER this
|
|
|
2752 |
};
|
|
|
2753 |
}
|
|
|
2754 |
|
| 36650 |
ranu |
2755 |
state.authUserId = parseInt(autoUid);
|
|
|
2756 |
state.categoryId = parseInt($('#bp-category').val()) || 4;
|
|
|
2757 |
state.mode = 'list';
|
|
|
2758 |
$('#bp-user-label').text(autoName || ('User #' + autoUid));
|
|
|
2759 |
|
|
|
2760 |
$.ajax({
|
|
|
2761 |
url: context + '/beatPlan/getBaseLocation', type: 'GET', dataType: 'json',
|
|
|
2762 |
data: {authUserId: autoUid},
|
|
|
2763 |
success: function (r) {
|
|
|
2764 |
console.log('[BEAT-AUTO] getBaseLocation ok');
|
|
|
2765 |
var data = r.response || r;
|
|
|
2766 |
if (data.locationName) {
|
|
|
2767 |
state.homeLat = parseFloat(data.latitude);
|
|
|
2768 |
state.homeLng = parseFloat(data.longitude);
|
|
|
2769 |
state.homeName = data.locationName;
|
|
|
2770 |
}
|
| 36651 |
ranu |
2771 |
if (editBeatId) {
|
|
|
2772 |
console.log('[BEAT-AUTO] entering edit for beat', editBeatId, 'date', editDate);
|
|
|
2773 |
editBeat(String(editBeatId), editDate || null);
|
|
|
2774 |
} else {
|
|
|
2775 |
showBeatList();
|
|
|
2776 |
setTimeout(function () {
|
|
|
2777 |
$('.panel-tab[data-tab="calendar"]').click();
|
|
|
2778 |
}, 100);
|
| 36740 |
ranu |
2779 |
if (state.assignDeferred) {
|
|
|
2780 |
// Banner instructing the head to click an upcoming beat.
|
|
|
2781 |
var ad = state.assignDeferred;
|
|
|
2782 |
$('#deferred-assign-banner').remove();
|
|
|
2783 |
$('body').prepend(
|
|
|
2784 |
'<div id="deferred-assign-banner" style="position:sticky;top:0;z-index:2000;'
|
|
|
2785 |
+ 'background:#0d9488;color:#fff;padding:8px 16px;font-size:13px;">'
|
|
|
2786 |
+ 'Adding deferred ' + (ad.type === 'lead' ? 'lead' : 'partner') + ': '
|
|
|
2787 |
+ '<strong>' + ad.name + '</strong> — click an upcoming beat on the calendar to drop it in.'
|
|
|
2788 |
+ '</div>');
|
|
|
2789 |
}
|
| 36651 |
ranu |
2790 |
}
|
| 36650 |
ranu |
2791 |
},
|
|
|
2792 |
error: function (xhr) {
|
|
|
2793 |
console.error('[BEAT-AUTO] getBaseLocation failed:', xhr.status, xhr.responseText);
|
|
|
2794 |
}
|
|
|
2795 |
});
|
|
|
2796 |
});
|
| 36787 |
ranu |
2797 |
|
|
|
2798 |
// ============ VISIT REQUEST PANEL ============
|
|
|
2799 |
// Lives on the route tab (sidebar) when a sales user is loaded. Fetches
|
|
|
2800 |
// PENDING requests addressed at this user's hierarchy and shows three actions
|
|
|
2801 |
// per row: Schedule on a beat, Reassign, Reject. Schedule reuses the calendar
|
|
|
2802 |
// chip-click flow by setting state.assignVisitRequest = {…} and switching
|
|
|
2803 |
// the panel to the Calendar tab — clicking a future chip then fires the POST.
|
|
|
2804 |
function loadVisitRequests() {
|
|
|
2805 |
if (!state.authUserId) return;
|
|
|
2806 |
$.ajax({
|
|
|
2807 |
url: context + '/visitRequest/list',
|
|
|
2808 |
type: 'GET', dataType: 'json',
|
| 36788 |
ranu |
2809 |
// Force a fresh fetch — otherwise the browser HTTP cache can serve the
|
|
|
2810 |
// pre-reassign list and the panel won't reflect the new ownership.
|
|
|
2811 |
cache: false,
|
| 36787 |
ranu |
2812 |
data: {assigneeAuthId: state.authUserId, status: 'PENDING'},
|
|
|
2813 |
success: function (r) {
|
|
|
2814 |
var data = r.response || r;
|
|
|
2815 |
var rows = (data && data.rows) || [];
|
|
|
2816 |
state.visitRequests = rows;
|
|
|
2817 |
renderVisitRequests(rows);
|
|
|
2818 |
},
|
|
|
2819 |
error: function () {
|
|
|
2820 |
$('#visit-request-panel').hide();
|
|
|
2821 |
}
|
|
|
2822 |
});
|
|
|
2823 |
}
|
|
|
2824 |
|
|
|
2825 |
function renderVisitRequests(rows) {
|
|
|
2826 |
if (!rows || rows.length === 0) {
|
|
|
2827 |
$('#visit-request-panel').hide();
|
|
|
2828 |
return;
|
|
|
2829 |
}
|
|
|
2830 |
$('#vr-count').text('(' + rows.length + ')');
|
|
|
2831 |
var html = '';
|
|
|
2832 |
rows.forEach(function (r) {
|
|
|
2833 |
var safeName = String(r.leadName || '').replace(/"/g, '"');
|
| 37120 |
vikas |
2834 |
// Line 1: outlet · mobile · city, state
|
|
|
2835 |
var top = [];
|
|
|
2836 |
if (r.leadOutlet) top.push(r.leadOutlet);
|
|
|
2837 |
if (r.leadMobile) top.push(r.leadMobile);
|
|
|
2838 |
var loc = [r.leadCity, r.leadState].filter(Boolean).join(', ');
|
|
|
2839 |
if (loc) top.push(loc);
|
|
|
2840 |
var topLine = top.length ? '<div class="beat-meta">' + top.join(' · ') + '</div>' : '';
|
|
|
2841 |
// Line 2: nearest store (only when known)
|
|
|
2842 |
var nearLine = r.nearestStoreCode
|
|
|
2843 |
? '<div class="beat-meta">Near ' + r.nearestStoreCode
|
|
|
2844 |
+ (r.nearestStoreOutlet ? ' — ' + r.nearestStoreOutlet : '') + '</div>'
|
|
|
2845 |
: '';
|
|
|
2846 |
// Line 3: by requester · Pref date. Assignee + communication type are intentionally
|
|
|
2847 |
// not shown here (assignee id still travels on the button data for the Reassign prompt).
|
|
|
2848 |
var reqBits = [];
|
|
|
2849 |
if (r.requestedByName) reqBits.push('by ' + r.requestedByName + (r.requestedByAuthId ? ' (#' + r.requestedByAuthId + ')' : ''));
|
|
|
2850 |
if (r.requestedDate) reqBits.push('Pref <span style="color:#fbbf24;">' + r.requestedDate + '</span>');
|
|
|
2851 |
var reqLine = reqBits.length ? '<div class="beat-meta">' + reqBits.join(' · ') + '</div>' : '';
|
|
|
2852 |
html += '<div class="beat-card" data-rid="' + r.id + '" style="cursor:default;">'
|
|
|
2853 |
+ '<div class="beat-name"><span style="display:inline-block;width:12px;height:12px;border-radius:2px;flex:none;background:#fbbf24;"></span> ' + (r.leadName || ('Lead #' + r.leadId)) + '</div>'
|
|
|
2854 |
+ topLine
|
| 36787 |
ranu |
2855 |
+ nearLine
|
| 37120 |
vikas |
2856 |
+ reqLine
|
|
|
2857 |
+ '<div class="beat-actions" style="flex-wrap:wrap;">'
|
|
|
2858 |
+ '<button class="vr-schedule btn-sm" data-rid="' + r.id + '" data-lead="' + r.leadId + '" data-leadname="' + safeName + '" data-assignee="' + r.assigneeAuthId + '" style="background:#22c55e;color:#fff;">Schedule</button>'
|
|
|
2859 |
+ '<button class="vr-reassign btn-sm" data-rid="' + r.id + '" style="background:none;border:1px solid #475569;color:#cbd5e1;">Reassign</button>'
|
|
|
2860 |
+ '<button class="vr-reject btn-sm" data-rid="' + r.id + '" style="background:#ef4444;color:#fff;">Reject</button>'
|
| 36787 |
ranu |
2861 |
+ '</div>'
|
|
|
2862 |
+ '</div>';
|
|
|
2863 |
});
|
|
|
2864 |
$('#vr-list').html(html);
|
|
|
2865 |
$('#visit-request-panel').show();
|
|
|
2866 |
}
|
|
|
2867 |
|
|
|
2868 |
$(document).on('click', '#vr-refresh', function () {
|
|
|
2869 |
loadVisitRequests();
|
|
|
2870 |
});
|
|
|
2871 |
|
| 37120 |
vikas |
2872 |
// Collapse / expand the visit-requests list (accordion header).
|
|
|
2873 |
$(document).on('click', '#vr-toggle', function () {
|
|
|
2874 |
var open = $('#vr-body').is(':visible');
|
|
|
2875 |
$('#vr-body').slideToggle(120);
|
|
|
2876 |
$('#vr-caret').html(open ? '▸' : '▾');
|
|
|
2877 |
});
|
|
|
2878 |
|
| 36787 |
ranu |
2879 |
// Enter "schedule a visit request" mode — same handoff to the calendar chip
|
|
|
2880 |
// click that the deferred-assign flow uses.
|
|
|
2881 |
$(document).on('click', '.vr-schedule', function () {
|
|
|
2882 |
var $b = $(this);
|
|
|
2883 |
state.assignVisitRequest = {
|
|
|
2884 |
id: parseInt($b.data('rid')),
|
|
|
2885 |
leadId: parseInt($b.data('lead')),
|
|
|
2886 |
leadName: String($b.data('leadname') || ''),
|
|
|
2887 |
assigneeAuthId: parseInt($b.data('assignee'))
|
|
|
2888 |
};
|
|
|
2889 |
alert('Click a future beat-chip on the calendar to schedule "'
|
|
|
2890 |
+ (state.assignVisitRequest.leadName || ('Lead #' + state.assignVisitRequest.leadId))
|
|
|
2891 |
+ '" onto it.');
|
|
|
2892 |
$('.panel-tab[data-tab="calendar"]').click();
|
|
|
2893 |
});
|
|
|
2894 |
|
| 37120 |
vikas |
2895 |
// "+ New beat" — open the full map-first planner, same as the big "+ New Beat"
|
|
|
2896 |
// button (no date prompt / no auto 1-day beat).
|
| 36847 |
ranu |
2897 |
$(document).on('click', '.vr-new-beat', function () {
|
| 37120 |
vikas |
2898 |
startNewBeat();
|
| 36847 |
ranu |
2899 |
});
|
|
|
2900 |
|
| 36787 |
ranu |
2901 |
$(document).on('click', '.vr-reassign', function () {
|
|
|
2902 |
var rid = $(this).data('rid');
|
|
|
2903 |
var newId = prompt('New assignee auth user id (must be in your downline):');
|
|
|
2904 |
if (!newId) return;
|
|
|
2905 |
var newIdInt = parseInt(newId);
|
|
|
2906 |
if (!newIdInt) return;
|
|
|
2907 |
$.ajax({
|
|
|
2908 |
url: context + '/visitRequest/' + rid + '/reassign',
|
|
|
2909 |
type: 'POST', contentType: 'application/json',
|
|
|
2910 |
data: JSON.stringify({newAssigneeAuthId: newIdInt}),
|
|
|
2911 |
success: function () {
|
| 36788 |
ranu |
2912 |
alert('Reassigned to user #' + newIdInt + '. The request will drop off this panel.');
|
| 36787 |
ranu |
2913 |
loadVisitRequests();
|
|
|
2914 |
},
|
|
|
2915 |
error: function (xhr) {
|
|
|
2916 |
var msg = 'Reassign failed';
|
|
|
2917 |
try {
|
|
|
2918 |
var e = JSON.parse(xhr.responseText);
|
|
|
2919 |
if (e && e.response) msg = (typeof e.response === 'string') ? e.response : (e.response.message || msg);
|
|
|
2920 |
} catch (_) {
|
|
|
2921 |
}
|
|
|
2922 |
alert(msg);
|
|
|
2923 |
}
|
|
|
2924 |
});
|
|
|
2925 |
});
|
|
|
2926 |
|
|
|
2927 |
$(document).on('click', '.vr-reject', function () {
|
|
|
2928 |
var rid = $(this).data('rid');
|
|
|
2929 |
var reason = prompt('Reject reason (required):');
|
|
|
2930 |
if (!reason || !reason.trim()) return;
|
|
|
2931 |
$.ajax({
|
|
|
2932 |
url: context + '/visitRequest/' + rid + '/reject',
|
|
|
2933 |
type: 'POST', contentType: 'application/json',
|
|
|
2934 |
data: JSON.stringify({rejectReason: reason.trim()}),
|
|
|
2935 |
success: function () {
|
| 36788 |
ranu |
2936 |
alert('Request rejected. It will drop off this panel.');
|
| 36787 |
ranu |
2937 |
loadVisitRequests();
|
|
|
2938 |
},
|
|
|
2939 |
error: function (xhr) {
|
|
|
2940 |
var msg = 'Reject failed';
|
|
|
2941 |
try {
|
|
|
2942 |
var e = JSON.parse(xhr.responseText);
|
|
|
2943 |
if (e && e.response) msg = (typeof e.response === 'string') ? e.response : (e.response.message || msg);
|
|
|
2944 |
} catch (_) {
|
|
|
2945 |
}
|
|
|
2946 |
alert(msg);
|
|
|
2947 |
}
|
|
|
2948 |
});
|
|
|
2949 |
});
|
|
|
2950 |
|
| 37120 |
vikas |
2951 |
// ============ ASSIGNED LEADS PANEL ============
|
|
|
2952 |
// Lives on the route tab (sidebar) below Pending Visit Requests. Lists the
|
|
|
2953 |
// loaded user's active/open leads (pending + follow-up, yellow/green). Each row
|
|
|
2954 |
// offers "Schedule on a beat" (reuses the calendar-chip handoff, like visit
|
|
|
2955 |
// requests) and "New beat" (a 1-day beat with just this lead). Hidden when empty.
|
|
|
2956 |
function loadAssignedLeads() {
|
|
|
2957 |
if (!state.authUserId) return;
|
|
|
2958 |
$.ajax({
|
|
|
2959 |
url: context + '/beatPlan/assignedLeads',
|
|
|
2960 |
type: 'GET', dataType: 'json',
|
|
|
2961 |
cache: false,
|
|
|
2962 |
data: {authUserId: state.authUserId},
|
|
|
2963 |
success: function (r) {
|
|
|
2964 |
var data = r.response || r;
|
|
|
2965 |
var rows = (data && data.rows) || [];
|
|
|
2966 |
state.assignedLeads = rows;
|
|
|
2967 |
renderAssignedLeads(rows);
|
|
|
2968 |
},
|
|
|
2969 |
error: function () {
|
|
|
2970 |
$('#assigned-leads-panel').hide();
|
|
|
2971 |
}
|
|
|
2972 |
});
|
|
|
2973 |
}
|
|
|
2974 |
|
|
|
2975 |
function renderAssignedLeads(rows) {
|
|
|
2976 |
if (!rows || rows.length === 0) {
|
|
|
2977 |
$('#assigned-leads-panel').hide();
|
|
|
2978 |
return;
|
|
|
2979 |
}
|
|
|
2980 |
$('#al-count').text('(' + rows.length + ')');
|
|
|
2981 |
$('#al-search').val('');
|
|
|
2982 |
$('#al-list').html(alRowsHtml(rows));
|
|
|
2983 |
$('#assigned-leads-panel').show();
|
|
|
2984 |
}
|
|
|
2985 |
|
|
|
2986 |
// Lead rows reuse the beat-card look (.beat-card/.beat-name/.beat-meta/.beat-actions)
|
|
|
2987 |
// so leads and routes read as one system. The colour dot mirrors the beat colour
|
|
|
2988 |
// swatch — here it's the lead's yellow/green priority colour.
|
|
|
2989 |
function alRowsHtml(rows) {
|
|
|
2990 |
if (!rows || rows.length === 0) {
|
|
|
2991 |
return '<div style="padding:12px;color:#64748b;font-size:11px;text-align:center;">No matching leads.</div>';
|
|
|
2992 |
}
|
|
|
2993 |
var html = '';
|
|
|
2994 |
rows.forEach(function (r) {
|
|
|
2995 |
var safeName = String(r.leadName || '').replace(/"/g, '"');
|
|
|
2996 |
var dot = r.color === 'green' ? '#22c55e' : (r.color === 'yellow' ? '#eab308' : '#64748b');
|
|
|
2997 |
var metaParts = [];
|
|
|
2998 |
if (r.outletName) metaParts.push(r.outletName);
|
|
|
2999 |
if (r.mobile) metaParts.push(r.mobile);
|
|
|
3000 |
var loc = [r.city, r.state].filter(Boolean).join(', ');
|
|
|
3001 |
if (loc) metaParts.push(loc);
|
|
|
3002 |
if (r.stage) metaParts.push(r.stage);
|
|
|
3003 |
var meta = metaParts.join(' · ')
|
|
|
3004 |
+ (r.hasGeo ? '' : ' <span style="color:#f59e0b;" title="No approved location — the field rep won\'t get a map pin">· no location</span>');
|
|
|
3005 |
html += '<div class="beat-card" data-lid="' + r.leadId + '" style="cursor:default;">'
|
|
|
3006 |
+ '<div class="beat-name"><span style="display:inline-block;width:12px;height:12px;border-radius:2px;flex:none;background:' + dot + ';"></span> ' + (r.leadName || ('Lead #' + r.leadId)) + '</div>'
|
|
|
3007 |
+ '<div class="beat-meta">' + meta + '</div>'
|
|
|
3008 |
+ '<div class="beat-actions">'
|
|
|
3009 |
+ '<button class="al-schedule btn-sm" data-lead="' + r.leadId + '" data-leadname="' + safeName + '" style="background:#22c55e;color:#fff;">Schedule on a beat</button>'
|
|
|
3010 |
+ '<button class="al-new-beat btn-sm" data-lead="' + r.leadId + '" data-leadname="' + safeName + '" style="background:#3b82f6;color:#fff;" title="Use this when the user has no beat to land on">+ New beat</button>'
|
|
|
3011 |
+ '</div>'
|
|
|
3012 |
+ '</div>';
|
|
|
3013 |
});
|
|
|
3014 |
return html;
|
|
|
3015 |
}
|
|
|
3016 |
|
|
|
3017 |
$(document).on('click', '#al-refresh', function () {
|
|
|
3018 |
loadAssignedLeads();
|
|
|
3019 |
});
|
|
|
3020 |
|
|
|
3021 |
// Collapse / expand the assigned-leads list (accordion header).
|
|
|
3022 |
$(document).on('click', '#al-toggle', function () {
|
|
|
3023 |
var open = $('#al-body').is(':visible');
|
|
|
3024 |
$('#al-body').slideToggle(120);
|
|
|
3025 |
$('#al-caret').html(open ? '▸' : '▾');
|
|
|
3026 |
});
|
|
|
3027 |
|
|
|
3028 |
// Client-side filter over the loaded leads — keeps the panel short without a round-trip.
|
|
|
3029 |
$(document).on('input', '#al-search', function () {
|
|
|
3030 |
var q = $(this).val().toLowerCase().trim();
|
|
|
3031 |
var src = state.assignedLeads || [];
|
|
|
3032 |
var list = !q ? src : src.filter(function (p) {
|
|
|
3033 |
return (p.leadName || '').toLowerCase().indexOf(q) !== -1
|
|
|
3034 |
|| (p.outletName || '').toLowerCase().indexOf(q) !== -1
|
|
|
3035 |
|| (p.city || '').toLowerCase().indexOf(q) !== -1
|
|
|
3036 |
|| (p.mobile || '').toLowerCase().indexOf(q) !== -1;
|
|
|
3037 |
});
|
|
|
3038 |
$('#al-list').html(alRowsHtml(list));
|
|
|
3039 |
});
|
|
|
3040 |
|
|
|
3041 |
// Enter "schedule a lead" mode — same calendar-chip handoff as visit requests.
|
|
|
3042 |
$(document).on('click', '.al-schedule', function () {
|
|
|
3043 |
var $b = $(this);
|
|
|
3044 |
state.assignLead = {
|
|
|
3045 |
leadId: parseInt($b.data('lead')),
|
|
|
3046 |
leadName: String($b.data('leadname') || '')
|
|
|
3047 |
};
|
|
|
3048 |
alert('Click a future beat-chip on the calendar to schedule "'
|
|
|
3049 |
+ (state.assignLead.leadName || ('Lead #' + state.assignLead.leadId))
|
|
|
3050 |
+ '" onto it.');
|
|
|
3051 |
$('.panel-tab[data-tab="calendar"]').click();
|
|
|
3052 |
});
|
|
|
3053 |
|
|
|
3054 |
// "+ New beat" — open the full map-first planner, same as the big "+ New Beat"
|
|
|
3055 |
// button (no date prompt / no auto 1-day beat).
|
|
|
3056 |
$(document).on('click', '.al-new-beat', function () {
|
|
|
3057 |
startNewBeat();
|
|
|
3058 |
});
|
|
|
3059 |
|
|
|
3060 |
// Drop the picked lead onto the chosen beat+date (strictly-future chip).
|
|
|
3061 |
function approveLeadOnBeat(pg, date) {
|
|
|
3062 |
var al = state.assignLead;
|
|
|
3063 |
if (!al) return;
|
|
|
3064 |
var today = new Date();
|
|
|
3065 |
var todayStr = today.getFullYear() + '-' + ('0' + (today.getMonth() + 1)).slice(-2) + '-' + ('0' + today.getDate()).slice(-2);
|
|
|
3066 |
if (date <= todayStr) {
|
|
|
3067 |
alert('Pick a FUTURE date (after today).');
|
|
|
3068 |
return;
|
|
|
3069 |
}
|
|
|
3070 |
if (!confirm('Schedule "' + (al.leadName || ('Lead #' + al.leadId)) + '" onto this beat on ' + date + '?')) return;
|
|
|
3071 |
$.ajax({
|
|
|
3072 |
url: context + '/beatPlan/lead/scheduleOnBeat',
|
|
|
3073 |
type: 'POST', contentType: 'application/json',
|
|
|
3074 |
data: JSON.stringify({leadId: al.leadId, beatId: parseInt(pg), date: date}),
|
|
|
3075 |
success: function (resp) {
|
|
|
3076 |
var r = resp && resp.response ? resp.response : resp;
|
|
|
3077 |
state.assignLead = null;
|
|
|
3078 |
alert(r.message || 'Scheduled.');
|
|
|
3079 |
$('.panel-tab[data-tab="route"]').click();
|
|
|
3080 |
loadAssignedLeads();
|
|
|
3081 |
},
|
|
|
3082 |
error: function (xhr) {
|
|
|
3083 |
var msg = 'Schedule failed';
|
|
|
3084 |
try {
|
|
|
3085 |
var er = JSON.parse(xhr.responseText);
|
|
|
3086 |
if (er && er.response) msg = (typeof er.response === 'string') ? er.response : (er.response.message || msg);
|
|
|
3087 |
} catch (_) {
|
|
|
3088 |
}
|
|
|
3089 |
alert(msg);
|
|
|
3090 |
}
|
|
|
3091 |
});
|
|
|
3092 |
}
|
|
|
3093 |
|
| 36792 |
ranu |
3094 |
// ============ DEFERRED ITEMS PANEL (inside beat-plan-window) ============
|
|
|
3095 |
// Reuses the existing /beatPlan/deferred read (hierarchy-scoped) and filters
|
|
|
3096 |
// client-side to the currently-loaded sales user. Schedule → switches to the
|
|
|
3097 |
// Calendar tab in deferred-assign mode and reuses assignDeferredToBeat() on
|
|
|
3098 |
// chip click (existing flow). Cancel posts /beatPlan/deferred/action with a
|
|
|
3099 |
// required reason. The standalone Deferred Partners page is untouched.
|
|
|
3100 |
function loadDeferredItems() {
|
|
|
3101 |
if (!state.authUserId) return;
|
|
|
3102 |
// Wide window so recent misses + any not-yet-acted older ones surface.
|
|
|
3103 |
var now = new Date();
|
|
|
3104 |
|
|
|
3105 |
function fmt(d) {
|
|
|
3106 |
return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);
|
|
|
3107 |
}
|
|
|
3108 |
|
|
|
3109 |
var start = new Date();
|
|
|
3110 |
start.setDate(now.getDate() - 30);
|
|
|
3111 |
var end = new Date();
|
|
|
3112 |
end.setDate(now.getDate() + 30);
|
|
|
3113 |
$.ajax({
|
|
|
3114 |
url: context + '/beatPlan/deferred',
|
|
|
3115 |
type: 'GET', dataType: 'json', cache: false,
|
|
|
3116 |
data: {startDate: fmt(start), endDate: fmt(end)},
|
|
|
3117 |
success: function (r) {
|
|
|
3118 |
var data = r.response || r;
|
|
|
3119 |
var all = (data && data.rows) || [];
|
|
|
3120 |
var mine = all.filter(function (row) {
|
|
|
3121 |
return row.authUserId === state.authUserId;
|
|
|
3122 |
});
|
|
|
3123 |
state.deferredItems = mine;
|
|
|
3124 |
renderDeferredItems(mine);
|
|
|
3125 |
},
|
|
|
3126 |
error: function () {
|
|
|
3127 |
$('#di-list').html('<p style="color:#d9534f;font-size:12px;text-align:center;padding:20px 10px;">Could not load deferred items.</p>');
|
|
|
3128 |
}
|
|
|
3129 |
});
|
|
|
3130 |
}
|
|
|
3131 |
|
|
|
3132 |
function renderDeferredItems(rows) {
|
|
|
3133 |
var n = (rows && rows.length) || 0;
|
|
|
3134 |
$('#di-count').text('(' + n + ')');
|
|
|
3135 |
// Tab pill counter — keeps the head aware of pending items while on other tabs.
|
|
|
3136 |
if (n > 0) {
|
|
|
3137 |
$('#di-tab-count').text('(' + n + ')').css('color', '#fca5a5');
|
|
|
3138 |
} else {
|
|
|
3139 |
$('#di-tab-count').text('').css('color', '');
|
|
|
3140 |
}
|
|
|
3141 |
if (n === 0) {
|
|
|
3142 |
$('#di-list').html('<p style="color:#64748b;font-size:12px;text-align:center;padding:20px 10px;">No deferred items for this user.</p>');
|
|
|
3143 |
return;
|
|
|
3144 |
}
|
|
|
3145 |
var html = '';
|
|
|
3146 |
rows.forEach(function (r) {
|
|
|
3147 |
var safeName = String(r.name || '').replace(/"/g, '"');
|
|
|
3148 |
var typeTag = r.type === 'Lead'
|
|
|
3149 |
? '<span style="background:#fbbf24;color:#0f172a;font-size:10px;padding:1px 5px;border-radius:3px;margin-right:6px;">Lead</span>'
|
|
|
3150 |
: '<span style="background:#60a5fa;color:#0f172a;font-size:10px;padding:1px 5px;border-radius:3px;margin-right:6px;">Visit</span>';
|
|
|
3151 |
var nextLine = r.nextScheduledDate
|
|
|
3152 |
? '<div style="color:#22c55e;font-size:11px;">Auto-covered on ' + r.nextScheduledDate + '</div>' : '';
|
|
|
3153 |
html += '<div data-did="' + r.id + '" style="padding:8px;border:1px solid #334155;border-radius:6px;background:#0f172a;margin-bottom:6px;">'
|
|
|
3154 |
+ typeTag
|
|
|
3155 |
+ '<strong style="color:#e2e8f0;">' + (r.name || '#' + r.fofoStoreId) + '</strong>'
|
|
|
3156 |
+ '<div style="color:#94a3b8;font-size:11px;margin-top:2px;">'
|
|
|
3157 |
+ 'Deferred: <span style="color:#fca5a5;">' + (r.deferredDate || '-') + '</span>'
|
|
|
3158 |
+ (r.reason ? ' · ' + r.reason : '')
|
|
|
3159 |
+ '</div>'
|
|
|
3160 |
+ nextLine
|
|
|
3161 |
+ '<div style="margin-top:6px;display:flex;gap:6px;flex-wrap:wrap;">'
|
|
|
3162 |
+ '<button class="di-schedule" data-did="' + r.id + '" data-type="' + (r.type === 'Lead' ? 'lead' : 'visit') + '" data-name="' + safeName + '" data-deferdate="' + (r.deferredDate || '') + '" style="font-size:11px;padding:3px 8px;border:none;border-radius:4px;background:#22c55e;color:#fff;cursor:pointer;">Schedule on a beat</button>'
|
|
|
3163 |
+ '<button class="di-cancel" data-did="' + r.id + '" data-name="' + safeName + '" style="font-size:11px;padding:3px 8px;border:none;border-radius:4px;background:#ef4444;color:#fff;cursor:pointer;">Cancel</button>'
|
|
|
3164 |
+ '</div>'
|
|
|
3165 |
+ '</div>';
|
|
|
3166 |
});
|
|
|
3167 |
$('#di-list').html(html);
|
|
|
3168 |
}
|
|
|
3169 |
|
|
|
3170 |
$(document).on('click', '#di-refresh', function () {
|
|
|
3171 |
loadDeferredItems();
|
|
|
3172 |
});
|
|
|
3173 |
|
|
|
3174 |
// Schedule: set state.assignDeferred (existing pattern) and switch to the
|
|
|
3175 |
// Calendar tab. The existing .cal-chip-view handler dispatches into
|
|
|
3176 |
// assignDeferredToBeat() when state.assignDeferred is set.
|
|
|
3177 |
$(document).on('click', '.di-schedule', function () {
|
|
|
3178 |
var $b = $(this);
|
|
|
3179 |
var deferDate = String($b.data('deferdate') || '');
|
|
|
3180 |
var todayObj = new Date();
|
|
|
3181 |
var todayStr = todayObj.getFullYear() + '-' + ('0' + (todayObj.getMonth() + 1)).slice(-2) + '-' + ('0' + todayObj.getDate()).slice(-2);
|
|
|
3182 |
// Floor: later of the deferred-date OR today — same guard the iframe flow uses.
|
|
|
3183 |
var floor = (deferDate && deferDate > todayStr) ? deferDate : todayStr;
|
|
|
3184 |
state.assignDeferred = {
|
|
|
3185 |
id: parseInt($b.data('did')),
|
|
|
3186 |
type: String($b.data('type') || 'visit'),
|
|
|
3187 |
name: String($b.data('name') || 'item'),
|
|
|
3188 |
deferDate: deferDate,
|
|
|
3189 |
minDate: floor
|
|
|
3190 |
};
|
|
|
3191 |
alert('Click a future beat-chip on the calendar to drop "' + (state.assignDeferred.name) + '" onto it (after ' + floor + ').');
|
|
|
3192 |
$('.panel-tab[data-tab="calendar"]').click();
|
|
|
3193 |
});
|
|
|
3194 |
|
|
|
3195 |
$(document).on('click', '.di-cancel', function () {
|
|
|
3196 |
var did = $(this).data('did');
|
|
|
3197 |
var name = String($(this).data('name') || 'this deferred item');
|
|
|
3198 |
var reason = prompt('Cancel reason (required) — why is "' + name + '" being cancelled?');
|
|
|
3199 |
if (!reason || !reason.trim()) return;
|
|
|
3200 |
$.ajax({
|
|
|
3201 |
url: context + '/beatPlan/deferred/action',
|
|
|
3202 |
type: 'POST', contentType: 'application/json',
|
|
|
3203 |
data: JSON.stringify({deferredId: did, action: 'cancel', reason: reason.trim()}),
|
|
|
3204 |
success: function () {
|
|
|
3205 |
alert('Cancelled. The row will drop off this panel.');
|
|
|
3206 |
loadDeferredItems();
|
|
|
3207 |
},
|
|
|
3208 |
error: function (xhr) {
|
|
|
3209 |
var msg = 'Cancel failed';
|
|
|
3210 |
try {
|
|
|
3211 |
var e = JSON.parse(xhr.responseText);
|
|
|
3212 |
if (e && e.response) msg = (typeof e.response === 'string') ? e.response : (e.response.message || msg);
|
|
|
3213 |
} catch (_) {
|
|
|
3214 |
}
|
|
|
3215 |
alert(msg);
|
|
|
3216 |
}
|
|
|
3217 |
});
|
|
|
3218 |
});
|
|
|
3219 |
|
| 36787 |
ranu |
3220 |
function approveVisitRequestOnBeat(pg, date) {
|
|
|
3221 |
var vr = state.assignVisitRequest;
|
|
|
3222 |
if (!vr) return;
|
|
|
3223 |
var today = new Date();
|
|
|
3224 |
var todayStr = today.getFullYear() + '-' + ('0' + (today.getMonth() + 1)).slice(-2) + '-' + ('0' + today.getDate()).slice(-2);
|
|
|
3225 |
if (date <= todayStr) {
|
|
|
3226 |
alert('Pick a FUTURE date (after today).');
|
|
|
3227 |
return;
|
|
|
3228 |
}
|
|
|
3229 |
if (!confirm('Schedule "' + (vr.leadName || ('Lead #' + vr.leadId)) + '" onto this beat on ' + date + '?')) return;
|
|
|
3230 |
$.ajax({
|
|
|
3231 |
url: context + '/visitRequest/' + vr.id + '/approve-schedule',
|
|
|
3232 |
type: 'POST', contentType: 'application/json',
|
|
|
3233 |
data: JSON.stringify({beatId: parseInt(pg), scheduleDate: date}),
|
|
|
3234 |
success: function () {
|
|
|
3235 |
state.assignVisitRequest = null;
|
|
|
3236 |
alert('Scheduled. The request has been resolved.');
|
|
|
3237 |
$('.panel-tab[data-tab="route"]').click();
|
|
|
3238 |
loadVisitRequests();
|
|
|
3239 |
},
|
|
|
3240 |
error: function (xhr) {
|
|
|
3241 |
var msg = 'Schedule failed';
|
|
|
3242 |
try {
|
|
|
3243 |
var er = JSON.parse(xhr.responseText);
|
|
|
3244 |
if (er && er.response) msg = (typeof er.response === 'string') ? er.response : (er.response.message || msg);
|
|
|
3245 |
} catch (_) {
|
|
|
3246 |
}
|
|
|
3247 |
alert(msg);
|
|
|
3248 |
}
|
|
|
3249 |
});
|
|
|
3250 |
}
|