Subversion Repositories SmartDukaan

Rev

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

Rev 36663 Rev 36716
Line 90... Line 90...
90
    function renderDayViewRows(rows) {
90
    function renderDayViewRows(rows) {
91
        if (!rows || rows.length === 0) {
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>');
92
            $('#dayViewRows').html('<tr><td colspan="8" style="text-align:center; color:#999;">No beats scheduled in this range.</td></tr>');
93
            return;
93
            return;
94
        }
94
        }
-
 
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
        })();
95
        var html = '';
102
        var html = '';
96
        rows.forEach(function (r) {
103
        rows.forEach(function (r) {
-
 
104
            var isToday = r.scheduleDate === todayStr;
97
            html += '<tr>'
105
            html += '<tr>'
98
                    + '<td>' + r.userName + '</td>'
106
                    + '<td>' + r.userName + '</td>'
99
                    + '<td><span class="label label-primary">' + r.scheduleDate + '</span></td>'
107
                    + '<td><span class="label label-primary">' + r.scheduleDate + '</span></td>'
100
                    + '<td><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:' + (r.beatColor || '#3498db') + ';margin-right:6px;"></span>' + r.beatName + '</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>'
101
                    + '<td>' + r.dayNumber + '</td>'
109
                    + '<td>' + r.dayNumber + '</td>'
Line 104... Line 112...
104
                    + '<td><strong>' + r.visitCount + '</strong></td>'
112
                    + '<td><strong>' + r.visitCount + '</strong></td>'
105
                    + '<td>'
113
                    + '<td>'
106
                    + '<button class="btn btn-xs btn-info view-user-calendar" data-userid="' + r.authUserId + '" data-username="' + r.userName + '">View Calendar</button> '
114
                    + '<button class="btn btn-xs btn-info view-user-calendar" data-userid="' + r.authUserId + '" data-username="' + r.userName + '">View Calendar</button> '
107
                    + '<button class="btn btn-xs btn-warning edit-beat-on-date" '
115
                    + '<button class="btn btn-xs btn-warning edit-beat-on-date" '
108
                    + 'data-userid="' + r.authUserId + '" data-username="' + r.userName + '" '
116
                    + 'data-userid="' + r.authUserId + '" data-username="' + r.userName + '" '
109
                    + 'data-beatid="' + r.beatId + '" data-date="' + r.scheduleDate + '">Edit</button> '
117
                    + 'data-beatid="' + r.beatId + '" data-date="' + r.scheduleDate + '">Edit</button>'
-
 
118
                    + (isToday
110
                    + '<button class="btn btn-xs btn-success assign-visit-btn" '
119
                            ? ' <button class="btn btn-xs btn-success assign-visit-btn"'
111
                    + 'data-userid="' + r.authUserId + '" data-username="' + r.userName + '" '
120
                            + ' data-userid="' + r.authUserId + '" data-username="' + r.userName + '"'
-
 
121
                            + ' data-beatid="' + r.beatId + '"'
112
                    + 'data-date="' + r.scheduleDate + '">Assign Visit</button>'
122
                            + ' data-date="' + r.scheduleDate + '">Assign Visit</button>'
-
 
123
                            : '')
113
                    + '</td>'
124
                    + '</td>'
114
                    + '</tr>';
125
                    + '</tr>';
115
        });
126
        });
116
        $('#dayViewRows').html(html);
127
        $('#dayViewRows').html(html);
117
    }
128
    }
Line 140... Line 151...
140
        $('#userCalendarFrame').attr('src', url);
151
        $('#userCalendarFrame').attr('src', url);
141
        $('#userCalendarModal').modal('show');
152
        $('#userCalendarModal').modal('show');
142
    });
153
    });
143
 
154
 
144
    // ---------- ASSIGN VISIT ----------
155
    // ---------- ASSIGN VISIT ----------
145
    var avContext = {authUserId: null, userName: '', date: '', allParties: []};
156
    var avContext = {authUserId: null, userName: '', date: '', beatId: null, allParties: [], agendaOptions: []};
