Files
angel 72b6879f4b v1.7.0: refactor max_bot to flat structure, add VCF+UserModel+NLP history context, portal pages and proxy fixes
- Refactored max_bot from nested packages to flat module structure
- Q2: Extended BotUser model (patronymic, email, org, address, vcf_raw, contact_hash, phone_verified, email_verified, last_interaction, total_conversations, total_tickets)
- Q2: VCF parser (FN, N, TEL, EMAIL, ORG, ADR), upsert on re-contact, NLP history context (_get_user_history_context -> YandexGPT)
- Q1: Broadcast preview modal with 10s confirmation timer
- Q3: CSS var(--white)->var(--bg-card), var(--text)->var(--text-primary)
- Q4: bot_settings showNotification(), editable max_bot_id
- Q5: Webhook secret passthrough via X-Max-Bot-Api-Secret
- Masking sensitive keys, dialog_cleared handler, migrate via _add_column_if_not_exists()
- Rate limit (asyncio.sleep 0.5 per 10), dead code removed, conv.intent context in contact.py
- Portal pages: bot_consent, bot_kb (edit), bot_settings, bot_test, bot_tickets, portal_settings
- Tests: 21/21 passing, added test_yandex_gpt.py, test_email_sender.py
- Deploy: deploy_full.sh, schema.sql, seed_knowledge_base.sql
2026-05-29 02:30:30 +03:00

