| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- $(document).ready(function () {
- function initMobileDatePicker() {
- const mobileQuery = window.matchMedia('(max-width: 767.98px)');
- const coarsePointer = window.matchMedia('(pointer: coarse)');
- if (!mobileQuery.matches || !coarsePointer.matches) {
- return;
- }
- const dateInputs = Array.from(document.querySelectorAll('input[type="date"]')).filter(function (input) {
- return !input.disabled && !input.dataset.mobileDateEnhanced;
- });
- if (!dateInputs.length) {
- return;
- }
- const monthFormatter = new Intl.DateTimeFormat('ru-RU', {
- month: 'long',
- year: 'numeric',
- });
- const displayFormatter = new Intl.DateTimeFormat('ru-RU');
- const weekdayLabels = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
- const overlay = document.createElement('div');
- overlay.className = 'mobile-date-picker';
- overlay.hidden = true;
- overlay.innerHTML = '' +
- '<div class="mobile-date-picker__dialog" role="dialog" aria-modal="true" aria-label="Выбор даты">' +
- '<div class="mobile-date-picker__header">' +
- '<button type="button" class="btn btn-link mobile-date-picker__close" aria-label="Закрыть"><i class="bi bi-x-lg"></i></button>' +
- '<div class="mobile-date-picker__title"></div>' +
- '<button type="button" class="btn btn-link mobile-date-picker__today">Сегодня</button>' +
- '</div>' +
- '<div class="mobile-date-picker__month-nav">' +
- '<button type="button" class="btn btn-link mobile-date-picker__nav mobile-date-picker__nav--prev" aria-label="Предыдущий месяц"><i class="bi bi-chevron-left"></i></button>' +
- '<div class="mobile-date-picker__month-label"></div>' +
- '<button type="button" class="btn btn-link mobile-date-picker__nav mobile-date-picker__nav--next" aria-label="Следующий месяц"><i class="bi bi-chevron-right"></i></button>' +
- '</div>' +
- '<div class="mobile-date-picker__weekdays"></div>' +
- '<div class="mobile-date-picker__grid"></div>' +
- '<div class="mobile-date-picker__footer">' +
- '<button type="button" class="btn btn-outline-secondary mobile-date-picker__clear">Очистить</button>' +
- '<button type="button" class="btn btn-primary mobile-date-picker__apply">Готово</button>' +
- '</div>' +
- '</div>';
- document.body.appendChild(overlay);
- const titleEl = overlay.querySelector('.mobile-date-picker__title');
- const monthLabelEl = overlay.querySelector('.mobile-date-picker__month-label');
- const weekdaysEl = overlay.querySelector('.mobile-date-picker__weekdays');
- const gridEl = overlay.querySelector('.mobile-date-picker__grid');
- const closeBtn = overlay.querySelector('.mobile-date-picker__close');
- const todayBtn = overlay.querySelector('.mobile-date-picker__today');
- const clearBtn = overlay.querySelector('.mobile-date-picker__clear');
- const applyBtn = overlay.querySelector('.mobile-date-picker__apply');
- const prevBtn = overlay.querySelector('.mobile-date-picker__nav--prev');
- const nextBtn = overlay.querySelector('.mobile-date-picker__nav--next');
- weekdaysEl.innerHTML = weekdayLabels.map(function (day) {
- return '<div class="mobile-date-picker__weekday">' + day + '</div>';
- }).join('');
- function parseIsoDate(value) {
- if (!value || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
- return null;
- }
- const parts = value.split('-').map(Number);
- const date = new Date(parts[0], parts[1] - 1, parts[2]);
- if (Number.isNaN(date.getTime())) {
- return null;
- }
- return date;
- }
- function formatIsoDate(date) {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- return year + '-' + month + '-' + day;
- }
- function formatDisplayDate(value) {
- const date = parseIsoDate(value);
- return date ? displayFormatter.format(date) : '';
- }
- function normalizeMonthLabel(date) {
- const label = monthFormatter.format(date);
- return label.charAt(0).toUpperCase() + label.slice(1);
- }
- function startOfMonth(date) {
- return new Date(date.getFullYear(), date.getMonth(), 1);
- }
- function isSameDate(a, b) {
- return a && b
- && a.getFullYear() === b.getFullYear()
- && a.getMonth() === b.getMonth()
- && a.getDate() === b.getDate();
- }
- const state = {
- activeInput: null,
- hiddenInput: null,
- displayInput: null,
- selectedDate: null,
- draftDate: null,
- viewDate: startOfMonth(new Date()),
- };
- function syncDisplay(hiddenInput, displayInput) {
- displayInput.value = formatDisplayDate(hiddenInput.value);
- displayInput.classList.toggle('mobile-date-input--empty', !hiddenInput.value);
- }
- function closePicker() {
- overlay.hidden = true;
- document.body.classList.remove('mobile-date-picker-open');
- state.activeInput = null;
- state.hiddenInput = null;
- state.displayInput = null;
- state.selectedDate = null;
- state.draftDate = null;
- }
- function renderCalendar() {
- const today = new Date();
- const monthStart = startOfMonth(state.viewDate);
- const gridStart = new Date(monthStart);
- const firstWeekday = (monthStart.getDay() + 6) % 7;
- gridStart.setDate(monthStart.getDate() - firstWeekday);
- monthLabelEl.textContent = normalizeMonthLabel(monthStart);
- gridEl.innerHTML = '';
- for (let index = 0; index < 42; index += 1) {
- const day = new Date(gridStart);
- day.setDate(gridStart.getDate() + index);
- const button = document.createElement('button');
- button.type = 'button';
- button.className = 'mobile-date-picker__day';
- button.textContent = String(day.getDate());
- button.dataset.value = formatIsoDate(day);
- if (day.getMonth() !== monthStart.getMonth()) {
- button.classList.add('is-outside');
- }
- if (isSameDate(day, today)) {
- button.classList.add('is-today');
- }
- if (state.draftDate && isSameDate(day, state.draftDate)) {
- button.classList.add('is-selected');
- }
- button.addEventListener('click', function () {
- state.draftDate = parseIsoDate(button.dataset.value);
- renderCalendar();
- });
- gridEl.appendChild(button);
- }
- }
- function openPicker(hiddenInput, displayInput) {
- state.activeInput = hiddenInput;
- state.hiddenInput = hiddenInput;
- state.displayInput = displayInput;
- state.selectedDate = parseIsoDate(hiddenInput.value);
- state.draftDate = state.selectedDate;
- state.viewDate = startOfMonth(state.selectedDate || new Date());
- titleEl.textContent = hiddenInput.dataset.mobileDateTitle || 'Выбор даты';
- overlay.hidden = false;
- document.body.classList.add('mobile-date-picker-open');
- renderCalendar();
- }
- closeBtn.addEventListener('click', closePicker);
- overlay.addEventListener('click', function (event) {
- if (event.target === overlay) {
- closePicker();
- }
- });
- prevBtn.addEventListener('click', function () {
- state.viewDate = new Date(state.viewDate.getFullYear(), state.viewDate.getMonth() - 1, 1);
- renderCalendar();
- });
- nextBtn.addEventListener('click', function () {
- state.viewDate = new Date(state.viewDate.getFullYear(), state.viewDate.getMonth() + 1, 1);
- renderCalendar();
- });
- todayBtn.addEventListener('click', function () {
- state.draftDate = startOfMonth(new Date());
- state.draftDate = new Date(state.draftDate.getFullYear(), state.draftDate.getMonth(), new Date().getDate());
- state.viewDate = startOfMonth(state.draftDate);
- renderCalendar();
- });
- clearBtn.addEventListener('click', function () {
- if (!state.hiddenInput || !state.displayInput) {
- return;
- }
- state.hiddenInput.value = '';
- state.hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
- state.hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
- syncDisplay(state.hiddenInput, state.displayInput);
- closePicker();
- });
- applyBtn.addEventListener('click', function () {
- if (!state.hiddenInput || !state.displayInput || !state.draftDate) {
- closePicker();
- return;
- }
- state.hiddenInput.value = formatIsoDate(state.draftDate);
- state.hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
- state.hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
- syncDisplay(state.hiddenInput, state.displayInput);
- closePicker();
- });
- document.addEventListener('keydown', function (event) {
- if (event.key === 'Escape' && !overlay.hidden) {
- closePicker();
- }
- });
- dateInputs.forEach(function (input) {
- input.dataset.mobileDateEnhanced = '1';
- input.dataset.mobileDateTitle = input.closest('.row')?.querySelector('label[for="' + input.id + '"]')?.textContent.trim() || 'Выбор даты';
- const displayInput = input.cloneNode(false);
- displayInput.type = 'text';
- displayInput.removeAttribute('name');
- displayInput.value = '';
- displayInput.readOnly = true;
- displayInput.dataset.mobileDateDisplay = '1';
- displayInput.classList.add('mobile-date-input');
- displayInput.placeholder = 'Выберите дату';
- input.type = 'hidden';
- input.dataset.mobileDateHidden = '1';
- input.parentNode.insertBefore(displayInput, input.nextSibling);
- syncDisplay(input, displayInput);
- displayInput.addEventListener('click', function () {
- openPicker(input, displayInput);
- });
- });
- }
- function cleanupStaleModalState() {
- if (!document.querySelector('.modal.show')) {
- document.querySelectorAll('.modal-backdrop').forEach(function (backdrop) {
- backdrop.remove();
- });
- document.body.classList.remove('modal-open');
- document.body.style.removeProperty('padding-right');
- document.body.style.removeProperty('overflow');
- }
- }
- function getNotificationBadge() {
- return document.getElementById('notification-badge');
- }
- function updateNotificationBadge(count) {
- const badge = getNotificationBadge();
- if (!badge) {
- return;
- }
- const safeCount = Math.max(0, parseInt(count || 0, 10));
- badge.dataset.count = String(safeCount);
- badge.classList.toggle('d-none', safeCount === 0);
- }
- function incrementNotificationBadge() {
- const badge = getNotificationBadge();
- if (!badge) {
- return;
- }
- const current = parseInt(badge.dataset.count || '0', 10);
- updateNotificationBadge(current + 1);
- }
- function markNotificationRead(notificationId) {
- if (!notificationId || !window.notificationsReadUrlTemplate) {
- return;
- }
- fetch(window.notificationsReadUrlTemplate.replace('__id__', String(notificationId)), {
- method: 'POST',
- headers: {
- 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
- 'Accept': 'application/json',
- },
- keepalive: true,
- })
- .then(function (response) {
- if (!response.ok) {
- return null;
- }
- return response.json();
- })
- .then(function (data) {
- if (data && typeof data.unread !== 'undefined') {
- updateNotificationBadge(data.unread);
- }
- })
- .catch(function () {
- // noop
- });
- }
- function appendWsPopup(notification) {
- const container = document.getElementById('ws-notification-container');
- if (!container || !notification) {
- return;
- }
- const popup = document.createElement('div');
- popup.className = 'ws-notification-popup';
- popup.dataset.id = String(notification.id || '');
- const safeTitle = notification.title || 'Уведомление';
- const bodyHtml = notification.message_html || notification.message || '';
- popup.innerHTML =
- '<div class="ws-notification-header">' +
- '<strong>' + safeTitle + '</strong>' +
- '<button type="button" class="btn-close btn-sm ws-notification-close" aria-label="Закрыть"></button>' +
- '</div>' +
- '<div class="ws-notification-body">' + bodyHtml + '</div>';
- const closeBtn = popup.querySelector('.ws-notification-close');
- if (closeBtn) {
- closeBtn.addEventListener('click', function (event) {
- event.stopPropagation();
- markNotificationRead(notification.id);
- popup.remove();
- });
- }
- container.appendChild(popup);
- }
- cleanupStaleModalState();
- initMobileDatePicker();
- window.addEventListener('pageshow', cleanupStaleModalState);
- if ($('.main-alert').length) {
- setTimeout(function () {
- $('.main-alert').fadeTo(2000, 500).slideUp(500, function () {
- $('.main-alert').slideUp(500);
- });
- }, 3000);
- }
- const user = localStorage.getItem('user');
- if (user > 0) {
- const socket = new WebSocket(localStorage.getItem('socketAddress'));
- socket.onopen = function () {
- console.log('[WS] Connected. Listen messages for user ' + user);
- };
- socket.onmessage = function (event) {
- const received = JSON.parse(event.data);
- if (parseInt(received.data.user_id, 10) !== parseInt(user, 10)) {
- return;
- }
- const action = received.data.action;
- if (action === 'persistent_notification') {
- incrementNotificationBadge();
- appendWsPopup(received.data.notification);
- return;
- }
- if (action !== 'message') {
- return;
- }
- if (received.data.payload.download) {
- document.location.href = '/storage/export/' + received.data.payload.download;
- }
- if (received.data.payload.link) {
- document.location.href = received.data.payload.link;
- setTimeout(function () {
- document.location.reload();
- }, 2000);
- }
- setTimeout(function () {
- if (received.data.payload.error) {
- $('.alerts').append('<div class="main-alert2 alert alert-danger" role="alert">' + received.data.message + '</div>');
- } else {
- $('.alerts').append('<div class="main-alert2 alert alert-success" role="alert">' + received.data.message + '</div>');
- }
- setTimeout(function () {
- $('.main-alert2').fadeTo(2000, 500).slideUp(500, function () {
- $('.main-alert2').slideUp(500);
- });
- }, 3000);
- }, 1000);
- };
- socket.onclose = function (event) {
- if (event.wasClean) {
- console.log('[WS] Closed clear, code=' + event.code + ' reason=' + event.reason);
- } else {
- console.log('[WS] Connection lost', event);
- }
- };
- socket.onerror = function (error) {
- console.log('[error]', error);
- };
- }
- });
|