Subversion Repositories SmartDukaan

Rev

Rev 36663 | Rev 36761 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
36662 ranu 1
<section class="wrapper">
2
    <div class="row">
3
        <div class="col-lg-12">
4
            <h3 class="page-header"><i class="icon_calendar"></i> Scheduled Beats - Day View</h3>
5
            <ol class="breadcrumb">
6
                <li><i class="fa fa-home"></i><a href="${rc.contextPath}/dashboard">Home</a></li>
7
                <li><i class="icon_group"></i> Partner Acquisition</li>
8
                <li><i class="icon_calendar"></i> Beat Day View</li>
9
            </ol>
10
        </div>
11
    </div>
12
 
13
    <div class="row" style="margin-bottom:10px;">
14
        <div class="col-lg-12" style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;">
15
            <label style="margin:0;">From:</label>
16
            <input type="date" id="dayViewStart" class="form-control input-sm" style="width:160px;">
17
            <label style="margin:0;">To:</label>
18
            <input type="date" id="dayViewEnd" class="form-control input-sm" style="width:160px;">
19
            <button class="btn btn-primary btn-sm" id="dayViewLoad">Load</button>
20
            <span id="dayViewSummary" style="color:#777; font-size:12px; margin-left:10px;"></span>
21
        </div>
22
    </div>
23
 
24
    <div class="row">
25
        <div class="col-lg-12">
26
            <div class="table-responsive">
27
                <table class="table table-bordered table-striped table-hover" id="dayViewTable">
28
                    <thead>
29
                    <tr>
30
                        <th>User</th>
31
                        <th>Date</th>
32
                        <th>Beat</th>
33
                        <th>Day</th>
34
                        <th>Partners</th>
35
                        <th>Leads</th>
36
                        <th>Total Visits</th>
37
                        <th>Action</th>
38
                    </tr>
39
                    </thead>
40
                    <tbody id="dayViewRows">
41
                    <tr>
42
                        <td colspan="8" style="text-align:center; color:#999;">Pick a date range and click Load.</td>
43
                    </tr>
44
                    </tbody>
45
                </table>
46
            </div>
47
        </div>
48
    </div>
49
</section>
50
 
51
 
52
<script>
53
    (function () {
54
        // Default: today → today + 7 days
55
        var today = new Date();
56
        var week = new Date();
57
        week.setDate(today.getDate() + 7);
58
 
59
        function fmt(d) {
60
            return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2);
61
        }
62
 
63
        $('#dayViewStart').val(fmt(today));
64
        $('#dayViewEnd').val(fmt(week));
65
        loadDayView();
66
    })();
67
 
68
    $(document).on('click', '#dayViewLoad', function () {
69
        loadDayView();
70
    });
71
 
72
    function loadDayView() {
73
        var s = $('#dayViewStart').val();
74
        var e = $('#dayViewEnd').val();
75
        if (!s || !e) {
76
            alert('Pick a date range');
77
            return;
78
        }
79
        $('#dayViewRows').html('<tr><td colspan="8" style="text-align:center;">Loading...</td></tr>');
80
        $('#dayViewSummary').text('');
81
        $.get(context + '/beatPlan/scheduledList', {startDate: s, endDate: e}).done(function (r) {
82
            var data = r.response || r;
83
            renderDayViewRows(data.rows || []);
84
            $('#dayViewSummary').text(data.rows.length + ' beats scheduled between ' + data.startDate + ' and ' + data.endDate);
85
        }).fail(function (xhr) {
86
            $('#dayViewRows').html('<tr><td colspan="8" style="color:#d9534f; text-align:center;">Error: ' + (xhr.responseText || xhr.statusText) + '</td></tr>');
87
        });
88
    }
89
 
