Subversion Repositories SmartDukaan

Rev

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

Rev 36650 Rev 36651
Line 118... Line 118...
118
                });
118
                });
119
 
119
 
120
                var statusCls = b.status === 'running' ? 'status-running' : b.status === 'completed' ? 'status-completed' : 'status-scheduled';
120
                var statusCls = b.status === 'running' ? 'status-running' : b.status === 'completed' ? 'status-completed' : 'status-scheduled';
121
                var statusLabel = b.status === 'running' ? 'Live' : b.status === 'completed' ? 'Done' : b.status === 'unscheduled' ? 'Unscheduled' : 'Scheduled';
121
                var statusLabel = b.status === 'running' ? 'Live' : b.status === 'completed' ? 'Done' : b.status === 'unscheduled' ? 'Unscheduled' : 'Scheduled';
122
 
122
 
123
                html += '<div class="beat-card beat-list-item" data-plangroup="' + b.planGroupId + '" style="cursor:pointer;">';
123
				html += '<div class="beat-card beat-list-item" data-plangroup="' + b.planGroupId + '" style="cursor:pointer; position:relative;">';
124
                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>';
124
                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>';
125
                html += '<div class="beat-meta">' + b.days.length + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km';
125
                html += '<div class="beat-meta">' + b.days.length + ' days | ' + totalV + ' visits | ' + totalKm.toFixed(0) + ' km';
126
                if (item.count > 1) html += ' | ' + item.count + 'x scheduled';
126
                if (item.count > 1) html += ' | ' + item.count + 'x scheduled';
127
                html += '</div>';
127
                html += '</div>';
-
 
128
				html += '<button class="beat-edit-btn" data-plangroup="' + b.planGroupId + '" '
-
 
129
					+ 'style="position:absolute;top:8px;right:8px;background:#475569;color:#e2e8f0;'
-
 
130
					+ 'border:none;border-radius:4px;padding:3px 8px;font-size:10px;cursor:pointer;'
-
 
131
					+ 'font-family:inherit;">Edit</button>';
128
                html += '</div>';
132
                html += '</div>';
129
            });
133
            });
130
 
134
 
131
            $('#route-list').html(html);
135
            $('#route-list').html(html);
132
        }
136
        }
Line 137... Line 141...
137
$(document).on('click', '.beat-list-item', function () {
141
$(document).on('click', '.beat-list-item', function () {
138
    var pg = $(this).data('plangroup');
142
    var pg = $(this).data('plangroup');
139
    viewBeat(pg);
143
    viewBeat(pg);
140
});
144
});
141
 
145
 
-
 
146
// Edit button on a beat card → load in EDIT mode (modifiable)
-
 
147
$(document).on('click', '.beat-edit-btn', function (e) {
-
 
148
	e.stopPropagation(); // don't also trigger beat-list-item view
-
 
149
	var pg = $(this).data('plangroup');
-
 
150
	var date = $(this).data('date') || null;
-
 
151
	editBeat(String(pg), date ? String(date) : null);
-
 
152
});
-
 
153
 
-
 
154
// Loads a beat into edit mode — same UI as create but Finish becomes Save.
-
 
155
// When planDate is provided: leads scheduled on that date are loaded too and
-
 
156
// can be removed (template = partners stays the same across runs).
-
 
