Initial commit
This commit is contained in:
@@ -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>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
@@ -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'; ?>
|
||||
Reference in New Issue
Block a user