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()">×</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()">×</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.