Rev 36663 | Rev 36761 | Go to most recent revision | 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;">From:</label><input type="date" id="dayViewStart" class="form-control input-sm" style="width:160px;"><label style="margin:0;">To:</label><input type="date" id="dayViewEnd" 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>(function () {// Default: today → today + 7 daysvar today = new Date();var week = new Date();week.setDate(today.getDate() + 7);function fmt(d) {return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);}$('#dayViewStart').val(fmt(today));$('#dayViewEnd').val(fmt(week));loadDayView();})();$(document).on('click', '#dayViewLoad', function () {loadDayView();});function loadDayView() {var s = $('#dayViewStart').val();var e = $('#dayViewEnd').val();if (!s || !e) {alert('Pick a date range');return;}$('#dayViewRows').html('<tr><td colspan="8" style="text-align:center;">Loading...</td></tr>');$('#dayViewSummary').text('');$.get(context + '/beatPlan/scheduledList', {startDate: s, endDate: e}).done(function (r) {var data = r.response || r;renderDayViewRows(data.rows || []);$('#dayViewSummary').text(data.rows.length + ' beats scheduled between ' + data.startDate + ' and ' + data.endDate);}).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> '+ '<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>': '')+ '</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');}});});</script><!-- 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>