157
function editBeat(planGroupId, planDate) {
-
 
158
	state.mode = 'edit';
-
 
159
	state.editingBeatId = planGroupId;
-
 
160
	state.editingDate = planDate || null;
-
 
161
	state.viewDate = null;
-
 
162
	state.savedPlanGroupId = null;
-
 
163
	isSubmittingBeat = false;
-
 
164
 
-
 
165
	$.ajax({
-
 
166
		url: context + '/beatPlan/getPartners', type: 'GET', dataType: 'json',
-
 
167
		data: {
-
 
168
			authUserId: state.authUserId, categoryId: state.categoryId,
-
 
169
			startLat: state.homeLat, startLng: state.homeLng
-
 
170
		},
-
 
171
		success: function (r) {
-
 
172
			var pData = r.response || r;
-
 
173
			state.partners = pData.partners || [];
-
 
174
			var partnerMap = {};
-
 
175
			state.partners.forEach(function (p) {
-
 
176
				partnerMap[p.fofoId] = p;
-
 
177
			});
-
 
178
 
-
 
179
			$.ajax({
-
 
180
				url: context + '/beatPlan/calendar', type: 'GET', dataType: 'json',
-
 
181
				data: {authUserId: state.authUserId, month: '2020-01'},
-
 
182
				success: function (r2) {
-
 
183
					var calData = r2.response || r2;
-
 
184
					var beat = (calData.scheduledBeats || []).find(function (b) {
-
 
185
						return String(b.planGroupId) === String(planGroupId);
-
 
186
					});
-
 
187
					if (!beat) {
-
 
188
						alert('Beat not found');
-
 
189
						return;
-
 
190
					}
-
 
191
					state.beatTitle = beat.beatName || 'Beat';
-
 
192
 
-
 
193
					// Pass planDate to getBeatVisits so leads for that date are included
-
 
194
					var visitsData = planDate
-
 
195
						? {planGroupId: planGroupId, planDate: planDate}
-
 
196
						: {planGroupId: planGroupId};
-
 
197
 
-
 
198
					$.ajax({
-
 
199
						url: context + '/beatPlan/getBeatVisits', type: 'GET', dataType: 'json',
-
 
200
						data: visitsData,
-
 
201
						success: function (r3) {
-
 
202
							var visits = (r3.response || r3) || [];
-
 
203
							var dayVisitsMap = {};
-
 
204
							visits.forEach(function (v) {
-
 
205
								if (!dayVisitsMap[v.dayNumber]) dayVisitsMap[v.dayNumber] = [];
-
 
206
								dayVisitsMap[v.dayNumber].push(v);
-
 
207
							});
-
 
208
 
-
 
209
							// Collect lead IDs so we can fetch their name/geo in parallel
-
 
210
							var leadIdsToFetch = [];
-
 
211
							visits.forEach(function (v) {
-
 
212
								if (v.visitType === 'lead') leadIdsToFetch.push(v.fofoId);
-
 
213
							});
-
 
214
 
-
 
215
							state.days = [];
-
 
216
							var seen = {};
-
 
217
							beat.days.forEach(function (d) {
-
 
218
								if (seen[d.dayNumber]) return;
-
 
219
								seen[d.dayNumber] = true;
-
 
220
								var dayVisits = (dayVisitsMap[d.dayNumber] || []).sort(function (a, b) {
-
 
221
									return a.sequenceOrder - b.sequenceOrder;
-
 
222
								});
-
 
223
								var builtVisits = [];
-
 
224
								dayVisits.forEach(function (v) {
-
 
225
									if (v.visitType === 'lead') {
-
 
226
										builtVisits.push({
-
 
227
											id: v.fofoId, type: 'lead',
-
 
228
											name: 'LEAD #' + v.fofoId, code: 'LEAD',
-
 
229
											lat: null, lng: null, isLead: true
-
 
230
										});
-
 
231
									} else {
-
 
232
										var p = partnerMap[v.fofoId];
-
 
233
										if (p) builtVisits.push({
-
 
234
											id: p.fofoId, type: 'partner',
-
 
235
											name: p.code + ' - ' + (p.outletName || p.businessName || ''),
-
 
236
											code: p.code,
-
 
237
											lat: p.latitude ? parseFloat(p.latitude) : null,
-
 
238
											lng: p.longitude ? parseFloat(p.longitude) : null
-
 
239
										});
-
 
240
									}
-
 
241
								});
-
 
242
								state.days.push({
-
 
243
									dayNumber: d.dayNumber,
-
 
244
									startLat: state.homeLat, startLng: state.homeLng,
-
 
245
									startName: state.homeName,
-
 
246
									visits: builtVisits,
-
 
247
									endAction: d.dayNumber < beat.days.length ? 'DAYBREAK' : 'HOME',
-
 
248
									totalKm: 0, totalMins: 0
-
 
249
								});
-
 
250
							});
-
 
251
							state.currentDay = 1;
-
 
252
 
-
 
253
							// Async enrich each lead with name + geo
-
 
254
							var leadPromises = [];
-
 
255
							state.days.forEach(function (day) {
-
 
256
								day.visits.forEach(function (v) {
-
 
257
									if (!v.isLead) return;
-
 
258
									leadPromises.push(
-
 
259
										$.get(context + '/lead-geo/check/' + v.id).then(function (rr) {
-
 
260
											var g = rr.response || rr;
-
 
261
											if (g && g.hasApprovedGeo) {
-
 
262
												v.lat = g.latitude;
-
 
263
												v.lng = g.longitude;
-
 
264
											}
-
 
265
										}),
-
 
266
										$.get(context + '/getLead?leadId=' + v.id).then(function (rr) {
-
 
267
											var l = rr.response || rr;
-
 
268
											if (l && l.firstName) v.name = 'LEAD - ' + l.firstName + ' ' + (l.lastName || '');
-
 
269
										})
-
 
270
									);
-
 
271
								});
-
 
272
							});
-
 
273
 
-
 
274
							$.when.apply($, leadPromises).always(function () {
-
 
275
								renderRoutePanel();
-
 
276
								var dateLabel = planDate
-
 
277
									? '<div style="font-size:11px;color:#f59e0b;margin-bottom:8px;">Editing run on: ' + planDate + ' (leads on this date can be removed)</div>'
-
 
278
									: '<div style="font-size:11px;color:#94a3b8;margin-bottom:8px;">Editing beat template (partners only)</div>';
-
 
279
								$('#route-list').prepend(
-
 
280
									'<div style="margin-bottom:8px;"><button class="btn-back-to-list" '
-
 
281
									+ 'style="font-size:11px;padding:4px 10px;border:1px solid #475569;'
-
 
282
									+ 'border-radius:4px;background:none;color:#94a3b8;cursor:pointer;'
-
 
283
									+ 'font-family:inherit;">← Back to list</button></div>'
-
 
284
									+ '<div style="font-size:14px;font-weight:600;color:#f59e0b;margin-bottom:4px;">'
-
 
285
									+ 'EDITING: ' + state.beatTitle + '</div>' + dateLabel);
-
 
286
 
-
 
287
								initMap();
-
 
288
								// Add lead markers (orange) on the map
-
 
289
								state.days.forEach(function (day) {
-
 
290
									day.visits.forEach(function (v) {
-
 
291
										if (v.isLead && v.lat && v.lng) {
-
 
292
											var m = new google.maps.Marker({
-
 
293
												map: map,
-
 
294
												position: {lat: v.lat, lng: v.lng},
-
 
295
												title: v.name,
-
 
296
												label: {text: 'L', color: '#fff', fontSize: '11px', fontWeight: '700'},
-
 
297
												icon: {
-
 
298
													path: google.maps.SymbolPath.CIRCLE, scale: 16,
-
 
299
													fillColor: '#e67e22', fillOpacity: 0.95,
-
 
300
													strokeColor: '#fff', strokeWeight: 2
-
 
301
												}
-
 
302
											});
-
 
303
											markers[v.id] = m;
-
 
304
										}
-
 
305
									});
-
 
306
								});
-
 
307
								resetMarkerColors();
-
 
308
								drawRoute();
-
 
309
								updateBottomBar();
-
 
310
								$('#bottom-bar').show();        // make Save bar visible
-
 
311
								$('#btn-finish').text('Save Changes').prop('disabled', false);
-
 
312
							});
-
 
313
						}
-
 
314
					});
-
 
315
				}
-
 
316
			});
-
 
317
		}
-
 
318
	});
-
 
319
}
-
 
