| 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,
|
|
|
20 |
calPickedDates: []
|
|
|
21 |
};
|
|
|
22 |
|
|
|
23 |
var VISIT_MINS = 30;
|
|
|
24 |
var AVG_SPEED = 30;
|
|
|
25 |
var DAY_LIMIT = 540;
|
|
|
26 |
var ROAD_FACTOR = 1.3;
|
| 36644 |
ranu |
27 |
var isSubmittingBeat = false; // guard against double-submit
|
| 36621 |
ranu |
28 |
var COLORS = ['#3b82f6', '#ef4444', '#22c55e', '#a855f7', '#f59e0b', '#06b6d4', '#ec4899', '#334155', '#14b8a6', '#dc2626'];
|
|
|
29 |
|
|
|
30 |
// ============ TOP BAR ============
|
|
|
31 |
$('#bp-level').on('change', function () {
|
|
|
32 |
var level = $(this).val();
|
|
|
33 |
$('#bp-auth-user').html('<option value="">Select User</option>');
|
|
|
34 |
if (!level) return;
|
|
|
35 |
|
|
|
36 |
$.ajax({
|
|
|
37 |
url: context + "/beatPlan/getAuthUsers", type: "GET", dataType: "json",
|
|
|
38 |
data: {categoryId: $('#bp-category').val(), escalationType: level},
|
|
|
39 |
success: function (r) {
|
|
|
40 |
var data = r.response || r;
|
|
|
41 |
var html = '<option value="">Select User</option>';
|
|
|
42 |
data.forEach(function (u) {
|
|
|
43 |
html += '<option value="' + u.id + '">' + u.name + '</option>';
|
|
|
44 |
});
|
|
|
45 |
$('#bp-auth-user').html(html);
|
|
|
46 |
}
|
|
|
47 |
});
|
|
|
48 |
});
|
|
|
49 |
|
|
|
50 |
$('#bp-load').on('click', function () {
|
|
|
51 |
var uid = $('#bp-auth-user').val();
|
|
|
52 |
if (!uid) {
|
|
|
53 |
alert('Select a user');
|
|
|
54 |
return;
|
|
|
55 |
}
|
|
|
56 |
state.authUserId = parseInt(uid);
|
|
|
57 |
state.categoryId = parseInt($('#bp-category').val());
|
| 36632 |
ranu |
58 |
state.mode = 'list'; // 'list' or 'create' or 'view'
|
| 36621 |
ranu |
59 |
$('#bp-user-label').text($('#bp-auth-user option:selected').text());
|
|
|
60 |
|
| 36681 |
ranu |
61 |
// Load all saved base locations — default goes in state.home*, the rest is
|
|
|
62 |
// exposed via a picker in the route panel so the planner can pick a
|
|
|
63 |
// different starting point for this one beat without changing the default.
|
| 36621 |
ranu |
64 |
$.ajax({
|
| 36681 |
ranu |
65 |
url: context + "/beatPlan/listBaseLocations", type: "GET", dataType: "json",
|
| 36621 |
ranu |
66 |
data: {authUserId: uid},
|
|
|
67 |
success: function (r) {
|
|
|
68 |
var data = r.response || r;
|
| 36681 |
ranu |
69 |
state.baseLocations = data.locations || [];
|
|
|
70 |
var def = state.baseLocations.find(function (l) {
|
|
|
71 |
return l.isDefault;
|
|
|
72 |
}) || state.baseLocations[0];
|
|
|
73 |
if (def) {
|
|
|
74 |
state.homeLat = parseFloat(def.latitude);
|
|
|
75 |
state.homeLng = parseFloat(def.longitude);
|
|
|
76 |
state.homeName = def.locationName;
|
|
|
77 |
state.activeBaseLocationId = def.id;
|
|
|
78 |
} else {
|
|
|
79 |
state.activeBaseLocationId = null;
|
| 36632 |
ranu |
80 |
}
|
|
|
81 |
showBeatList();
|
|
|
82 |
}
|
|
|
83 |
});
|
| 36621 |
ranu |
84 |
});
|
|
|
85 |
|
| 36681 |
ranu |
86 |
// Called from the picker → swap the start location for the beat being
|
|
|
87 |
// created/edited. Updates state.home* + day 1's start point, then re-draws.
|
| 36698 |
ranu |
88 |
// Inline typeahead inside the route sidebar — same data source as the
|
|
|
89 |
// Partners tab (state.partners, fed by /beatPlan/getPartners, which mirrors
|
|
|
90 |
// the /getPartnerReadonlyInfo pattern used by the Partner Access tab).
|
|
|
91 |
// User types 2+ chars → suggestions appear → click adds to the current day.
|
|
|
92 |
function renderPartnerQuickAdd() {
|
|
|
93 |
if (!state.partners || state.partners.length === 0) return '';
|
|
|
94 |
return ''
|
|
|
95 |
+ '<div style="position:relative; margin-bottom:10px;">'
|
|
|
96 |
+ ' <input type="text" id="bp-partner-quick" autocomplete="off"'
|
|
|
97 |
+ ' placeholder="Search & add a partner..."'
|
|
|
98 |
+ ' style="width:100%; padding:8px 10px; border:1px solid #475569; border-radius:6px;'
|
|
|
99 |
+ ' background:#0f172a; color:#e2e8f0; font-size:12px; font-family:inherit;">'
|
|
|
100 |
+ ' <div id="bp-partner-suggest"'
|
|
|
101 |
+ ' style="display:none; position:absolute; left:0; right:0; top:100%; z-index:50;'
|
|
|
102 |
+ ' background:#1e293b; border:1px solid #334155; border-radius:6px;'
|
|
|
103 |
+ ' margin-top:4px; max-height:260px; overflow-y:auto;'
|
|
|
104 |
+ ' box-shadow:0 6px 16px rgba(0,0,0,0.4);"></div>'
|
|
|
105 |
+ '</div>';
|
|
|
106 |
}
|
|
|
107 |
|
|
|
108 |
function refreshPartnerSuggestions() {
|
|
|
109 |
var $input = $('#bp-partner-quick');
|
|
|
110 |
if (!$input.length) return;
|
|
|
111 |
var q = ($input.val() || '').trim().toLowerCase();
|
|
|
112 |
var $sug = $('#bp-partner-suggest');
|
|
|
113 |
if (q.length < 2) {
|
|
|
114 |
$sug.hide().empty();
|
|
|
115 |
return;
|
|
|
116 |
}
|
|
|
117 |
|
|
|
118 |
// Lookup of fofoId → day number for partners already on the route
|
|
|
119 |
var inRoute = {};
|
|
|
120 |
(state.days || []).forEach(function (d) {
|
|
|
121 |
(d.visits || []).forEach(function (v) {
|
|
|
122 |
if (!v.isLead && v.type !== 'lead') inRoute[v.id] = d.dayNumber;
|
|
|
123 |
});
|
|
|
124 |
});
|
|
|
125 |
|
|
|
126 |
var matches = (state.partners || []).filter(function (p) {
|
|
|
127 |
var hay = ((p.code || '') + ' '
|
|
|
128 |
+ (p.outletName || '') + ' '
|
|
|
129 |
+ (p.businessName || '') + ' '
|
|
|
130 |
+ (p.city || '')).toLowerCase();
|
|
|
131 |
return hay.indexOf(q) !== -1;
|
|
|
132 |
}).slice(0, 10);
|
|
|
133 |
|
|
|
134 |
if (matches.length === 0) {
|
|
|
135 |
$sug.html('<div style="padding:10px; color:#94a3b8; font-size:11px; text-align:center;">No partners found</div>').show();
|
|
|
136 |
return;
|
|
|
137 |
}
|
|
|
138 |
|
|
|
139 |
var html = '';
|
|
|
140 |
matches.forEach(function (p) {
|
|
|
141 |
var day = inRoute[p.fofoId];
|
|
|
142 |
var nameLine = (p.outletName || p.businessName || '');
|
|
|
143 |
if (p.city) nameLine += (nameLine ? ' - ' : '') + p.city;
|
|
|
144 |
if (day) {
|
|
|
145 |
html += '<div class="bp-partner-row in-route" style="padding:8px 10px; font-size:11px;'
|
|
|
146 |
+ ' color:#94a3b8; border-bottom:1px solid #0f172a; cursor:not-allowed;">'
|
|
|
147 |
+ '<div style="font-weight:600;">' + (p.code || '') + '</div>'
|
|
|
148 |
+ '<div>' + nameLine + ' <span style="color:#22c55e;">(in D' + day + ')</span></div>'
|
|
|
149 |
+ '</div>';
|
|
|
150 |
} else {
|
|
|
151 |
html += '<div class="bp-partner-row" data-fofoid="' + p.fofoId + '"'
|
|
|
152 |
+ ' style="padding:8px 10px; font-size:12px; color:#e2e8f0;'
|
|
|
153 |
+ ' border-bottom:1px solid #0f172a; cursor:pointer;">'
|
|
|
154 |
+ '<div style="font-weight:600;">' + (p.code || '') + '</div>'
|
|
|
155 |
+ '<div style="font-size:11px; color:#94a3b8;">' + nameLine + '</div>'
|
|
|
156 |
+ '</div>';
|
|
|
157 |
}
|
|
|
158 |
});
|
|
|
159 |
$sug.html(html).show();
|
|
|
160 |
}
|
|
|
161 |
|
|
|
162 |
$(document).on('input', '#bp-partner-quick', refreshPartnerSuggestions);
|
|
|
163 |
|
|
|
164 |
$(document).on('focus', '#bp-partner-quick', function () {
|
|
|
165 |
if (($(this).val() || '').trim().length >= 2) refreshPartnerSuggestions();
|
|
|
166 |
});
|
|
|
167 |
|
|
|
168 |
// Hide on outside click
|
|
|
169 |
$(document).on('click', function (e) {
|
|
|
170 |
if (!$(e.target).closest('#bp-partner-quick, #bp-partner-suggest').length) {
|
|
|
171 |
$('#bp-partner-suggest').hide();
|
|
|
172 |
}
|
|
|
173 |
});
|
|
|
174 |
|
|
|
175 |
$(document).on('mousedown', '#bp-partner-suggest .bp-partner-row', function (e) {
|
|
|
176 |
if ($(this).hasClass('in-route')) return;
|
|
|
177 |
e.preventDefault(); // keep input from losing focus before we read fofoid
|
|
|
178 |
var fofoId = parseInt($(this).data('fofoid'));
|
|
|
179 |
if (!fofoId) return;
|
|
|
180 |
if (typeof addVisit === 'function') addVisit(fofoId, 'next');
|
|
|
181 |
$('#bp-partner-quick').val('').focus();
|
|
|
182 |
$('#bp-partner-suggest').hide().empty();
|
|
|
183 |
});
|
|
|
184 |
|
| 36681 |
ranu |
185 |
function renderBaseLocationPicker() {
|
|
|
186 |
var locs = state.baseLocations || [];
|
|
|
187 |
if (locs.length === 0) return '';
|
|
|
188 |
var activeId = state.activeBaseLocationId;
|
|
|
189 |
if (locs.length === 1) {
|
|
|
190 |
var only = locs[0];
|
|
|
191 |
return '<div style="font-size:11px;color:#94a3b8;margin-bottom:10px;">'
|
|
|
192 |
+ 'Starting from: <strong style="color:#e2e8f0;">' + (only.locationName || 'Home') + '</strong>'
|
|
|
193 |
+ (only.isDefault ? ' <span style="color:#22c55e;">(default)</span>' : '')
|
|
|
194 |
+ '</div>';
|
|
|
195 |
}
|
|
|
196 |
var html = '<div style="margin-bottom:10px;">'
|
|
|
197 |
+ '<label style="font-size:11px;color:#94a3b8;display:block;margin-bottom:4px;">Start this beat from:</label>'
|
|
|
198 |
+ '<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;">';
|
|
|
199 |
locs.forEach(function (l) {
|
|
|
200 |
var sel = (l.id === activeId) ? ' selected' : '';
|
|
|
201 |
var label = (l.locationName || 'Location ' + l.id) + (l.isDefault ? ' (default)' : '');
|
|
|
202 |
html += '<option value="' + l.id + '"' + sel + '>' + label + '</option>';
|
|
|
203 |
});
|
|
|
204 |
html += '</select></div>';
|
|
|
205 |
return html;
|
|
|
206 |
}
|
|
|
207 |
|
|
|
208 |
$(document).on('change', '#bp-base-picker', function () {
|
|
|
209 |
var id = parseInt($(this).val());
|
|
|
210 |
if (!id) return;
|
|
|
211 |
applyBaseLocation(id);
|
|
|
212 |
});
|
|
|
213 |
|
|
|
214 |
function applyBaseLocation(id) {
|
|
|
215 |
var loc = (state.baseLocations || []).find(function (l) {
|
|
|
216 |
return l.id === id;
|
|
|
217 |
});
|
|
|
218 |
if (!loc) return;
|
|
|
219 |
state.homeLat = parseFloat(loc.latitude);
|
|
|
220 |
state.homeLng = parseFloat(loc.longitude);
|
|
|
221 |
state.homeName = loc.locationName;
|
|
|
222 |
state.activeBaseLocationId = id;
|
|
|
223 |
|
|
|
224 |
// Only day 1 starts from the chosen base; later days start where the
|
|
|
225 |
// previous day ended (existing behavior unchanged).
|
|
|
226 |
if (state.days && state.days.length > 0) {
|
|
|
227 |
state.days[0].startLat = state.homeLat;
|
|
|
228 |
state.days[0].startLng = state.homeLng;
|
|
|
229 |
state.days[0].startName = state.homeName;
|
|
|
230 |
if (typeof recalcDay === 'function') {
|
|
|
231 |
var saved = state.currentDay;
|
|
|
232 |
state.currentDay = 1;
|
|
|
233 |
recalcDay();
|
|
|
234 |
state.currentDay = saved;
|
|
|
235 |
}
|
|
|
236 |
}
|
|
|
237 |
|
|
|
238 |
// Move the home marker on the map + recenter so the user actually sees it.
|
|
|
239 |
if (typeof google !== 'undefined' && google.maps && map) {
|
|
|
240 |
var pos = new google.maps.LatLng(state.homeLat, state.homeLng);
|
|
|
241 |
if (homeMarker) {
|
|
|
242 |
homeMarker.setPosition(pos);
|
|
|
243 |
homeMarker.setTitle('Home: ' + state.homeName);
|
|
|
244 |
} else {
|
|
|
245 |
homeMarker = new google.maps.Marker({
|
|
|
246 |
map: map, position: pos, title: 'Home: ' + state.homeName,
|
|
|
247 |
icon: {
|
|
|
248 |
url: 'https://maps.google.com/mapfiles/kml/shapes/homegardenbusiness.png',
|
|
|
249 |
scaledSize: new google.maps.Size(32, 32)
|
|
|
250 |
},
|
|
|
251 |
zIndex: 100
|
|
|
252 |
});
|
|
|
253 |
}
|
|
|
254 |
map.panTo(pos);
|
|
|
255 |
}
|
|
|
256 |
|
|
|
257 |
if (typeof drawRoute === 'function') drawRoute();
|
|
|
258 |
if (typeof renderRoutePanel === 'function') renderRoutePanel();
|
|
|
259 |
if (typeof updateBottomBar === 'function') updateBottomBar();
|
|
|
260 |
}
|
|
|
261 |
|
| 36632 |
ranu |
262 |
// ============ BEAT LIST VIEW ============
|
|
|
263 |
function showBeatList() {
|
|
|
264 |
state.mode = 'list';
|
|
|
265 |
$('#bottom-bar').hide();
|
|
|
266 |
|
|
|
267 |
// Load all beats for this user
|
|
|
268 |
var now = new Date();
|
|
|
269 |
var month = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
|
|
|
270 |
|
|
|
271 |
$.ajax({
|
|
|
272 |
url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
|
|
|
273 |
data: {authUserId: state.authUserId, month: month},
|
|
|
274 |
success: function (r) {
|
|
|
275 |
var data = r.response || r;
|
|
|
276 |
var beats = data.scheduledBeats || [];
|
|
|
277 |
|
|
|
278 |
var html = '<div style="margin-bottom:10px;">';
|
|
|
279 |
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>';
|
|
|
280 |
html += '</div>';
|
|
|
281 |
|
|
|
282 |
if (beats.length === 0) {
|
|
|
283 |
html += '<p style="color:#64748b;font-size:12px;text-align:center;padding:20px;">No beats created yet.</p>';
|
|
|
284 |
}
|
|
|
285 |
|
|
|
286 |
// Deduplicate by beatName
|
|
|
287 |
var seen = {};
|
|
|
288 |
beats.forEach(function (b) {
|
|
|
289 |
var key = b.beatName || b.planGroupId;
|
|
|
290 |
if (!seen[key]) {
|
|
|
291 |
seen[key] = {beat: b, count: 0};
|
|
|
292 |
}
|
|
|
293 |
seen[key].count++;
|
|
|
294 |
});
|
|
|
295 |
|
|
|
296 |
Object.keys(seen).forEach(function (key) {
|
|
|
297 |
var item = seen[key];
|
|
|
298 |
var b = item.beat;
|
|
|
299 |
var totalV = 0, totalKm = 0;
|
|
|
300 |
b.days.forEach(function (d) {
|
|
|
301 |
totalV += d.visitCount || 0;
|
|
|
302 |
totalKm += d.totalKm || 0;
|
|
|
303 |
});
|
|
|
304 |
|
|
|
305 |
var statusCls = b.status === 'running' ? 'status-running' : b.status === 'completed' ? 'status-completed' : 'status-scheduled';
|
|
|
306 |
var statusLabel = b.status === 'running' ? 'Live' : b.status === 'completed' ? 'Done' : b.status === 'unscheduled' ? 'Unscheduled' : 'Scheduled';
|
|
|
307 |
|
| 36651 |
ranu |
308 |
html += '<div class="beat-card beat-list-item" data-plangroup="' + b.planGroupId + '" style="cursor:pointer; position:relative;">';
|
| 36632 |
ranu |
309 |
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>';
|
|
|
310 |
html += '<div class="beat-meta">' + b.days.length + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km';
|
|
|
311 |
if (item.count > 1) html += ' | ' + item.count + 'x scheduled';
|
|
|
312 |
html += '</div>';
|
| 36651 |
ranu |
313 |
html += '<button class="beat-edit-btn" data-plangroup="' + b.planGroupId + '" '
|
|
|
314 |
+ 'style="position:absolute;top:8px;right:8px;background:#475569;color:#e2e8f0;'
|
|
|
315 |
+ 'border:none;border-radius:4px;padding:3px 8px;font-size:10px;cursor:pointer;'
|
|
|
316 |
+ 'font-family:inherit;">Edit</button>';
|
| 36632 |
ranu |
317 |
html += '</div>';
|
|
|
318 |
});
|
|
|
319 |
|
|
|
320 |
$('#route-list').html(html);
|
|
|
321 |
}
|
|
|
322 |
});
|
|
|
323 |
}
|
|
|
324 |
|
|
|
325 |
// Click on existing beat → view its route on map
|
|
|
326 |
$(document).on('click', '.beat-list-item', function () {
|
|
|
327 |
var pg = $(this).data('plangroup');
|
|
|
328 |
viewBeat(pg);
|
|
|
329 |
});
|
|
|
330 |
|
| 36651 |
ranu |
331 |
// Edit button on a beat card → load in EDIT mode (modifiable)
|
|
|
332 |
$(document).on('click', '.beat-edit-btn', function (e) {
|
|
|
333 |
e.stopPropagation(); // don't also trigger beat-list-item view
|
|
|
334 |
var pg = $(this).data('plangroup');
|
|
|
335 |
var date = $(this).data('date') || null;
|
|
|
336 |
editBeat(String(pg), date ? String(date) : null);
|
|
|
337 |
});
|
|
|
338 |
|
|
|
339 |
// Loads a beat into edit mode — same UI as create but Finish becomes Save.
|
|
|
340 |
// When planDate is provided: leads scheduled on that date are loaded too and
|
|
|
341 |
// can be removed (template = partners stays the same across runs).
|
|
|
342 |
function editBeat(planGroupId, planDate) {
|
|
|
343 |
state.mode = 'edit';
|
|
|
344 |
state.editingBeatId = planGroupId;
|
|
|
345 |
state.editingDate = planDate || null;
|
|
|
346 |
state.viewDate = null;
|
|
|
347 |
state.savedPlanGroupId = null;
|
|
|
348 |
isSubmittingBeat = false;
|
|
|
349 |
|
|
|
350 |
$.ajax({
|
|
|
351 |
url: context + '/beatPlan/getPartners', type: 'GET', dataType: 'json',
|
|
|
352 |
data: {
|
|
|
353 |
authUserId: state.authUserId, categoryId: state.categoryId,
|
|
|
354 |
startLat: state.homeLat, startLng: state.homeLng
|
|
|
355 |
},
|
|
|
356 |
success: function (r) {
|
|
|
357 |
var pData = r.response || r;
|
|
|
358 |
state.partners = pData.partners || [];
|
|
|
359 |
var partnerMap = {};
|
|
|
360 |
state.partners.forEach(function (p) {
|
|
|
361 |
partnerMap[p.fofoId] = p;
|
|
|
362 |
});
|
|
|
363 |
|
|
|
364 |
$.ajax({
|
|
|
365 |
url: context + '/beatPlan/calendar', type: 'GET', dataType: 'json',
|
|
|
366 |
data: {authUserId: state.authUserId, month: '2020-01'},
|
|
|
367 |
success: function (r2) {
|
|
|
368 |
var calData = r2.response || r2;
|
|
|
369 |
var beat = (calData.scheduledBeats || []).find(function (b) {
|
|
|
370 |
return String(b.planGroupId) === String(planGroupId);
|
|
|
371 |
});
|
|
|
372 |
if (!beat) {
|
|
|
373 |
alert('Beat not found');
|
|
|
374 |
return;
|
|
|
375 |
}
|
|
|
376 |
state.beatTitle = beat.beatName || 'Beat';
|
|
|
377 |
|
|
|
378 |
// Pass planDate to getBeatVisits so leads for that date are included
|
|
|
379 |
var visitsData = planDate
|
|
|
380 |
? {planGroupId: planGroupId, planDate: planDate}
|
|
|
381 |
: {planGroupId: planGroupId};
|
|
|
382 |
|
|
|
383 |
$.ajax({
|
|
|
384 |
url: context + '/beatPlan/getBeatVisits', type: 'GET', dataType: 'json',
|
|
|
385 |
data: visitsData,
|
|
|
386 |
success: function (r3) {
|
|
|
387 |
var visits = (r3.response || r3) || [];
|
|
|
388 |
var dayVisitsMap = {};
|
|
|
389 |
visits.forEach(function (v) {
|
|
|
390 |
if (!dayVisitsMap[v.dayNumber]) dayVisitsMap[v.dayNumber] = [];
|
|
|
391 |
dayVisitsMap[v.dayNumber].push(v);
|
|
|
392 |
});
|
|
|
393 |
|
|
|
394 |
// Collect lead IDs so we can fetch their name/geo in parallel
|
|
|
395 |
var leadIdsToFetch = [];
|
|
|
396 |
visits.forEach(function (v) {
|
|
|
397 |
if (v.visitType === 'lead') leadIdsToFetch.push(v.fofoId);
|
|
|
398 |
});
|
|
|
399 |
|
|
|
400 |
state.days = [];
|
|
|
401 |
var seen = {};
|
|
|
402 |
beat.days.forEach(function (d) {
|
|
|
403 |
if (seen[d.dayNumber]) return;
|
|
|
404 |
seen[d.dayNumber] = true;
|
|
|
405 |
var dayVisits = (dayVisitsMap[d.dayNumber] || []).sort(function (a, b) {
|
|
|
406 |
return a.sequenceOrder - b.sequenceOrder;
|
|
|
407 |
});
|
|
|
408 |
var builtVisits = [];
|
|
|
409 |
dayVisits.forEach(function (v) {
|
|
|
410 |
if (v.visitType === 'lead') {
|
|
|
411 |
builtVisits.push({
|
|
|
412 |
id: v.fofoId, type: 'lead',
|
|
|
413 |
name: 'LEAD #' + v.fofoId, code: 'LEAD',
|
|
|
414 |
lat: null, lng: null, isLead: true
|
|
|
415 |
});
|
|
|
416 |
} else {
|
|
|
417 |
var p = partnerMap[v.fofoId];
|
|
|
418 |
if (p) builtVisits.push({
|
|
|
419 |
id: p.fofoId, type: 'partner',
|
|
|
420 |
name: p.code + ' - ' + (p.outletName || p.businessName || ''),
|
|
|
421 |
code: p.code,
|
|
|
422 |
lat: p.latitude ? parseFloat(p.latitude) : null,
|
|
|
423 |
lng: p.longitude ? parseFloat(p.longitude) : null
|
|
|
424 |
});
|
|
|
425 |
}
|
|
|
426 |
});
|
|
|
427 |
state.days.push({
|
|
|
428 |
dayNumber: d.dayNumber,
|
|
|
429 |
startLat: state.homeLat, startLng: state.homeLng,
|
|
|
430 |
startName: state.homeName,
|
|
|
431 |
visits: builtVisits,
|
|
|
432 |
endAction: d.dayNumber < beat.days.length ? 'DAYBREAK' : 'HOME',
|
|
|
433 |
totalKm: 0, totalMins: 0
|
|
|
434 |
});
|
|
|
435 |
});
|
|
|
436 |
state.currentDay = 1;
|
|
|
437 |
|
| 36681 |
ranu |
438 |
// Snapshot the originals — used during save to detect day-count
|
|
|
439 |
// increases (blocked) and lead removals (popup with cancel/reschedule).
|
|
|
440 |
state.originalDayCount = state.days.length;
|
|
|
441 |
state.originalLeads = [];
|
|
|
442 |
state.days.forEach(function (d) {
|
|
|
443 |
d.visits.forEach(function (v) {
|
|
|
444 |
if (v.type === 'lead' || v.isLead) {
|
|
|
445 |
state.originalLeads.push({
|
|
|
446 |
leadId: v.id,
|
|
|
447 |
dayNumber: d.dayNumber,
|
|
|
448 |
name: v.name || ('Lead #' + v.id)
|
|
|
449 |
});
|
|
|
450 |
}
|
|
|
451 |
});
|
|
|
452 |
});
|
|
|
453 |
|
| 36651 |
ranu |
454 |
// Async enrich each lead with name + geo
|
|
|
455 |
var leadPromises = [];
|
|
|
456 |
state.days.forEach(function (day) {
|
|
|
457 |
day.visits.forEach(function (v) {
|
|
|
458 |
if (!v.isLead) return;
|
|
|
459 |
leadPromises.push(
|
|
|
460 |
$.get(context + '/lead-geo/check/' + v.id).then(function (rr) {
|
|
|
461 |
var g = rr.response || rr;
|
|
|
462 |
if (g && g.hasApprovedGeo) {
|
|
|
463 |
v.lat = g.latitude;
|
|
|
464 |
v.lng = g.longitude;
|
|
|
465 |
}
|
|
|
466 |
}),
|
|
|
467 |
$.get(context + '/getLead?leadId=' + v.id).then(function (rr) {
|
|
|
468 |
var l = rr.response || rr;
|
| 36681 |
ranu |
469 |
if (l && l.firstName) {
|
|
|
470 |
var nm = 'LEAD - ' + l.firstName + ' ' + (l.lastName || '');
|
|
|
471 |
v.name = nm;
|
|
|
472 |
// Keep the originalLeads snapshot label in sync so the
|
|
|
473 |
// removed-leads popup shows the real name, not 'LEAD #123'.
|
|
|
474 |
(state.originalLeads || []).forEach(function (ol) {
|
|
|
475 |
if (ol.leadId === v.id) ol.name = nm;
|
|
|
476 |
});
|
|
|
477 |
}
|
| 36651 |
ranu |
478 |
})
|
|
|
479 |
);
|
|
|
480 |
});
|
|
|
481 |
});
|
|
|
482 |
|
|
|
483 |
$.when.apply($, leadPromises).always(function () {
|
| 36681 |
ranu |
484 |
// Pre-select whichever saved base location matches the beat's
|
|
|
485 |
// existing day-1 start (lat/lng), so the picker reflects current state.
|
|
|
486 |
var day1 = state.days[0];
|
|
|
487 |
if (day1 && state.baseLocations) {
|
|
|
488 |
var match = state.baseLocations.find(function (l) {
|
|
|
489 |
return Math.abs(parseFloat(l.latitude) - day1.startLat) < 1e-4
|
|
|
490 |
&& Math.abs(parseFloat(l.longitude) - day1.startLng) < 1e-4;
|
|
|
491 |
});
|
|
|
492 |
if (match) state.activeBaseLocationId = match.id;
|
|
|
493 |
}
|
|
|
494 |
|
| 36651 |
ranu |
495 |
renderRoutePanel();
|
|
|
496 |
var dateLabel = planDate
|
|
|
497 |
? '<div style="font-size:11px;color:#f59e0b;margin-bottom:8px;">Editing run on: ' + planDate + ' (leads on this date can be removed)</div>'
|
|
|
498 |
: '<div style="font-size:11px;color:#94a3b8;margin-bottom:8px;">Editing beat template (partners only)</div>';
|
|
|
499 |
$('#route-list').prepend(
|
|
|
500 |
'<div style="margin-bottom:8px;"><button class="btn-back-to-list" '
|
|
|
501 |
+ 'style="font-size:11px;padding:4px 10px;border:1px solid #475569;'
|
|
|
502 |
+ 'border-radius:4px;background:none;color:#94a3b8;cursor:pointer;'
|
|
|
503 |
+ 'font-family:inherit;">← Back to list</button></div>'
|
|
|
504 |
+ '<div style="font-size:14px;font-weight:600;color:#f59e0b;margin-bottom:4px;">'
|
| 36681 |
ranu |
505 |
+ 'EDITING: ' + state.beatTitle + '</div>' + dateLabel
|
| 36698 |
ranu |
506 |
+ renderBaseLocationPicker()
|
|
|
507 |
+ renderPartnerQuickAdd());
|
| 36651 |
ranu |
508 |
|
|
|
509 |
initMap();
|
|
|
510 |
// Add lead markers (orange) on the map
|
|
|
511 |
state.days.forEach(function (day) {
|
|
|
512 |
day.visits.forEach(function (v) {
|
|
|
513 |
if (v.isLead && v.lat && v.lng) {
|
|
|
514 |
var m = new google.maps.Marker({
|
|
|
515 |
map: map,
|
|
|
516 |
position: {lat: v.lat, lng: v.lng},
|
|
|
517 |
title: v.name,
|
|
|
518 |
label: {text: 'L', color: '#fff', fontSize: '11px', fontWeight: '700'},
|
|
|
519 |
icon: {
|
|
|
520 |
path: google.maps.SymbolPath.CIRCLE, scale: 16,
|
|
|
521 |
fillColor: '#e67e22', fillOpacity: 0.95,
|
|
|
522 |
strokeColor: '#fff', strokeWeight: 2
|
|
|
523 |
}
|
|
|
524 |
});
|
|
|
525 |
markers[v.id] = m;
|
|
|
526 |
}
|
|
|
527 |
});
|
|
|
528 |
});
|
|
|
529 |
resetMarkerColors();
|
|
|
530 |
drawRoute();
|
|
|
531 |
updateBottomBar();
|
|
|
532 |
$('#bottom-bar').show(); // make Save bar visible
|
|
|
533 |
$('#btn-finish').text('Save Changes').prop('disabled', false);
|
|
|
534 |
});
|
|
|
535 |
}
|
|
|
536 |
});
|
|
|
537 |
}
|
|
|
538 |
});
|
|
|
539 |
}
|
|
|
540 |
});
|
|
|
541 |
}
|
|
|
542 |
|
| 36644 |
ranu |
543 |
function viewBeat(planGroupId, planDate) {
|
| 36632 |
ranu |
544 |
state.mode = 'view';
|
| 36644 |
ranu |
545 |
state.viewDate = planDate || null; // when set, show that run's leads
|
| 36632 |
ranu |
546 |
$('#bottom-bar').hide();
|
|
|
547 |
|
|
|
548 |
// Load partners first (for coordinates), then load beat visits
|
|
|
549 |
$.ajax({
|
|
|
550 |
url: context + "/beatPlan/getPartners", type: "GET", dataType: "json",
|
|
|
551 |
data: {
|
|
|
552 |
authUserId: state.authUserId,
|
|
|
553 |
categoryId: state.categoryId,
|
|
|
554 |
startLat: state.homeLat,
|
|
|
555 |
startLng: state.homeLng
|
|
|
556 |
},
|
|
|
557 |
success: function (r) {
|
|
|
558 |
var pData = r.response || r;
|
|
|
559 |
state.partners = pData.partners || [];
|
|
|
560 |
|
|
|
561 |
// Build partner lookup by fofoId
|
|
|
562 |
var partnerMap = {};
|
|
|
563 |
state.partners.forEach(function (p) {
|
|
|
564 |
partnerMap[p.fofoId] = p;
|
|
|
565 |
});
|
|
|
566 |
|
|
|
567 |
// Load beat visits
|
|
|
568 |
$.ajax({
|
|
|
569 |
url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
|
|
|
570 |
data: {authUserId: state.authUserId, month: '2020-01'},
|
|
|
571 |
success: function (r2) {
|
|
|
572 |
var calData = r2.response || r2;
|
|
|
573 |
var beat = (calData.scheduledBeats || []).find(function (b) {
|
| 36644 |
ranu |
574 |
return String(b.planGroupId) === String(planGroupId);
|
| 36632 |
ranu |
575 |
});
|
|
|
576 |
if (!beat) {
|
|
|
577 |
alert('Beat not found');
|
|
|
578 |
return;
|
|
|
579 |
}
|
|
|
580 |
|
|
|
581 |
// Reconstruct state.days from beat data for map drawing
|
|
|
582 |
// We need actual visit fofoIds — fetch from beat_plan records
|
|
|
583 |
$.ajax({
|
|
|
584 |
url: context + "/beatPlan/getBeatVisits", type: "GET", dataType: "json",
|
| 36644 |
ranu |
585 |
data: planDate
|
|
|
586 |
? {planGroupId: planGroupId, planDate: planDate}
|
|
|
587 |
: {planGroupId: planGroupId},
|
| 36632 |
ranu |
588 |
success: function (r3) {
|
|
|
589 |
var visits = (r3.response || r3) || [];
|
|
|
590 |
|
|
|
591 |
// Group visits by dayNumber
|
|
|
592 |
var dayVisitsMap = {};
|
|
|
593 |
visits.forEach(function (v) {
|
|
|
594 |
if (!dayVisitsMap[v.dayNumber]) dayVisitsMap[v.dayNumber] = [];
|
|
|
595 |
dayVisitsMap[v.dayNumber].push(v);
|
|
|
596 |
});
|
|
|
597 |
|
| 36644 |
ranu |
598 |
// Build days for route display.
|
|
|
599 |
// A beat scheduled on multiple dates has multiple schedule
|
|
|
600 |
// rows per day_number — dedupe so each day appears once.
|
| 36632 |
ranu |
601 |
state.days = [];
|
| 36644 |
ranu |
602 |
var seenDayNumbers = {};
|
| 36632 |
ranu |
603 |
beat.days.forEach(function (d) {
|
| 36644 |
ranu |
604 |
if (seenDayNumbers[d.dayNumber]) return; // skip duplicate day
|
|
|
605 |
seenDayNumbers[d.dayNumber] = true;
|
|
|
606 |
|
| 36632 |
ranu |
607 |
var dayVisits = (dayVisitsMap[d.dayNumber] || []).sort(function (a, b) {
|
|
|
608 |
return a.sequenceOrder - b.sequenceOrder;
|
|
|
609 |
});
|
|
|
610 |
var startLat = state.homeLat, startLng = state.homeLng, startName = state.homeName;
|
|
|
611 |
|
|
|
612 |
// If not first day, start from previous day's last visit
|
|
|
613 |
if (state.days.length > 0) {
|
|
|
614 |
var prevDay = state.days[state.days.length - 1];
|
|
|
615 |
if (prevDay.visits.length > 0 && prevDay.endAction === 'DAYBREAK') {
|
|
|
616 |
var lastV = prevDay.visits[prevDay.visits.length - 1];
|
|
|
617 |
startLat = lastV.lat;
|
|
|
618 |
startLng = lastV.lng;
|
|
|
619 |
startName = lastV.code || lastV.name;
|
|
|
620 |
}
|
|
|
621 |
}
|
|
|
622 |
|
|
|
623 |
var builtVisits = [];
|
|
|
624 |
dayVisits.forEach(function (v) {
|
| 36642 |
ranu |
625 |
if (v.visitType === 'lead') {
|
| 36644 |
ranu |
626 |
builtVisits.push({
|
| 36642 |
ranu |
627 |
id: v.fofoId, type: 'lead',
|
|
|
628 |
name: 'LEAD #' + v.fofoId,
|
|
|
629 |
code: 'LEAD',
|
|
|
630 |
lat: null, lng: null,
|
|
|
631 |
isLead: true
|
| 36632 |
ranu |
632 |
});
|
| 36642 |
ranu |
633 |
} else {
|
|
|
634 |
var p = partnerMap[v.fofoId];
|
|
|
635 |
if (p) {
|
|
|
636 |
builtVisits.push({
|
|
|
637 |
id: p.fofoId, type: 'partner',
|
|
|
638 |
name: p.code + ' - ' + (p.outletName || p.businessName || ''),
|
|
|
639 |
code: p.code,
|
|
|
640 |
lat: p.latitude ? parseFloat(p.latitude) : null,
|
|
|
641 |
lng: p.longitude ? parseFloat(p.longitude) : null
|
|
|
642 |
});
|
|
|
643 |
}
|
| 36632 |
ranu |
644 |
}
|
|
|
645 |
});
|
|
|
646 |
|
|
|
647 |
state.days.push({
|
|
|
648 |
dayNumber: d.dayNumber,
|
|
|
649 |
startLat: startLat,
|
|
|
650 |
startLng: startLng,
|
|
|
651 |
startName: startName,
|
|
|
652 |
visits: builtVisits,
|
|
|
653 |
endAction: d.dayNumber < beat.days.length ? 'DAYBREAK' : 'HOME',
|
|
|
654 |
totalKm: d.totalKm || 0,
|
|
|
655 |
totalMins: d.totalMins || 0
|
|
|
656 |
});
|
|
|
657 |
});
|
|
|
658 |
|
|
|
659 |
state.currentDay = 1;
|
|
|
660 |
|
| 36644 |
ranu |
661 |
// Collect all lead data promises across all days
|
|
|
662 |
var allLeadPromises = [];
|
|
|
663 |
state.days.forEach(function (d) {
|
|
|
664 |
d.visits.forEach(function (v) {
|
|
|
665 |
if (v.isLead) {
|
|
|
666 |
allLeadPromises.push(
|
|
|
667 |
$.get(context + '/lead-geo/check/' + v.id).then(function (resp) {
|
|
|
668 |
var geoData = resp.response || resp;
|
|
|
669 |
if (geoData && geoData.hasApprovedGeo) {
|
|
|
670 |
v.lat = geoData.latitude;
|
|
|
671 |
v.lng = geoData.longitude;
|
|
|
672 |
}
|
|
|
673 |
})
|
|
|
674 |
);
|
|
|
675 |
allLeadPromises.push(
|
|
|
676 |
$.get(context + '/getLead?leadId=' + v.id).then(function (resp) {
|
|
|
677 |
var lead = resp.response || resp;
|
|
|
678 |
if (lead && lead.firstName) {
|
|
|
679 |
v.name = 'LEAD - ' + lead.firstName + ' ' + (lead.lastName || '');
|
|
|
680 |
}
|
|
|
681 |
})
|
|
|
682 |
);
|
|
|
683 |
}
|
|
|
684 |
});
|
|
|
685 |
});
|
| 36632 |
ranu |
686 |
|
| 36644 |
ranu |
687 |
// Wait for all lead data, then render
|
|
|
688 |
$.when.apply($, allLeadPromises).always(function () {
|
|
|
689 |
// Render left panel
|
|
|
690 |
state.mode = 'view';
|
|
|
691 |
renderRoutePanel();
|
|
|
692 |
var dateLabel = state.viewDate
|
|
|
693 |
? '<div style="font-size:11px;color:#f59e0b;margin-bottom:8px;">Run date: ' + state.viewDate + ' (showing this day\'s leads)</div>'
|
|
|
694 |
: '<div style="font-size:11px;color:#94a3b8;margin-bottom:8px;">Beat template (partners only)</div>';
|
|
|
695 |
$('#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 |
696 |
|
| 36644 |
ranu |
697 |
// Init map with all partners + lead markers
|
|
|
698 |
initMap();
|
|
|
699 |
state.days.forEach(function (d) {
|
|
|
700 |
d.visits.forEach(function (v) {
|
|
|
701 |
if (v.isLead && v.lat && v.lng) {
|
|
|
702 |
var m = new google.maps.Marker({
|
|
|
703 |
map: map,
|
|
|
704 |
position: {lat: v.lat, lng: v.lng},
|
|
|
705 |
title: v.name,
|
|
|
706 |
label: {text: 'L', color: '#fff', fontSize: '11px', fontWeight: '700'},
|
|
|
707 |
icon: {
|
|
|
708 |
path: google.maps.SymbolPath.CIRCLE,
|
|
|
709 |
scale: 16,
|
|
|
710 |
fillColor: '#e67e22',
|
|
|
711 |
fillOpacity: 0.95,
|
|
|
712 |
strokeColor: '#fff',
|
|
|
713 |
strokeWeight: 2
|
|
|
714 |
}
|
|
|
715 |
});
|
|
|
716 |
markers[v.id] = m;
|
|
|
717 |
}
|
|
|
718 |
});
|
|
|
719 |
});
|
|
|
720 |
resetMarkerColors();
|
|
|
721 |
drawRoute();
|
|
|
722 |
});
|
| 36632 |
ranu |
723 |
}
|
|
|
724 |
});
|
|
|
725 |
}
|
|
|
726 |
});
|
|
|
727 |
}
|
|
|
728 |
});
|
|
|
729 |
}
|
|
|
730 |
|
|
|
731 |
// Back to list
|
|
|
732 |
$(document).on('click', '.btn-back-to-list', function () {
|
|
|
733 |
showBeatList();
|
|
|
734 |
// Clear map
|
|
|
735 |
if (map) {
|
|
|
736 |
for (var key in markers) {
|
|
|
737 |
markers[key].setMap(null);
|
|
|
738 |
}
|
|
|
739 |
markers = {};
|
|
|
740 |
clearRoute();
|
|
|
741 |
}
|
|
|
742 |
});
|
|
|
743 |
|
|
|
744 |
// New Beat button
|
|
|
745 |
$(document).on('click', '#btn-new-beat', function () {
|
|
|
746 |
var beatTitle = prompt('Enter beat name:', '');
|
|
|
747 |
if (!beatTitle || !beatTitle.trim()) return;
|
|
|
748 |
|
|
|
749 |
state.beatTitle = beatTitle.trim();
|
|
|
750 |
state.mode = 'create';
|
| 36644 |
ranu |
751 |
state.savedPlanGroupId = null; // reset — this is a fresh beat
|
|
|
752 |
state.days = []; // clear any leftover route data
|
|
|
753 |
isSubmittingBeat = false;
|
| 36632 |
ranu |
754 |
|
|
|
755 |
if (!state.homeLat) {
|
|
|
756 |
showHomeModal();
|
|
|
757 |
} else {
|
|
|
758 |
loadPartners();
|
|
|
759 |
}
|
|
|
760 |
});
|
|
|
761 |
|
| 36621 |
ranu |
762 |
// ============ HOME LOCATION MODAL ============
|
|
|
763 |
var homeMap, homeMapMarker;
|
|
|
764 |
|
|
|
765 |
function showHomeModal() {
|
|
|
766 |
$('#modal-home').addClass('show');
|
|
|
767 |
setTimeout(function () {
|
|
|
768 |
if (!homeMap) {
|
|
|
769 |
homeMap = new google.maps.Map(document.getElementById('home-map'), {
|
|
|
770 |
center: {lat: 28.6, lng: 77.2},
|
|
|
771 |
zoom: 6
|
|
|
772 |
});
|
|
|
773 |
var ac = new google.maps.places.Autocomplete(document.getElementById('home-search'), {componentRestrictions: {country: 'in'}});
|
|
|
774 |
homeMapMarker = new google.maps.Marker({map: homeMap, draggable: true});
|
|
|
775 |
|
|
|
776 |
ac.addListener('place_changed', function () {
|
|
|
777 |
var p = ac.getPlace();
|
|
|
778 |
if (!p.geometry) return;
|
|
|
779 |
homeMap.setCenter(p.geometry.location);
|
|
|
780 |
homeMap.setZoom(14);
|
|
|
781 |
homeMapMarker.setPosition(p.geometry.location);
|
|
|
782 |
state.homeLat = p.geometry.location.lat();
|
|
|
783 |
state.homeLng = p.geometry.location.lng();
|
|
|
784 |
state.homeName = p.name || p.formatted_address || '';
|
|
|
785 |
});
|
|
|
786 |
homeMapMarker.addListener('dragend', function () {
|
|
|
787 |
state.homeLat = homeMapMarker.getPosition().lat();
|
|
|
788 |
state.homeLng = homeMapMarker.getPosition().lng();
|
|
|
789 |
});
|
|
|
790 |
homeMap.addListener('click', function (e) {
|
|
|
791 |
homeMapMarker.setPosition(e.latLng);
|
|
|
792 |
state.homeLat = e.latLng.lat();
|
|
|
793 |
state.homeLng = e.latLng.lng();
|
|
|
794 |
});
|
|
|
795 |
}
|
|
|
796 |
}, 200);
|
|
|
797 |
}
|
|
|
798 |
|
|
|
799 |
$('#home-cancel').on('click', function () {
|
|
|
800 |
$('#modal-home').removeClass('show');
|
|
|
801 |
});
|
|
|
802 |
$('#home-confirm').on('click', function () {
|
|
|
803 |
if (!state.homeLat) {
|
|
|
804 |
alert('Please select a location');
|
|
|
805 |
return;
|
|
|
806 |
}
|
|
|
807 |
$('#modal-home').removeClass('show');
|
|
|
808 |
$.ajax({
|
|
|
809 |
url: context + "/beatPlan/saveBaseLocation", type: "POST",
|
|
|
810 |
data: {
|
|
|
811 |
authUserId: state.authUserId,
|
|
|
812 |
locationName: state.homeName || 'Home',
|
|
|
813 |
latitude: state.homeLat,
|
|
|
814 |
longitude: state.homeLng,
|
|
|
815 |
address: $('#home-search').val()
|
|
|
816 |
}
|
|
|
817 |
});
|
|
|
818 |
loadPartners();
|
|
|
819 |
});
|
|
|
820 |
|
|
|
821 |
// ============ LOAD PARTNERS + INIT MAP ============
|
|
|
822 |
function loadPartners() {
|
|
|
823 |
$.ajax({
|
|
|
824 |
url: context + "/beatPlan/getPartners", type: "GET", dataType: "json",
|
|
|
825 |
data: {
|
|
|
826 |
authUserId: state.authUserId,
|
|
|
827 |
categoryId: state.categoryId,
|
|
|
828 |
startLat: state.homeLat,
|
|
|
829 |
startLng: state.homeLng
|
|
|
830 |
},
|
|
|
831 |
success: function (r) {
|
|
|
832 |
var data = r.response || r;
|
|
|
833 |
state.partners = data.partners || [];
|
|
|
834 |
initDay1();
|
|
|
835 |
initMap();
|
|
|
836 |
}
|
|
|
837 |
});
|
|
|
838 |
}
|
|
|
839 |
|
|
|
840 |
function initDay1() {
|
|
|
841 |
state.days = [{
|
|
|
842 |
dayNumber: 1, startLat: state.homeLat, startLng: state.homeLng, startName: state.homeName,
|
|
|
843 |
visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
|
|
|
844 |
}];
|
|
|
845 |
state.currentDay = 1;
|
| 36651 |
ranu |
846 |
if (state.mode === 'create' || state.mode === 'edit') $('#bottom-bar').show();
|
| 36621 |
ranu |
847 |
renderRoutePanel();
|
|
|
848 |
}
|
|
|
849 |
|
|
|
850 |
function initMap() {
|
|
|
851 |
var center = {lat: state.homeLat, lng: state.homeLng};
|
|
|
852 |
map = new google.maps.Map(document.getElementById('beat-map'), {
|
|
|
853 |
center: center, zoom: 9,
|
|
|
854 |
styles: [
|
|
|
855 |
{elementType: 'geometry', stylers: [{color: '#1d2c4d'}]},
|
|
|
856 |
{elementType: 'labels.text.fill', stylers: [{color: '#8ec3b9'}]},
|
|
|
857 |
{elementType: 'labels.text.stroke', stylers: [{color: '#1a3646'}]},
|
|
|
858 |
{featureType: 'road', elementType: 'geometry', stylers: [{color: '#304a7d'}]},
|
|
|
859 |
{featureType: 'water', elementType: 'geometry', stylers: [{color: '#0e1626'}]}
|
|
|
860 |
]
|
|
|
861 |
});
|
|
|
862 |
infoWindow = new google.maps.InfoWindow();
|
|
|
863 |
|
|
|
864 |
// Home marker
|
|
|
865 |
homeMarker = new google.maps.Marker({
|
|
|
866 |
map: map, position: center, title: 'Home: ' + state.homeName,
|
|
|
867 |
icon: {
|
|
|
868 |
url: 'https://maps.google.com/mapfiles/kml/shapes/homegardenbusiness.png',
|
|
|
869 |
scaledSize: new google.maps.Size(32, 32)
|
|
|
870 |
},
|
|
|
871 |
zIndex: 100
|
|
|
872 |
});
|
|
|
873 |
|
|
|
874 |
// Partner markers with name labels
|
|
|
875 |
for (var key in markers) {
|
|
|
876 |
markers[key].setMap(null);
|
|
|
877 |
}
|
|
|
878 |
markers = {};
|
|
|
879 |
|
|
|
880 |
state.partners.forEach(function (p) {
|
|
|
881 |
if (!p.latitude || !p.longitude) return;
|
|
|
882 |
var lat = parseFloat(p.latitude), lng = parseFloat(p.longitude);
|
|
|
883 |
if (isNaN(lat) || isNaN(lng)) return;
|
|
|
884 |
|
|
|
885 |
var m = new google.maps.Marker({
|
|
|
886 |
map: map, position: {lat: lat, lng: lng},
|
|
|
887 |
title: p.code + ' - ' + (p.outletName || p.businessName || ''),
|
|
|
888 |
label: {text: p.code || '', color: '#fff', fontSize: '10px', fontWeight: '600'},
|
|
|
889 |
icon: {
|
|
|
890 |
path: google.maps.SymbolPath.CIRCLE,
|
|
|
891 |
scale: 14,
|
|
|
892 |
fillColor: '#ef4444',
|
|
|
893 |
fillOpacity: 0.9,
|
|
|
894 |
strokeColor: '#fff',
|
|
|
895 |
strokeWeight: 1
|
|
|
896 |
}
|
|
|
897 |
});
|
|
|
898 |
|
|
|
899 |
m.addListener('click', function () {
|
|
|
900 |
showMarkerPopup(p, m);
|
|
|
901 |
});
|
|
|
902 |
m.addListener('mouseover', function () {
|
|
|
903 |
showPreviewLine(p);
|
|
|
904 |
});
|
|
|
905 |
m.addListener('mouseout', function () {
|
|
|
906 |
hidePreviewLine();
|
|
|
907 |
});
|
|
|
908 |
|
|
|
909 |
markers[p.fofoId] = m;
|
|
|
910 |
});
|
|
|
911 |
|
|
|
912 |
fitBounds();
|
|
|
913 |
}
|
|
|
914 |
|
|
|
915 |
// ============ HOVER PREVIEW ============
|
|
|
916 |
function showPreviewLine(partner) {
|
|
|
917 |
if (!partner.latitude || !partner.longitude) return;
|
|
|
918 |
var day = curDay();
|
|
|
919 |
var lastPt = day.visits.length > 0
|
|
|
920 |
? {lat: day.visits[day.visits.length - 1].lat, lng: day.visits[day.visits.length - 1].lng}
|
|
|
921 |
: {lat: day.startLat, lng: day.startLng};
|
|
|
922 |
if (!lastPt.lat) return;
|
|
|
923 |
|
|
|
924 |
var endPt = {lat: parseFloat(partner.latitude), lng: parseFloat(partner.longitude)};
|
|
|
925 |
if (previewLine) previewLine.setMap(null);
|
|
|
926 |
previewLine = new google.maps.Polyline({
|
|
|
927 |
path: [lastPt, endPt],
|
|
|
928 |
geodesic: true,
|
|
|
929 |
strokeColor: '#60a5fa',
|
|
|
930 |
strokeOpacity: 0.5,
|
|
|
931 |
strokeWeight: 2,
|
|
|
932 |
strokePattern: [10, 5],
|
|
|
933 |
icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
|
|
|
934 |
map: map
|
|
|
935 |
});
|
|
|
936 |
}
|
|
|
937 |
|
|
|
938 |
function hidePreviewLine() {
|
|
|
939 |
if (previewLine) {
|
|
|
940 |
previewLine.setMap(null);
|
|
|
941 |
previewLine = null;
|
|
|
942 |
}
|
|
|
943 |
}
|
|
|
944 |
|
|
|
945 |
// ============ MARKER CLICK POPUP ============
|
|
|
946 |
function showMarkerPopup(partner, marker) {
|
|
|
947 |
var day = curDay();
|
|
|
948 |
var alreadyAdded = day.visits.some(function (v) {
|
|
|
949 |
return v.id === partner.fofoId;
|
|
|
950 |
});
|
|
|
951 |
if (alreadyAdded) {
|
|
|
952 |
infoWindow.setContent('<div class="marker-popup"><h4>' + partner.code + '</h4><p>Already in today\'s route</p></div>');
|
|
|
953 |
infoWindow.open(map, marker);
|
|
|
954 |
return;
|
|
|
955 |
}
|
|
|
956 |
|
|
|
957 |
var lastPt = day.visits.length > 0
|
|
|
958 |
? {lat: day.visits[day.visits.length - 1].lat, lng: day.visits[day.visits.length - 1].lng}
|
|
|
959 |
: {lat: day.startLat, lng: day.startLng};
|
|
|
960 |
|
|
|
961 |
var dist = 0, travelMins = 0;
|
|
|
962 |
if (lastPt.lat && partner.latitude) {
|
|
|
963 |
dist = haversine(lastPt.lat, lastPt.lng, parseFloat(partner.latitude), parseFloat(partner.longitude)) * ROAD_FACTOR;
|
|
|
964 |
travelMins = (dist / AVG_SPEED) * 60;
|
|
|
965 |
}
|
|
|
966 |
|
|
|
967 |
var name = partner.outletName || partner.businessName || '';
|
|
|
968 |
var addr = partner.address || '';
|
|
|
969 |
|
|
|
970 |
var html = '<div class="marker-popup">';
|
|
|
971 |
html += '<h4>' + partner.code + ' - ' + name + '</h4>';
|
|
|
972 |
html += '<div class="popup-meta">' + addr + '</div>';
|
|
|
973 |
html += '<div class="popup-distance">~' + dist.toFixed(1) + ' km | ~' + fmtMins(travelMins) + ' travel + ' + VISIT_MINS + 'min visit</div>';
|
|
|
974 |
html += '<button class="popup-next" onclick="addVisit(' + partner.fofoId + ',\'next\')">Next Visit</button>';
|
|
|
975 |
html += '<button class="popup-last" onclick="addVisit(' + partner.fofoId + ',\'last\')">Last Visit</button>';
|
|
|
976 |
html += '</div>';
|
|
|
977 |
|
|
|
978 |
infoWindow.setContent(html);
|
|
|
979 |
infoWindow.open(map, marker);
|
|
|
980 |
}
|
|
|
981 |
|
|
|
982 |
// ============ ADD VISIT ============
|
|
|
983 |
function addVisit(fofoId, action) {
|
|
|
984 |
infoWindow.close();
|
|
|
985 |
var p = state.partners.find(function (x) {
|
|
|
986 |
return x.fofoId === fofoId;
|
|
|
987 |
});
|
|
|
988 |
if (!p) return;
|
|
|
989 |
|
|
|
990 |
var day = curDay();
|
|
|
991 |
var visit = {
|
|
|
992 |
id: p.fofoId, type: 'partner',
|
|
|
993 |
name: p.code + ' - ' + (p.outletName || p.businessName || ''),
|
|
|
994 |
code: p.code,
|
|
|
995 |
lat: p.latitude ? parseFloat(p.latitude) : null,
|
|
|
996 |
lng: p.longitude ? parseFloat(p.longitude) : null
|
|
|
997 |
};
|
|
|
998 |
|
|
|
999 |
day.visits.push(visit);
|
|
|
1000 |
|
|
|
1001 |
// Update marker to green
|
|
|
1002 |
if (markers[fofoId]) {
|
|
|
1003 |
markers[fofoId].setIcon({
|
|
|
1004 |
path: google.maps.SymbolPath.CIRCLE, scale: 14,
|
|
|
1005 |
fillColor: '#22c55e', fillOpacity: 0.9, strokeColor: '#fff', strokeWeight: 2
|
|
|
1006 |
});
|
|
|
1007 |
markers[fofoId].setLabel({text: '' + day.visits.length, color: '#fff', fontSize: '11px', fontWeight: '700'});
|
|
|
1008 |
}
|
|
|
1009 |
|
|
|
1010 |
recalcDay();
|
|
|
1011 |
drawRoute();
|
|
|
1012 |
renderRoutePanel();
|
|
|
1013 |
|
|
|
1014 |
if (action === 'last') {
|
|
|
1015 |
// Last visit — end day, go home
|
|
|
1016 |
day.endAction = 'HOME';
|
|
|
1017 |
day.stayLat = state.homeLat;
|
|
|
1018 |
day.stayLng = state.homeLng;
|
|
|
1019 |
day.stayName = state.homeName;
|
|
|
1020 |
drawReturnHome();
|
|
|
1021 |
renderRoutePanel();
|
|
|
1022 |
} else if (action === 'nextday') {
|
|
|
1023 |
// Continue next day — end current day, start new day from here
|
|
|
1024 |
day.endAction = 'CONTINUE';
|
|
|
1025 |
day.stayLat = visit.lat;
|
|
|
1026 |
day.stayLng = visit.lng;
|
|
|
1027 |
day.stayName = visit.name;
|
|
|
1028 |
startNextDay(visit.lat, visit.lng, visit.name);
|
|
|
1029 |
}
|
|
|
1030 |
// 'next' — just added, continue on same day
|
|
|
1031 |
}
|
|
|
1032 |
|
|
|
1033 |
function drawReturnHome() {
|
|
|
1034 |
var day = curDay();
|
|
|
1035 |
if (day.visits.length === 0) return;
|
|
|
1036 |
var lastVisit = day.visits[day.visits.length - 1];
|
|
|
1037 |
if (!lastVisit.lat || !state.homeLat) return;
|
|
|
1038 |
|
|
|
1039 |
var line = new google.maps.Polyline({
|
|
|
1040 |
path: [{lat: lastVisit.lat, lng: lastVisit.lng}, {lat: state.homeLat, lng: state.homeLng}],
|
|
|
1041 |
geodesic: true, strokeColor: '#22c55e', strokeOpacity: 0.6, strokeWeight: 2,
|
|
|
1042 |
icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
|
|
|
1043 |
map: map
|
|
|
1044 |
});
|
|
|
1045 |
routeLines.push(line);
|
|
|
1046 |
|
|
|
1047 |
// Add return distance to day total
|
|
|
1048 |
var retDist = haversine(lastVisit.lat, lastVisit.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
|
|
|
1049 |
day.totalKm += retDist;
|
|
|
1050 |
day.totalMins += (retDist / AVG_SPEED) * 60;
|
|
|
1051 |
updateBottomBar();
|
|
|
1052 |
}
|
|
|
1053 |
|
|
|
1054 |
function startNextDay(startLat, startLng, startName) {
|
|
|
1055 |
var nextNum = state.days.length + 1;
|
|
|
1056 |
state.days.push({
|
|
|
1057 |
dayNumber: nextNum, startLat: startLat, startLng: startLng, startName: startName,
|
|
|
1058 |
visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
|
|
|
1059 |
});
|
|
|
1060 |
state.currentDay = nextNum;
|
|
|
1061 |
updateBottomBar();
|
|
|
1062 |
renderRoutePanel();
|
|
|
1063 |
// Reset marker colors for unvisited
|
|
|
1064 |
resetMarkerColors();
|
|
|
1065 |
}
|
|
|
1066 |
|
|
|
1067 |
function resetMarkerColors() {
|
|
|
1068 |
var allVisited = {};
|
|
|
1069 |
state.days.forEach(function (d) {
|
|
|
1070 |
d.visits.forEach(function (v) {
|
|
|
1071 |
allVisited[v.id] = true;
|
|
|
1072 |
});
|
|
|
1073 |
});
|
|
|
1074 |
|
|
|
1075 |
state.partners.forEach(function (p) {
|
|
|
1076 |
if (!markers[p.fofoId]) return;
|
|
|
1077 |
if (allVisited[p.fofoId]) {
|
|
|
1078 |
markers[p.fofoId].setIcon({
|
|
|
1079 |
path: google.maps.SymbolPath.CIRCLE,
|
|
|
1080 |
scale: 14,
|
|
|
1081 |
fillColor: '#22c55e',
|
|
|
1082 |
fillOpacity: 0.9,
|
|
|
1083 |
strokeColor: '#fff',
|
|
|
1084 |
strokeWeight: 1
|
|
|
1085 |
});
|
|
|
1086 |
} else {
|
|
|
1087 |
markers[p.fofoId].setIcon({
|
|
|
1088 |
path: google.maps.SymbolPath.CIRCLE,
|
|
|
1089 |
scale: 14,
|
|
|
1090 |
fillColor: '#ef4444',
|
|
|
1091 |
fillOpacity: 0.9,
|
|
|
1092 |
strokeColor: '#fff',
|
|
|
1093 |
strokeWeight: 1
|
|
|
1094 |
});
|
|
|
1095 |
markers[p.fofoId].setLabel({text: p.code || '', color: '#fff', fontSize: '10px', fontWeight: '600'});
|
|
|
1096 |
}
|
|
|
1097 |
});
|
|
|
1098 |
|
|
|
1099 |
// Re-label current day visits
|
|
|
1100 |
var day = curDay();
|
|
|
1101 |
day.visits.forEach(function (v, i) {
|
|
|
1102 |
if (markers[v.id]) markers[v.id].setLabel({
|
|
|
1103 |
text: '' + (i + 1),
|
|
|
1104 |
color: '#fff',
|
|
|
1105 |
fontSize: '11px',
|
|
|
1106 |
fontWeight: '700'
|
|
|
1107 |
});
|
|
|
1108 |
});
|
|
|
1109 |
}
|
|
|
1110 |
|
|
|
1111 |
// ============ ROUTE DRAWING ============
|
|
|
1112 |
function clearRoute() {
|
|
|
1113 |
routeLines.forEach(function (l) {
|
|
|
1114 |
l.setMap(null);
|
|
|
1115 |
});
|
|
|
1116 |
routeLines = [];
|
|
|
1117 |
distLabels.forEach(function (l) {
|
|
|
1118 |
l.setMap(null);
|
|
|
1119 |
});
|
|
|
1120 |
distLabels = [];
|
|
|
1121 |
}
|
|
|
1122 |
|
| 36632 |
ranu |
1123 |
var DAY_ROUTE_COLORS = ['#3b82f6', '#a855f7', '#06b6d4', '#ec4899', '#f59e0b', '#14b8a6'];
|
|
|
1124 |
|
| 36621 |
ranu |
1125 |
function drawRoute() {
|
|
|
1126 |
clearRoute();
|
|
|
1127 |
|
| 36632 |
ranu |
1128 |
// Draw routes for ALL days, not just current
|
|
|
1129 |
state.days.forEach(function (day, dayIdx) {
|
|
|
1130 |
var isCurrent = day.dayNumber === state.currentDay;
|
|
|
1131 |
var routeColor = DAY_ROUTE_COLORS[dayIdx % DAY_ROUTE_COLORS.length];
|
|
|
1132 |
var opacity = isCurrent ? 0.9 : 0.5;
|
|
|
1133 |
var weight = isCurrent ? 3 : 2;
|
| 36621 |
ranu |
1134 |
|
| 36632 |
ranu |
1135 |
var pts = [{lat: day.startLat, lng: day.startLng}];
|
|
|
1136 |
day.visits.forEach(function (v) {
|
|
|
1137 |
if (v.lat && v.lng) pts.push({lat: v.lat, lng: v.lng});
|
|
|
1138 |
});
|
|
|
1139 |
|
|
|
1140 |
// Draw visit route lines for this day
|
|
|
1141 |
for (var i = 0; i < pts.length - 1; i++) {
|
|
|
1142 |
var line = new google.maps.Polyline({
|
|
|
1143 |
path: [pts[i], pts[i + 1]], geodesic: true,
|
|
|
1144 |
strokeColor: routeColor, strokeOpacity: opacity, strokeWeight: weight, map: map
|
|
|
1145 |
});
|
|
|
1146 |
routeLines.push(line);
|
|
|
1147 |
|
|
|
1148 |
// Only show distance labels on current day
|
|
|
1149 |
if (isCurrent) {
|
|
|
1150 |
var d = haversine(pts[i].lat, pts[i].lng, pts[i + 1].lat, pts[i + 1].lng) * ROAD_FACTOR;
|
|
|
1151 |
var mid = {lat: (pts[i].lat + pts[i + 1].lat) / 2, lng: (pts[i].lng + pts[i + 1].lng) / 2};
|
|
|
1152 |
var lbl = new google.maps.Marker({
|
|
|
1153 |
position: mid, map: map, icon: {path: google.maps.SymbolPath.CIRCLE, scale: 0},
|
|
|
1154 |
label: {text: d.toFixed(1) + 'km', color: '#93c5fd', fontSize: '10px', fontWeight: '600'}
|
|
|
1155 |
});
|
|
|
1156 |
distLabels.push(lbl);
|
|
|
1157 |
}
|
|
|
1158 |
}
|
|
|
1159 |
|
|
|
1160 |
// Draw return-to-home line (not on DAYBREAK days)
|
|
|
1161 |
if (day.visits.length > 0 && day.endAction !== 'DAYBREAK') {
|
|
|
1162 |
var lastV = day.visits[day.visits.length - 1];
|
|
|
1163 |
if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
|
|
|
1164 |
var retLine = new google.maps.Polyline({
|
|
|
1165 |
path: [{lat: lastV.lat, lng: lastV.lng}, {lat: state.homeLat, lng: state.homeLng}],
|
|
|
1166 |
geodesic: true, strokeColor: '#22c55e', strokeOpacity: isCurrent ? 0.4 : 0.2, strokeWeight: 2,
|
|
|
1167 |
icons: [{icon: {path: 'M 0,-1 0,1', strokeOpacity: 1, scale: 3}, offset: '0', repeat: '15px'}],
|
|
|
1168 |
map: map
|
|
|
1169 |
});
|
|
|
1170 |
routeLines.push(retLine);
|
|
|
1171 |
}
|
|
|
1172 |
}
|
|
|
1173 |
});
|
| 36621 |
ranu |
1174 |
}
|
|
|
1175 |
|
|
|
1176 |
function recalcDay() {
|
|
|
1177 |
var day = curDay();
|
|
|
1178 |
var pts = [{lat: day.startLat, lng: day.startLng}];
|
|
|
1179 |
day.visits.forEach(function (v) {
|
|
|
1180 |
if (v.lat && v.lng) pts.push({lat: v.lat, lng: v.lng});
|
|
|
1181 |
});
|
|
|
1182 |
|
|
|
1183 |
var km = 0;
|
|
|
1184 |
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;
|
|
|
1185 |
day.totalKm = km;
|
|
|
1186 |
day.totalMins = (km / AVG_SPEED) * 60 + day.visits.length * VISIT_MINS;
|
|
|
1187 |
updateBottomBar();
|
|
|
1188 |
}
|
|
|
1189 |
|
|
|
1190 |
function updateBottomBar() {
|
|
|
1191 |
var day = curDay();
|
|
|
1192 |
$('#bb-day-label').text('Day ' + day.dayNumber);
|
|
|
1193 |
$('#bb-stops').text(day.visits.length + ' stops');
|
|
|
1194 |
$('#bb-distance').text(day.totalKm.toFixed(1) + ' km');
|
|
|
1195 |
$('#bb-time').text(fmtMins(day.totalMins));
|
|
|
1196 |
|
|
|
1197 |
var pct = Math.min((day.totalMins / DAY_LIMIT) * 100, 100);
|
|
|
1198 |
var col = pct < 78 ? '#22c55e' : pct < 89 ? '#f59e0b' : '#ef4444';
|
|
|
1199 |
$('#bb-progress').css({width: pct + '%', background: col});
|
|
|
1200 |
}
|
|
|
1201 |
|
|
|
1202 |
// ============ ROUTE PANEL ============
|
|
|
1203 |
function renderRoutePanel() {
|
|
|
1204 |
var html = '';
|
| 36632 |
ranu |
1205 |
if (state.mode === 'create' && state.beatTitle) {
|
|
|
1206 |
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>';
|
|
|
1207 |
html += '<div style="font-size:14px;font-weight:600;color:#60a5fa;margin-bottom:10px;">' + state.beatTitle + '</div>';
|
| 36681 |
ranu |
1208 |
html += renderBaseLocationPicker();
|
| 36698 |
ranu |
1209 |
html += renderPartnerQuickAdd();
|
| 36632 |
ranu |
1210 |
}
|
| 36621 |
ranu |
1211 |
state.days.forEach(function (day) {
|
|
|
1212 |
var isCurrent = day.dayNumber === state.currentDay;
|
| 36632 |
ranu |
1213 |
var isLastDay = day.dayNumber === state.days.length;
|
| 36621 |
ranu |
1214 |
html += '<div class="route-day">';
|
|
|
1215 |
html += '<div class="route-day-header">';
|
|
|
1216 |
html += '<span>Day ' + day.dayNumber + (isCurrent ? ' (current)' : '') + '</span>';
|
|
|
1217 |
html += '<span>' + fmtMins(day.totalMins) + '</span>';
|
|
|
1218 |
html += '</div>';
|
|
|
1219 |
|
|
|
1220 |
// Start point
|
| 36632 |
ranu |
1221 |
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 |
1222 |
|
|
|
1223 |
// Visits with distance between each
|
|
|
1224 |
var prevLat = day.startLat, prevLng = day.startLng;
|
| 36632 |
ranu |
1225 |
var isLastStop;
|
| 36621 |
ranu |
1226 |
day.visits.forEach(function (v, i) {
|
| 36632 |
ranu |
1227 |
isLastStop = (i === day.visits.length - 1);
|
|
|
1228 |
|
| 36621 |
ranu |
1229 |
var segDist = 0, segTime = 0;
|
|
|
1230 |
if (prevLat && prevLng && v.lat && v.lng) {
|
|
|
1231 |
segDist = haversine(prevLat, prevLng, v.lat, v.lng) * ROAD_FACTOR;
|
|
|
1232 |
segTime = (segDist / AVG_SPEED) * 60;
|
|
|
1233 |
}
|
|
|
1234 |
if (segDist > 0) {
|
|
|
1235 |
html += '<div class="route-segment"><span class="seg-line"></span><span class="seg-dist">' + segDist.toFixed(1) + ' km | ' + fmtMins(segTime) + '</span></div>';
|
|
|
1236 |
}
|
|
|
1237 |
|
| 36642 |
ranu |
1238 |
var isLeadVisit = v.isLead || v.type === 'lead';
|
|
|
1239 |
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;' : '') + '">';
|
|
|
1240 |
html += '<span class="stop-num" style="' + (isLeadVisit ? 'background:#e67e22;' : '') + '">' + (i + 1) + '</span>';
|
|
|
1241 |
html += '<div class="stop-info"><div class="stop-name">' + (v.code || '') + (isLeadVisit ? ' <span style="color:#e67e22;font-size:10px;">LEAD VISIT</span>' : '') + '</div>';
|
| 36632 |
ranu |
1242 |
html += '<div class="stop-meta">' + (v.name || '') + '</div>';
|
|
|
1243 |
|
|
|
1244 |
// Show "Last Visit" + "Day Break" buttons on last stop of current day
|
|
|
1245 |
if (isLastStop && isCurrent && !day.endAction) {
|
|
|
1246 |
html += '<div style="margin-top:4px;display:flex;gap:4px;">';
|
|
|
1247 |
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>';
|
|
|
1248 |
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>';
|
|
|
1249 |
html += '</div>';
|
|
|
1250 |
}
|
|
|
1251 |
|
|
|
1252 |
html += '</div>';
|
| 36651 |
ranu |
1253 |
if (state.mode === 'create' || state.mode === 'edit') {
|
| 36632 |
ranu |
1254 |
html += '<span class="stop-remove" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" title="Remove">×</span>';
|
|
|
1255 |
}
|
| 36621 |
ranu |
1256 |
html += '</div>';
|
|
|
1257 |
|
|
|
1258 |
prevLat = v.lat;
|
|
|
1259 |
prevLng = v.lng;
|
|
|
1260 |
});
|
|
|
1261 |
|
| 36632 |
ranu |
1262 |
// Show return-to-home only when day is complete (Last Visit) or still in progress
|
|
|
1263 |
if (day.visits.length > 0 && day.endAction !== 'DAYBREAK') {
|
| 36621 |
ranu |
1264 |
var lastV = day.visits[day.visits.length - 1];
|
|
|
1265 |
var retDist = 0, retTime = 0;
|
|
|
1266 |
if (lastV.lat && lastV.lng && state.homeLat && state.homeLng) {
|
|
|
1267 |
retDist = haversine(lastV.lat, lastV.lng, state.homeLat, state.homeLng) * ROAD_FACTOR;
|
|
|
1268 |
retTime = (retDist / AVG_SPEED) * 60;
|
|
|
1269 |
}
|
|
|
1270 |
if (retDist > 0) {
|
| 36632 |
ranu |
1271 |
html += '<div class="route-segment"><span class="seg-line seg-line-home"></span><span class="seg-dist">' + retDist.toFixed(1) + ' km | ' + fmtMins(retTime) + ' (return)</span></div>';
|
| 36621 |
ranu |
1272 |
}
|
|
|
1273 |
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>';
|
|
|
1274 |
}
|
|
|
1275 |
|
| 36632 |
ranu |
1276 |
// Day break indicator
|
|
|
1277 |
if (day.endAction === 'DAYBREAK' && day.visits.length > 0) {
|
|
|
1278 |
html += '<div style="margin-left:12px;padding:4px 8px;font-size:11px;color:#f59e0b;font-weight:600;">-- Day Break (stays near last visit) --</div>';
|
|
|
1279 |
}
|
|
|
1280 |
|
| 36621 |
ranu |
1281 |
html += '<div class="day-summary">' + day.visits.length + ' stops | ' + day.totalKm.toFixed(1) + ' km</div>';
|
|
|
1282 |
html += '</div>';
|
|
|
1283 |
});
|
|
|
1284 |
|
|
|
1285 |
$('#route-list').html(html);
|
|
|
1286 |
}
|
|
|
1287 |
|
|
|
1288 |
// Click on stop name in route panel → show popup on map
|
|
|
1289 |
$(document).on('click', '.route-stop-clickable', function (e) {
|
|
|
1290 |
if ($(e.target).hasClass('stop-remove')) return; // let remove handler handle it
|
|
|
1291 |
var fofoId = parseInt($(this).data('fofoid'));
|
|
|
1292 |
if (!fofoId || !markers[fofoId]) return;
|
|
|
1293 |
|
|
|
1294 |
var marker = markers[fofoId];
|
|
|
1295 |
var p = state.partners.find(function (x) {
|
|
|
1296 |
return x.fofoId === fofoId;
|
|
|
1297 |
});
|
|
|
1298 |
if (!p) return;
|
|
|
1299 |
|
|
|
1300 |
map.panTo(marker.getPosition());
|
|
|
1301 |
map.setZoom(12);
|
|
|
1302 |
|
|
|
1303 |
var name = p.outletName || p.businessName || '';
|
|
|
1304 |
var addr = p.address || '';
|
|
|
1305 |
infoWindow.setContent('<div class="marker-popup"><h4>' + p.code + ' - ' + name + '</h4><div class="popup-meta">' + addr + '</div></div>');
|
|
|
1306 |
infoWindow.open(map, marker);
|
|
|
1307 |
});
|
|
|
1308 |
|
|
|
1309 |
// Remove stop from route panel
|
|
|
1310 |
$(document).on('click', '.stop-remove', function (e) {
|
|
|
1311 |
e.stopPropagation();
|
|
|
1312 |
var fofoId = parseInt($(this).data('fofoid'));
|
|
|
1313 |
var dayNum = parseInt($(this).data('daynum'));
|
|
|
1314 |
|
|
|
1315 |
var day = state.days[dayNum - 1];
|
|
|
1316 |
if (!day) return;
|
|
|
1317 |
|
|
|
1318 |
// Remove visit from day
|
|
|
1319 |
day.visits = day.visits.filter(function (v) {
|
|
|
1320 |
return v.id !== fofoId;
|
|
|
1321 |
});
|
|
|
1322 |
|
|
|
1323 |
// Reset marker back to red (unvisited)
|
|
|
1324 |
if (markers[fofoId]) {
|
|
|
1325 |
var p = state.partners.find(function (x) {
|
|
|
1326 |
return x.fofoId === fofoId;
|
|
|
1327 |
});
|
|
|
1328 |
markers[fofoId].setIcon({
|
|
|
1329 |
path: google.maps.SymbolPath.CIRCLE, scale: 14,
|
|
|
1330 |
fillColor: '#ef4444', fillOpacity: 0.9, strokeColor: '#fff', strokeWeight: 1
|
|
|
1331 |
});
|
|
|
1332 |
markers[fofoId].setLabel({text: (p ? p.code : '') || '', color: '#fff', fontSize: '10px', fontWeight: '600'});
|
|
|
1333 |
}
|
|
|
1334 |
|
|
|
1335 |
// Re-number remaining markers for this day
|
|
|
1336 |
day.visits.forEach(function (v, i) {
|
|
|
1337 |
if (markers[v.id]) {
|
|
|
1338 |
markers[v.id].setLabel({text: '' + (i + 1), color: '#fff', fontSize: '11px', fontWeight: '700'});
|
|
|
1339 |
}
|
|
|
1340 |
});
|
|
|
1341 |
|
|
|
1342 |
recalcDay();
|
|
|
1343 |
drawRoute();
|
|
|
1344 |
renderRoutePanel();
|
|
|
1345 |
updateBottomBar();
|
|
|
1346 |
});
|
|
|
1347 |
|
| 36632 |
ranu |
1348 |
// ============ LAST VISIT (from sidebar) — marks beat as complete, return home ============
|
|
|
1349 |
$(document).on('click', '.btn-last-visit', function (e) {
|
|
|
1350 |
e.stopPropagation();
|
|
|
1351 |
var dayNum = parseInt($(this).data('daynum'));
|
|
|
1352 |
var day = state.days[dayNum - 1];
|
|
|
1353 |
if (!day || day.visits.length === 0) return;
|
| 36621 |
ranu |
1354 |
|
| 36632 |
ranu |
1355 |
var lastVisit = day.visits[day.visits.length - 1];
|
|
|
1356 |
if (!confirm('Mark ' + (lastVisit.code || lastVisit.name) + ' as last visit? Beat will end here and return home.')) return;
|
| 36621 |
ranu |
1357 |
|
| 36632 |
ranu |
1358 |
day.endAction = 'HOME';
|
|
|
1359 |
drawRoute();
|
|
|
1360 |
renderRoutePanel();
|
|
|
1361 |
updateBottomBar();
|
| 36621 |
ranu |
1362 |
});
|
|
|
1363 |
|
| 36632 |
ranu |
1364 |
// ============ DAY BREAK (from sidebar) — end day, next day starts from last visit ============
|
|
|
1365 |
$(document).on('click', '.btn-day-break', function (e) {
|
|
|
1366 |
e.stopPropagation();
|
|
|
1367 |
var dayNum = parseInt($(this).data('daynum'));
|
|
|
1368 |
var day = state.days[dayNum - 1];
|
|
|
1369 |
if (!day || day.visits.length === 0) return;
|
| 36621 |
ranu |
1370 |
|
| 36632 |
ranu |
1371 |
var lastVisit = day.visits[day.visits.length - 1];
|
|
|
1372 |
if (!confirm('Day Break at ' + (lastVisit.code || lastVisit.name) + '? Next day will start from this location.')) return;
|
|
|
1373 |
|
|
|
1374 |
day.endAction = 'DAYBREAK';
|
|
|
1375 |
|
|
|
1376 |
// Next day starts from LAST VISIT location (not home)
|
|
|
1377 |
var nextNum = state.days.length + 1;
|
|
|
1378 |
state.days.push({
|
|
|
1379 |
dayNumber: nextNum,
|
|
|
1380 |
startLat: lastVisit.lat, startLng: lastVisit.lng, startName: lastVisit.code || lastVisit.name,
|
|
|
1381 |
visits: [], endAction: null,
|
|
|
1382 |
stayName: null, stayLat: null, stayLng: null,
|
|
|
1383 |
totalKm: 0, totalMins: 0
|
|
|
1384 |
});
|
|
|
1385 |
state.currentDay = nextNum;
|
|
|
1386 |
|
|
|
1387 |
resetMarkerColors();
|
|
|
1388 |
drawRoute();
|
| 36621 |
ranu |
1389 |
renderRoutePanel();
|
| 36632 |
ranu |
1390 |
updateBottomBar();
|
| 36621 |
ranu |
1391 |
});
|
|
|
1392 |
|
|
|
1393 |
$('#btn-finish').on('click', function () {
|
| 36644 |
ranu |
1394 |
// Guard 1: already submitting — ignore duplicate clicks
|
|
|
1395 |
if (isSubmittingBeat) {
|
|
|
1396 |
return;
|
|
|
1397 |
}
|
|
|
1398 |
// Guard 2: this beat was already saved — don't re-submit
|
|
|
1399 |
if (state.savedPlanGroupId) {
|
|
|
1400 |
alert('This beat is already saved.');
|
|
|
1401 |
showBeatList();
|
|
|
1402 |
return;
|
|
|
1403 |
}
|
| 36621 |
ranu |
1404 |
if (state.days.every(function (d) {
|
|
|
1405 |
return d.visits.length === 0;
|
|
|
1406 |
})) {
|
|
|
1407 |
alert('No visits added');
|
|
|
1408 |
return;
|
|
|
1409 |
}
|
|
|
1410 |
|
| 36681 |
ranu |
1411 |
var beatTitle = state.beatTitle || 'Beat';
|
|
|
1412 |
var isEdit = state.mode === 'edit' && state.editingBeatId;
|
| 36621 |
ranu |
1413 |
|
| 36681 |
ranu |
1414 |
// EDIT RULE: cannot grow day count. Caught client-side so the modal
|
|
|
1415 |
// doesn't even open; server enforces the same rule.
|
|
|
1416 |
if (isEdit && state.originalDayCount && state.days.length > state.originalDayCount) {
|
|
|
1417 |
alert('Cannot increase the number of days while editing.\n\n'
|
|
|
1418 |
+ 'Original: ' + state.originalDayCount + ' day(s)\n'
|
|
|
1419 |
+ 'Current: ' + state.days.length + ' day(s)\n\n'
|
|
|
1420 |
+ 'Please create a new beat for additional days.');
|
|
|
1421 |
return;
|
|
|
1422 |
}
|
|
|
1423 |
|
|
|
1424 |
// Build plan data
|
| 36621 |
ranu |
1425 |
var daysData = [];
|
|
|
1426 |
state.days.forEach(function (day) {
|
|
|
1427 |
if (day.visits.length === 0) return;
|
|
|
1428 |
daysData.push({
|
|
|
1429 |
dayNumber: day.dayNumber,
|
|
|
1430 |
startLocationName: day.startName,
|
|
|
1431 |
startLatitude: day.startLat ? day.startLat.toString() : null,
|
|
|
1432 |
startLongitude: day.startLng ? day.startLng.toString() : null,
|
|
|
1433 |
endAction: day.endAction,
|
|
|
1434 |
stayLocationName: day.stayName,
|
|
|
1435 |
stayLatitude: day.stayLat ? day.stayLat.toString() : null,
|
|
|
1436 |
stayLongitude: day.stayLng ? day.stayLng.toString() : null,
|
|
|
1437 |
totalDistanceKm: day.totalKm,
|
|
|
1438 |
totalTimeMins: Math.round(day.totalMins),
|
|
|
1439 |
visits: day.visits.map(function (v) {
|
|
|
1440 |
return {id: v.id, type: v.type || 'partner'};
|
|
|
1441 |
})
|
|
|
1442 |
});
|
|
|
1443 |
});
|
|
|
1444 |
|
| 36681 |
ranu |
1445 |
// EDIT RULE: detect removed leads → show popup with cancel/reschedule per lead
|
|
|
1446 |
var removedLeads = [];
|
|
|
1447 |
if (isEdit && state.originalLeads && state.originalLeads.length) {
|
|
|
1448 |
var currentLeadIds = {};
|
|
|
1449 |
state.days.forEach(function (d) {
|
|
|
1450 |
d.visits.forEach(function (v) {
|
|
|
1451 |
if (v.type === 'lead' || v.isLead) currentLeadIds[v.id] = true;
|
|
|
1452 |
});
|
|
|
1453 |
});
|
|
|
1454 |
removedLeads = state.originalLeads.filter(function (l) {
|
|
|
1455 |
return !currentLeadIds[l.leadId];
|
|
|
1456 |
});
|
| 36651 |
ranu |
1457 |
}
|
| 36681 |
ranu |
1458 |
|
|
|
1459 |
if (removedLeads.length > 0) {
|
|
|
1460 |
openRemovedLeadsModal(removedLeads, function (actions) {
|
|
|
1461 |
doFinishSubmit(daysData, beatTitle, isEdit, actions);
|
|
|
1462 |
});
|
|
|
1463 |
return;
|
|
|
1464 |
}
|
|
|
1465 |
|
|
|
1466 |
doFinishSubmit(daysData, beatTitle, isEdit, null);
|
|
|
1467 |
});
|
|
|
1468 |
|
|
|
1469 |
function doFinishSubmit(daysData, beatTitle, isEdit, removedLeadActions) {
|
|
|
1470 |
var planPayload = {
|
|
|
1471 |
days: daysData,
|
|
|
1472 |
dates: daysData.map(function () {
|
|
|
1473 |
return null;
|
|
|
1474 |
}),
|
|
|
1475 |
beatName: beatTitle.trim()
|
|
|
1476 |
};
|
|
|
1477 |
if (state.mode === 'edit' && state.editingDate) planPayload.planDate = state.editingDate;
|
|
|
1478 |
if (removedLeadActions && removedLeadActions.length) {
|
|
|
1479 |
planPayload.removedLeadActions = JSON.stringify(removedLeadActions);
|
|
|
1480 |
}
|
| 36651 |
ranu |
1481 |
var planData = JSON.stringify(planPayload);
|
| 36621 |
ranu |
1482 |
|
| 36644 |
ranu |
1483 |
isSubmittingBeat = true;
|
| 36651 |
ranu |
1484 |
$('#btn-finish').prop('disabled', true).text(isEdit ? 'Updating...' : 'Saving...');
|
| 36621 |
ranu |
1485 |
|
| 36651 |
ranu |
1486 |
var url = isEdit ? '/beatPlan/updateBeat' : '/beatPlan/submitPlan';
|
|
|
1487 |
var ajaxData = isEdit
|
|
|
1488 |
? {beatId: state.editingBeatId, planData: planData}
|
|
|
1489 |
: {authUserId: state.authUserId, planData: planData};
|
|
|
1490 |
|
| 36621 |
ranu |
1491 |
$.ajax({
|
| 36651 |
ranu |
1492 |
url: context + url,
|
| 36621 |
ranu |
1493 |
type: "POST",
|
| 36651 |
ranu |
1494 |
data: ajaxData,
|
| 36621 |
ranu |
1495 |
success: function (response) {
|
|
|
1496 |
var data = (typeof response === 'string' ? JSON.parse(response) : response);
|
|
|
1497 |
var result = data.response || data;
|
|
|
1498 |
|
|
|
1499 |
if (result.status) {
|
|
|
1500 |
state.savedPlanGroupId = result.planGroupId;
|
|
|
1501 |
$('#bottom-bar').hide();
|
|
|
1502 |
|
| 36681 |
ranu |
1503 |
if (result.duplicate) {
|
| 36644 |
ranu |
1504 |
alert('This beat already exists.');
|
| 36651 |
ranu |
1505 |
} else if (isEdit) {
|
| 36681 |
ranu |
1506 |
var extras = [];
|
|
|
1507 |
if (result.leadsCancelled) extras.push(result.leadsCancelled + ' lead(s) cancelled');
|
|
|
1508 |
if (result.leadsRescheduled) extras.push(result.leadsRescheduled + ' lead(s) rescheduled');
|
|
|
1509 |
alert('Beat "' + beatTitle + '" updated successfully'
|
|
|
1510 |
+ (extras.length ? '\n\n' + extras.join('\n') : '') + '.');
|
|
|
1511 |
} else {
|
|
|
1512 |
alert('Beat "' + beatTitle + '" saved successfully!');
|
|
|
1513 |
}
|
| 36632 |
ranu |
1514 |
|
| 36698 |
ranu |
1515 |
// Wipe in-progress beat state for this user — the next "+ New Beat"
|
|
|
1516 |
// must start from a clean slate (no leftover days/visits/title/leads).
|
| 36651 |
ranu |
1517 |
state.editingBeatId = null;
|
| 36698 |
ranu |
1518 |
state.editingDate = null;
|
|
|
1519 |
state.beatTitle = null;
|
|
|
1520 |
state.days = [];
|
|
|
1521 |
state.currentDay = 1;
|
|
|
1522 |
state.originalDayCount = null;
|
|
|
1523 |
state.originalLeads = [];
|
|
|
1524 |
state.savedPlanGroupId = null;
|
|
|
1525 |
|
|
|
1526 |
// Clear map artifacts from the just-saved route so the canvas
|
|
|
1527 |
// doesn't show stale polylines/labels behind the beat list.
|
|
|
1528 |
routeLines.forEach(function (l) {
|
|
|
1529 |
l.setMap(null);
|
|
|
1530 |
});
|
|
|
1531 |
routeLines = [];
|
|
|
1532 |
if (previewLine) {
|
|
|
1533 |
previewLine.setMap(null);
|
|
|
1534 |
previewLine = null;
|
|
|
1535 |
}
|
|
|
1536 |
if (typeof resetMarkerColors === 'function') resetMarkerColors();
|
|
|
1537 |
|
| 36651 |
ranu |
1538 |
state.mode = 'list';
|
| 36681 |
ranu |
1539 |
showBeatList();
|
| 36621 |
ranu |
1540 |
} else {
|
| 36651 |
ranu |
1541 |
$('#btn-finish').prop('disabled', false).text(isEdit ? 'Save Changes' : 'Finish Plan');
|
| 36621 |
ranu |
1542 |
alert('Error saving beat plan');
|
|
|
1543 |
}
|
| 36644 |
ranu |
1544 |
isSubmittingBeat = false;
|
| 36621 |
ranu |
1545 |
},
|
|
|
1546 |
error: function (xhr) {
|
| 36644 |
ranu |
1547 |
isSubmittingBeat = false;
|
| 36651 |
ranu |
1548 |
$('#btn-finish').prop('disabled', false).text(isEdit ? 'Save Changes' : 'Finish Plan');
|
| 36681 |
ranu |
1549 |
// Surface a clean server message if present (e.g., day-increase block, missing beat on reschedule date)
|
|
|
1550 |
var msg = xhr.responseText || xhr.statusText;
|
|
|
1551 |
try {
|
|
|
1552 |
var err = JSON.parse(xhr.responseText);
|
|
|
1553 |
if (err && err.response) {
|
|
|
1554 |
if (typeof err.response === 'string') msg = err.response;
|
|
|
1555 |
else if (err.response.message) msg = err.response.message;
|
|
|
1556 |
}
|
|
|
1557 |
} catch (_) {
|
|
|
1558 |
}
|
|
|
1559 |
alert(msg);
|
| 36621 |
ranu |
1560 |
}
|
|
|
1561 |
});
|
| 36681 |
ranu |
1562 |
}
|
| 36621 |
ranu |
1563 |
|
| 36681 |
ranu |
1564 |
// ============ REMOVED LEADS POPUP ============
|
|
|
1565 |
// Shown when an edit removes one or more leads. Per lead, the user picks:
|
|
|
1566 |
// - Cancel → mark CANCELLED
|
|
|
1567 |
// - Reschedule + date → move to whichever beat the same user has on that date
|
|
|
1568 |
// The date input is validated against /beatPlan/userBeatsOnDate so the user
|
|
|
1569 |
// sees "No beat on this date — pick another" before submit.
|
|
|
1570 |
function openRemovedLeadsModal(removedLeads, onConfirm) {
|
|
|
1571 |
$('#modal-removed-leads').remove(); // clean any prior instance
|
|
|
1572 |
|
|
|
1573 |
var rowsHtml = removedLeads.map(function (l) {
|
|
|
1574 |
return ''
|
|
|
1575 |
+ '<div class="rl-row" data-leadid="' + l.leadId + '" style="border:1px solid #334155;border-radius:6px;padding:8px;margin-bottom:8px;">'
|
|
|
1576 |
+ ' <div style="font-weight:600;color:#f1f5f9;margin-bottom:6px;">'
|
|
|
1577 |
+ ' ' + (l.name || ('Lead #' + l.leadId)) + ' <span style="color:#64748b;font-weight:400;font-size:11px;">(was on day ' + l.dayNumber + ')</span>'
|
|
|
1578 |
+ ' </div>'
|
|
|
1579 |
+ ' <label style="display:inline-flex;align-items:center;gap:6px;margin-right:14px;cursor:pointer;font-size:12px;">'
|
|
|
1580 |
+ ' <input type="radio" name="rl-act-' + l.leadId + '" value="cancel" checked> Cancel this lead'
|
|
|
1581 |
+ ' </label>'
|
|
|
1582 |
+ ' <label style="display:inline-flex;align-items:center;gap:6px;cursor:pointer;font-size:12px;">'
|
|
|
1583 |
+ ' <input type="radio" name="rl-act-' + l.leadId + '" value="reschedule"> Reschedule to another date'
|
|
|
1584 |
+ ' </label>'
|
|
|
1585 |
+ ' <div class="rl-date-wrap" style="display:none;margin-top:6px;">'
|
|
|
1586 |
+ ' <input type="date" class="rl-date" min="' + fmtDate(new Date()) + '" style="width:auto;display:inline-block;margin-bottom:0;">'
|
|
|
1587 |
+ ' <span class="rl-date-status" style="font-size:11px;margin-left:8px;"></span>'
|
|
|
1588 |
+ ' </div>'
|
|
|
1589 |
+ '</div>';
|
|
|
1590 |
}).join('');
|
|
|
1591 |
|
|
|
1592 |
var html = ''
|
|
|
1593 |
+ '<div class="modal-overlay show" id="modal-removed-leads">'
|
|
|
1594 |
+ ' <div class="modal-box" style="min-width:520px;max-width:680px;">'
|
|
|
1595 |
+ ' <h3>You removed ' + removedLeads.length + ' lead(s) from this beat</h3>'
|
|
|
1596 |
+ ' <p style="font-size:12px;color:#94a3b8;margin-bottom:12px;">'
|
|
|
1597 |
+ ' Choose what to do with each lead before saving. Reschedule moves the lead to whichever beat this user has on the selected date.'
|
|
|
1598 |
+ ' </p>'
|
|
|
1599 |
+ ' <div id="rl-list" style="max-height:55vh;overflow-y:auto;">' + rowsHtml + '</div>'
|
|
|
1600 |
+ ' <div class="modal-actions">'
|
|
|
1601 |
+ ' <button class="modal-cancel" id="rl-cancel">Cancel (keep editing)</button>'
|
|
|
1602 |
+ ' <button class="modal-confirm" id="rl-confirm">Save with these actions</button>'
|
|
|
1603 |
+ ' </div>'
|
|
|
1604 |
+ ' </div>'
|
|
|
1605 |
+ '</div>';
|
|
|
1606 |
$('body').append(html);
|
|
|
1607 |
|
|
|
1608 |
// Show/hide date picker per row
|
|
|
1609 |
$('#modal-removed-leads').on('change', 'input[type="radio"]', function () {
|
|
|
1610 |
var $row = $(this).closest('.rl-row');
|
|
|
1611 |
var isResched = $row.find('input[type="radio"]:checked').val() === 'reschedule';
|
|
|
1612 |
$row.find('.rl-date-wrap').toggle(isResched);
|
|
|
1613 |
$row.find('.rl-date-status').text('');
|
|
|
1614 |
});
|
|
|
1615 |
|
|
|
1616 |
// Validate the date as the user types it — show "no beat on this date" warning
|
|
|
1617 |
$('#modal-removed-leads').on('change', '.rl-date', function () {
|
|
|
1618 |
var $row = $(this).closest('.rl-row');
|
|
|
1619 |
var $status = $row.find('.rl-date-status');
|
|
|
1620 |
var d = $(this).val();
|
|
|
1621 |
if (!d) {
|
|
|
1622 |
$status.text('');
|
|
|
1623 |
return;
|
|
|
1624 |
}
|
|
|
1625 |
$status.text('Checking...').css('color', '#94a3b8');
|
|
|
1626 |
$.ajax({
|
|
|
1627 |
url: context + '/beatPlan/userBeatsOnDate', type: 'GET', dataType: 'json',
|
|
|
1628 |
data: {authUserId: state.authUserId, date: d},
|
|
|
1629 |
success: function (r) {
|
|
|
1630 |
var dd = r.response || r;
|
|
|
1631 |
var beats = dd.beats || [];
|
|
|
1632 |
if (beats.length === 0) {
|
|
|
1633 |
$status.text('No beat on this date — pick another date, or Cancel this lead instead.').css('color', '#ef4444');
|
|
|
1634 |
} else {
|
|
|
1635 |
var names = beats.map(function (b) {
|
|
|
1636 |
return '"' + b.beatName + '" (day ' + b.dayNumber + ')';
|
|
|
1637 |
}).join(', ');
|
|
|
1638 |
$status.text('Will attach to: ' + names).css('color', '#22c55e');
|
|
|
1639 |
}
|
|
|
1640 |
},
|
|
|
1641 |
error: function () {
|
|
|
1642 |
$status.text('Could not verify date').css('color', '#f59e0b');
|
|
|
1643 |
}
|
|
|
1644 |
});
|
|
|
1645 |
});
|
|
|
1646 |
|
|
|
1647 |
$('#modal-removed-leads').on('click', '#rl-cancel', function () {
|
|
|
1648 |
$('#modal-removed-leads').remove();
|
|
|
1649 |
// abort save — user goes back to editing
|
|
|
1650 |
});
|
|
|
1651 |
|
|
|
1652 |
$('#modal-removed-leads').on('click', '#rl-confirm', function () {
|
|
|
1653 |
var actions = [];
|
|
|
1654 |
var problems = [];
|
|
|
1655 |
$('#modal-removed-leads .rl-row').each(function () {
|
|
|
1656 |
var $row = $(this);
|
|
|
1657 |
var leadId = parseInt($row.data('leadid'));
|
|
|
1658 |
var mode = $row.find('input[type="radio"]:checked').val();
|
|
|
1659 |
if (mode === 'reschedule') {
|
|
|
1660 |
var d = $row.find('.rl-date').val();
|
|
|
1661 |
if (!d) {
|
|
|
1662 |
problems.push('Lead ' + leadId + ': pick a date or switch to Cancel');
|
|
|
1663 |
return;
|
|
|
1664 |
}
|
|
|
1665 |
actions.push({leadId: leadId, action: 'reschedule', toDate: d});
|
|
|
1666 |
} else {
|
|
|
1667 |
actions.push({leadId: leadId, action: 'cancel'});
|
|
|
1668 |
}
|
|
|
1669 |
});
|
|
|
1670 |
if (problems.length) {
|
|
|
1671 |
alert(problems.join('\n'));
|
|
|
1672 |
return;
|
|
|
1673 |
}
|
|
|
1674 |
$('#modal-removed-leads').remove();
|
|
|
1675 |
onConfirm(actions);
|
|
|
1676 |
});
|
|
|
1677 |
}
|
|
|
1678 |
|
| 36621 |
ranu |
1679 |
// ============ PANEL TABS ============
|
|
|
1680 |
$('.panel-tab').on('click', function () {
|
|
|
1681 |
var tab = $(this).data('tab');
|
|
|
1682 |
$('.panel-tab').removeClass('active');
|
|
|
1683 |
$(this).addClass('active');
|
|
|
1684 |
$('#panel-route').toggle(tab === 'route');
|
|
|
1685 |
$('#panel-calendar').toggle(tab === 'calendar');
|
|
|
1686 |
$('#cal-grid-container').toggle(tab === 'calendar');
|
| 36698 |
ranu |
1687 |
$('#bottom-bar').toggle(tab === 'route' && (state.mode === 'create' || state.mode === 'edit') && state.days && state.days.length > 0);
|
| 36621 |
ranu |
1688 |
|
|
|
1689 |
if (tab === 'calendar' && !state.calMonth) {
|
|
|
1690 |
var now = new Date();
|
|
|
1691 |
state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
|
|
|
1692 |
loadCalendarData();
|
|
|
1693 |
}
|
|
|
1694 |
});
|
|
|
1695 |
|
| 36644 |
ranu |
1696 |
// Click a beat chip on the calendar → view that specific day's route
|
|
|
1697 |
// (partners + only the leads scheduled for that exact date)
|
|
|
1698 |
$(document).on('click', '.cal-chip-view', function (e) {
|
|
|
1699 |
if ($(e.target).hasClass('cal-chip-remove')) return; // handled separately
|
|
|
1700 |
e.stopPropagation();
|
|
|
1701 |
var pg = $(this).data('plangroup');
|
|
|
1702 |
var date = $(this).data('date');
|
|
|
1703 |
if (!pg || !date) return;
|
|
|
1704 |
// Switch to Route tab and load that day's run
|
|
|
1705 |
$('.panel-tab[data-tab="route"]').click();
|
|
|
1706 |
viewBeat(String(pg), String(date));
|
|
|
1707 |
});
|
|
|
1708 |
|
| 36621 |
ranu |
1709 |
// ============ CALENDAR ============
|
|
|
1710 |
$('#cal-prev').on('click', function () {
|
|
|
1711 |
navMonth(-1);
|
|
|
1712 |
});
|
|
|
1713 |
$('#cal-next').on('click', function () {
|
|
|
1714 |
navMonth(1);
|
|
|
1715 |
});
|
|
|
1716 |
|
|
|
1717 |
function navMonth(delta) {
|
|
|
1718 |
var p = state.calMonth.split('-');
|
|
|
1719 |
var d = new Date(parseInt(p[0]), parseInt(p[1]) - 1 + delta, 1);
|
|
|
1720 |
state.calMonth = d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2);
|
|
|
1721 |
loadCalendarData();
|
|
|
1722 |
}
|
|
|
1723 |
|
|
|
1724 |
function loadCalendarData() {
|
|
|
1725 |
if (!state.authUserId) return;
|
|
|
1726 |
$('#cal-month-label').text(state.calMonth);
|
|
|
1727 |
|
|
|
1728 |
$.ajax({
|
|
|
1729 |
url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
|
|
|
1730 |
data: {authUserId: state.authUserId, month: state.calMonth},
|
|
|
1731 |
success: function (r) {
|
|
|
1732 |
var data = r.response || r;
|
|
|
1733 |
state.calData = data;
|
|
|
1734 |
renderCalGrid(data);
|
|
|
1735 |
renderBeatCards(data.scheduledBeats);
|
|
|
1736 |
}
|
|
|
1737 |
});
|
|
|
1738 |
}
|
|
|
1739 |
|
|
|
1740 |
function renderCalGrid(data) {
|
|
|
1741 |
var p = state.calMonth.split('-');
|
|
|
1742 |
var year = parseInt(p[0]), month = parseInt(p[1]) - 1;
|
|
|
1743 |
var first = new Date(year, month, 1);
|
|
|
1744 |
var last = new Date(year, month + 1, 0);
|
|
|
1745 |
var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
|
1746 |
$('#cal-month-label').text(months[month] + ' ' + year);
|
|
|
1747 |
|
|
|
1748 |
var holidayMap = {};
|
|
|
1749 |
(data.holidays || []).forEach(function (h) {
|
|
|
1750 |
holidayMap[h.date] = h.occasion;
|
|
|
1751 |
});
|
|
|
1752 |
var blockedSet = {};
|
|
|
1753 |
(data.blockedDates || []).forEach(function (d) {
|
|
|
1754 |
blockedSet[d] = true;
|
|
|
1755 |
});
|
|
|
1756 |
|
|
|
1757 |
var beatDateMap = {};
|
|
|
1758 |
(data.scheduledBeats || []).forEach(function (b) {
|
|
|
1759 |
(b.days || []).forEach(function (d) {
|
|
|
1760 |
if (d.planDate) {
|
|
|
1761 |
if (!beatDateMap[d.planDate]) beatDateMap[d.planDate] = [];
|
|
|
1762 |
beatDateMap[d.planDate].push({
|
|
|
1763 |
name: b.beatName,
|
|
|
1764 |
color: b.beatColor,
|
|
|
1765 |
day: d.dayNumber,
|
|
|
1766 |
status: b.status,
|
| 36632 |
ranu |
1767 |
visits: d.visitCount,
|
|
|
1768 |
planGroupId: b.planGroupId
|
| 36621 |
ranu |
1769 |
});
|
|
|
1770 |
}
|
|
|
1771 |
});
|
|
|
1772 |
});
|
|
|
1773 |
|
|
|
1774 |
var today = fmtDate(new Date());
|
|
|
1775 |
var startOff = (first.getDay() + 6) % 7;
|
|
|
1776 |
var calStart = new Date(year, month, 1 - startOff);
|
|
|
1777 |
|
|
|
1778 |
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>';
|
|
|
1779 |
var d = new Date(calStart);
|
|
|
1780 |
for (var w = 0; w < 6; w++) {
|
|
|
1781 |
var hasDay = false;
|
|
|
1782 |
var row = '<tr>';
|
|
|
1783 |
for (var wd = 0; wd < 7; wd++) {
|
|
|
1784 |
var ds = fmtDate(d);
|
|
|
1785 |
var inMonth = d.getMonth() === month;
|
|
|
1786 |
var sun = d.getDay() === 0;
|
|
|
1787 |
var hol = !!holidayMap[ds];
|
|
|
1788 |
var isToday = ds === today;
|
|
|
1789 |
var beats = beatDateMap[ds] || [];
|
|
|
1790 |
var isPicked = state.calPickedDates.indexOf(ds) > -1;
|
|
|
1791 |
|
| 36632 |
ranu |
1792 |
var isPast = ds < today;
|
|
|
1793 |
var hasBeats = beats.length > 0;
|
|
|
1794 |
|
| 36621 |
ranu |
1795 |
var cls = [];
|
| 36632 |
ranu |
1796 |
if (hol && !sun) cls.push('holiday', 'blocked');
|
|
|
1797 |
else if (isPast) cls.push('blocked', 'past');
|
|
|
1798 |
if (sun && !isPast) cls.push('sunday'); // Sundays styled but not blocked
|
|
|
1799 |
if (sun && isPast) cls.push('blocked', 'past');
|
| 36621 |
ranu |
1800 |
if (isToday) cls.push('today');
|
|
|
1801 |
if (!inMonth) cls.push('blocked');
|
|
|
1802 |
|
|
|
1803 |
var style = inMonth ? '' : 'opacity:0.3;';
|
| 36632 |
ranu |
1804 |
if (isPast && inMonth) style += 'opacity:0.5;';
|
| 36621 |
ranu |
1805 |
|
| 36632 |
ranu |
1806 |
row += '<td class="' + cls.join(' ') + '" data-date="' + ds + '" data-hasbeat="' + (hasBeats ? '1' : '0') + '" style="' + style + '">';
|
| 36621 |
ranu |
1807 |
row += '<div class="cal-date">' + d.getDate() + '</div>';
|
|
|
1808 |
if (hol) row += '<div class="cal-holiday-label">' + holidayMap[ds] + '</div>';
|
|
|
1809 |
|
|
|
1810 |
beats.forEach(function (b) {
|
|
|
1811 |
var chipCls = 'cal-chip';
|
|
|
1812 |
if (b.status === 'running') chipCls += ' running';
|
|
|
1813 |
if (b.status === 'completed') chipCls += ' completed';
|
| 36632 |
ranu |
1814 |
var removeBtn = (!isPast && b.status !== 'running' && b.status !== 'completed')
|
|
|
1815 |
? '<span class="cal-chip-remove" data-date="' + ds + '" data-plangroup="' + (b.planGroupId || '') + '" title="Remove">×</span>'
|
|
|
1816 |
: '';
|
| 36670 |
ranu |
1817 |
var draggable = (!isPast && b.status !== 'running' && b.status !== 'completed') ? ' draggable="true"' : '';
|
|
|
1818 |
var cursor = draggable ? 'grab' : 'pointer';
|
|
|
1819 |
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">'
|
| 36632 |
ranu |
1820 |
+ b.name + ' D' + b.day + (b.status === 'running' ? ' [live]' : '') + ' (' + b.visits + ')'
|
|
|
1821 |
+ removeBtn + '</span>';
|
| 36621 |
ranu |
1822 |
});
|
|
|
1823 |
|
|
|
1824 |
if (isPicked) {
|
|
|
1825 |
var idx = state.calPickedDates.indexOf(ds) + 1;
|
|
|
1826 |
row += '<span class="cal-chip" style="background:#3b82f6;border:1px dashed #93c5fd;">Day ' + idx + '</span>';
|
|
|
1827 |
}
|
|
|
1828 |
|
|
|
1829 |
row += '</td>';
|
|
|
1830 |
if (inMonth) hasDay = true;
|
|
|
1831 |
d.setDate(d.getDate() + 1);
|
|
|
1832 |
}
|
|
|
1833 |
row += '</tr>';
|
|
|
1834 |
if (hasDay) html += row;
|
|
|
1835 |
}
|
|
|
1836 |
html += '</table>';
|
|
|
1837 |
$('#cal-grid-content').html(html);
|
|
|
1838 |
}
|
|
|
1839 |
|
|
|
1840 |
function renderBeatCards(beats) {
|
|
|
1841 |
var html = '';
|
|
|
1842 |
if (!beats || beats.length === 0) {
|
|
|
1843 |
html = '<p style="color:#64748b;font-size:12px;padding:20px;">No beats scheduled. Create a beat first.</p>';
|
| 36632 |
ranu |
1844 |
$('#cal-beat-cards').html(html);
|
|
|
1845 |
return;
|
| 36621 |
ranu |
1846 |
}
|
| 36632 |
ranu |
1847 |
|
|
|
1848 |
// Deduplicate by beatName — group beats with same name, show count
|
|
|
1849 |
var beatGroups = {};
|
| 36621 |
ranu |
1850 |
(beats || []).forEach(function (b) {
|
| 36632 |
ranu |
1851 |
var key = b.beatName || b.planGroupId;
|
|
|
1852 |
if (!beatGroups[key]) {
|
|
|
1853 |
beatGroups[key] = {
|
|
|
1854 |
beatName: b.beatName,
|
|
|
1855 |
beatColor: b.beatColor,
|
|
|
1856 |
instances: []
|
|
|
1857 |
};
|
|
|
1858 |
}
|
|
|
1859 |
beatGroups[key].instances.push(b);
|
|
|
1860 |
});
|
| 36621 |
ranu |
1861 |
|
| 36632 |
ranu |
1862 |
Object.keys(beatGroups).forEach(function (key) {
|
|
|
1863 |
var group = beatGroups[key];
|
|
|
1864 |
var first = group.instances[0];
|
|
|
1865 |
var scheduledCount = group.instances.filter(function (b) {
|
|
|
1866 |
return b.status === 'scheduled';
|
|
|
1867 |
}).length;
|
|
|
1868 |
var unscheduledCount = group.instances.filter(function (b) {
|
|
|
1869 |
return b.status === 'unscheduled';
|
|
|
1870 |
}).length;
|
|
|
1871 |
var totalInstances = group.instances.length;
|
|
|
1872 |
|
| 36621 |
ranu |
1873 |
var totalV = 0, totalKm = 0;
|
| 36632 |
ranu |
1874 |
first.days.forEach(function (d) {
|
|
|
1875 |
totalV += d.visitCount || 0;
|
|
|
1876 |
totalKm += d.totalKm || 0;
|
|
|
1877 |
});
|
| 36621 |
ranu |
1878 |
|
| 36632 |
ranu |
1879 |
// Use first unscheduled instance for scheduling, or first overall
|
|
|
1880 |
var primaryPg = first.planGroupId;
|
|
|
1881 |
var unscheduledInstance = group.instances.find(function (b) {
|
|
|
1882 |
return b.status === 'unscheduled';
|
|
|
1883 |
});
|
|
|
1884 |
if (unscheduledInstance) primaryPg = unscheduledInstance.planGroupId;
|
|
|
1885 |
|
|
|
1886 |
var statusText = '';
|
|
|
1887 |
if (scheduledCount > 0) statusText += scheduledCount + ' scheduled';
|
|
|
1888 |
if (unscheduledCount > 0) statusText += (statusText ? ', ' : '') + unscheduledCount + ' unscheduled';
|
|
|
1889 |
|
|
|
1890 |
var isScheduling = state.calSelectedBeat === primaryPg && state.calPickedDates.length > 0;
|
|
|
1891 |
|
|
|
1892 |
html += '<div class="beat-card" data-plangroup="' + primaryPg + '" data-beatname="' + (group.beatName || '') + '" draggable="true">';
|
|
|
1893 |
html += '<div class="beat-name"><span style="display:inline-block;width:12px;height:12px;border-radius:2px;background:' + group.beatColor + ';"></span> ' + group.beatName + '</div>';
|
|
|
1894 |
html += '<div class="beat-meta">' + first.days.length + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km</div>';
|
|
|
1895 |
html += '<div class="beat-meta" style="color:#94a3b8;">' + statusText + ' (' + totalInstances + ' total)</div>';
|
|
|
1896 |
|
|
|
1897 |
html += '<div class="beat-actions">';
|
|
|
1898 |
if (isScheduling) {
|
|
|
1899 |
html += '<button class="btn-sm btn-confirm-schedule" data-pg="' + primaryPg + '" data-days="' + first.days.length + '" style="background:#22c55e;color:#fff;">Confirm (' + state.calPickedDates.length + ' dates)</button>';
|
|
|
1900 |
html += '<button class="btn-sm btn-cancel-schedule" style="background:#475569;color:#e2e8f0;">Cancel</button>';
|
|
|
1901 |
} else {
|
|
|
1902 |
html += '<button class="btn-sm btn-schedule" data-pg="' + primaryPg + '" data-days="' + first.days.length + '">Schedule</button>';
|
|
|
1903 |
}
|
|
|
1904 |
html += '<button class="btn-sm" style="background:#ef4444;color:#fff;" onclick="deleteBeat(\'' + primaryPg + '\')">Delete</button>';
|
|
|
1905 |
html += '</div>';
|
|
|
1906 |
|
|
|
1907 |
if (isScheduling) {
|
|
|
1908 |
html += '<div style="font-size:10px;color:#60a5fa;margin-top:4px;">Dates: ' + state.calPickedDates.join(', ') + '</div>';
|
|
|
1909 |
html += '<div style="font-size:10px;color:#94a3b8;">Click calendar cells to adjust</div>';
|
|
|
1910 |
}
|
|
|
1911 |
|
|
|
1912 |
html += '<div style="font-size:9px;color:#475569;margin-top:4px;">Drag onto calendar to schedule</div>';
|
| 36621 |
ranu |
1913 |
html += '</div>';
|
|
|
1914 |
});
|
| 36632 |
ranu |
1915 |
|
| 36621 |
ranu |
1916 |
$('#cal-beat-cards').html(html);
|
|
|
1917 |
}
|
|
|
1918 |
|
|
|
1919 |
// Schedule button — suggest dates, show Confirm button in beat card
|
|
|
1920 |
$(document).on('click', '.btn-schedule', function (e) {
|
|
|
1921 |
e.stopPropagation();
|
|
|
1922 |
var pg = $(this).data('pg');
|
|
|
1923 |
var days = parseInt($(this).data('days'));
|
|
|
1924 |
state.calSelectedBeat = pg;
|
|
|
1925 |
state.calPickedDates = [];
|
|
|
1926 |
|
|
|
1927 |
$.ajax({
|
|
|
1928 |
url: context + "/beatPlan/availableSlots", type: "GET", dataType: "json",
|
|
|
1929 |
data: {authUserId: state.authUserId, month: state.calMonth, daysNeeded: days},
|
|
|
1930 |
success: function (r) {
|
|
|
1931 |
var data = r.response || r;
|
|
|
1932 |
state.calPickedDates = data.suggestedDates || [];
|
|
|
1933 |
renderCalGrid(state.calData);
|
|
|
1934 |
renderBeatCards(state.calData.scheduledBeats);
|
|
|
1935 |
if (state.calPickedDates.length < days) {
|
|
|
1936 |
alert('Only ' + state.calPickedDates.length + ' slots available this month. Need ' + days + '.');
|
|
|
1937 |
}
|
|
|
1938 |
}
|
|
|
1939 |
});
|
|
|
1940 |
});
|
|
|
1941 |
|
| 36632 |
ranu |
1942 |
// Click calendar cell during Schedule flow — auto-fill consecutive days from clicked date
|
|
|
1943 |
$(document).on('click', '#cal-grid-content td', function () {
|
| 36621 |
ranu |
1944 |
if (!state.calSelectedBeat) return;
|
|
|
1945 |
var ds = $(this).data('date');
|
| 36632 |
ranu |
1946 |
if (!ds || isDateBlocked(ds)) return;
|
|
|
1947 |
|
|
|
1948 |
// Find the beat to get days needed
|
|
|
1949 |
var beat = (state.calData.scheduledBeats || []).find(function (b) {
|
| 36644 |
ranu |
1950 |
return String(b.planGroupId) === String(state.calSelectedBeat);
|
| 36632 |
ranu |
1951 |
});
|
|
|
1952 |
var daysNeeded = beat ? beat.days.length : 1;
|
|
|
1953 |
|
|
|
1954 |
// Auto-fill consecutive slots from clicked date
|
|
|
1955 |
var slots = findConsecutiveSlots(ds, daysNeeded);
|
|
|
1956 |
if (slots) {
|
|
|
1957 |
state.calPickedDates = slots;
|
|
|
1958 |
renderCalGrid(state.calData);
|
|
|
1959 |
renderBeatCards(state.calData.scheduledBeats);
|
|
|
1960 |
}
|
| 36621 |
ranu |
1961 |
});
|
|
|
1962 |
|
|
|
1963 |
// Cancel scheduling mode
|
|
|
1964 |
$(document).on('click', '.btn-cancel-schedule', function (e) {
|
|
|
1965 |
e.stopPropagation();
|
|
|
1966 |
state.calSelectedBeat = null;
|
|
|
1967 |
state.calPickedDates = [];
|
|
|
1968 |
renderCalGrid(state.calData);
|
|
|
1969 |
renderBeatCards(state.calData.scheduledBeats);
|
|
|
1970 |
});
|
|
|
1971 |
|
|
|
1972 |
// Confirm schedule — save to server
|
|
|
1973 |
$(document).on('click', '.btn-confirm-schedule', function (e) {
|
|
|
1974 |
e.stopPropagation();
|
|
|
1975 |
var pg = $(this).data('pg');
|
|
|
1976 |
var daysNeeded = parseInt($(this).data('days'));
|
|
|
1977 |
|
|
|
1978 |
if (state.calPickedDates.length < daysNeeded) {
|
|
|
1979 |
alert('Need ' + daysNeeded + ' dates but only ' + state.calPickedDates.length + ' selected.');
|
|
|
1980 |
return;
|
|
|
1981 |
}
|
|
|
1982 |
|
|
|
1983 |
var beat = (state.calData.scheduledBeats || []).find(function (b) {
|
| 36644 |
ranu |
1984 |
return String(b.planGroupId) === String(pg);
|
| 36621 |
ranu |
1985 |
});
|
|
|
1986 |
if (!beat) return;
|
|
|
1987 |
|
|
|
1988 |
$.ajax({
|
|
|
1989 |
url: context + "/beatPlan/scheduleOnCalendar", type: "POST",
|
|
|
1990 |
data: {
|
|
|
1991 |
planGroupId: pg,
|
|
|
1992 |
dates: JSON.stringify(state.calPickedDates.slice(0, daysNeeded)),
|
|
|
1993 |
beatName: beat.beatName,
|
|
|
1994 |
beatColor: beat.beatColor
|
|
|
1995 |
},
|
|
|
1996 |
success: function () {
|
|
|
1997 |
state.calSelectedBeat = null;
|
|
|
1998 |
state.calPickedDates = [];
|
|
|
1999 |
loadCalendarData();
|
|
|
2000 |
},
|
|
|
2001 |
error: function (xhr) {
|
|
|
2002 |
alert('Error: ' + (xhr.responseText || xhr.statusText));
|
|
|
2003 |
}
|
|
|
2004 |
});
|
|
|
2005 |
});
|
|
|
2006 |
|
| 36632 |
ranu |
2007 |
// ============ DRAG & DROP BEAT TO CALENDAR ============
|
|
|
2008 |
$(document).on('dragstart', '.beat-card', function (e) {
|
|
|
2009 |
var pg = $(this).data('plangroup');
|
|
|
2010 |
var beatName = $(this).data('beatname');
|
|
|
2011 |
e.originalEvent.dataTransfer.setData('text/plain', pg);
|
|
|
2012 |
e.originalEvent.dataTransfer.setData('beatname', beatName || '');
|
|
|
2013 |
$(this).css('opacity', '0.5');
|
|
|
2014 |
});
|
| 36621 |
ranu |
2015 |
|
| 36632 |
ranu |
2016 |
$(document).on('dragend', '.beat-card', function () {
|
|
|
2017 |
$(this).css('opacity', '1');
|
|
|
2018 |
});
|
| 36621 |
ranu |
2019 |
|
| 36632 |
ranu |
2020 |
// Calendar cells accept drops
|
|
|
2021 |
$(document).on('dragover', '#cal-grid-content td', function (e) {
|
|
|
2022 |
var ds = $(this).data('date');
|
|
|
2023 |
if (!ds) return;
|
|
|
2024 |
if (isDateBlocked(ds)) return; // don't allow drop indicator on blocked dates
|
|
|
2025 |
e.preventDefault();
|
|
|
2026 |
$(this).css('background', '#1e3a5f');
|
|
|
2027 |
});
|
|
|
2028 |
|
|
|
2029 |
$(document).on('dragleave', '#cal-grid-content td', function () {
|
|
|
2030 |
$(this).css('background', '');
|
|
|
2031 |
});
|
|
|
2032 |
|
|
|
2033 |
$(document).on('drop', '#cal-grid-content td', function (e) {
|
|
|
2034 |
e.preventDefault();
|
|
|
2035 |
$(this).css('background', '');
|
|
|
2036 |
|
|
|
2037 |
var pg = e.originalEvent.dataTransfer.getData('text/plain');
|
|
|
2038 |
var dropDate = $(this).data('date');
|
|
|
2039 |
if (!pg || !dropDate) return;
|
|
|
2040 |
|
|
|
2041 |
if (isDateBlocked(dropDate)) {
|
|
|
2042 |
alert('Cannot schedule on this date (past, Sunday, or holiday)');
|
|
|
2043 |
return;
|
|
|
2044 |
}
|
|
|
2045 |
|
|
|
2046 |
var beat = (state.calData.scheduledBeats || []).find(function (b) {
|
| 36644 |
ranu |
2047 |
return String(b.planGroupId) === String(pg);
|
| 36632 |
ranu |
2048 |
});
|
|
|
2049 |
if (!beat) return;
|
|
|
2050 |
var beatName = beat.beatName || 'Beat';
|
|
|
2051 |
var daysNeeded = beat.days ? beat.days.length : 1;
|
|
|
2052 |
|
|
|
2053 |
// Auto-fill consecutive available days starting from drop date
|
|
|
2054 |
var scheduleDates = findConsecutiveSlots(dropDate, daysNeeded);
|
|
|
2055 |
|
|
|
2056 |
if (!scheduleDates) return; // alerts already shown inside findConsecutiveSlots
|
|
|
2057 |
|
|
|
2058 |
var dateStr = scheduleDates.map(function (d, i) {
|
|
|
2059 |
return 'Day ' + (i + 1) + ': ' + d;
|
|
|
2060 |
}).join('\n');
|
|
|
2061 |
if (!confirm('Schedule "' + beatName + '" (' + daysNeeded + ' days):\n\n' + dateStr + '\n\nConfirm?')) return;
|
|
|
2062 |
|
|
|
2063 |
$.ajax({
|
|
|
2064 |
url: context + "/beatPlan/repeatBeat", type: "POST",
|
|
|
2065 |
data: {
|
|
|
2066 |
sourcePlanGroupId: pg,
|
|
|
2067 |
authUserId: state.authUserId,
|
|
|
2068 |
dates: JSON.stringify(scheduleDates)
|
|
|
2069 |
},
|
|
|
2070 |
success: function () {
|
|
|
2071 |
loadCalendarData();
|
|
|
2072 |
},
|
|
|
2073 |
error: function (xhr) {
|
|
|
2074 |
alert('Error: ' + (xhr.responseText || xhr.statusText));
|
|
|
2075 |
}
|
|
|
2076 |
});
|
|
|
2077 |
});
|
|
|
2078 |
|
|
|
2079 |
// Find N consecutive available days starting from startDate, skipping Sundays/holidays
|
|
|
2080 |
// Returns array of date strings, or null if can't fit
|
|
|
2081 |
function findConsecutiveSlots(startDateStr, daysNeeded) {
|
|
|
2082 |
var startDate = new Date(startDateStr);
|
|
|
2083 |
var startMonth = startDate.getMonth();
|
|
|
2084 |
var dates = [];
|
|
|
2085 |
var d = new Date(startDate);
|
|
|
2086 |
var sundayAsked = false;
|
|
|
2087 |
var includeSundays = false;
|
|
|
2088 |
|
|
|
2089 |
while (dates.length < daysNeeded) {
|
|
|
2090 |
var ds = fmtDate(d);
|
|
|
2091 |
|
|
|
2092 |
// Rule 3: must stay within same month
|
|
|
2093 |
if (d.getMonth() !== startMonth) {
|
|
|
2094 |
alert('Beat has ' + daysNeeded + ' days but only ' + dates.length + ' available days left in this month starting from ' + startDateStr + '. Try an earlier date.');
|
|
|
2095 |
return null;
|
| 36621 |
ranu |
2096 |
}
|
| 36632 |
ranu |
2097 |
|
|
|
2098 |
// Past dates — skip
|
|
|
2099 |
var today = fmtDate(new Date());
|
|
|
2100 |
if (ds <= today) {
|
|
|
2101 |
d.setDate(d.getDate() + 1);
|
|
|
2102 |
continue;
|
|
|
2103 |
}
|
|
|
2104 |
|
|
|
2105 |
// Holidays — always skip
|
|
|
2106 |
if (isHoliday(ds)) {
|
|
|
2107 |
d.setDate(d.getDate() + 1);
|
|
|
2108 |
continue;
|
|
|
2109 |
}
|
|
|
2110 |
|
|
|
2111 |
// Sunday — ask once whether to include
|
|
|
2112 |
if (isSunday(ds)) {
|
|
|
2113 |
if (!sundayAsked) {
|
|
|
2114 |
sundayAsked = true;
|
|
|
2115 |
includeSundays = confirm('This beat falls on a Sunday (' + ds + '). Include Sundays in the schedule?');
|
|
|
2116 |
}
|
|
|
2117 |
if (!includeSundays) {
|
|
|
2118 |
d.setDate(d.getDate() + 1);
|
|
|
2119 |
continue;
|
|
|
2120 |
}
|
|
|
2121 |
}
|
|
|
2122 |
|
|
|
2123 |
// Already occupied — block
|
|
|
2124 |
if (getBeatsOnDate(ds).length > 0) {
|
|
|
2125 |
alert('Cannot schedule: ' + ds + ' already has a beat scheduled. Consecutive days not available from ' + startDateStr + '.');
|
|
|
2126 |
return null;
|
|
|
2127 |
}
|
|
|
2128 |
|
|
|
2129 |
dates.push(ds);
|
|
|
2130 |
d.setDate(d.getDate() + 1);
|
|
|
2131 |
}
|
|
|
2132 |
|
|
|
2133 |
return dates;
|
|
|
2134 |
}
|
|
|
2135 |
|
|
|
2136 |
function isDateBlocked(ds) {
|
|
|
2137 |
var today = fmtDate(new Date());
|
|
|
2138 |
if (ds <= today) return true; // past or today
|
|
|
2139 |
// Sundays NOT blocked — handled via confirm dialog instead
|
|
|
2140 |
// Only block holidays
|
|
|
2141 |
if (state.calData && state.calData.blockedDates) {
|
|
|
2142 |
var blocked = state.calData.blockedDates;
|
|
|
2143 |
// blockedDates from server includes Sundays+holidays, but we only block holidays now
|
|
|
2144 |
var d = new Date(ds);
|
|
|
2145 |
if (d.getDay() !== 0) { // not Sunday — check holidays
|
|
|
2146 |
if (Array.isArray(blocked) && blocked.indexOf(ds) > -1) return true;
|
|
|
2147 |
if (typeof blocked === 'object' && blocked[ds]) return true;
|
|
|
2148 |
}
|
|
|
2149 |
}
|
|
|
2150 |
return false;
|
|
|
2151 |
}
|
|
|
2152 |
|
|
|
2153 |
function isSunday(ds) {
|
|
|
2154 |
return new Date(ds).getDay() === 0;
|
|
|
2155 |
}
|
|
|
2156 |
|
|
|
2157 |
function isHoliday(ds) {
|
|
|
2158 |
if (!state.calData || !state.calData.holidays) return false;
|
|
|
2159 |
return state.calData.holidays.some(function (h) {
|
|
|
2160 |
return h.date === ds;
|
|
|
2161 |
});
|
|
|
2162 |
}
|
|
|
2163 |
|
|
|
2164 |
function getBeatsOnDate(ds) {
|
|
|
2165 |
var result = [];
|
|
|
2166 |
if (!state.calData || !state.calData.scheduledBeats) return result;
|
|
|
2167 |
state.calData.scheduledBeats.forEach(function (b) {
|
|
|
2168 |
(b.days || []).forEach(function (d) {
|
|
|
2169 |
if (d.planDate === ds) result.push({planGroupId: b.planGroupId, beatName: b.beatName});
|
|
|
2170 |
});
|
|
|
2171 |
});
|
|
|
2172 |
return result;
|
|
|
2173 |
}
|
|
|
2174 |
|
|
|
2175 |
// ============ REMOVE BEAT FROM CALENDAR DATE ============
|
|
|
2176 |
$(document).on('click', '.cal-chip-remove', function (e) {
|
|
|
2177 |
e.stopPropagation();
|
|
|
2178 |
var date = $(this).data('date');
|
|
|
2179 |
var planGroupId = $(this).data('plangroup');
|
|
|
2180 |
if (!planGroupId || !date) return;
|
|
|
2181 |
|
| 36670 |
ranu |
2182 |
if (!confirm('Unschedule this beat from ' + date + '?\n\n(The beat stays in the list — only this date is cleared.)')) return;
|
| 36632 |
ranu |
2183 |
|
| 36670 |
ranu |
2184 |
// Unschedule ONLY this date — the beat itself is preserved
|
| 36632 |
ranu |
2185 |
$.ajax({
|
| 36670 |
ranu |
2186 |
url: context + "/beatPlan/unscheduleDate", type: "POST",
|
|
|
2187 |
data: {planGroupId: String(planGroupId), date: date},
|
| 36632 |
ranu |
2188 |
success: function () {
|
|
|
2189 |
loadCalendarData();
|
|
|
2190 |
},
|
|
|
2191 |
error: function (xhr) {
|
|
|
2192 |
alert('Error: ' + (xhr.responseText || xhr.statusText));
|
|
|
2193 |
}
|
| 36621 |
ranu |
2194 |
});
|
|
|
2195 |
});
|
|
|
2196 |
|
| 36670 |
ranu |
2197 |
// ============ CALENDAR SHUFFLE (drag chip to another date) ============
|
|
|
2198 |
$(document).on('dragstart', '.cal-chip-view', function (e) {
|
|
|
2199 |
var pg = $(this).data('plangroup');
|
|
|
2200 |
var date = $(this).data('date');
|
|
|
2201 |
if (!pg || !date) {
|
|
|
2202 |
e.preventDefault();
|
|
|
2203 |
return;
|
|
|
2204 |
}
|
|
|
2205 |
var payload = JSON.stringify({pg: String(pg), fromDate: String(date)});
|
|
|
2206 |
var dt = e.originalEvent.dataTransfer;
|
|
|
2207 |
dt.effectAllowed = 'move';
|
|
|
2208 |
dt.setData('text/plain', payload);
|
|
|
2209 |
$(this).css('opacity', '0.4');
|
|
|
2210 |
});
|
|
|
2211 |
|
|
|
2212 |
$(document).on('dragend', '.cal-chip-view', function () {
|
|
|
2213 |
$(this).css('opacity', '');
|
|
|
2214 |
});
|
|
|
2215 |
|
|
|
2216 |
$(document).on('dragover', '.cal-grid td', function (e) {
|
|
|
2217 |
var $td = $(this);
|
|
|
2218 |
if ($td.hasClass('past') || $td.hasClass('blocked') || $td.hasClass('holiday') || $td.hasClass('sunday')) return;
|
|
|
2219 |
e.preventDefault();
|
|
|
2220 |
e.originalEvent.dataTransfer.dropEffect = 'move';
|
|
|
2221 |
$td.css('box-shadow', 'inset 0 0 0 2px #60a5fa');
|
|
|
2222 |
});
|
|
|
2223 |
|
|
|
2224 |
$(document).on('dragleave', '.cal-grid td', function () {
|
|
|
2225 |
$(this).css('box-shadow', '');
|
|
|
2226 |
});
|
|
|
2227 |
|
|
|
2228 |
$(document).on('drop', '.cal-grid td', function (e) {
|
|
|
2229 |
var $td = $(this);
|
|
|
2230 |
$td.css('box-shadow', '');
|
|
|
2231 |
if ($td.hasClass('past') || $td.hasClass('blocked') || $td.hasClass('holiday') || $td.hasClass('sunday')) return;
|
|
|
2232 |
e.preventDefault();
|
|
|
2233 |
|
|
|
2234 |
var raw;
|
|
|
2235 |
try {
|
|
|
2236 |
raw = e.originalEvent.dataTransfer.getData('text/plain');
|
|
|
2237 |
} catch (_) {
|
|
|
2238 |
return;
|
|
|
2239 |
}
|
|
|
2240 |
if (!raw) return;
|
|
|
2241 |
var data;
|
|
|
2242 |
try {
|
|
|
2243 |
data = JSON.parse(raw);
|
|
|
2244 |
} catch (_) {
|
|
|
2245 |
return;
|
|
|
2246 |
}
|
|
|
2247 |
if (!data.pg || !data.fromDate) return;
|
|
|
2248 |
|
|
|
2249 |
var toDate = $td.data('date');
|
|
|
2250 |
if (!toDate || toDate === data.fromDate) return;
|
|
|
2251 |
|
|
|
2252 |
$.ajax({
|
|
|
2253 |
url: context + "/beatPlan/moveScheduleDate", type: "POST",
|
|
|
2254 |
data: {planGroupId: data.pg, fromDate: data.fromDate, toDate: String(toDate)},
|
|
|
2255 |
success: function () {
|
|
|
2256 |
loadCalendarData();
|
|
|
2257 |
},
|
|
|
2258 |
error: function (xhr) {
|
|
|
2259 |
var msg = 'Move failed';
|
|
|
2260 |
try {
|
|
|
2261 |
var err = JSON.parse(xhr.responseText);
|
|
|
2262 |
if (err && err.response) {
|
|
|
2263 |
if (typeof err.response === 'string') msg = err.response;
|
|
|
2264 |
else if (err.response.message) msg = err.response.message;
|
|
|
2265 |
}
|
|
|
2266 |
} catch (_) {
|
|
|
2267 |
}
|
|
|
2268 |
alert(msg);
|
|
|
2269 |
}
|
|
|
2270 |
});
|
|
|
2271 |
});
|
|
|
2272 |
|
| 36621 |
ranu |
2273 |
// ============ DELETE BEAT ============
|
|
|
2274 |
function deleteBeat(planGroupId) {
|
|
|
2275 |
if (!confirm('Delete this beat? This cannot be undone.')) return;
|
|
|
2276 |
$.ajax({
|
|
|
2277 |
url: context + "/beatPlan/delete", type: "POST",
|
|
|
2278 |
data: {planGroupId: planGroupId},
|
|
|
2279 |
success: function () {
|
|
|
2280 |
loadCalendarData();
|
|
|
2281 |
},
|
|
|
2282 |
error: function (xhr) {
|
|
|
2283 |
alert('Error: ' + (xhr.responseText || xhr.statusText));
|
|
|
2284 |
}
|
|
|
2285 |
});
|
|
|
2286 |
}
|
|
|
2287 |
|
|
|
2288 |
// ============ HELPERS ============
|
|
|
2289 |
function curDay() {
|
|
|
2290 |
return state.days[state.currentDay - 1];
|
|
|
2291 |
}
|
|
|
2292 |
|
|
|
2293 |
function haversine(lat1, lng1, lat2, lng2) {
|
|
|
2294 |
var R = 6371, dLat = (lat2 - lat1) * Math.PI / 180, dLng = (lng2 - lng1) * Math.PI / 180;
|
|
|
2295 |
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);
|
|
|
2296 |
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
2297 |
}
|
|
|
2298 |
|
|
|
2299 |
function fmtMins(m) {
|
|
|
2300 |
return Math.floor(m / 60) + 'h ' + Math.round(m % 60) + 'm';
|
|
|
2301 |
}
|
|
|
2302 |
|
|
|
2303 |
function fmtDate(d) {
|
|
|
2304 |
return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);
|
|
|
2305 |
}
|
|
|
2306 |
|
|
|
2307 |
function fitBounds() {
|
|
|
2308 |
if (Object.keys(markers).length > 0) {
|
|
|
2309 |
var b = new google.maps.LatLngBounds();
|
|
|
2310 |
if (homeMarker) b.extend(homeMarker.getPosition());
|
|
|
2311 |
for (var k in markers) b.extend(markers[k].getPosition());
|
|
|
2312 |
map.fitBounds(b);
|
|
|
2313 |
}
|
|
|
2314 |
}
|
| 36650 |
ranu |
2315 |
|
|
|
2316 |
// ============ AUTO-LOAD FROM URL PARAMS ============
|
|
|
2317 |
// When opened with ?autoUserId=X[&autoUserName=Y], skip the user-picker
|
|
|
2318 |
// dropdowns and load that user's beats directly (and jump to Calendar tab).
|
|
|
2319 |
$(function () {
|
|
|
2320 |
// Use a manual parser so we don't depend on URLSearchParams
|
|
|
2321 |
function getParam(name) {
|
|
|
2322 |
var q = window.location.search.substring(1);
|
|
|
2323 |
var pairs = q.split('&');
|
|
|
2324 |
for (var i = 0; i < pairs.length; i++) {
|
|
|
2325 |
var kv = pairs[i].split('=');
|
|
|
2326 |
if (decodeURIComponent(kv[0]) === name) {
|
|
|
2327 |
return kv.length > 1 ? decodeURIComponent(kv[1].replace(/\+/g, ' ')) : '';
|
|
|
2328 |
}
|
|
|
2329 |
}
|
|
|
2330 |
return null;
|
|
|
2331 |
}
|
|
|
2332 |
|
|
|
2333 |
var autoUid = getParam('autoUserId');
|
|
|
2334 |
console.log('[BEAT-AUTO] autoUserId =', autoUid);
|
|
|
2335 |
if (!autoUid) return;
|
|
|
2336 |
|
|
|
2337 |
var autoName = getParam('autoUserName');
|
| 36651 |
ranu |
2338 |
var editBeatId = getParam('editBeatId');
|
|
|
2339 |
var editDate = getParam('editDate');
|
|
|
2340 |
|
| 36650 |
ranu |
2341 |
state.authUserId = parseInt(autoUid);
|
|
|
2342 |
state.categoryId = parseInt($('#bp-category').val()) || 4;
|
|
|
2343 |
state.mode = 'list';
|
|
|
2344 |
$('#bp-user-label').text(autoName || ('User #' + autoUid));
|
|
|
2345 |
|
|
|
2346 |
$.ajax({
|
|
|
2347 |
url: context + '/beatPlan/getBaseLocation', type: 'GET', dataType: 'json',
|
|
|
2348 |
data: {authUserId: autoUid},
|
|
|
2349 |
success: function (r) {
|
|
|
2350 |
console.log('[BEAT-AUTO] getBaseLocation ok');
|
|
|
2351 |
var data = r.response || r;
|
|
|
2352 |
if (data.locationName) {
|
|
|
2353 |
state.homeLat = parseFloat(data.latitude);
|
|
|
2354 |
state.homeLng = parseFloat(data.longitude);
|
|
|
2355 |
state.homeName = data.locationName;
|
|
|
2356 |
}
|
| 36651 |
ranu |
2357 |
if (editBeatId) {
|
|
|
2358 |
console.log('[BEAT-AUTO] entering edit for beat', editBeatId, 'date', editDate);
|
|
|
2359 |
editBeat(String(editBeatId), editDate || null);
|
|
|
2360 |
} else {
|
|
|
2361 |
showBeatList();
|
|
|
2362 |
setTimeout(function () {
|
|
|
2363 |
$('.panel-tab[data-tab="calendar"]').click();
|
|
|
2364 |
}, 100);
|
|
|
2365 |
}
|
| 36650 |
ranu |
2366 |
},
|
|
|
2367 |
error: function (xhr) {
|
|
|
2368 |
console.error('[BEAT-AUTO] getBaseLocation failed:', xhr.status, xhr.responseText);
|
|
|
2369 |
}
|
|
|
2370 |
});
|
|
|
2371 |
});
|