Rev 36962 | Blame | Compare with Previous | Last modification | View Log | RSS feed
<section class="wrapper"><div class="row"><div class="col-lg-12"><h3 class="page-header"><i class="icon_calendar"></i> Scheduled Beats - Day View</h3><ol class="breadcrumb"><li><i class="fa fa-home"></i><a href="${rc.contextPath}/dashboard">Home</a></li><li><i class="icon_group"></i> Partner Acquisition</li><li><i class="icon_calendar"></i> Beat Day View</li></ol></div></div><div class="row" style="margin-bottom:10px;"><div class="col-lg-12" style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;"><label style="margin:0;">Category:</label><select id="dayViewCategory" class="form-control input-sm" style="width:140px;"><option value="$categoryId">Sales</option></select><label style="margin:0;">Level:</label><select id="dayViewLevel" class="form-control input-sm" style="width:140px;"><option value="">All Levels</option>#foreach($lvl in $escalationTypes)<option value="$lvl">$lvl</option>#end</select><label style="margin:0;">Date:</label><input type="date" id="dayViewDate" class="form-control input-sm" style="width:160px;"><button class="btn btn-primary btn-sm" id="dayViewLoad">Load</button><span id="dayViewSummary" style="color:#777; font-size:12px; margin-left:10px;"></span></div></div><div class="row"><div class="col-lg-12"><div class="table-responsive"><table class="table table-bordered table-striped table-hover" id="dayViewTable"><thead><tr><th>User</th><th>Date</th><th>Beat</th><th>Day</th><th>Partners</th><th>Leads</th><th>Total Visits</th><th>Action</th></tr></thead><tbody id="dayViewRows"><tr><td colspan="8" style="text-align:center; color:#999;">Pick a date range and click Load.</td></tr></tbody></table></div></div></div></section><script>// Edit-beat / assign-visit in the Beat Planner are L2-and-above only.var CAN_EDIT_BEAT = $canEditBeat;(function () {// Default: today only. Other dates → use the per-user calendar from the row's View button.var today = new Date();function fmt(d) {return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);}$('#dayViewDate').val(fmt(today));loadDayView();})();$(document).on('click', '#dayViewLoad', function () {loadDayView();});function loadDayView() {var d = $('#dayViewDate').val();if (!d) {alert('Pick a date');return;}$('#dayViewRows').html('<tr><td colspan="8" style="text-align:center;">Loading...</td></tr>');$('#dayViewSummary').text('');var params = {startDate: d, endDate: d};var cat = $('#dayViewCategory').val();var lvl = $('#dayViewLevel').val();if (cat) params.categoryId = cat;if (lvl) params.escalationType = lvl;$.get(context + '/beatPlan/scheduledList', params).done(function (r) {var data = r.response || r;renderDayViewRows(data.rows || []);$('#dayViewSummary').text((data.rows || []).length + ' beats scheduled on ' + data.startDate);}).fail(function (xhr) {$('#dayViewRows').html('<tr><td colspan="8" style="color:#d9534f; text-align:center;">Error: ' + (xhr.responseText || xhr.statusText) + '</td></tr>');});}function renderDayViewRows(rows) {if (!rows || rows.length === 0) {$('#dayViewRows').html('<tr><td colspan="8" style="text-align:center; color:#999;">No beats scheduled in this range.</td></tr>');return;}// Assign Visit is only valid for TODAY's run — future/past dates hide the button.var todayStr = (function () {var d = new Date();var mm = ('0' + (d.getMonth() + 1)).slice(-2);var dd = ('0' + d.getDate()).slice(-2);return d.getFullYear() + '-' + mm + '-' + dd;})();var html = '';rows.forEach(function (r) {var isToday = r.scheduleDate === todayStr;html += '<tr>'+ '<td>' + r.userName + '</td>'+ '<td><span class="label label-primary">' + r.scheduleDate + '</span></td>'+ '<td><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:' + (r.beatColor || '#3498db') + ';margin-right:6px;"></span>' + r.beatName + '</td>'+ '<td>' + r.dayNumber + '</td>'+ '<td>' + r.partnerCount + '</td>'+ '<td>' + (r.leadCount > 0 ? '<span class="label label-warning">' + r.leadCount + '</span>' : '0') + '</td>'+ '<td><strong>' + r.visitCount + '</strong></td>'+ '<td>'+ '<button class="btn btn-xs btn-info view-user-calendar" data-userid="' + r.authUserId + '" data-username="' + r.userName + '">View Calendar</button> '+ (CAN_EDIT_BEAT? '<button class="btn btn-xs btn-warning edit-beat-on-date" '+ 'data-userid="' + r.authUserId + '" data-username="' + r.userName + '" '+ 'data-beatid="' + r.beatId + '" data-date="' + r.scheduleDate + '">Edit</button>'+ (isToday? ' <button class="btn btn-xs btn-success assign-visit-btn"'+ ' data-userid="' + r.authUserId + '" data-username="' + r.userName + '"'+ ' data-beatid="' + r.beatId + '"'+ ' data-date="' + r.scheduleDate + '">Assign Visit</button>': '')// Add Lead is allowed for today AND future runs — a lead is a dated stop.+ ' <button class="btn btn-xs add-lead-btn"'+ ' style="background:#0d9488;color:#fff;border:none;"'+ ' data-userid="' + r.authUserId + '" data-username="' + r.userName + '"'+ ' data-beatid="' + r.beatId + '"'+ ' data-date="' + r.scheduleDate + '">Add Lead</button>': '')+ '</td>'+ '</tr>';});$('#dayViewRows').html(html);}// View -> open user's calendar inside the dashboard in a modal iframe (pre-selected)$(document).on('click', '.view-user-calendar', function () {var userId = $(this).data('userid');var userName = $(this).data('username') || '';var url = context + '/beatPlanWindow?autoUserId=' + userId+ '&autoUserName=' + encodeURIComponent(userName);$('#userCalendarTitle').text('Calendar - ' + userName);$('#userCalendarFrame').attr('src', url);$('#userCalendarModal').modal('show');});// Edit a beat on a specific date — opens beatPlanWindow in edit mode for that run$(document).on('click', '.edit-beat-on-date', function () {var userId = $(this).data('userid');var userName = $(this).data('username') || '';var beatId = $(this).data('beatid');var date = $(this).data('date');var url = context + '/beatPlanWindow?autoUserId=' + userId+ '&autoUserName=' + encodeURIComponent(userName)+ '&editBeatId=' + beatId + '&editDate=' + date;$('#userCalendarTitle').text('Edit Beat - ' + userName + ' on ' + date);$('#userCalendarFrame').attr('src', url);$('#userCalendarModal').modal('show');});// ---------- ASSIGN VISIT ----------var avContext = {authUserId: null, userName: '', date: '', beatId: null, allParties: [], agendaOptions: []};$(document).on('click', '.assign-visit-btn', function () {avContext.authUserId = $(this).data('userid');avContext.userName = $(this).data('username') || '';avContext.date = $(this).data('date');avContext.beatId = $(this).data('beatid');avContext.allParties = [];$('#avTitle').text('Assign Visit - ' + avContext.userName + ' on ' + avContext.date);$('#avSearch').val('');// The default-agenda multiselect is rebuilt (and re-initialized) below in rebuildDefaultAgenda$('#avPartyList').html('<div style="padding:20px; color:#999; text-align:center;">Loading parties...</div>');$('#avMsg').text('');$('#avSelectedCount').text('0 selected');$('#avSubmit').prop('disabled', true);$('#assignVisitModal').modal('show');$.get(context + '/beatPlan/assignVisit/parties', {authUserId: avContext.authUserId,date: avContext.date,beatId: avContext.beatId}).done(function (r) {var data = r.response || r;// Drop parties already in the beat — they're handled by the beat plan itself,// Assign Visit is purely for adding extra (non-beat) visits.avContext.allParties = (data.parties || []).filter(function (p) {return !p.inBeat;});avContext.agendaOptions = data.agendaOptions || [];if (!data.dtrUserId) {$('#avPartyList').html('<div style="padding:20px; color:#c62828; text-align:center;">' +'This user has no dtr.users record (cannot create visit tasks).</div>');return;}rebuildDefaultAgenda();renderAvParties(avContext.allParties);}).fail(function (xhr) {$('#avPartyList').html('<div style="padding:20px; color:#c62828; text-align:center;">' +'Error loading parties: ' + (xhr.responseText || xhr.statusText) + '</div>');});});// Per-row agenda markup. Initialized as bootstrap-multiselect dropdown// (project standard — same as today-offer.vm, lead.vm, etc.) after render.// In-beat parties are filtered out of the list entirely, so only non-beat// (extra-visit) parties show up here. existingAgendas[] is pre-selected for// parties that already have a saved visit today (so editing existing ones works).function agendaSelectHtml(p) {var preset = {};(p.existingAgendas || []).forEach(function (a) {preset[a] = true;});var html = '<select multiple class="av-party-agenda"'+ ' data-fofoid="' + p.fofoStoreId + '">';avContext.agendaOptions.forEach(function (opt) {html += '<option value="' + opt + '"' + (preset[opt] ? ' selected' : '') + '>' + opt + '</option>';});html += '</select>';return html;}var AGENDA_MULTISELECT_OPTS = {includeSelectAllOption: false,maxHeight: 220,buttonWidth: '240px',numberDisplayed: 2,nonSelectedText: 'Pick agenda(s)',nSelectedText: ' agendas',allSelectedText: 'All agendas',enableFiltering: true,enableCaseInsensitiveFiltering: true};function rebuildDefaultAgenda() {var $sel = $('#avDefaultAgenda');// Destroy any prior instance before re-initing (re-open of the modal)try {$sel.multiselect('destroy');} catch (e) {}var html = '';avContext.agendaOptions.forEach(function (opt) {html += '<option value="' + opt + '">' + opt + '</option>';});$sel.html(html);// Build opts as a variable — inline {...} object literal after $.extend()// confused Velocity's parser ("Encountered { ... was expecting ...").var defaultOpts = $.extend({}, AGENDA_MULTISELECT_OPTS);defaultOpts.nonSelectedText = 'Pick agenda(s) to apply';$sel.multiselect(defaultOpts);}function initRowMultiselects() {$('.av-party-agenda').each(function () {var $sel = $(this);try {$sel.multiselect('destroy');} catch (e) {}$sel.multiselect(AGENDA_MULTISELECT_OPTS);});}function renderAvParties(list) {if (!list || list.length === 0) {$('#avPartyList').html('<div style="padding:20px; color:#999; text-align:center;">No parties found.</div>');return;}var html = '<table class="table table-condensed table-hover" style="margin-bottom:0; font-size:13px;">';html += '<thead><tr>'+ '<th style="width:30px;"><input type="checkbox" id="avSelectAll" title="Select all"></th>'+ '<th>Code</th><th>Outlet</th><th>City</th>'+ '<th style="width:260px;">Agenda</th>'+ '<th style="width:260px;">Description</th>'+ '</tr></thead><tbody>';list.forEach(function (p) {var safeName = (p.outletName || '').replace(/"/g, '"');var existingDesc = (p.existingDescription || '').replace(/"/g, '"');html += '<tr>'+ '<td><input type="checkbox" class="av-party-chk" '+ 'data-fofoid="' + p.fofoStoreId + '" '+ 'data-name="' + safeName + '" '+ 'data-lat="' + (p.latitude || '') + '" '+ 'data-lng="' + (p.longitude || '') + '"></td>'+ '<td>' + (p.code || '') + '</td>'+ '<td>' + (p.outletName || '') + '</td>'+ '<td>' + (p.city || '') + '</td>'+ '<td>' + agendaSelectHtml(p) + '</td>'+ '<td><textarea class="form-control input-sm av-party-desc"'+ ' data-fofoid="' + p.fofoStoreId + '"'+ ' rows="2" placeholder="Optional notes..."'+ ' style="resize:vertical; min-height:34px;">' + existingDesc + '</textarea></td>'+ '</tr>';});html += '</tbody></table>';$('#avPartyList').html(html);initRowMultiselects();updateAvSelectedCount();}// Search filter$(document).on('input', '#avSearch', function () {var q = $(this).val().toLowerCase().trim();if (!q) {renderAvParties(avContext.allParties);return;}var filtered = avContext.allParties.filter(function (p) {return (p.code || '').toLowerCase().indexOf(q) !== -1|| (p.outletName || '').toLowerCase().indexOf(q) !== -1|| (p.city || '').toLowerCase().indexOf(q) !== -1;});renderAvParties(filtered);});// Select all toggle$(document).on('change', '#avSelectAll', function () {$('.av-party-chk').prop('checked', this.checked);updateAvSelectedCount();});$(document).on('change', '.av-party-chk', updateAvSelectedCount);function updateAvSelectedCount() {var n = $('.av-party-chk:checked').length;$('#avSelectedCount').text(n + ' selected');$('#avSubmit').prop('disabled', n === 0);}// Apply the default agendas (multi-select) to all currently-selected rows.// MERGES with each row's existing selections so in-beat rows keep prior agendas.// After updating <select>.val(), call multiselect('refresh') so the checkbox// UI re-syncs with the underlying option state.$(document).on('click', '#avApplyAgenda', function () {var defValues = $('#avDefaultAgenda').val() || [];if (defValues.length === 0) {alert('Pick at least one agenda above first');return;}$('.av-party-chk:checked').each(function () {var fid = $(this).data('fofoid');var $sel = $('.av-party-agenda[data-fofoid="' + fid + '"]');var existing = $sel.val() || [];var merged = existing.slice();defValues.forEach(function (v) {if (merged.indexOf(v) === -1) merged.push(v);});$sel.val(merged);try {$sel.multiselect('refresh');} catch (e) {}});});// Submit$(document).on('click', '#avSubmit', function () {var picks = [];var missing = 0;$('.av-party-chk:checked').each(function () {var fid = parseInt($(this).data('fofoid'));var agendas = $('.av-party-agenda[data-fofoid="' + fid + '"]').val() || [];var description = ($('.av-party-desc[data-fofoid="' + fid + '"]').val() || '').trim();if (agendas.length === 0) missing++;picks.push({fofoStoreId: fid,outletName: $(this).data('name'),latitude: $(this).data('lat') ? String($(this).data('lat')) : null,longitude: $(this).data('lng') ? String($(this).data('lng')) : null,agendas: agendas,description: description});});if (picks.length === 0) return;if (missing > 0) {if (!confirm(missing + ' selected row(s) have no agenda. Submit anyway with default "Visit"?')) return;}var btn = $(this);btn.prop('disabled', true).text('Assigning...');$('#avMsg').text('');$.ajax({url: context + '/beatPlan/assignVisit/submit',type: 'POST',contentType: 'application/json',data: JSON.stringify({authUserId: avContext.authUserId,planDate: avContext.date,parties: picks}),success: function (r) {var d = r.response || r;$('#avMsg').html('<span style="color:#2e7d32;">' + (d.message || 'Saved') + '</span>');btn.prop('disabled', false).text('Save Assignments');setTimeout(function () {$('#assignVisitModal').modal('hide');}, 1500);},error: function (xhr) {var msg = 'Failed';try {msg = (JSON.parse(xhr.responseText).response.message) || msg;} catch (e) {}$('#avMsg').html('<span style="color:#c62828;">' + msg + '</span>');btn.prop('disabled', false).text('Save Assignments');}});});// ---------- ADD LEAD ----------// Routes the rep's assigned leads onto this scheduled beat/date. Writes an// APPROVED lead_route per pick via /beatPlan/leadsForBeat/submit.var alCtx = {authUserId: null, userName: '', beatId: null, date: '', allLeads: []};$(document).on('click', '.add-lead-btn', function () {alCtx.authUserId = $(this).data('userid');alCtx.userName = $(this).data('username') || '';alCtx.beatId = $(this).data('beatid');alCtx.date = $(this).data('date');alCtx.allLeads = [];$('#alTitle').text('Add Lead - ' + alCtx.userName + ' on ' + alCtx.date);$('#alSearch').val('');$('#alLeadList').html('<div style="padding:20px; color:#999; text-align:center;">Loading leads...</div>');$('#alMsg').text('');$('#alSelectedCount').text('0 selected');$('#alSubmit').prop('disabled', true);$('#addLeadModal').modal('show');$.get(context + '/beatPlan/assignedLeads', {authUserId: alCtx.authUserId,beatId: alCtx.beatId,date: alCtx.date}).done(function (r) {var data = r.response || r;alCtx.allLeads = data.rows || [];renderAlLeads(alCtx.allLeads);}).fail(function (xhr) {$('#alLeadList').html('<div style="padding:20px; color:#c62828; text-align:center;">' +'Error loading leads: ' + (xhr.responseText || xhr.statusText) + '</div>');});});function renderAlLeads(list) {if (!list || list.length === 0) {$('#alLeadList').html('<div style="padding:20px; color:#999; text-align:center;">No active leads assigned to this user.</div>');return;}var html = '<table class="table table-condensed table-hover" style="margin-bottom:0; font-size:13px;">';html += '<thead><tr>'+ '<th style="width:30px;"><input type="checkbox" id="alSelectAll" title="Select all"></th>'+ '<th>Lead</th><th>Outlet</th><th>City</th><th>Stage</th><th style="width:80px;"></th>'+ '</tr></thead><tbody>';list.forEach(function (p) {var onBeat = !!p.alreadyOnBeat;html += '<tr' + (onBeat ? ' style="opacity:.55;"' : '') + '>'+ '<td><input type="checkbox" class="al-lead-chk" data-lead="' + p.leadId + '"'+ (onBeat ? ' checked disabled' : '') + '></td>'+ '<td>' + (p.leadName || '') + (p.mobile ? ' <span style="color:#999;">· ' + p.mobile + '</span>' : '') + '</td>'+ '<td>' + (p.outletName || '') + '</td>'+ '<td>' + (p.city || '') + '</td>'+ '<td>' + (p.stage || '') + (p.hasGeo ? '' : ' <span class="label label-warning" title="No approved location">no geo</span>') + '</td>'+ '<td>' + (onBeat ? '<span class="label label-success">On beat</span>' : '') + '</td>'+ '</tr>';});html += '</tbody></table>';$('#alLeadList').html(html);updateAlSelectedCount();}$(document).on('input', '#alSearch', function () {var q = $(this).val().toLowerCase().trim();if (!q) {renderAlLeads(alCtx.allLeads);return;}var filtered = alCtx.allLeads.filter(function (p) {return (p.leadName || '').toLowerCase().indexOf(q) !== -1|| (p.outletName || '').toLowerCase().indexOf(q) !== -1|| (p.city || '').toLowerCase().indexOf(q) !== -1|| (p.mobile || '').toLowerCase().indexOf(q) !== -1;});renderAlLeads(filtered);});$(document).on('change', '#alSelectAll', function () {$('.al-lead-chk:not(:disabled)').prop('checked', this.checked);updateAlSelectedCount();});$(document).on('change', '.al-lead-chk', updateAlSelectedCount);function updateAlSelectedCount() {var n = $('.al-lead-chk:checked:not(:disabled)').length;$('#alSelectedCount').text(n + ' selected');$('#alSubmit').prop('disabled', n === 0);}$(document).on('click', '#alSubmit', function () {var ids = [];$('.al-lead-chk:checked:not(:disabled)').each(function () {ids.push(parseInt($(this).data('lead')));});if (ids.length === 0) return;var btn = $(this);btn.prop('disabled', true).text('Adding...');$('#alMsg').text('');$.ajax({url: context + '/beatPlan/leadsForBeat/submit',type: 'POST',contentType: 'application/json',data: JSON.stringify({authUserId: alCtx.authUserId,beatId: alCtx.beatId,date: alCtx.date,leadIds: ids}),success: function (r) {var d = r.response || r;$('#alMsg').html('<span style="color:#2e7d32;">' + (d.message || 'Saved') + '</span>');btn.prop('disabled', false).text('Add Leads');setTimeout(function () {$('#addLeadModal').modal('hide');loadDayView();}, 1200);},error: function (xhr) {var msg = 'Failed';try {msg = (JSON.parse(xhr.responseText).response.message) || msg;} catch (e) {}$('#alMsg').html('<span style="color:#c62828;">' + msg + '</span>');btn.prop('disabled', false).text('Add Leads');}});});</script><!-- Add Lead Modal --><div class="modal fade" id="addLeadModal" tabindex="-1" style="overflow-y:auto;"><div class="modal-dialog" style="width:90%; max-width:820px; margin-top:30px;"><div class="modal-content"><div class="modal-header"><button type="button" class="close" data-dismiss="modal">×</button><h4 class="modal-title" id="alTitle">Add Lead</h4></div><div class="modal-body"><div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;"><input type="text" id="alSearch" class="form-control input-sm"placeholder="Search by name, outlet, city or mobile..." style="flex:1;"><span id="alSelectedCount" style="font-size:12px; color:#666;">0 selected</span></div><div id="alLeadList"style="max-height:50vh; overflow-y:auto; border:1px solid #e5e5e5; border-radius:4px;"></div><div id="alMsg" style="margin-top:10px; font-size:12px;"></div></div><div class="modal-footer"><button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button><button type="button" class="btn btn-success" id="alSubmit" disabled>Add Leads</button></div></div></div></div><!-- Assign Visit Modal --><div class="modal fade" id="assignVisitModal" tabindex="-1" style="overflow-y:auto;"><div class="modal-dialog" style="width:90%; max-width:900px; margin-top:30px;"><div class="modal-content"><div class="modal-header"><button type="button" class="close" data-dismiss="modal">×</button><h4 class="modal-title" id="avTitle">Assign Visit</h4></div><div class="modal-body"><div style="display:flex; gap:10px; align-items:flex-start; margin-bottom:10px;"><div style="flex:1;"><label style="font-size:11px; color:#666; font-weight:normal; margin-bottom:2px;">Agenda - applies to selected rows</label><select id="avDefaultAgenda" multiple></select></div><button type="button" class="btn btn-default btn-sm" id="avApplyAgenda"style="margin-top:18px;"title="Add the picked agendas into all selected rows (merges with existing)">Apply to Selected</button></div><div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;"><input type="text" id="avSearch" class="form-control input-sm"placeholder="Search by code, outlet name or city..." style="flex:1;"><span id="avSelectedCount" style="font-size:12px; color:#666;">0 selected</span></div><div id="avPartyList"style="max-height:50vh; overflow-y:auto; border:1px solid #e5e5e5; border-radius:4px;"></div><div id="avMsg" style="margin-top:10px; font-size:12px;"></div></div><div class="modal-footer"><button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button><button type="button" class="btn btn-success" id="avSubmit" disabled>Save Assignments</button></div></div></div></div><!-- Calendar modal (iframe loads /beatPlanWindow with auto-selected user) --><div class="modal fade" id="userCalendarModal" tabindex="-1" style="overflow-y:auto;"><div class="modal-dialog" style="width:95%; max-width:1300px; margin-top:20px;"><div class="modal-content"><div class="modal-header"><button type="button" class="close" data-dismiss="modal"onclick="$('#userCalendarFrame').attr('src','');">×</button><h4 class="modal-title" id="userCalendarTitle">Calendar</h4></div><div class="modal-body" style="padding:0;"><iframe id="userCalendarFrame" style="width:100%; height:80vh; border:none;"></iframe></div></div></div></div>