320
 
142
function viewBeat(planGroupId, planDate) {
321
function viewBeat(planGroupId, planDate) {
143
    state.mode = 'view';
322
    state.mode = 'view';
144
	state.viewDate = planDate || null; // when set, show that run's leads
323
	state.viewDate = planDate || null; // when set, show that run's leads
145
    $('#bottom-bar').hide();
324
    $('#bottom-bar').hide();
146
 
325
 
Line 440... Line 619...
440
	state.days = [{
619
	state.days = [{
441
		dayNumber: 1, startLat: state.homeLat, startLng: state.homeLng, startName: state.homeName,
620
		dayNumber: 1, startLat: state.homeLat, startLng: state.homeLng, startName: state.homeName,
442
		visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
621
		visits: [], endAction: null, stayName: null, stayLat: null, stayLng: null, totalKm: 0, totalMins: 0
443
	}];
622
	}];
444
	state.currentDay = 1;
623
	state.currentDay = 1;
445
    if (state.mode === 'create') $('#bottom-bar').show();
624
	if (state.mode === 'create' || state.mode === 'edit') $('#bottom-bar').show();
446
	renderRoutePanel();
625
	renderRoutePanel();
447
}
626
}
448
 
627
 
449
function initMap() {
628
function initMap() {
450
	var center = {lat: state.homeLat, lng: state.homeLng};
629
	var center = {lat: state.homeLat, lng: state.homeLng};
Line 845... Line 1024...
845
                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>';
1024
                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>';
846
                html += '</div>';
1025
                html += '</div>';
847
            }
1026
            }
848
 
1027
 
849
            html += '</div>';
1028
            html += '</div>';
