PageSourceSearch

https://harpocrates-publishing.com/static/js/support.js?v=1791305688

js harpocrates-publishing.com collected 2026-10-06 16:54:53 UTC 69,486 bytes, 1,621 lines download raw bytes

1// ============= ФУНКЦИИ СИСТЕМЫ ПОДДЕРЖКИ =============
2let currentTicketId = null;
3let allTickets = [];
4let autoRefreshInterval = null;
5let supportBadgeRefreshInterval = null;
6
7// Обновление badge на кнопке поддержки
8function updateSupportBadge(unreadCount) {
9    const supportTab = document.getElementById('supportAdminTab');
10    if (!supportTab) return;
11
12    if (unreadCount > 0) {
13        supportTab.innerHTML = `💬 Система поддержки <span class="admin-tab-badge">${unreadCount}</span>`;
14    } else {
15        supportTab.innerHTML = '💬 Система поддержки';
16    }
17}
18
19// Загрузка только счётчика непрочитанных (для badge)
20async function loadSupportUnreadCount() {
21    try {
22        const response = await fetch('/api/support/admin/tickets');
23        const data = await response.json();
24
25        if (data.success) {
26            updateSupportBadge(data.unread_count || 0);
27        }
28    } catch (error) {
29        console.error('Ошибка загрузки счётчика поддержки:', error);
30    }
31}
32
33// Запуск автообновления badge поддержки
34function startSupportBadgeRefresh() {
35    loadSupportUnreadCount();
36
37    if (supportBadgeRefreshInterval) {
38        clearInterval(supportBadgeRefreshInterval);
39    }
40    supportBadgeRefreshInterval = setInterval(loadSupportUnreadCount, 30000);
41}
42
43// Остановка автообновления badge поддержки
44function stopSupportBadgeRefresh() {
45    if (supportBadgeRefreshInterval) {
46        clearInterval(supportBadgeRefreshInterval);
47        supportBadgeRefreshInterval = null;
48    }
49}
50
51// Функция показа уведомлений
52function showNotification(message, type = 'info') {
53    // Создаем элемент уведомления
54    const notification = document.createElement('div');
55    notification.className = `notification ${type}`;
56    notification.innerHTML = `
57        <span>${message}</span>
58        <button onclick="this.parentElement.remove()">✕</button>
59    `;
60
61    // Добавляем стили если их еще нет
62    if (!document.getElementById('notificationStyles')) {
63        const style = document.createElement('style');
64        style.id = 'notificationStyles';
65        style.innerHTML = `
66            .notification {
67                position: fixed;
68                top: 20px;
69                right: 20px;
70                padding: 15px 20px;
71                background: white;
72                border-radius: 8px;
73                box-shadow: 0 4px 12px rgba(0,0,0,0.15);
74                display: flex;
75                align-items: center;
76                gap: 10px;
77                z-index: 10000;
78                animation: slideIn 0.3s ease;
79                max-width: 400px;
80            }
81
82            .notification.success {
83                background: linear-gradient(135deg, #4CAF50, #45a049);
84                color: white;
85            }
86
87            .notification.error {
88                background: linear-gradient(135deg, #f44336, #da190b);
89                color: white;
90            }
91
92            .notification.warning {
93                background: linear-gradient(135deg, #ff9800, #f57c00);
94                color: white;
95            }
96
97            .notification button {
98                background: transparent;
99                border: none;
100                color: white;
101                font-size: 20px;
102                cursor: pointer;
103                padding: 0;
104                margin-left: 10px;
105            }
106
107            @keyframes slideIn {
108                from {
109                    transform: translateX(100%);
110                    opacity: 0;
111                }
112                to {
113                    transform: translateX(0);
114                    opacity: 1;
115                }
116            }
117        `;
118        document.head.appendChild(style);
119    }
120
121    // Добавляем на страницу
122    document.body.appendChild(notification);
123
124    // Автоматически удаляем через 10 секунд (увеличено для удобного чтения)
125    setTimeout(() => {
126        notification.remove();
127    }, 10000);
128}
129
130// Загрузка списка тикетов для админа
131async function loadSupportTickets() {
132    try {
133        const response = await fetch('/api/support/admin/tickets');
134        const data = await response.json();
135
136        if (data.success) {
137            allTickets = data.tickets;
138            displaySupportTickets(allTickets);
139            updateSupportStats();
140
141            // Обновляем счетчик непрочитанных с красивым badge
142            updateSupportBadge(data.unread_count || 0);
143        }
144    } catch (error) {
145        console.error('Ошибка загрузки тикетов:', error);
146        showNotification('Ошибка загрузки тикетов', 'error');
147    }
148}
149
150// Отображение тикетов
151function displaySupportTickets(tickets) {
152    const container = document.getElementById('supportTicketsList');
153
154    if (tickets.length === 0) {
155        container.innerHTML = '<div class="no-tickets">Нет тикетов</div>';
156        return;
157    }
158
159    let html = '<div class="tickets-table">';
160    tickets.forEach(ticket => {
161        const statusClass = ticket.status === 'new' ? 'status-new' :
162                          ticket.status === 'closed' ? 'status-closed' :
163                          ticket.status === 'waiting_reply' ? 'status-waiting' : 'status-progress';
164
165        const unreadClass = ticket.unread_by_admin ? 'unread' : '';
166
167        html += `
168            <div class="ticket-row ${unreadClass}" onclick="openTicket(${ticket.id})">
169                <div class="ticket-id">#${ticket.id}</div>
170                <div class="ticket-client">${ticket.client_login}</div>
171                <div class="ticket-subject">${ticket.subject}</div>
172                <div class="ticket-status ${statusClass}">${getStatusText(ticket.status)}</div>
173                <div class="ticket-messages">${ticket.message_count} сообщений</div>
174                <div class="ticket-date">${formatDate(ticket.updated_at)}</div>
175            </div>
176        `;
177    });
178    html += '</div>';
179
180    container.innerHTML = html;
181}
182
183// Фильтрация тикетов
184function filterSupportTickets() {
185    const filter = document.getElementById('supportStatusFilter').value;
186
187    if (!filter) {
188        displaySupportTickets(allTickets);
189    } else {
190        const filtered = allTickets.filter(t => t.status === filter);
191        displaySupportTickets(filtered);
192    }
193}
194
195// Обновление статистики
196function updateSupportStats() {
197    const stats = {
198        new: 0,
199        in_progress: 0,
200        waiting_reply: 0,
201        closed: 0
202    };
203
204    allTickets.forEach(ticket => {
205        if (stats.hasOwnProperty(ticket.status)) {
206            stats[ticket.status]++;
207        }
208    });
209
210    document.getElementById('newTicketsCount').textContent = stats.new;
211    document.getElementById('inProgressCount').textContent = stats.in_progress;
212    document.getElementById('waitingReplyCount').textContent = stats.waiting_reply;
213    document.getElementById('closedCount').textContent = stats.closed;
214}
215
216// Открытие тикета
217async function openTicket(ticketId) {
218    currentTicketId = ticketId;
219
220    try {
221        const response = await fetch(`/api/support/admin/ticket/${ticketId}`);
222        const data = await response.json();
223
224        if (data.success) {
225            // Заполняем заголовок с логином пользователя
226            const clientLogin = data.ticket.client_login || 'guest';
227            document.getElementById('ticketModalTitle').textContent = `Тикет #${ticketId} - ${data.ticket.subject}`;
228            document.getElementById('ticketClient').innerHTML = `👤 <strong>Пользователь: ${clientLogin}</strong>`;
229            document.getElementById('ticketStatus').textContent = `📌 ${getStatusText(data.ticket.status)}`;
230            document.getElementById('ticketDate').textContent = `📅 ${formatDate(data.ticket.created_at)}`;
231
232            // Отображаем сообщения
233            displayTicketMessages(data.messages);
234
235            // Очищаем поле ответа
236            document.getElementById('adminReplyMessage').value = '';
237
238            // Устанавливаем текущий статус
239            document.getElementById('ticketStatusUpdate').value = '';
240
241            // Сначала очищаем блок файлов
242            const adminAttachmentsContainer = document.getElementById('adminAttachmentsList');
243            const adminAttachmentsWrapper = document.getElementById('adminTicketAttachments');
244            if (adminAttachmentsContainer && adminAttachmentsWrapper) {
245                adminAttachmentsContainer.innerHTML = '';
246                adminAttachmentsWrapper.style.display = 'none';
247            }
248
249            // Добавляем обработчик для выбора файла админом (только один раз)
250            const adminFileInput = document.getElementById('adminReplyFile');
251            if (adminFileInput && !adminFileInput.dataset.listenerAttached) {
252                adminFileInput.dataset.listenerAttached = 'true';
253                adminFileInput.addEventListener('change', function(e) {
254                    const file = e.target.files[0];
255                    if (file) {
256                        if (file.size > 10 * 1024 * 1024) {
257                            showNotification('Файл слишком большой (максимум 10 МБ)', 'error');
258                            e.target.value = '';
259                            return;
260                        }
261
262                        const selectedFileDiv = document.getElementById('adminSelectedFile');
263                        const fileName = selectedFileDiv.querySelector('.file-name');
264                        fileName.textContent = `${file.name} (${formatFileSize(file.size)})`;
265                        selectedFileDiv.style.display = 'flex';
266                    }
267                });
268            }
269
270            // Загружаем файлы тикета
271            await loadTicketAttachments(ticketId, 'admin');
272
273            // Показываем модальное окно
274            const modal = document.getElementById('ticketViewModal');
275            modal.style.display = 'flex';
276            modal.classList.add('show');
277
278            // Обновляем список тикетов (чтобы убрать пометку непрочитанного)
279            await loadSupportTickets();
280        }
281    } catch (error) {
282        console.error('Ошибка загрузки тикета:', error);
283        showNotification('Ошибка загрузки тикета', 'error');
284    }
285}
286
287// Отображение сообщений тикета
288function displayTicketMessages(messages) {
289    const container = document.getElementById('ticketMessages');
290
291    let html = '';
292    messages.forEach(msg => {
293        const isAdmin = msg.sender_type === 'admin';
294        html += `
295            <div class="ticket-message ${isAdmin ? 'admin-message' : 'client-message'}">
296                <div class="message-header">
297                    <span class="sender">${msg.sender_name}</span>
298                    <span class="time">${formatDate(msg.created_at)}</span>
299                </div>
300                <div class="message-content">${msg.message}</div>
301            </div>
302        `;
303    });
304
305    container.innerHTML = html;
306    // Прокручиваем к последнему сообщению
307    container.scrollTop = container.scrollHeight;
308}
309
310// Отправка ответа администратора
311// Функция удаления выбранного файла для админа
312function removeAdminFile() {
313    document.getElementById('adminReplyFile').value = '';
314    document.getElementById('adminSelectedFile').style.display = 'none';
315}
316
317async function sendAdminReply() {
318    const message = document.getElementById('adminReplyMessage').value.trim();
319    const newStatus = document.getElementById('ticketStatusUpdate').value;
320    const fileInput = document.getElementById('adminReplyFile');
321    const file = fileInput.files[0];
322
323    if (!message && !newStatus && !file) {
324        showNotification('Введите сообщение, выберите новый статус или прикрепите файл', 'warning');
325        return;
326    }
327
328    try {
329        // Сначала загружаем файл если есть
330        let attachmentId = null;
331        if (file) {
332            const uploadResult = await uploadFileToTicket(currentTicketId, file, 'admin');
333            if (uploadResult.success) {
334                attachmentId = uploadResult.attachment_id;
335            } else {
336                showNotification('Ошибка загрузки файла: ' + uploadResult.message, 'error');
337                return;
338            }
339        }
340
341        // Отправляем сообщение если есть
342        if (message) {
343            const response = await fetch(`/api/support/admin/ticket/${currentTicketId}/reply`, {
344                method: 'POST',
345                headers: {'Content-Type': 'application/json'},
346                body: JSON.stringify({message})
347            });
348
349            const data = await response.json();
350            if (!data.success) {
351                throw new Error(data.message);
352            }
353        }
354
355        // Обновляем статус если выбран
356        if (newStatus) {
357            const statusResponse = await fetch(`/api/support/admin/ticket/${currentTicketId}/status`, {
358                method: 'PUT',
359                headers: {'Content-Type': 'application/json'},
360                body: JSON.stringify({status: newStatus})
361            });
362
363            const statusData = await statusResponse.json();
364            if (!statusData.success) {
365                throw new Error(statusData.message);
366            }
367        }
368
369        showNotification('Ответ отправлен', 'success');
370
371        // Очищаем поля
372        document.getElementById('adminReplyMessage').value = '';
373        document.getElementById('ticketStatusUpdate').value = '';
374        removeAdminFile();
375
376        // Перезагружаем тикет
377        await openTicket(currentTicketId);
378
379    } catch (error) {
380        console.error('Ошибка отправки ответа:', error);
381        showNotification('Ошибка отправки ответа', 'error');
382    }
383}
384
385// Удаление тикета
386async function deleteTicket() {
387    if (!currentTicketId) return;
388
389    if (!confirm('Вы уверены, что хотите удалить этот тикет? Это действие нельзя отменить.')) {
390        return;
391    }
392
393    try {
394        const response = await fetch(`/api/support/admin/ticket/${currentTicketId}/delete`, {
395            method: 'DELETE'
396        });
397
398        const data = await response.json();
399        if (data.success) {
400            showNotification('Тикет успешно удален', 'success');
401            closeTicketModal();
402            await loadSupportTickets();
403        } else {
404            throw new Error(data.message);
405        }
406    } catch (error) {
407        console.error('Ошибка удаления тикета:', error);
408        showNotification('Ошибка удаления тикета', 'error');
409    }
410}
411
412// Закрытие модального окна тикета
413function closeTicketModal() {
414    const modal = document.getElementById('ticketViewModal');
415    modal.classList.remove('show');
416    modal.style.display = 'none';
417}
418
419// Получение текста статуса
420function getStatusText(status) {
421    const statuses = {
422        'new': 'Новый',
423        'in_progress': 'В работе',
424        'waiting_reply': 'Ожидает ответа',
425        'closed': 'Закрыт'
426    };
427    return statuses[status] || status;
428}
429
430// Форматирование даты
431function formatDate(dateString) {
432    if (!dateString) return '';
433
434    // SQLite CURRENT_TIMESTAMP возвращает UTC время
435    // Нужно преобразовать в локальное время
436    // Формат из БД: "2025-10-07 12:30:15" (UTC)
437
438    const parts = dateString.split(' ');
439    if (parts.length !== 2) return dateString;
440
441    const datePart = parts[0].split('-');
442    const timePart = parts[1].split(':');
443
444    if (datePart.length !== 3 || timePart.length !== 3) return dateString;
445
446    // Создаем дату как UTC
447    const date = new Date(Date.UTC(
448        parseInt(datePart[0]), // год
449        parseInt(datePart[1]) - 1, // месяц (0-11)
450        parseInt(datePart[2]), // день
451        parseInt(timePart[0]), // часы
452        parseInt(timePart[1]), // минуты
453        parseInt(timePart[2])  // секунды
454    ));
455
456    const now = new Date();
457    const diff = now - date;
458
459    if (diff < 0) return 'в будущем';
460    if (diff < 60000) return 'только что';
461    if (diff < 3600000) return `${Math.floor(diff/60000)} мин назад`;
462    if (diff < 86400000) return `${Math.floor(diff/3600000)} ч назад`;
463
464    return date.toLocaleDateString('ru-RU') + ' ' + date.toLocaleTimeString('ru-RU', {hour: '2-digit', minute: '2-digit'});
465}
466
467// Функции для пользователей (Support карточка)
468function openSupportModal() {
469    // Проверяем авторизацию
470    if (!app || !app.currentUser) {
471        if (app) {
472            app.showNotification('error', 'Необходимо войти в систему');
473            app.showAuthModal('login');
474        }
475        return;
476    }
477
478    let modal = document.getElementById('supportModal');
479    if (!modal) {
480        // Если модального окна еще нет, создаем его
481        createSupportModal();
482        modal = document.getElementById('supportModal');
483    }
484
485    // Используем WindowManager
486    if (typeof windowManager !== 'undefined') {
487        windowManager.open('supportModal', { title: 'Support 24/7', icon: '🎧' });
488    } else {
489        if (modal) {
490            modal.style.display = 'flex';
491            modal.classList.add('show');
492        }
493    }
494    loadMyTickets();
495}
496
497// Создание модального окна поддержки для пользователей
498function createSupportModal() {
499    const modalHtml = `
500        <div class="modal" id="supportModal">
501            <div class="modal-content">
502                <div class="modal-header">
503                    <h3>💬 Система поддержки</h3>
504                    <button class="modal-close" onclick="closeSupportModal()">&times;</button>
505                </div>
506                <div class="modal-body">
507                    <div class="support-admin-contact" style="background: linear-gradient(135deg, #e3f2fd, #bbdefb); border-radius: 12px; padding: 1rem 1.2rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.8rem; border: 1px solid #90caf9;">
508                        <span style="font-size: 1.5rem;">✈️</span>
509                        <div style="flex: 1;">
510                            <div style="font-weight: 600; color: #1565c0; font-size: 0.9rem;">Telegram админа</div>
511                            <div style="color: #1976d2; font-size: 0.85rem;">@Socora097</div>
512                        </div>
513                        <button onclick="navigator.clipboard.writeText('https://t.me/Socora097').then(() => { this.textContent = '✅ Скопировано!'; setTimeout(() => this.textContent = '📋 Копировать', 2000); })" style="background: #1976d2; color: white; border: none; padding: 0.5rem 1rem; border-radius: 8px; cursor: pointer; font-size: 0.8rem; transition: all 0.3s;">📋 Копировать</button>
514                    </div>
515                    <div class="support-tabs">
516                        <button class="support-tab active" onclick="switchSupportTab('new')">Новое обращение</button>
517                        <button class="support-tab" id="historyTabButton" onclick="switchSupportTab('history')">История обращений</button>
518                    </div>
519
520                    <div id="newTicketTab" class="support-tab-content">
521                        <form id="newTicketForm">
522                            <input type="text" id="ticketSubject" placeholder="Тема обращения" required>
523                            <textarea id="ticketMessage" placeholder="Опишите вашу проблему..." rows="6" required></textarea>
524                            <div class="file-upload-area">
525                                <label for="ticketFile" class="file-upload-label">
526                                    📎 Прикрепить файл (макс. 10 МБ)
527                                </label>
528                                <input type="file" id="ticketFile" class="file-upload-input" accept="*/*">
529                                <div id="selectedFile" class="selected-file" style="display: none;">
530                                    <span class="file-name"></span>
531                                    <button type="button" class="remove-file" onclick="removeSelectedFile()">✕</button>
532                                </div>
533                            </div>
534                            <button type="submit" class="btn-primary">
535                                <span>📤</span>
536                                <span>Отправить</span>
537                            </button>
538                        </form>
539                    </div>
540
541                    <div id="historyTab" class="support-tab-content" style="display: none;">
542                        <div id="myTicketsList">Загрузка...</div>
543                    </div>
544                </div>
545            </div>
546        </div>
547    `;
548
549    document.body.insertAdjacentHTML('beforeend', modalHtml);
550
551    // Добавляем обработчик формы
552    document.getElementById('newTicketForm').addEventListener('submit', async (e) => {
553        e.preventDefault();
554        await createNewTicket();
555    });
556
557    // Добавляем обработчик для выбора файла
558    document.getElementById('ticketFile').addEventListener('change', function(e) {
559        const file = e.target.files[0];
560        if (file) {
561            if (file.size > 10 * 1024 * 1024) {
562                showNotification('Файл слишком большой (максимум 10 МБ)', 'error');
563                e.target.value = '';
564                return;
565            }
566
567            const selectedFileDiv = document.getElementById('selectedFile');
568            const fileName = selectedFileDiv.querySelector('.file-name');
569            fileName.textContent = `${file.name} (${formatFileSize(file.size)})`;
570            selectedFileDiv.style.display = 'flex';
571        }
572    });
573
574    // НЕ показываем модальное окно здесь - это сделает WindowManager или openSupportModal
575}
576
577// Создание нового тикета пользователем
578async function createNewTicket() {
579    const subject = document.getElementById('ticketSubject').value.trim();
580    const message = document.getElementById('ticketMessage').value.trim();
581    const fileInput = document.getElementById('ticketFile');
582    const file = fileInput.files[0];
583
584    if (!subject || !message) {
585        showNotification('Заполните все поля', 'warning');
586        return;
587    }
588
589    try {
590        // Создаем тикет
591        const response = await fetch('/api/support/create-ticket', {
592            method: 'POST',
593            headers: {'Content-Type': 'application/json'},
594            body: JSON.stringify({subject, message})
595        });
596
597        const data = await response.json();
598        if (data.success) {
599            const ticketId = data.ticket_id;
600
601            // Если есть файл, загружаем его
602            if (file) {
603                const uploadResult = await uploadFileToTicket(ticketId, file);
604                if (!uploadResult.success) {
605                    showNotification(`Тикет создан, но файл не загружен: ${uploadResult.message}`, 'warning');
606                } else {
607                    showNotification('Тикет и файл успешно загружены', 'success');
608                }
609            } else {
610                showNotification('Тикет создан успешно', 'success');
611            }
612
613            document.getElementById('newTicketForm').reset();
614            removeSelectedFile();
615            switchSupportTab('history');
616            await loadMyTickets();
617        } else {
618            // Показываем сообщение от сервера
619            showNotification(data.message || 'Ошибка создания тикета', 'error');
620        }
621    } catch (error) {
622        console.error('Ошибка создания тикета:', error);
623        showNotification(error.message || 'Ошибка создания тикета', 'error');
624    }
625}
626
627// Обновление индикатора во вкладке История обращений
628function updateHistoryTabIndicator(hasUnread) {
629    const historyTab = document.getElementById('historyTabButton');
630    if (historyTab) {
631        if (hasUnread) {
632            historyTab.innerHTML = 'История обращений <span class="unread-badge">🔴 Есть ответ</span>';
633        } else {
634            historyTab.innerHTML = 'История обращений';
635        }
636    }
637}
638
639// Переключение вкладок в модальном окне поддержки
640function switchSupportTab(tab) {
641    document.querySelectorAll('.support-tab').forEach(t => t.classList.remove('active'));
642    document.querySelectorAll('.support-tab-content').forEach(c => c.style.display = 'none');
643
644    if (tab === 'new') {
645        document.querySelector('[onclick="switchSupportTab(\'new\')"]').classList.add('active');
646        document.getElementById('newTicketTab').style.display = 'block';
647    } else {
648        document.querySelector('[onclick="switchSupportTab(\'history\')"]').classList.add('active');
649        document.getElementById('historyTab').style.display = 'block';
650        loadMyTickets();
651    }
652}
653
654// Загрузка тикетов пользователя
655async function loadMyTickets() {
656    try {
657        const response = await fetch('/api/support/my-tickets');
658        const data = await response.json();
659
660        if (data.success) {
661            displayMyTickets(data.tickets);
662        }
663    } catch (error) {
664        console.error('Ошибка загрузки тикетов:', error);
665    }
666}
667
668// Отображение тикетов пользователя
669function displayMyTickets(tickets) {
670    const container = document.getElementById('myTicketsList');
671
672    if (tickets.length === 0) {
673        container.innerHTML = '<div class="no-tickets">У вас пока нет обращений</div>';
674        return;
675    }
676
677    // Проверяем есть ли непрочитанные ответы и обновляем вкладку
678    const hasUnread = tickets.some(t => t.unread_by_client);
679    updateHistoryTabIndicator(hasUnread);
680
681    let html = '';
682    tickets.forEach(ticket => {
683        const statusClass = ticket.status === 'new' ? 'status-new' :
684                          ticket.status === 'closed' ? 'status-closed' :
685                          ticket.status === 'waiting_reply' ? 'status-waiting' : 'status-progress';
686
687        html += `
688            <div class="my-ticket" onclick="openMyTicket(${ticket.id})">
689                <div class="ticket-header">
690                    <span class="ticket-subject">${ticket.subject}</span>
691                    <span class="ticket-status ${statusClass}">${getStatusText(ticket.status)}</span>
692                </div>
693                <div class="ticket-info">
694                    <span>${ticket.message_count} сообщений</span>
695                    <span>${formatDate(ticket.updated_at)}</span>
696                </div>
697            </div>
698        `;
699    });
700
701    container.innerHTML = html;
702}
703
704// Открытие тикета пользователя
705async function openMyTicket(ticketId) {
706    try {
707        const response = await fetch(`/api/support/ticket/${ticketId}`);
708        const data = await response.json();
709
710        if (data.success) {
711            // Создаем модальное окно для просмотра тикета пользователем
712            const modalHtml = `
713                <div class="modal" id="userTicketModal">
714                    <div class="modal-content ticket-modal">
715                        <div class="modal-header">
716                            <h3>Тикет #${ticketId} - ${data.ticket.subject}</h3>
717                            <button class="modal-close" onclick="closeUserTicketModal()">&times;</button>
718                        </div>
719                        <div class="modal-body">
720                            <div class="ticket-info">
721                                <span>📌 ${getStatusText(data.ticket.status)}</span>
722                                <span>📅 ${formatDate(data.ticket.created_at)}</span>
723                            </div>
724                            <div class="ticket-messages" id="userTicketMessages">
725                                <!-- Сообщения будут загружены здесь -->
726                            </div>
727                            <div id="userTicketAttachments" class="ticket-attachments" style="display: none;">
728                                <h4>📎 Прикрепленные файлы</h4>
729                                <div id="userAttachmentsList"></div>
730                            </div>
731                            ${data.ticket.status !== 'closed' ? `
732                                <div class="ticket-reply">
733                                    <textarea id="userReplyMessage" placeholder="Введите сообщение..." rows="4"></textarea>
734                                    <div class="file-upload-area">
735                                        <label for="userReplyFile" class="file-upload-label">
736                                            📎 Прикрепить файл (макс. 10 МБ)
737                                        </label>
738                                        <input type="file" id="userReplyFile" class="file-upload-input" accept="*/*">
739                                        <div id="userSelectedFile" class="selected-file" style="display: none;">
740                                            <span class="file-name"></span>
741                                            <button type="button" class="remove-file" onclick="removeUserReplyFile()">×</button>
742                                        </div>
743                                    </div>
744                                    <button class="btn-primary" onclick="sendUserReply(${ticketId})">
745                                        <span>📤</span>
746                                        <span>Отправить</span>
747                                    </button>
748                                </div>
749                            ` : '<div class="ticket-closed-notice">Тикет закрыт</div>'}
750                        </div>
751                    </div>
752                </div>
753            `;
754
755            // Удаляем старое модальное окно если есть
756            const oldModal = document.getElementById('userTicketModal');
757            if (oldModal) {
758                oldModal.remove();
759            }
760
761            // Добавляем новое
762            document.body.insertAdjacentHTML('beforeend', modalHtml);
763
764            // Отображаем сообщения
765            const messagesContainer = document.getElementById('userTicketMessages');
766            let messagesHtml = '';
767            data.messages.forEach(msg => {
768                const isAdmin = msg.sender_type === 'admin';
769                messagesHtml += `
770                    <div class="ticket-message ${isAdmin ? 'admin-message' : 'client-message'}">
771                        <div class="message-header">
772                            <span class="sender">${msg.sender_name}</span>
773                            <span class="time">${formatDate(msg.created_at)}</span>
774                        </div>
775                        <div class="message-content">${msg.message}</div>
776                    </div>
777                `;
778            });
779            messagesContainer.innerHTML = messagesHtml;
780            messagesContainer.scrollTop = messagesContainer.scrollHeight;
781
782            // Загружаем и отображаем файлы
783            await loadTicketAttachments(ticketId, 'user');
784
785            // Добавляем обработчик для выбора файла пользователем
786            const userReplyFileInput = document.getElementById('userReplyFile');
787            if (userReplyFileInput) {
788                userReplyFileInput.addEventListener('change', function(e) {
789                    const file = e.target.files[0];
790                    if (file) {
791                        if (file.size > 10 * 1024 * 1024) {
792                            showNotification('Файл слишком большой (максимум 10 МБ)', 'error');
793                            e.target.value = '';
794                            return;
795                        }
796
797                        const selectedFileDiv = document.getElementById('userSelectedFile');
798                        const fileName = selectedFileDiv.querySelector('.file-name');
799                        fileName.textContent = `${file.name} (${formatFileSize(file.size)})`;
800                        selectedFileDiv.style.display = 'flex';
801                    }
802                });
803            }
804
805            // Показываем модальное окно с WindowManager
806            if (typeof windowManager !== 'undefined') {
807                windowManager.open('userTicketModal', {
808                    title: `Тикет #${ticketId}`,
809                    icon: '📝',
810                    parentId: 'supportModal'
811                });
812            } else {
813                const modal = document.getElementById('userTicketModal');
814                modal.style.display = 'flex';
815                modal.classList.add('show');
816            }
817        }
818    } catch (error) {
819        console.error('Ошибка загрузки тикета:', error);
820        showNotification('Ошибка загрузки тикета', 'error');
821    }
822}
823
824// Отправка ответа пользователя
825async function sendUserReply(ticketId) {
826    const message = document.getElementById('userReplyMessage').value.trim();
827    const fileInput = document.getElementById('userReplyFile');
828    const file = fileInput ? fileInput.files[0] : null;
829
830    if (!message && !file) {
831        showNotification('Введите сообщение или прикрепите файл', 'warning');
832        return;
833    }
834
835    try {
836        // Сначала загружаем файл если есть
837        let attachmentId = null;
838        if (file) {
839            const uploadResult = await uploadFileToTicket(ticketId, file, 'user');
840            if (uploadResult.success) {
841                attachmentId = uploadResult.attachment_id;
842            } else {
843                showNotification('Ошибка загрузки файла: ' + uploadResult.message, 'error');
844                return;
845            }
846        }
847
848        // Отправляем сообщение если есть
849        if (message) {
850            const response = await fetch(`/api/support/ticket/${ticketId}/reply`, {
851                method: 'POST',
852                headers: {'Content-Type': 'application/json'},
853                body: JSON.stringify({message})
854            });
855
856            const data = await response.json();
857            if (!data.success) {
858                throw new Error(data.message);
859            }
860        }
861
862        showNotification('Сообщение отправлено', 'success');
863
864        // Очищаем поля
865        document.getElementById('userReplyMessage').value = '';
866        if (fileInput) {
867            removeUserReplyFile();
868        }
869
870        // Обновляем только сообщения и файлы без перезагрузки модального окна
871        await refreshUserTicketContent(ticketId);
872    } catch (error) {
873        console.error('Ошибка отправки сообщения:', error);
874        showNotification('Ошибка отправки сообщения', 'error');
875    }
876}
877
878// Закрытие модального окна тикета пользователя
879function closeUserTicketModal() {
880    if (typeof windowManager !== 'undefined') {
881        if (windowManager.openWindows.has('userTicketModal') || windowManager.minimizedWindows.has('userTicketModal')) {
882            windowManager.close('userTicketModal');
883            // WindowManager автоматически восстановит supportModal
884            // Удаляем модальное окно из DOM после анимации закрытия
885            const modal = document.getElementById('userTicketModal');
886            if (modal) {
887                setTimeout(() => modal.remove(), 300);
888            }
889            return;
890        }
891    }
892    // Fallback
893    const modal = document.getElementById('userTicketModal');
894    if (modal) {
895        modal.classList.remove('show');
896        modal.remove();
897    }
898}
899
900// Закрытие модального окна поддержки
901function closeSupportModal() {
902    if (typeof windowManager !== 'undefined') {
903        // Проверяем и в открытых, и в свёрнутых окнах
904        if (windowManager.openWindows.has('supportModal') || windowManager.minimizedWindows.has('supportModal')) {
905            windowManager.close('supportModal');
906            return;
907        }
908    }
909    // Fallback если WindowManager не управляет окном
910    const modal = document.getElementById('supportModal');
911    if (modal) {
912        modal.classList.remove('show');
913        modal.style.display = 'none';
914    }
915}
916
917// Функция для удаления выбранного файла
918function removeSelectedFile() {
919    document.getElementById('ticketFile').value = '';
920    document.getElementById('selectedFile').style.display = 'none';
921}
922
923// Функция для удаления выбранного файла в ответе пользователя
924function removeUserReplyFile() {
925    document.getElementById('userReplyFile').value = '';
926    document.getElementById('userSelectedFile').style.display = 'none';
927}
928
929// Форматирование размера файла
930function formatFileSize(bytes) {
931    if (bytes === 0) return '0 Bytes';
932    const k = 1024;
933    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
934    const i = Math.floor(Math.log(bytes) / Math.log(k));
935    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
936}
937
938// Загрузка файла в тикет
939async function uploadFileToTicket(ticketId, file, userType = 'user') {
940    const formData = new FormData();
941    formData.append('file', file);
942
943    const endpoint = userType === 'admin'
944        ? `/api/support/admin/upload-file/${ticketId}`
945        : `/api/support/upload-file/${ticketId}`;
946
947    try {
948        const response = await fetch(endpoint, {
949            method: 'POST',
950            body: formData
951        });
952
953        const data = await response.json();
954        return data;
955    } catch (error) {
956        console.error('Error uploading file:', error);
957        return { success: false, message: 'Ошибка загрузки файла' };
958    }
959}
960
961// Загрузка списка файлов тикета
962async function loadTicketAttachments(ticketId, userType = 'user') {
963    try {
964        const endpoint = userType === 'admin'
965            ? `/api/support/admin/ticket/${ticketId}/attachments`
966            : `/api/support/ticket/${ticketId}/attachments`;
967
968        const response = await fetch(endpoint);
969        const data = await response.json();
970
971        const containerId = userType === 'admin' ? 'adminAttachmentsList' : 'userAttachmentsList';
972        const wrapperID = userType === 'admin' ? 'adminTicketAttachments' : 'userTicketAttachments';
973
974        const container = document.getElementById(containerId);
975        const wrapper = document.getElementById(wrapperID);
976
977        if (!container || !wrapper) return;
978
979        if (data.success && data.attachments && data.attachments.length > 0) {
980            displayTicketAttachments(data.attachments, userType);
981        } else {
982            // Скрываем блок если нет файлов
983            container.innerHTML = '';
984            wrapper.style.display = 'none';
985        }
986    } catch (error) {
987        console.error('Error loading attachments:', error);
988        // Скрываем блок при ошибке
989        const containerId = userType === 'admin' ? 'adminAttachmentsList' : 'userAttachmentsList';
990        const wrapperID = userType === 'admin' ? 'adminTicketAttachments' : 'userTicketAttachments';
991
992        const container = document.getElementById(containerId);
993        const wrapper = document.getElementById(wrapperID);
994
995        if (container && wrapper) {
996            container.innerHTML = '';
997            wrapper.style.display = 'none';
998        }
999    }
1000}
1001
1002// Отображение файлов тикета
1003function displayTicketAttachments(attachments, userType = 'user') {
1004    const containerId = userType === 'admin' ? 'adminAttachmentsList' : 'userAttachmentsList';
1005    const wrapperID = userType === 'admin' ? 'adminTicketAttachments' : 'userTicketAttachments';
1006
1007    const container = document.getElementById(containerId);
1008    const wrapper = document.getElementById(wrapperID);
1009
1010    if (!container || !wrapper) return;
1011
1012    let html = '';
1013    attachments.forEach(attachment => {
1014        const fileIcon = getFileIcon(attachment.file_type);
1015        const uploadedBy = attachment.uploaded_by === 'admin' ? 'Администратор' : 'Клиент';
1016
1017        html += `
1018            <div class="attachment-item">
1019                <div class="attachment-info">
1020                    <span class="attachment-icon">${fileIcon}</span>
1021                    <div class="attachment-details">
1022                        <div class="attachment-name">${attachment.original_filename}</div>
1023                        <div class="attachment-meta">
1024                            ${formatFileSize(attachment.file_size)} • ${uploadedBy} • ${formatDate(attachment.uploaded_at)}
1025                        </div>
1026                    </div>
1027                </div>
1028                <button class="attachment-download" onclick="downloadAttachment(${attachment.id})">
1029                    Скачать
1030                </button>
1031            </div>
1032        `;
1033    });
1034
1035    container.innerHTML = html;
1036    wrapper.style.display = 'block';
1037}
1038
1039// Получение иконки файла по типу
1040function getFileIcon(fileType) {
1041    if (!fileType) return '📄';
1042
1043    if (fileType.startsWith('image/')) return '🖼️';
1044    if (fileType.startsWith('video/')) return '🎥';
1045    if (fileType.startsWith('audio/')) return '🎵';
1046    if (fileType.includes('pdf')) return '📕';
1047    if (fileType.includes('word') || fileType.includes('document')) return '📘';
1048    if (fileType.includes('excel') || fileType.includes('spreadsheet')) return '📗';
1049    if (fileType.includes('zip') || fileType.includes('rar') || fileType.includes('archive')) return '📦';
1050
1051    return '📄';
1052}
1053
1054// Скачивание файла
1055function downloadAttachment(attachmentId) {
1056    window.open(`/api/support/download/${attachmentId}`, '_blank');
1057}
1058
1059// ============= ФУНКЦИИ АВТООБНОВЛЕНИЯ =============
1060
1061// Запуск автообновления
1062function startAutoRefresh() {
1063    // Останавливаем предыдущий интервал если есть
1064    if (autoRefreshInterval) {
1065        clearInterval(autoRefreshInterval);
1066    }
1067
1068    // Запускаем новый интервал каждые 5 секунд
1069    autoRefreshInterval = setInterval(() => {
1070        // Обновляем админ тикеты если находимся в админ интерфейсе
1071        if (document.getElementById('supportTicketsList')) {
1072            loadSupportTickets();
1073        }
1074
1075        // Обновляем пользовательские тикеты если находимся в пользовательском интерфейсе
1076        if (document.getElementById('myTicketsList')) {
1077            loadMyTickets();
1078        }
1079    }, 5000);
1080}
1081
1082// Остановка автообновления
1083function stopAutoRefresh() {
1084    if (autoRefreshInterval) {
1085        clearInterval(autoRefreshInterval);
1086        autoRefreshInterval = null;
1087    }
1088}
1089
1090// Автоматический запуск автообновления при загрузке страницы
1091document.addEventListener('DOMContentLoaded', function() {
1092    // Запускаем автообновление если есть элементы поддержки на странице
1093    if (document.getElementById('supportTicketsList') || document.getElementById('myTicketsList')) {
1094        startAutoRefresh();
1095    }
1096
1097    // Обновляем статус карточек на главной странице при загрузке
1098    if (document.querySelector('.service-card')) {
1099        updateMainPageCards();
1100    }
1101});
1102
1103// Остановка автообновления при закрытии/переходе со страницы
1104window.addEventListener('beforeunload', function() {
1105    stopAutoRefresh();
1106});
1107
1108// Обновление содержимого пользовательского модального окна без перезагрузки
1109async function refreshUserTicketContent(ticketId) {
1110    try {
1111        const response = await fetch(`/api/support/ticket/${ticketId}`);
1112        const data = await response.json();
1113
1114        if (data.success) {
1115            const ticket = data.ticket;
1116            const messages = data.messages;
1117
1118            // Обновляем сообщения
1119            const messagesContainer = document.getElementById('userTicketMessages');
1120            if (messagesContainer) {
1121                let messagesHtml = '';
1122                messages.forEach(message => {
1123                    const messageClass = message.sender_type === 'client' ? 'client-message' : 'admin-message';
1124                    messagesHtml += `
1125                        <div class="ticket-message ${messageClass}">
1126                            <div class="message-header">
1127                                <span class="sender">${message.sender_name}</span>
1128                                <span class="time">${formatDate(message.created_at)}</span>
1129                            </div>
1130                            <div class="message-content">${message.message}</div>
1131                        </div>
1132                    `;
1133                });
1134                messagesContainer.innerHTML = messagesHtml;
1135            }
1136
1137            // Обновляем файлы
1138            await loadTicketAttachments(ticketId, 'user');
1139        }
1140    } catch (error) {
1141        console.error('Ошибка обновления содержимого тикета:', error);
1142    }
1143}
1144
1145// ============= ФУНКЦИИ УПРАВЛЕНИЯ ПОЛЬЗОВАТЕЛЯМИ =============
1146
1147// Глобальная переменная для хранения состояния блокировок
1148let clientBlockStates = {};
1149
1150// Загрузка списка клиентов для админа
1151async function loadClientsForSupport() {
1152    try {
1153        const response = await fetch('/api/support/admin/clients');
1154        const data = await response.json();
1155
1156        if (data.success) {
1157            // Всегда обновляем состояние данными с сервера
1158            data.clients.forEach(client => {
1159                clientBlockStates[client.id] = Boolean(client.support_blocked);
1160            });
1161            displayClientsForSupport(data.clients);
1162        } else {
1163            throw new Error(data.message);
1164        }
1165    } catch (error) {
1166        console.error('Ошибка загрузки клиентов:', error);
1167        showNotification('Ошибка загрузки клиентов', 'error');
1168    }
1169}
1170
1171// Отображение списка клиентов
1172function displayClientsForSupport(clients) {
1173    const container = document.getElementById('clientsList');
1174
1175    if (clients.length === 0) {
1176        container.innerHTML = '<div class="no-tickets">Нет зарегистрированных пользователей</div>';
1177        return;
1178    }
1179
1180    let html = `
1181        <div class="clients-header">
1182            <div>Email</div>
1183            <div>
1183Дата регистрации</div>
1184            <div>Статус</div>
1185            <div>Блокировка тикетов</div>
1186        </div>
1187    `;
1188
1189    clients.forEach(client => {
1190        const activeClass = client.is_active ? 'status-active' : 'status-inactive';
1191        const activeText = client.is_active ? 'Активен' : 'Неактивен';
1192
1193        // Используем состояние из кеша вместо данных с сервера
1194        const isBlocked = clientBlockStates[client.id] || false;
1195        const blockedClass = isBlocked ? 'blocked' : '';
1196
1197        html += `
1198            <div class="client-item ${blockedClass}" data-client-id="${client.id}">
1199                <div class="client-email">${client.email}</div>
1200                <div class="client-date">${formatDate(client.registration_date)}</div>
1201                <div class="client-status">
1202                    <span class="${activeClass}">${activeText}</span>
1203                </div>
1204                <div class="block-toggle">
1205                    <label class="toggle-switch">
1206                        <input type="checkbox" ${isBlocked ? 'checked' : ''}
1207                               onchange="toggleClientBlock('${client.id}', this.checked)">
1208                        <span class="toggle-slider"></span>
1209                    </label>
1210                </div>
1211            </div>
1212        `;
1213    });
1214
1215    container.innerHTML = html;
1216}
1217
1218// Переключение блокировки создания тикетов
1219async function toggleClientBlock(clientId, isBlocked) {
1220    try {
1221        const response = await fetch(`/api/support/admin/client/${clientId}/toggle-support-block`, {
1222            method: 'POST',
1223            headers: {'Content-Type': 'application/json'},
1224            body: JSON.stringify({block_status: isBlocked})
1225        });
1226
1227        const data = await response.json();
1228
1229        if (data.success) {
1230            const action = isBlocked ? 'заблокирован' : 'разблокирован';
1231            showNotification(`Пользователь ${action}`, 'success');
1232
1233            // Обновляем состояние в кеше
1234            clientBlockStates[clientId] = isBlocked;
1235
1236            // Обновляем класс блокировки для клиента без перезагрузки всего списка
1237            const clientItem = document.querySelector(`[data-client-id="${clientId}"]`);
1238            if (clientItem) {
1239                if (isBlocked) {
1240                    clientItem.classList.add('blocked');
1241                } else {
1242                    clientItem.classList.remove('blocked');
1243                }
1244            }
1245        } else {
1246            throw new Error(data.message);
1247        }
1248    } catch (error) {
1249        console.error('Ошибка переключения блокировки:', error);
1250        showNotification('Ошибка переключения блокировки', 'error');
1251
1252        // Возвращаем чекбокс в предыдущее состояние
1253        const clientItem = document.querySelector(`[data-client-id="${clientId}"]`);
1254        if (clientItem) {
1255            const checkbox = clientItem.querySelector('input[type="checkbox"]');
1256            if (checkbox) {
1257                checkbox.checked = !isBlocked;
1258            }
1259        }
1260    }
1261}
1262
1263// ============= УПРАВЛЕНИЕ КАРТОЧКАМИ =============
1264
1265// Маппинг иконок для карточек
1266const cardIcons = {
1267    'email_generation': '📧',
1268    'pdf_generation': '📄',
1269    'photo_generation': '📸',
1270    'checking_numbers': '📱',
1271    'bookmaker_shop': '🎰',
1272    'support': '💬',
1273    'phone_control': '📱',
1274    'proxy': '🌐',
1275    'software': '💻'
1276};
1277
1278// Загрузка списка карточек для управления
1279async function loadCardsManagement() {
1280    try {
1281        const response = await fetch('/api/admin/cards');
1282        const data = await response.json();
1283
1284        if (data.success) {
1285            console.log('DEBUG: Cards data from API:', data.cards);
1286            const container = document.getElementById('cardsManagementList');
1287
1288            if (data.cards.length === 0) {
1289                container.innerHTML = '<div class="no-data">Нет карточек для управления</div>';
1290                return;
1291            }
1292
1293            let html = '';
1294            data.cards.forEach(card => {
1295                console.log('DEBUG: Processing card:', card);
1296                const icon = cardIcons[card.card_id] || '🔲';
1297                const statusClass = card.is_enabled ? 'enabled' : 'disabled';
1298                const statusText = card.is_enabled ? 'Включена' : 'Отключена';
1299                const maintenanceClass = card.is_maintenance ? 'maintenance-enabled' : 'maintenance-disabled';
1300                const maintenanceText = card.is_maintenance ? 'Включено' : 'Отключено';
1301                const hiddenClass = card.is_hidden ? 'hidden-enabled' : 'hidden-disabled';
1302                const hiddenText = card.is_hidden ? 'Скрыто' : 'Видимо';
1303
1304                html += `
1305                    <div class="card-management-item" data-card-id="${card.card_id}" draggable="true"
1306                         ondragstart="handleDragStart(event)"
1307                         ondragover="handleDragOver(event)"
1308                         ondrop="handleDrop(event)"
1309                         ondragend="handleDragEnd(event)">
1310                        <div class="card-drag-handle" title="Перетащите для изменения порядка">⋮⋮</div>
1311                        <div class="card-management-icon">${icon}</div>
1312                        <div class="card-management-info">
1313                            <div class="card-management-name">${card.card_name}</div>
1314                            <div class="card-management-status ${statusClass}">
1315                                ${statusText}
1316                            </div>
1317                        </div>
1318                        <div class="card-toggle-wrapper">
1319                            <span class="card-toggle-label">${card.is_enabled ? 'Включена' : 'Отключена'}</span>
1320                            <label class="toggle-switch">
1321                                <input type="checkbox" ${card.is_enabled ? 'checked' : ''}
1322                                       onchange="toggleCardStatus('${card.card_id}', this.checked)">
1323                                <span class="toggle-slider"></span>
1324                            </label>
1325                        </div>
1326                        <div class="card-toggle-wrapper">
1327                            <span class="card-toggle-label card-maintenance-label ${maintenanceClass}">${maintenanceText}</span>
1328                            <label class="toggle-switch">
1329                                <input type="checkbox" ${card.is_maintenance ? 'checked' : ''}
1330                                       onchange="toggleMaintenanceStatus('${card.card_id}', this.checked)">
1331                                <span class="toggle-slider"></span>
1332                            </label>
1333                        </div>
1334                        <div class="card-toggle-wrapper">
1335                            <span class="card-toggle-label card-hidden-label ${hiddenClass}">${hiddenText}</span>
1336                            <label class="toggle-switch">
1337                                <input type="checkbox" ${card.is_hidden ? 'checked' : ''}
1338                                       onchange="toggleHiddenStatus('${card.card_id}', this.checked)">
1339                                <span class="toggle-slider"></span>
1340                            </label>
1341                        </div>
1342                    </div>
1343                `;
1344            });
1345
1346            container.innerHTML = html;
1347        } else {
1348            showNotification('Ошибка загрузки карточек', 'error');
1349        }
1350    } catch (error) {
1351        console.error('Ошибка загрузки карточек:', error);
1352        showNotification('Ошибка загрузки карточек', 'error');
1353    }
1354}
1355
1356// Переключение статуса карточки
1357async function toggleCardStatus(cardId, isEnabled) {
1358    try {
1359        const response = await fetch(`/api/admin/cards/${cardId}/toggle`, {
1360            method: 'POST',
1361            headers: {'Content-Type': 'application/json'},
1362            body: JSON.stringify({is_enabled: isEnabled})
1363        });
1364
1365        const data = await response.json();
1366
1367        if (data.success) {
1368            const action = isEnabled ? 'включена' : 'отключена';
1369            showNotification(`Карточка ${action}`, 'success');
1370
1371            // Обновляем статус в UI без перезагрузки
1372            const cardItem = document.querySelector(`[data-card-id="${cardId}"]`);
1373            if (cardItem) {
1374                const statusElement = cardItem.querySelector('.card-management-status');
1375                const labelElement = cardItem.querySelector('.card-toggle-label');
1376
1377                if (statusElement && labelElement) {
1378                    if (isEnabled) {
1379                        statusElement.classList.remove('disabled');
1380                        statusElement.classList.add('enabled');
1381                        statusElement.textContent = 'Включена';
1382                        labelElement.textContent = 'Включена';
1383                    } else {
1384                        statusElement.classList.remove('enabled');
1385                        statusElement.classList.add('disabled');
1386                        statusElement.textContent = 'Отключена';
1387                        labelElement.textContent = 'Отключена';
1388                    }
1389                }
1390            }
1391
1392            // Обновляем состояние карточек на главной странице
1393            if (typeof updateMainPageCards === 'function') {
1394                await updateMainPageCards();
1395            }
1396        } else {
1397            throw new Error(data.message);
1398        }
1399    } catch (error) {
1400        console.error('Ошибка переключения карточки:', error);
1401        showNotification('Ошибка переключения карточки', 'error');
1402
1403        // Возвращаем чекбокс в предыдущее состояние
1404        const cardItem = document.querySelector(`[data-card-id="${cardId}"]`);
1405        if (cardItem) {
1406            const checkbox = cardItem.querySelector('input[type="checkbox"]');
1407            if (checkbox) {
1408                checkbox.checked = !isEnabled;
1409            }
1410        }
1411    }
1412}
1413
1414// Переключение статуса техобслуживания карточки
1415async function toggleMaintenanceStatus(cardId, isMaintenance) {
1416    try {
1417        const response = await fetch(`/api/admin/cards/${cardId}/maintenance`, {
1418            method: 'POST',
1419            headers: {'Content-Type': 'application/json'},
1420            body: JSON.stringify({is_maintenance: isMaintenance})
1421        });
1422
1423        const data = await response.json();
1424
1425        if (data.success) {
1426            const action = isMaintenance ? 'включено' : 'отключено';
1427            showNotification(`Тех. обслуживание ${action}`, 'success');
1428
1429            // Обновляем статус в UI без перезагрузки
1430            const cardItem = document.querySelector(`[data-card-id="${cardId}"]`);
1431            if (cardItem) {
1432                const labels = cardItem.querySelectorAll('.card-maintenance-label');
1433                labels.forEach(label => {
1434                    if (isMaintenance) {
1435                        label.classList.remove('maintenance-disabled');
1436                        label.classList.add('maintenance-enabled');
1437                        label.textContent = 'Включено';
1438                    } else {
1439                        label.classList.remove('maintenance-enabled');
1440                        label.classList.add('maintenance-disabled');
1441                        label.textContent = 'Отключено';
1442                    }
1443                });
1444            }
1445
1446            // Обновляем состояние карточек на главной странице
1447            updateMainPageCards();
1448        } else {
1449            throw new Error(data.message);
1450        }
1451    } catch (error) {
1452        console.error('Ошибка переключения тех.обслуживания:', error);
1453        showNotification('Ошибка переключения тех.обслуживания', 'error');
1454
1455        // Возвращаем чекбокс в предыдущее состояние
1456        const cardItem = document.querySelector(`[data-card-id="${cardId}"]`);
1457        if (cardItem) {
1458            const checkboxes = cardItem.querySelectorAll('input[type="checkbox"]');
1459            if (checkboxes[1]) {
1460                checkboxes[1].checked = !isMaintenance;
1461            }
1462        }
1463    }
1464}
1465
1466// Переключение скрытия карточки от не-админов
1467async function toggleHiddenStatus(cardId, isHidden) {
1468    try {
1469        const response = await fetch(`/api/admin/cards/${cardId}/hidden`, {
1470            method: 'POST',
1471            headers: {'Content-Type': 'application/json'},
1472            body: JSON.stringify({is_hidden: isHidden})
1473        });
1474
1475        const data = await response.json();
1476
1477        if (data.success) {
1478            const action = isHidden ? 'скрыта' : 'видима';
1479            showNotification(`Карточка ${action} для пользователей`, 'success');
1480
1481            // Обновляем подпись в UI без перезагрузки
1482            const cardItem = document.querySelector(`[data-card-id="${cardId}"]`);
1483            if (cardItem) {
1484                const labels = cardItem.querySelectorAll('.card-hidden-label');
1485                labels.forEach(label => {
1486                    if (isHidden) {
1487                        label.classList.remove('hidden-disabled');
1488                        label.classList.add('hidden-enabled');
1489                        label.textContent = 'Скрыто';
1490                    } else {
1491                        label.classList.remove('hidden-enabled');
1492                        label.classList.add('hidden-disabled');
1493                        label.textContent = 'Видимо';
1494                    }
1495                });
1496            }
1497
1498            if (typeof updateMainPageCards === 'function') {
1499                await updateMainPageCards();
1500            }
1501        } else {
1502            throw new Error(data.message);
1503        }
1504    } catch (error) {
1505        console.error('Ошибка переключения видимости:', error);
1506        showNotification('Ошибка переключения видимости', 'error');
1507
1508        // Возвращаем чекбокс в предыдущее состояние
1509        const cardItem = document.querySelector(`[data-card-id="${cardId}"]`);
1510        if (cardItem) {
1511            const checkboxes = cardItem.querySelectorAll('input[type="checkbox"]');
1512            if (checkboxes[2]) {
1513                checkboxes[2].checked = !isHidden;
1514            }
1515        }
1516    }
1517}
1518
1519// Обновление состояния карточек на главной странице
1520// ============= DRAG AND DROP ДЛЯ КАРТОЧЕК =============
1521
1522let draggedCard = null;
1523
1524function handleDragStart(e) {
1525    draggedCard = e.target.closest('.card-management-item');
1526    e.dataTransfer.effectAllowed = 'move';
1527    e.dataTransfer.setData('text/html', draggedCard.innerHTML);
1528
1529    // Добавляем визуальный эффект
1530    setTimeout(() => {
1531        draggedCard.style.opacity = '0.5';
1532    }, 0);
1533}
1534
1535function handleDragOver(e) {
1536    e.preventDefault();
1537    e.dataTransfer.dropEffect = 'move';
1538
1539    const container = document.getElementById('cardsManagementList');
1540    const afterElement = getDragAfterElement(container, e.clientY);
1541
1542    if (afterElement == null) {
1543        container.appendChild(draggedCard);
1544    } else {
1545        container.insertBefore(draggedCard, afterElement);
1546    }
1547
1548    return false;
1549}
1550
1551function handleDrop(e) {
1552    e.preventDefault();
1553    if (e.stopPropagation) {
1554        e.stopPropagation();
1555    }
1556
1557    // Сохраняем новый порядок
1558    saveCardsOrder();
1559
1560    return false;
1561}
1562
1563function handleDragEnd(e) {
1564    draggedCard.style.opacity = '1';
1565    draggedCard = null;
1566}
1567
1568function getDragAfterElement(container, y) {
1569    const draggableElements = [...container.querySelectorAll('.card-management-item:not(.dragging)')];
1570
1571    return draggableElements.reduce((closest, child) => {
1572        const box = child.getBoundingClientRect();
1573        const offset = y - box.top - box.height / 2;
1574
1575        if (offset < 0 && offset > closest.offset) {
1576            return { offset: offset, element: child };
1577        } else {
1578            return closest;
1579        }
1580    }, { offset: Number.NEGATIVE_INFINITY }).element;
1581}
1582
1583// Сохранение порядка карточек
1584async function saveCardsOrder() {
1585    const container = document.getElementById('cardsManagementList');
1586    const cards = container.querySelectorAll('.card-management-item');
1587
1588    const order = [];
1589    cards.forEach((card, index) => {
1590        order.push({
1591            card_id: card.dataset.cardId,
1592            order: index + 1
1593        });
1594    });
1595
1596    try {
1597        const response = await fetch('/api/admin/cards/reorder', {
1598            method: 'POST',
1599            headers: {
1600                'Content-Type': 'application/json'
1601            },
1602            body: JSON.stringify({ order: order })
1603        });
1604
1605        const data = await response.json();
1606
1607        if (data.success) {
1608            showNotification('Порядок карточек обновлен', 'success');
1609
1610            // Обновляем главную страницу
1611            if (typeof updateMainPageCards === 'function') {
1612                await updateMainPageCards();
1613            }
1614        } else {
1615            showNotification('Ошибка обновления порядка', 'error');
1616        }
1617    } catch (error) {
1618        console.error('❌ Error saving cards order:', error);
1619        showNotification('Ошибка сохранения порядка', 'error');
1620    }
1621}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.