Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd8a2de90a |
202
admin.php
202
admin.php
@ -1,68 +1,30 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/db/config.php';
|
require_once __DIR__ . '/auth.php';
|
||||||
|
$user = requireRole('curator');
|
||||||
|
|
||||||
// Simple handling of form submissions
|
$users = db()->query("SELECT * FROM users ORDER BY created_at DESC")->fetchAll();
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
if (isset($_POST['action']) && $_POST['action'] === 'add') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||||
$keywords = $_POST['keywords'] ?? '';
|
if ($_POST['action'] === 'change_role') {
|
||||||
$answer = $_POST['answer'] ?? '';
|
$stmt = db()->prepare("UPDATE users SET role = ? WHERE id = ?");
|
||||||
if ($keywords && $answer) {
|
$stmt->execute([$_POST['role'], $_POST['user_id']]);
|
||||||
$stmt = db()->prepare("INSERT INTO faqs (keywords, answer) VALUES (?, ?)");
|
} elseif ($_POST['action'] === 'toggle_status') {
|
||||||
$stmt->execute([$keywords, $answer]);
|
$newStatus = $_POST['status'] === 'active' ? 'blocked' : 'active';
|
||||||
}
|
$stmt = db()->prepare("UPDATE users SET status = ? WHERE id = ?");
|
||||||
} elseif (isset($_POST['action']) && $_POST['action'] === 'delete') {
|
$stmt->execute([$newStatus, $_POST['user_id']]);
|
||||||
$id = $_POST['id'] ?? 0;
|
|
||||||
if ($id) {
|
|
||||||
$stmt = db()->prepare("DELETE FROM faqs WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
}
|
|
||||||
} elseif (isset($_POST['action']) && $_POST['action'] === 'update_settings') {
|
|
||||||
$token = $_POST['telegram_token'] ?? '';
|
|
||||||
$stmt = db()->prepare("INSERT INTO settings (setting_key, setting_value) VALUES ('telegram_token', ?) ON DUPLICATE KEY UPDATE setting_value = ?");
|
|
||||||
$stmt->execute([$token, $token]);
|
|
||||||
}
|
}
|
||||||
header("Location: admin.php");
|
header('Location: admin.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$faqs = db()->query("SELECT * FROM faqs ORDER BY created_at DESC")->fetchAll();
|
|
||||||
$messages = db()->query("SELECT * FROM messages ORDER BY created_at DESC LIMIT 50")->fetchAll();
|
|
||||||
|
|
||||||
$telegramToken = '';
|
|
||||||
$stmt = db()->query("SELECT setting_value FROM settings WHERE setting_key = 'telegram_token'");
|
|
||||||
$row = $stmt->fetch();
|
|
||||||
if ($row) {
|
|
||||||
$telegramToken = $row['setting_value'];
|
|
||||||
}
|
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Admin - FAQ Manager</title>
|
<title>Админ-панель - Система поддержки</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||||
<style>
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||||
.btn-delete {
|
|
||||||
background: #dc3545;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.btn-add {
|
|
||||||
background: #212529;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="bg-animations">
|
<div class="bg-animations">
|
||||||
@ -70,97 +32,65 @@ if ($row) {
|
|||||||
<div class="blob blob-2"></div>
|
<div class="blob blob-2"></div>
|
||||||
<div class="blob blob-3"></div>
|
<div class="blob blob-3"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-container">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<h1>FAQ Manager</h1>
|
|
||||||
<a href="index.php" class="admin-link">Back to Chat</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-card" style="background: rgba(255, 255, 255, 0.6); padding: 2rem; border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.5); margin-bottom: 2.5rem; box-shadow: 0 10px 30px rgba(0,0,0,0.05);">
|
<div class="main-wrapper">
|
||||||
<h3 style="margin-top: 0; margin-bottom: 1.5rem; font-weight: 700;">Telegram Bot Settings</h3>
|
<nav class="navbar">
|
||||||
<form method="POST">
|
<a href="index.php" class="logo" style="text-decoration: none;">SupportSystem</a>
|
||||||
<input type="hidden" name="action" value="update_settings">
|
<div class="user-info">
|
||||||
<div class="form-group">
|
<span><?= htmlspecialchars($user['username']) ?> (<?= $user['role'] ?>)</span>
|
||||||
<label for="telegram_token">Telegram Bot Token</label>
|
<a href="logout.php" class="logout-link">Выйти</a>
|
||||||
<input type="text" name="telegram_token" id="telegram_token" class="form-control" placeholder="Paste your bot token from @BotFather" value="<?= htmlspecialchars($telegramToken) ?>">
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
<p style="font-size: 0.85em; color: #555; margin-top: 0.5rem;">
|
|
||||||
Webhook URL: <code>https://<?= $_SERVER['HTTP_HOST'] ?>/api/telegram_webhook.php</code>
|
|
||||||
</p>
|
|
||||||
<button type="submit" class="btn-add" style="background: #0088cc; color: white; border: none; padding: 0.8rem 1.5rem; border-radius: 12px; cursor: pointer; font-weight: 600; width: 100%; transition: all 0.3s ease;">Save Token</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="admin-card" style="background: rgba(255, 255, 255, 0.6); padding: 2rem; border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.5); margin-bottom: 2.5rem; box-shadow: 0 10px 30px rgba(0,0,0,0.05);">
|
<h1>Управление пользователями</h1>
|
||||||
<h3 style="margin-top: 0; margin-bottom: 1.5rem; font-weight: 700;">Add New FAQ</h3>
|
|
||||||
<form method="POST">
|
<div style="background-color: var(--card-bg); border-radius: 1rem; border: 1px solid var(--border-color); overflow-x: auto; margin-top: 2rem;">
|
||||||
<input type="hidden" name="action" value="add">
|
<table style="width: 100%; border-collapse: collapse;">
|
||||||
<div class="form-group">
|
|
||||||
<label for="keywords">Keywords (comma separated)</label>
|
|
||||||
<input type="text" name="keywords" id="keywords" class="form-control" placeholder="e.g. price, cost, dollar" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="answer">Answer</label>
|
|
||||||
<textarea name="answer" id="answer" class="form-control" rows="3" placeholder="Enter the answer..." required></textarea>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn-add" style="background: #212529; color: white; border: none; padding: 0.8rem 1.5rem; border-radius: 12px; cursor: pointer; font-weight: 600; width: 100%; transition: all 0.3s ease;">Save FAQ</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>Existing FAQs</h3>
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Keywords</th>
|
|
||||||
<th>Answer</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($faqs as $faq): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= htmlspecialchars($faq['keywords']) ?></td>
|
|
||||||
<td><?= htmlspecialchars($faq['answer']) ?></td>
|
|
||||||
<td>
|
|
||||||
<form method="POST" style="display:inline;" onsubmit="return confirm('Delete this FAQ?');">
|
|
||||||
<input type="hidden" name="action" value="delete">
|
|
||||||
<input type="hidden" name="id" value="<?= $faq['id'] ?>">
|
|
||||||
<button type="submit" class="btn-delete">Delete</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h3 style="margin-top: 3rem; margin-bottom: 1rem;">Recent Chat History (Last 50)</h3>
|
|
||||||
<div style="overflow-x: auto; background: rgba(255, 255, 255, 0.4); padding: 1rem; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.3);">
|
|
||||||
<table class="table" style="width: 100%;">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
<th style="width: 15%;">Time</th>
|
<th style="padding: 1rem; text-align: left;">ID</th>
|
||||||
<th style="width: 35%;">User Message</th>
|
<th style="padding: 1rem; text-align: left;">Имя</th>
|
||||||
<th style="width: 50%;">AI Response</th>
|
<th style="padding: 1rem; text-align: left;">Роль</th>
|
||||||
|
<th style="padding: 1rem; text-align: left;">Статус</th>
|
||||||
|
<th style="padding: 1rem; text-align: left;">Дата рег.</th>
|
||||||
|
<th style="padding: 1rem; text-align: left;">Действия</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php if (empty($messages)): ?>
|
<?php foreach ($users as $u): ?>
|
||||||
<tr>
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
<td colspan="3" style="text-align: center; color: #777;">No messages yet.</td>
|
<td style="padding: 1rem;"><?= $u['id'] ?></td>
|
||||||
</tr>
|
<td style="padding: 1rem;"><?= htmlspecialchars($u['username']) ?></td>
|
||||||
<?php else: ?>
|
<td style="padding: 1rem;">
|
||||||
<?php foreach ($messages as $msg): ?>
|
<form method="POST" style="display: flex; gap: 0.5rem; align-items: center;">
|
||||||
<tr>
|
<input type="hidden" name="user_id" value="<?= $u['id'] ?>">
|
||||||
<td style="white-space: nowrap; font-size: 0.85em; color: #555;"><?= htmlspecialchars($msg['created_at']) ?></td>
|
<input type="hidden" name="action" value="change_role">
|
||||||
<td style="background: rgba(255, 255, 255, 0.3); border-radius: 8px; padding: 8px;"><?= htmlspecialchars($msg['user_message']) ?></td>
|
<select name="role" onchange="this.form.submit()" style="padding: 0.25rem 0.5rem;">
|
||||||
<td style="background: rgba(255, 255, 255, 0.5); border-radius: 8px; padding: 8px;"><?= htmlspecialchars($msg['ai_response']) ?></td>
|
<option value="user" <?= $u['role'] === 'user' ? 'selected' : '' ?>>Пользователь</option>
|
||||||
|
<option value="helper" <?= $u['role'] === 'helper' ? 'selected' : '' ?>>Помощник</option>
|
||||||
|
<option value="curator" <?= $u['role'] === 'curator' ? 'selected' : '' ?>>Куратор</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 1rem;"><?= $u['status'] ?></td>
|
||||||
|
<td style="padding: 1rem;"><?= date('d.m.Y', strtotime($u['created_at'])) ?></td>
|
||||||
|
<td style="padding: 1rem;">
|
||||||
|
<?php if ($u['id'] != $user['id']): ?>
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="user_id" value="<?= $u['id'] ?>">
|
||||||
|
<input type="hidden" name="action" value="toggle_status">
|
||||||
|
<input type="hidden" name="status" value="<?= $u['status'] ?>">
|
||||||
|
<button type="submit" class="btn-primary" style="width: auto; padding: 0.25rem 0.75rem; background-color: <?= $u['status'] === 'active' ? 'var(--error)' : 'var(--success)' ?>;">
|
||||||
|
<?= $u['status'] === 'active' ? 'Блокировать' : 'Разблокировать' ?>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
36
api/messages.php
Normal file
36
api/messages.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../auth.php';
|
||||||
|
$user = requireAuth();
|
||||||
|
|
||||||
|
$ticketId = $_GET['ticket_id'] ?? null;
|
||||||
|
if (!$ticketId) {
|
||||||
|
echo json_encode(['error' => 'Missing ticket_id']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check access
|
||||||
|
$stmt = db()->prepare("SELECT * FROM tickets WHERE id = ?");
|
||||||
|
$stmt->execute([$ticketId]);
|
||||||
|
$ticket = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$ticket) {
|
||||||
|
echo json_encode(['error' => 'Ticket not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user['role'] === 'user' && $ticket['user_id'] != $user['id']) {
|
||||||
|
echo json_encode(['error' => 'Access denied']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = db()->prepare("
|
||||||
|
SELECT m.*, u.username, u.role
|
||||||
|
FROM messages m
|
||||||
|
JOIN users u ON m.user_id = u.id
|
||||||
|
WHERE m.ticket_id = ?
|
||||||
|
ORDER BY m.created_at ASC
|
||||||
|
");
|
||||||
|
$stmt->execute([$ticketId]);
|
||||||
|
$messages = $stmt->fetchAll();
|
||||||
|
|
||||||
|
echo json_encode($messages);
|
||||||
66
api/send_message.php
Normal file
66
api/send_message.php
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../auth.php';
|
||||||
|
$user = requireAuth();
|
||||||
|
|
||||||
|
$ticketId = $_POST['ticket_id'] ?? null;
|
||||||
|
$message = $_POST['message'] ?? '';
|
||||||
|
$file = $_FILES['file'] ?? null;
|
||||||
|
|
||||||
|
if (!$ticketId) {
|
||||||
|
echo json_encode(['error' => 'Missing ticket_id']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check access
|
||||||
|
$stmt = db()->prepare("SELECT * FROM tickets WHERE id = ?");
|
||||||
|
$stmt->execute([$ticketId]);
|
||||||
|
$ticket = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$ticket || $ticket['status'] === 'closed') {
|
||||||
|
echo json_encode(['error' => 'Ticket closed or not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user['role'] === 'user' && $ticket['user_id'] != $user['id']) {
|
||||||
|
echo json_encode(['error' => 'Access denied']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filePath = null;
|
||||||
|
$fileName = null;
|
||||||
|
$fileType = null;
|
||||||
|
|
||||||
|
if ($file && $file['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf', 'application/zip', 'text/plain'];
|
||||||
|
$maxSize = 50 * 1024 * 1024; // 50MB
|
||||||
|
|
||||||
|
if ($file['size'] > $maxSize) {
|
||||||
|
echo json_encode(['error' => 'File too large (max 50MB)']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = pathinfo($file['name'], PATHINFO_EXTENSION);
|
||||||
|
$newFileName = bin2hex(random_bytes(16)) . '.' . $extension;
|
||||||
|
$uploadDir = __DIR__ . '/../uploads/';
|
||||||
|
$targetPath = $uploadDir . $newFileName;
|
||||||
|
|
||||||
|
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
|
||||||
|
$filePath = 'uploads/' . $newFileName;
|
||||||
|
$fileName = $file['name'];
|
||||||
|
$fileType = $file['type'];
|
||||||
|
} else {
|
||||||
|
echo json_encode(['error' => 'Failed to upload file']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = db()->prepare("INSERT INTO messages (ticket_id, user_id, message, file_path, file_name, file_type) VALUES (?, ?, ?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$ticketId, $user['id'], $message, $filePath, $fileName, $fileType]);
|
||||||
|
|
||||||
|
// Update ticket status automatically if helper replies
|
||||||
|
if ($user['role'] !== 'user' && $ticket['status'] === 'open') {
|
||||||
|
$updateStmt = db()->prepare("UPDATE tickets SET status = 'in_progress' WHERE id = ?");
|
||||||
|
$updateStmt->execute([$ticketId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
12
api/stats.php
Normal file
12
api/stats.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../auth.php';
|
||||||
|
$user = requireRole('curator');
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'total_tickets' => db()->query("SELECT COUNT(*) FROM tickets")->fetchColumn(),
|
||||||
|
'active_tickets' => db()->query("SELECT COUNT(*) FROM tickets WHERE status != 'closed'")->fetchColumn(),
|
||||||
|
'users_count' => db()->query("SELECT COUNT(*) FROM users")->fetchColumn(),
|
||||||
|
'helpers_count' => db()->query("SELECT COUNT(*) FROM users WHERE role = 'helper'")->fetchColumn()
|
||||||
|
];
|
||||||
|
|
||||||
|
echo json_encode($stats);
|
||||||
@ -1,91 +1,2 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/../db/config.php';
|
echo "Telegram webhook not implemented.";
|
||||||
require_once __DIR__ . '/../ai/LocalAIApi.php';
|
|
||||||
|
|
||||||
// Get Telegram Update
|
|
||||||
$content = file_get_contents("php://input");
|
|
||||||
$update = json_decode($content, true);
|
|
||||||
|
|
||||||
if (!$update || !isset($update['message'])) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = $update['message'];
|
|
||||||
$chatId = $message['chat']['id'];
|
|
||||||
$text = $message['text'] ?? '';
|
|
||||||
|
|
||||||
if (empty($text)) {
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Telegram Token from DB
|
|
||||||
$stmt = db()->query("SELECT setting_value FROM settings WHERE setting_key = 'telegram_token'");
|
|
||||||
$token = $stmt->fetchColumn();
|
|
||||||
|
|
||||||
if (!$token) {
|
|
||||||
error_log("Telegram Error: No bot token found in settings.");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendTelegramMessage($chatId, $text, $token) {
|
|
||||||
$url = "https://api.telegram.org/bot$token/sendMessage";
|
|
||||||
$data = [
|
|
||||||
'chat_id' => $chatId,
|
|
||||||
'text' => $text,
|
|
||||||
'parse_mode' => 'Markdown'
|
|
||||||
];
|
|
||||||
|
|
||||||
$options = [
|
|
||||||
'http' => [
|
|
||||||
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
|
|
||||||
'method' => 'POST',
|
|
||||||
'content' => http_build_query($data),
|
|
||||||
],
|
|
||||||
];
|
|
||||||
$context = stream_context_create($options);
|
|
||||||
return file_get_contents($url, false, $context);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process with AI (Similar logic to api/chat.php)
|
|
||||||
try {
|
|
||||||
// 1. Fetch Knowledge Base
|
|
||||||
$stmt = db()->query("SELECT keywords, answer FROM faqs");
|
|
||||||
$faqs = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
$knowledgeBase = "Here is the knowledge base for this website:\n\n";
|
|
||||||
foreach ($faqs as $faq) {
|
|
||||||
$knowledgeBase .= "Q: " . $faq['keywords'] . "\nA: " . $faq['answer'] . "\n---\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
$systemPrompt = "You are a helpful AI assistant integrated with Telegram. " .
|
|
||||||
"Use the provided Knowledge Base to answer user questions. " .
|
|
||||||
"Keep answers concise for mobile reading. Use Markdown for formatting.\n\n" .
|
|
||||||
$knowledgeBase;
|
|
||||||
|
|
||||||
// 2. Call AI
|
|
||||||
$response = LocalAIApi::createResponse([
|
|
||||||
'model' => 'gpt-4o-mini',
|
|
||||||
'input' => [
|
|
||||||
['role' => 'system', 'content' => $systemPrompt],
|
|
||||||
['role' => 'user', 'content' => $text],
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!empty($response['success'])) {
|
|
||||||
$aiReply = LocalAIApi::extractText($response);
|
|
||||||
|
|
||||||
// 3. Save History
|
|
||||||
try {
|
|
||||||
$stmt = db()->prepare("INSERT INTO messages (user_message, ai_response) VALUES (?, ?)");
|
|
||||||
$stmt->execute(["[Telegram] " . $text, $aiReply]);
|
|
||||||
} catch (Exception $e) {}
|
|
||||||
|
|
||||||
// 4. Send back to Telegram
|
|
||||||
sendTelegramMessage($chatId, $aiReply, $token);
|
|
||||||
} else {
|
|
||||||
sendTelegramMessage($chatId, "I'm sorry, I encountered an error processing your request.", $token);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error_log("Telegram Webhook Error: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
65
api/update_ticket.php
Normal file
65
api/update_ticket.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../auth.php';
|
||||||
|
$user = requireAuth();
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if (!$input) {
|
||||||
|
// Fallback to traditional POST
|
||||||
|
$input = $_POST;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ticketId = $input['ticket_id'] ?? null;
|
||||||
|
$status = $input['status'] ?? null;
|
||||||
|
$priority = $input['priority'] ?? null;
|
||||||
|
$helperId = $input['helper_id'] ?? null;
|
||||||
|
|
||||||
|
if (!$ticketId) {
|
||||||
|
echo json_encode(['error' => 'Missing ticket_id']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch ticket
|
||||||
|
$stmt = db()->prepare("SELECT * FROM tickets WHERE id = ?");
|
||||||
|
$stmt->execute([$ticketId]);
|
||||||
|
$ticket = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$ticket) {
|
||||||
|
echo json_encode(['error' => 'Ticket not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check access
|
||||||
|
if ($user['role'] === 'user' && $ticket['user_id'] != $user['id']) {
|
||||||
|
echo json_encode(['error' => 'Access denied']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For users, only closing is allowed
|
||||||
|
if ($user['role'] === 'user') {
|
||||||
|
if ($status === 'closed') {
|
||||||
|
$stmt = db()->prepare("UPDATE tickets SET status = 'closed' WHERE id = ?");
|
||||||
|
$stmt->execute([$ticketId]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['error' => 'Only closing is allowed for users']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For helpers and curators, allow everything
|
||||||
|
if ($status) {
|
||||||
|
$stmt = db()->prepare("UPDATE tickets SET status = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$status, $ticketId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($priority) {
|
||||||
|
$stmt = db()->prepare("UPDATE tickets SET priority = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$priority, $ticketId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($helperId !== null && $user['role'] === 'curator') {
|
||||||
|
$stmt = db()->prepare("UPDATE tickets SET helper_id = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$helperId == 0 ? null : $helperId, $ticketId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
@ -1,279 +1,89 @@
|
|||||||
body {
|
:root {
|
||||||
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
|
--bg-dark: #0f172a;
|
||||||
background-size: 400% 400%;
|
--card-bg: #1e293b;
|
||||||
animation: gradient 15s ease infinite;
|
--primary: #3b82f6;
|
||||||
color: #212529;
|
--primary-hover: #2563eb;
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
--text-primary: #f8fafc;
|
||||||
font-size: 14px;
|
--text-secondary: #94a3b8;
|
||||||
margin: 0;
|
--border-color: #334155;
|
||||||
min-height: 100vh;
|
--error: #ef4444;
|
||||||
|
--success: #22c55e;
|
||||||
|
--warning: #f59e0b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-wrapper {
|
* {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
padding: 20px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: relative;
|
margin: 0;
|
||||||
z-index: 1;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes gradient {
|
body {
|
||||||
0% {
|
font-family: 'Inter', sans-serif;
|
||||||
background-position: 0% 50%;
|
background-color: var(--bg-dark);
|
||||||
}
|
color: var(--text-primary);
|
||||||
50% {
|
|
||||||
background-position: 100% 50%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
background: rgba(255, 255, 255, 0.85);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 20px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 85vh;
|
|
||||||
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
|
|
||||||
backdrop-filter: blur(15px);
|
|
||||||
-webkit-backdrop-filter: blur(15px);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-header {
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
background: rgba(255, 255, 255, 0.5);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-messages {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 1.5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom Scrollbar */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
max-width: 85%;
|
|
||||||
padding: 0.85rem 1.1rem;
|
|
||||||
border-radius: 16px;
|
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-size: 0.95rem;
|
min-height: 100vh;
|
||||||
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
overflow-x: hidden;
|
||||||
animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; transform: translateY(20px) scale(0.95); }
|
|
||||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.visitor {
|
|
||||||
align-self: flex-end;
|
|
||||||
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
|
|
||||||
color: #fff;
|
|
||||||
border-bottom-right-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.bot {
|
|
||||||
align-self: flex-start;
|
|
||||||
background: #ffffff;
|
|
||||||
color: #212529;
|
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input-area {
|
|
||||||
padding: 1.25rem;
|
|
||||||
background: rgba(255, 255, 255, 0.5);
|
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input-area form {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input-area input {
|
|
||||||
flex: 1;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
outline: none;
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input-area input:focus {
|
|
||||||
border-color: #23a6d5;
|
|
||||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input-area button {
|
|
||||||
background: #212529;
|
|
||||||
color: #fff;
|
|
||||||
border: none;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-input-area button:hover {
|
|
||||||
background: #000;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Background Animations */
|
|
||||||
.bg-animations {
|
.bg-animations {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 0;
|
z-index: -1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blob {
|
.blob {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
height: 500px;
|
height: 500px;
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: radial-gradient(circle, rgba(59, 130, 246, 0.15) 0%, transparent 70%);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
filter: blur(80px);
|
filter: blur(40px);
|
||||||
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blob-1 {
|
.blob-1 { top: -100px; left: -100px; animation: float 20s infinite alternate; }
|
||||||
top: -10%;
|
.blob-2 { bottom: -100px; right: -100px; animation: float 25s infinite alternate-reverse; }
|
||||||
left: -10%;
|
.blob-3 { top: 50%; left: 50%; transform: translate(-50%, -50%); opacity: 0.5; }
|
||||||
background: rgba(238, 119, 82, 0.4);
|
|
||||||
|
@keyframes float {
|
||||||
|
0% { transform: translate(0, 0); }
|
||||||
|
100% { transform: translate(100px, 50px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.blob-2 {
|
.main-wrapper {
|
||||||
bottom: -10%;
|
max-width: 1200px;
|
||||||
right: -10%;
|
margin: 0 auto;
|
||||||
background: rgba(35, 166, 213, 0.4);
|
padding: 2rem;
|
||||||
animation-delay: -7s;
|
min-height: 100vh;
|
||||||
width: 600px;
|
|
||||||
height: 600px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blob-3 {
|
/* Auth Pages */
|
||||||
top: 40%;
|
.auth-page {
|
||||||
left: 30%;
|
display: flex;
|
||||||
background: rgba(231, 60, 126, 0.3);
|
align-items: center;
|
||||||
animation-delay: -14s;
|
justify-content: center;
|
||||||
width: 450px;
|
|
||||||
height: 450px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes move {
|
.auth-card {
|
||||||
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
|
background-color: var(--card-bg);
|
||||||
33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
|
|
||||||
66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
|
|
||||||
100% { transform: translate(0, 0) rotate(360deg) scale(1); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-link {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-link:hover {
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Admin Styles */
|
|
||||||
.admin-container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 3rem auto;
|
|
||||||
padding: 2.5rem;
|
padding: 2.5rem;
|
||||||
background: rgba(255, 255, 255, 0.85);
|
border-radius: 1rem;
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
border-radius: 24px;
|
|
||||||
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-container h1 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: #212529;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: separate;
|
max-width: 400px;
|
||||||
border-spacing: 0 8px;
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
|
||||||
margin-top: 1.5rem;
|
border: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table th {
|
.auth-card h2 {
|
||||||
background: transparent;
|
margin-bottom: 1.5rem;
|
||||||
border: none;
|
text-align: center;
|
||||||
padding: 1rem;
|
font-size: 1.875rem;
|
||||||
color: #6c757d;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.table td {
|
|
||||||
background: #fff;
|
|
||||||
padding: 1rem;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table tr td:first-child { border-radius: 12px 0 0 12px; }
|
|
||||||
.table tr td:last-child { border-radius: 0 12px 12px 0; }
|
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1.25rem;
|
||||||
}
|
}
|
||||||
@ -281,22 +91,314 @@ body {
|
|||||||
.form-group label {
|
.form-group label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
font-weight: 600;
|
color: var(--text-secondary);
|
||||||
font-size: 0.9rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control {
|
.form-group input, .form-group select, .form-group textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
border-radius: 0.5rem;
|
||||||
border-radius: 12px;
|
border: 1px solid var(--border-color);
|
||||||
background: #fff;
|
background-color: var(--bg-dark);
|
||||||
transition: all 0.3s ease;
|
color: var(--text-primary);
|
||||||
box-sizing: border-box;
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
.form-group input:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-footer a {
|
||||||
|
color: var(--primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-error {
|
||||||
|
background-color: rgba(239, 68, 68, 0.1);
|
||||||
|
color: var(--error);
|
||||||
|
border: 1px solid var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: rgba(34, 197, 94, 0.1);
|
||||||
|
color: var(--success);
|
||||||
|
border: 1px solid var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard / Index */
|
||||||
|
.navbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 1rem 0;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info span {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-link {
|
||||||
|
color: var(--error);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-card {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-open { background-color: rgba(59, 130, 246, 0.1); color: #60a5fa; }
|
||||||
|
.badge-in_progress { background-color: rgba(245, 158, 11, 0.1); color: #fbbf24; }
|
||||||
|
.badge-awaiting_response { background-color: rgba(139, 92, 246, 0.1); color: #a78bfa; }
|
||||||
|
.badge-closed { background-color: rgba(148, 163, 184, 0.1); color: #94a3b8; }
|
||||||
|
|
||||||
|
.ticket-card h3 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-card p {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-meta {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat Styles */
|
||||||
|
.ticket-detail-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-box {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
max-width: 80%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
position: relative;
|
||||||
|
animation: fadeIn 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-info {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.mine {
|
||||||
|
align-self: flex-end;
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.other {
|
||||||
|
align-self: flex-start;
|
||||||
|
background-color: var(--bg-dark);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-area {
|
||||||
|
padding: 1rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input {
|
||||||
|
flex: 1;
|
||||||
|
background-color: var(--bg-dark);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
color: var(--text-primary);
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #23a6d5;
|
}
|
||||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
|
|
||||||
}
|
.file-upload-btn {
|
||||||
|
background-color: var(--bg-dark);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Statistics Dashboard */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.main-wrapper {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.ticket-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
73
auth.php
Normal file
73
auth.php
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
|
||||||
|
function getCurrentUser() {
|
||||||
|
if (isset($_SESSION['user_id'])) {
|
||||||
|
$stmt = db()->prepare("SELECT * FROM users WHERE id = ?");
|
||||||
|
$stmt->execute([$_SESSION['user_id']]);
|
||||||
|
return $stmt->fetch();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireAuth() {
|
||||||
|
$user = getCurrentUser();
|
||||||
|
if (!$user) {
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($user['status'] === 'blocked') {
|
||||||
|
session_destroy();
|
||||||
|
die('Ваш аккаунт заблокирован.');
|
||||||
|
}
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireRole($role) {
|
||||||
|
$user = requireAuth();
|
||||||
|
if (is_array($role)) {
|
||||||
|
if (!in_array($user['role'], $role)) {
|
||||||
|
die('Доступ запрещен.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($user['role'] !== $role) {
|
||||||
|
die('Доступ запрещен.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
function login($username, $password) {
|
||||||
|
$stmt = db()->prepare("SELECT * FROM users WHERE username = ?");
|
||||||
|
$stmt->execute([$username]);
|
||||||
|
$user = $stmt->fetch();
|
||||||
|
if ($user && password_verify($password, $user['password_hash'])) {
|
||||||
|
if ($user['status'] === 'blocked') {
|
||||||
|
return ['error' => 'Ваш аккаунт заблокирован.'];
|
||||||
|
}
|
||||||
|
$_SESSION['user_id'] = $user['id'];
|
||||||
|
return ['success' => true];
|
||||||
|
}
|
||||||
|
return ['error' => 'Неверное имя пользователя или пароль.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function register($username, $password) {
|
||||||
|
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||||
|
try {
|
||||||
|
$stmt = db()->prepare("INSERT INTO users (username, password_hash) VALUES (?, ?)");
|
||||||
|
$stmt->execute([$username, $passwordHash]);
|
||||||
|
return ['success' => true];
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
if ($e->getCode() == 23000) {
|
||||||
|
return ['error' => 'Имя пользователя уже занято.'];
|
||||||
|
}
|
||||||
|
return ['error' => 'Ошибка при регистрации.'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
session_destroy();
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
81
create_ticket.php
Normal file
81
create_ticket.php
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/auth.php';
|
||||||
|
$user = requireAuth();
|
||||||
|
|
||||||
|
if ($user['role'] !== 'user') {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check open tickets limit (max 3)
|
||||||
|
$stmt = db()->prepare("SELECT COUNT(*) FROM tickets WHERE user_id = ? AND status != 'closed'");
|
||||||
|
$stmt->execute([$user['id']]);
|
||||||
|
$openTicketsCount = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
if ($openTicketsCount >= 3) {
|
||||||
|
die('У вас уже 3 открытых тикета. Пожалуйста, закройте один из них, прежде чем открывать новый.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = null;
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$title = trim($_POST['title']);
|
||||||
|
$description = trim($_POST['description']);
|
||||||
|
$category = trim($_POST['category']);
|
||||||
|
|
||||||
|
if (empty($title) || empty($description)) {
|
||||||
|
$error = 'Пожалуйста, заполните все обязательные поля.';
|
||||||
|
} else {
|
||||||
|
$stmt = db()->prepare("INSERT INTO tickets (user_id, title, description, category) VALUES (?, ?, ?, ?)");
|
||||||
|
$stmt->execute([$user['id'], $title, $description, $category]);
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<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">
|
||||||
|
<div class="auth-card" style="max-width: 600px; margin: 0 auto;">
|
||||||
|
<h2>Новый тикет</h2>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="title">Тема</label>
|
||||||
|
<input type="text" name="title" id="title" required placeholder="Например: Не работает вход в систему">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="category">Категория</label>
|
||||||
|
<select name="category" id="category">
|
||||||
|
<option value="technical">Техническая проблема</option>
|
||||||
|
<option value="question">Вопрос</option>
|
||||||
|
<option value="complaint">Жалоба</option>
|
||||||
|
<option value="other">Другое</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="description">Описание</label>
|
||||||
|
<textarea name="description" id="description" rows="6" required placeholder="Опишите проблему как можно подробнее..."></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary">Отправить тикет</button>
|
||||||
|
<a href="index.php" class="auth-footer" style="display: block; margin-top: 1rem; color: var(--text-secondary);">Отмена</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,17 +1,22 @@
|
|||||||
<?php
|
<?php
|
||||||
// Generated by setup_mariadb_project.sh — edit as needed.
|
// Database configuration
|
||||||
define('DB_HOST', '127.0.0.1');
|
define('DB_HOST', '127.0.0.1');
|
||||||
define('DB_NAME', 'app_38384');
|
define('DB_NAME', 'app_38916');
|
||||||
define('DB_USER', 'app_38384');
|
define('DB_USER', 'app_38916');
|
||||||
define('DB_PASS', '5561099f-23a3-43d8-b1a2-54739c50721b');
|
define('DB_PASS', 'new_secure_password');
|
||||||
|
|
||||||
function db() {
|
function db() {
|
||||||
static $pdo;
|
static $pdo;
|
||||||
if (!$pdo) {
|
if (!$pdo) {
|
||||||
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
try {
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
]);
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log($e->getMessage());
|
||||||
|
die('Database connection failed.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return $pdo;
|
return $pdo;
|
||||||
}
|
}
|
||||||
40
db/migrations/init.sql
Normal file
40
db/migrations/init.sql
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(255) UNIQUE NOT NULL,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
role ENUM('curator', 'helper', 'user') DEFAULT 'user',
|
||||||
|
status ENUM('active', 'blocked') DEFAULT 'active',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
helper_id INT,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
category VARCHAR(255),
|
||||||
|
priority ENUM('low', 'medium', 'high') DEFAULT 'medium',
|
||||||
|
status ENUM('open', 'in_progress', 'awaiting_response', 'closed') DEFAULT 'open',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||||
|
FOREIGN KEY (helper_id) REFERENCES users(id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
ticket_id INT NOT NULL,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
message TEXT,
|
||||||
|
file_path VARCHAR(255),
|
||||||
|
file_name VARCHAR(255),
|
||||||
|
file_type VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- Seed initial curator
|
||||||
|
-- Password is 'admin123'
|
||||||
|
INSERT IGNORE INTO users (username, password_hash, role) VALUES ('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'curator');
|
||||||
3
healthz.php
Normal file
3
healthz.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
http_response_code(200);
|
||||||
|
echo "OK";
|
||||||
212
index.php
212
index.php
@ -1,25 +1,100 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/db/config.php';
|
require_once __DIR__ . '/auth.php';
|
||||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Modern AI-ready Chat Assistant';
|
$user = requireAuth();
|
||||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|
||||||
|
// Filters for Helpers and Curators
|
||||||
|
$statusFilter = $_GET['status'] ?? '';
|
||||||
|
$categoryFilter = $_GET['category'] ?? '';
|
||||||
|
$searchQuery = $_GET['search'] ?? '';
|
||||||
|
|
||||||
|
$params = [];
|
||||||
|
$sql = "SELECT t.*, u.username as creator_name FROM tickets t JOIN users u ON t.user_id = u.id";
|
||||||
|
$whereClauses = [];
|
||||||
|
|
||||||
|
if ($user['role'] === 'user') {
|
||||||
|
$whereClauses[] = "t.user_id = ?";
|
||||||
|
$params[] = $user['id'];
|
||||||
|
} else {
|
||||||
|
if ($statusFilter) {
|
||||||
|
$whereClauses[] = "t.status = ?";
|
||||||
|
$params[] = $statusFilter;
|
||||||
|
}
|
||||||
|
if ($categoryFilter) {
|
||||||
|
$whereClauses[] = "t.category = ?";
|
||||||
|
$params[] = $categoryFilter;
|
||||||
|
}
|
||||||
|
if ($searchQuery) {
|
||||||
|
$whereClauses[] = "(t.title LIKE ? OR t.description LIKE ?)";
|
||||||
|
$params[] = "%$searchQuery%";
|
||||||
|
$params[] = "%$searchQuery%";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($whereClauses)) {
|
||||||
|
$sql .= " WHERE " . implode(" AND ", $whereClauses);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " ORDER BY t.created_at DESC";
|
||||||
|
|
||||||
|
$stmt = db()->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$tickets = $stmt->fetchAll();
|
||||||
|
|
||||||
|
$openTicketsCount = 0;
|
||||||
|
if ($user['role'] === 'user') {
|
||||||
|
foreach ($tickets as $t) {
|
||||||
|
if ($t['status'] !== 'closed') $openTicketsCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats for Curator
|
||||||
|
$stats = [];
|
||||||
|
if ($user['role'] === 'curator') {
|
||||||
|
$stats['total'] = db()->query("SELECT COUNT(*) FROM tickets")->fetchColumn();
|
||||||
|
$stats['active'] = db()->query("SELECT COUNT(*) FROM tickets WHERE status != 'closed'")->fetchColumn();
|
||||||
|
// Avg response time (simplified: difference between ticket creation and first message from a helper/curator)
|
||||||
|
$stats['avg_response'] = db()->query("
|
||||||
|
SELECT ROUND(AVG(TIMESTAMPDIFF(HOUR, t.created_at, m.created_at)), 1)
|
||||||
|
FROM tickets t
|
||||||
|
JOIN messages m ON t.id = m.ticket_id
|
||||||
|
JOIN users u ON m.user_id = u.id
|
||||||
|
WHERE u.role != 'user'
|
||||||
|
AND m.id = (
|
||||||
|
SELECT MIN(m2.id)
|
||||||
|
FROM messages m2
|
||||||
|
JOIN users u2 ON m2.user_id = u2.id
|
||||||
|
WHERE m2.ticket_id = t.id AND u2.role != 'user'
|
||||||
|
)
|
||||||
|
")->fetchColumn() ?: '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Chat Assistant</title>
|
<title>Панель управления - Система поддержки</title>
|
||||||
<?php if ($projectDescription): ?>
|
|
||||||
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>">
|
|
||||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>">
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($projectImageUrl): ?>
|
|
||||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>">
|
|
||||||
<?php endif; ?>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="bg-animations">
|
<div class="bg-animations">
|
||||||
@ -27,23 +102,104 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
|||||||
<div class="blob blob-2"></div>
|
<div class="blob blob-2"></div>
|
||||||
<div class="blob blob-3"></div>
|
<div class="blob blob-3"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-wrapper">
|
<div class="main-wrapper">
|
||||||
<div class="chat-container">
|
<nav class="navbar">
|
||||||
<div class="chat-header">
|
<div class="logo">SupportSystem</div>
|
||||||
<span>Chat Assistant</span>
|
<div class="user-info">
|
||||||
<a href="admin.php" class="admin-link">Admin</a>
|
<span><?= htmlspecialchars($user['username']) ?> (<?= $user['role'] ?>)</span>
|
||||||
|
<a href="logout.php" class="logout-link">Выйти</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-messages" id="chat-messages">
|
</nav>
|
||||||
<div class="message bot">
|
|
||||||
Hello! I'm your assistant. How can I help you today?
|
<?php if ($user['role'] === 'curator'): ?>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value"><?= $stats['total'] ?></div>
|
||||||
|
<div class="stat-label">Всего тикетов</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value"><?= $stats['active'] ?></div>
|
||||||
|
<div class="stat-label">Активных</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-value"><?= $stats['avg_response'] ?> ч</div>
|
||||||
|
<div class="stat-label">Ср. время ответа</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<a href="admin.php" style="text-decoration: none;">
|
||||||
|
<div class="stat-value">👥</div>
|
||||||
|
<div class="stat-label">Управление пользователями</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-input-area">
|
<?php endif; ?>
|
||||||
<form id="chat-form">
|
|
||||||
<input type="text" id="chat-input" placeholder="Type your message..." autocomplete="off">
|
<div class="dashboard-header">
|
||||||
<button type="submit">Send</button>
|
<h1>Тикеты</h1>
|
||||||
</form>
|
<?php if ($user['role'] === 'user'): ?>
|
||||||
</div>
|
<?php if ($openTicketsCount < 3): ?>
|
||||||
|
<a href="create_ticket.php" class="btn-primary" style="padding: 0.5rem 1rem; width: auto; text-decoration: none;">Создать тикет</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-secondary" style="font-size: 0.875rem;">Максимум 3 открытых тикета</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($user['role'] !== 'user'): ?>
|
||||||
|
<form class="filters-area" style="background-color: var(--card-bg); padding: 1rem; border-radius: 0.75rem; border: 1px solid var(--border-color); margin-bottom: 1.5rem; display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-end;">
|
||||||
|
<div class="form-group" style="margin-bottom: 0; flex: 1; min-width: 200px;">
|
||||||
|
<label>Поиск</label>
|
||||||
|
<input type="text" name="search" value="<?= htmlspecialchars($searchQuery) ?>" placeholder="По теме или описанию...">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label>Статус</label>
|
||||||
|
<select name="status">
|
||||||
|
<option value="">Все</option>
|
||||||
|
<option value="open" <?= $statusFilter === 'open' ? 'selected' : '' ?>>Открыт</option>
|
||||||
|
<option value="in_progress" <?= $statusFilter === 'in_progress' ? 'selected' : '' ?>>В работе</option>
|
||||||
|
<option value="awaiting_response" <?= $statusFilter === 'awaiting_response' ? 'selected' : '' ?>>Ожидает ответа</option>
|
||||||
|
<option value="closed" <?= $statusFilter === 'closed' ? 'selected' : '' ?>>Закрыт</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom: 0;">
|
||||||
|
<label>Категория</label>
|
||||||
|
<select name="category">
|
||||||
|
<option value="">Все</option>
|
||||||
|
<option value="technical" <?= $categoryFilter === 'technical' ? 'selected' : '' ?>>Техническая</option>
|
||||||
|
<option value="question" <?= $categoryFilter === 'question' ? 'selected' : '' ?>>Вопрос</option>
|
||||||
|
<option value="complaint" <?= $categoryFilter === 'complaint' ? 'selected' : '' ?>>Жалоба</option>
|
||||||
|
<option value="other" <?= $categoryFilter === 'other' ? 'selected' : '' ?>>Другое</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary" style="width: auto; height: 42px; padding: 0 1rem;">Найти</button>
|
||||||
|
<a href="index.php" style="color: var(--text-secondary); text-decoration: none; padding-bottom: 0.5rem;">Сбросить</a>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="ticket-grid">
|
||||||
|
<?php if (empty($tickets)): ?>
|
||||||
|
<p class="text-secondary">Тикетов не найдено.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($tickets as $ticket): ?>
|
||||||
|
<a href="ticket.php?id=<?= $ticket['id'] ?>" class="ticket-card">
|
||||||
|
<div class="ticket-badge badge-<?= $ticket['status'] ?>">
|
||||||
|
<?= getStatusLabel($ticket['status']) ?>
|
||||||
|
</div>
|
||||||
|
<h3><?= htmlspecialchars($ticket['title']) ?></h3>
|
||||||
|
<p><?= htmlspecialchars($ticket['description']) ?></p>
|
||||||
|
<div class="ticket-meta">
|
||||||
|
<span>Приоритет: <?= getPriorityLabel($ticket['priority']) ?></span>
|
||||||
|
<span><?= date('d.m.Y H:i', strtotime($ticket['created_at'])) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php if (isset($ticket['creator_name'])): ?>
|
||||||
|
<div class="ticket-meta" style="margin-top: 0.5rem;">
|
||||||
|
<span>От: <?= htmlspecialchars($ticket['creator_name']) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
55
login.php
Normal file
55
login.php
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/auth.php';
|
||||||
|
|
||||||
|
if (isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = null;
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$res = login($_POST['username'], $_POST['password']);
|
||||||
|
if (!empty($res['success'])) {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$error = $res['error'] ?? 'Неизвестная ошибка';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<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 class="auth-page">
|
||||||
|
<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">
|
||||||
|
<div class="auth-card">
|
||||||
|
<h2>Вход</h2>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Имя пользователя</label>
|
||||||
|
<input type="text" name="username" id="username" required autocomplete="username">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Пароль</label>
|
||||||
|
<input type="password" name="password" id="password" required autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary">Войти</button>
|
||||||
|
</form>
|
||||||
|
<p class="auth-footer">Нет аккаунта? <a href="register.php">Зарегистрироваться</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3
logout.php
Normal file
3
logout.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/auth.php';
|
||||||
|
logout();
|
||||||
56
register.php
Normal file
56
register.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/auth.php';
|
||||||
|
|
||||||
|
if (isset($_SESSION['user_id'])) {
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$error = null;
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$res = register($_POST['username'], $_POST['password']);
|
||||||
|
if (!empty($res['success'])) {
|
||||||
|
login($_POST['username'], $_POST['password']);
|
||||||
|
header('Location: index.php');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$error = $res['error'] ?? 'Неизвестная ошибка';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<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 class="auth-page">
|
||||||
|
<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">
|
||||||
|
<div class="auth-card">
|
||||||
|
<h2>Регистрация</h2>
|
||||||
|
<?php if ($error): ?>
|
||||||
|
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">Имя пользователя</label>
|
||||||
|
<input type="text" name="username" id="username" required autocomplete="username">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Пароль</label>
|
||||||
|
<input type="password" name="password" id="password" required autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary">Зарегистрироваться</button>
|
||||||
|
</form>
|
||||||
|
<p class="auth-footer">Уже есть аккаунт? <a href="login.php">Войти</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
264
ticket.php
Normal file
264
ticket.php
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
<?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>
|
||||||
Loading…
x
Reference in New Issue
Block a user