90
    function renderDayViewRows(rows) {
91
        if (!rows || rows.length === 0) {
92
            $('#dayViewRows').html('<tr><td colspan="8" style="text-align:center; color:#999;">No beats scheduled in this range.</td></tr>');
93
            return;
94
        }
36716 ranu 95
        // Assign Visit is only valid for TODAY's run — future/past dates hide the button.
96
        var todayStr = (function () {
97
            var d = new Date();
98
            var mm = ('0' + (d.getMonth() + 1)).slice(-2);
99
            var dd = ('0' + d.getDate()).slice(-2);
100
            return d.getFullYear() + '-' + mm + '-' + dd;
101
        })();
36662 ranu 102
        var html = '';
103
        rows.forEach(function (r) {
36716 ranu 104
            var isToday = r.scheduleDate === todayStr;
36662 ranu 105
            html += '<tr>'
106
                    + '<td>' + r.userName + '</td>'
107
                    + '<td><span class="label label-primary">' + r.scheduleDate + '</span></td>'
108
                    + '<td><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:' + (r.beatColor || '#3498db') + ';margin-right:6px;"></span>' + r.beatName + '</td>'
109
                    + '<td>' + r.dayNumber + '</td>'
110
                    + '<td>' + r.partnerCount + '</td>'
111
                    + '<td>' + (r.leadCount > 0 ? '<span class="label label-warning">' + r.leadCount + '</span>' : '0') + '</td>'
112
                    + '<td><strong>' + r.visitCount + '</strong></td>'
113
                    + '<td>'
114
                    + '<button class="btn btn-xs btn-info view-user-calendar" data-userid="' + r.authUserId + '" data-username="' + r.userName + '">View Calendar</button> '
115
                    + '<button class="btn btn-xs btn-warning edit-beat-on-date" '
116
                    + 'data-userid="' + r.authUserId + '" data-username="' + r.userName + '" '
36716 ranu 117
                    + 'data-beatid="' + r.beatId + '" data-date="' + r.scheduleDate + '">Edit</button>'
118
                    + (isToday
119
                            ? ' <button class="btn btn-xs btn-success assign-visit-btn"'
120
                            + ' data-userid="' + r.authUserId + '" data-username="' + r.userName + '"'
121
                            + ' data-beatid="' + r.beatId + '"'
122
                            + ' data-date="' + r.scheduleDate + '">Assign Visit</button>'
123
                            : '')
36662 ranu 124
                    + '</td>'
125
                    + '</tr>';
126
        });
127
        $('#dayViewRows').html(html);
128
    }
129
 
130
    // View -> open user's calendar inside the dashboard in a modal iframe (pre-selected)
131
    $(document).on('click', '.view-user-calendar', function () {
132
        var userId = $(this).data('userid');
133
        var userName = $(this).data('username') || '';
134
        var url = context + '/beatPlanWindow?autoUserId=' + userId
135
                + '&autoUserName=' + encodeURIComponent(userName);
136
        $('#userCalendarTitle').text('Calendar - ' + userName);
137
        $('#userCalendarFrame').attr('src', url);
138
        $('#userCalendarModal').modal('show');
139
    });
140
 
141
    // Edit a beat on a specific date — opens beatPlanWindow in edit mode for that run
142
    $(document).on('click', '.edit-beat-on-date', function () {
143
        var userId = $(this).data('userid');
144
        var userName = $(this).data('username') || '';
145
        var beatId = $(this).data('beatid');
146
        var date = $(this).data('date');
147
        var url = context + '/beatPlanWindow?autoUserId=' + userId
148
                + '&autoUserName=' + encodeURIComponent(userName)
149
                + '&editBeatId=' + beatId + '&editDate=' + date;
150
        $('#userCalendarTitle').text('Edit Beat - ' + userName + ' on ' + date);
151
        $('#userCalendarFrame').attr('src', url);
152
        $('#userCalendarModal').modal('show');
153
    });
36663 ranu 154
 
155
    // ---------- ASSIGN VISIT ----------
36716 ranu 156
    var avContext = {authUserId: null, userName: '', date: '', beatId: null, allParties: [], agendaOptions: []};
36663 ranu 157
 
158
    $(document).on('click', '.assign-visit-btn', function () {
159
        avContext.authUserId = $(this).data('userid');
160
        avContext.userName = $(this).data('username') || '';
161
        avContext.date = $(this).data('date');
36716 ranu 162
        avContext.beatId = $(this).data('beatid');
36663 ranu 163
        avContext.allParties = [];
164
 
36716 ranu 165
        $('#avTitle').text('Assign Visit - ' + avContext.userName + ' on ' + avContext.date);
36663 ranu 166
        $('#avSearch').val('');
36716 ranu 167
        // The default-agenda multiselect is rebuilt (and re-initialized) below in rebuildDefaultAgenda
36663 ranu 168
        $('#avPartyList').html('<div style="padding:20px; color:#999; text-align:center;">Loading parties...</div>');
169
        $('#avMsg').text('');
170
        $('#avSelectedCount').text('0 selected');
171
        $('#avSubmit').prop('disabled', true);
172
        $('#assignVisitModal').modal('show');
173
 
36716 ranu 174
        $.get(context + '/beatPlan/assignVisit/parties', {
175
            authUserId: avContext.authUserId,
176
            date: avContext.date,
177
            beatId: avContext.beatId
178
        }).done(function (r) {
179
            var data = r.response || r;
180
            // Drop parties already in the beat — they're handled by the beat plan itself,
181
            // Assign Visit is purely for adding extra (non-beat) visits.
182
            avContext.allParties = (data.parties || []).filter(function (p) {
183
                return !p.inBeat;
184
            });
185
            avContext.agendaOptions = data.agendaOptions || [];
186
            if (!data.dtrUserId) {
187
                $('#avPartyList').html('<div style="padding:20px; color:#c62828; text-align:center;">' +
188
                        'This user has no dtr.users record (cannot create visit tasks).</div>');
189
                return;
190
            }
191
            rebuildDefaultAgenda();
192
            renderAvParties(avContext.allParties);
193
        }).fail(function (xhr) {
194
            $('#avPartyList').html('<div style="padding:20px; color:#c62828; text-align:center;">' +
195
                    'Error loading parties: ' + (xhr.responseText || xhr.statusText) + '</div>');
196
        });
36663 ranu 197
    });