850
            if (state.mode === 'create') {
1029
			if (state.mode === 'create' || state.mode === 'edit') {
851
                html += '<span class="stop-remove" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" title="Remove">&times;</span>';
1030
                html += '<span class="stop-remove" data-fofoid="' + v.id + '" data-daynum="' + day.dayNumber + '" title="Remove">&times;</span>';
852
            }
1031
            }
853
			html += '</div>';
1032
			html += '</div>';
854
 
1033
 
855
			prevLat = v.lat;
1034
			prevLat = v.lat;
Line 1030... Line 1209...
1030
 
1209
 
1031
	// Dates will be assigned later via calendar — pass nulls for now
1210
	// Dates will be assigned later via calendar — pass nulls for now
1032
	var dates = daysData.map(function () {
1211
	var dates = daysData.map(function () {
1033
		return null;
1212
		return null;
1034
	});
1213
	});
1035
	var planData = JSON.stringify({days: daysData, dates: dates, beatName: beatTitle.trim()});
1214
	var planPayload = {days: daysData, dates: dates, beatName: beatTitle.trim()};
-
 
1215
	// When editing a specific scheduled date, pass it so leads removed from that date get cancelled
-
 
1216
	if (state.mode === 'edit' && state.editingDate) {
-
 
1217
		planPayload.planDate = state.editingDate;
-
 
1218
	}
-
 
1219
	var planData = JSON.stringify(planPayload);
1036
 
1220
 
1037
	isSubmittingBeat = true;
1221
	isSubmittingBeat = true;
-
 
1222
	var isEdit = state.mode === 'edit' && state.editingBeatId;
1038
	$('#btn-finish').prop('disabled', true).text('Saving...');
1223
	$('#btn-finish').prop('disabled', true).text(isEdit ? 'Updating...' : 'Saving...');
-
 
1224
 
-
 
1225
	var url = isEdit ? '/beatPlan/updateBeat' : '/beatPlan/submitPlan';
-
 
1226
	var ajaxData = isEdit
-
 
1227
		? {beatId: state.editingBeatId, planData: planData}
-
 
1228
		: {authUserId: state.authUserId, planData: planData};
1039
 
1229
 
1040
	$.ajax({
1230
	$.ajax({
1041
		url: context + "/beatPlan/submitPlan",
1231
		url: context + url,
1042
		type: "POST",
1232
		type: "POST",
1043
		data: {authUserId: state.authUserId, planData: planData},
1233
		data: ajaxData,
1044
		success: function (response) {
1234
		success: function (response) {
1045
			var data = (typeof response === 'string' ? JSON.parse(response) : response);
1235
			var data = (typeof response === 'string' ? JSON.parse(response) : response);
1046
			var result = data.response || data;
1236
			var result = data.response || data;
1047
 
1237
 
1048
			if (result.status) {
1238
			if (result.status) {
Line 1050... Line 1240...
1050
				state.savedPlanGroupId = result.planGroupId;
1240
				state.savedPlanGroupId = result.planGroupId;
1051
				$('#bottom-bar').hide();
1241
				$('#bottom-bar').hide();
1052
 
1242
 
1053
                if (result.duplicate) {
1243
                if (result.duplicate) {
1054
					alert('This beat already exists.');
1244
					alert('This beat already exists.');
-
 
1245
				} else if (isEdit) {
-
 
1246
					alert('Beat "' + beatTitle + '" updated successfully!');
1055
                } else {
1247
                } else {
1056
                    alert('Beat "' + beatTitle + '" saved successfully!');
1248
                    alert('Beat "' + beatTitle + '" saved successfully!');
1057
                }
1249
                }
1058
 
1250
 
1059
                // Go back to beat list
1251
				// Reset edit state, go back to beat list
-
 
1252
				state.editingBeatId = null;
-
 
1253
				state.mode = 'list';
1060
                showBeatList();
1254
                showBeatList();
1061
			} else {
1255
			} else {
1062
				$('#btn-finish').prop('disabled', false).text('Finish Plan');
1256
				$('#btn-finish').prop('disabled', false).text(isEdit ? 'Save Changes' : 'Finish Plan');
1063
				alert('Error saving beat plan');
1257
				alert('Error saving beat plan');
1064
			}
1258
			}
1065
			isSubmittingBeat = false;
1259
			isSubmittingBeat = false;
1066
		},
1260
		},
1067
		error: function (xhr) {
1261
		error: function (xhr) {
1068
			isSubmittingBeat = false;
1262
			isSubmittingBeat = false;
1069
			$('#btn-finish').prop('disabled', false).text('Finish Plan');
1263
			$('#btn-finish').prop('disabled', false).text(isEdit ? 'Save Changes' : 'Finish Plan');
1070
			alert('Error: ' + (xhr.responseText || xhr.statusText));
1264
			alert('Error: ' + (xhr.responseText || xhr.statusText));
1071
		}
1265
		}
1072
	});
1266
	});