221 lines
10 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('engineer', 'technician');
$pdo = getDB();
$message = '';
$userId = $session['user_id'];
$role = $session['user_role'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$objectId = (int)($_POST['object_id'] ?? 0);
$title = trim($_POST['title'] ?? '');
$desc = trim($_POST['description'] ?? '');
$severity = $_POST['severity'] ?? 'P3';
$assignedTo = (int)($_POST['assigned_to'] ?? 0);
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
} elseif (!$objectId || !$title) {
$message = 'Объект и название обязательны.';
} else {
try {
if ($id > 0) {
$stmt = $pdo->prepare("UPDATE incidents SET object_id=?, title=?, description=?, severity=?, assigned_to=? WHERE id=?");
$stmt->execute([$objectId, $title, $desc, $severity, $assignedTo ?: null, $id]);
} else {
$stmt = $pdo->prepare("INSERT INTO incidents (object_id, reported_by, assigned_to, title, description, severity) VALUES (?,?,?,?,?,?)");
$stmt->execute([$objectId, $userId, $assignedTo ?: null, $title, $desc, $severity]);
}
$message = 'Инцидент сохранён.';
auth_log($userId, 'incident_save', "Title: $title");
} catch (\PDOException $e) {
$message = 'Ошибка: ' . $e->getMessage();
}
}
}
// Resolve incident
if (isset($_GET['resolve'])) {
$id = (int)$_GET['resolve'];
$csrf = $_GET['csrf'] ?? '';
if (auth_csrf_verify($csrf)) {
$resolution = trim($_GET['resolution'] ?? '');
$pdo->prepare("UPDATE incidents SET status='resolved', resolution=?, resolved_at=NOW() WHERE id=?")
->execute([$resolution ?: 'Выполнено', $id]);
$message = 'Инцидент закрыт.';
}
}
$editInc = null;
if (isset($_GET['edit'])) {
$stmt = $pdo->prepare("SELECT * FROM incidents WHERE id=?");
$stmt->execute([(int)$_GET['edit']]);
$editInc = $stmt->fetch();
}
$statusFilter = $_GET['status'] ?? '';
$severityFilter = $_GET['severity'] ?? '';
$sql = "SELECT i.*, o.name as object_name, u.full_name as assigned_name, r.full_name as reported_name
FROM incidents i
JOIN objects o ON o.id = i.object_id
LEFT JOIN users u ON u.id = i.assigned_to
LEFT JOIN users r ON r.id = i.reported_by
WHERE 1=1";
$params = [];
if ($role === 'technician') {
$sql .= " AND (i.assigned_to = ? OR i.reported_by = ?)";
$params[] = $userId;
$params[] = $userId;
}
if ($statusFilter) {
$sql .= " AND i.status = ?";
$params[] = $statusFilter;
}
if ($severityFilter) {
$sql .= " AND i.severity = ?";
$params[] = $severityFilter;
}
$sql .= " ORDER BY CASE i.severity WHEN 'P1' THEN 1 WHEN 'P2' THEN 2 WHEN 'P3' THEN 3 END, i.created_at DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$incidents = $stmt->fetchAll();
$objects = $pdo->query("SELECT id, name FROM objects WHERE status='active' ORDER BY name")->fetchAll();
$engineers = $pdo->query("SELECT id, full_name FROM users WHERE is_active ORDER BY full_name")->fetchAll();
$csrf = auth_csrf_token();
require __DIR__ . '/../../inc/header.php';
?>
<div class="main-header">
<div>
<h1>Инциденты</h1>
<div class="breadcrumb"><a href="/service/dashboard.php">Главная</a> / Инциденты</div>
</div>
<button class="btn btn-primary btn-sm" onclick="openModal()">+ Создать</button>
</div>
<?php if ($message): ?>
<div class="alert alert-info"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<div class="filter-bar">
<form method="get">
<select name="severity" onchange="this.form.submit()">
<option value="">Все уровни</option>
<option value="P1" <?= $severityFilter === 'P1' ? 'selected' : '' ?>>P1 — Критический</option>
<option value="P2" <?= $severityFilter === 'P2' ? 'selected' : '' ?>>P2 — Частичный</option>
<option value="P3" <?= $severityFilter === 'P3' ? 'selected' : '' ?>>P3 — Незначительный</option>
</select>
<select name="status" onchange="this.form.submit()">
<option value="">Все статусы</option>
<option value="open" <?= $statusFilter === 'open' ? 'selected' : '' ?>>Открытые</option>
<option value="in_progress" <?= $statusFilter === 'in_progress' ? 'selected' : '' ?>>В работе</option>
<option value="resolved" <?= $statusFilter === 'resolved' ? 'selected' : '' ?>>Решённые</option>
</select>
<?php if ($statusFilter || $severityFilter): ?><a href="incidents.php" class="btn btn-secondary btn-sm">Сброс</a><?php endif; ?>
</form>
</div>
<div class="card">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Уровень</th>
<th>Название</th>
<th>Объект</th>
<th>Назначен</th>
<th>Статус</th>
<th>Создан</th>
<th class="actions">Действия</th>
</tr>
</thead>
<tbody>
<?php if (empty($incidents)): ?>
<tr><td colspan="7" style="text-align:center;color:var(--text-muted);">Нет инцидентов</td></tr>
<?php endif; ?>
<?php foreach ($incidents as $inc): ?>
<tr>
<td><span class="badge badge-<?= strtolower($inc['severity']) ?>"><?= $inc['severity'] ?></span></td>
<td><strong><?= htmlspecialchars($inc['title']) ?></strong></td>
<td><?= htmlspecialchars($inc['object_name']) ?></td>
<td><?= htmlspecialchars($inc['assigned_name'] ?: '—') ?></td>
<td><span class="badge badge-<?= $inc['status'] ?>"><?= $inc['status'] ?></span></td>
<td><?= date('d.m.Y H:i', strtotime($inc['created_at'])) ?></td>
<td class="actions">
<a href="?edit=<?= $inc['id'] ?>" class="btn btn-secondary btn-sm">✏️</a>
<?php if ($inc['status'] !== 'resolved' && $inc['status'] !== 'closed'): ?>
<a href="?resolve=<?= $inc['id'] ?>&csrf=<?= $csrf ?>" class="btn btn-success btn-sm" onclick="return confirm('Закрыть инцидент?')">✓</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="modal-overlay <?= $editInc ? 'open' : '' ?>" id="incModal">
<div class="modal">
<button class="modal-close" onclick="closeModal()">&times;</button>
<h2><?= $editInc ? 'Редактировать' : 'Создать' ?> инцидент</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="id" value="<?= $editInc['id'] ?? 0 ?>">
<div class="form-group">
<label for="title">Название *</label>
<input type="text" id="title" name="title" value="<?= htmlspecialchars($editInc['title'] ?? '') ?>" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="object_id">Объект *</label>
<select id="object_id" name="object_id" required>
<option value="">— выберите —</option>
<?php foreach ($objects as $o): ?>
<option value="<?= $o['id'] ?>" <?= ($editInc['object_id'] ?? 0) === $o['id'] ? 'selected' : '' ?>><?= htmlspecialchars($o['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="severity">Уровень</label>
<select id="severity" name="severity">
<option value="P1" <?= ($editInc['severity'] ?? '') === 'P1' ? 'selected' : '' ?>>P1 — Критический</option>
<option value="P2" <?= ($editInc['severity'] ?? '') === 'P2' ? 'selected' : '' ?>>P2 — Частичный</option>
<option value="P3" <?= ($editInc['severity'] ?? '') === 'P3' ? 'selected' : '' ?>>P3 — Незначительный</option>
</select>
</div>
</div>
<div class="form-group">
<label for="assigned_to">Назначить</label>
<select id="assigned_to" name="assigned_to">
<option value="">— не назначено —</option>
<?php foreach ($engineers as $e): ?>
<option value="<?= $e['id'] ?>" <?= ($editInc['assigned_to'] ?? 0) === $e['id'] ? 'selected' : '' ?>><?= htmlspecialchars($e['full_name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="description">Описание</label>
<textarea id="description" name="description"><?= htmlspecialchars($editInc['description'] ?? '') ?></textarea>
</div>
<button type="submit" class="btn btn-primary">Сохранить</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Отмена</button>
</form>
</div>
</div>
<script>
function openModal() { document.getElementById('incModal').classList.add('open'); }
function closeModal() { document.getElementById('incModal').classList.remove('open'); window.location.href = 'incidents.php'; }
</script>
<?php require __DIR__ . '/../../inc/footer.php'; ?>