Первоначальная версия проекта chinilich-1.0
This commit is contained in:
+611
@@ -0,0 +1,611 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$user = checkAuth($pdo);
|
||||
if (!$user) {
|
||||
redirect('index.php');
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/glpi_api.php';
|
||||
$glpi = new GlpiApi(GLPI_API_URL, GLPI_APP_TOKEN, GLPI_USERNAME, GLPI_PASSWORD);
|
||||
|
||||
$message = '';
|
||||
$messageType = '';
|
||||
|
||||
$userForms = getUserForms($pdo, $user['id']);
|
||||
|
||||
// Маппинг статусов GLPI на русские
|
||||
$statusMap = [
|
||||
'new' => 'Новая',
|
||||
'processing' => 'В обработке',
|
||||
'incoming' => 'Входящая',
|
||||
'waiting' => 'Ожидание',
|
||||
'pending' => 'На рассмотрении',
|
||||
'closed' => 'Закрыта',
|
||||
'solved' => 'Решена',
|
||||
'assigned' => 'Назначена',
|
||||
'rejected' => 'Отклонена'
|
||||
];
|
||||
|
||||
// Синхронизация с GLPI
|
||||
$updatedForms = [];
|
||||
foreach ($userForms as $form) {
|
||||
if (syncFormWithGlpi($pdo, $form, $glpi)) {
|
||||
$updatedForms[] = $form;
|
||||
}
|
||||
}
|
||||
$userForms = $updatedForms;
|
||||
|
||||
// Обработка создания заявки
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_request'])) {
|
||||
$formType = $_POST['form_type'] ?? '';
|
||||
$priority = $_POST['priority'] ?? 'medium';
|
||||
$locationId = !empty($_POST['location']) ? (int)$_POST['location'] : null;
|
||||
|
||||
$allowedTypes = ['it_request', 'anonymous_complaint'];
|
||||
if (!in_array($formType, $allowedTypes)) {
|
||||
$message = 'Выберите корректный тип заявки';
|
||||
$messageType = 'danger';
|
||||
} else {
|
||||
$formData = [];
|
||||
$glpiDescription = '';
|
||||
$uploadedFilePath = null;
|
||||
|
||||
if (!empty($_FILES['attachment']) && $_FILES['attachment']['error'] !== UPLOAD_ERR_NO_FILE) {
|
||||
$uploadedFilePath = uploadFile($_FILES['attachment']);
|
||||
if (!$uploadedFilePath) {
|
||||
$message = 'Ошибка загрузки файла. Проверьте размер и тип.';
|
||||
$messageType = 'danger';
|
||||
}
|
||||
}
|
||||
|
||||
if (!$message) {
|
||||
if ($formType === 'it_request') {
|
||||
$computerId = $_POST['computer_id'] ?? '';
|
||||
$categoryId = $_POST['itil_category'] ?? '';
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
|
||||
if (!$computerId || !$categoryId || empty($description)) {
|
||||
$message = 'Заполните все обязательные поля IT-заявки';
|
||||
$messageType = 'danger';
|
||||
} else {
|
||||
// Получаем читаемые имена из GLPI
|
||||
$computerName = $glpi->getComputerName((int)$computerId);
|
||||
$categoryName = $glpi->getCategoryName((int)$categoryId);
|
||||
|
||||
// Данные пользователя
|
||||
$userFullName = trim($user['last_name'] . ' ' . $user['first_name'] . ' ' . $user['patronymic']);
|
||||
$userDepartment = $user['department'] ?? 'не указан';
|
||||
$userPosition = $user['position'] ?? 'не указана';
|
||||
|
||||
// Формируем красивое описание
|
||||
$glpiDescription = "Пользователь: $userFullName\n";
|
||||
$glpiDescription .= "Отдел: $userDepartment\n";
|
||||
$glpiDescription .= "Должность: $userPosition\n";
|
||||
$glpiDescription .= "Оборудование: $computerName\n";
|
||||
$glpiDescription .= "Категория: $categoryName\n";
|
||||
$glpiDescription .= "Описание проблемы:\n$description";
|
||||
|
||||
if ($uploadedFilePath) {
|
||||
$glpiDescription .= "\n\nПрикреплённый файл: " . $_SERVER['HTTP_HOST'] . '/' . $uploadedFilePath;
|
||||
}
|
||||
|
||||
$formData = [
|
||||
'computer_id' => $computerId,
|
||||
'computer_name' => $computerName,
|
||||
'itil_category' => $categoryId,
|
||||
'category_name' => $categoryName,
|
||||
'description' => $description,
|
||||
'location_id' => $locationId,
|
||||
'user_info' => [
|
||||
'name' => $userFullName,
|
||||
'department' => $userDepartment,
|
||||
'position' => $userPosition
|
||||
]
|
||||
];
|
||||
if ($uploadedFilePath) $formData['attachment'] = $uploadedFilePath;
|
||||
}
|
||||
} else {
|
||||
$subject = trim($_POST['subject'] ?? '');
|
||||
$complaintText = trim($_POST['complaint_text'] ?? '');
|
||||
|
||||
if (empty($subject) || empty($complaintText)) {
|
||||
$message = 'Укажите тему и текст жалобы';
|
||||
$messageType = 'danger';
|
||||
} else {
|
||||
// Для анонимной жалобы не пишем данные пользователя
|
||||
$glpiDescription = "Тема: $subject\n\nТекст жалобы:\n$complaintText";
|
||||
if ($uploadedFilePath) {
|
||||
$glpiDescription .= "\n\nПрикреплённый файл: " . $_SERVER['HTTP_HOST'] . '/' . $uploadedFilePath;
|
||||
}
|
||||
$formData = ['subject' => $subject, 'text' => $complaintText];
|
||||
if ($uploadedFilePath) $formData['attachment'] = $uploadedFilePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$message) {
|
||||
$glpiData = [
|
||||
'name' => $formType === 'it_request' ? 'IT-заявка' : 'Анонимная жалоба',
|
||||
'content' => $glpiDescription,
|
||||
'priority' => $priority === 'high' ? 4 : ($priority === 'medium' ? 2 : 1), // 1-5, обычно 3 средний, но уточните по вашей конфигурации
|
||||
'type' => 1, // инцидент
|
||||
'requesttypes_id' => 1, // по умолчанию
|
||||
];
|
||||
|
||||
if ($formType === 'it_request' && !empty($computerId)) {
|
||||
$glpiData['items_id'] = ['itemtype' => 'Computer', 'items_id' => (int)$computerId];
|
||||
}
|
||||
if ($locationId) {
|
||||
$glpiData['locations_id'] = $locationId;
|
||||
}
|
||||
|
||||
// Попробуем установить автора заявки (если текущий пользователь может создавать заявки от своего имени)
|
||||
// В GLPI API можно передать _users_id_requester. Но для этого нужны права.
|
||||
// Оставим как есть, GLPI сам подставит автора из сессии API.
|
||||
|
||||
$glpiResponse = $glpi->createTicket($glpiData);
|
||||
$userIdForDb = $formType === 'it_request' ? $user['id'] : null;
|
||||
$localData = [
|
||||
'glpi_response' => $glpiResponse ? 'success' : 'error',
|
||||
'glpi_ticket_id' => $glpiResponse['id'] ?? null,
|
||||
'form_fields' => $formData,
|
||||
];
|
||||
createLocalForm($pdo, $userIdForDb, $formType, $localData, 'pending', $priority, $locationId);
|
||||
|
||||
if ($glpiResponse) {
|
||||
$message = "Заявка успешно отправлена! ID GLPI: " . ($glpiResponse['id'] ?? 'N/A');
|
||||
$messageType = 'success';
|
||||
} else {
|
||||
$message = "Ошибка при отправке в GLPI. Заявка сохранена локально.";
|
||||
$messageType = 'warning';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Повторно получаем формы после возможного обновления
|
||||
$userForms = getUserForms($pdo, $user['id']);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Панель заявок – Service Desk</title>
|
||||
<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;500;600&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="header containers">
|
||||
<div class="container">
|
||||
<h1>🧙♂️ Чинилыч</h1>
|
||||
<div class="user-info">
|
||||
<span><?= htmlspecialchars($user['last_name'] . ' ' . $user['first_name']) ?></span>
|
||||
<button id="themeToggle" class="theme-toggle" aria-label="Тёмная тема">🌙</button>
|
||||
<a href="logout.php" class="btn btn-outline">Выход</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<div class="nav-links">
|
||||
<a href="dashboard.php" class="active">📋 Заявки</a>
|
||||
<a href="knowledge.php">📚 База знаний</a>
|
||||
</div>
|
||||
|
||||
<?php if ($message): ?>
|
||||
<div class="alert alert-<?= $messageType ?>"><?= htmlspecialchars($message) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="info-block">
|
||||
<p><strong>📌 Как это работает?</strong> Выберите тип заявки: ИТ‑заявка или анонимная жалоба. Для ИТ‑заявок поля подгружаются из GLPI. При выборе оборудования локация подставится автоматически. Заполните все поля, при необходимости прикрепите файл, и нажмите «Отправить».</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>➕ Новая заявка</h2>
|
||||
<form method="post" enctype="multipart/form-data" id="requestForm">
|
||||
<div class="form-group">
|
||||
<label for="form_type">Тип заявки</label>
|
||||
<select id="form_type" name="form_type" required>
|
||||
<option value="">-- Выберите --</option>
|
||||
<option value="it_request">🖥️ ИТ-заявка</option>
|
||||
<option value="anonymous_complaint">🕵️ Анонимная жалоба</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="priority">Приоритет</label>
|
||||
<select id="priority" name="priority">
|
||||
<option value="low">Низкий</option>
|
||||
<option value="medium" selected>Средний</option>
|
||||
<option value="high">Высокий</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="dynamicFields"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="attachment">📎 Прикрепить файл (скриншот, документ)</label>
|
||||
<input type="file" id="attachment" name="attachment" accept="image/*,.pdf">
|
||||
<div class="hint">Максимальный размер: 5 МБ. Допустимые типы: JPG, PNG, GIF, PDF.</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" name="create_request" class="btn btn-primary">Отправить заявку</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>📋 Мои заявки</h2>
|
||||
<div class="table-wrapper">
|
||||
<?php if (empty($userForms)): ?>
|
||||
<p style="color: var(--text-muted);">У вас пока нет заявок.</p>
|
||||
<?php else: ?>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>ID</th><th>Название</th><th>Приоритет</th><th>Статус</th><th>Дата</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($userForms as $form):
|
||||
$formData = json_decode($form['data'], true);
|
||||
$glpiId = $formData['glpi_ticket_id'] ?? null;
|
||||
$glpiName = $formData['glpi_name'] ?? $form['type'];
|
||||
$statusRaw = strtolower($form['status']);
|
||||
// Перевод статуса
|
||||
$statusRu = $statusMap[$statusRaw] ?? ucfirst($statusRaw);
|
||||
$priorityRaw = strtolower($form['priority']);
|
||||
?>
|
||||
<tr>
|
||||
<td>#<?= $form['id'] ?></td>
|
||||
<td><?= htmlspecialchars($glpiName) ?><?php if ($glpiId): ?><br><small class="hint">GLPI #<?= $glpiId ?></small><?php endif; ?></td>
|
||||
<td><span class="priority priority-<?= $priorityRaw ?>"><?= $priorityRaw === 'low' ? 'Низкий' : ($priorityRaw === 'medium' ? 'Средний' : ($priorityRaw === 'high' ? 'Высокий' : $priorityRaw)) ?></span></td>
|
||||
<td><span class="status status-<?= $statusRaw ?>"><?= htmlspecialchars($statusRu) ?></span></td>
|
||||
<td><?= date('d.m.Y H:i', strtotime($form['created_at'])) ?></td>
|
||||
<td><?php if ($glpiId): ?><button class="btn btn-outline btn-sm view-ticket" data-ticket-id="<?= $glpiId ?>">Подробнее</button><?php else: ?><span class="hint">Ожидает GLPI</span><?php endif; ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAQ блок -->
|
||||
<div class="card">
|
||||
<h2>❓ Часто задаваемые вопросы</h2>
|
||||
<div class="faq-item"><input type="checkbox" id="faq1" class="faq-toggle"><label for="faq1" class="faq-question">Как долго обрабатывается заявка?</label><div class="faq-answer"><p>Обычно заявки рассматриваются в течение 2 рабочих часов.</p></div></div>
|
||||
<div class="faq-item"><input type="checkbox" id="faq2" class="faq-toggle"><label for="faq2" class="faq-question">Что делать, если компьютер не загружается?</label><div class="faq-answer"><p>Создайте ИТ‑заявку с категорией «Аппаратная неисправность».</p></div></div>
|
||||
<div class="faq-item"><input type="checkbox" id="faq3" class="faq-toggle"><label for="faq3" class="faq-question">Можно ли подать жалобу анонимно?</label><div class="faq-answer"><p>Да, для этого выберите тип «Анонимная жалоба».</p></div></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>🔗 Полезные ссылки</h2>
|
||||
<div class="external-links">
|
||||
<a href="https://glpi.yourdomain.com" target="_blank" class="btn-link">🌐 Портал GLPI</a>
|
||||
<a href="https://kb.yourdomain.com" target="_blank" class="btn-link">📚 База знаний</a>
|
||||
<a href="mailto:support@yourdomain.com" class="btn-link">📧 Написать в поддержку</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Модальное окно деталей заявки (улучшенное) -->
|
||||
<div class="modal-overlay" id="ticketModal">
|
||||
<div class="modal modal-ticket">
|
||||
<div class="modal-header">
|
||||
<h3>Заявка <span id="modalTicketId"></span></h3>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modalContent">
|
||||
<div class="loading-spinner">Загрузка...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Стили для модального окна заявки */
|
||||
.modal-ticket {
|
||||
max-width: 750px;
|
||||
width: 90%;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ticket-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid var(--border-light);
|
||||
}
|
||||
.ticket-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
.ticket-status {
|
||||
display: inline-block;
|
||||
padding: 6px 14px;
|
||||
border-radius: 30px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 25px;
|
||||
background: var(--bg-light);
|
||||
padding: 15px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.info-value {
|
||||
color: var(--text-dark);
|
||||
word-break: break-word;
|
||||
}
|
||||
.description-box {
|
||||
background: var(--bg-white);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: 16px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.description-box h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
.followups-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.followup-card {
|
||||
background: var(--bg-light);
|
||||
border-radius: 16px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.followup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
border-bottom: 1px dashed var(--border-light);
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.followup-author {
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
.followup-content {
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.no-followups {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
.info-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.ticket-header {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Тёмная тема
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
if (localStorage.getItem('theme') === 'dark') {
|
||||
document.body.classList.add('dark');
|
||||
themeToggle.textContent = '☀️';
|
||||
}
|
||||
themeToggle.addEventListener('click', () => {
|
||||
document.body.classList.toggle('dark');
|
||||
const isDark = document.body.classList.contains('dark');
|
||||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||||
themeToggle.textContent = isDark ? '☀️' : '🌙';
|
||||
});
|
||||
|
||||
// Динамические поля формы
|
||||
const typeSelect = document.getElementById('form_type');
|
||||
const dynamicContainer = document.getElementById('dynamicFields');
|
||||
let glpiDataCache = { locations: null, computers: null, itilcategories: null };
|
||||
|
||||
async function fetchGlpiData(type) {
|
||||
if (glpiDataCache[type]) return glpiDataCache[type];
|
||||
try {
|
||||
const response = await fetch(`get_glpi_data.php?type=${type}`, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
glpiDataCache[type] = data;
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function createSelectOptions(items) {
|
||||
let options = '<option value="">-- Выберите --</option>';
|
||||
items.forEach(item => { options += `<option value="${item.id}">${escapeHtml(item.name)}</option>`; });
|
||||
return options;
|
||||
}
|
||||
|
||||
async function renderFields() {
|
||||
const type = typeSelect.value;
|
||||
if (type === 'it_request') {
|
||||
dynamicContainer.innerHTML = '<div class="loading">Загрузка справочников...</div>';
|
||||
try {
|
||||
const [locations, computers, categories] = await Promise.all([
|
||||
fetchGlpiData('locations'),
|
||||
fetchGlpiData('computers'),
|
||||
fetchGlpiData('itilcategories')
|
||||
]);
|
||||
let computersOptions = '';
|
||||
computers.forEach(item => {
|
||||
computersOptions += `<option value="${escapeHtml(item.name)}" data-id="${item.id}">${escapeHtml(item.name)}</option>`;
|
||||
});
|
||||
let html = `
|
||||
<div class="field-group">
|
||||
<h3>Информация об оборудовании</h3>
|
||||
<div class="form-group">
|
||||
<label for="location">Местоположение</label>
|
||||
<select id="location" name="location">${createSelectOptions(locations)}</select>
|
||||
<div class="hint">Автоматически подставится при выборе оборудования</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="computer_search">Оборудование *</label>
|
||||
<input type="text" id="computer_search" list="computers_list" placeholder="Начните вводить название или инв. номер" autocomplete="off" required>
|
||||
<datalist id="computers_list">${computersOptions}</datalist>
|
||||
<input type="hidden" id="computer_id" name="computer_id" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="itil_category">Категория проблемы *</label>
|
||||
<select id="itil_category" name="itil_category" required>${createSelectOptions(categories)}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">Описание проблемы *</label>
|
||||
<textarea id="description" name="description" rows="4" required placeholder="Подробно опишите, что случилось..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
dynamicContainer.innerHTML = html;
|
||||
|
||||
const computerSearch = document.getElementById('computer_search');
|
||||
const computerIdField = document.getElementById('computer_id');
|
||||
const locationSelect = document.getElementById('location');
|
||||
const datalist = document.getElementById('computers_list');
|
||||
|
||||
computerSearch.addEventListener('change', async function() {
|
||||
const val = this.value;
|
||||
const option = Array.from(datalist.options).find(opt => opt.value === val);
|
||||
if (option && option.dataset.id) {
|
||||
const id = option.dataset.id;
|
||||
computerIdField.value = id;
|
||||
try {
|
||||
const resp = await fetch(`get_computer_location.php?id=${id}`, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
const locData = await resp.json();
|
||||
if (locData.location_id && locationSelect) {
|
||||
locationSelect.value = locData.location_id;
|
||||
}
|
||||
} catch(e) { console.error(e); }
|
||||
} else {
|
||||
computerIdField.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('requestForm').addEventListener('submit', function(e) {
|
||||
if (typeSelect.value === 'it_request' && !computerIdField.value) {
|
||||
e.preventDefault();
|
||||
alert('Пожалуйста, выберите оборудование из списка.');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
dynamicContainer.innerHTML = '<div class="error-message">Ошибка загрузки справочников.</div>';
|
||||
}
|
||||
} else if (type === 'anonymous_complaint') {
|
||||
dynamicContainer.innerHTML = `
|
||||
<div class="field-group">
|
||||
<h3>Анонимная жалоба</h3>
|
||||
<div class="form-group"><label for="subject">Тема *</label><input type="text" id="subject" name="subject" required placeholder="Краткая суть жалобы"></div>
|
||||
<div class="form-group"><label for="complaint_text">Текст жалобы *</label><textarea id="complaint_text" name="complaint_text" rows="6" required placeholder="Опишите ситуацию..."></textarea></div>
|
||||
<p class="hint">Жалоба будет отправлена анонимно, ваше имя не будет указано.</p>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
dynamicContainer.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
typeSelect.addEventListener('change', renderFields);
|
||||
if (typeSelect.value) renderFields();
|
||||
|
||||
// Модальное окно заявки (улучшенное)
|
||||
const modal = document.getElementById('ticketModal');
|
||||
const modalTicketId = document.getElementById('modalTicketId');
|
||||
const modalContent = document.getElementById('modalContent');
|
||||
function closeModal() { modal.style.display = 'none'; }
|
||||
window.addEventListener('click', (e) => { if (e.target === modal) closeModal(); });
|
||||
|
||||
document.querySelectorAll('.view-ticket').forEach(btn => {
|
||||
btn.addEventListener('click', async function() {
|
||||
const ticketId = this.dataset.ticketId;
|
||||
modal.style.display = 'flex';
|
||||
modalTicketId.textContent = '#' + ticketId;
|
||||
modalContent.innerHTML = '<div class="loading-spinner">Загрузка...</div>';
|
||||
try {
|
||||
const response = await fetch(`get_ticket_details.php?id=${ticketId}`, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
||||
const data = await response.json();
|
||||
if (data.error) throw new Error(data.error);
|
||||
|
||||
// Формируем красивый HTML
|
||||
let html = `
|
||||
<div class="ticket-header">
|
||||
<div class="ticket-title">${escapeHtml(data.name || 'Заявка')}</div>
|
||||
<span class="ticket-status status status-${data.status.toLowerCase()}">${escapeHtml(data.status)}</span>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-label">Приоритет</div><div class="info-value"><span class="priority priority-${data.priority.toLowerCase()}">${escapeHtml(data.priority)}</span></div>
|
||||
<div class="info-label">Дата создания</div><div class="info-value">${escapeHtml(data.date)}</div>
|
||||
${data.solvedate ? `<div class="info-label">Дата решения</div><div class="info-value">${escapeHtml(data.solvedate)}</div>` : ''}
|
||||
</div>
|
||||
<div class="description-box">
|
||||
<h4>📝 Описание</h4>
|
||||
<div>${data.content}</div>
|
||||
</div>
|
||||
<h4>💬 Комментарии и ход работ</h4>
|
||||
<div class="followups-list">
|
||||
`;
|
||||
|
||||
if (data.followups && data.followups.length > 0) {
|
||||
data.followups.forEach(f => {
|
||||
html += `
|
||||
<div class="followup-card">
|
||||
<div class="followup-header">
|
||||
<span class="followup-author">${escapeHtml(f.author)}</span>
|
||||
<span>${escapeHtml(f.date)}</span>
|
||||
</div>
|
||||
<div class="followup-content">${escapeHtml(f.content)}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
html += `<div class="no-followups">Комментариев пока нет.</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
modalContent.innerHTML = html;
|
||||
} catch (error) {
|
||||
modalContent.innerHTML = '<div class="error-message">Не удалось загрузить детали заявки.</div>';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/[&<>]/g, function(m) {
|
||||
if (m === '&') return '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
return m;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user