146
 
157
 
147
    $(document).on('click', '.assign-visit-btn', function () {
158
    $(document).on('click', '.assign-visit-btn', function () {
148
        avContext.authUserId = $(this).data('userid');
159
        avContext.authUserId = $(this).data('userid');
149
        avContext.userName = $(this).data('username') || '';
160
        avContext.userName = $(this).data('username') || '';
150
        avContext.date = $(this).data('date');
161
        avContext.date = $(this).data('date');
-
 
162
        avContext.beatId = $(this).data('beatid');
151
        avContext.allParties = [];
163
        avContext.allParties = [];
152
 
164
 
153
        $('#avTitle').text('Assign Visit — ' + avContext.userName + ' on ' + avContext.date);
165
        $('#avTitle').text('Assign Visit - ' + avContext.userName + ' on ' + avContext.date);
154
        $('#avSearch').val('');
166
        $('#avSearch').val('');
-
 
167
        // The default-agenda multiselect is rebuilt (and re-initialized) below in rebuildDefaultAgenda
155
        $('#avPartyList').html('<div style="padding:20px; color:#999; text-align:center;">Loading parties...</div>');
168
        $('#avPartyList').html('<div style="padding:20px; color:#999; text-align:center;">Loading parties...</div>');
156
        $('#avMsg').text('');
169
        $('#avMsg').text('');
157
        $('#avSelectedCount').text('0 selected');
170
        $('#avSelectedCount').text('0 selected');
158
        $('#avSubmit').prop('disabled', true);
171
        $('#avSubmit').prop('disabled', true);
159
        $('#assignVisitModal').modal('show');
172
        $('#assignVisitModal').modal('show');
160
 
173
 
161
        $.get(context + '/beatPlan/assignVisit/parties', {authUserId: avContext.authUserId})
174
        $.get(context + '/beatPlan/assignVisit/parties', {
-
 
175
            authUserId: avContext.authUserId,
-
 
176
            date: avContext.date,
-
 
177
            beatId: avContext.beatId
162
                .done(function (r) {
178
        }).done(function (r) {
163
                    var data = r.response || 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
            });
164
                    avContext.allParties = data.parties || [];
185
            avContext.agendaOptions = data.agendaOptions || [];
165
                    if (!data.dtrUserId) {
186
            if (!data.dtrUserId) {
166
                        $('#avPartyList').html('<div style="padding:20px; color:#c62828; text-align:center;">' +
187
                $('#avPartyList').html('<div style="padding:20px; color:#c62828; text-align:center;">' +
167
                                'This user has no dtr.users record (cannot create visit tasks).</div>');
188
                        'This user has no dtr.users record (cannot create visit tasks).</div>');
168
                        return;
189
                return;
169
                    }
190
            }
-
 
191
            rebuildDefaultAgenda();
170
                    renderAvParties(avContext.allParties);
192
            renderAvParties(avContext.allParties);
171
                })
-
 
172
                .fail(function (xhr) {
193
        }).fail(function (xhr) {
173
                    $('#avPartyList').html('<div style="padding:20px; color:#c62828; text-align:center;">' +
194
            $('#avPartyList').html('<div style="padding:20px; color:#c62828; text-align:center;">' +
174
                            'Error loading parties: ' + (xhr.responseText || xhr.statusText) + '</div>');
195
                    'Error loading parties: ' + (xhr.responseText || xhr.statusText) + '</div>');
175
                });
196
        });
176
    });
197
    });
177
 
198
 
-
 
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
 