1073
});
1267
});
1074
 
1268
 
Line 1078... Line 1272...
1078
	$('.panel-tab').removeClass('active');
1272
	$('.panel-tab').removeClass('active');
1079
	$(this).addClass('active');
1273
	$(this).addClass('active');
1080
	$('#panel-route').toggle(tab === 'route');
1274
	$('#panel-route').toggle(tab === 'route');
1081
	$('#panel-calendar').toggle(tab === 'calendar');
1275
	$('#panel-calendar').toggle(tab === 'calendar');
1082
	$('#cal-grid-container').toggle(tab === 'calendar');
1276
	$('#cal-grid-container').toggle(tab === 'calendar');
1083
    $('#bottom-bar').toggle(tab === 'route' && state.mode === 'create' && state.days && state.days.length > 0);
1277
	$('#bottom-bar').toggle(tab === 'route' && (state.mode === 'create' || state.mode === 'edit') && state.days && state.days.length > 0);
1084
 
1278
 
1085
	if (tab === 'calendar' && !state.calMonth) {
1279
	if (tab === 'calendar' && !state.calMonth) {
1086
		var now = new Date();
1280
		var now = new Date();
1087
		state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
1281
		state.calMonth = now.getFullYear() + '-' + ('0' + (now.getMonth() + 1)).slice(-2);
1088
		loadCalendarData();
1282
		loadCalendarData();
Line 1651... Line 1845...
1651
	var autoUid = getParam('autoUserId');
1845
	var autoUid = getParam('autoUserId');
1652
	console.log('[BEAT-AUTO] autoUserId =', autoUid);
1846
	console.log('[BEAT-AUTO] autoUserId =', autoUid);
1653
	if (!autoUid) return;
1847
	if (!autoUid) return;
1654
 
1848
 
1655
	var autoName = getParam('autoUserName');
1849
	var autoName = getParam('autoUserName');
-
 
1850
	var editBeatId = getParam('editBeatId');
-
 
1851
	var editDate = getParam('editDate');
-
 
1852
 
1656
	state.authUserId = parseInt(autoUid);
1853
	state.authUserId = parseInt(autoUid);
1657
	state.categoryId = parseInt($('#bp-category').val()) || 4;
1854
	state.categoryId = parseInt($('#bp-category').val()) || 4;
1658
	state.mode = 'list';
1855
	state.mode = 'list';
1659
	$('#bp-user-label').text(autoName || ('User #' + autoUid));
1856
	$('#bp-user-label').text(autoName || ('User #' + autoUid));
1660
 
1857
 
Line 1667... Line 1864...
1667
			if (data.locationName) {
1864
			if (data.locationName) {
1668
				state.homeLat = parseFloat(data.latitude);
1865
				state.homeLat = parseFloat(data.latitude);
1669
				state.homeLng = parseFloat(data.longitude);
1866
				state.homeLng = parseFloat(data.longitude);
1670
				state.homeName = data.locationName;
1867
				state.homeName = data.locationName;
1671
			}
1868
			}
1672
			showBeatList();
1869
			if (editBeatId) {
1673
			// Jump straight to the Calendar tab (that's what the user came here for)
1870
				console.log('[BEAT-AUTO] entering edit for beat', editBeatId, 'date', editDate);
-
 
1871
				editBeat(String(editBeatId), editDate || null);
-
 
1872
			} else {
-
 
1873
				showBeatList();
1674
			setTimeout(function () {
1874
				setTimeout(function () {
1675
				$('.panel-tab[data-tab="calendar"]').click();
1875
					$('.panel-tab[data-tab="calendar"]').click();
1676
			}, 100);
1876
				}, 100);
-
 
1877
			}
1677
		},
1878
		},
1678
		error: function (xhr) {
1879
		error: function (xhr) {
1679
			console.error('[BEAT-AUTO] getBaseLocation failed:', xhr.status, xhr.responseText);
1880
			console.error('[BEAT-AUTO] getBaseLocation failed:', xhr.status, xhr.responseText);
1680
		}
1881
		}
1681
	});
1882
	});