198
 
36716 ranu 199
    // Per-row agenda markup. Initialized as bootstrap-multiselect dropdown
200
    // (project standard — same as today-offer.vm, lead.vm, etc.) after render.
201
    // In-beat parties are filtered out of the list entirely, so only non-beat
202
    // (extra-visit) parties show up here. existingAgendas[] is pre-selected for
203
    // parties that already have a saved visit today (so editing existing ones works).
204
    function agendaSelectHtml(p) {
205
        var preset = {};
206
        (p.existingAgendas || []).forEach(function (a) {
207
            preset[a] = true;
208
        });
209
        var html = '<select multiple class="av-party-agenda"'
210
                + ' data-fofoid="' + p.fofoStoreId + '">';
211
        avContext.agendaOptions.forEach(function (opt) {
212
            html += '<option value="' + opt + '"' + (preset[opt] ? ' selected' : '') + '>' + opt + '</option>';
213
        });
214
        html += '</select>';
215
        return html;
216
    }
217
 
218
    var AGENDA_MULTISELECT_OPTS = {
219
        includeSelectAllOption: false,
220
        maxHeight: 220,
221
        buttonWidth: '240px',
222
        numberDisplayed: 2,
223
        nonSelectedText: 'Pick agenda(s)',
224
        nSelectedText: ' agendas',
225
        allSelectedText: 'All agendas',
226
        enableFiltering: true,
227
        enableCaseInsensitiveFiltering: true
228
    };
229
 
230
    function rebuildDefaultAgenda() {
231
        var $sel = $('#avDefaultAgenda');
232
        // Destroy any prior instance before re-initing (re-open of the modal)
233
        try {
234
                $sel.multiselect('destroy');
235
        } catch (e) {
236
        }
237
        var html = '';
238
        avContext.agendaOptions.forEach(function (opt) {
239
            html += '<option value="' + opt + '">' + opt + '</option>';
240
        });
241
            $sel.html(html);
242
        // Build opts as a variable — inline {...} object literal after $.extend()
243
        // confused Velocity's parser ("Encountered { ... was expecting ...").
244
        var defaultOpts = $.extend({}, AGENDA_MULTISELECT_OPTS);
245
        defaultOpts.nonSelectedText = 'Pick agenda(s) to apply';
246
            $sel.multiselect(defaultOpts);
247
    }
248
 
249
    function initRowMultiselects() {
250
        $('.av-party-agenda').each(function () {
251
            var $sel = $(this);
252
            try {
253
                    $sel.multiselect('destroy');
254
            } catch (e) {
255
            }
256
                $sel.multiselect(AGENDA_MULTISELECT_OPTS);
257
        });
258
    }
259
 