178
    function renderAvParties(list) {
260
    function renderAvParties(list) {
179
        if (!list || list.length === 0) {
261
        if (!list || list.length === 0) {
180
            $('#avPartyList').html('<div style="padding:20px; color:#999; text-align:center;">No parties found.</div>');
262
            $('#avPartyList').html('<div style="padding:20px; color:#999; text-align:center;">No parties found.</div>');
181
            return;
263
            return;
182
        }
264
        }
183
        var html = '<table class="table table-condensed table-hover" style="margin-bottom:0; font-size:13px;">';
265
        var html = '<table class="table table-condensed table-hover" style="margin-bottom:0; font-size:13px;">';
184
        html += '<thead><tr>'
266
        html += '<thead><tr>'
185
                + '<th style="width:30px;"><input type="checkbox" id="avSelectAll" title="Select all"></th>'
267
                + '<th style="width:30px;"><input type="checkbox" id="avSelectAll" title="Select all"></th>'
186
                + '<th>Code</th><th>Outlet</th><th>City</th>'
268
                + '<th>Code</th><th>Outlet</th><th>City</th>'
187
                + '<th style="width:220px;">Agenda</th>'
269
                + '<th style="width:260px;">Agenda</th>'
-
 
270
                + '<th style="width:260px;">Description</th>'
188
                + '</tr></thead><tbody>';
271
                + '</tr></thead><tbody>';
189
        list.forEach(function (p) {
272
        list.forEach(function (p) {
190
            var safeName = (p.outletName || '').replace(/"/g, '&quot;');
273
            var safeName = (p.outletName || '').replace(/"/g, '&quot;');
-
 
274
            var existingDesc = (p.existingDescription || '').replace(/"/g, '&quot;');
191
            html += '<tr>'
275
            html += '<tr>'
192
                    + '<td><input type="checkbox" class="av-party-chk" '
276
                    + '<td><input type="checkbox" class="av-party-chk" '
193
                    + 'data-fofoid="' + p.fofoStoreId + '" '
277
                    + 'data-fofoid="' + p.fofoStoreId + '" '
194
                    + 'data-name="' + safeName + '" '
278
                    + 'data-name="' + safeName + '" '
195
                    + 'data-lat="' + (p.latitude || '') + '" '
279
                    + 'data-lat="' + (p.latitude || '') + '" '
196
                    + 'data-lng="' + (p.longitude || '') + '"></td>'
280
                    + 'data-lng="' + (p.longitude || '') + '"></td>'
197
                    + '<td>' + (p.code || '') + '</td>'
281
                    + '<td>' + (p.code || '') + '</td>'
198
                    + '<td>' + (p.outletName || '') + '</td>'
282
                    + '<td>' + (p.outletName || '') + '</td>'
199
                    + '<td>' + (p.city || '') + '</td>'
283
                    + '<td>' + (p.city || '') + '</td>'
-
 
284
                    + '<td>' + agendaSelectHtml(p) + '</td>'
200
                    + '<td><input type="text" class="form-control input-sm av-party-agenda" '
285
                    + '<td><textarea class="form-control input-sm av-party-desc"'
201
                    + 'placeholder="e.g. Stock review" '
286
                    + ' data-fofoid="' + p.fofoStoreId + '"'
202
                    + 'data-fofoid="' + p.fofoStoreId + '"></td>'
287
                    + ' rows="2" placeholder="Optional notes..."'
-
 
288
                    + ' style="resize:vertical; min-height:34px;">' + existingDesc + '</textarea></td>'
203
                    + '</tr>';
289
                    + '</tr>';
204
        });
290
        });
205
        html += '</tbody></table>';
291
        html += '</tbody></table>';
206
        $('#avPartyList').html(html);
292
        $('#avPartyList').html(html);
-
 
293
        initRowMultiselects();
207
        updateAvSelectedCount();
294
        updateAvSelectedCount();
208
    }
295
    }
209
 
296
 
210
    // Search filter
297
    // Search filter
211
    $(document).on('input', '#avSearch', function () {
298
    $(document).on('input', '#avSearch', function () {
Line 226... Line 313...
226
    $(document).on('change', '#avSelectAll', function () {
313
    $(document).on('change', '#avSelectAll', function () {
227
        $('.av-party-chk').prop('checked', this.checked);
314
        $('.av-party-chk').prop('checked', this.checked);
228
        updateAvSelectedCount();
315
        updateAvSelectedCount();
229
    });
316
    });
230
 
317
 
231
    // Per-row checkbox
-
 
232
    $(document).on('change', '.av-party-chk', updateAvSelectedCount);
318
    $(document).on('change', '.av-party-chk', updateAvSelectedCount);
233
 
319
 
234
    function updateAvSelectedCount() {
320
    function updateAvSelectedCount() {
235
        var n = $('.av-party-chk:checked').length;
321
        var n = $('.av-party-chk:checked').length;
236
        $('#avSelectedCount').text(n + ' selected');
322
        $('#avSelectedCount').text(n + ' selected');
237
        $('#avSubmit').prop('disabled', n === 0);
323
        $('#avSubmit').prop('disabled', n === 0);
238
    }
324
    }
239
 
325
 
240
    // Apply the default agenda value to all currently-selected rows
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.
241
    $(document).on('click', '#avApplyAgenda', function () {
330
    $(document).on('click', '#avApplyAgenda', function () {
242
        var def = $('#avDefaultAgenda').val();
331
        var defValues = $('#avDefaultAgenda').val() || [];
243
        if (!def) {
332
        if (defValues.length === 0) {
244
            alert('Type an agenda first');
333
            alert('Pick at least one agenda above first');
245
            return;
334
            return;
246
        }
335
        }
247
        $('.av-party-chk:checked').each(function () {
336
        $('.av-party-chk:checked').each(function () {
248
            var fid = $(this).data('fofoid');
337
            var fid = $(this).data('fofoid');
249
            $('.av-party-agenda[data-fofoid="' + fid + '"]').val(def);
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
            }
250
        });
349
        });
251
    });
350
    });
252
 
351
 
253
    // Submit
352
    // Submit
254
    $(document).on('click', '#avSubmit', function () {
353
    $(document).on('click', '#avSubmit', function () {
255
        var picks = [];
354
        var picks = [];
256
        var missing = 0;
355
        var missing = 0;
257
        $('.av-party-chk:checked').each(function () {
356
        $('.av-party-chk:checked').each(function () {
258
            var fid = parseInt($(this).data('fofoid'));
357
            var fid = parseInt($(this).data('fofoid'));
259
            var agenda = $('.av-party-agenda[data-fofoid="' + fid + '"]').val();
358
            var agendas = $('.av-party-agenda[data-fofoid="' + fid + '"]').val() || [];
260
            agenda = agenda ? agenda.trim() : '';
359
            var description = ($('.av-party-desc[data-fofoid="' + fid + '"]').val() || '').trim();
261
            if (!agenda) missing++;
360
            if (agendas.length === 0) missing++;
262
            picks.push({
361
            picks.push({
263
                fofoStoreId: fid,
362
                fofoStoreId: fid,
264
                outletName: $(this).data('name'),
363
                outletName: $(this).data('name'),
265
                latitude: $(this).data('lat') ? String($(this).data('lat')) : null,
364
                latitude: $(this).data('lat') ? String($(this).data('lat')) : null,
266
                longitude: $(this).data('lng') ? String($(this).data('lng')) : null,
365
                longitude: $(this).data('lng') ? String($(this).data('lng')) : null,
267
                agenda: agenda
366
                agendas: agendas,
-
 
367
                description: description
268
            });
368
            });
269
        });
369
        });
270
        if (picks.length === 0) return;
370
        if (picks.length === 0) return;
271
        if (missing > 0) {
371
        if (missing > 0) {
272
            if (!confirm(missing + ' selected row(s) have no agenda. Submit anyway with default "Visit"?')) return;
372
            if (!confirm(missing + ' selected row(s) have no agenda. Submit anyway with default "Visit"?')) return;
Line 285... Line 385...
285
                planDate: avContext.date,
385
                planDate: avContext.date,
286
                parties: picks
386
                parties: picks
287
            }),
387
            }),
288
            success: function (r) {
388
            success: function (r) {
289
                var d = r.response || r;
389
                var d = r.response || r;
290
                $('#avMsg').html('<span style="color:#2e7d32;">' + (d.message || 'Assigned successfully') + '</span>');
390
                $('#avMsg').html('<span style="color:#2e7d32;">' + (d.message || 'Saved') + '</span>');
291
                btn.prop('disabled', false).text('Assign Selected');
391
                btn.prop('disabled', false).text('Save Assignments');
292
                setTimeout(function () {
392
                setTimeout(function () {
293
                    $('#assignVisitModal').modal('hide');
393
                    $('#assignVisitModal').modal('hide');
294
                }, 1200);
394
                }, 1500);
295
            },
395
            },
296
            error: function (xhr) {
396
            error: function (xhr) {
297
                var msg = 'Failed';
397
                var msg = 'Failed';
298
                try {
398
                try {
299
                    msg = (JSON.parse(xhr.responseText).response.message) || msg;
399
                    msg = (JSON.parse(xhr.responseText).response.message) || msg;
300
                } catch (e) {
400
                } catch (e) {
301
                }
401
                }
302
                $('#avMsg').html('<span style="color:#c62828;">' + msg + '</span>');
402
                $('#avMsg').html('<span style="color:#c62828;">' + msg + '</span>');
303
                btn.prop('disabled', false).text('Assign Selected');
403
                btn.prop('disabled', false).text('Save Assignments');
304
            }
404
            }
305
        });
405
        });
306
    });
406
    });
