Subversion Repositories SmartDukaan

Rev

Rev 36642 | Rev 36650 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 36642 Rev 36644
Line 22... Line 22...
22
 
22
 
23
var VISIT_MINS = 30;
23
var VISIT_MINS = 30;
24
var AVG_SPEED = 30;
24
var AVG_SPEED = 30;
25
var DAY_LIMIT = 540;
25
var DAY_LIMIT = 540;
26
var ROAD_FACTOR = 1.3;
26
var ROAD_FACTOR = 1.3;
-
 
27
var isSubmittingBeat = false; // guard against double-submit
27
var COLORS = ['#3b82f6', '#ef4444', '#22c55e', '#a855f7', '#f59e0b', '#06b6d4', '#ec4899', '#334155', '#14b8a6', '#dc2626'];
28
var COLORS = ['#3b82f6', '#ef4444', '#22c55e', '#a855f7', '#f59e0b', '#06b6d4', '#ec4899', '#334155', '#14b8a6', '#dc2626'];
28
 
29
 
29
// ============ TOP BAR ============
30
// ============ TOP BAR ============
30
$('#bp-level').on('change', function () {
31
$('#bp-level').on('change', function () {
31
	var level = $(this).val();
32
	var level = $(this).val();
Line 136... Line 137...
136
$(document).on('click', '.beat-list-item', function () {
137
$(document).on('click', '.beat-list-item', function () {
137
    var pg = $(this).data('plangroup');
138
    var pg = $(this).data('plangroup');
138
    viewBeat(pg);
139
    viewBeat(pg);
139
});
140
});
140
 
141
 
141
function viewBeat(planGroupId) {
142
function viewBeat(planGroupId, planDate) {
142
    state.mode = 'view';
143
    state.mode = 'view';
-
 
144
	state.viewDate = planDate || null; // when set, show that run's leads
143
    $('#bottom-bar').hide();
145
    $('#bottom-bar').hide();
144
 
146
 
145
    // Load partners first (for coordinates), then load beat visits
147
    // Load partners first (for coordinates), then load beat visits
146
    $.ajax({
148
    $.ajax({
147
        url: context + "/beatPlan/getPartners", type: "GET", dataType: "json",
149
        url: context + "/beatPlan/getPartners", type: "GET", dataType: "json",
Line 166... Line 168...
166
                url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
168
                url: context + "/beatPlan/calendar", type: "GET", dataType: "json",
167
                data: {authUserId: state.authUserId, month: '2020-01'},
169
                data: {authUserId: state.authUserId, month: '2020-01'},
168
                success: function (r2) {
170
                success: function (r2) {
169
                    var calData = r2.response || r2;
171
                    var calData = r2.response || r2;
170
                    var beat = (calData.scheduledBeats || []).find(function (b) {
172
                    var beat = (calData.scheduledBeats || []).find(function (b) {
171
                        return b.planGroupId === planGroupId;
173
						return String(b.planGroupId) === String(planGroupId);
172
                    });
174
                    });
173
                    if (!beat) {
175
                    if (!beat) {
174
                        alert('Beat not found');
176
                        alert('Beat not found');
175
                        return;
177
                        return;
176
                    }
178
                    }
177
 
179
 
178
                    // Reconstruct state.days from beat data for map drawing
180
                    // Reconstruct state.days from beat data for map drawing
179
                    // We need actual visit fofoIds — fetch from beat_plan records
181
                    // We need actual visit fofoIds — fetch from beat_plan records
180
                    $.ajax({
182
                    $.ajax({
181
                        url: context + "/beatPlan/getBeatVisits", type: "GET", dataType: "json",
183
                        url: context + "/beatPlan/getBeatVisits", type: "GET", dataType: "json",
-
 
184
						data: planDate
-
 
185
							? {planGroupId: planGroupId, planDate: planDate}
182
                        data: {planGroupId: planGroupId},
186
							: {planGroupId: planGroupId},
183
                        success: function (r3) {
187
                        success: function (r3) {
184
                            var visits = (r3.response || r3) || [];
188
                            var visits = (r3.response || r3) || [];
185
 
189
 
186
                            // Group visits by dayNumber
190
                            // Group visits by dayNumber
187
                            var dayVisitsMap = {};
191
                            var dayVisitsMap = {};
188
                            visits.forEach(function (v) {
192
                            visits.forEach(function (v) {
189
                                if (!dayVisitsMap[v.dayNumber]) dayVisitsMap[v.dayNumber] = [];
193
                                if (!dayVisitsMap[v.dayNumber]) dayVisitsMap[v.dayNumber] = [];
190
                                dayVisitsMap[v.dayNumber].push(v);
194
                                dayVisitsMap[v.dayNumber].push(v);
191
                            });
195
                            });
192
 
196
 
193
                            // Build days for route display
197
							// Build days for route display.
-
 
198
							// A beat scheduled on multiple dates has multiple schedule
-
 
199
							// rows per day_number — dedupe so each day appears once.
194
                            state.days = [];
200
                            state.days = [];
-
 
201
							var seenDayNumbers = {};
195
                            beat.days.forEach(function (d) {
202
                            beat.days.forEach(function (d) {
-
 
203
								if (seenDayNumbers[d.dayNumber]) return; // skip duplicate day
-
 
204
								seenDayNumbers[d.dayNumber] = true;
-
 
205
 
196
                                var dayVisits = (dayVisitsMap[d.dayNumber] || []).sort(function (a, b) {
206
                                var dayVisits = (dayVisitsMap[d.dayNumber] || []).sort(function (a, b) {
197
                                    return a.sequenceOrder - b.sequenceOrder;
207
                                    return a.sequenceOrder - b.sequenceOrder;
198
                                });
208
                                });
199
                                var startLat = state.homeLat, startLng = state.homeLng, startName = state.homeName;
209
                                var startLat = state.homeLat, startLng = state.homeLng, startName = state.homeName;
200
 
210
 
Line 210... Line 220...
210
                                }
220
                                }
211
 
221
 
212
                                var builtVisits = [];
222
                                var builtVisits = [];
213
                                dayVisits.forEach(function (v) {
223
                                dayVisits.forEach(function (v) {
214
                                    if (v.visitType === 'lead') {
224
                                    if (v.visitType === 'lead') {
215
                                        // Lead visit — get lead geo from API (sync for simplicity)
-
 
216
                                        var leadVisit = {
225
										builtVisits.push({
217
                                            id: v.fofoId, type: 'lead',
226
                                            id: v.fofoId, type: 'lead',
218
                                            name: 'LEAD #' + v.fofoId,
227
                                            name: 'LEAD #' + v.fofoId,
219
                                            code: 'LEAD',
228
                                            code: 'LEAD',
220
                                            lat: null, lng: null,
229
                                            lat: null, lng: null,
221
                                            isLead: true
230
                                            isLead: true
222
                                        };
-
 
223
                                        $.ajax({
-
 
224
                                            url: context + '/lead-geo/check/' + v.fofoId,
-
 
225
                                            method: 'GET',
-
 
226
                                            async: false,
-
 
227
                                            success: function (resp) {
-
 
228
                                                var geoData = resp.response || resp;
-
 
229
                                                if (geoData && geoData.hasApprovedGeo) {
-
 
230
                                                    leadVisit.lat = geoData.latitude;
-
 
231
                                                    leadVisit.lng = geoData.longitude;
-
 
232
                                                }
-
 
233
                                            }
-
 
234
                                        });
-
 
235
                                        // Also get lead name
-
 
236
                                        $.ajax({
-
 
237
                                            url: context + '/getLead?leadId=' + v.fofoId,
-
 
238
                                            method: 'GET',
-
 
239
                                            async: false,
-
 
240
                                            success: function (resp) {
-
 
241
                                                var lead = resp.response || resp;
-
 
242
                                                if (lead && lead.firstName) {
-
 
243
                                                    leadVisit.name = 'LEAD - ' + lead.firstName + ' ' + (lead.lastName || '');
-
 
244
                                                }
-
 
245
                                            }
-
 
246
                                        });
231
                                        });
247
                                        builtVisits.push(leadVisit);
-
 
248
                                    } else {
232
                                    } else {
249
                                        var p = partnerMap[v.fofoId];
233
                                        var p = partnerMap[v.fofoId];
250
                                        if (p) {
234
                                        if (p) {
251
                                            builtVisits.push({
235
                                            builtVisits.push({
252
                                                id: p.fofoId, type: 'partner',
236
                                                id: p.fofoId, type: 'partner',
Line 271... Line 255...
271
                                });
255
                                });
272
                            });
256
                            });
273
 
257
 
274
                            state.currentDay = 1;
258
                            state.currentDay = 1;
275
 
259
 
276
                            // Render left panel
260
							// Collect all lead data promises across all days
-
 
261
							var allLeadPromises = [];
277
                            var html = '<div style="margin-bottom:8px;"><button class="btn-back-to-list" style="font-size:11px;padding:4px 10px;border:1px solid #475569;border-radius:4px;background:none;color:#94a3b8;cursor:pointer;font-family:inherit;">← Back to list</button></div>';
262
							state.days.forEach(function (d) {
-
 
263
								d.visits.forEach(function (v) {
-
 
264
									if (v.isLead) {
-
 
265
										allLeadPromises.push(
278
                            html += '<div style="font-size:14px;font-weight:600;color:#60a5fa;margin-bottom:10px;">' + beat.beatName + '</div>';
266
											$.get(context + '/lead-geo/check/' + v.id).then(function (resp) {
279
                            $('#route-list').html(html);
267
												var geoData = resp.response || resp;
-
 
268
												if (geoData && geoData.hasApprovedGeo) {
-
 
269
													v.lat = geoData.latitude;
-
 
270
													v.lng = geoData.longitude;
-
 
271
												}
-
 
272
											})
-
 
273
										);
-
 
274
										allLeadPromises.push(
-
 
275
											$.get(context + '/getLead?leadId=' + v.id).then(function (resp) {
-
 
276
												var lead = resp.response || resp;
-
 
277
												if (lead && lead.firstName) {
-
 
278
													v.name = 'LEAD - ' + lead.firstName + ' ' + (lead.lastName || '');
-
 
279
												}
-
 
280
											})
-
 
281
										);
-
 
282
									}
-
 
283
								});
-
 
284
							});
280
 
285
 
-
 
286
							// Wait for all lead data, then render
281
                            // Use renderRoutePanel for consistent display (but without remove/daybreak buttons)
287
							$.when.apply($, allLeadPromises).always(function () {
-
 
288
								// Render left panel
282
                            state.mode = 'view';
289
								state.mode = 'view';
283
                            renderRoutePanel();
290
								renderRoutePanel();
-
 
291
								var dateLabel = state.viewDate
-
 
292
									? '<div style="font-size:11px;color:#f59e0b;margin-bottom:8px;">Run date: ' + state.viewDate + ' (showing this day\'s leads)</div>'
284
                            // Prepend the back button + title before route days
293
									: '<div style="font-size:11px;color:#94a3b8;margin-bottom:8px;">Beat template (partners only)</div>';
285
                            $('#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:10px;">' + beat.beatName + '</div>');
294
								$('#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);
286
 
295
 
287
                            // Init map with all partners + highlight beat route
296
								// Init map with all partners + lead markers
288
                            initMap();
297
								initMap();
289
                            // Add lead markers to map
-
 
290
                            state.days.forEach(function (d) {
298
								state.days.forEach(function (d) {
291
                                d.visits.forEach(function (v, i) {
299
									d.visits.forEach(function (v) {
292
                                    if (v.isLead && v.lat && v.lng) {
300
										if (v.isLead && v.lat && v.lng) {
293
                                        var m = new google.maps.Marker({
301
											var m = new google.maps.Marker({
294
                                            map: map,
302
												map: map,
295
                                            position: {lat: v.lat, lng: v.lng},
303
												position: {lat: v.lat, lng: v.lng},
296
                                            title: v.name,
304
												title: v.name,
297
                                            label: {text: 'L', color: '#fff', fontSize: '11px', fontWeight: '700'},
305
												label: {text: 'L', color: '#fff', fontSize: '11px', fontWeight: '700'},
298
                                            icon: {
306
												icon: {
299
                                                path: google.maps.SymbolPath.CIRCLE,
307
													path: google.maps.SymbolPath.CIRCLE,
300
                                                scale: 16,
308
													scale: 16,
301
                                                fillColor: '#e67e22',
309
													fillColor: '#e67e22',
302
                                                fillOpacity: 0.95,
310
													fillOpacity: 0.95,
303
                                                strokeColor: '#fff',
311
													strokeColor: '#fff',
304
                                                strokeWeight: 2
312
													strokeWeight: 2
305
                                            }
313
												}
306
                                        });
314
											});
307
                                        markers[v.id] = m;
315
											markers[v.id] = m;
308
                                    }
316
										}
309
                                });
317
									});
310
                            });
318
								});
311
                            resetMarkerColors();
319
								resetMarkerColors();
312
                            drawRoute();
320
								drawRoute();
-
 
321
							});
313
                        }
322
                        }
314
                    });
323
                    });
315
                }
324
                }
316
            });
325
            });
317
        }
326
        }
Line 336... Line 345...
336
    var beatTitle = prompt('Enter beat name:', '');
345
    var beatTitle = prompt('Enter beat name:', '');
337
    if (!beatTitle || !beatTitle.trim()) return;
346
    if (!beatTitle || !beatTitle.trim()) return;
338
 
347
 
339
    state.beatTitle = beatTitle.trim();
348
    state.beatTitle = beatTitle.trim();
340
    state.mode = 'create';
349
    state.mode = 'create';
-
 
350
	state.savedPlanGroupId = null; // reset — this is a fresh beat
-
 
351
	state.days = [];               // clear any leftover route data
-
 
352
	isSubmittingBeat = false;
341
 
353
 
342
    if (!state.homeLat) {
354
    if (!state.homeLat) {
343
        showHomeModal();
355
        showHomeModal();
344
    } else {
356
    } else {
345
        loadPartners();
357
        loadPartners();
Line 974... Line 986...
974
	renderRoutePanel();
986
	renderRoutePanel();
975
    updateBottomBar();
987
    updateBottomBar();
976
});
988
});
977
 
989
 
978
$('#btn-finish').on('click', function () {
990
$('#btn-finish').on('click', function () {
-
 
991
	// Guard 1: already submitting — ignore duplicate clicks
-
 
992
	if (isSubmittingBeat) {
-
 
993
		return;
-
 
994
	}
-
 
995
	// Guard 2: this beat was already saved — don't re-submit
-
 
996
	if (state.savedPlanGroupId) {
-
 
997
		alert('This beat is already saved.');
-
 
998
		showBeatList();
-
 
999
		return;
-
 
1000
	}
979
	if (state.days.every(function (d) {
1001
	if (state.days.every(function (d) {
980
		return d.visits.length === 0;
1002
		return d.visits.length === 0;
981
	})) {
1003
	})) {
982
		alert('No visits added');
1004
		alert('No visits added');
983
		return;
1005
		return;
Line 1010... Line 1032...
1010
	var dates = daysData.map(function () {
1032
	var dates = daysData.map(function () {
1011
		return null;
1033
		return null;
1012
	});
1034
	});
1013
	var planData = JSON.stringify({days: daysData, dates: dates, beatName: beatTitle.trim()});
1035
	var planData = JSON.stringify({days: daysData, dates: dates, beatName: beatTitle.trim()});
1014
 
1036
 
-
 
1037
	isSubmittingBeat = true;
1015
	$('#btn-finish').prop('disabled', true).text('Saving...');
1038
	$('#btn-finish').prop('disabled', true).text('Saving...');
1016
 
1039
 
1017
	$.ajax({
1040
	$.ajax({
1018
		url: context + "/beatPlan/submitPlan",
1041
		url: context + "/beatPlan/submitPlan",
1019
		type: "POST",
1042
		type: "POST",
1020
		data: {authUserId: state.authUserId, planData: planData},
1043
		data: {authUserId: state.authUserId, planData: planData},
1021
		success: function (response) {
1044
		success: function (response) {
1022
			var data = (typeof response === 'string' ? JSON.parse(response) : response);
1045
			var data = (typeof response === 'string' ? JSON.parse(response) : response);
1023
			var result = data.response || data;
1046
			var result = data.response || data;
1024
			$('#btn-finish').prop('disabled', false).text('Finish Plan');
-
 
1025
 
1047
 
1026
			if (result.status) {
1048
			if (result.status) {
-
 
1049
				// Mark as saved — finish button stays disabled, no re-submit possible
1027
				state.savedPlanGroupId = result.planGroupId;
1050
				state.savedPlanGroupId = result.planGroupId;
1028
				$('#bottom-bar').hide();
1051
				$('#bottom-bar').hide();
1029
 
1052
 
1030
                if (result.duplicate) {
1053
                if (result.duplicate) {
1031
                    alert('This beat already exists (same visits).');
1054
					alert('This beat already exists.');
1032
                } else {
1055
                } else {
1033
                    alert('Beat "' + beatTitle + '" saved successfully!');
1056
                    alert('Beat "' + beatTitle + '" saved successfully!');
1034
                }
1057
                }
1035
 
1058
 
1036
                // Go back to beat list
1059
                // Go back to beat list
1037
                showBeatList();
1060
                showBeatList();
1038
			} else {
1061
			} else {
-
 
1062
				$('#btn-finish').prop('disabled', false).text('Finish Plan');
1039
				alert('Error saving beat plan');
1063
				alert('Error saving beat plan');
1040
			}
1064
			}
-
 
1065
			isSubmittingBeat = false;
1041
		},
1066
		},
1042
		error: function (xhr) {
1067
		error: function (xhr) {
-
 
1068
			isSubmittingBeat = false;
1043
			$('#btn-finish').prop('disabled', false).text('Finish Plan');
1069
			$('#btn-finish').prop('disabled', false).text('Finish Plan');
1044
			alert('Error: ' + (xhr.responseText || xhr.statusText));
1070
			alert('Error: ' + (xhr.responseText || xhr.statusText));
1045
		}
1071
		}
1046
	});
1072
	});
1047
});
1073
});
Line 1061... Line 1087...
1061
		state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
1087
		state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
1062
		loadCalendarData();
1088
		loadCalendarData();
1063
	}
1089
	}
1064
});
1090
});
1065
 
1091
 
-
 
1092
// Click a beat chip on the calendar → view that specific day's route
-
 
1093
// (partners + only the leads scheduled for that exact date)
-
 
1094
$(document).on('click', '.cal-chip-view', function (e) {
-
 
1095
	if ($(e.target).hasClass('cal-chip-remove')) return; // handled separately
-
 
1096
	e.stopPropagation();
-
 
1097
	var pg = $(this).data('plangroup');
-
 
1098
	var date = $(this).data('date');
-
 
1099
	if (!pg || !date) return;
-
 
1100
	// Switch to Route tab and load that day's run
-
 
1101
	$('.panel-tab[data-tab="route"]').click();
-
 
1102
	viewBeat(String(pg), String(date));
-
 
1103
});
-
 
1104
 
1066
// ============ CALENDAR ============
1105
// ============ CALENDAR ============
1067
$('#cal-prev').on('click', function () {
1106
$('#cal-prev').on('click', function () {
1068
	navMonth(-1);
1107
	navMonth(-1);
1069
});
1108
});
1070
$('#cal-next').on('click', function () {
1109
$('#cal-next').on('click', function () {
Line 1169... Line 1208...
1169
				if (b.status === 'running') chipCls += ' running';
1208
				if (b.status === 'running') chipCls += ' running';
1170
				if (b.status === 'completed') chipCls += ' completed';
1209
				if (b.status === 'completed') chipCls += ' completed';
1171
                var removeBtn = (!isPast && b.status !== 'running' && b.status !== 'completed')
1210
                var removeBtn = (!isPast && b.status !== 'running' && b.status !== 'completed')
1172
                    ? '<span class="cal-chip-remove" data-date="' + ds + '" data-plangroup="' + (b.planGroupId || '') + '" title="Remove">&times;</span>'
1211
                    ? '<span class="cal-chip-remove" data-date="' + ds + '" data-plangroup="' + (b.planGroupId || '') + '" title="Remove">&times;</span>'
1173
                    : '';
1212
                    : '';
1174
                row += '<span class="' + chipCls + '" style="background:' + b.color + ';position:relative;">'
1213
				row += '<span class="' + chipCls + ' cal-chip-view" data-plangroup="' + (b.planGroupId || '') + '" data-date="' + ds + '" style="background:' + b.color + ';position:relative;cursor:pointer;">'
1175
                    + b.name + ' D' + b.day + (b.status === 'running' ? ' [live]' : '') + ' (' + b.visits + ')'
1214
                    + b.name + ' D' + b.day + (b.status === 'running' ? ' [live]' : '') + ' (' + b.visits + ')'
1176
                    + removeBtn + '</span>';
1215
                    + removeBtn + '</span>';
1177
			});
1216
			});
1178
 
1217
 
1179
			if (isPicked) {
1218
			if (isPicked) {
Line 1300... Line 1339...
1300
	var ds = $(this).data('date');
1339
	var ds = $(this).data('date');
1301
    if (!ds || isDateBlocked(ds)) return;
1340
    if (!ds || isDateBlocked(ds)) return;
1302
 
1341
 
1303
    // Find the beat to get days needed
1342
    // Find the beat to get days needed
1304
    var beat = (state.calData.scheduledBeats || []).find(function (b) {
1343
    var beat = (state.calData.scheduledBeats || []).find(function (b) {
1305
        return b.planGroupId === state.calSelectedBeat;
1344
		return String(b.planGroupId) === String(state.calSelectedBeat);
1306
    });
1345
    });
1307
    var daysNeeded = beat ? beat.days.length : 1;
1346
    var daysNeeded = beat ? beat.days.length : 1;
1308
 
1347
 
1309
    // Auto-fill consecutive slots from clicked date
1348
    // Auto-fill consecutive slots from clicked date
1310
    var slots = findConsecutiveSlots(ds, daysNeeded);
1349
    var slots = findConsecutiveSlots(ds, daysNeeded);
Line 1334... Line 1373...
1334
		alert('Need ' + daysNeeded + ' dates but only ' + state.calPickedDates.length + ' selected.');
1373
		alert('Need ' + daysNeeded + ' dates but only ' + state.calPickedDates.length + ' selected.');
1335
		return;
1374
		return;
1336
	}
1375
	}
1337
 
1376
 
1338
	var beat = (state.calData.scheduledBeats || []).find(function (b) {
1377
	var beat = (state.calData.scheduledBeats || []).find(function (b) {
1339
		return b.planGroupId === pg;
1378
		return String(b.planGroupId) === String(pg);
1340
	});
1379
	});
1341
	if (!beat) return;
1380
	if (!beat) return;
1342
 
1381
 
1343
	$.ajax({
1382
	$.ajax({
1344
		url: context + "/beatPlan/scheduleOnCalendar", type: "POST",
1383
		url: context + "/beatPlan/scheduleOnCalendar", type: "POST",
Line 1397... Line 1436...
1397
        alert('Cannot schedule on this date (past, Sunday, or holiday)');
1436
        alert('Cannot schedule on this date (past, Sunday, or holiday)');
1398
        return;
1437
        return;
1399
    }
1438
    }
1400
 
1439
 
1401
    var beat = (state.calData.scheduledBeats || []).find(function (b) {
1440
    var beat = (state.calData.scheduledBeats || []).find(function (b) {
1402
        return b.planGroupId === pg;
1441
		return String(b.planGroupId) === String(pg);
1403
    });
1442
    });
1404
    if (!beat) return;
1443
    if (!beat) return;
1405
    var beatName = beat.beatName || 'Beat';
1444
    var beatName = beat.beatName || 'Beat';
1406
    var daysNeeded = beat.days ? beat.days.length : 1;
1445
    var daysNeeded = beat.days ? beat.days.length : 1;
1407
 
1446