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