307
</script>
407
</script>
308
 
408
 
Line 313... Line 413...
313
            <div class="modal-header">
413
            <div class="modal-header">
314
                <button type="button" class="close" data-dismiss="modal">&times;</button>
414
                <button type="button" class="close" data-dismiss="modal">&times;</button>
315
                <h4 class="modal-title" id="avTitle">Assign Visit</h4>
415
                <h4 class="modal-title" id="avTitle">Assign Visit</h4>
316
            </div>
416
            </div>
317
            <div class="modal-body">
417
            <div class="modal-body">
318
                <div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">
418
                <div style="display:flex; gap:10px; align-items:flex-start; margin-bottom:10px;">
-
 
419
                    <div style="flex:1;">
319
                    <input type="text" id="avDefaultAgenda" class="form-control input-sm"
420
                        <label style="font-size:11px; color:#666; font-weight:normal; margin-bottom:2px;">
320
                           placeholder="Agenda (applies to selected rows)" style="flex:1;">
421
                            Agenda - applies to selected rows
-
 
422
                        </label>
-
 
423
                        <select id="avDefaultAgenda" multiple></select>
-
 
424
                    </div>
321
                    <button type="button" class="btn btn-default btn-sm" id="avApplyAgenda"
425
                    <button type="button" class="btn btn-default btn-sm" id="avApplyAgenda"
