38916-vm/ticket.php
Flatlogic Bot fd8a2de90a z
2026-03-01 18:23:38 +00:00

264 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
require_once __DIR__ . '/auth.php';
$user = requireAuth();
$ticketId = $_GET['id'] ?? null;
if (!$ticketId) {
header('Location: index.php');
exit;
}
// Fetch ticket
$stmt = db()->prepare("SELECT t.*, u.username as creator_name, h.username as helper_name FROM tickets t JOIN users u ON t.user_id = u.id LEFT JOIN users h ON t.helper_id = h.id WHERE t.id = ?");
$stmt->execute([$ticketId]);
$ticket = $stmt->fetch();
if (!$ticket) {
die('Тикет не найден.');
}
// Check access
if ($user['role'] === 'user' && $ticket['user_id'] != $user['id']) {
die('Доступ запрещен.');
}
// Fetch helpers for curator
$helpers = [];
if ($user['role'] === 'curator') {
$helpers = db()->query("SELECT id, username FROM users WHERE role IN ('helper', 'curator')")->fetchAll();
}
function getStatusLabel($status) {
$labels = [
'open' => 'Открыт',
'in_progress' => 'В работе',
'awaiting_response' => 'Ожидает ответа',
'closed' => 'Закрыт'
];
return $labels[$status] ?? $status;
}
function getPriorityLabel($priority) {
$labels = [
'low' => 'Низкий',
'medium' => 'Средний',
'high' => 'Высокий'
];
return $labels[$priority] ?? $priority;
}
?>
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Тикет #<?= $ticket['id'] ?> - <?= htmlspecialchars($ticket['title']) ?></title>
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div class="bg-animations">
<div class="blob blob-1"></div>
<div class="blob blob-2"></div>
<div class="blob blob-3"></div>
</div>
<div class="main-wrapper">
<nav class="navbar">
<a href="index.php" class="logo" style="text-decoration: none;">SupportSystem</a>
<div class="user-info">
<span><?= htmlspecialchars($user['username']) ?> (<?= $user['role'] ?>)</span>
<a href="logout.php" class="logout-link">Выйти</a>
</div>
</nav>
<div class="ticket-detail-header" style="background-color: var(--card-bg); padding: 2rem; border-radius: 1rem; border: 1px solid var(--border-color);">
<div style="display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 1.5rem;">
<div style="flex: 1; min-width: 300px;">
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 0.5rem;">
<div class="ticket-badge badge-<?= $ticket['status'] ?>">
<?= getStatusLabel($ticket['status']) ?>
</div>
<span style="color: var(--text-secondary); font-size: 0.875rem;">Категория: <?= htmlspecialchars($ticket['category'] ?: 'Не указана') ?></span>
</div>
<h1>#<?= $ticket['id'] ?> <?= htmlspecialchars($ticket['title']) ?></h1>
<p class="text-secondary" style="margin-top: 1rem; font-size: 1.125rem;"><?= nl2br(htmlspecialchars($ticket['description'])) ?></p>
</div>
<div style="min-width: 250px; background: var(--bg-dark); padding: 1.5rem; border-radius: 0.75rem; border: 1px solid var(--border-color);">
<div class="form-group">
<label>Приоритет</label>
<?php if ($user['role'] !== 'user'): ?>
<select onchange="updateTicket({priority: this.value})" style="padding: 0.5rem;">
<option value="low" <?= $ticket['priority'] === 'low' ? 'selected' : '' ?>>Низкий</option>
<option value="medium" <?= $ticket['priority'] === 'medium' ? 'selected' : '' ?>>Средний</option>
<option value="high" <?= $ticket['priority'] === 'high' ? 'selected' : '' ?>>Высокий</option>
</select>
<?php else: ?>
<div style="font-weight: 600; color: var(--primary);"><?= getPriorityLabel($ticket['priority']) ?></div>
<?php endif; ?>
</div>
<?php if ($user['role'] === 'curator'): ?>
<div class="form-group">
<label>Ответственный</label>
<select onchange="updateTicket({helper_id: this.value})" style="padding: 0.5rem;">
<option value="0">Не назначен</option>
<?php foreach ($helpers as $h): ?>
<option value="<?= $h['id'] ?>" <?= $ticket['helper_id'] == $h['id'] ? 'selected' : '' ?>><?= htmlspecialchars($h['username']) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php elseif ($ticket['helper_name']): ?>
<div class="form-group" style="margin-bottom: 0;">
<label>Ответственный</label>
<div style="color: var(--text-primary);"><?= htmlspecialchars($ticket['helper_name']) ?></div>
</div>
<?php endif; ?>
</div>
</div>
<div style="margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
<div class="ticket-meta" style="margin-top: 0;">
<span>Создано: <?= date('d.m.Y H:i', strtotime($ticket['created_at'])) ?></span>
<span>От: <?= htmlspecialchars($ticket['creator_name']) ?></span>
</div>
<div style="display: flex; gap: 0.5rem;">
<?php if ($user['role'] !== 'user' && $ticket['status'] !== 'closed'): ?>
<?php if ($ticket['status'] === 'open'): ?>
<button class="btn-primary" onclick="updateTicket({status: 'in_progress'})" style="width: auto; padding: 0.5rem 1rem;">В работу</button>
<?php endif; ?>
<button class="btn-primary" onclick="updateTicket({status: 'awaiting_response'})" style="width: auto; padding: 0.5rem 1rem; background-color: var(--warning);">Ожидать ответа</button>
<?php endif; ?>
<?php if ($ticket['status'] !== 'closed'): ?>
<button class="btn-primary" onclick="updateTicket({status: 'closed'})" style="width: auto; padding: 0.5rem 1rem; background-color: var(--error);">Закрыть тикет</button>
<?php endif; ?>
</div>
</div>
</div>
<div class="chat-box" style="margin-top: 2rem;">
<div class="chat-messages" id="chat-messages">
<!-- Messages will be loaded here via JS -->
</div>
<?php if ($ticket['status'] !== 'closed'): ?>
<div class="chat-input-area">
<form id="message-form" class="chat-form" enctype="multipart/form-data">
<input type="hidden" name="ticket_id" value="<?= $ticket['id'] ?>">
<input type="text" name="message" id="message-input" class="chat-input" placeholder="Напишите сообщение..." autocomplete="off">
<label for="file-upload" class="file-upload-btn" title="Прикрепить файл (до 50МБ)">
📎
<input type="file" id="file-upload" name="file" style="display: none;" onchange="updateFileName(this)">
</label>
<button type="submit" class="btn-primary" style="width: auto; padding: 0 1.5rem;">Отправить</button>
</form>
<div id="file-name" class="text-secondary" style="font-size: 0.75rem; margin-top: 0.25rem;"></div>
</div>
<?php else: ?>
<div class="chat-input-area" style="text-align: center; color: var(--text-secondary); background: rgba(0,0,0,0.2);">
Тикет закрыт. Общение невозможно.
</div>
<?php endif; ?>
</div>
</div>
<script>
const ticketId = <?= $ticket['id'] ?>;
const currentUserId = <?= $user['id'] ?>;
let lastStatus = "<?= $ticket['status'] ?>";
function updateFileName(input) {
const fileName = input.files[0] ? input.files[0].name : '';
document.getElementById('file-name').textContent = fileName ? 'Файл: ' + fileName : '';
}
async function loadMessages() {
try {
const res = await fetch(`api/messages.php?ticket_id=${ticketId}`);
const messages = await res.json();
if (messages.error) return;
const container = document.getElementById('chat-messages');
const isAtBottom = container.scrollHeight - container.scrollTop <= container.clientHeight + 100;
container.innerHTML = messages.map(msg => `
<div class="message ${msg.user_id == currentUserId ? 'mine' : 'other'}">
<div class="message-info">
${msg.username} (${msg.role === 'user' ? 'Пользователь' : (msg.role === 'helper' ? 'Помощник' : 'Куратор')}) • ${new Date(msg.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
</div>
<div class="message-text">${msg.message || ''}</div>
${msg.file_path ? `
<div class="message-file" style="margin-top: 0.5rem;">
${msg.file_type && msg.file_type.startsWith('image/') ?
`<a href="${msg.file_path}" target="_blank"><img src="${msg.file_path}" style="max-width: 100%; max-height: 300px; border-radius: 0.5rem; margin-top: 0.25rem; border: 1px solid rgba(255,255,255,0.1);"></a>` :
`<a href="${msg.file_path}" target="_blank" style="color: white; background: rgba(0,0,0,0.2); padding: 0.5rem; border-radius: 0.5rem; display: inline-block; text-decoration: none;">📎 ${msg.file_name}</a>`
}
</div>
` : ''}
</div>
`).join('');
if (isAtBottom) {
container.scrollTop = container.scrollHeight;
}
} catch (e) { console.error(e); }
}
document.getElementById('message-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const messageInput = document.getElementById('message-input');
const fileInput = document.getElementById('file-upload');
const submitBtn = e.target.querySelector('button[type="submit"]');
if (!messageInput.value.trim() && !fileInput.files.length) return;
submitBtn.disabled = true;
submitBtn.textContent = '...';
const res = await fetch('api/send_message.php', {
method: 'POST',
body: formData
});
const result = await res.json();
submitBtn.disabled = false;
submitBtn.textContent = 'Отправить';
if (result.success) {
messageInput.value = '';
fileInput.value = '';
document.getElementById('file-name').textContent = '';
loadMessages();
} else {
alert(result.error || 'Ошибка при отправке сообщения');
}
});
async function updateTicket(data) {
const res = await fetch('api/update_ticket.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ticket_id: ticketId, ...data })
});
const result = await res.json();
if (result.success) {
location.reload();
} else {
alert(result.error || 'Ошибка при обновлении');
}
}
// Polling for new messages
setInterval(loadMessages, 3000);
loadMessages();
// Scroll to bottom on load
setTimeout(() => {
const container = document.getElementById('chat-messages');
container.scrollTop = container.scrollHeight;
}, 500);
</script>
</body>
</html>