36663 ranu 260
    function renderAvParties(list) {
261
        if (!list || list.length === 0) {
262
            $('#avPartyList').html('<div style="padding:20px; color:#999; text-align:center;">No parties found.</div>');
263
            return;
264
        }
265
        var html = '<table class="table table-condensed table-hover" style="margin-bottom:0; font-size:13px;">';
266
        html += '<thead><tr>'
267
                + '<th style="width:30px;"><input type="checkbox" id="avSelectAll" title="Select all"></th>'
268
                + '<th>Code</th><th>Outlet</th><th>City</th>'
36716 ranu 269
                + '<th style="width:260px;">Agenda</th>'
270
                + '<th style="width:260px;">Description</th>'
36663 ranu 271
                + '</tr></thead><tbody>';
272
        list.forEach(function (p) {
273
            var safeName = (p.outletName || '').replace(/"/g, '&quot;');
36716 ranu 274
            var existingDesc = (p.existingDescription || '').replace(/"/g, '&quot;');
36663 ranu 275
            html += '<tr>'
276
                    + '<td><input type="checkbox" class="av-party-chk" '
277
                    + 'data-fofoid="' + p.fofoStoreId + '" '
278
                    + 'data-name="' + safeName + '" '
279
                    + 'data-lat="' + (p.latitude || '') + '" '
280
                    + 'data-lng="' + (p.longitude || '') + '"></td>'
281
                    + '<td>' + (p.code || '') + '</td>'
282
                    + '<td>' + (p.outletName || '') + '</td>'
283
                    + '<td>' + (p.city || '') + '</td>'
36716 ranu 284
                    + '<td>' + agendaSelectHtml(p) + '</td>'
285
                    + '<td><textarea class="form-control input-sm av-party-desc"'
286
                    + ' data-fofoid="' + p.fofoStoreId + '"'
287
                    + ' rows="2" placeholder="Optional notes..."'
288
                    + ' style="resize:vertical; min-height:34px;">' + existingDesc + '</textarea></td>'
36663 ranu 289
                    + '</tr>';
290
        });
291
        html += '</tbody></table>';
292
        $('#avPartyList').html(html);
36716 ranu 293
        initRowMultiselects();
36663 ranu 294
        updateAvSelectedCount();
295
    }
296
 
297
    // Search filter
298
    $(document).on('input', '#avSearch', function () {
299
        var q = $(this).val().toLowerCase().trim();
300
        if (!q) {
301
            renderAvParties(avContext.allParties);
302
            return;
303
        }
304
        var filtered = avContext.allParties.filter(function (p) {
305
            return (p.code || '').toLowerCase().indexOf(q) !== -1
306
                    || (p.outletName || '').toLowerCase().indexOf(q) !== -1
307
                    || (p.city || '').toLowerCase().indexOf(q) !== -1;
308
        });
309
        renderAvParties(filtered);
310
    });
311
 
312
    // Select all toggle
313
    $(document).on('change', '#avSelectAll', function () {
314
        $('.av-party-chk').prop('checked', this.checked);
315
        updateAvSelectedCount();
316
    });
317
 
318
    $(document).on('change', '.av-party-chk', updateAvSelectedCount);
319
 
320
    function updateAvSelectedCount() {
321
        var n = $('.av-party-chk:checked').length;
322
        $('#avSelectedCount').text(n + ' selected');
323
        $('#avSubmit').prop('disabled', n === 0);
324
    }
325
 
36716 ranu 326
    // Apply the default agendas (multi-select) to all currently-selected rows.
327
    // MERGES with each row's existing selections so in-beat rows keep prior agendas.
328
    // After updating <select>.val(), call multiselect('refresh') so the checkbox
329
    // UI re-syncs with the underlying option state.
36663 ranu 330
    $(document).on('click', '#avApplyAgenda', function () {
36716 ranu 331
        var defValues = $('#avDefaultAgenda').val() || [];
332
        if (defValues.length === 0) {
333
            alert('Pick at least one agenda above first');
36663 ranu 334
            return;
335
        }
336
        $('.av-party-chk:checked').each(function () {
337
            var fid = $(this).data('fofoid');
36716 ranu 338
            var $sel = $('.av-party-agenda[data-fofoid="' + fid + '"]');
339
            var existing = $sel.val() || [];
340
            var merged = existing.slice();
341
            defValues.forEach(function (v) {
342
                if (merged.indexOf(v) === -1) merged.push(v);
343
            });
344
                $sel.val(merged);
345
            try {
346
                    $sel.multiselect('refresh');
347
            } catch (e) {
348
            }
36663 ranu 349
        });
350
    });
351
 
352
    // Submit
353
    $(document).on('click', '#avSubmit', function () {
354
        var picks = [];
355
        var missing = 0;
356
        $('.av-party-chk:checked').each(function () {
357
            var fid = parseInt($(this).data('fofoid'));
36716 ranu 358
            var agendas = $('.av-party-agenda[data-fofoid="' + fid + '"]').val() || [];
359
            var description = ($('.av-party-desc[data-fofoid="' + fid + '"]').val() || '').trim();
360
            if (agendas.length === 0) missing++;
36663 ranu 361
            picks.push({
362
                fofoStoreId: fid,
363
                outletName: $(this).data('name'),
364
                latitude: $(this).data('lat') ? String($(this).data('lat')) : null,
365
                longitude: $(this).data('lng') ? String($(this).data('lng')) : null,
36716 ranu 366
                agendas: agendas,
367
                description: description
36663 ranu 368
            });
369
        });
370
        if (picks.length === 0) return;
371
        if (missing > 0) {
372
            if (!confirm(missing + ' selected row(s) have no agenda. Submit anyway with default "Visit"?')) return;
373
        }
374
 
375
        var btn = $(this);
376
        btn.prop('disabled', true).text('Assigning...');
377
        $('#avMsg').text('');
378
 
379
        $.ajax({
380
            url: context + '/beatPlan/assignVisit/submit',
381
            type: 'POST',
382
            contentType: 'application/json',
383
            data: JSON.stringify({
384
                authUserId: avContext.authUserId,
385
                planDate: avContext.date,
386
                parties: picks
387
            }),
388
            success: function (r) {
389
                var d = r.response || r;
36716 ranu 390
                $('#avMsg').html('<span style="color:#2e7d32;">' + (d.message || 'Saved') + '</span>');
391
                btn.prop('disabled', false).text('Save Assignments');
36663 ranu 392
                setTimeout(function () {
393
                    $('#assignVisitModal').modal('hide');
36716 ranu 394
                }, 1500);
36663 ranu 395
            },
396
            error: function (xhr) {
397
                var msg = 'Failed';
398
                try {
399
                    msg = (JSON.parse(xhr.responseText).response.message) || msg;
400
                } catch (e) {
401
                }
402
                $('#avMsg').html('<span style="color:#c62828;">' + msg + '</span>');
36716 ranu 403
                btn.prop('disabled', false).text('Save Assignments');
36663 ranu 404
            }
405
        });
406
    });
36662 ranu 407
</script>
408
 
36663 ranu 409
<!-- Assign Visit Modal -->
410
<div class="modal fade" id="assignVisitModal" tabindex="-1" style="overflow-y:auto;">
411
    <div class="modal-dialog" style="width:90%; max-width:900px; margin-top:30px;">
412
        <div class="modal-content">
413
            <div class="modal-header">
414
                <button type="button" class="close" data-dismiss="modal">&times;</button>
415
                <h4 class="modal-title" id="avTitle">Assign Visit</h4>
416
            </div>
417
            <div class="modal-body">
36716 ranu 418
                <div style="display:flex; gap:10px; align-items:flex-start; margin-bottom:10px;">
419
                    <div style="flex:1;">
420
                        <label style="font-size:11px; color:#666; font-weight:normal; margin-bottom:2px;">
421
                            Agenda - applies to selected rows
422
                        </label>
423
                        <select id="avDefaultAgenda" multiple></select>
424
                    </div>
36663 ranu 425
                    <button type="button" class="btn btn-default btn-sm" id="avApplyAgenda"
36716 ranu 426
                            style="margin-top:18px;"
427
                            title="Add the picked agendas into all selected rows (merges with existing)">
36663 ranu 428
                        Apply to Selected
429
                    </button>
430
                </div>
431
                <div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">
432
                    <input type="text" id="avSearch" class="form-control input-sm"
433
                           placeholder="Search by code, outlet name or city..." style="flex:1;">
434
                    <span id="avSelectedCount" style="font-size:12px; color:#666;">0 selected</span>
435
                </div>
436
                <div id="avPartyList"
437
                     style="max-height:50vh; overflow-y:auto; border:1px solid #e5e5e5; border-radius:4px;"></div>
438
                <div id="avMsg" style="margin-top:10px; font-size:12px;"></div>
439
            </div>
440
            <div class="modal-footer">
441
                <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
36716 ranu 442
                <button type="button" class="btn btn-success" id="avSubmit" disabled>Save Assignments</button>
36663 ranu 443
            </div>
444
        </div>
445
    </div>
446
</div>
447
 
36662 ranu 448
<!-- Calendar modal (iframe loads /beatPlanWindow with auto-selected user) -->
449
<div class="modal fade" id="userCalendarModal" tabindex="-1" style="overflow-y:auto;">
450
    <div class="modal-dialog" style="width:95%; max-width:1300px; margin-top:20px;">
451
        <div class="modal-content">
452
            <div class="modal-header">
453
                <button type="button" class="close" data-dismiss="modal"
454
                        onclick="$('#userCalendarFrame').attr('src','');">&times;
455
                </button>
456
                <h4 class="modal-title" id="userCalendarTitle">Calendar</h4>
457
            </div>
458
            <div class="modal-body" style="padding:0;">
459
                <iframe id="userCalendarFrame" style="width:100%; height:80vh; border:none;"></iframe>
460
            </div>
461
        </div>
462
    </div>
463
</div>