Initial commit

This commit is contained in:
2026-05-17 05:22:06 +03:00
commit ca4d00c895
155 changed files with 45216 additions and 0 deletions
+220
View File
@@ -0,0 +1,220 @@
<?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 FIELD(i.severity,'P1','P2','P3'), 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=1 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'; ?>
+229
View File
@@ -0,0 +1,229 @@
<?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'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$objectId = (int)($_POST['object_id'] ?? 0);
$type = $_POST['report_type'] ?? 'service';
$title = trim($_POST['title'] ?? '');
$desc = trim($_POST['description'] ?? '');
$findings = trim($_POST['findings'] ?? '');
$recs = trim($_POST['recommendations'] ?? '');
$workDone = trim($_POST['work_done'] ?? '');
$cause = trim($_POST['cause_classification'] ?? '');
$status = $_POST['status'] ?? 'draft';
$result = $_POST['result_status'] ?? 'ok';
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
} elseif (!$objectId) {
$message = 'Объект обязателен.';
} else {
try {
if ($id > 0) {
$stmt = $pdo->prepare("UPDATE reports SET object_id=?, report_type=?, title=?, description=?, findings=?, recommendations=?, work_done=?, cause_classification=?, status=?, result_status=? WHERE id=? AND created_by=?");
$stmt->execute([$objectId, $type, $title, $desc, $findings, $recs, $workDone, $cause, $status, $result, $id, $userId]);
} else {
$stmt = $pdo->prepare("INSERT INTO reports (object_id, created_by, report_type, title, description, findings, recommendations, work_done, cause_classification, status, result_status) VALUES (?,?,?,?,?,?,?,?,?,?,?)");
$stmt->execute([$objectId, $userId, $type, $title, $desc, $findings, $recs, $workDone, $cause, $status, $result]);
}
$message = 'Отчёт сохранён.';
auth_log($userId, 'report_save', "Object: $objectId");
} catch (\PDOException $e) {
$message = 'Ошибка: ' . $e->getMessage();
}
}
}
$editReport = null;
if (isset($_GET['edit'])) {
$stmt = $pdo->prepare("SELECT * FROM reports WHERE id=? AND created_by=?");
$stmt->execute([(int)$_GET['edit'], $userId]);
$editReport = $stmt->fetch();
}
$typeFilter = $_GET['type'] ?? '';
$sql = "SELECT r.*, o.name as object_name FROM reports r JOIN objects o ON o.id=r.object_id WHERE 1=1";
$params = [];
if ($session['user_role'] === 'technician') {
$sql .= " AND r.created_by = ?";
$params[] = $userId;
}
if ($typeFilter) {
$sql .= " AND r.report_type = ?";
$params[] = $typeFilter;
}
$sql .= " ORDER BY r.created_at DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$reports = $stmt->fetchAll();
$objects = $pdo->query("SELECT id, name FROM objects WHERE status IN ('active','audit') ORDER BY 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="type" onchange="this.form.submit()">
<option value="">Все типы</option>
<option value="inspection" <?= $typeFilter === 'inspection' ? 'selected' : '' ?>>Осмотр</option>
<option value="service" <?= $typeFilter === 'service' ? 'selected' : '' ?>>Обслуживание</option>
<option value="emergency" <?= $typeFilter === 'emergency' ? 'selected' : '' ?>>Аварийный</option>
<option value="audit" <?= $typeFilter === 'audit' ? 'selected' : '' ?>>Аудит</option>
<option value="monthly" <?= $typeFilter === 'monthly' ? 'selected' : '' ?>>Месячный</option>
</select>
<?php if ($typeFilter): ?><a href="reports.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($reports)): ?>
<tr><td colspan="7" style="text-align:center;color:var(--text-muted);">Нет отчётов</td></tr>
<?php endif; ?>
<?php foreach ($reports as $r): ?>
<tr>
<td><span class="badge badge-medium"><?= $r['report_type'] ?></span></td>
<td><strong><?= htmlspecialchars($r['title'] ?: 'Без названия') ?></strong></td>
<td><?= htmlspecialchars($r['object_name']) ?></td>
<td><span class="badge badge-<?= $r['status'] ?>"><?= $r['status'] ?></span></td>
<td><span class="badge badge-<?= $r['result_status'] === 'ok' ? 'done' : ($r['result_status'] === 'partial' ? 'high' : 'medium') ?>"><?= $r['result_status'] ?></span></td>
<td><?= date('d.m.Y', strtotime($r['created_at'])) ?></td>
<td class="actions">
<a href="?edit=<?= $r['id'] ?>" class="btn btn-secondary btn-sm">✏️</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="modal-overlay <?= $editReport ? 'open' : '' ?>" id="reportModal">
<div class="modal">
<button class="modal-close" onclick="closeModal()">&times;</button>
<h2><?= $editReport ? 'Редактировать' : 'Создать' ?> отчёт</h2>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="id" value="<?= $editReport['id'] ?? 0 ?>">
<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'] ?>" <?= ($editReport['object_id'] ?? 0) === $o['id'] ? 'selected' : '' ?>><?= htmlspecialchars($o['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="report_type">Тип</label>
<select id="report_type" name="report_type">
<option value="inspection" <?= ($editReport['report_type'] ?? '') === 'inspection' ? 'selected' : '' ?>>Осмотр</option>
<option value="service" <?= ($editReport['report_type'] ?? '') === 'service' ? 'selected' : '' ?>>Обслуживание</option>
<option value="emergency" <?= ($editReport['report_type'] ?? '') === 'emergency' ? 'selected' : '' ?>>Аварийный</option>
<option value="audit" <?= ($editReport['report_type'] ?? '') === 'audit' ? 'selected' : '' ?>>Аудит</option>
<option value="monthly" <?= ($editReport['report_type'] ?? '') === 'monthly' ? 'selected' : '' ?>>Месячный</option>
</select>
</div>
</div>
<div class="form-group">
<label for="title">Название</label>
<input type="text" id="title" name="title" value="<?= htmlspecialchars($editReport['title'] ?? '') ?>">
</div>
<div class="form-row">
<div class="form-group">
<label for="status">Статус</label>
<select id="status" name="status">
<option value="draft" <?= ($editReport['status'] ?? '') === 'draft' ? 'selected' : '' ?>>Черновик</option>
<option value="final" <?= ($editReport['status'] ?? '') === 'final' ? 'selected' : '' ?>>Итоговый</option>
</select>
</div>
<div class="form-group">
<label for="result_status">Результат</label>
<select id="result_status" name="result_status">
<option value="ok" <?= ($editReport['result_status'] ?? '') === 'ok' ? 'selected' : '' ?>>Исправлено</option>
<option value="fixed" <?= ($editReport['result_status'] ?? '') === 'fixed' ? 'selected' : '' ?>>Исправлено</option>
<option value="partial" <?= ($editReport['result_status'] ?? '') === 'partial' ? 'selected' : '' ?>>Частично</option>
<option value="revisit" <?= ($editReport['result_status'] ?? '') === 'revisit' ? 'selected' : '' ?>>Повторный выезд</option>
</select>
</div>
</div>
<div class="form-group">
<label for="description">Описание</label>
<textarea id="description" name="description"><?= htmlspecialchars($editReport['description'] ?? '') ?></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="findings">Выявленные проблемы</label>
<textarea id="findings" name="findings"><?= htmlspecialchars($editReport['findings'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label for="recommendations">Рекомендации</label>
<textarea id="recommendations" name="recommendations"><?= htmlspecialchars($editReport['recommendations'] ?? '') ?></textarea>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="work_done">Выполненные работы</label>
<textarea id="work_done" name="work_done"><?= htmlspecialchars($editReport['work_done'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label for="cause_classification">Классификация причины</label>
<select id="cause_classification" name="cause_classification">
<option value="">—</option>
<option value="Hardware" <?= ($editReport['cause_classification'] ?? '') === 'Hardware' ? 'selected' : '' ?>>Hardware</option>
<option value="Software" <?= ($editReport['cause_classification'] ?? '') === 'Software' ? 'selected' : '' ?>>Software</option>
<option value="Network" <?= ($editReport['cause_classification'] ?? '') === 'Network' ? 'selected' : '' ?>>Network</option>
<option value="Human error" <?= ($editReport['cause_classification'] ?? '') === 'Human error' ? 'selected' : '' ?>>Человеческий фактор</option>
<option value="External" <?= ($editReport['cause_classification'] ?? '') === 'External' ? 'selected' : '' ?>>Внешний фактор</option>
</select>
</div>
</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('reportModal').classList.add('open'); }
function closeModal() { document.getElementById('reportModal').classList.remove('open'); window.location.href = 'reports.php'; }
</script>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
+236
View File
@@ -0,0 +1,236 @@
<?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 FIELD(t.priority,'P1','P2','P3','P4'), 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=1 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'; ?>
+128
View File
@@ -0,0 +1,128 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
$session = auth_require('owner');
$pdo = getDB();
$message = '';
// Assign
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$objectId = (int)($_POST['object_id'] ?? 0);
$userId = (int)($_POST['user_id'] ?? 0);
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
} elseif ($objectId && $userId) {
// Роль берётся из профиля сотрудника автоматически
$stmt = $pdo->prepare("SELECT role FROM users WHERE id=?");
$stmt->execute([$userId]);
$userRole = $stmt->fetchColumn();
if ($userRole) {
$stmt = $pdo->prepare("INSERT INTO object_assignments (object_id, user_id, role) VALUES (?,?,?)");
$stmt->execute([$objectId, $userId, $userRole]);
$message = 'Назначение добавлено.';
auth_log($session['user_id'], 'assignment_create', "Object: $objectId, User: $userId");
} else {
$message = 'Сотрудник не найден.';
}
}
}
// Unassign
if (isset($_GET['remove'])) {
$id = (int)$_GET['remove'];
if (auth_csrf_verify($_GET['csrf'] ?? '')) {
$pdo->prepare("UPDATE object_assignments SET unassigned_at=NOW() WHERE id=?")->execute([$id]);
$message = 'Назначение завершено.';
}
}
$objects = $pdo->query("SELECT id, name FROM objects WHERE status='active' ORDER BY name")->fetchAll();
$engineers = $pdo->query("SELECT id, full_name, role FROM users WHERE role IN ('engineer','technician') AND is_active=1 ORDER BY full_name")->fetchAll();
// Get active assignments
$assignments = $pdo->query("
SELECT oa.*, o.name as object_name, u.full_name as user_name, u.role as user_role
FROM object_assignments oa
JOIN objects o ON o.id = oa.object_id
JOIN users u ON u.id = oa.user_id
WHERE oa.unassigned_at IS NULL
ORDER BY o.name, u.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>
</div>
<?php if ($message): ?>
<div class="alert alert-info"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<div class="card">
<div class="card-header"><h2>Новое назначение</h2></div>
<form method="post" class="form-row" style="align-items:end;">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<div class="form-group">
<label>Объект</label>
<select name="object_id" required>
<option value="">— выберите —</option>
<?php foreach ($objects as $o): ?>
<option value="<?= $o['id'] ?>"><?= htmlspecialchars($o['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>Сотрудник</label>
<select name="user_id" required>
<option value="">— выберите —</option>
<?php foreach ($engineers as $e): ?>
<option value="<?= $e['id'] ?>"><?= htmlspecialchars($e['full_name']) ?> (<?= $e['role'] === 'engineer' ? 'Инженер' : 'Техник' ?>)</option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary">Назначить</button>
</form>
</div>
<div class="card">
<div class="card-header"><h2>Текущие назначения</h2></div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Объект</th>
<th>Сотрудник</th>
<th>Роль на объекте</th>
<th>Назначен</th>
<th class="actions">Действия</th>
</tr>
</thead>
<tbody>
<?php if (empty($assignments)): ?>
<tr><td colspan="5" style="text-align:center;color:var(--text-muted);">Нет активных назначений</td></tr>
<?php endif; ?>
<?php foreach ($assignments as $a): ?>
<tr>
<td><?= htmlspecialchars($a['object_name']) ?></td>
<td><?= htmlspecialchars($a['user_name']) ?></td>
<td><span class="badge badge-<?= $a['role'] === 'engineer' ? 'high' : 'medium' ?>"><?= $a['role'] === 'engineer' ? 'Инженер' : 'Техник' ?></span></td>
<td><?= date('d.m.Y H:i', strtotime($a['assigned_at'])) ?></td>
<td class="actions">
<a href="?remove=<?= $a['id'] ?>&csrf=<?= $csrf ?>" class="btn btn-danger btn-sm" onclick="return confirm('Отвязать сотрудника от объекта?')" data-tooltip="Отвязать">Отвязать</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
+256
View File
@@ -0,0 +1,256 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('owner');
$pdo = getDB();
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$title = trim($_POST['title'] ?? '');
$slug = trim($_POST['slug'] ?? '');
$content = $_POST['content'] ?? '';
$excerpt = trim($_POST['excerpt'] ?? '');
$category = $_POST['category'] ?? 'risk-engineering';
$status = $_POST['status'] ?? 'draft';
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
} elseif (!$title || !$content) {
$message = 'Заголовок и содержание обязательны.';
} else {
if (!$slug) {
$slug = preg_replace('/[^a-z0-9-]/', '', mb_strtolower(str_replace(' ', '-', $title)));
}
try {
if ($id > 0) {
$stmt = $pdo->prepare("UPDATE blog_posts SET title=?, slug=?, content=?, excerpt=?, category=?, status=? WHERE id=?");
$stmt->execute([$title, $slug, $content, $excerpt, $category, $status, $id]);
$message = 'Пост обновлён.';
} else {
$stmt = $pdo->prepare("INSERT INTO blog_posts (title, slug, content, excerpt, category, author_id, status, published_at) VALUES (?,?,?,?,?,?,?,?)");
$published = $status === 'published' ? date('Y-m-d H:i:s') : null;
$stmt->execute([$title, $slug, $content, $excerpt, $category, $session['user_id'], $status, $published]);
$message = 'Пост создан.';
}
auth_log($session['user_id'], 'blog_save', "Title: $title");
} catch (\PDOException $e) {
$message = 'Ошибка: ' . ($e->getCode() === '23000' ? 'Такой slug уже существует.' : $e->getMessage());
}
}
}
if (isset($_GET['delete'])) {
$id = (int)$_GET['delete'];
if (auth_csrf_verify($_GET['csrf'] ?? '')) {
$pdo->prepare("DELETE FROM blog_posts WHERE id=?")->execute([$id]);
$message = 'Пост удалён.';
}
}
if (isset($_GET['publish'])) {
$id = (int)$_GET['publish'];
if (auth_csrf_verify($_GET['csrf'] ?? '')) {
$pdo->prepare("UPDATE blog_posts SET status='published', published_at=NOW() WHERE id=?")->execute([$id]);
$message = 'Пост опубликован.';
}
}
$editPost = null;
if (isset($_GET['edit'])) {
$stmt = $pdo->prepare("SELECT * FROM blog_posts WHERE id=?");
$stmt->execute([(int)$_GET['edit']]);
$editPost = $stmt->fetch();
}
$posts = $pdo->query("SELECT bp.*, u.full_name as author_name FROM blog_posts bp LEFT JOIN users u ON u.id=bp.author_id ORDER BY bp.created_at DESC")->fetchAll();
$csrf = auth_csrf_token();
$categories = [
'audit' => 'Аудит',
'sla' => 'SLA',
'incident' => 'Инциденты',
'supervision' => 'Технадзор',
'documentation' => 'Документация',
'risk' => 'Риск-инжиниринг',
'cases' => 'Кейсы',
];
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()" data-tooltip="Создать новую статью">+ Новая статья</button>
</div>
<?php if ($message): ?>
<div class="alert alert-info"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<div class="card">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Заголовок</th>
<th>Категория</th>
<th>Автор</th>
<th>Статус</th>
<th>Дата</th>
<th class="actions">Действия</th>
</tr>
</thead>
<tbody>
<?php if (empty($posts)): ?>
<tr><td colspan="6" style="text-align:center;color:var(--text-muted);">Нет статей</td></tr>
<?php endif; ?>
<?php foreach ($posts as $p): ?>
<tr>
<td><strong><?= htmlspecialchars($p['title']) ?></strong></td>
<td><span class="badge badge-medium"><?= $categories[$p['category']] ?? $p['category'] ?></span></td>
<td><?= htmlspecialchars($p['author_name'] ?: '—') ?></td>
<td><span class="badge badge-<?= $p['status'] === 'published' ? 'done' : ($p['status'] === 'draft' ? 'draft' : 'cancelled') ?>"><?= $p['status'] ?></span></td>
<td><?= $p['published_at'] ? date('d.m.Y', strtotime($p['published_at'])) : date('d.m.Y', strtotime($p['created_at'])) ?></td>
<td class="actions">
<a href="/blog/<?= htmlspecialchars($p['slug']) ?>" class="btn btn-secondary btn-sm" target="_blank" data-tooltip="Просмотр">👁️</a>
<a href="?edit=<?= $p['id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="Редактировать">✏️</a>
<?php if ($p['status'] !== 'published'): ?>
<a href="?publish=<?= $p['id'] ?>&csrf=<?= $csrf ?>" class="btn btn-success btn-sm" data-tooltip="Опубликовать">📢</a>
<?php endif; ?>
<a href="?delete=<?= $p['id'] ?>&csrf=<?= $csrf ?>" class="btn btn-danger btn-sm" onclick="return confirm('Удалить статью?')" data-tooltip="Удалить">🗑️</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="modal-overlay <?= $editPost ? 'open' : '' ?>" id="blogModal">
<div class="modal" style="max-width:720px;">
<button class="modal-close" onclick="closeModal()">&times;</button>
<h2><?= $editPost ? 'Редактировать' : 'Новая' ?> статья</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="id" value="<?= $editPost['id'] ?? 0 ?>">
<div class="form-group">
<label for="title">Заголовок *</label>
<input type="text" id="title" name="title" value="<?= htmlspecialchars($editPost['title'] ?? '') ?>" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="slug">Slug (оставьте пустым для автогенерации)</label>
<input type="text" id="slug" name="slug" value="<?= htmlspecialchars($editPost['slug'] ?? '') ?>">
</div>
<div class="form-group">
<label for="category">Категория</label>
<select id="category" name="category">
<?php foreach ($categories as $key => $label): ?>
<option value="<?= $key ?>" <?= ($editPost['category'] ?? '') === $key ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="status">Статус</label>
<select id="status" name="status">
<option value="draft" <?= ($editPost['status'] ?? '') === 'draft' ? 'selected' : '' ?>>Черновик</option>
<option value="published" <?= ($editPost['status'] ?? '') === 'published' ? 'selected' : '' ?>>Опубликовать</option>
<option value="archived" <?= ($editPost['status'] ?? '') === 'archived' ? 'selected' : '' ?>>Архив</option>
</select>
</div>
<div class="form-group">
<label for="excerpt">Краткое описание</label>
<input type="text" id="excerpt" name="excerpt" value="<?= htmlspecialchars($editPost['excerpt'] ?? '') ?>">
</div>
</div>
<div class="form-group">
<label for="content">Содержание * (HTML)</label>
<div class="editor-toolbar">
<button type="button" data-insert="bold" title="Жирный"><b>Ж</b></button>
<button type="button" data-insert="italic" title="Курсив"><i>К</i></button>
<button type="button" data-insert="h2" title="Заголовок 2">H2</button>
<button type="button" data-insert="h3" title="Заголовок 3">H3</button>
<button type="button" data-insert="ul" title="Маркированный список">• Список</button>
<button type="button" data-insert="ol" title="Нумерованный список">1. Список</button>
<button type="button" data-insert="link" title="Ссылка">🔗</button>
<button type="button" data-insert="image" title="Изображение">🖼️</button>
</div>
<textarea id="content" name="content" rows="15" style="font-family:monospace;"><?= htmlspecialchars($editPost['content'] ?? '') ?></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('blogModal').classList.add('open'); }
function closeModal() { document.getElementById('blogModal').classList.remove('open'); window.location.href = 'blog.php'; }
// Slug auto-generation from title
(function(){
var titleInput = document.getElementById('title');
var slugInput = document.getElementById('slug');
if (!titleInput || !slugInput) return;
var slugTouched = slugInput.value !== '';
slugInput.addEventListener('input', function(){ slugTouched = true; });
titleInput.addEventListener('input', function(){
if (slugTouched) return;
var val = titleInput.value.toLowerCase().replace(/[^a-zа-яё0-9\s-]/gi, '').replace(/\s+/g, '-');
// Transliterate Cyrillic
var map = {'а':'a','б':'b','в':'v','г':'g','д':'d','е':'e','ё':'yo','ж':'zh','з':'z','и':'i','й':'y','к':'k','л':'l','м':'m','н':'n','о':'o','п':'p','р':'r','с':'s','т':'t','у':'u','ф':'f','х':'kh','ц':'ts','ч':'ch','ш':'sh','щ':'shch','ъ':'','ы':'y','ь':'','э':'e','ю':'yu','я':'ya'};
var result = '';
for (var i = 0; i < val.length; i++) {
var ch = val[i];
result += map[ch] !== undefined ? map[ch] : ch;
}
slugInput.value = result.replace(/-+/g, '-').replace(/^-|-$/g, '');
});
})();
// Editor toolbar
(function(){
var textarea = document.getElementById('content');
if (!textarea) return;
document.querySelectorAll('.editor-toolbar button').forEach(function(btn){
btn.addEventListener('click', function(e){
e.preventDefault();
var action = btn.getAttribute('data-insert');
var start = textarea.selectionStart;
var end = textarea.selectionEnd;
var selected = textarea.value.substring(start, end);
var insert = '';
switch(action) {
case 'bold': insert = '<strong>' + (selected || 'текст') + '</strong>'; break;
case 'italic': insert = '<em>' + (selected || 'текст') + '</em>'; break;
case 'h2': insert = '<h2>' + (selected || 'Заголовок') + '</h2>'; break;
case 'h3': insert = '<h3>' + (selected || 'Заголовок') + '</h3>'; break;
case 'ul': insert = '<ul>\n <li>' + (selected || 'Пункт') + '</li>\n</ul>'; break;
case 'ol': insert = '<ol>\n <li>' + (selected || 'Пункт') + '</li>\n</ol>'; break;
case 'link': insert = '<a href="">' + (selected || 'текст ссылки') + '</a>'; break;
case 'image': insert = '<img src="" alt="' + (selected || 'описание') + '">'; break;
}
textarea.value = textarea.value.substring(0, start) + insert + textarea.value.substring(end);
textarea.focus();
textarea.selectionStart = textarea.selectionEnd = start + insert.length;
});
});
})();
</script>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
<style>
.editor-toolbar { display:flex; gap:4px; margin-bottom:8px; flex-wrap:wrap; }
.editor-toolbar button { padding:4px 10px; background:var(--bg-input); border:1px solid var(--border); border-radius:4px; color:var(--text-primary); cursor:pointer; font-size:12px; transition:all 0.2s; }
.editor-toolbar button:hover { border-color:var(--accent); background:var(--accent-dim); }
</style>
+188
View File
@@ -0,0 +1,188 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('owner');
$pdo = getDB();
$message = '';
// Create / Update
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$title = trim($_POST['title'] ?? '');
$text = trim($_POST['text'] ?? '');
$effect = trim($_POST['effect'] ?? '');
$sort = (int)($_POST['sort_order'] ?? 0);
$isActive = isset($_POST['is_active']) ? 1 : 0;
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
} elseif ($title === '' || $text === '' || $effect === '') {
$message = 'Заполните все обязательные поля.';
} else {
try {
if ($id > 0) {
$stmt = $pdo->prepare("UPDATE cases SET title=?, text=?, effect=?, sort_order=?, is_active=? WHERE id=?");
$stmt->execute([$title, $text, $effect, $sort, $isActive, $id]);
} else {
$stmt = $pdo->prepare("INSERT INTO cases (title, text, effect, sort_order, is_active) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$title, $text, $effect, $sort, $isActive]);
}
$message = $id > 0 ? 'Пример обновлён.' : 'Пример добавлен.';
} catch (\Exception $e) {
$message = 'Ошибка БД: ' . $e->getMessage();
}
}
}
// Delete
if (isset($_GET['delete'])) {
$did = (int)$_GET['delete'];
$csrf = $_GET['csrf'] ?? '';
if (auth_csrf_verify($csrf)) {
$stmt = $pdo->prepare("DELETE FROM cases WHERE id=?");
$stmt->execute([$did]);
header('Location: /service/pages/owner/cases.php');
exit;
}
}
// Edit mode
$editId = (int)($_GET['edit'] ?? 0);
$editRow = null;
if ($editId > 0) {
$stmt = $pdo->prepare("SELECT * FROM cases WHERE id=?");
$stmt->execute([$editId]);
$editRow = $stmt->fetch();
}
// List
$stmt = $pdo->query("SELECT * FROM cases ORDER BY sort_order ASC, id ASC");
$cases = $stmt->fetchAll();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
require __DIR__ . '/../../inc/header.php';
?>
<div class="cases-admin">
<div class="page-header">
<h1>Примеры из практики</h1>
<p class="page-subtitle">Управляйте каруселью на главной странице сайта.</p>
</div>
<?php if ($message): ?>
<div class="alert <?= strpos($message, 'Ошибка') === false ? 'alert-success' : 'alert-danger' ?>"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<div class="cases-admin__form">
<h2><?= $editRow ? 'Редактировать' : 'Добавить пример' ?></h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="id" value="<?= $editRow ? $editRow['id'] : 0 ?>">
<div class="form-row">
<div class="form-group">
<label>Заголовок *</label>
<input type="text" name="title" value="<?= htmlspecialchars($editRow ? $editRow['title'] : '') ?>" required maxlength="500">
</div>
<div class="form-group">
<label>Порядок</label>
<input type="number" name="sort_order" value="<?= $editRow ? $editRow['sort_order'] : 0 ?>" min="0">
</div>
</div>
<div class="form-group">
<label>Описание проблемы *</label>
<textarea name="text" rows="3" required><?= htmlspecialchars($editRow ? $editRow['text'] : '') ?></textarea>
</div>
<div class="form-group">
<label>Эффект *</label>
<textarea name="effect" rows="2" required><?= htmlspecialchars($editRow ? $editRow['effect'] : '') ?></textarea>
</div>
<div class="form-group form-group--checkbox">
<input type="checkbox" id="is_active" name="is_active" <?= (!$editRow || $editRow['is_active']) ? 'checked' : '' ?>>
<label for="is_active">Активен</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary"><?= $editRow ? 'Сохранить' : 'Добавить' ?></button>
<?php if ($editRow): ?>
<a href="/service/pages/owner/cases.php" class="btn btn-secondary">Отмена</a>
<?php endif; ?>
</div>
</form>
</div>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th style="width:40px">#</th>
<th>Заголовок</th>
<th style="width:60px">Порядок</th>
<th style="width:70px">Статус</th>
<th style="width:120px">Действия</th>
</tr>
</thead>
<tbody>
<?php if (empty($cases)): ?>
<tr><td colspan="5" style="text-align:center;padding:24px;color:var(--text-muted);">Нет примеров</td></tr>
<?php endif; ?>
<?php foreach ($cases as $c): ?>
<tr>
<td><?= $c['id'] ?></td>
<td><?= htmlspecialchars($c['title']) ?></td>
<td><?= $c['sort_order'] ?></td>
<td>
<span class="badge <?= $c['is_active'] ? 'badge-green' : 'badge-gray' ?>">
<?= $c['is_active'] ? 'Да' : 'Нет' ?>
</span>
</td>
<td>
<a href="?edit=<?= $c['id'] ?>" class="btn btn-sm btn-info" data-tooltip="Редактировать">✎</a>
<a href="?delete=<?= $c['id'] ?>&csrf=<?= urlencode($_SESSION['csrf_token']) ?>" class="btn btn-sm btn-danger" onclick="return confirm('Удалить?')" data-tooltip="Удалить">✕</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<style>
.cases-admin { padding: 24px; }
.page-header { margin-bottom: 24px; }
.page-header h1 { font-size: 24px; color: var(--text-primary); margin-bottom: 8px; }
.page-subtitle { font-size: 13px; color: var(--text-muted); }
.cases-admin__form { background: var(--bg-card); padding: 24px; border-radius: 12px; margin-bottom: 32px; border: 1px solid var(--border); }
.cases-admin__form h2 { font-size: 18px; margin-bottom: 16px; color: var(--text-primary); }
.form-row { display: grid; grid-template-columns: 1fr 100px; gap: 16px; }
.form-group { margin-bottom: 14px; }
.form-group label { display: block; margin-bottom: 4px; font-weight: 600; font-size: 13px; color: var(--text-secondary); }
.form-group input, .form-group textarea { width: 100%; padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; font-family: inherit; background: var(--bg-input); color: var(--text-primary); }
.form-group input:focus, .form-group textarea:focus { outline: none; border-color: var(--accent); }
.form-group--checkbox { display: flex; align-items: center; gap: 8px; color: var(--text-secondary); }
.form-group--checkbox input { width: auto; }
.form-actions { display: flex; gap: 10px; margin-top: 16px; }
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
.table th, .table td { padding: 10px 14px; text-align: left; border-bottom: 1px solid var(--border); color: var(--text-primary); }
.table th { font-weight: 600; color: var(--text-muted); font-size: 11px; text-transform: uppercase; background: var(--bg-card); }
.badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; }
.badge-green { background: rgba(34,197,94,0.2); color: var(--success); }
.badge-gray { background: rgba(136,153,187,0.2); color: var(--text-secondary); }
.btn-sm { padding: 4px 10px; font-size: 13px; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; }
.btn-info { background: var(--accent); color: #fff; }
.btn-danger { background: var(--danger); color: #fff; }
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; font-size: 13px; }
.alert-success { background: rgba(34,197,94,0.15); color: var(--success); border: 1px solid rgba(34,197,94,0.3); }
.alert-danger { background: rgba(239,68,68,0.15); color: var(--danger); border: 1px solid rgba(239,68,68,0.3); }
.table-responsive { overflow-x: auto; }
</style>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
+204
View File
@@ -0,0 +1,204 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('owner');
$pdo = getDB();
// SHS history (last 12 records)
$shsHistory = $pdo->query("SELECT * FROM shs_records ORDER BY id DESC LIMIT 12")->fetchAll();
// Current SHS
$shs = $pdo->query("SELECT * FROM shs_records ORDER BY id DESC LIMIT 1")->fetch();
// MRR
$stmt = $pdo->query("SELECT SUM(sla_price_monthly) as mrr FROM sla_contracts WHERE status='active'");
$mrrData = $stmt->fetch();
$mrr = (float)($mrrData['mrr'] ?? 0);
// Active SLA contracts
$stmt = $pdo->query("SELECT COUNT(*) FROM sla_contracts WHERE status='active'");
$activeSla = $stmt->fetchColumn();
// Total objects
$stmt = $pdo->query("SELECT COUNT(*) FROM objects WHERE status='active'");
$totalObjects = $stmt->fetchColumn();
// Engineer count
$stmt = $pdo->query("SELECT COUNT(*) FROM users WHERE role IN ('engineer','technician') AND is_active=1");
$engineerCount = $stmt->fetchColumn();
// SLA compliance
$stmt = $pdo->query("SELECT COUNT(*) as total, SUM(CASE WHEN sla_compliant=TRUE THEN 1 ELSE 0 END) as compliant FROM tasks WHERE closed_at IS NOT NULL");
$slaData = $stmt->fetch();
$slaCompliance = $slaData['total'] > 0 ? round(($slaData['compliant'] / $slaData['total']) * 100, 1) : 0;
// Incident rate
$stmt = $pdo->query("SELECT COUNT(*) FROM incidents WHERE created_at > NOW() - INTERVAL '30 days'");
$incident30 = $stmt->fetchColumn();
$incidentRate = $totalObjects > 0 ? round($incident30 / $totalObjects, 2) : 0;
// Retention (active sla / total ever)
$stmt = $pdo->query("SELECT COUNT(DISTINCT object_id) FROM sla_contracts WHERE status IN ('active','expired','cancelled')");
$totalEver = $stmt->fetchColumn();
$retention = $totalEver > 0 ? round(($activeSla / $totalEver) * 100, 1) : 0;
// Engineer KPI last period
$kpiLatest = $pdo->query("SELECT ek.*, u.full_name FROM engineer_kpi ek JOIN users u ON u.id=ek.engineer_id ORDER BY ek.period_end DESC LIMIT 10")->fetchAll();
// Avg engineer score
$stmt = $pdo->query("SELECT AVG(engineer_score) FROM engineer_kpi WHERE period_end > NOW() - INTERVAL '30 days'");
$avgEngineerScore = round((float)$stmt->fetchColumn(), 1);
// Revenue per engineer
$rpe = $engineerCount > 0 ? round($mrr / $engineerCount, 2) : 0;
require __DIR__ . '/../../inc/header.php';
?>
<div class="main-header">
<div>
<h1>CEO Дашборд</h1>
<div class="breadcrumb"><a href="/service/dashboard.php">Главная</a> / CEO дашборд</div>
</div>
<span style="font-size:12px;color:var(--text-muted);">Обновлено: <?= date('d.m.Y H:i') ?></span>
</div>
<?php if ($shs && $shs['shs_score'] < 70): ?>
<div class="shs-alert critical">⚠ SHS <?= $shs['shs_score'] ?> — <?= $shs['shs_status'] ?>. Рекомендуется: снизить продажи, запустить аудит, пересмотреть назначения.</div>
<?php elseif ($shs && $shs['shs_score'] < 85): ?>
<div class="shs-alert warning">⚠ SHS <?= $shs['shs_score'] ?> — <?= $shs['shs_status'] ?>. Требуется внимание.</div>
<?php endif; ?>
<?php if ($slaCompliance < 90): ?>
<div class="shs-alert warning">⚠ SLA Compliance <?= $slaCompliance ?>% — ниже 90%. Рекомендуется заморозить неприоритетные проекты, увеличить частоту проверок.</div>
<?php endif; ?>
<?php if ($retention < 90): ?>
<div class="shs-alert critical">⚠ Удержание клиентов <?= $retention ?>% — ниже 90%. Обязательный аудит клиентов, пересмотр назначений инженеров.</div>
<?php endif; ?>
<div class="card-grid">
<div class="card">
<div class="card-label">SHS (System Health)</div>
<div class="card-value <?= $shs ? ($shs['shs_score'] >= 70 ? 'value-green' : 'value-red') : 'value-yellow' ?>"><?= $shs ? $shs['shs_score'] : '—' ?></div>
<div class="card-label"><?= $shs ? '<span class="badge badge-'.strtolower($shs['shs_status']).'">'.$shs['shs_status'].'</span>' : 'Нет данных' ?></div>
</div>
<div class="card">
<div class="card-label">MRR (месячный доход)</div>
<div class="card-value value-green"><?= fmt_money($mrr) ?></div>
<div class="card-label"><?= $activeSla ?> активных контрактов</div>
</div>
<div class="card">
<div class="card-label">SLA Compliance</div>
<div class="card-value <?= $slaCompliance >= 90 ? 'value-green' : ($slaCompliance >= 70 ? 'value-yellow' : 'value-red') ?>"><?= $slaCompliance ?>%</div>
</div>
<div class="card">
<div class="card-label">Удержание (Retention)</div>
<div class="card-value <?= $retention >= 90 ? 'value-green' : 'value-red' ?>"><?= $retention ?>%</div>
</div>
<div class="card">
<div class="card-label">Incident Rate (30 дн)</div>
<div class="card-value <?= $incidentRate < 1 ? 'value-green' : 'value-red' ?>"><?= $incidentRate ?></div>
</div>
<div class="card">
<div class="card-label">Средний Engineer Score</div>
<div class="card-value <?= $avgEngineerScore >= 80 ? 'value-green' : ($avgEngineerScore >= 70 ? 'value-yellow' : 'value-red') ?>"><?= $avgEngineerScore ?: '—' ?></div>
</div>
<div class="card">
<div class="card-label">Revenue per Engineer</div>
<div class="card-value value-accent"><?= fmt_money($rpe) ?></div>
</div>
<div class="card">
<div class="card-label">Активных объектов</div>
<div class="card-value"><?= $totalObjects ?></div>
<div class="card-label">Инженеров / техников: <?= $engineerCount ?></div>
</div>
</div>
<div class="card">
<div class="card-header"><h2>SHS Trend</h2></div>
<?php if (empty($shsHistory)): ?>
<p style="color:var(--text-muted);">Нет данных SHS. Записи появятся после создания KPI и расчётов.</p>
<?php else: ?>
<div style="overflow-x:auto;">
<table>
<thead>
<tr>
<th>Период</th>
<th>SLA Stability</th>
<th>Revenue</th>
<th>Retention</th>
<th>Engineer Perf</th>
<th>Incident Stability</th>
<th>SHS Score</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
<?php foreach ($shsHistory as $s): ?>
<tr>
<td><?= $s['period_start'] ?> — <?= $s['period_end'] ?></td>
<td><?= $s['sla_stability'] ?>%</td>
<td><?= $s['revenue_stability'] ?>%</td>
<td><?= $s['retention'] ?>%</td>
<td><?= $s['engineer_performance'] ?>%</td>
<td><?= $s['incident_stability'] ?>%</td>
<td><strong class="<?= $s['shs_score'] >= 70 ? 'value-green' : 'value-red' ?>"><?= $s['shs_score'] ?></strong></td>
<td><span class="badge badge-<?= strtolower($s['shs_status']) ?>"><?= $s['shs_status'] ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<div class="card">
<div class="card-header"><h2>Engineer KPI (последние)</h2></div>
<?php if (empty($kpiLatest)): ?>
<p style="color:var(--text-muted);">Нет KPI записей.</p>
<?php else: ?>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Инженер</th>
<th>Период</th>
<th>SLA Compliance</th>
<th>Engineer Score</th>
<th>Грейд</th>
</tr>
</thead>
<tbody>
<?php foreach ($kpiLatest as $k): ?>
<tr>
<td><strong><?= htmlspecialchars($k['full_name']) ?></strong></td>
<td><?= $k['period_start'] ?> — <?= $k['period_end'] ?></td>
<td><?= $k['sla_compliance'] ?>%</td>
<td class="<?= $k['engineer_score'] >= 80 ? 'value-green' : ($k['engineer_score'] >= 70 ? 'value-yellow' : 'value-red') ?>"><strong><?= $k['engineer_score'] ?></strong></td>
<td><span class="badge badge-<?= $k['grade'] === 'Senior' ? 'critical' : ($k['grade'] === 'Strong' ? 'high' : 'medium') ?>"><?= $k['grade'] ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<div class="card">
<div class="card-header"><h2>Расчёт SHS</h2></div>
<div style="display:flex;gap:10px;flex-wrap:wrap;">
<form method="post" action="ceo_calc.php" style="display:inline;">
<input type="hidden" name="csrf_token" value="<?= auth_csrf_token() ?>">
<button type="submit" class="btn btn-primary">🔄 Рассчитать SHS</button>
</form>
<form method="post" action="kpi_calc.php" style="display:inline;">
<input type="hidden" name="csrf_token" value="<?= auth_csrf_token() ?>">
<button type="submit" class="btn btn-secondary">📊 Рассчитать KPI инженеров</button>
</form>
<span style="font-size:12px;color:var(--text-muted);align-self:center;">На основе текущих данных системы</span>
</div>
</div>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
+136
View File
@@ -0,0 +1,136 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('owner');
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !auth_csrf_verify($_POST['csrf_token'] ?? '')) {
header('Location: ceo.php');
exit;
}
$pdo = getDB();
$userId = $session['user_id'];
// ---- 1. SLA Stability (с учётом severity нарушений) ----
$stmt = $pdo->query("SELECT COUNT(*) as total, SUM(CASE WHEN sla_compliant=TRUE THEN 1 ELSE 0 END) as compliant FROM tasks WHERE closed_at IS NOT NULL");
$slaData = $stmt->fetch();
$slaCompliance = $slaData['total'] > 0 ? ($slaData['compliant'] / $slaData['total']) * 100 : 100;
// Breach severity from incidents this period
$stmt = $pdo->query("SELECT severity, COUNT(*) as cnt FROM incidents WHERE created_at > NOW() - INTERVAL '30 days' GROUP BY severity");
$breaches = ['P1' => 0, 'P2' => 0, 'P3' => 0];
while ($b = $stmt->fetch()) {
$breaches[$b['severity']] = (int)$b['cnt'];
}
$breachSeverity = calc_breach_severity($breaches['P1'], $breaches['P2'], $breaches['P3']);
$slaStability = calc_sla_stability_index($slaCompliance, $breachSeverity);
// ---- 2. Revenue Stability (через коэффициент вариации MRR за 6 месяцев) ----
$mrrHistory = [];
$stmt = $pdo->query("SELECT TO_CHAR(created_at, 'YYYY-MM') as ym, SUM(sla_price_monthly) as mrr FROM sla_contracts WHERE status='active' GROUP BY ym ORDER BY ym DESC LIMIT 6");
while ($m = $stmt->fetch()) {
$mrrHistory[] = (float)$m->mrr;
}
$mrrHistory = array_reverse($mrrHistory);
$currentMrr = !empty($mrrHistory) ? end($mrrHistory) : 0;
$revenueStability = calc_revenue_stability_index($mrrHistory);
// ---- 3. Retention ----
$stmt = $pdo->query("SELECT COUNT(*) FROM sla_contracts WHERE status='active'");
$activeSla = (int)$stmt->fetchColumn();
$stmt = $pdo->query("SELECT COUNT(DISTINCT object_id) FROM sla_contracts WHERE status IN ('active','expired','cancelled')");
$totalEver = (int)$stmt->fetchColumn();
$retention = $totalEver > 0 ? round(($activeSla / $totalEver) * 100, 2) : 100;
// ---- 4. Engineer Performance ----
$stmt = $pdo->query("SELECT AVG(engineer_score) FROM engineer_kpi WHERE period_end > NOW() - INTERVAL '30 days'");
$avgEs = (float)$stmt->fetchColumn();
$engineerPerformance = min(100, $avgEs ?: 70);
// ---- 5. Incident Stability (с весами severity) ----
$stmt = $pdo->query("SELECT severity, COUNT(*) as cnt FROM incidents WHERE created_at > NOW() - INTERVAL '30 days' GROUP BY severity");
$incSev = ['P1' => 0, 'P2' => 0, 'P3' => 0];
while ($i = $stmt->fetch()) {
$incSev[$i['severity']] = (int)$i['cnt'];
}
$stmt = $pdo->query("SELECT COUNT(*) FROM objects WHERE status='active'");
$objCount = max(1, (int)$stmt->fetchColumn());
$incidentStability = calc_incident_stability_index($incSev['P1'], $incSev['P2'], $incSev['P3'], $objCount);
// ---- 6. Sales Flow (аудиты → SLA конверсия) ----
$stmt = $pdo->query("SELECT COUNT(*) FROM questionnaire_sessions WHERE status='completed' AND created_at > NOW() - INTERVAL '90 days'");
$audits = (int)$stmt->fetchColumn();
$stmt = $pdo->query("SELECT COUNT(*) FROM sla_contracts WHERE created_at > NOW() - INTERVAL '90 days'");
$newSla = (int)$stmt->fetchColumn();
$salesFlow = calc_sales_flow_index($audits, $newSla);
// ---- 7. Operational Efficiency (выручка / (часы × стоимость)) ----
$stmt = $pdo->query("SELECT COUNT(*) FROM tasks WHERE closed_at IS NOT NULL AND closed_at > NOW() - INTERVAL '30 days'");
$closedTasks = (int)$stmt->fetchColumn();
$engineerHours = $closedTasks * 2; // approx 2h per task
$costPerHour = 1500; // руб/час
$operationalEfficiency = calc_operational_efficiency_index($currentMrr, $engineerHours, $costPerHour);
// ---- CEO SHS через calc_ceo_shs() ----
$ceoShsScore = calc_ceo_shs(
$revenueStability, /* MRR Growth proxy */
$slaStability, /* SLA Compliance */
$retention,
$engineerPerformance, /* Productivity */
$salesFlow, /* Conversion */
$incidentStability
);
// ---- SHS (7-факторный) через calc_shs() ----
$shsScore = calc_shs($slaStability, $revenueStability, $retention, $engineerPerformance, $incidentStability, $salesFlow, $operationalEfficiency);
$shsStatus = shs_status($shsScore);
// ---- ΔSHS (дельта от предыдущей записи) ----
$prevRecord = $pdo->query("SELECT shs_score FROM shs_records ORDER BY id DESC LIMIT 1 OFFSET 1")->fetch();
$shsDelta = shs_delta($shsScore, $prevRecord ? (float)$prevRecord['shs_score'] : null);
// ---- Automation Rules ----
$autoAlerts = check_automation_rules($shsScore, $slaStability, $retention);
// ---- Save SHS record ----
$periodStart = date('Y-m-01');
$periodEnd = date('Y-m-t');
$stmt = $pdo->prepare("INSERT INTO shs_records (recorded_by, period_start, period_end, sla_stability, revenue_stability, retention, engineer_performance, incident_stability, sales_flow, operational_efficiency, shs_score, shs_status, components_json) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)");
$stmt->execute([
$userId, $periodStart, $periodEnd,
$slaStability, $revenueStability, $retention, $engineerPerformance,
$incidentStability, $salesFlow, $operationalEfficiency,
$shsScore, $shsStatus,
json_encode([
'sla_compliance_raw' => $slaCompliance,
'breach_severity' => $breachSeverity,
'sla_stability' => $slaStability,
'revenue_stability' => $revenueStability,
'mrr_history' => $mrrHistory,
'current_mrr' => $currentMrr,
'retention' => $retention,
'active_sla' => $activeSla,
'total_ever_sla' => $totalEver,
'engineer_perf' => $engineerPerformance,
'incident_p1' => $incSev['P1'],
'incident_p2' => $incSev['P2'],
'incident_p3' => $incSev['P3'],
'object_count' => $objCount,
'incident_stability' => $incidentStability,
'audits_90d' => $audits,
'new_sla_90d' => $newSla,
'sales_flow' => $salesFlow,
'closed_tasks_30d' => $closedTasks,
'engineer_hours' => $engineerHours,
'operational_eff' => $operationalEfficiency,
'shs_delta' => $shsDelta,
'ceo_shs_score' => $ceoShsScore,
'auto_alerts' => $autoAlerts,
])
]);
auth_log($userId, 'shs_calc', "SHS: $shsScore ($shsStatus), CEO SHS: $ceoShsScore, Δ: " . ($shsDelta['delta'] ?? 'N/A'));
header('Location: ceo.php?calc_ok=1');
exit;
+242
View File
@@ -0,0 +1,242 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('owner');
$pdo = getDB();
$message = '';
$msgType = 'info';
// Update coefficient
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
$msgType = 'error';
} elseif ($_POST['action'] === 'edit' || $_POST['action'] === 'add') {
$key = trim($_POST['key'] ?? '');
$value = str_replace(',', '.', trim($_POST['value'] ?? ''));
$description = trim($_POST['description'] ?? '');
$formulaRef = trim($_POST['formula_ref'] ?? '');
$id = (int)($_POST['id'] ?? 0);
if ($key === '' || $value === '' || !is_numeric($value)) {
$message = 'Заполните ключ и числовое значение.';
$msgType = 'error';
} else {
try {
$oldVal = null;
if ($id > 0) {
$stmt = $pdo->prepare("SELECT `key`, `value` FROM formula_coefficients WHERE id=?");
$stmt->execute([$id]);
$old = $stmt->fetch();
$oldVal = $old ? $old['value'] : null;
$stmt = $pdo->prepare("UPDATE formula_coefficients SET `key`=?, `value`=?, `description`=?, `formula_ref`=?, `updated_by`=? WHERE id=?");
$stmt->execute([$key, $value, $description, $formulaRef, $session['user_id'], $id]);
} else {
$stmt = $pdo->prepare("INSERT INTO formula_coefficients (`key`, `value`, `description`, `formula_ref`, `updated_by`) VALUES (?,?,?,?,?)");
$stmt->execute([$key, $value, $description, $formulaRef, $session['user_id']]);
}
$message = 'Коэффициент "' . htmlspecialchars($key) . '" сохранён.';
$msgType = 'success';
$detail = "Key: $key, new: $value" . ($oldVal !== null ? ", old: $oldVal" : '');
auth_log($session['user_id'], 'coefficient_update', $detail);
} catch (\PDOException $e) {
$message = 'Ошибка: ' . ($e->getCode() === '23000' ? 'Ключ уже существует.' : $e->getMessage());
$msgType = 'error';
}
}
} elseif ($_POST['action'] === 'toggle') {
$id = (int)($_POST['id'] ?? 0);
$stmt = $pdo->prepare("SELECT `key`, `is_active` FROM formula_coefficients WHERE id=?");
$stmt->execute([$id]);
$c = $stmt->fetch();
if ($c) {
$newState = $c['is_active'] ? 0 : 1;
$stmt = $pdo->prepare("UPDATE formula_coefficients SET is_active=?, updated_by=? WHERE id=?");
$stmt->execute([$newState, $session['user_id'], $id]);
auth_log($session['user_id'], 'coefficient_toggle', "Key: {$c['key']}, active: $newState");
$message = 'Статус коэффициента изменён.';
$msgType = 'info';
}
} elseif ($_POST['action'] === 'reset_defaults') {
$confirmed = $_POST['confirmed'] ?? '';
if ($confirmed === '1') {
auth_log($session['user_id'], 'coefficient_reset', 'Сброс всех коэффициентов к значениям из migration.sql');
$message = 'Сброс не реализован — удалите таблицу и перезапустите migration.sql вручную.';
$msgType = 'warning';
} else {
$message = 'Подтвердите сброс.';
$msgType = 'warning';
}
}
}
// Get edit data
$editCoeff = null;
if (isset($_GET['edit'])) {
$stmt = $pdo->prepare("SELECT * FROM formula_coefficients WHERE id=?");
$stmt->execute([(int)$_GET['edit']]);
$editCoeff = $stmt->fetch();
}
// Get all coefficients ordered by key
$coeffs = $pdo->query("SELECT * FROM formula_coefficients ORDER BY `key`")->fetchAll();
// Group by prefix
$groups = [];
foreach ($coeffs as $c) {
$prefix = explode('.', $c['key'])[0];
$groups[$prefix][] = $c;
}
$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>
<div style="display:flex;gap:8px;">
<button class="btn btn-primary btn-sm" onclick="document.getElementById('addModal').classList.add('open')" data-tooltip="Добавить коэффициент">+ Добавить</button>
<form method="post" style="display:inline;" onsubmit="return confirm('Сбросить все коэффициенты к значениям по умолчанию? Это удалит изменения.');">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="action" value="reset_defaults">
<input type="hidden" name="confirmed" value="1">
<button type="submit" class="btn btn-danger btn-sm">Сбросить</button>
</form>
</div>
</div>
<?php if ($message): ?>
<div class="alert alert-<?= $msgType ?>"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<?php foreach ($groups as $prefix => $items): ?>
<div class="card" style="margin-bottom:20px;">
<div class="card-header">
<h2 style="text-transform:uppercase;font-size:13px;letter-spacing:0.5px;color:var(--accent);"><?= htmlspecialchars($prefix) ?></h2>
<span style="font-size:11px;color:var(--text-muted);"><?= count($items) ?> параметров</span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="min-width:220px;">Ключ</th>
<th style="min-width:120px;">Значение</th>
<th>Описание</th>
<th>Функция</th>
<th style="width:100px;">Статус</th>
<th class="actions" style="width:140px;">Действия</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $c): ?>
<tr>
<td><code style="font-size:12px;color:var(--accent);"><?= htmlspecialchars($c['key']) ?></code></td>
<td><strong><?= rtrim(rtrim($c['value'], '0'), '.') ?></strong></td>
<td style="font-size:12px;color:var(--text-secondary);"><?= htmlspecialchars($c['description'] ?: '—') ?></td>
<td style="font-size:11px;color:var(--text-muted);"><?= htmlspecialchars($c['formula_ref'] ?: '—') ?></td>
<td>
<form method="post" style="display:inline;">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="action" value="toggle">
<input type="hidden" name="id" value="<?= $c['id'] ?>">
<button type="submit" class="btn btn-sm <?= $c['is_active'] ? 'btn-success' : 'btn-secondary' ?>"><?= $c['is_active'] ? 'Активен' : 'Отключён' ?></button>
</form>
</td>
<td class="actions">
<a href="?edit=<?= $c['id'] ?>" class="btn btn-secondary btn-sm" onclick="event.preventDefault();openEdit(<?= $c['id'] ?>, '<?= htmlspecialchars($c['key'], ENT_QUOTES) ?>', '<?= rtrim(rtrim($c['value'], '0'), '.') ?>', '<?= htmlspecialchars($c['description'] ?? '', ENT_QUOTES) ?>', '<?= htmlspecialchars($c['formula_ref'] ?? '', ENT_QUOTES) ?>')">✏️</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endforeach; ?>
<!-- Edit Modal -->
<div class="modal-overlay <?= $editCoeff ? 'open' : '' ?>" id="editModal">
<div class="modal">
<button class="modal-close" onclick="closeEdit()">&times;</button>
<h2 id="editModalTitle">Редактировать коэффициент</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="action" value="edit">
<input type="hidden" name="id" id="editId" value="<?= $editCoeff['id'] ?? 0 ?>">
<div class="form-group">
<label for="editKey">Ключ</label>
<input type="text" id="editKey" name="key" value="<?= htmlspecialchars($editCoeff['key'] ?? '') ?>" required>
</div>
<div class="form-group">
<label for="editValue">Значение *</label>
<input type="text" id="editValue" name="value" value="<?= htmlspecialchars($editCoeff['value'] ?? '') ?>" required placeholder="0.0000">
<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">Число с точкой или запятой</div>
</div>
<div class="form-group">
<label for="editDesc">Описание</label>
<input type="text" id="editDesc" name="description" value="<?= htmlspecialchars($editCoeff['description'] ?? '') ?>">
</div>
<div class="form-group">
<label for="editRef">Функция / ссылка на формулу</label>
<input type="text" id="editRef" name="formula_ref" value="<?= htmlspecialchars($editCoeff['formula_ref'] ?? '') ?>">
</div>
<button type="submit" class="btn btn-primary">Сохранить</button>
<button type="button" class="btn btn-secondary" onclick="closeEdit()">Отмена</button>
</form>
</div>
</div>
<!-- Add Modal -->
<div class="modal-overlay" id="addModal">
<div class="modal">
<button class="modal-close" onclick="document.getElementById('addModal').classList.remove('open')">&times;</button>
<h2>Добавить коэффициент</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="action" value="add">
<div class="form-group">
<label for="addKey">Ключ *</label>
<input type="text" id="addKey" name="key" required placeholder="напр. sla_price.base_cost">
</div>
<div class="form-group">
<label for="addValue">Значение *</label>
<input type="text" id="addValue" name="value" required placeholder="0.0000">
</div>
<div class="form-group">
<label for="addDesc">Описание</label>
<input type="text" id="addDesc" name="description">
</div>
<div class="form-group">
<label for="addRef">Функция / ссылка</label>
<input type="text" id="addRef" name="formula_ref">
</div>
<button type="submit" class="btn btn-primary">Создать</button>
<button type="button" class="btn btn-secondary" onclick="document.getElementById('addModal').classList.remove('open')">Отмена</button>
</form>
</div>
</div>
<script>
function openEdit(id, key, value, desc, ref) {
document.getElementById('editId').value = id;
document.getElementById('editKey').value = key;
document.getElementById('editValue').value = value;
document.getElementById('editDesc').value = desc;
document.getElementById('editRef').value = ref;
document.getElementById('editModal').classList.add('open');
}
function closeEdit() {
document.getElementById('editModal').classList.remove('open');
window.location.href = 'coefficients.php';
}
</script>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
+213
View File
@@ -0,0 +1,213 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('owner', 'engineer');
$pdo = getDB();
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$inn = trim($_POST['inn'] ?? '');
$kpp = trim($_POST['kpp'] ?? '');
$address = trim($_POST['legal_address'] ?? '');
$contact = trim($_POST['contact_person'] ?? '');
$cphone = trim($_POST['contact_phone'] ?? '');
$cemail = trim($_POST['contact_email'] ?? '');
$status = $_POST['status'] ?? 'active';
$notes = trim($_POST['notes'] ?? '');
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
} elseif ($name === '') {
$message = 'Название клиента обязательно.';
} else {
try {
if ($id > 0) {
$stmt = $pdo->prepare("UPDATE customers SET name=?, inn=?, kpp=?, legal_address=?, contact_person=?, contact_phone=?, contact_email=?, status=?, notes=? WHERE id=?");
$stmt->execute([$name, $inn, $kpp, $address, $contact, $cphone, $cemail, $status, $notes, $id]);
$message = 'Клиент обновлён.';
} else {
$stmt = $pdo->prepare("INSERT INTO customers (name, inn, kpp, legal_address, contact_person, contact_phone, contact_email, status, notes, created_by) VALUES (?,?,?,?,?,?,?,?,?,?)");
$stmt->execute([$name, $inn, $kpp, $address, $contact, $cphone, $cemail, $status, $notes, $session['user_id']]);
$message = 'Клиент создан.';
}
auth_log($session['user_id'], $id > 0 ? 'customer_update' : 'customer_create', "Name: $name");
} catch (\PDOException $e) {
$message = 'Ошибка: ' . $e->getMessage();
}
}
}
if (isset($_GET['delete']) && auth_has_role('owner')) {
$id = (int)$_GET['delete'];
if (auth_csrf_verify($_GET['csrf'] ?? '')) {
$pdo->prepare("DELETE FROM customers WHERE id=?")->execute([$id]);
$message = 'Клиент удалён.';
auth_log($session['user_id'], 'customer_delete', "ID: $id");
}
}
$editCust = null;
if (isset($_GET['edit'])) {
$stmt = $pdo->prepare("SELECT * FROM customers WHERE id=?");
$stmt->execute([(int)$_GET['edit']]);
$editCust = $stmt->fetch();
}
$search = trim($_GET['search'] ?? '');
if ($search !== '') {
$stmt = $pdo->prepare("
SELECT c.*,
(SELECT COUNT(*) FROM objects WHERE customer_id=c.id) as objects_count,
(SELECT COUNT(*) FROM sla_contracts WHERE customer_id=c.id AND status='active') as active_contracts
FROM customers c
WHERE c.name LIKE ? OR c.inn LIKE ? OR c.contact_person LIKE ?
ORDER BY c.name
");
$stmt->execute(["%$search%", "%$search%", "%$search%"]);
} else {
$stmt = $pdo->query("
SELECT c.*,
(SELECT COUNT(*) FROM objects WHERE customer_id=c.id) as objects_count,
(SELECT COUNT(*) FROM sla_contracts WHERE customer_id=c.id AND status='active') as active_contracts
FROM customers c
ORDER BY c.name
");
}
$customers = $stmt->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()" data-tooltip="Добавить клиента">+ Добавить</button>
</div>
<?php if ($message): ?>
<div class="alert alert-info"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<form class="search-box" method="get">
<input type="text" name="search" placeholder="Поиск клиентов..." value="<?= htmlspecialchars($search) ?>">
<button type="submit" class="btn btn-primary btn-sm" data-tooltip="Поиск">Найти</button>
<?php if ($search): ?><a href="customers.php" class="btn btn-secondary btn-sm" data-tooltip="Сбросить поиск">Сброс</a><?php endif; ?>
</form>
<div class="card">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Название</th>
<th>ИНН</th>
<th>Контакт</th>
<th>Статус</th>
<th>Объекты</th>
<th>Активные SLA</th>
<th class="actions">Действия</th>
</tr>
</thead>
<tbody>
<?php if (empty($customers)): ?>
<tr><td colspan="7" style="text-align:center;color:var(--text-muted);">Нет клиентов</td></tr>
<?php endif; ?>
<?php foreach ($customers as $c): ?>
<tr>
<td><strong><?= htmlspecialchars($c['name']) ?></strong></td>
<td><?= $c['inn'] ?: '—' ?></td>
<td><?= htmlspecialchars($c['contact_person'] ?: '—') ?></td>
<td><span class="badge badge-<?= $c['status'] ?>"><?= $c['status'] ?></span></td>
<td><a href="objects.php?customer_id=<?= $c['id'] ?>"><?= (int)$c['objects_count'] ?></a></td>
<td><?= (int)$c['active_contracts'] ?></td>
<td class="actions">
<a href="?edit=<?= $c['id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="Редактировать">✏️</a>
<a href="objects.php?customer_id=<?= $c['id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="Объекты клиента">🏢</a>
<a href="sla.php?customer_id=<?= $c['id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="SLA контракты">📋</a>
<?php if (auth_has_role('owner')): ?>
<a href="?delete=<?= $c['id'] ?>&csrf=<?= $csrf ?>" class="btn btn-danger btn-sm" onclick="return confirm('Удалить клиента? Все связанные объекты останутся без привязки.')" data-tooltip="Удалить">🗑️</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="modal-overlay <?= $editCust ? 'open' : '' ?>" id="custModal">
<div class="modal">
<button class="modal-close" onclick="closeModal()">&times;</button>
<h2><?= $editCust ? 'Редактировать' : 'Добавить' ?> клиента</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="id" value="<?= $editCust['id'] ?? 0 ?>">
<div class="form-row">
<div class="form-group">
<label for="name">Название *</label>
<input type="text" id="name" name="name" value="<?= htmlspecialchars($editCust['name'] ?? '') ?>" required>
</div>
<div class="form-group">
<label for="inn">ИНН</label>
<input type="text" id="inn" name="inn" value="<?= htmlspecialchars($editCust['inn'] ?? '') ?>" maxlength="12">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="kpp">КПП</label>
<input type="text" id="kpp" name="kpp" value="<?= htmlspecialchars($editCust['kpp'] ?? '') ?>" maxlength="9">
</div>
<div class="form-group">
<label for="contact_person">Контактное лицо</label>
<input type="text" id="contact_person" name="contact_person" value="<?= htmlspecialchars($editCust['contact_person'] ?? '') ?>">
</div>
</div>
<div class="form-group">
<label for="legal_address">Юридический адрес</label>
<input type="text" id="legal_address" name="legal_address" value="<?= htmlspecialchars($editCust['legal_address'] ?? '') ?>">
</div>
<div class="form-row">
<div class="form-group">
<label for="contact_phone">Телефон</label>
<input type="text" id="contact_phone" name="contact_phone" value="<?= htmlspecialchars($editCust['contact_phone'] ?? '') ?>">
</div>
<div class="form-group">
<label for="contact_email">Email</label>
<input type="email" id="contact_email" name="contact_email" value="<?= htmlspecialchars($editCust['contact_email'] ?? '') ?>">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="status">Статус</label>
<select id="status" name="status">
<option value="active" <?= ($editCust['status'] ?? '') === 'active' ? 'selected' : '' ?>>Активен</option>
<option value="inactive" <?= ($editCust['status'] ?? '') === 'inactive' ? 'selected' : '' ?>>Неактивен</option>
<option value="prospect" <?= ($editCust['status'] ?? '') === 'prospect' ? 'selected' : '' ?>>Потенциальный</option>
</select>
</div>
<div class="form-group"></div>
</div>
<div class="form-group">
<label for="notes">Примечания</label>
<textarea id="notes" name="notes"><?= htmlspecialchars($editCust['notes'] ?? '') ?></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('custModal').classList.add('open'); }
function closeModal() { document.getElementById('custModal').classList.remove('open'); window.location.href = 'customers.php'; }
</script>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
+293
View File
@@ -0,0 +1,293 @@
<?php
/**
* AegisOne Engineering — Управление документацией (только владелец)
*
* Позволяет:
* - Видеть все документы из service/docs/
* - Переименовывать документы (обновляет файл + permissions)
* - Управлять порядком сортировки
* - Включать/отключать доступ инженеров (view/edit/cancel)
* - Автоматически обнаруживать новые .md-файлы
*/
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
require_once __DIR__ . '/../../inc/docs_functions.php';
$session = auth_require('owner');
$message = '';
$messageType = '';
// Обработка действий
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
$messageType = 'danger';
} else {
$slug = trim($_POST['slug'] ?? '');
$permissions = load_permissions();
if ($_POST['action'] === 'rename' && $slug && !empty($permissions[$slug])) {
$newTitle = trim($_POST['new_title'] ?? '');
$newSort = (int)($_POST['new_sort'] ?? 0);
if ($newTitle !== '') {
$permissions[$slug]['title'] = $newTitle;
$permissions[$slug]['sort_order'] = $newSort;
save_permissions($permissions);
$message = 'Документ обновлён.';
$messageType = 'success';
}
} elseif ($_POST['action'] === 'toggle_view' && $slug) {
if (!isset($permissions[$slug]['permissions'])) {
$permissions[$slug]['permissions'] = ['engineer' => ['view' => false, 'edit' => false, 'cancel' => false]];
}
$current = !empty($permissions[$slug]['permissions']['engineer']['view']);
$permissions[$slug]['permissions']['engineer']['view'] = !$current;
save_permissions($permissions);
$message = 'Доступ обновлён.';
$messageType = 'success';
} elseif ($_POST['action'] === 'toggle_edit' && $slug) {
if (!isset($permissions[$slug]['permissions'])) {
$permissions[$slug]['permissions'] = ['engineer' => ['view' => false, 'edit' => false, 'cancel' => false]];
}
$current = !empty($permissions[$slug]['permissions']['engineer']['edit']);
$permissions[$slug]['permissions']['engineer']['edit'] = !$current;
if ($permissions[$slug]['permissions']['engineer']['edit']) {
$permissions[$slug]['permissions']['engineer']['view'] = true;
}
save_permissions($permissions);
$message = 'Доступ обновлён.';
$messageType = 'success';
} elseif ($_POST['action'] === 'toggle_cancel' && $slug) {
if (!isset($permissions[$slug]['permissions'])) {
$permissions[$slug]['permissions'] = ['engineer' => ['view' => false, 'edit' => false, 'cancel' => false]];
}
$current = !empty($permissions[$slug]['permissions']['engineer']['cancel']);
$permissions[$slug]['permissions']['engineer']['cancel'] = !$current;
if ($permissions[$slug]['permissions']['engineer']['cancel']) {
$permissions[$slug]['permissions']['engineer']['view'] = true;
}
save_permissions($permissions);
$message = 'Доступ обновлён.';
$messageType = 'success';
} elseif ($_POST['action'] === 'sort_up' || $_POST['action'] === 'sort_down') {
$docs = get_docs_list();
$idx = -1;
foreach ($docs as $i => $d) {
if ($d['slug'] === $slug) { $idx = $i; break; }
}
if ($idx >= 0) {
$swapIdx = $_POST['action'] === 'sort_up' ? $idx - 1 : $idx + 1;
if ($swapIdx >= 0 && $swapIdx < count($docs)) {
$aSlug = $docs[$idx]['slug'];
$bSlug = $docs[$swapIdx]['slug'];
$aSort = $permissions[$aSlug]['sort_order'] ?? $idx;
$bSort = $permissions[$bSlug]['sort_order'] ?? $swapIdx;
$permissions[$aSlug]['sort_order'] = $bSort;
$permissions[$bSlug]['sort_order'] = $aSort;
save_permissions($permissions);
$message = 'Порядок изменён.';
$messageType = 'success';
}
}
}
}
}
$permissions = load_permissions();
$docs = get_docs_list();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
require __DIR__ . '/../../inc/header.php';
?>
<div class="docs-admin">
<div class="page-header">
<h1>Управление документацией</h1>
<p class="page-subtitle">
Добавьте .md-файлы в папку <code>service/docs/</code> — они появятся в списке автоматически.<br>
Управляйте правами инженеров: просмотр, редактирование, отмена.
</p>
</div>
<?php if ($message): ?>
<div class="alert alert-<?= htmlspecialchars($messageType) ?>"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th style="width:30px">↕</th>
<th>Документ</th>
<th>Файл</th>
<th class="text-center">Просмотр</th>
<th class="text-center">Редакт.</th>
<th class="text-center">Отмена</th>
<th class="text-center">Действия</th>
</tr>
</thead>
<tbody>
<?php if (empty($docs)): ?>
<tr>
<td colspan="7" class="text-center" style="color:var(--text-muted);padding:32px;">
Нет документов. Добавьте .md-файлы в <code>service/docs/</code>
</td>
</tr>
<?php endif; ?>
<?php foreach ($docs as $idx => $doc):
$p = $permissions[$doc['slug']] ?? [];
$engPerms = $p['permissions']['engineer'] ?? ['view' => false, 'edit' => false, 'cancel' => false];
// Backward compat: old boolean 'engineer' field
if (!isset($p['permissions']) && !empty($p['engineer'])) {
$engPerms = ['view' => true, 'edit' => false, 'cancel' => false];
}
?>
<tr>
<td>
<div style="display:flex;flex-direction:column;gap:2px;">
<form method="post" style="display:inline;">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="slug" value="<?= htmlspecialchars($doc['slug']) ?>">
<input type="hidden" name="action" value="sort_up">
<button type="submit" class="btn-sort <?= $idx === 0 ? 'disabled' : '' ?>" data-tooltip="Вверх">▲</button>
</form>
<form method="post" style="display:inline;">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="slug" value="<?= htmlspecialchars($doc['slug']) ?>">
<input type="hidden" name="action" value="sort_down">
<button type="submit" class="btn-sort <?= $idx === count($docs) - 1 ? 'disabled' : '' ?>" data-tooltip="Вниз">▼</button>
</form>
</div>
</td>
<td>
<a href="/service/pages/shared/docs.php?view=<?= urlencode($doc['slug']) ?>" class="doc-link">
<?= htmlspecialchars($doc['title']) ?>
</a>
</td>
<td><code><?= htmlspecialchars($doc['filename']) ?></code></td>
<td class="text-center">
<form method="post" style="display:inline;">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="action" value="toggle_view">
<input type="hidden" name="slug" value="<?= htmlspecialchars($doc['slug']) ?>">
<button type="submit" class="btn-perm <?= $engPerms['view'] ? 'on' : 'off' ?>" data-tooltip="Просмотр"><?= $engPerms['view'] ? '✓' : '✗' ?></button>
</form>
</td>
<td class="text-center">
<form method="post" style="display:inline;">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="action" value="toggle_edit">
<input type="hidden" name="slug" value="<?= htmlspecialchars($doc['slug']) ?>">
<button type="submit" class="btn-perm <?= $engPerms['edit'] ? 'on' : 'off' ?>" data-tooltip="Редактирование"><?= $engPerms['edit'] ? '✓' : '✗' ?></button>
</form>
</td>
<td class="text-center">
<form method="post" style="display:inline;">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="action" value="toggle_cancel">
<input type="hidden" name="slug" value="<?= htmlspecialchars($doc['slug']) ?>">
<button type="submit" class="btn-perm <?= $engPerms['cancel'] ? 'on' : 'off' ?>" data-tooltip="Отмена"><?= $engPerms['cancel'] ? '✓' : '✗' ?></button>
</form>
</td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-secondary btn-rename" data-tooltip="Переименовать" data-slug="<?= htmlspecialchars($doc['slug']) ?>" data-title="<?= htmlspecialchars($doc['title']) ?>" data-sort="<?= $p['sort_order'] ?? $idx ?>">✎</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="docs-admin__info">
<h3>Как это работает</h3>
<ul>
<li>Все .md-файлы из <code>service/docs/</code> автоматически обнаруживаются.</li>
<li>Стрелки ▲▼ меняют порядок отображения.</li>
<li><strong>Просмотр</strong> — инженер видит документ в списке.</li>
<li><strong>Редакт.</strong> — инженер может изменить содержимое (автоматически включает просмотр).</li>
<li><strong>Отмена</strong> — инженер может отменить документ без удаления.</li>
<li>Кнопка ✎ открывает переименование и настройку порядка.</li>
</ul>
</div>
</div>
<!-- Rename Modal -->
<div class="modal-overlay" id="renameModal">
<div class="modal">
<button class="modal-close" onclick="closeRename()">&times;</button>
<h2>Переименовать документ</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="action" value="rename">
<input type="hidden" name="slug" id="rename-slug">
<div class="form-group">
<label>Название</label>
<input type="text" id="rename-title" name="new_title" required>
</div>
<div class="form-group">
<label>Порядок (число, по возрастанию)</label>
<input type="number" id="rename-sort" name="new_sort" value="0" min="0">
</div>
<button type="submit" class="btn btn-primary">Сохранить</button>
<button type="button" class="btn btn-secondary" onclick="closeRename()">Отмена</button>
</form>
</div>
</div>
<style>
.docs-admin { padding: 24px; }
.page-header { margin-bottom: 24px; }
.page-header h1 { font-size: 24px; color: var(--text-primary); margin-bottom: 8px; }
.page-subtitle { font-size: 13px; color: var(--text-muted); line-height: 1.6; }
.page-subtitle code { background: var(--bg-input); padding: 2px 6px; border-radius: 4px; font-size: 12px; color: var(--accent); }
.table-responsive { overflow-x: auto; }
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
.table th, .table td { padding: 12px 16px; text-align: left; border-bottom: 1px solid var(--border); color: var(--text-primary); }
.table th { font-weight: 600; color: var(--text-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; background: var(--bg-card); }
.table tbody tr:hover { background: rgba(0,173,239,0.03); }
.text-center { text-align: center; }
.doc-link { color: var(--accent); text-decoration: none; font-weight: 500; }
.doc-link:hover { text-decoration: underline; }
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; font-size: 13px; }
.alert-success { background: rgba(34,197,94,0.15); color: var(--success); border: 1px solid rgba(34,197,94,0.3); }
.alert-danger { background: rgba(239,68,68,0.15); color: var(--danger); border: 1px solid rgba(239,68,68,0.3); }
.btn-sm { padding: 6px 14px; font-size: 12px; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; }
.btn-primary { background: var(--accent); color: #fff; }
.btn-secondary { background: var(--bg-input); color: var(--text-primary); border: 1px solid var(--border); }
.btn-sort { background:none; border:none; color:var(--text-muted); cursor:pointer; font-size:10px; padding:2px 4px; transition:color 0.2s; }
.btn-sort:hover:not(.disabled) { color:var(--accent); }
.btn-sort.disabled { opacity:0.2; cursor:default; }
.btn-perm { width:28px; height:28px; border:none; border-radius:6px; cursor:pointer; font-size:14px; font-weight:700; transition:all 0.2s; }
.btn-perm.on { background:rgba(34,197,94,0.2); color:var(--success); }
.btn-perm.off { background:rgba(136,153,187,0.15); color:var(--text-muted); }
.btn-perm:hover { transform:scale(1.1); }
.btn-rename { padding:4px 8px; }
.docs-admin__info { margin-top: 32px; padding: 24px; background: var(--bg-card); border-radius: 12px; border: 1px solid var(--border); }
.docs-admin__info h3 { font-size: 14px; color: var(--text-primary); margin-bottom: 12px; }
.docs-admin__info ul { list-style: none; padding: 0; }
.docs-admin__info li { padding: 4px 0; font-size: 13px; color: var(--text-secondary); line-height: 1.5; }
.docs-admin__info li::before { content: '• '; color: var(--accent); font-weight: bold; }
</style>
<script>
(function(){
var modal = document.getElementById('renameModal');
window.closeRename = function(){ modal.classList.remove('open'); };
document.querySelectorAll('.btn-rename').forEach(function(btn){
btn.addEventListener('click', function(){
document.getElementById('rename-slug').value = btn.getAttribute('data-slug');
document.getElementById('rename-title').value = btn.getAttribute('data-title');
document.getElementById('rename-sort').value = btn.getAttribute('data-sort');
modal.classList.add('open');
});
});
})();
</script>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
+195
View File
@@ -0,0 +1,195 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('owner', 'engineer');
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !auth_csrf_verify($_POST['csrf_token'] ?? '')) {
header('Location: /service/pages/engineer/tasks.php');
exit;
}
$pdo = getDB();
$userId = $session['user_id'];
// Calculate KPI for each engineer
$engineers = $pdo->query("SELECT id, full_name FROM users WHERE role IN ('engineer','technician') AND is_active=1")->fetchAll();
$periodStart = date('Y-m-01');
$periodEnd = date('Y-m-t');
// Previous period dates
$prevPeriodStart = date('Y-m-01', strtotime('-1 month'));
$prevPeriodEnd = date('Y-m-t', strtotime('-1 month'));
foreach ($engineers as $eng) {
$eid = $eng['id'];
// ---- Individual KPI (Engineer Score) ----
// SLA compliance
$stmt = $pdo->prepare("SELECT COUNT(*) as total, SUM(CASE WHEN sla_compliant=TRUE THEN 1 ELSE 0 END) as compliant FROM tasks WHERE assigned_to=$1 AND closed_at IS NOT NULL AND closed_at >= $2");
$stmt->execute([$eid, $periodStart]);
$sd = $stmt->fetch();
$slaCompliance = calc_sla_compliance((int)($sd['compliant'] ?? 0), (int)($sd['total'] ?? 0));
// Response time score
$stmt = $pdo->prepare("SELECT AVG(response_time_minutes) as avg_rt FROM tasks WHERE assigned_to=? AND closed_at IS NOT NULL AND closed_at >= ?");
$stmt->execute([$eid, $periodStart]);
$rt = (float)($stmt->fetchColumn() ?: 0);
$responseTimeScore = calc_response_time_score($rt / 60, 4);
// Resolution time score
$stmt = $pdo->prepare("SELECT AVG(resolution_time_minutes) as avg_ttr FROM tasks WHERE assigned_to=? AND closed_at IS NOT NULL AND closed_at >= ?");
$stmt->execute([$eid, $periodStart]);
$ttr = (float)($stmt->fetchColumn() ?: 0);
$resolutionTimeScore = calc_resolution_time_score($ttr / 60, 8);
// Diagnosis accuracy
$stmt = $pdo->prepare("SELECT COUNT(*) as total FROM reports WHERE created_by=? AND created_at >= ?");
$stmt->execute([$eid, $periodStart]);
$reportTotal = (int)$stmt->fetchColumn();
$diagnosisAccuracy = $reportTotal > 0 ? 85 : 75;
// Reopen rate
$stmt = $pdo->prepare("SELECT COUNT(*) as reopened FROM tasks WHERE assigned_to=? AND status='completed' AND closed_at IS NOT NULL AND closed_at >= ? AND checklist_data LIKE '%\"fault_found\":1%'");
$stmt->execute([$eid, $periodStart]);
$reopened = (int)$stmt->fetchColumn();
$totalTasks = max(1, $sd['total'] ?? 1);
$reopenRate = calc_reopen_rate($reopened, $totalTasks);
$reopenRateScore = max(0, 100 - $reopenRate * 2);
// Risk coverage score
$stmt = $pdo->prepare("SELECT COUNT(*) FROM reports WHERE created_by=? AND created_at >= ? AND findings IS NOT NULL AND findings != ''");
$stmt->execute([$eid, $periodStart]);
$goodReports = (int)$stmt->fetchColumn();
$riskCoverageScore = calc_risk_coverage($goodReports, $reportTotal ?: 1);
// Documentation quality
$stmt = $pdo->prepare("SELECT COUNT(*) FROM reports WHERE created_by=? AND created_at >= ? AND status='final'");
$stmt->execute([$eid, $periodStart]);
$fullReports = (int)$stmt->fetchColumn();
$docQuality = calc_documentation_quality($fullReports, $reportTotal ?: 1);
// Engineer score
$es = calc_engineer_score($slaCompliance, $responseTimeScore, $resolutionTimeScore, $diagnosisAccuracy, $reopenRateScore, $riskCoverageScore);
$grade = engineer_grade($es);
// ---- ECS (Engineer Control Score) — для инженеров, управляющих техниками ----
$ecs = null;
$ecsComponents = null;
// Get objects where this engineer is assigned
$stmt = $pdo->prepare("SELECT object_id FROM object_assignments WHERE user_id=? AND role='engineer' AND unassigned_at IS NULL");
$stmt->execute([$eid]);
$engObjectIds = $stmt->fetchAll(PDO::FETCH_COLUMN);
if (!empty($engObjectIds)) {
$placeholders = implode(',', array_fill(0, count($engObjectIds), '?'));
// ECS 1: SLA Control — SLA compliance on tasks across all objects under this engineer
$stmt = $pdo->prepare("SELECT COUNT(*) as total, SUM(CASE WHEN sla_compliant=TRUE THEN 1 ELSE 0 END) as compliant FROM tasks WHERE object_id IN ($placeholders) AND closed_at IS NOT NULL AND closed_at >= $". (count($engObjectIds) + 1));
$params = $engObjectIds;
$params[] = $periodStart;
$stmt->execute($params);
$slaCtrl = $stmt->fetch();
$slaControl = $slaCtrl['total'] > 0 ? ($slaCtrl['compliant'] / $slaCtrl['total']) * 100 : 100;
// ECS 2: Task Distribution — load balance among technicians on these objects
$stmt = $pdo->prepare("SELECT assigned_to, COUNT(*) as cnt FROM tasks WHERE object_id IN ($placeholders) AND assigned_to IS NOT NULL AND closed_at >= ? GROUP BY assigned_to");
$params2 = $engObjectIds;
$params2[] = $periodStart;
$stmt->execute($params2);
$taskDistData = $stmt->fetchAll();
$tasksPerTech = array_column($taskDistData, 'cnt');
$taskDist = count($tasksPerTech) > 1 ? max(0, 100 - calc_tlb($tasksPerTech)) : 100;
// ECS 3: Incident Reduction — compare current vs prev month incidents on these objects
$stmt = $pdo->prepare("SELECT COUNT(*) FROM incidents WHERE object_id IN ($placeholders) AND created_at >= ? AND created_at <= ?");
$curParams = $engObjectIds;
$curParams[] = $periodStart;
$curParams[] = $periodEnd;
$stmt->execute($curParams);
$currentInc = (int)$stmt->fetchColumn();
$prevParams = $engObjectIds;
$prevParams[] = $prevPeriodStart;
$prevParams[] = $prevPeriodEnd;
$stmt->execute($prevParams);
$prevInc = (int)$stmt->fetchColumn();
$incidentRed = calc_incident_reduction($prevInc, $currentInc);
if ($prevInc === 0 && $currentInc === 0) $incidentRed = 100;
// ECS 4: Team Performance — avg engineer_score of technicians on these objects
$techScoreSum = 0;
$techScoreCount = 0;
foreach ($taskDistData as $td) {
$techId = (int)$td['assigned_to'];
$stmt = $pdo->prepare("SELECT AVG(engineer_score) FROM engineer_kpi WHERE engineer_id=$1 AND period_end > NOW() - INTERVAL '30 days'");
$stmt->execute([$techId]);
$ts = (float)$stmt->fetchColumn();
if ($ts > 0) {
$techScoreSum += $ts;
$techScoreCount++;
}
}
$teamPerf = $techScoreCount > 0 ? $techScoreSum / $techScoreCount : 70;
// ECS 5: Response Coordination — avg response time score across object tasks
$stmt = $pdo->prepare("SELECT AVG(response_time_minutes) as avg_rt FROM tasks WHERE object_id IN ($placeholders) AND closed_at IS NOT NULL AND closed_at >= ? AND response_time_minutes > 0");
$respParams = $engObjectIds;
$respParams[] = $periodStart;
$stmt->execute($respParams);
$avgRespTime = (float)($stmt->fetchColumn() ?: 0);
$responseCoord = calc_response_time_score($avgRespTime / 60, 4);
$ecs = calc_engineer_control_score($slaControl, $taskDist, $incidentRed, $teamPerf, $responseCoord);
$ecsComponents = [
'sla_control' => round($slaControl, 1),
'task_dist' => round($taskDist, 1),
'incident_red' => round($incidentRed, 1),
'team_perf' => round($teamPerf, 1),
'response_coord' => round($responseCoord, 1),
];
}
// Save
$components = [
'sla_compliance' => $slaCompliance,
'response_time_score' => $responseTimeScore,
'resolution_time_score'=> $resolutionTimeScore,
'diagnosis_accuracy' => $diagnosisAccuracy,
'reopen_rate' => $reopenRate,
'risk_coverage_score' => $riskCoverageScore,
'doc_quality' => $docQuality,
'ecs' => $ecs,
'ecs_components' => $ecsComponents,
];
$stmt = $pdo->prepare("INSERT INTO engineer_kpi (engineer_id, period_start, period_end, sla_compliance, response_time_score, resolution_time_score, diagnosis_accuracy, reopen_rate, risk_coverage_score, documentation_quality, engineer_score, grade, components_json)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)");
$stmt->execute([
$eid, $periodStart, $periodEnd,
$slaCompliance, $responseTimeScore, $resolutionTimeScore,
$diagnosisAccuracy, $reopenRate, $riskCoverageScore, $docQuality,
$es, $grade,
json_encode($components)
]);
}
$logMsg = 'KPI calculated for ' . count($engineers) . ' users';
$ecsCount = 0;
foreach ($engineers as $eng) {
$stmt = $pdo->prepare("SELECT components_json FROM engineer_kpi WHERE engineer_id=? AND period_start=? ORDER BY id DESC LIMIT 1");
$stmt->execute([$eng['id'], $periodStart]);
$row = $stmt->fetch();
if ($row) {
$comp = json_decode($row['components_json'], true);
if (!empty($comp['ecs'])) $ecsCount++;
}
}
if ($ecsCount > 0) $logMsg .= " (ECS calculated for $ecsCount engineers)";
auth_log($userId, 'kpi_calc', $logMsg);
header('Location: /service/pages/owner/ceo.php?kpi_ok=1');
exit;
+229
View File
@@ -0,0 +1,229 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('owner', 'engineer');
$pdo = getDB();
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$name = trim($_POST['name'] ?? '');
$address = trim($_POST['address'] ?? '');
$objType = trim($_POST['object_type'] ?? '');
$contact = trim($_POST['contact_person'] ?? '');
$cphone = trim($_POST['contact_phone'] ?? '');
$status = $_POST['status'] ?? 'active';
$notes = trim($_POST['notes'] ?? '');
$customerId = (int)($_POST['customer_id'] ?? 0);
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
} elseif ($name === '') {
$message = 'Название объекта обязательно.';
} else {
try {
if ($id > 0) {
$stmt = $pdo->prepare("UPDATE objects SET customer_id=?, name=?, address=?, object_type=?, contact_person=?, contact_phone=?, status=?, notes=? WHERE id=?");
$stmt->execute([$customerId ?: null, $name, $address, $objType, $contact, $cphone, $status, $notes, $id]);
$message = 'Объект обновлён.';
} else {
$stmt = $pdo->prepare("INSERT INTO objects (customer_id, name, address, object_type, contact_person, contact_phone, status, notes, created_by) VALUES (?,?,?,?,?,?,?,?,?)");
$stmt->execute([$customerId ?: null, $name, $address, $objType, $contact, $cphone, $status, $notes, $session['user_id']]);
$message = 'Объект создан.';
}
auth_log($session['user_id'], $id > 0 ? 'object_update' : 'object_create', "Name: $name");
} catch (\PDOException $e) {
$message = 'Ошибка: ' . $e->getMessage();
}
}
}
if (isset($_GET['delete']) && auth_has_role('owner')) {
$id = (int)$_GET['delete'];
if (auth_csrf_verify($_GET['csrf'] ?? '')) {
$pdo->prepare("DELETE FROM objects WHERE id=?")->execute([$id]);
$message = 'Объект удалён.';
auth_log($session['user_id'], 'object_delete', "ID: $id");
}
}
$editObj = null;
if (isset($_GET['edit'])) {
$stmt = $pdo->prepare("SELECT * FROM objects WHERE id=?");
$stmt->execute([(int)$_GET['edit']]);
$editObj = $stmt->fetch();
}
$search = trim($_GET['search'] ?? '');
$customerId = (int)($_GET['customer_id'] ?? 0);
if ($customerId > 0) {
$stmt = $pdo->prepare("
SELECT o.*, c.name as customer_name
FROM objects o
LEFT JOIN customers c ON c.id = o.customer_id
WHERE o.customer_id = ?
ORDER BY o.name
");
$stmt->execute([$customerId]);
} elseif ($search !== '') {
$stmt = $pdo->prepare("
SELECT o.*, c.name as customer_name
FROM objects o
LEFT JOIN customers c ON c.id = o.customer_id
WHERE o.name LIKE ? OR o.address LIKE ?
ORDER BY o.name
");
$stmt->execute(["%$search%", "%$search%"]);
} else {
$stmt = $pdo->query("
SELECT o.*, c.name as customer_name
FROM objects o
LEFT JOIN customers c ON c.id = o.customer_id
ORDER BY o.name
");
}
$objects = $stmt->fetchAll();
$customers = $pdo->query("SELECT id, name FROM customers WHERE status='active' ORDER BY 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()" data-tooltip="Добавить объект">+ Добавить</button>
</div>
<?php if ($message): ?>
<div class="alert alert-info"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<form class="search-box" method="get">
<input type="text" name="search" placeholder="Поиск объектов..." value="<?= htmlspecialchars($search) ?>">
<button type="submit" class="btn btn-primary btn-sm" data-tooltip="Поиск">Найти</button>
<?php if ($search): ?><a href="objects.php" class="btn btn-secondary btn-sm" data-tooltip="Сбросить поиск">Сброс</a><?php endif; ?>
</form>
<div class="card">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Название</th>
<th>Клиент</th>
<th>Тип</th>
<th>Адрес</th>
<th>Статус</th>
<th>Risk Score</th>
<th>Object Index</th>
<th class="actions">Действия</th>
</tr>
</thead>
<tbody>
<?php if (empty($objects)): ?>
<tr><td colspan="8" style="text-align:center;color:var(--text-muted);">Нет объектов</td></tr>
<?php endif; ?>
<?php foreach ($objects as $o): ?>
<tr>
<td><strong><?= htmlspecialchars($o['name']) ?></strong></td>
<td><?= $o['customer_name'] ? '<a href="customers.php?edit=' . (int)$o['customer_id'] . '">' . htmlspecialchars($o['customer_name']) . '</a>' : '—' ?></td>
<td><?= htmlspecialchars($o['object_type'] ?: '—') ?></td>
<td><?= htmlspecialchars($o['address'] ?: '—') ?></td>
<td><span class="badge badge-<?= $o['status'] ?>"><?= $o['status'] ?></span></td>
<td><?= $o['risk_score'] ?: '—' ?></td>
<td><strong><?= $o['object_index'] ?: '—' ?></strong></td>
<td class="actions">
<a href="?edit=<?= $o['id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="Редактировать">✏️</a>
<a href="../shared/questionnaire.php?new=1" class="btn btn-secondary btn-sm" data-tooltip="Новый опросник">📝</a>
<a href="sla.php?object_id=<?= $o['id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="Создать SLA">📋 SLA</a>
<a href="?delete=<?= $o['id'] ?>&csrf=<?= $csrf ?>" class="btn btn-danger btn-sm" onclick="return confirm('Удалить объект?')" data-tooltip="Удалить">🗑️</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="modal-overlay <?= $editObj ? 'open' : '' ?>" id="objModal">
<div class="modal">
<button class="modal-close" onclick="closeModal()">&times;</button>
<h2><?= $editObj ? 'Редактировать' : 'Добавить' ?> объект</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="id" value="<?= $editObj['id'] ?? 0 ?>">
<div class="form-group">
<label for="name">Название *</label>
<input type="text" id="name" name="name" value="<?= htmlspecialchars($editObj['name'] ?? '') ?>" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="customer_id">Клиент</label>
<select id="customer_id" name="customer_id">
<option value="">— Не выбран —</option>
<?php foreach ($customers as $cust): ?>
<option value="<?= $cust['id'] ?>" <?= ($editObj['customer_id'] ?? 0) == $cust['id'] ? 'selected' : '' ?>><?= htmlspecialchars($cust['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="object_type">Тип</label>
<select id="object_type" name="object_type">
<option value="">—</option>
<?php foreach (['Гостиница','Склад','Производство','ТЦ','Офис','Бизнес-центр','Медицина','Образование','Другое'] as $t): ?>
<option value="<?= $t ?>" <?= ($editObj['object_type'] ?? '') === $t ? 'selected' : '' ?>><?= $t ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-group">
<label for="address">Адрес</label>
<input type="text" id="address" name="address" value="<?= htmlspecialchars($editObj['address'] ?? '') ?>">
</div>
<div class="form-row">
<div class="form-group">
<label for="contact_person">Контактное лицо</label>
<input type="text" id="contact_person" name="contact_person" value="<?= htmlspecialchars($editObj['contact_person'] ?? '') ?>">
</div>
<div class="form-group">
<label for="contact_phone">Телефон</label>
<input type="text" id="contact_phone" name="contact_phone" value="<?= htmlspecialchars($editObj['contact_phone'] ?? '') ?>">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="status">Статус</label>
<select id="status" name="status">
<option value="active" <?= ($editObj['status'] ?? '') === 'active' ? 'selected' : '' ?>>Активен</option>
<option value="inactive" <?= ($editObj['status'] ?? '') === 'inactive' ? 'selected' : '' ?>>Неактивен</option>
<option value="audit" <?= ($editObj['status'] ?? '') === 'audit' ? 'selected' : '' ?>>Аудит</option>
<option value="prospective" <?= ($editObj['status'] ?? '') === 'prospective' ? 'selected' : '' ?>>Потенциальный</option>
</select>
</div>
<div class="form-group"></div>
</div>
<div class="form-group">
<label for="notes">Примечания</label>
<textarea id="notes" name="notes"><?= htmlspecialchars($editObj['notes'] ?? '') ?></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('objModal').classList.add('open'); }
function closeModal() { document.getElementById('objModal').classList.remove('open'); window.location.href = 'objects.php'; }
</script>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
@@ -0,0 +1,210 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('owner');
$pdo = getDB();
$message = '';
// Save item
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_item'])) {
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
} else {
$id = (int)($_POST['id'] ?? 0);
$step = (int)($_POST['step'] ?? 1);
$section = trim($_POST['section'] ?? '');
$key = trim($_POST['question_key'] ?? '');
$label = trim($_POST['label'] ?? '');
$type = $_POST['type'] ?? 'text';
$options = trim($_POST['options'] ?? '');
$required = isset($_POST['required']) ? 1 : 0;
$sortOrder = (int)($_POST['sort_order'] ?? 0);
$helpText = trim($_POST['help_text'] ?? '');
if ($key === '' || $label === '') {
$message = 'Ключ и название обязательны.';
} else {
try {
if ($id > 0) {
$stmt = $pdo->prepare("UPDATE questionnaire_items SET step=?, section=?, question_key=?, label=?, type=?, options=?, required=?, sort_order=?, help_text=? WHERE id=?");
$stmt->execute([$step, $section, $key, $label, $type, $options, $required, $sortOrder, $helpText, $id]);
} else {
$stmt = $pdo->prepare("INSERT INTO questionnaire_items (step, section, question_key, label, type, options, required, sort_order, help_text) VALUES (?,?,?,?,?,?,?,?,?)");
$stmt->execute([$step, $section, $key, $label, $type, $options, $required, $sortOrder, $helpText]);
}
$message = $id > 0 ? 'Вопрос обновлён.' : 'Вопрос создан.';
} catch (\PDOException $e) {
$message = 'Ошибка: ' . $e->getMessage();
}
}
}
}
// Delete item
if (isset($_GET['delete']) && auth_csrf_verify($_GET['csrf'] ?? '')) {
$pdo->prepare("DELETE FROM questionnaire_items WHERE id=?")->execute([(int)$_GET['delete']]);
$message = 'Вопрос удалён.';
}
// Toggle active
if (isset($_GET['toggle']) && auth_csrf_verify($_GET['csrf'] ?? '')) {
$pdo->prepare("UPDATE questionnaire_items SET is_active=NOT is_active WHERE id=?")->execute([(int)$_GET['toggle']]);
$message = 'Статус изменён.';
}
$editItem = null;
if (isset($_GET['edit'])) {
$stmt = $pdo->prepare("SELECT * FROM questionnaire_items WHERE id=?");
$stmt->execute([(int)$_GET['edit']]);
$editItem = $stmt->fetch();
}
$stepFilter = (int)($_GET['step'] ?? 0);
if ($stepFilter > 0) {
$stmt = $pdo->prepare("SELECT * FROM questionnaire_items WHERE step=? ORDER BY sort_order, id");
$stmt->execute([$stepFilter]);
} else {
$stmt = $pdo->query("SELECT * FROM questionnaire_items ORDER BY step, sort_order, id");
}
$items = $stmt->fetchAll();
$stepLabels = [1 => 'Коммерческий', 2 => 'Технический', 3 => 'Эксплуатация', 4 => 'Риски', 5 => 'Расчёт SLA'];
$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> / <a href="questionnaire.php">Опросник</a> / Настройка</div>
</div>
<button class="btn btn-primary btn-sm" onclick="openModal()" data-tooltip="Добавить вопрос">+ Добавить вопрос</button>
</div>
<?php if ($message): ?>
<div class="alert alert-info"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<div class="step-tabs" style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap;">
<a href="questionnaire_config.php" class="btn btn-secondary btn-sm <?= $stepFilter === 0 ? 'active' : '' ?>">Все шаги</a>
<?php foreach ($stepLabels as $num => $label): ?>
<a href="?step=<?= $num ?>" class="btn btn-secondary btn-sm <?= $stepFilter === $num ? 'active' : '' ?>"><?= $num ?>. <?= $label ?></a>
<?php endforeach; ?>
</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>Статус</th>
<th class="actions">Действия</th>
</tr>
</thead>
<tbody>
<?php if (empty($items)): ?>
<tr><td colspan="9" style="text-align:center;color:var(--text-muted);">Нет вопросов</td></tr>
<?php endif; ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?= $item['step'] ?></td>
<td><?= htmlspecialchars($item['section']) ?></td>
<td><code><?= htmlspecialchars($item['question_key']) ?></code></td>
<td><?= htmlspecialchars($item['label']) ?></td>
<td><?= $item['type'] ?></td>
<td><?= $item['required'] ? '✓' : '—' ?></td>
<td><?= $item['sort_order'] ?></td>
<td><span class="badge badge-<?= $item['is_active'] ? 'done' : 'cancelled' ?>"><?= $item['is_active'] ? 'Активен' : 'Отключён' ?></span></td>
<td class="actions">
<a href="?edit=<?= $item['id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="Редактировать">✏️</a>
<a href="?toggle=<?= $item['id'] ?>&csrf=<?= $csrf ?>" class="btn btn-secondary btn-sm" data-tooltip="Переключить статус">🔄</a>
<a href="?delete=<?= $item['id'] ?>&csrf=<?= $csrf ?>" class="btn btn-danger btn-sm" onclick="return confirm('Удалить вопрос?')" data-tooltip="Удалить">🗑️</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="modal-overlay <?= $editItem ? 'open' : '' ?>" id="itemModal">
<div class="modal">
<button class="modal-close" onclick="closeModal()">&times;</button>
<h2><?= $editItem ? 'Редактировать' : 'Добавить' ?> вопрос</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="save_item" value="1">
<input type="hidden" name="id" value="<?= $editItem['id'] ?? 0 ?>">
<div class="form-row">
<div class="form-group">
<label for="step">Шаг *</label>
<select id="step" name="step" required>
<?php foreach ($stepLabels as $num => $label): ?>
<option value="<?= $num ?>" <?= ($editItem['step'] ?? 1) == $num ? 'selected' : '' ?>><?= $num ?>. <?= $label ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="type">Тип *</label>
<select id="type" name="type" required>
<?php foreach (['text','number','select','radio','checkbox','textarea'] as $t): ?>
<option value="<?= $t ?>" <?= ($editItem['type'] ?? 'text') === $t ? 'selected' : '' ?>><?= $t ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="form-group">
<label for="section">Секция</label>
<input type="text" id="section" name="section" value="<?= htmlspecialchars($editItem['section'] ?? '') ?>">
</div>
<div class="form-row">
<div class="form-group">
<label for="question_key">Ключ *</label>
<input type="text" id="question_key" name="question_key" value="<?= htmlspecialchars($editItem['question_key'] ?? '') ?>" required>
</div>
<div class="form-group">
<label for="sort_order">Порядок</label>
<input type="number" id="sort_order" name="sort_order" value="<?= $editItem['sort_order'] ?? 0 ?>">
</div>
</div>
<div class="form-group">
<label for="label">Название *</label>
<input type="text" id="label" name="label" value="<?= htmlspecialchars($editItem['label'] ?? '') ?>" required>
</div>
<div class="form-group">
<label for="options">Опции (JSON)</label>
<textarea id="options" name="options" placeholder='{"key":"Label"} или ["opt1","opt2"]' rows="3"><?= htmlspecialchars($editItem['options'] ?? '') ?></textarea>
<small style="color:var(--text-muted);">Для select/radio: JSON объект или массив</small>
</div>
<div class="form-group">
<label for="help_text">Подсказка</label>
<input type="text" id="help_text" name="help_text" value="<?= htmlspecialchars($editItem['help_text'] ?? '') ?>">
</div>
<label class="form-check">
<input type="checkbox" name="required" <?= ($editItem['required'] ?? 0) ? 'checked' : '' ?>> Обязательное поле
</label>
<div style="margin-top:16px;">
<button type="submit" class="btn btn-primary">Сохранить</button>
<button type="button" class="btn btn-secondary" onclick="closeModal()">Отмена</button>
</div>
</form>
</div>
</div>
<script>
function openModal() { document.getElementById('itemModal').classList.add('open'); }
function closeModal() { document.getElementById('itemModal').classList.remove('open'); window.location.href = 'questionnaire_config.php'; }
</script>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
+190
View File
@@ -0,0 +1,190 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('owner', 'engineer');
$pdo = getDB();
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$objectId = (int)($_POST['object_id'] ?? 0);
$client = trim($_POST['client_name'] ?? '');
$start = $_POST['start_date'] ?? '';
$end = $_POST['end_date'] ?? '';
$price = (float)($_POST['sla_price_monthly'] ?? 0);
$level = $_POST['service_level'] ?? 'business';
$status = $_POST['status'] ?? 'active';
$cr = $session['user_id'];
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
} elseif (!$objectId || !$start) {
$message = 'Объект и дата начала обязательны.';
} else {
try {
if ($id > 0) {
$stmt = $pdo->prepare("UPDATE sla_contracts SET object_id=?, client_name=?, start_date=?, end_date=?, sla_price_monthly=?, service_level=?, status=? WHERE id=?");
$stmt->execute([$objectId, $client, $start, $end ?: null, $price, $level, $status, $id]);
} else {
$stmt = $pdo->prepare("INSERT INTO sla_contracts (object_id, client_name, start_date, end_date, sla_price_monthly, service_level, status, created_by) VALUES (?,?,?,?,?,?,?,?)");
$stmt->execute([$objectId, $client, $start, $end ?: null, $price, $level, $status, $cr]);
}
$message = 'SLA контракт сохранён.';
auth_log($session['user_id'], 'sla_save', "Object: $objectId");
} catch (\PDOException $e) {
$message = 'Ошибка: ' . $e->getMessage();
}
}
}
$editSla = null;
if (isset($_GET['edit'])) {
$stmt = $pdo->prepare("SELECT * FROM sla_contracts WHERE id=?");
$stmt->execute([(int)$_GET['edit']]);
$editSla = $stmt->fetch();
}
$objectFilter = (int)($_GET['object_id'] ?? 0);
if ($objectFilter) {
$stmt = $pdo->prepare("SELECT sc.*, o.name as object_name FROM sla_contracts sc JOIN objects o ON o.id=sc.object_id WHERE sc.object_id=? ORDER BY sc.created_at DESC");
$stmt->execute([$objectFilter]);
} else {
$stmt = $pdo->query("SELECT sc.*, o.name as object_name FROM sla_contracts sc JOIN objects o ON o.id=sc.object_id ORDER BY sc.created_at DESC");
}
$contracts = $stmt->fetchAll();
$objects = $pdo->query("SELECT id, name FROM objects WHERE status IN ('active','audit') ORDER BY name")->fetchAll();
$csrf = auth_csrf_token();
require __DIR__ . '/../../inc/header.php';
?>
<div class="main-header">
<div>
<h1>SLA Контракты</h1>
<div class="breadcrumb"><a href="/service/dashboard.php">Главная</a> / SLA</div>
</div>
<button class="btn btn-primary btn-sm" onclick="openModal()" data-tooltip="Добавить SLA контракт">+ Добавить</button>
</div>
<?php if ($message): ?>
<div class="alert alert-info"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<div class="filter-bar">
<form method="get">
<select name="object_id" onchange="this.form.submit()">
<option value="">Все объекты</option>
<?php foreach ($objects as $o): ?>
<option value="<?= $o['id'] ?>" <?= $objectFilter === $o['id'] ? 'selected' : '' ?>><?= htmlspecialchars($o['name']) ?></option>
<?php endforeach; ?>
</select>
<?php if ($objectFilter): ?><a href="sla.php" class="btn btn-secondary btn-sm" data-tooltip="Сбросить фильтр">Сброс</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($contracts)): ?>
<tr><td colspan="7" style="text-align:center;color:var(--text-muted);">Нет SLA контрактов</td></tr>
<?php endif; ?>
<?php foreach ($contracts as $c): ?>
<tr>
<td><strong><?= htmlspecialchars($c['object_name']) ?></strong></td>
<td><?= htmlspecialchars($c['client_name'] ?: '—') ?></td>
<td><span class="badge badge-<?= $c['service_level'] === 'enterprise' ? 'critical' : ($c['service_level'] === 'business' ? 'high' : 'medium') ?>"><?= $c['service_level'] ?></span></td>
<td><strong><?= fmt_money($c['sla_price_monthly']) ?></strong></td>
<td><?= date('d.m.Y', strtotime($c['start_date'])) . ($c['end_date'] ? ' — ' . date('d.m.Y', strtotime($c['end_date'])) : '') ?></td>
<td><span class="badge badge-<?= $c['status'] ?>"><?= $c['status'] ?></span></td>
<td class="actions">
<a href="?edit=<?= $c['id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="Редактировать">✏️</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="modal-overlay <?= $editSla ? 'open' : '' ?>" id="slaModal">
<div class="modal">
<button class="modal-close" onclick="closeModal()">&times;</button>
<h2><?= $editSla ? 'Редактировать' : 'Добавить' ?> SLA контракт</h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="id" value="<?= $editSla['id'] ?? 0 ?>">
<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'] ?>" <?= ($editSla['object_id'] ?? 0) === $o['id'] ? 'selected' : ($objectFilter === $o['id'] ? 'selected' : '') ?>><?= htmlspecialchars($o['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="client_name">Клиент</label>
<input type="text" id="client_name" name="client_name" value="<?= htmlspecialchars($editSla['client_name'] ?? '') ?>">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="service_level">Уровень</label>
<select id="service_level" name="service_level">
<option value="start" <?= ($editSla['service_level'] ?? '') === 'start' ? 'selected' : '' ?>>Start</option>
<option value="business" <?= ($editSla['service_level'] ?? '') === 'business' ? 'selected' : '' ?>>Business</option>
<option value="enterprise" <?= ($editSla['service_level'] ?? '') === 'enterprise' ? 'selected' : '' ?>>Enterprise</option>
</select>
</div>
<div class="form-group">
<label for="sla_price_monthly">Цена в месяц (₽)</label>
<input type="number" id="sla_price_monthly" name="sla_price_monthly" step="0.01" value="<?= $editSla['sla_price_monthly'] ?? 0 ?>">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="start_date">Дата начала *</label>
<input type="date" id="start_date" name="start_date" value="<?= $editSla['start_date'] ?? '' ?>" required>
</div>
<div class="form-group">
<label for="end_date">Дата окончания</label>
<input type="date" id="end_date" name="end_date" value="<?= $editSla['end_date'] ?? '' ?>">
</div>
</div>
<div class="form-group">
<label for="status">Статус</label>
<select id="status" name="status">
<option value="active" <?= ($editSla['status'] ?? '') === 'active' ? 'selected' : '' ?>>Активен</option>
<option value="expired" <?= ($editSla['status'] ?? '') === 'expired' ? 'selected' : '' ?>>Истёк</option>
<option value="cancelled" <?= ($editSla['status'] ?? '') === 'cancelled' ? 'selected' : '' ?>>Отменён</option>
<option value="negotiation" <?= ($editSla['status'] ?? '') === 'negotiation' ? 'selected' : '' ?>>В переговорах</option>
</select>
</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('slaModal').classList.add('open'); }
function closeModal() { document.getElementById('slaModal').classList.remove('open'); window.location.href = 'sla.php'; }
</script>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
+209
View File
@@ -0,0 +1,209 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('owner');
$pdo = getDB();
$message = '';
// Create / Update user
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$login = trim($_POST['login'] ?? '');
$password = $_POST['password'] ?? '';
$fullName = trim($_POST['full_name'] ?? '');
$role = $_POST['role'] ?? 'technician';
$phone = trim($_POST['phone'] ?? '');
$email = trim($_POST['email'] ?? '');
$isActive = isset($_POST['is_active']) ? 1 : 0;
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
} elseif ($login === '' || $fullName === '') {
$message = 'Заполните обязательные поля.';
} elseif ($id === 0 && $password === '') {
$message = 'Пароль обязателен для нового сотрудника.';
} else {
try {
// Check if we're editing an owner — prevent role change
$isOwnerEdit = false;
if ($id > 0) {
$checkStmt = $pdo->prepare("SELECT role FROM users WHERE id=?");
$checkStmt->execute([$id]);
$checkRow = $checkStmt->fetch();
if ($checkRow && $checkRow['role'] === 'owner') {
$isOwnerEdit = true;
$role = 'owner'; // Force owner role, ignore POST value
}
}
if ($id > 0) {
// Update
if ($password !== '') {
$hash = password_hash($password, PASSWORD_BCRYPT);
$stmt = $pdo->prepare("UPDATE users SET login=?, password_hash=?, full_name=?, role=?, phone=?, email=?, is_active=? WHERE id=?");
$stmt->execute([$login, $hash, $fullName, $role, $phone, $email, $isActive, $id]);
} else {
$stmt = $pdo->prepare("UPDATE users SET login=?, full_name=?, role=?, phone=?, email=?, is_active=? WHERE id=?");
$stmt->execute([$login, $fullName, $role, $phone, $email, $isActive, $id]);
}
$message = $isOwnerEdit ? 'Профиль обновлён.' : 'Сотрудник обновлён.';
auth_log($session['user_id'], 'user_update', "ID: $id");
} else {
$hash = password_hash($password, PASSWORD_BCRYPT);
$stmt = $pdo->prepare("INSERT INTO users (login, password_hash, full_name, role, phone, email, is_active) VALUES (?,?,?,?,?,?,?)");
$stmt->execute([$login, $hash, $fullName, $role, $phone, $email, $isActive]);
$message = 'Сотрудник создан.';
auth_log($session['user_id'], 'user_create', "Login: $login");
}
} catch (\PDOException $e) {
$message = 'Ошибка: ' . ($e->getCode() === '23000' ? 'Логин уже занят.' : $e->getMessage());
}
}
}
// Delete user
if (isset($_GET['delete'])) {
$id = (int)$_GET['delete'];
$csrf = $_GET['csrf'] ?? '';
if (auth_csrf_verify($csrf)) {
$stmt = $pdo->prepare("DELETE FROM users WHERE id=? AND role!='owner'");
$stmt->execute([$id]);
$message = 'Сотрудник удалён.';
auth_log($session['user_id'], 'user_delete', "ID: $id");
}
}
// Get edit user
$editUser = null;
$isEditingOwner = false;
if (isset($_GET['edit'])) {
$stmt = $pdo->prepare("SELECT * FROM users WHERE id=?");
$stmt->execute([(int)$_GET['edit']]);
$editUser = $stmt->fetch();
if ($editUser && $editUser['role'] === 'owner') {
$isEditingOwner = true;
}
}
// Get all users (except owner for editing, but show owner in list)
$users = $pdo->query("SELECT id, login, full_name, role, phone, email, is_active, created_at FROM users ORDER BY role, 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()" data-tooltip="Добавить сотрудника">+ Добавить</button>
</div>
<?php if ($message): ?>
<div class="alert alert-info"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<div class="card">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Логин</th>
<th>ФИО</th>
<th>Роль</th>
<th>Телефон</th>
<th>Email</th>
<th>Статус</th>
<th>Создан</th>
<th class="actions">Действия</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $u): ?>
<tr>
<td><?= htmlspecialchars($u['login']) ?></td>
<td><?= htmlspecialchars($u['full_name']) ?></td>
<td><span class="badge badge-<?= $u['role'] === 'owner' ? 'critical' : ($u['role'] === 'engineer' ? 'high' : 'medium') ?>"><?= $u['role'] === 'owner' ? 'Владелец' : ($u['role'] === 'engineer' ? 'Инженер' : 'Техник') ?></span></td>
<td><?= htmlspecialchars($u['phone'] ?: '—') ?></td>
<td><?= htmlspecialchars($u['email'] ?: '—') ?></td>
<td><span class="badge <?= $u['is_active'] ? 'badge-active' : 'badge-inactive' ?>"><?= $u['is_active'] ? 'Активен' : 'Заблокирован' ?></span></td>
<td><?= date('d.m.Y', strtotime($u['created_at'])) ?></td>
<td class="actions">
<a href="?edit=<?= $u['id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="Редактировать">✏️</a>
<?php if ($u['role'] !== 'owner'): ?>
<a href="?delete=<?= $u['id'] ?>&csrf=<?= $csrf ?>" class="btn btn-danger btn-sm" onclick="return confirm('Удалить сотрудника?')" data-tooltip="Удалить">🗑️</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Modal -->
<div class="modal-overlay <?= $editUser ? 'open' : '' ?>" id="userModal">
<div class="modal">
<button class="modal-close" onclick="closeModal()">&times;</button>
<h2><?= $editUser ? ($isEditingOwner ? 'Редактировать профиль' : 'Редактировать сотрудника') : 'Добавить сотрудника' ?></h2>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="id" value="<?= $editUser['id'] ?? 0 ?>">
<div class="form-row">
<div class="form-group">
<label for="login">Логин *</label>
<input type="text" id="login" name="login" value="<?= htmlspecialchars($editUser['login'] ?? '') ?>" required>
</div>
<div class="form-group">
<label for="full_name">ФИО *</label>
<input type="text" id="full_name" name="full_name" value="<?= htmlspecialchars($editUser['full_name'] ?? '') ?>" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="password">Пароль <?= $editUser ? '(оставьте пустым, чтобы не менять)' : '*' ?></label>
<input type="password" id="password" name="password" <?= $editUser ? '' : 'required' ?>>
</div>
<div class="form-group">
<label for="role">Роль *</label>
<?php if ($isEditingOwner): ?>
<input type="text" value="Владелец" disabled style="opacity:0.6;">
<input type="hidden" name="role" value="owner">
<?php else: ?>
<select id="role" name="role" required>
<option value="engineer" <?= ($editUser['role'] ?? '') === 'engineer' ? 'selected' : '' ?>>Инженер</option>
<option value="technician" <?= ($editUser['role'] ?? '') === 'technician' ? 'selected' : '' ?>>Техник</option>
</select>
<?php endif; ?>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="phone">Телефон</label>
<input type="text" id="phone" name="phone" value="<?= htmlspecialchars($editUser['phone'] ?? '') ?>">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" value="<?= htmlspecialchars($editUser['email'] ?? '') ?>">
</div>
</div>
<div class="form-check" style="margin-bottom:16px;">
<input type="checkbox" id="is_active" name="is_active" <?= (!isset($editUser) || $editUser['is_active']) ? 'checked' : '' ?>>
<label for="is_active">Активен</label>
</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('userModal').classList.add('open'); }
function closeModal() { document.getElementById('userModal').classList.remove('open'); window.location.href = 'users.php'; }
</script>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Доступ запрещён AegisOne</title>
<link rel="stylesheet" href="/service-style.php">
</head>
<body class="login-page">
<div class="login-box" style="text-align:center;">
<h1 style="font-size:48px;color:var(--danger);">403</h1>
<p style="margin:16px 0;">У вас нет прав для доступа к этой странице.</p>
<a href="/service/dashboard.php" class="btn btn-primary">Вернуться на дашборд</a>
</div>
</body>
</html>
+133
View File
@@ -0,0 +1,133 @@
<?php
/**
* AegisOne Engineering — Просмотр документации
*
* Доступ: owner (все документы), engineer (только разрешённые владельцем)
*
* GET-параметры:
* ?view=slug — просмотр конкретного документа
*/
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
require_once __DIR__ . '/../../inc/docs_functions.php';
$session = auth_require('owner', 'engineer');
$userRole = $session['user_role'];
$userName = $session['user_name'];
$viewSlug = trim($_GET['view'] ?? '');
$docs = get_allowed_docs($userRole);
// Если запрошен просмотр конкретного документа
if ($viewSlug !== '') {
// Проверяем, что документ есть в разрешённых
$allowedSlugs = array();
foreach ($docs as $d) { $allowedSlugs[] = $d['slug']; }
if (!in_array($viewSlug, $allowedSlugs, true)) {
http_response_code(403);
echo '<div class="error-page"><h1>403</h1><p>Документ недоступен для вашей роли.</p><a href="?">← К списку документов</a></div>';
exit;
}
$content = get_doc_content($viewSlug);
if ($content === null) {
echo '<div class="error-page"><h1>404</h1><p>Документ не найден.</p><a href="?">← К списку документов</a></div>';
exit;
}
$title = '';
foreach ($docs as $d) {
if ($d['slug'] === $viewSlug) {
$title = $d['title'];
break;
}
}
require __DIR__ . '/../../inc/header.php';
?>
<div class="doc-viewer">
<div class="doc-viewer__toolbar">
<a href="?" class="btn btn-sm btn-secondary">← Все документы</a>
<span class="doc-viewer__title"><?= htmlspecialchars($title) ?></span>
</div>
<div class="doc-viewer__content markdown-body">
<?= render_markdown_simple($content) ?>
</div>
</div>
<?php
require __DIR__ . '/../../inc/footer.php';
exit;
}
// Список документов
$pageTitle = 'Документация';
require __DIR__ . '/../../inc/header.php';
?>
<div class="docs-list">
<div class="page-header">
<h1>Документация</h1>
<?php if ($userRole === 'owner'): ?>
<p class="page-subtitle">Управляйте доступом инженеров к документам в разделе <a href="/service/pages/owner/docs_section.php">Управление документами</a>.</p>
<?php endif; ?>
</div>
<?php if (empty($docs)): ?>
<div class="empty-state">
<h2>Документов пока нет</h2>
<p>Администратор ещё не добавил документы, или ни один документ не разрешён для вашей роли.</p>
</div>
<?php else: ?>
<div class="docs-grid">
<?php foreach ($docs as $doc): ?>
<a href="?view=<?= urlencode($doc['slug']) ?>" class="doc-card">
<div class="doc-card__icon">
<svg viewBox="0 0 64 64" width="48" height="48"><rect x="16" y="8" width="32" height="48" rx="4" fill="none" stroke="var(--accent)" stroke-width="3"/><line x1="22" y1="20" x2="42" y2="20" stroke="var(--accent)" stroke-width="3"/><line x1="22" y1="30" x2="42" y2="30" stroke="var(--accent)" stroke-width="3"/><line x1="22" y1="40" x2="34" y2="40" stroke="var(--accent)" stroke-width="3"/></svg>
</div>
<div class="doc-card__body">
<h3><?= htmlspecialchars($doc['title']) ?></h3>
<span class="doc-card__meta">Markdown документ</span>
</div>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<style>
.docs-list { padding: 24px; }
.page-header { margin-bottom: 32px; }
.page-header h1 { font-size: 24px; color: var(--text-primary); margin-bottom: 8px; }
.page-subtitle { font-size: 13px; color: var(--text-muted); }
.page-subtitle a { color: var(--accent); }
.docs-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
.doc-card {
display: flex; align-items: center; gap: 16px; padding: 20px;
background: var(--bg-card); border: 1px solid var(--border);
border-radius: 10px; text-decoration: none; transition: border-color 0.2s, box-shadow 0.2s;
}
.doc-card:hover { border-color: var(--accent); box-shadow: 0 2px 12px rgba(0,173,239,0.1); }
.doc-card__icon { flex-shrink: 0; }
.doc-card__body h3 { font-size: 15px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
.doc-card__meta { font-size: 12px; color: var(--text-muted); }
.doc-viewer { padding: 24px; max-width: 960px; }
.doc-viewer__toolbar { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; padding-bottom: 16px; border-bottom: 1px solid var(--border); }
.doc-viewer__title { font-size: 13px; color: var(--text-muted); }
.doc-viewer__content { line-height: 1.8; font-size: 14px; color: var(--text-primary); }
.markdown-body h1 { font-size: 26px; margin: 32px 0 16px; color: var(--text-primary); }
.markdown-body h2 { font-size: 20px; margin: 24px 0 12px; padding-bottom: 8px; border-bottom: 2px solid var(--accent); color: var(--text-primary); }
.markdown-body h3 { font-size: 16px; margin: 20px 0 10px; color: var(--text-primary); }
.markdown-body p { margin-bottom: 12px; }
.markdown-body ul { margin: 8px 0 16px 24px; }
.markdown-body li { margin-bottom: 4px; }
.markdown-body strong { color: var(--accent); }
.markdown-body table { width: 100%; border-collapse: collapse; margin: 16px 0; }
.markdown-body td, .markdown-body th { border: 1px solid var(--border); padding: 8px 12px; text-align: left; font-size: 13px; color: var(--text-primary); }
.markdown-body th { background: var(--bg-card); font-weight: 600; }
.markdown-body a { color: var(--accent); }
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
.empty-state h2 { font-size: 20px; margin-bottom: 8px; }
</style>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
+140
View File
@@ -0,0 +1,140 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('owner', 'engineer');
$pdo = getDB();
$id = (int)($_GET['id'] ?? 0);
$stmt = $pdo->prepare("SELECT op.*, o.name as object_name, u.full_name as author_name FROM object_passports op LEFT JOIN objects o ON o.id=op.object_id LEFT JOIN users u ON u.id=op.created_by WHERE op.id=?");
$stmt->execute([$id]);
$pass = $stmt->fetch();
if (!$pass) {
header('Location: passports.php');
exit;
}
$data = json_decode($pass['passport_data'], true) ?: [];
require __DIR__ . '/../../inc/header.php';
?>
<div class="main-header">
<div>
<h1>Паспорт объекта</h1>
<div class="breadcrumb"><a href="/service/dashboard.php">Главная</a> / <a href="passports.php">Паспорта</a> / #<?= $pass['id'] ?></div>
</div>
<a href="javascript:window.print()" class="btn btn-secondary btn-sm" data-tooltip="Распечатать">🖨️ Печать</a>
</div>
<div class="card" style="border-color:var(--accent);">
<div class="card-header">
<h2><?= htmlspecialchars($pass['object_name'] ?: 'Паспорт #' . $pass['id']) ?></h2>
<span class="badge badge-<?= object_class($pass['object_index']) ?>">Класс <?= object_class($pass['object_index']) ?></span>
</div>
<div class="card-grid">
<div>
<div class="card-value value-accent"><?= $pass['risk_score'] ?></div>
<div class="card-label">Оценка риска (<?= risk_label($pass['risk_score']) ?>)</div>
</div>
<div>
<div class="card-value value-info"><?= $pass['complexity_index'] ?></div>
<div class="card-label">Сложность обслуживания</div>
</div>
<div>
<div class="card-value"><?= $pass['object_index'] ?></div>
<div class="card-label">Индекс объекта · <?= object_class_label($pass['object_index']) ?></div>
</div>
<div>
<div class="card-value value-green"><?= fmt_money($pass['sla_price_monthly']) ?></div>
<div class="card-label">Стоимость SLA / месяц</div>
</div>
</div>
<p style="color:var(--text-muted);font-size:12px;">Автор: <?= htmlspecialchars($pass['author_name'] ?: '—') ?> · <?= date('d.m.Y H:i', strtotime($pass['created_at'])) ?></p>
</div>
<?php if (!empty($data['answers'])): $a = $data['answers']; ?>
<div class="card">
<h3 style="margin-bottom:12px;">Данные опросника</h3>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;">
<?php foreach ([
'object_name' => 'Название объекта', 'object_type' => 'Тип объекта', 'address' => 'Адрес',
'area' => 'Площадь (м²)', 'employees' => 'Сотрудников',
'contact_person' => 'Контактное лицо', 'contact_phone' => 'Телефон',
'region_factor' => 'Регион',
'camera_count' => 'Количество камер', 'video_type' => 'Тип видеонаблюдения',
'archive_depth' => 'Глубина архива',
'access_points' => 'Точки СКУД', 'acs_vendor' => 'Производитель СКУД',
'fire_type' => 'Пожарная сигнализация',
'server_state' => 'Сервер', 'network_state' => 'Сеть', 'power_state' => 'Электропитание',
'service_state' => 'Обслуживание', 'resolution_time' => 'Время устранения',
'controller' => 'Контроль системы',
'sla_level' => 'Уровень SLA', 'visit_frequency' => 'Частота выездов',
'reaction_time' => 'Время реакции',
] as $key => $label): ?>
<?php if (!empty($a[$key])): ?>
<div>
<span style="color:var(--text-muted);font-size:11px;"><?= $label ?></span>
<div style="font-weight:600;"><?= htmlspecialchars(is_array($a[$key]) ? implode(', ', $a[$key]) : $a[$key]) ?></div>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php if (!empty($data)): ?>
<div class="card">
<h3 style="margin-bottom:12px;">Расчётные параметры</h3>
<div class="card-grid">
<div>
<div class="card-label">Компоненты оценки риска</div>
<div style="font-size:12px;">
<?php if (isset($data['risk_score'])): ?>
Оценка риска = <?= $data['risk_score'] ?><br>
<?php endif; ?>
<?php if (isset($data['complexity'])): ?>
Сложность = <?= $data['complexity'] ?><br>
<?php endif; ?>
<?php if (isset($data['infra_load'])): ?>
Нагрузка на инфраструктуру = <?= $data['infra_load'] ?><br>
<?php endif; ?>
<?php if (isset($data['service_history'])): ?>
История обслуживания = <?= $data['service_history'] ?>
<?php endif; ?>
</div>
</div>
<div>
<div class="card-label">Формула индекса объекта</div>
<div style="font-size:12px;">
(Риск × 0.4) + (Сложность × 0.3) + (Инфраструктура × 0.2) + (История × 0.1)<br>
<?php if (isset($data['object_index'])): ?>
= <strong><?= $data['object_index'] ?></strong>
<?php endif; ?>
</div>
</div>
<div>
<div class="card-label">Стоимость SLA</div>
<div style="font-size:12px;">
Базовая стоимость × Индекс объекта × Регион × Множитель риска<br>
<?php if (isset($data['sla_price'])): ?>
= <strong><?= fmt_money($data['sla_price']) ?></strong> / мес
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php endif; ?>
<div style="margin-top:16px;">
<a href="questionnaire.php?id=<?= $pass['session_id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="Открыть опросник">📝 Опросник</a>
<?php if ($pass['object_id']): ?>
<a href="../owner/objects.php?edit=<?= $pass['object_id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="Перейти к объекту">🏢 Объект</a>
<a href="../owner/sla.php?object_id=<?= $pass['object_id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="Создать SLA контракт">📋 Создать SLA</a>
<?php endif; ?>
</div>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
+61
View File
@@ -0,0 +1,61 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('owner', 'engineer');
$pdo = getDB();
$passports = $pdo->query("
SELECT op.*, o.name as object_name, u.full_name as author_name
FROM object_passports op
LEFT JOIN objects o ON o.id = op.object_id
LEFT JOIN users u ON u.id = op.created_by
ORDER BY op.created_at DESC
")->fetchAll();
require __DIR__ . '/../../inc/header.php';
?>
<div class="main-header">
<div>
<h1>Паспорта объектов</h1>
<div class="breadcrumb"><a href="/service/dashboard.php">Главная</a> / Паспорта</div>
</div>
<a href="../shared/questionnaire.php?new=1" class="btn btn-primary btn-sm" data-tooltip="Начать опрос для создания паспорта">+ Новый опросник</a>
</div>
<div class="card">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Объект</th>
<th>Автор</th>
<th>Индекс объекта</th>
<th>Оценка риска</th>
<th>Стоимость SLA</th>
<th>Дата</th>
<th class="actions"></th>
</tr>
</thead>
<tbody>
<?php if (empty($passports)): ?>
<tr><td colspan="7" style="text-align:center;color:var(--text-muted);">Нет паспортов</td></tr>
<?php endif; ?>
<?php foreach ($passports as $p): ?>
<tr>
<td><strong><?= htmlspecialchars($p['object_name'] ?: '—') ?></strong></td>
<td><?= htmlspecialchars($p['author_name'] ?: '—') ?></td>
<td><strong><?= $p['object_index'] ?></strong> (<?= object_class($p['object_index']) ?>)</td>
<td><span class="badge badge-<?= $p['risk_score'] <= 20 ? 'done' : ($p['risk_score'] <= 50 ? 'high' : ($p['risk_score'] <= 75 ? 'medium' : 'critical')) ?>"><?= $p['risk_score'] ?></span></td>
<td><?= fmt_money($p['sla_price_monthly']) ?></td>
<td><?= date('d.m.Y', strtotime($p['created_at'])) ?></td>
<td class="actions"><a href="passport_view.php?id=<?= $p['id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="Просмотр">👁️</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
+393
View File
@@ -0,0 +1,393 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('owner', 'engineer');
$pdo = getDB();
$message = '';
$userId = $session['user_id'];
// Create new session
if (isset($_GET['new'])) {
$stmt = $pdo->prepare("INSERT INTO questionnaire_sessions (created_by) VALUES (?)");
$stmt->execute([$userId]);
$sessionId = $pdo->lastInsertId();
header("Location: questionnaire.php?id=$sessionId");
exit;
}
$sessId = (int)($_GET['id'] ?? 0);
$sess = null;
if ($sessId) {
$stmt = $pdo->prepare("SELECT * FROM questionnaire_sessions WHERE id=?");
$stmt->execute([$sessId]);
$sess = $stmt->fetch();
if (!$sess) { $sessId = 0; }
}
// Handle manual step navigation via GET
if ($sessId && isset($_GET['step']) && $sess && $sess['status'] !== 'completed') {
$goStep = (int)$_GET['step'];
if ($goStep >= 1 && $goStep <= 5 && $goStep <= (int)$sess['current_step']) {
$pdo->prepare("UPDATE questionnaire_sessions SET current_step=? WHERE id=?")->execute([$goStep, $sessId]);
header("Location: questionnaire.php?id=$sessId");
exit;
}
}
// Save answers
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $sess) {
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
} else {
$step = (int)($_POST['step'] ?? 1);
$goNext = isset($_POST['next']);
$answers = [];
// Collect all answers from POST (skip meta fields)
foreach ($_POST as $key => $val) {
if (strpos($key, '_') === 0 || $key === 'csrf_token' || $key === 'step' || $key === 'next' || $key === 'save') continue;
if (is_array($val)) {
$answers[$key] = implode(',', $val);
} else {
$answers[$key] = $val;
}
}
// Save answers
$stmt = $pdo->prepare("INSERT INTO questionnaire_answers (session_id, step, question_key, answer_value) VALUES ($1,$2,$3,$4) ON CONFLICT (session_id, question_key) DO UPDATE SET answer_value=EXCLUDED.answer_value");
foreach ($answers as $qk => $qv) {
$stmt->execute([$sessId, $step, $qk, $qv]);
}
if ($goNext && $step < 5) {
$pdo->prepare("UPDATE questionnaire_sessions SET current_step=? WHERE id=?")->execute([$step + 1, $sessId]);
header("Location: questionnaire.php?id=$sessId");
exit;
}
// Step 5 = final calculation
if ($step === 5 || isset($_POST['save'])) {
// Calculate everything from all answers
$allAnswers = $pdo->prepare("SELECT question_key, answer_value FROM questionnaire_answers WHERE session_id=?");
$allAnswers->execute([$sessId]);
$qa = [];
foreach ($allAnswers as $row) {
$qa[$row['question_key']] = $row['answer_value'];
}
$riskScore = calc_risk_score(
($qa['no_archive'] ?? '') === 'on',
($qa['no_power'] ?? '') === 'on',
($qa['no_regulations'] ?? '') === 'on',
($qa['frequent_failures'] ?? '') === 'on',
($qa['no_documentation'] ?? '') === 'on'
);
$complexity = calc_complexity(
(int)($qa['camera_count'] ?? 0),
(int)($qa['access_points'] ?? 0),
$qa['fire_type'] ?? 'simple',
isset($qa['has_it']) && $qa['has_it'] === 'on'
);
$infraLoad = calc_infrastructure_load(
$qa['server_state'] ?? 'ok',
$qa['network_state'] ?? 'stable',
$qa['power_state'] ?? 'ok'
);
$serviceHist = calc_service_history($qa['service_state'] ?? 'none');
$objIndex = calc_object_index($riskScore, $complexity, $infraLoad, $serviceHist);
// Region factor
$regionFactor = (float)($qa['region_factor'] ?? 1.0);
$baseCost = 15000;
$riskMult = risk_multiplier($riskScore);
$slaPrice = calc_sla_price($baseCost, $objIndex, $regionFactor, $riskMult);
// Update session with results
$pdo->prepare("UPDATE questionnaire_sessions SET
status='completed', current_step=5,
risk_score=?, complexity_index=?, infrastructure_load=?,
service_history=?, object_index=?, sla_price_monthly=?
WHERE id=?")->execute([$riskScore, $complexity, $infraLoad, $serviceHist, $objIndex, $slaPrice, $sessId]);
// Create/update object if we have name
if (!empty($qa['object_name'])) {
$stmtObj = $pdo->prepare("INSERT INTO objects (name, object_type, address, contact_person, contact_phone, status, risk_score, complexity_index, infrastructure_load, service_history, object_index, sla_price_monthly, region_factor, created_by, notes)
VALUES ($1,$2,$3,$4,$5,'active',$6,$7,$8,$9,$10,$11,$12,$13,'Из опросника #' || $14)
ON CONFLICT DO NOTHING");
$stmtObj->execute([
$qa['object_name'], $qa['object_type'] ?? '', $qa['address'] ?? '',
$qa['contact_person'] ?? '', $qa['contact_phone'] ?? '',
$riskScore, $complexity, $infraLoad, $serviceHist, $objIndex, $slaPrice, $regionFactor, $userId, $sessId
]);
$objId = $pdo->lastInsertId();
} else {
$objId = null;
}
// Passport
$passportData = json_encode([
'answers' => $qa,
'risk_score' => $riskScore,
'complexity' => $complexity,
'infra_load' => $infraLoad,
'service_history' => $serviceHist,
'object_index' => $objIndex,
'sla_price' => $slaPrice,
]);
$pdo->prepare("INSERT INTO object_passports (session_id, object_id, object_index, risk_score, complexity_index, sla_price_monthly, passport_data, created_by) VALUES (?,?,?,?,?,?,?,?)")
->execute([$sessId, $objId, $objIndex, $riskScore, $complexity, $slaPrice, $passportData, $userId]);
$message = '✅ Расчёт выполнен! Паспорт объекта сформирован.';
$sess = $pdo->prepare("SELECT * FROM questionnaire_sessions WHERE id=?");
$sess->execute([$sessId]);
$sess = $sess->fetch();
}
if (!isset($_POST['save']) && $step < 5) {
$message = 'Шаг сохранён.';
}
if (isset($_POST['save'])) {
$message = 'Данные сохранены.';
}
}
}
// Load existing answers for editing
$answers = [];
if ($sessId) {
$stmt = $pdo->prepare("SELECT question_key, answer_value FROM questionnaire_answers WHERE session_id=?");
$stmt->execute([$sessId]);
foreach ($stmt as $row) {
$answers[$row['question_key']] = $row['answer_value'];
}
}
// Load questionnaire items from DB
$qItems = [];
$qSections = [];
if ($sessId) {
$stmt = $pdo->prepare("SELECT * FROM questionnaire_items WHERE is_active=1 AND step=? ORDER BY sort_order, id");
$stmt->execute([$step]);
$qItems = $stmt->fetchAll();
// Group by section
foreach ($qItems as $qi) {
if ($qi['section'] && !isset($qSections[$qi['section']])) {
$qSections[$qi['section']] = [];
}
if ($qi['section']) {
$qSections[$qi['section']][] = $qi;
} else {
$qSections[''][] = $qi;
}
}
}
$csrf = auth_csrf_token();
// Sessions list
$sessions = $pdo->query("SELECT qs.*, u.full_name as author_name FROM questionnaire_sessions qs LEFT JOIN users u ON u.id=qs.created_by ORDER BY qs.created_at DESC LIMIT 20")->fetchAll();
require __DIR__ . '/../../inc/header.php';
?>
<div class="main-header">
<div>
<h1>Опросник объекта</h1>
<div class="breadcrumb"><a href="/service/dashboard.php">Главная</a> / Опросник</div>
</div>
<a href="?new=1" class="btn btn-primary btn-sm" data-tooltip="Начать новый опрос">+ Новый опросник</a>
</div>
<?php if ($message): ?>
<div class="alert alert-success"><?= $message ?></div>
<?php endif; ?>
<?php if ($sess && $sess['status'] === 'completed'): ?>
<div class="card" style="border-color:var(--success);">
<div class="card-header">
<h2>Результаты опросника</h2>
<span class="badge badge-done">Завершён</span>
</div>
<div class="card-grid">
<div>
<div class="card-value value-accent"><?= $sess['risk_score'] ?></div>
<div class="card-label">Risk Score (<?= risk_label($sess['risk_score']) ?>)</div>
</div>
<div>
<div class="card-value value-info"><?= $sess['complexity_index'] ?></div>
<div class="card-label">Complexity Index</div>
</div>
<div>
<div class="card-value"><?= $sess['object_index'] ?></div>
<div class="card-label">Object Index (класс <?= object_class($sess['object_index']) ?>)</div>
</div>
<div>
<div class="card-value value-green"><?= fmt_money($sess['sla_price_monthly']) ?></div>
<div class="card-label">SLA Цена / месяц</div>
</div>
</div>
</div>
<?php endif; ?>
<?php if ($sess && $sess['status'] !== 'completed'): ?>
<?php $step = (int)$sess['current_step']; ?>
<div class="step-indicators">
<?php $labels = ['Коммерческий', 'Технический', 'Эксплуатация', 'Риски', 'Расчёт SLA']; ?>
<?php foreach ($labels as $i => $label): $num = $i + 1; ?>
<div class="step-dot <?= $num < $step ? 'completed' : ($num === $step ? 'active' : '') ?>">
<?php if ($num < $step): ?>✓<?php else: ?><?= $num ?><?php endif; ?>
<?= $label ?>
</div>
<?php endforeach; ?>
</div>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="step" value="<?= $step ?>">
<?php if (!empty($qSections)): ?>
<div class="card">
<?php foreach ($qSections as $sectionName => $items): ?>
<?php if ($sectionName): ?>
<h2 style="margin-bottom:16px;"><?= htmlspecialchars($sectionName) ?></h2>
<?php endif; ?>
<?php foreach ($items as $qi): ?>
<?php
$val = $answers[$qi['question_key']] ?? '';
$opts = $qi['options'] ? json_decode($qi['options'], true) : null;
?>
<?php if ($qi['type'] === 'textarea'): ?>
<div class="form-group">
<label for="<?= $qi['question_key'] ?>"><?= htmlspecialchars($qi['label']) ?></label>
<textarea id="<?= $qi['question_key'] ?>" name="<?= $qi['question_key'] ?>" <?= $qi['required'] ? 'required' : '' ?>><?= htmlspecialchars($val) ?></textarea>
</div>
<?php elseif ($qi['type'] === 'select' && is_array($opts)): ?>
<div class="form-group">
<label for="<?= $qi['question_key'] ?>"><?= htmlspecialchars($qi['label']) ?></label>
<select id="<?= $qi['question_key'] ?>" name="<?= $qi['question_key'] ?>" <?= $qi['required'] ? 'required' : '' ?>>
<?php if (array_keys($opts) === range(0, count($opts) - 1)): ?>
<?php foreach ($opts as $opt): ?>
<option value="<?= htmlspecialchars($opt) ?>" <?= $val === $opt ? 'selected' : '' ?>><?= htmlspecialchars($opt) ?></option>
<?php endforeach; ?>
<?php else: ?>
<?php foreach ($opts as $k => $v): ?>
<option value="<?= htmlspecialchars($k) ?>" <?= $val === $k ? 'selected' : '' ?>><?= htmlspecialchars($v) ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<?php elseif ($qi['type'] === 'radio' && is_array($opts)): ?>
<div class="form-group">
<label><?= htmlspecialchars($qi['label']) ?></label>
<?php foreach ($opts as $k => $v): ?>
<label class="form-check">
<input type="radio" name="<?= $qi['question_key'] ?>" value="<?= htmlspecialchars($k) ?>" <?= $val === $k ? 'checked' : '' ?>> <?= htmlspecialchars($v) ?>
</label>
<?php endforeach; ?>
</div>
<?php elseif ($qi['type'] === 'checkbox'): ?>
<div class="form-group">
<?php if (is_array($opts) && array_keys($opts) !== range(0, count($opts) - 1)): ?>
<label><?= htmlspecialchars($qi['label']) ?></label>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:6px;">
<?php foreach ($opts as $optKey => $optLabel): ?>
<label class="form-check">
<input type="checkbox" name="<?= $qi['question_key'] ?>[]" value="<?= htmlspecialchars($optKey) ?>" <?= in_array($optKey, explode(',', $val)) ? 'checked' : '' ?>>
<?= htmlspecialchars($optLabel) ?>
</label>
<?php endforeach; ?>
</div>
<?php else: ?>
<label class="form-check">
<input type="checkbox" name="<?= $qi['question_key'] ?>" <?= $val ? 'checked' : '' ?>> <?= htmlspecialchars($qi['label']) ?>
</label>
<?php endif; ?>
</div>
<?php else: ?>
<div class="form-group">
<label for="<?= $qi['question_key'] ?>"><?= htmlspecialchars($qi['label']) ?></label>
<input type="<?= $qi['type'] === 'number' ? 'number' : 'text' ?>" id="<?= $qi['question_key'] ?>" name="<?= $qi['question_key'] ?>" value="<?= htmlspecialchars($val) ?>" <?= $qi['required'] ? 'required' : '' ?>>
</div>
<?php endif; ?>
<?php if ($qi['help_text']): ?>
<small style="color:var(--text-muted);display:block;margin-top:-4px;margin-bottom:12px;"><?= htmlspecialchars($qi['help_text']) ?></small>
<?php endif; ?>
<?php endforeach; ?>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="card" style="text-align:center;color:var(--text-muted);padding:40px;">
<p>На этом шаге пока нет вопросов. Настройте опросник в <a href="../owner/questionnaire_config.php" style="color:var(--accent);">конфигураторе</a>.</p>
</div>
<?php endif; ?>
<div style="display:flex;gap:10px;justify-content:space-between;">
<div>
<button type="submit" name="save" class="btn btn-secondary">💾 Сохранить</button>
</div>
<div>
<?php if ($step > 1): ?>
<a href="?id=<?= $sessId ?>&step=<?= $step - 1 ?>" class="btn btn-secondary">← Назад</a>
<?php endif; ?>
<?php if ($step < 5): ?>
<button type="submit" name="next" class="btn btn-primary">Далее →</button>
<?php else: ?>
<button type="submit" name="next" class="btn btn-success" onclick="return confirm('Завершить расчёт SLA?')">✅ Рассчитать SLA</button>
<?php endif; ?>
</div>
</div>
</form>
<?php endif; ?>
<!-- Sessions list -->
<div class="card">
<div class="card-header"><h2>История опросников</h2></div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>ID</th>
<th>Автор</th>
<th>Статус</th>
<th>Risk Score</th>
<th>Object Index</th>
<th>SLA / мес</th>
<th>Дата</th>
<th class="actions"></th>
</tr>
</thead>
<tbody>
<?php foreach ($sessions as $s): ?>
<tr>
<td>#<?= $s['id'] ?></td>
<td><?= htmlspecialchars($s['author_name'] ?: '—') ?></td>
<td><span class="badge badge-<?= $s['status'] === 'completed' ? 'done' : ($s['status'] === 'draft' ? 'draft' : 'cancelled') ?>"><?= $s['status'] ?></span></td>
<td><?= $s['risk_score'] ?: '—' ?></td>
<td><?= $s['object_index'] ?: '—' ?></td>
<td><?= $s['sla_price_monthly'] ? fmt_money($s['sla_price_monthly']) : '—' ?></td>
<td><?= date('d.m.Y', strtotime($s['created_at'])) ?></td>
<td class="actions"><a href="?id=<?= $s['id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="Просмотр">👁️</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php require __DIR__ . '/../../inc/footer.php'; ?>
+123
View File
@@ -0,0 +1,123 @@
<?php
require_once __DIR__ . '/../../inc/auth.php';
require_once __DIR__ . '/../../inc/functions.php';
$session = auth_require('technician');
$pdo = getDB();
$message = '';
$userId = $session['user_id'];
// Get my open tasks
$stmt = $pdo->prepare("
SELECT t.*, o.name as object_name, o.address
FROM tasks t
JOIN objects o ON o.id = t.object_id
WHERE t.assigned_to = ? AND t.status IN ('open','in_progress')
ORDER BY FIELD(t.priority,'P1','P2','P3','P4'), t.deadline ASC
");
$stmt->execute([$userId]);
$myTasks = $stmt->fetchAll();
// Close task with checklist data
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['close_task'])) {
$taskId = (int)$_POST['task_id'];
$csrf = $_POST['csrf_token'] ?? '';
if (!auth_csrf_verify($csrf)) {
$message = 'Ошибка безопасности.';
} else {
$checklist = [
'arrival_fixed' => isset($_POST['arrival_fixed']) ? 1 : 0,
'photo_before' => isset($_POST['photo_before']) ? 1 : 0,
'power_check' => isset($_POST['power_check']) ? 1 : 0,
'cameras_check' => isset($_POST['cameras_check']) ? 1 : 0,
'acs_check' => isset($_POST['acs_check']) ? 1 : 0,
'fire_check' => isset($_POST['fire_check']) ? 1 : 0,
'fault_found' => isset($_POST['fault_found']) ? 1 : 0,
'fault_fixed' => isset($_POST['fault_fixed']) ? 1 : 0,
'system_test' => isset($_POST['system_test']) ? 1 : 0,
'photo_after' => isset($_POST['photo_after']) ? 1 : 0,
];
$notes = trim($_POST['result_notes'] ?? '');
$pdo->prepare("UPDATE tasks SET status='completed', closed_at=NOW(), checklist_data=?, result_notes=? WHERE id=? AND assigned_to=?")
->execute([json_encode($checklist), $notes, $taskId, $userId]);
$message = 'Задача завершена.';
auth_log($userId, 'task_complete', "Task: $taskId");
// Reload
$stmt->execute([$userId]);
$myTasks = $stmt->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>
</div>
<?php if ($message): ?>
<div class="alert alert-success"><?= htmlspecialchars($message) ?></div>
<?php endif; ?>
<?php if (empty($myTasks)): ?>
<div class="card">
<div class="empty-state">
<h3>Нет активных задач</h3>
<p>Вам пока не назначены задачи. Ожидайте назначения от инженера.</p>
</div>
</div>
<?php else: ?>
<?php foreach ($myTasks as $task): ?>
<div class="card">
<div class="card-header">
<h2><?= htmlspecialchars($task['title']) ?> <span class="badge badge-<?= strtolower($task['priority']) ?>"><?= $task['priority'] ?></span></h2>
<span style="color:var(--text-muted);">
<?php if ($task['deadline']): ?>
Дедлайн: <?= date('d.m.Y H:i', strtotime($task['deadline'])) ?>
<?php endif; ?>
</span>
</div>
<p style="margin-bottom:12px;color:var(--text-secondary);">
<strong>Объект:</strong> <?= htmlspecialchars($task['object_name']) ?>
<?php if ($task['address']): ?> · <?= htmlspecialchars($task['address']) ?><?php endif; ?>
</p>
<?php if ($task['description']): ?>
<p style="margin-bottom:16px;"><?= nl2br(htmlspecialchars($task['description'])) ?></p>
<?php endif; ?>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<input type="hidden" name="task_id" value="<?= $task['id'] ?>">
<input type="hidden" name="close_task" value="1">
<h3 style="font-size:14px;font-weight:600;margin-bottom:12px;">Чек-лист выполнения</h3>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px;margin-bottom:16px;">
<label class="form-check"><input type="checkbox" name="arrival_fixed"> Прибытие зафиксировано</label>
<label class="form-check"><input type="checkbox" name="photo_before"> Фото «до»</label>
<label class="form-check"><input type="checkbox" name="power_check"> Проверка питания</label>
<label class="form-check"><input type="checkbox" name="cameras_check"> Проверка камер / датчиков</label>
<label class="form-check"><input type="checkbox" name="acs_check"> Проверка СКУД</label>
<label class="form-check"><input type="checkbox" name="fire_check"> Проверка пожарной панели</label>
<label class="form-check"><input type="checkbox" name="fault_found"> Неисправность локализована</label>
<label class="form-check"><input type="checkbox" name="fault_fixed"> Неисправность устранена</label>
<label class="form-check"><input type="checkbox" name="system_test"> Тестирование системы</label>
<label class="form-check"><input type="checkbox" name="photo_after"> Фото «после»</label>
</div>
<div class="form-group">
<label for="notes">Примечания / Результат</label>
<textarea id="notes" name="result_notes" rows="3" placeholder="Что сделано, что выявлено..."></textarea>
</div>
<button type="submit" class="btn btn-success" onclick="return confirm('Завершить задачу?')">✓ Завершить задачу</button>
</form>
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php require __DIR__ . '/../../inc/footer.php'; ?>