-
 
426
                            style="margin-top:18px;"
322
                            title="Fill agenda into all selected rows">
427
                            title="Add the picked agendas into all selected rows (merges with existing)">
323
                        Apply to Selected
428
                        Apply to Selected
324
                    </button>
429
                    </button>
325
                </div>
430
                </div>
326
                <div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">
431
                <div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">
327
                    <input type="text" id="avSearch" class="form-control input-sm"
432
                    <input type="text" id="avSearch" class="form-control input-sm"
Line 332... Line 437...
332
                     style="max-height:50vh; overflow-y:auto; border:1px solid #e5e5e5; border-radius:4px;"></div>
437
                     style="max-height:50vh; overflow-y:auto; border:1px solid #e5e5e5; border-radius:4px;"></div>
333
                <div id="avMsg" style="margin-top:10px; font-size:12px;"></div>
438
                <div id="avMsg" style="margin-top:10px; font-size:12px;"></div>
334
            </div>
439
            </div>
335
            <div class="modal-footer">
440
            <div class="modal-footer">
336
                <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
441
                <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
337
                <button type="button" class="btn btn-success" id="avSubmit" disabled>Assign Selected</button>
442
                <button type="button" class="btn btn-success" id="avSubmit" disabled>Save Assignments</button>
338
            </div>
443
            </div>
339
        </div>
444
        </div>
340
    </div>
445
    </div>
341
</div>
446
</div>
342
 
447