264 lines
12 KiB
PHP
264 lines
12 KiB
PHP
<?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>
|