$(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 = '' +
'
' +
'' +
'
' +
'
' +
'
' +
'' +
'
';
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 '' + day + '
';
}).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 =
'' +
'' + bodyHtml + '
';
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('' + received.data.message + '
');
} else {
$('.alerts').append('' + received.data.message + '
');
}
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);
};
}
});