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

237 lines
11 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'];
// Create / update task (engineer+)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $role !== 'technician') {
$id = (int)($_POST['id'] ?? 0);
$objectId = (int)($_POST['object_id'] ?? 0);
$assignedTo = (int)($_POST['assigned_to'] ?? 0);
$title = trim($_POST['title'] ?? '');
$desc = trim($_POST['description'] ?? '');
$priority = $_POST['priority'] ?? 'P3';
$deadline = $_POST['deadline'] ?? '';
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
} elseif (!$objectId || !$title) {
$message = 'Объект и название обязательны.';
} else {
try {
if ($id > 0) {
$stmt = $pdo->prepare("UPDATE tasks SET object_id=?, assigned_to=?, title=?, description=?, priority=?, deadline=? WHERE id=?");
$stmt->execute([$objectId, $assignedTo ?: null, $title, $desc, $priority, $deadline ?: null, $id]);
} else {
$stmt = $pdo->prepare("INSERT INTO tasks (object_id, assigned_to, created_by, title, description, priority, deadline) VALUES (?,?,?,?,?,?,?)");
$stmt->execute([$objectId, $assignedTo ?: null, $userId, $title, $desc, $priority, $deadline ?: null]);
}
$message = 'Задача сохранена.';
auth_log($userId, 'task_save', "Title: $title");
} catch (\PDOException $e) {
$message = 'Ошибка: ' . $e->getMessage();
}
}
}
// Close task
if (isset($_GET['close'])) {
$id = (int)$_GET['close'];
$csrf = $_GET['csrf'] ?? '';
if (auth_csrf_verify($csrf)) {
$pdo->prepare("UPDATE tasks SET status='completed', closed_at=NOW() WHERE id=? AND (assigned_to=? OR ? IN ('owner','engineer'))")
->execute([$id, $userId, $role]);
$message = 'Задача закрыта.';
}
}
$editTask = null;
if (isset($_GET['edit']) && $role !== 'technician') {
$stmt = $pdo->prepare("SELECT * FROM tasks WHERE id=?");
$stmt->execute([(int)$_GET['edit']]);
$editTask = $stmt->fetch();
}
// Filter
$statusFilter = $_GET['status'] ?? '';
$search = trim($_GET['search'] ?? '');
$sql = "SELECT t.*, o.name as object_name, u.full_name as assigned_name
FROM tasks t
JOIN objects o ON o.id = t.object_id
LEFT JOIN users u ON u.id = t.assigned_to
WHERE 1=1";
$params = [];
if ($role === 'technician') {
$sql .= " AND t.assigned_to = ?";
$params[] = $userId;
}
if ($statusFilter) {
$sql .= " AND t.status = ?";
$params[] = $statusFilter;
}
if ($search) {
$sql .= " AND (t.title LIKE ? OR o.name LIKE ?)";
$params[] = "%$search%";
$params[] = "%$search%";
}
$sql .= " ORDER BY CASE t.priority WHEN 'P1' THEN 1 WHEN 'P2' THEN 2 WHEN 'P3' THEN 3 WHEN 'P4' THEN 4 END, t.deadline ASC, t.created_at DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$tasks = $stmt->fetchAll();
$activeObjects = $pdo->query("SELECT id, name FROM objects WHERE status='active' ORDER BY name")->fetchAll();
$engineers = $pdo->query("SELECT id, full_name FROM users WHERE role IN ('engineer','technician') AND 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>
<?php if ($role !== 'technician'): ?>
<button class="btn btn-primary btn-sm" onclick="openModal()">+ Создать</button>
<?php endif; ?>
</div>
<?php if ($message): ?>
<div class="alert alert-info"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<div class="filter-bar">
<form method="get">
<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="completed" <?= $statusFilter === 'completed' ? 'selected' : '' ?>>Завершённые</option>
</select>
<input type="text" name="search" placeholder="Поиск..." value="<?= htmlspecialchars($search) ?>">
<button type="submit" class="btn btn-secondary btn-sm">Фильтр</button>
<?php if ($statusFilter || $search): ?><a href="tasks.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>Результат</th>
<th class="actions">Действия</th>
</tr>
</thead>
<tbody>
<?php if (empty($tasks)): ?>
<tr><td colspan="8" style="text-align:center;color:var(--text-muted);">Нет задач</td></tr>
<?php endif; ?>
<?php foreach ($tasks as $t): ?>
<?php
$slaOk = null;
if ($t['deadline']) {
$slaOk = strtotime($t['deadline']) > time();
}
?>
<tr>
<td><span class="badge badge-<?= strtolower($t['priority']) ?>"><?= $t['priority'] ?></span></td>
<td><strong><?= htmlspecialchars($t['title']) ?></strong></td>
<td><?= htmlspecialchars($t['object_name']) ?></td>
<td><?= htmlspecialchars($t['assigned_name'] ?: '—') ?></td>
<td><span class="badge badge-<?= $t['status'] ?>"><?= $t['status'] ?></span></td>
<td style="color:<?= $slaOk === false ? 'var(--danger)' : 'var(--text-secondary)' ?>"><?= $t['deadline'] ? date('d.m.Y H:i', strtotime($t['deadline'])) : '—' ?></td>
<td><?= $t['sla_compliant'] !== null ? ($t['sla_compliant'] ? '<span class="badge badge-done">OK</span>' : '<span class="badge badge-critical">❌</span>') : '—' ?></td>
<td class="actions">
<?php if ($t['status'] !== 'completed'): ?>
<a href="?close=<?= $t['id'] ?>&csrf=<?= $csrf ?>" class="btn btn-success btn-sm" onclick="return confirm('Закрыть задачу?')">✓</a>
<?php endif; ?>
<?php if ($role !== 'technician'): ?>
<a href="?edit=<?= $t['id'] ?>" class="btn btn-secondary btn-sm">✏️</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if ($role !== 'technician'): ?>
<div class="modal-overlay <?= $editTask ? 'open' : '' ?>" id="taskModal">
<div class="modal">
<button class="modal-close" onclick="closeModal()">&times;</button>
<h2><?= $editTask ? 'Редактировать' : 'Создать' ?> задачу</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="id" value="<?= $editTask['id'] ?? 0 ?>">
<div class="form-group">
<label for="title">Название *</label>
<input type="text" id="title" name="title" value="<?= htmlspecialchars($editTask['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 ($activeObjects as $o): ?>
<option value="<?= $o['id'] ?>" <?= ($editTask['object_id'] ?? 0) === $o['id'] ? 'selected' : '' ?>><?= htmlspecialchars($o['name']) ?></option>
<?php endforeach; ?>
</select>
</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'] ?>" <?= ($editTask['assigned_to'] ?? 0) === $e['id'] ? 'selected' : '' ?>><?= htmlspecialchars($e['full_name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="priority">Приоритет</label>
<select id="priority" name="priority">
<option value="P1" <?= ($editTask['priority'] ?? '') === 'P1' ? 'selected' : '' ?>>P1 — Критический</option>
<option value="P2" <?= ($editTask['priority'] ?? '') === 'P2' ? 'selected' : '' ?>>P2 — Высокий</option>
<option value="P3" <?= ($editTask['priority'] ?? '') === 'P3' ? 'selected' : '' ?>>P3 — Средний</option>
<option value="P4" <?= ($editTask['priority'] ?? '') === 'P4' ? 'selected' : '' ?>>P4 — Низкий</option>
</select>
</div>
<div class="form-group">
<label for="deadline">Дедлайн</label>
<input type="datetime-local" id="deadline" name="deadline" value="<?= $editTask['deadline'] ? date('Y-m-d\TH:i', strtotime($editTask['deadline'])) : '' ?>">
</div>
</div>
<div class="form-group">
<label for="description">Описание</label>
<textarea id="description" name="description"><?= htmlspecialchars($editTask['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('taskModal').classList.add('open'); }
function closeModal() { document.getElementById('taskModal').classList.remove('open'); window.location.href = 'tasks.php'; }
</script>
<?php endif; ?>
<?php require __DIR__ . '/../../inc/footer.php'; ?>