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
+12
View File
@@ -0,0 +1,12 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../inc/auth.php';
$session = auth_require('owner', 'engineer');
$pdo = getDB();
$stmt = $pdo->query("SELECT * FROM shs_records ORDER BY id DESC LIMIT 1");
$shs = $stmt->fetch();
echo json_encode($shs ?: ['error' => 'Нет данных']);
+23
View File
@@ -0,0 +1,23 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../inc/auth.php';
$session = auth_require('engineer', 'technician');
$pdo = getDB();
$userId = $session['user_id'];
$role = $session['user_role'];
$sql = "SELECT t.*, o.name as object_name, o.address FROM tasks t JOIN objects o ON o.id=t.object_id WHERE t.status IN ('open','in_progress')";
$params = [];
if ($role === 'technician') {
$sql .= " AND t.assigned_to = ?";
$params[] = $userId;
}
$sql .= " ORDER BY FIELD(t.priority,'P1','P2','P3','P4'), t.deadline ASC";
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
echo json_encode($stmt->fetchAll());
+64
View File
@@ -0,0 +1,64 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../inc/auth.php';
auth_session_start();
if (empty($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['error' => 'Не авторизован']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($_FILES['photo'])) {
http_response_code(400);
echo json_encode(['error' => 'Нет файла']);
exit;
}
$taskId = (int)($_POST['task_id'] ?? 0);
$reportId = (int)($_POST['report_id'] ?? 0);
$type = $_POST['type'] ?? 'other';
$desc = trim($_POST['description'] ?? '');
$uploadDir = __DIR__ . '/../uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$file = $_FILES['photo'];
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$allowed = ['jpg','jpeg','png','gif','webp','heic'];
if (!in_array($ext, $allowed)) {
http_response_code(400);
echo json_encode(['error' => 'Недопустимый формат. Разрешены: ' . implode(', ', $allowed)]);
exit;
}
if ($file['size'] > 20 * 1024 * 1024) {
http_response_code(400);
echo json_encode(['error' => 'Файл больше 20MB']);
exit;
}
$fileName = uniqid('photo_') . '.' . $ext;
$destPath = $uploadDir . $fileName;
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
http_response_code(500);
echo json_encode(['error' => 'Ошибка сохранения файла']);
exit;
}
$dbPath = '/service/uploads/' . $fileName;
try {
$pdo = getDB();
$stmt = $pdo->prepare("INSERT INTO task_photos (task_id, report_id, file_path, description, type, uploaded_by) VALUES (?,?,?,?,?,?)");
$stmt->execute([$taskId ?: null, $reportId ?: null, $dbPath, $desc, $type, $_SESSION['user_id']]);
echo json_encode(['success' => true, 'path' => $dbPath, 'id' => $pdo->lastInsertId()]);
} catch (\Throwable $e) {
http_response_code(500);
echo json_encode(['error' => 'Ошибка БД: ' . $e->getMessage()]);
}
+896
View File
@@ -0,0 +1,896 @@
@charset "UTF-8";
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Montserrat:wght@600;700;800&display=swap');
:root {
--bg-body: #0a0e1a;
--bg-card: #131a2e;
--bg-card-hover: #1a2340;
--bg-sidebar: #0d1322;
--bg-input: #1a2340;
--border: #1e2a4a;
--border-focus: #00ADEF;
--text-primary: #e8edf5;
--text-secondary: #8899bb;
--text-muted: #556688;
--accent: #00ADEF;
--accent-hover: #33c4f5;
--accent-dim: rgba(0, 173, 239, 0.15);
--success: #22c55e;
--warning: #f59e0b;
--danger: #ef4444;
--info: #6366f1;
--font: 'Inter', 'Segoe UI', sans-serif;
--radius: 10px;
--shadow: 0 2px 12px rgba(0,0,0,0.3);
}
/* ---------- SCROLLBARS ---------- */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: rgba(255,255,255,0.03); border-radius: 3px; }
::-webkit-scrollbar-thumb { background: rgba(0,173,239,0.3); border-radius: 3px; transition: background 0.2s; }
::-webkit-scrollbar-thumb:hover { background: rgba(0,173,239,0.5); }
* { scrollbar-width: thin; scrollbar-color: rgba(0,173,239,0.3) rgba(255,255,255,0.03); }
* { margin:0; padding:0; box-sizing:border-box; }
html, body {
font-family: var(--font);
background: var(--bg-body);
color: var(--text-primary);
font-size: 14px;
line-height: 1.5;
min-height: 100vh;
}
a { color: var(--accent); text-decoration:none; }
a:hover { color: var(--accent-hover); }
img { max-width:100%; height:auto; }
/* ---------- LOGIN PAGE ---------- */
.login-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: linear-gradient(135deg, #0a0e1a 0%, #0d1a30 50%, #0a0e1a 100%);
}
.login-box {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 36px 32px;
width: 100%;
max-width: 400px;
box-shadow: var(--shadow);
}
.login-brand {
text-align: center;
margin-bottom: 24px;
}
.login-brand__img {
display: block;
height: 56px;
margin: 0 auto 16px;
}
.login-brand__title {
font-family: 'Montserrat', sans-serif;
font-weight: 800;
font-size: 24px;
color: var(--text-primary);
letter-spacing: -0.02em;
line-height: 1.1;
}
.login-brand__subtitle {
font-family: 'Inter', sans-serif;
font-weight: 500;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.2em;
color: var(--accent);
margin-top: 4px;
}
.login-divider {
border: none;
border-top: 1px solid var(--border);
margin: 20px 0;
}
.login-box h1 {
font-size: 20px;
font-weight: 700;
margin-bottom: 24px;
color: var(--text-primary);
text-align: center;
}
.login-box__footnote {
margin-top: 24px;
font-size: 11px;
color: var(--text-muted);
text-align: center;
line-height: 1.5;
}
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px 14px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-family: var(--font);
font-size: 14px;
transition: border 0.2s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--border-focus);
box-shadow: 0 0 0 3px var(--accent-dim);
}
.form-group textarea {
min-height: 80px;
resize: vertical;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 20px;
border: none;
border-radius: 6px;
font-family: var(--font);
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.btn-primary {
background: var(--accent);
color: #fff;
}
.btn-primary:hover {
background: var(--accent-hover);
}
.btn-secondary {
background: var(--bg-input);
color: var(--text-primary);
border: 1px solid var(--border);
}
.btn-secondary:hover {
border-color: var(--accent);
}
.btn-success {
background: var(--success);
color: #fff;
}
.btn-danger {
background: var(--danger);
color: #fff;
}
.btn-warning {
background: var(--warning);
color: #fff;
}
.btn-sm {
padding: 5px 12px;
font-size: 12px;
}
.btn-block {
width: 100%;
justify-content: center;
}
.alert {
padding: 12px 16px;
border-radius: 6px;
font-size: 13px;
margin-bottom: 16px;
}
.alert-error {
background: rgba(239, 68, 68, 0.15);
color: var(--danger);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.alert-success {
background: rgba(34, 197, 94, 0.15);
color: var(--success);
border: 1px solid rgba(34, 197, 94, 0.3);
}
.alert-warning {
background: rgba(245, 158, 11, 0.15);
color: var(--warning);
border: 1px solid rgba(245, 158, 11, 0.3);
}
.alert-info {
background: rgba(99, 102, 241, 0.15);
color: var(--info);
border: 1px solid rgba(99, 102, 241, 0.3);
}
/* ---------- LAYOUT ---------- */
.layout {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 240px;
background: var(--bg-sidebar);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
position: fixed;
top: 0;
left: 0;
height: 100vh;
z-index: 100;
transition: transform 0.3s;
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid var(--border);
}
.sidebar-header .logo {
font-size: 16px;
font-weight: 700;
color: var(--text-primary);
}
.sidebar-header .logo span {
color: var(--accent);
}
.sidebar-nav {
flex: 1;
padding: 12px 0;
overflow-y: auto;
}
.sidebar-nav a {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 20px;
color: var(--text-secondary);
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.sidebar-nav a:hover,
.sidebar-nav a.active {
color: var(--text-primary);
background: var(--accent-dim);
}
.sidebar-nav a.active {
border-right: 3px solid var(--accent);
}
.sidebar-section {
padding: 8px 20px 4px;
font-size: 10px;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1px;
}
.sidebar-footer {
padding: 16px 20px;
border-top: 1px solid var(--border);
font-size: 12px;
}
.sidebar-footer .user-name {
color: var(--text-primary);
font-weight: 600;
}
.sidebar-footer .user-role {
color: var(--text-muted);
font-size: 11px;
}
.sidebar-toggle {
display: none;
position: fixed;
top: 12px;
left: 12px;
z-index: 101;
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-primary);
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 18px;
}
.main {
flex: 1;
margin-left: 240px;
padding: 24px;
min-height: 100vh;
}
.main-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.main-header h1 {
font-size: 24px;
font-weight: 700;
}
.main-header .breadcrumb {
font-size: 12px;
color: var(--text-muted);
}
.main-header .breadcrumb a {
color: var(--text-secondary);
}
.main-header .breadcrumb a:hover {
color: var(--accent);
}
/* ---------- CARDS ---------- */
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
margin-bottom: 16px;
transition: box-shadow 0.2s;
}
.card:hover {
box-shadow: var(--shadow);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.card-header h2,
.card-header h3 {
font-size: 15px;
font-weight: 600;
}
.card-value {
font-size: 28px;
font-weight: 700;
}
.card-label {
font-size: 12px;
color: var(--text-secondary);
margin-top: 2px;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
/* ---------- KPI VALUES ---------- */
.value-green { color: var(--success); }
.value-yellow { color: var(--warning); }
.value-red { color: var(--danger); }
.value-accent { color: var(--accent); }
.value-info { color: var(--info); }
/* ---------- TABLES ---------- */
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
table th {
text-align: left;
padding: 10px 12px;
font-size: 11px;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
table td {
padding: 10px 12px;
font-size: 13px;
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
table tr:hover td {
background: rgba(0, 173, 239, 0.03);
}
table .actions {
white-space: nowrap;
text-align: right;
}
table .actions a,
table .actions button {
margin-left: 6px;
}
/* ---------- BADGES ---------- */
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.badge-critical { background: rgba(239,68,68,0.2); color: var(--danger); }
.badge-high { background: rgba(245,158,11,0.2); color: var(--warning); }
.badge-medium { background: rgba(99,102,241,0.2); color: var(--info); }
.badge-low { background: rgba(136,153,187,0.2); color: var(--text-secondary); }
.badge-open { background: rgba(99,102,241,0.2); color: var(--info); }
.badge-progress { background: rgba(245,158,11,0.2); color: var(--warning); }
.badge-done { background: rgba(34,197,94,0.2); color: var(--success); }
.badge-cancelled { background: rgba(239,68,68,0.2); color: var(--danger); }
.badge-active { background: rgba(34,197,94,0.2); color: var(--success); }
.badge-inactive { background: rgba(136,153,187,0.2); color: var(--text-secondary); }
.badge-draft { background: rgba(136,153,187,0.2); color: var(--text-secondary); }
.badge-risk { background: rgba(245,158,11,0.2); color: var(--warning); }
.badge-crisis { background: rgba(239,68,68,0.2); color: var(--danger); }
.badge-stable { background: rgba(99,102,241,0.2); color: var(--info); }
.badge-growth { background: rgba(34,197,94,0.2); color: var(--success); }
.badge-default { background: rgba(136,153,187,0.2); color: var(--text-secondary); }
/* ---------- MODAL ---------- */
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6);
display: none;
align-items: center;
justify-content: center;
z-index: 200;
}
.modal-overlay.open {
display: flex;
}
.modal {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 28px;
width: 100%;
max-width: 540px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 8px 40px rgba(0,0,0,0.5);
}
.modal h2 {
font-size: 18px;
font-weight: 700;
margin-bottom: 20px;
}
.modal-close {
float: right;
background: none;
border: none;
color: var(--text-muted);
font-size: 20px;
cursor: pointer;
}
/* ---------- TABS ---------- */
.tabs {
display: flex;
border-bottom: 1px solid var(--border);
margin-bottom: 20px;
gap: 0;
}
.tabs a {
padding: 10px 20px;
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 0.2s;
}
.tabs a:hover,
.tabs a.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
/* ---------- PAGINATION ---------- */
.pagination {
display: flex;
gap: 4px;
justify-content: center;
margin-top: 20px;
}
.pagination a,
.pagination span {
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-secondary);
font-size: 13px;
}
.pagination a:hover {
border-color: var(--accent);
color: var(--accent);
}
.pagination .current {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
/* ---------- SEARCH ---------- */
.search-box {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.search-box input {
flex: 1;
padding: 8px 14px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-family: var(--font);
font-size: 13px;
}
.search-box input:focus {
outline: none;
border-color: var(--border-focus);
}
/* ---------- PROGRESS ---------- */
.progress-bar {
height: 6px;
background: rgba(255,255,255,0.08);
border-radius: 3px;
overflow: hidden;
margin-top: 8px;
}
.progress-fill {
height: 100%;
border-radius: 3px;
transition: width 0.4s;
}
/* ---------- FORM INLINE ---------- */
.form-row {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 14px;
}
.form-check {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.form-check input[type="checkbox"],
.form-check input[type="radio"] {
width: auto;
}
/* ---------- FILTER BAR ---------- */
.filter-bar {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 16px;
align-items: center;
}
.filter-bar select,
.filter-bar input {
padding: 6px 10px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-family: var(--font);
font-size: 12px;
}
/* ---------- QUESTIONNAIRE ---------- */
.questionnaire-step {
display: none;
}
.questionnaire-step.active {
display: block;
}
.step-indicators {
display: flex;
gap: 0;
margin-bottom: 28px;
overflow-x: auto;
}
.step-dot {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
border-bottom: 2px solid var(--border);
white-space: nowrap;
cursor: default;
}
.step-dot.completed {
color: var(--success);
border-bottom-color: var(--success);
}
.step-dot.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
/* ---------- SHS ALERT ---------- */
.shs-alert {
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
font-weight: 600;
}
.shs-alert.warning {
background: rgba(245,158,11,0.15);
color: var(--warning);
border: 1px solid rgba(245,158,11,0.3);
}
.shs-alert.critical {
background: rgba(239,68,68,0.15);
color: var(--danger);
border: 1px solid rgba(239,68,68,0.3);
}
/* ---------- RESPONSIVE ---------- */
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
}
.sidebar-toggle {
display: block;
}
.main {
margin-left: 0;
padding: 16px;
padding-top: 56px;
}
.card-grid {
grid-template-columns: 1fr;
}
.form-row {
grid-template-columns: 1fr;
}
.login-box {
margin: 16px;
padding: 24px;
}
.tabs {
overflow-x: auto;
}
.step-indicators {
flex-wrap: nowrap;
}
}
/* ---------- PRINT ---------- */
@media print {
.sidebar, .sidebar-toggle, .main-header .actions { display: none; }
.main { margin-left: 0; }
.card { break-inside: avoid; }
}
/* ---------- TOAST ---------- */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 999;
}
.toast {
padding: 12px 20px;
border-radius: 6px;
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
animation: slideIn 0.3s ease;
}
.toast-success { background: var(--success); color: #fff; }
.toast-error { background: var(--danger); color: #fff; }
.toast-warning { background: var(--warning); color: #fff; }
.toast-info { background: var(--info); color: #fff; }
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* ---------- TOOLTIPS ---------- */
[data-tooltip] {
position: relative;
}
[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(4px);
background: #1a1a2e;
color: #e8edf5;
padding: 6px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 500;
white-space: nowrap;
pointer-events: none;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
[data-tooltip]::before {
content: '';
position: absolute;
bottom: calc(100% + 2px);
left: 50%;
transform: translateX(-50%) translateY(4px);
border: 5px solid transparent;
border-top-color: #1a1a2e;
pointer-events: none;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
z-index: 1000;
}
[data-tooltip]:hover::after,
[data-tooltip]:hover::before {
opacity: 1;
visibility: visible;
transform: translateX(-50%) translateY(0);
}
/* ---------- EMPTY STATE ---------- */
.empty-state {
text-align: center;
padding: 48px 20px;
color: var(--text-muted);
}
.empty-state svg {
width: 60px;
height: 60px;
margin-bottom: 16px;
opacity: 0.4;
}
.empty-state h3 {
font-size: 16px;
margin-bottom: 6px;
}
.empty-state p {
font-size: 13px;
}
/* ---------- LOADING ---------- */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
+106
View File
@@ -0,0 +1,106 @@
<?php
require_once __DIR__ . '/inc/auth.php';
$session = auth_require();
$role = $session['user_role'];
$pdo = getDB();
// Common stats
$stats = [];
// Total objects
$stmt = $pdo->query("SELECT COUNT(*) FROM objects WHERE status='active'");
$stats['objects'] = $stmt->fetchColumn();
// Active tasks
$stmt = $pdo->query("SELECT COUNT(*) FROM tasks WHERE status IN ('open','in_progress')");
$stats['tasks'] = $stmt->fetchColumn();
// Open incidents
$stmt = $pdo->query("SELECT COUNT(*) FROM incidents WHERE status IN ('open','in_progress')");
$stats['incidents'] = $stmt->fetchColumn();
// Active SLA contracts
$stmt = $pdo->query("SELECT COUNT(*) FROM sla_contracts WHERE status='active'");
$stats['sla_count'] = $stmt->fetchColumn();
// Latest SHS
$stmt = $pdo->query("SELECT shs_score, shs_status FROM shs_records ORDER BY id DESC LIMIT 1");
$stats['shs'] = $stmt->fetch();
// Engineer-specific: tasks assigned to me
if ($role === 'engineer' || $role === 'technician') {
$stmt = $pdo->prepare("SELECT COUNT(*) FROM tasks WHERE assigned_to = ? AND status IN ('open','in_progress')");
$stmt->execute([$session['user_id']]);
$stats['my_tasks'] = $stmt->fetchColumn();
}
// SLA compliance (overall)
$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();
$stats['sla_compliance'] = $slaData['total'] > 0 ? round(($slaData['compliant'] / $slaData['total']) * 100, 1) : 0;
$shsAlert = null;
if ($stats['shs'] && $stats['shs']['shs_score'] < 70) {
$shsAlert = 'SHS ниже 70 (' . $stats['shs']['shs_score'] . ') — ' . $stats['shs']['shs_status'];
}
require __DIR__ . '/inc/header.php';
?>
<div class="main-header">
<div>
<h1>Дашборд</h1>
<div class="breadcrumb"><a href="/service/dashboard.php">Главная</a></div>
</div>
<div class="actions">
<span style="font-size:12px;color:var(--text-muted);"><?= date('d.m.Y H:i') ?></span>
</div>
</div>
<?php if ($shsAlert): ?>
<div class="shs-alert critical">⚠ <?= htmlspecialchars($shsAlert) ?></div>
<?php endif; ?>
<div class="card-grid">
<a href="/service/pages/owner/objects.php" class="card card-link">
<div class="card-label">Активные объекты</div>
<div class="card-value value-accent"><?= $stats['objects'] ?></div>
</a>
<a href="/service/pages/engineer/tasks.php" class="card card-link">
<div class="card-label">Активные задачи</div>
<div class="card-value <?= $stats['tasks'] > 0 ? 'value-yellow' : 'value-green' ?>"><?= $stats['tasks'] ?></div>
</a>
<a href="/service/pages/engineer/incidents.php" class="card card-link">
<div class="card-label">Открытые инциденты</div>
<div class="card-value <?= $stats['incidents'] > 0 ? 'value-red' : 'value-green' ?>"><?= $stats['incidents'] ?></div>
</a>
<a href="/service/pages/owner/sla.php" class="card card-link">
<div class="card-label">SLA контракты</div>
<div class="card-value value-info"><?= $stats['sla_count'] ?></div>
</a>
<a href="/service/pages/owner/sla.php" class="card card-link">
<div class="card-label">SLA Compliance</div>
<div class="card-value <?= $stats['sla_compliance'] >= 90 ? 'value-green' : ($stats['sla_compliance'] >= 70 ? 'value-yellow' : 'value-red') ?>"><?= $stats['sla_compliance'] ?>%</div>
</a>
<?php if ($stats['shs']): ?>
<a href="/service/pages/owner/ceo.php" class="card card-link">
<div class="card-label">SHS (System Health)</div>
<div class="card-value <?= $stats['shs']['shs_status'] === 'Growth' || $stats['shs']['shs_status'] === 'Stable' ? 'value-green' : 'value-red' ?>"><?= $stats['shs']['shs_score'] ?></div>
<div class="card-label"><span class="badge badge-<?= strtolower($stats['shs']['shs_status']) ?>"><?= $stats['shs']['shs_status'] ?></span></div>
</a>
<?php endif; ?>
<?php if (isset($stats['my_tasks'])): ?>
<a href="/service/pages/engineer/tasks.php" class="card card-link">
<div class="card-label">Мои задачи</div>
<div class="card-value"><?= $stats['my_tasks'] ?></div>
</a>
<?php endif; ?>
</div>
<?php
require __DIR__ . '/inc/footer.php';
?>
<style>
.card-link { text-decoration:none; color:inherit; display:block; transition:transform 0.2s, box-shadow 0.2s; }
.card-link:hover { transform:translateY(-2px); box-shadow:var(--shadow); cursor:pointer; }
</style>
@@ -0,0 +1,836 @@
# AegisOne Engineering — План миграции инфраструктуры и переписывания сервисной части
> **Статус:** План утверждён, готов к реализации
> **Дата:** 2026-05-17
> **Команда:** Владелец + AI-ассистент
> **Примечание:** Если при реализации возникнут вопросы — лучше уточнить, чем переделывать потом.
---
## 1. Текущее состояние
### 1.1. Публичная часть (aegisone.ru)
- **Технологии:** PHP 7.4, MySQL, inline CSS/JS
- **Хостинг:** Shared-хостинг (проблемы: HTTP 302-редирект, блокировка `/assets/`, отключённый `RewriteEngine`, PHP 7.4 тупик)
- **Страницы:** Главная, услуги, блог, контакты, форма обратной связи
- **SEO:** `robots.txt`, `sitemap.xml`, канонические URL
- **Решение:** **Не трогать.** Перенести на VPS как есть в Docker-контейнер.
### 1.2. Сервисная часть (service.aegisone.ru)
- **Технологии:** PHP 7.4, MySQL, inline CSS/JS
- **Функционал:** Админ-панель (управление пользователями, объектами, SLA, опросниками, блогом, документами), расчёты (Risk Score, Object Index, SLA price), паспорта объектов
- **Проблемы:** PHP 7.4 ограничивает развитие, нет автоматизации отчётов, нет интеграций
- **Решение:** **Полностью переписать на Python/FastAPI + Jinja2**
### 1.3. Инфраструктура
- **VPS:** Уже есть, на нём один проект (FastAPI + Uvicorn + Celery + Nginx + PostgreSQL + Redis + Alembic)
- **Хостинг:** Платный, планируется отказ
- **Git:** Нет централизованного сервера, работа на локальной машине
- **SSL:** От регистратора домена (статический, неудобно для мультидомена)
- **Домены:** `aegisone.ru` (публичная часть), планируется `service.aegisone.ru` (админка)
---
## 2. Целевая архитектура
```
┌─────────────────────────────────────────────────────────────────────┐
│ VPS │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Nginx (reverse proxy + SSL) │ │
│ │ порт 80/443, Let's Encrypt │ │
│ │ │ │
│ │ aegisone.ru ──────────────► php-fpm container (порт 9000) │ │
│ │ service.aegisone.ru ─────► fastapi container (порт 8000) │ │
│ │ other-project.ru ────────► project2 container (порт 8001) │ │
│ │ git.aegisone.ru ─────────► gitea container (порт 3000) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ AegisOne PHP │ │ AegisOne Python │ │ Other Project │ │
│ │ (публичная) │ │ (админка) │ │ (FastAPI) │ │
│ │ │ │ │ │ │ │
│ │ • Главная │ │ • Аутентификация│ │ • ... │ │
│ │ • Услуги │ │ • Пользователи │ │ │ │
│ │ • Блог │ │ • Объекты │ │ │ │
│ │ • Контакты │ │ • Клиенты │ │ │ │
│ │ • FAQ │ │ • SLA │ │ │ │
│ │ • Карусель │ │ • Опросники │ │ │ │
│ │ • Форма │ │ • Расчёты │ │ │ │
│ │ │ │ • Отчёты (PDF) │ │ │ │
│ │ │ │ • Документы │ │ │ │
│ │ │ │ • Yandex Disk │ │ │ │
│ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └──────────┬──────────┘ │ │
│ │ │ │
│ ┌──────────▼──────────┐ ┌─────────▼────────┐ │
│ │ PostgreSQL │ │ PostgreSQL │ │
│ │ (AegisOne БД) │ │ (Project 2 БД) │ │
│ │ │ │ │ │
│ │ • users │ │ • ... │ │
│ │ • customers │ │ │ │
│ │ • objects │ │ │ │
│ │ • sla_contracts │ │ │ │
│ │ • questionnaire_* │ │ │ │
│ │ • blog_posts │ │ │ │
│ │ • cases │ │ │ │
│ │ • documents │ │ │ │
│ │ • photos │ │ │ │
│ │ • audit_log │ │ │ │
│ └─────────────────────┘ └──────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ Redis │ (кэш, Celery broker) │
│ └─────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Celery Workers (Python) │ │
│ │ │ │
│ │ • Генерация отчётов (PDF, Excel) │ │
│ │ • Загрузка фото на Yandex Disk │ │
│ │ • Интеграция с 1С (по мере необходимости) │ │
│ │ • Фоновые расчёты, уведомления │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Gitea │ │ Certbot │ │
│ │ (Git-сервер) │ │ (Let's Encrypt) │ │
│ │ │ │ │ │
│ │ • aegisone-php │ │ • aegisone.ru │ │
│ │ • aegisone-py │ │ • service.aeg.. │ │
│ │ • project2 │ │ • other.ru │ │
│ │ • CI/CD Actions │ │ • автообновление│ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Yandex Disk API (облачное хранилище) │ │
│ │ │ │
│ │ /AegisOne/ │ │
│ │ ├── objects/{object_id}/ │ │
│ │ │ ├── photos/{photo_id}.jpg │ │
│ │ │ └── documents/{doc_id}/ │ │
│ │ │ ├── {photo_id}.jpg │ │
│ │ │ └── report_{doc_id}.pdf │ │
│ │ └── ... │ │
│ │ │ │
│ │ В PostgreSQL хранятся: file_id, file_url, привязка к объекту│ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 3. Технологический стек
### 3.1. Публичная часть (без изменений)
| Компонент | Технология | Примечание |
|---|---|---|
| Язык | PHP 7.4 | Не обновлять, не трогать |
| БД | MySQL | Мигрировать на PostgreSQL позже (опционально) |
| Веб-сервер | Nginx + PHP-FPM | В Docker-контейнере |
| CSS/JS | Inline | Без изменений |
### 3.2. Сервисная часть (Python)
| Компонент | Технология | Примечание |
|---|---|---|
| Язык | Python 3.11+ | |
| Фреймворк | FastAPI | REST API + серверный рендеринг |
| Шаблоны | Jinja2 | Серверный рендеринг, как сейчас PHP |
| Веб-сервер | Uvicorn | ASGI-сервер |
| ORM | SQLAlchemy 2.0 + Alembic | Миграции БД |
| БД | PostgreSQL 15+ | Единая БД для всех проектов |
| Аутентификация | JWT (python-jose) | Токены для API и сессий |
| Фоновые задачи | Celery + Redis | Отчёты, загрузка файлов, интеграции |
| Генерация отчётов | WeasyPrint (PDF), openpyxl (Excel) | |
| Yandex Disk | yandex-disk SDK / REST API | OAuth 2.0 |
| Валидация | Pydantic | Схемы данных |
### 3.3. Инфраструктура
| Компонент | Технология | Примечание |
|---|---|---|
| Контейнеризация | Docker + Docker Compose | |
| Reverse proxy | Nginx | Маршрутизация по доменам |
| SSL | Let's Encrypt + Certbot | Автообновление |
| Git-сервер | Gitea | Лёгкий, встроенный CI/CD |
| CI/CD | Gitea Actions | Автоматический деплой |
| Мониторинг | (опционально) Uptime Kuma | |
### 3.4. Мобильные приложения (будущее)
| Компонент | Технология | Примечание |
|---|---|---|
| Фреймворк | Flutter | |
| API | FastAPI REST endpoints | Те же, что для веб-админки |
| Аутентификация | JWT | |
---
## 4. Почему Jinja2, а не SPA
### 4.1. Сравнение
| Критерий | Jinja2 (SSR) | SPA (React/Vue) |
|---|---|---|
| Серверная нагрузка | Рендерит HTML на сервере | Отдаёт статику + JSON API |
| Масштабирование | Горизонтальное: Nginx → N FastAPI инстансов | CDN для статики + Nginx → API |
| Сложность разработки | Один стек, один деплой | Два стека, два деплоя, CORS |
| Скорость разработки | Быстро | Медленнее |
| Команда | 1-2 Python-разработчика | Python + JS/TS разработчики |
| SEO | Не нужен (админка закрыта) | Не нужен |
| Мобильные приложения | API тот же — Flutter подключается | API тот же — Flutter подключается |
| Поддержка | Проще | Сложнее (два кодовых базы) |
### 4.2. Масштабирование Jinja2 по России
При росте нагрузки:
1. **Вертикальное:** Увеличить ресурсы VPS (CPU, RAM)
2. **Горизонтальное:** Несколько инстансов FastAPI за Nginx (load balancing)
3. **Кэширование:** Redis для кэширования тяжёлых запросов
4. **CDN:** Для статики (CSS, JS, изображения) — Cloudflare или аналог
Jinja2 **не является узким местом**. Узким местом будет БД или внешние API (Yandex Disk, 1С).
### 4.3. Вердикт
**Jinja2 — правильный выбор.** Для B2B-админки SPA — это оверинжиниринг. Jinja2 даёт:
- Быструю разработку
- Простую поддержку
- Легкое масштабирование
- Единую кодовую базу
- Готовность к мобильным приложениям (тот же API)
---
## 5. Этапы реализации
### Этап 1: Docker-фундамент на VPS (1-2 недели)
**Цель:** Подготовить VPS для мультипроектной работы с Docker.
#### 1.1. Установка Docker и Docker Compose
```bash
# На VPS (Ubuntu/Debian)
apt update && apt upgrade -y
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
```
#### 1.2. Базовая структура директорий на VPS
```
/opt/
├── nginx-proxy/ # Nginx reverse proxy + Certbot
│ ├── docker-compose.yml
│ ├── nginx.conf
│ └── certs/ # Let's Encrypt сертификаты
├── gitea/ # Git-сервер
│ ├── docker-compose.yml
│ └── data/ # Данные Gitea
├── aegisone-php/ # Публичная часть (PHP)
│ ├── docker-compose.yml
│ └── src/ # Исходный код (git clone)
├── aegisone-py/ # Сервисная часть (Python)
│ ├── docker-compose.yml
│ ├── app/ # FastAPI приложение
│ ├── migrations/ # Alembic миграции
│ └── requirements.txt
└── project2/ # Существующий проект
└── docker-compose.yml
```
#### 1.3. Nginx reverse proxy + Certbot
- Один Nginx-контейнер на портах 80/443
- Маршрутизация по `server_name`:
- `aegisone.ru``aegisone-php:80`
- `service.aegisone.ru``aegisone-py:8000`
- `git.aegisone.ru``gitea:3000`
- `other-project.ru``project2:8000`
- Certbot в отдельном контейнере или встроенный (nginx-proxy + acme-companion)
#### 1.4. Gitea (Git-сервер)
- Docker-образ `gitea/gitea:latest`
- Порт 3000 → `git.aegisone.ru`
- PostgreSQL для данных Gitea
- Встроенный CI/CD (Gitea Actions)
- Репозитории:
- `aegisone-php` (публичная часть, перенос с текущего хостинга)
- `aegisone-py` (новая сервисная часть)
- `project2` (существующий проект)
- `infra` (конфигурации Docker, Nginx, CI/CD)
#### 1.5. Обёртка существующего проекта в Docker
- Создать `docker-compose.yml` для project2
- Протестировать работоспособность
- Настроить домен `other-project.ru`
**Критерий завершения этапа:**
- [ ] Docker + Docker Compose установлены
- [ ] Nginx reverse proxy работает, маршрутизирует по доменам
- [ ] Certbot выдаёт SSL-сертификаты (Let's Encrypt)
- [ ] Gitea запущен на `git.aegisone.ru`
- [ ] Существующий проект работает в Docker
- [ ] Все домены доступны по HTTPS
---
### Этап 2: Перенос AegisOne на VPS (1 неделя)
**Цель:** Перенести публичную часть с хостинга на VPS, отключить хостинг.
#### 2.1. Docker-контейнер для PHP
```yaml
# /opt/aegisone-php/docker-compose.yml
services:
nginx:
image: nginx:alpine
ports:
- "9080:80" # Внутренний порт, внешний через reverse proxy
volumes:
- ./src:/var/www/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- php
php:
image: php:7.4-fpm
volumes:
- ./src:/var/www/html
environment:
- DB_HOST=mysql
- DB_NAME=aegisone
- DB_USER=aegisone
- DB_PASS=<password>
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: <root_password>
MYSQL_DATABASE: aegisone
MYSQL_USER: aegisone
MYSQL_PASSWORD: <password>
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
```
#### 2.2. Перенос файлов
- Скачать все файлы с хостинга (FTP/SFTP)
- Загрузить в `/opt/aegisone-php/src/`
- Настроить `config.php` для нового окружения (DB_HOST=mysql)
#### 2.3. Миграция БД
- Экспорт MySQL с хостинга: `mysqldump -u user -p db_name > backup.sql`
- Импорт в Docker MySQL: `docker exec -i aegisone-php-mysql mysql -u aegisone -p aegisone < backup.sql`
- Проверить работоспособность
#### 2.4. Настройка домена
- DNS: `aegisone.ru` → IP VPS
- Nginx reverse proxy: `server_name aegisone.ru``aegisone-php:9080`
- SSL: Certbot для `aegisone.ru`
#### 2.5. Тестирование и отключение хостинга
- Проверить все страницы: главная, услуги, блог, контакты, форма
- Проверить карусель, FAQ, тему (светлая/тёмная)
- Проверить Яндекс.Карту с переключением темы
- После подтверждения — отключить хостинг
**Критерий завершения этапа:**
- [ ] Публичная часть работает на VPS
- [ ] Все страницы загружаются корректно
- [ ] Форма обратной связи работает
- [ ] Яндекс.Карта переключает тему
- [ ] Домен `aegisone.ru` указывает на VPS
- [ ] Хостинг отключён
---
### Этап 3: Python-админка (FastAPI + Jinja2) (3-5 недель)
**Цель:** Полностью переписать сервисную часть на Python.
#### 3.1. Структура проекта
```
aegisone-py/
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
├── alembic.ini
├── migrations/ # Alembic миграции
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI приложение
│ ├── config.py # Настройки (env vars)
│ ├── database.py # SQLAlchemy engine, session
│ ├── models/ # SQLAlchemy модели
│ │ ├── __init__.py
│ │ ├── user.py
│ │ ├── customer.py
│ │ ├── object.py
│ │ ├── sla.py
│ │ ├── questionnaire.py
│ │ ├── blog.py
│ │ ├── case.py
│ │ ├── document.py
│ │ ├── photo.py
│ │ └── audit_log.py
│ ├── schemas/ # Pydantic схемы
│ │ ├── __init__.py
│ │ ├── user.py
│ │ ├── customer.py
│ │ └── ...
│ ├── api/ # API endpoints
│ │ ├── __init__.py
│ │ ├── auth.py # Логин, JWT
│ │ ├── users.py
│ │ ├── customers.py
│ │ ├── objects.py
│ │ ├── sla.py
│ │ ├── questionnaire.py
│ │ ├── blog.py
│ │ ├── cases.py
│ │ ├── documents.py
│ │ └── reports.py
│ ├── templates/ # Jinja2 шаблоны
│ │ ├── base.html # Базовый шаблон
│ │ ├── login.html
│ │ ├── dashboard.html
│ │ ├── users/
│ │ ├── customers/
│ │ ├── objects/
│ │ ├── sla/
│ │ ├── questionnaire/
│ │ ├── blog/
│ │ ├── documents/
│ │ └── reports/
│ ├── static/ # CSS, JS, изображения
│ │ ├── css/
│ │ │ ├── service.css # Стили админки
│ │ │ └── ...
│ │ ├── js/
│ │ │ ├── main.js
│ │ │ └── ...
│ │ └── img/
│ ├── services/ # Бизнес-логика
│ │ ├── __init__.py
│ │ ├── auth_service.py
│ │ ├── calc_service.py # Расчёты (Risk Score, Object Index, SLA)
│ │ ├── report_service.py # Генерация отчётов
│ │ ├── yandex_disk_service.py # Интеграция с Yandex Disk
│ │ └── ...
│ ├── tasks/ # Celery задачи
│ │ ├── __init__.py
│ │ ├── reports.py
│ │ ├── yandex_disk.py
│ │ └── ...
│ └── utils/ # Утилиты
│ ├── __init__.py
│ ├── password.py
│ └── ...
└── tests/ # Тесты (опционально)
```
#### 3.2. Миграция данных из MySQL в PostgreSQL
- Создать Alembic миграции для всех таблиц
- Написать скрипт миграции данных:
- `users``users`
- `customers``customers`
- `objects``objects`
- `sla_contracts``sla_contracts`
- `questionnaire_sessions`, `questionnaire_answers`, `questionnaire_items` → аналогично
- `blog_posts``blog_posts`
- `cases``cases`
- `audit_log``audit_log`
- Протестировать миграцию на копии БД
#### 3.3. Аутентификация
- JWT-токены (python-jose)
- Логин/пароль (bcrypt)
- Роли: `owner`, `engineer`, `technician`
- Сессии через cookies (для Jinja2) + JWT (для API)
- CSRF-защита для форм
#### 3.4. CRUD-модули (по порядку приоритета)
**3.4.1. Пользователи (`/service/users/`)**
- Список, создание, редактирование, удаление
- Роль `owner` нельзя удалить, нельзя изменить роль
- Блокировка/разблокировка
- Аудит действий
**3.4.2. Клиенты (`/service/customers/`)**
- Список, создание, редактирование, удаление
- ИНН, КПП, юридический адрес, контакты
- Связь с объектами и SLA
**3.4.3. Объекты (`/service/objects/`)**
- Список, создание, редактирование, удаление
- Привязка к клиенту
- Тип, адрес, площадь, сотрудники
- Risk Score, Complexity Index, Object Index (авторасчёт)
- SLA price (авторасчёт)
**3.4.4. SLA контракты (`/service/sla/`)**
- Список, создание, редактирование, удаление
- Привязка к клиенту и объекту
- Тарифы: Базовый, Оптимальный, Максимальный
- Время реакции, периодичность ТО
- Статус: active, expired, cancelled, negotiation
**3.4.5. Опросник (`/service/questionnaire/`)**
- Создание новой сессии
- 5 шагов: Коммерческий, Технический, Эксплуатация, Риски, Расчёт SLA
- Вопросы из БД (`questionnaire_items`)
- Сохранение ответов
- Авторасчёт: Risk Score, Complexity Index, Infrastructure Load, Service History, Object Index, SLA Price
- Формирование паспорта объекта
**3.4.6. Блог (`/service/blog/`)**
- Список, создание, редактирование, удаление
- Категории: audit, sla, incident, supervision, documentation, risk, cases
- HTML-редактор (toolbar)
- Автогенерация slug из заголовка
- Статус: draft, published
**3.4.7. Примеры из практики (`/service/cases/`)**
- Список, создание, редактирование, удаление
- Заголовок, текст, эффект
- Сортировка, активность
**3.4.8. Документы (`/service/documents/`)**
- Список документов (из `service/docs/*.md`)
- Ручная сортировка
- Переименование
- Гранулярные права: view, edit, cancel (по ролям)
- JSON-хранилище прав (`docs_permissions.json`)
#### 3.5. Расчёты (перенос из PHP)
- `calc_risk_score()` — Risk Score
- `calc_complexity()` — Complexity Index
- `calc_infrastructure_load()` — Infrastructure Load
- `calc_service_history()` — Service History
- `calc_object_index()` — Object Index
- `calc_sla_price()` — SLA Price
- `risk_multiplier()` — множитель риска
- `risk_label()` — текстовая метка риска
- `object_class()` — класс объекта
- `fmt_money()` — форматирование денег
Все функции переписать на Python, покрыть тестами.
#### 3.6. Дашборд (`/service/dashboard/`)
- Метрики: количество объектов, активных SLA, открытых задач
- Графики (Chart.js или аналог)
- Быстрые действия
- Последние активности (audit_log)
#### 3.7. Фронтенд (Jinja2)
- Базовый шаблон (`base.html`) с:
- Боковой панелью (sidebar) с навигацией
- Верхней панелью (header) с пользователем и переключателем темы
- Основным контентом
- Темы: system → dark → light (localStorage, как сейчас)
- Кастомные скроллбары
- Тултипы на кнопках
- Модальные окна (создание/редактирование)
- Таблицы с поиском и пагинацией
- Адаптивность (мобильная версия)
**Критерий завершения этапа:**
- [ ] Все CRUD-модули работают
- [ ] Расчёты корректны (сравнить с PHP-версией)
- [ ] Миграция данных завершена
- [ ] Фронтенд полностью функционален
- [ ] Темы переключаются
- [ ] Мобильная версия работает
- [ ] `service.aegisone.ru` доступен по HTTPS
---
### Этап 4: Интеграции (2-3 недели)
**Цель:** Добавить генерацию отчётов, интеграцию с Yandex Disk, фоновые задачи.
#### 4.1. Yandex Disk API
- Регистрация приложения в Yandex OAuth
- Получение OAuth-токена
- SDK: `yandex-disk` или REST API через `requests`
- Структура папок:
```
/AegisOne/
├── objects/{object_id}/
│ ├── photos/
│ │ └── {photo_id}.jpg
│ └── documents/{doc_id}/
│ ├── {photo_id}.jpg
│ └── report_{doc_id}.pdf
└── ...
```
- Эндпоинты:
- `POST /api/documents/{id}/upload-photo` — загрузка фото
- `GET /api/documents/{id}/photos` — список фото
- `DELETE /api/photos/{id}` — удаление фото
- В БД: таблица `photos` (id, document_id, object_id, yandex_file_id, yandex_file_url, uploaded_at, uploaded_by)
#### 4.2. Генерация отчётов
- **PDF:** WeasyPrint (HTML → PDF)
- Шаблон паспорта объекта
- Шаблон отчёта по аудиту
- Шаблон SLA-контракта
- **Excel:** openpyxl
- Экспорт списка объектов
- Экспорт SLA-контрактов
- Экспорт отчётов по KPI
- Фоновая генерация через Celery (для тяжёлых отчётов)
#### 4.3. Celery (фоновые задачи)
- Broker: Redis
- Задачи:
- Генерация PDF-отчётов
- Загрузка фото на Yandex Disk
- Отправка уведомлений (email, Telegram)
- Интеграция с 1С (по мере необходимости)
- Мониторинг: Flower (веб-интерфейс для Celery)
#### 4.4. Интеграция с 1С (опционально, по мере необходимости)
- REST API 1С или OData
- Синхронизация: клиенты, объекты, договоры
- Фоновая задача Celery
**Критерий завершения этапа:**
- [ ] Yandex Disk API интегрирован
- [ ] Фото загружаются и привязываются к документам
- [ ] PDF-отчёты генерируются
- [ ] Excel-экспорт работает
- [ ] Celery запущен, задачи выполняются
- [ ] (Опционально) Интеграция с 1С настроена
---
### Этап 5: CI/CD и автоматизация деплоя (1 неделя)
**Цель:** Настроить автоматический деплой при push в Git.
#### 5.1. Gitea Actions (CI/CD)
- Встроенный CI/CD в Gitea (совместим с GitHub Actions)
- Workflow для `aegisone-py`:
```yaml
name: Deploy AegisOne Python
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push Docker image
run: |
docker build -t registry.git.aegisone.ru/aegisone-py:latest .
docker push registry.git.aegisone.ru/aegisone-py:latest
- name: Deploy to VPS
run: |
ssh user@vps "cd /opt/aegisone-py && docker compose pull && docker compose up -d"
```
#### 5.2. Структура репозиториев в Gitea
```
git.aegisone.ru/
├── aegisone/aegisone-php # Публичная часть (PHP)
├── aegisone/aegisone-py # Сервисная часть (Python)
├── aegisone/infra # Docker, Nginx, CI/CD конфиги
└── aegisone/project2 # Существующий проект
```
#### 5.3. Автоматический деплой
- Push в `main` → Gitea Actions → сборка Docker-образа → деплой на VPS
- Rollback: `docker compose down && docker compose up -d` с предыдущим образом
- Миграции БД: Alembic `upgrade head` автоматически при деплое
#### 5.4. Локальная разработка
- `docker-compose.dev.yml` для локальной разработки
- Hot-reload для Python (uvicorn --reload)
- Локальная БД (PostgreSQL в Docker)
- Синхронизация с VPS: `git push` → автоматический деплой
**Критерий завершения этапа:**
- [ ] Gitea Actions настроены для всех проектов
- [ ] Push в `main` → автоматический деплой
- [ ] Rollback работает
- [ ] Миграции БД применяются автоматически
- [ ] Локальная разработка через Docker
---
## 6. Миграция данных: MySQL → PostgreSQL
### 6.1. Таблицы для миграции
| MySQL таблица | PostgreSQL таблица | Примечание |
|---|---|---|
| `users` | `users` | Без изменений |
| `login_attempts` | `login_attempts` | Без изменений |
| `audit_log` | `audit_log` | Без изменений |
| `objects` | `objects` | + `customer_id` FK |
| `object_assignments` | `object_assignments` | Без изменений |
| `tasks` | `tasks` | Без изменений |
| `task_comments` | `task_comments` | Без изменений |
| `customers` | `customers` | Новая таблица |
| `sla_contracts` | `sla_contracts` | + `customer_id` FK, `contract_number`, `description`, `response_time_hours` |
| `incidents` | `incidents` | Без изменений |
| `engineer_kpi` | `engineer_kpi` | Без изменений |
| `questionnaire_sessions` | `questionnaire_sessions` | Без изменений |
| `questionnaire_answers` | `questionnaire_answers` | Без изменений |
| `questionnaire_items` | `questionnaire_items` | Новая таблица |
| `object_passports` | `object_passports` | Без изменений |
| `blog_posts` | `blog_posts` | ENUM категории изменён |
| `cases` | `cases` | Без изменений |
### 6.2. Скрипт миграции
```python
# scripts/migrate_mysql_to_postgres.py
import mysql.connector
import psycopg2
from psycopg2.extras import execute_batch
# Подключение к MySQL
mysql_conn = mysql.connector.connect(host='...', user='...', password='...', database='...')
mysql_cursor = mysql_conn.cursor(dictionary=True)
# Подключение к PostgreSQL
pg_conn = psycopg2.connect(host='...', user='...', password='...', database='...')
pg_cursor = pg_conn.cursor()
# Миграция каждой таблицы
tables = ['users', 'customers', 'objects', 'sla_contracts', ...]
for table in tables:
mysql_cursor.execute(f"SELECT * FROM {table}")
rows = mysql_cursor.fetchall()
if rows:
columns = rows[0].keys()
placeholders = ', '.join(['%s'] * len(columns))
cols = ', '.join(columns)
execute_batch(
pg_cursor,
f"INSERT INTO {table} ({cols}) VALUES ({placeholders}) ON CONFLICT DO NOTHING",
[tuple(row[col] for col in columns) for row in rows]
)
pg_conn.commit()
```
### 6.3. Проверка миграции
- Сравнить количество записей в MySQL и PostgreSQL
- Проверить FK-связи
- Протестировать CRUD-операции на PostgreSQL
- Откат: сохранить бэкап MySQL до миграции
---
## 7. Безопасность
### 7.1. Аутентификация и авторизация
- JWT-токены с expiration (1 час)
- Refresh tokens (7 дней)
- Роли: `owner`, `engineer`, `technician`
- Гранулярные права для документов (view, edit, cancel)
- CSRF-защита для форм
### 7.2. Защита данных
- Пароли: bcrypt (cost factor 12)
- HTTPS для всех доменов (Let's Encrypt)
- Firewall: только порты 80, 443, 22 (SSH)
- Docker: изоляция контейнеров, нет root в контейнерах
- БД: нет внешнего доступа, только из Docker-сети
### 7.3. Бэкапы
- PostgreSQL: `pg_dump` ежедневно (cron)
- Yandex Disk: файлы уже в облаке
- Gitea: бэкап репозиториев (tar)
- Хранение бэкапов: отдельный диск или облако
### 7.4. Мониторинг
- (Опционально) Uptime Kuma для мониторинга доступности
- Логи: Docker logs + ротация
- Алерты: email/Telegram при ошибках
---
## 8. Риски и митигация
| Риск | Вероятность | Влияние | Митигация |
|---|---|---|---|
| Ошибка миграции данных | Средняя | Высокое | Тестирование на копии, бэкап MySQL |
| Простои при деплое | Низкая | Среднее | Zero-downtime деплой (docker compose up -d) |
| Проблемы с SSL | Низкая | Среднее | Certbot автообновление, мониторинг |
| Yandex Disk API лимиты | Низкая | Низкое | Кэширование, retry logic |
| Нехватка ресурсов VPS | Средняя | Высокое | Мониторинг CPU/RAM, масштабирование |
| Ошибки в расчётах | Средняя | Высокое | Тесты, сравнение с PHP-версией |
---
## 9. Оценки времени
| Этап | Описание | Оценка |
|---|---|---|
| Этап 1 | Docker-фундамент | 1-2 недели |
| Этап 2 | Перенос AegisOne на VPS | 1 неделя |
| Этап 3 | Python-админка | 3-5 недель |
| Этап 4 | Интеграции | 2-3 недели |
| Этап 5 | CI/CD | 1 неделя |
| **Итого** | | **8-12 недель** |
---
## 10. Контрольные точки
| Точка | Описание | Критерий успеха |
|---|---|---|
| КП1 | Docker-фундамент готов | Все сервисы запущены, домены работают по HTTPS |
| КП2 | Публичная часть на VPS | aegisone.ru работает, хостинг отключён |
| КП3 | Python-админка MVP | CRUD для пользователей, объектов, SLA работает |
| КП4 | Python-админка полная | Все модули, расчёты, миграция данных завершены |
| КП5 | Интеграции | Yandex Disk, отчёты, Celery работают |
| КП6 | CI/CD | Автоматический деплой при push |
---
## 11. Открытые вопросы (уточнить перед реализацией)
> **Важно:** Если при реализации возникнут вопросы — лучше уточнить, чем переделывать потом.
1. **Миграция данных:** Переносим все данные из MySQL в PostgreSQL или начинаем с чистой БД? (Рекомендация: мигрировать)
2. **Домен для админки:** `service.aegisone.ru` или `app.aegisone.ru`? (Рекомендация: `service.aegisone.ru`)
3. **Yandex Disk OAuth:** Использовать токен приложения или OAuth с авторизацией пользователя? (Рекомендация: токен приложения для простоты)
4. **Генерация отчётов:** Какие именно отчёты нужны в первую очередь? (Паспорт объекта, отчёт по аудиту, SLA-контракт?)
5. **Интеграция с 1С:** Какая версия 1С? Какой метод интеграции (REST API, OData, файловый обмен)?
6. **Мониторинг:** Нужен ли Uptime Kuma или другой мониторинг? (Рекомендация: да, для продакшена)
7. **Бэкапы:** Где хранить бэкапы PostgreSQL? (Отдельный диск на VPS, облако, другой сервер?)
8. **Тестирование:** Покрывать ли код тестами? (Рекомендация: да, хотя бы критические расчёты)
9. **Документация:** Вести ли документацию по API? (Рекомендация: да, через FastAPI автоматическую /docs)
10. **Логирование:** Какой уровень логирования? Куда писать логи? (Рекомендация: INFO в stdout, Docker logs)
---
## 12. Следующие шаги
1. **Утвердить план** — ответить на открытые вопросы (раздел 11)
2. **Начать Этап 1** — Docker-фундамент на VPS
3. **Параллельно** — создать репозитории в Gitea, настроить CI/CD skeleton
4. **После Этапа 1** — приступить к Этапу 2 (перенос PHP)
5. **После Этапа 2** — приступить к Этапу 3 (Python-админка)
---
**Документ создан:** 2026-05-17
**Версия:** 1.0
**Автор:** AI-ассистент + Владелец
**Статус:** Готов к реализации после утверждения
+485
View File
@@ -0,0 +1,485 @@
Ниже — система KPI, которая превращает инженеров и техников из “выездных исполнителей” в управляемую часть инженерной SLA-модели AegisOne Engineering.
Главная цель системы:
> не “оценивать занятость”, а измерять качество эксплуатации, скорость реакции и снижение рисков объекта
---
0. ПРИНЦИП СИСТЕМЫ KPI
Ты НЕ измеряешь:
сколько выездов сделал инженер
сколько он “починил”
Ты измеряешь:
1) надежность объектов
2) соблюдение SLA
3) качество диагностики
4) снижение повторных инцидентов
---
1. СТРУКТУРА KPI (3 УРОВНЯ)
2. Операционный KPI (ежедневный)
3. SLA KPI (контрактный)
4. Инженерный KPI качества (экспертный)
---
2. ОПЕРАЦИОННЫЙ KPI (Execution KPI)
Это “что сделал инженер”.
---
KPI 1: Время реакции (Response Time)
Формула:
RT = (фактическое время реакции / SLA время реакции)
---
Оценка:
RT Оценка
≤ 1.0 норма
1.01.2 допустимо
> 1.2 нарушение
---
Пример:
SLA: 2 часа
факт: 3 часа
RT = 3 / 2 = 1.5 → ❌ нарушение
---
KPI 2: Время устранения (Resolution Time)
TTR = фактическое время устранения / нормативное время
---
Важно:
Норматив зависит от типа инцидента:
P1 (критический) — 2–6 часов
P2 — до 24 часов
P3 — до 3 дней
---
KPI 3: Закрытие заявок в SLA
SLA Compliance = (заявки в SLA / все заявки) × 100%
---
Норма:
≥ 95% — отлично
9095% — допустимо
< 90% — проблема инженера
---
KPI 4: Повторные обращения (Reopen Rate)
RR = (повторные заявки / общее число заявок) × 100%
---
Норма:
≤ 5% — хорошо
510% — средне
> 10% — плохая диагностика
---
3. SLA KPI (контрактный уровень)
---
KPI 5: Выполнение SLA по объектам
Object SLA = (объекты без нарушений SLA / все объекты) × 100%
---
Норма:
≥ 95% — стабильная сеть объектов
< 90% — системная проблема команды
---
KPI 6: Доступность систем (System Uptime)
Uptime = (время работы системы / общее время) × 100%
---
Цель:
99%+ для критических объектов
9799% допустимо
< 97% — риск SLA разрыва
---
4. ИНЖЕНЕРНЫЙ KPI (КАЧЕСТВО ЭКСПЕРТИЗЫ)
Это ключевой уровень, который отличает тебя от монтажников.
---
KPI 7: Точность диагностики
Accuracy = (подтвержденные неисправности / выявленные) × 100%
---
Пример:
выявил 10 проблем
9 подтвердились
→ 90%
---
Норма:
≥ 85% — хорошо
< 80% — инженер слабый
---
KPI 8: Глубина диагностики (Risk Coverage Score)
Оценивает, насколько инженер видит системные риски.
---
Формула (балльная система):
Каждый объект оценивается по 5 зонам:
видео
СКУД
пожарка
питание
сеть
RCS = (проверенные зоны / 5) × 100%
---
Норма:
100% — стандарт AegisOne
< 80% — поверхностная работа
---
KPI 9: Снижение повторных инцидентов
IRR = ((инциденты до - инциденты после) / до) × 100%
---
Это ключевой KPI бизнеса
---
5. ФИНАНСОВЫЙ KPI ИНЖЕНЕРА
---
KPI 10: Доход на инженера (Revenue per Engineer)
RPE = общий SLA доход / количество инженеров
---
Норма:
500k – слабая загрузка
800k норм
1.2M+ – сильная модель
---
6. ИТОГОВЫЙ ИНДЕКС ИНЖЕНЕРА (ENGINEER SCORE)
Это твой главный инструмент управления.
---
Формула:
ES =
(0.25 × SLA Compliance) +
(0.20 × Response Time Score) +
(0.20 × Resolution Time Score) +
(0.15 × Diagnosis Accuracy) +
(0.10 × Reopen Rate Score) +
(0.10 × Risk Coverage Score)
---
Интерпретация:
Score Уровень
90100 Senior Engineer
8089 Strong Engineer
7079 Middle
< 70 требует обучения
---
7. ДОПОЛНИТЕЛЬНЫЕ KPI (ОЧЕНЬ ВАЖНЫЕ)
---
KPI 11: Документирование (Documentation Quality)
DQ = (полные отчёты / все выезды) × 100%
---
KPI 12: Качество отчёта (Report Quality Score)
Оценивается по чек-листу:
есть ли риски
есть ли вывод
есть ли рекомендации
есть ли фото/фиксация
---
KPI 13: Самостоятельность инженера
Autonomy = (самостоятельные решения / всего решений) × 100%
---
8. КАК ЭТО ВЫГЛЯДИТ В CRM
---
Инженер:
- SLA Compliance: 96%
- RT: 1.1
- TTR: 0.9
- Diagnosis: 88%
- Reopen: 3%
- Score: 91 (Senior)
---
9. СИСТЕМА МОТИВАЦИИ (ВАЖНО)
---
БАЗОВАЯ ЛОГИКА:
фикс оклад
бонус за KPI
штрафы только за SLA нарушения
---
БОНУС:
Score 90+ → +20% к премии
Score 8089 → +10%
Score < 80 → без бонуса
---
10. ГЛАВНЫЙ ПРИНЦИП СИСТЕМЫ
Ты НЕ платишь за:
❌ “работу”
❌ “выезды”
Ты платишь за:
стабильность инфраструктуры клиента
---
11. КАК ЭТА СИСТЕМА МАСШТАБИРУЕТ БИЗНЕС
---
1 инженер = управляемая единица SLA
Ты можешь:
добавлять инженеров
сравнивать эффективность
масштабировать регионы
контролировать качество без присутствия
---
12. СВЯЗЬ С ТВОЕЙ БИЗНЕС-МОДЕЛЬЮ
---
KPI инженера
качество SLA
удержание клиентов
MRR рост
масштаб компании
---
13. СЛЕДУЮЩИЙ УРОВЕНЬ (если продолжать систему)
Я могу дальше собрать:
систему грейдов инженеров (Junior → Lead → Chief)
модель расчёта зарплаты под KPI
автоматическую таблицу KPI в Excel/Notion
SLA dashboard (как у IT-компаний)
систему контроля качества через аудит отчётов
---
ИТОГ
Ты получил не “мотивацию сотрудников”.
Ты получил:
> систему управления инженерной эксплуатационной компанией через измеримые риски и SLA
Это уровень компаний, которые продают не услуги — а надежность инфраструктуры бизнеса.
+520
View File
@@ -0,0 +1,520 @@
Ты сейчас описываешь переход от “владельца инженерной фирмы” к роли:
операционного директора инженерной системы (Engineering Operations System Owner)
В такой модели ты уже не контролируешь людей напрямую — ты контролируешь поток рисков, денег, SLA и загрузки системы.
Я разложу это как управленческую “панель управления” (CEO Dashboard), разделив на:
1. критически важные метрики (must have)
2. важные (should have)
3. полезные (nice to have)
4. формулы
5. графики и визуализации
6. как это связывается в одну систему управления
---
1. КРИТИЧЕСКИ ВАЖНЫЕ ПАРАМЕТРЫ (CEO CORE CONTROL LAYER)
Это то, без чего ты не управляешь компанией, а просто “наблюдаешь бизнес”.
---
1.1 MRR / ARR (регулярный доход SLA)
Формула:
MRR = Σ (все SLA контракты / 12)
ARR = MRR × 12
---
Почему это главное:
Ты продаёшь не монтаж, а:
> стабильность объектов
---
График:
📈 линия роста MRR по месяцам
📊 разбивка по типам объектов
---
Управленческая логика:
если MRR растёт → система здорова
если нет → маркетинг/воронка сломана
---
1.2 RETENTION RATE (удержание клиентов)
Формула:
Retention = (клиенты в конце периода / клиенты в начале) × 100%
---
Норма:
95100% = отлично
8595% = нормально
<85% = проблема в SLA
---
График:
📉 “утечка клиентов по месяцам”
---
Интерпретация:
Если падает retention — проблема не в продажах, а в:
инженерах
SLA
качестве реакции
---
1.3 SLA COMPLIANCE (исполнение контрактов)
Формула:
SLA Compliance = (выполненные заявки в SLA / все заявки) × 100%
---
Норма:
95–99% = система стабильна
<90% = начинаются разрывы контрактов
---
График:
📊 compliance по инженерам / регионам
---
1.4 COST PER SLA OBJECT (стоимость обслуживания объекта)
Формула:
Cost per Object = (ФОТ + выезды + оборудование + накладные) / количество объектов
---
Почему важно:
Ты должен видеть:
> объект приносит деньги или сжигает ресурс
---
График:
📊 распределение стоимости по типам объектов
---
1.5 ENGINEER PRODUCTIVITY (производительность инженера)
Формула:
EP = SLA Revenue / количество инженеров
---
Или глубже:
EP2 = (закрытые заявки × сложность) / часы работы
---
График:
📊 эффективность по инженерам (ranking)
---
2. ВАЖНЫЕ ПАРАМЕТРЫ (OPTIMIZATION LAYER)
Это влияет на прибыль, но не ломает систему сразу.
---
2.1 LEAD → SLA CONVERSION RATE
Формула:
Conversion = SLA contracts / all qualified leads
---
График:
📈 воронка: лиды → аудит → SLA
---
Диагностика:
низкая конверсия → проблема продаж/аудита
высокая → система сильная
---
2.2 AVERAGE SLA CONTRACT VALUE
Формула:
ARPC = Total SLA revenue / number of contracts
---
График:
📊 средний чек по объектам
---
2.3 INCIDENT RATE (частота аварий)
Формула:
IR = incidents / objects
---
Почему важно:
Показывает:
> реальное качество эксплуатации
---
2.4 REOPEN RATE (повторные заявки)
Reopen = повторные обращения / все обращения
---
Если растёт:
→ инженер плохо диагностирует
---
3. ПОЛЕЗНЫЕ ПАРАМЕТРЫ (NICE TO HAVE)
Это усиливает контроль, но не критично.
---
3.1 CUSTOMER SATISFACTION (CSAT)
CSAT = (положительные оценки / все оценки) × 100%
---
3.2 RESPONSE TIME DISTRIBUTION
Не среднее, а распределение:
📊 02 часа
📊 26
📊 624
---
3.3 EQUIPMENT HEALTH INDEX
EHI = (исправное оборудование / общее оборудование) × 100%
---
3.4 ENGINEER UTILIZATION RATE
Utilization = рабочие часы / доступные часы
---
4. ГЛАВНЫЙ CEO-ИНДЕКС (SYSTEM HEALTH SCORE)
Это твоя “панель управления бизнесом”.
---
Формула:
SHS =
(0.25 × MRR growth) +
(0.20 × SLA compliance) +
(0.20 × retention) +
(0.15 × engineer productivity) +
(0.10 × conversion rate) +
(0.10 × incident stability)
---
Интерпретация:
SHS состояние
85–100 масштабируемый рост
70–85 стабильный бизнес
50–70 проблемы в системе
<50 системный кризис
---
5. КЛЮЧЕВЫЕ ГРАФИКИ CEO (ДОСКА УПРАВЛЕНИЯ)
---
5.1 MRR Growth Curve
📈 рост повторного дохода
---
5.2 SLA Compliance Heatmap
🟥 красный = проблемные инженеры
🟩 зелёный = сильные
---
5.3 Client Retention Funnel
📊 где теряются клиенты
---
5.4 Engineer Performance Ranking
📊 топ / низ производительности
---
5.5 Incident Map
📍 какие объекты ломаются чаще
---
5.6 Cost vs Revenue per Object
📊 прибыльность каждого SLA
---
6. КРИТИЧЕСКАЯ ЛОГИКА УПРАВЛЕНИЯ
Ты управляешь не людьми.
Ты управляешь:
рисками → SLA → деньгами → нагрузкой инженеров
---
7. ЧТО ТЕБЕ ОБЯЗАТЕЛЬНО ВИДЕТЬ КАЖДЫЙ ДЕНЬ
---
DAILY CEO DASHBOARD:
SLA compliance
аварии за 24ч
новые SLA контракты
просрочки
перегруз инженеров
---
8. ЧТО ТЕБЕ НУЖНО ВИДЕТЬ РАЗ В НЕДЕЛЮ
---
MRR рост
retention
эффективность инженеров
конверсия аудита в SLA
---
9. ЧТО СМОТРЕТЬ РАЗ В МЕСЯЦ
---
прибыльность объектов
эффективность регионов
нагрузка команды
системные сбои
---
10. ГЛАВНЫЙ ПРИНЦИП
Ты не управляешь:
❌ монтажами
❌ выездами
❌ людьми
---
Ты управляешь:
системой инженерной надежности объектов бизнеса
---
11. ЕСЛИ СВЯЗАТЬ ВСЁ В ОДНУ МОДЕЛЬ
Аудит → Risk Score → SLA цена → контракт → MRR → KPI инженеров → retention → рост
---
12. ИТОГ
Ты как CEO должен видеть не “что происходит”, а:
> где именно в системе возникает прибыль или утечка денег/риска
---
13. ЕСЛИ ДАЛЬШЕ РАЗВИВАТЬ СИСТЕМУ
Я могу собрать:
полноценный CEO-dashboard (Excel / Notion / Power BI)
автоматическую систему расчёта SHS
дашборд инженеров в реальном времени
модель прогнозирования MRR на 3–6 месяцев
систему раннего предупреждения потерь клиентов
---
Если коротко:
ты строишь не компанию — ты строишь управляемую инженерную систему с финансовыми законами внутри неё.
+488
View File
@@ -0,0 +1,488 @@
Сейчас мы подходим к одному из самых важных этапов.
Потому что SEO для тебя — это НЕ “трафик ради трафика”.
Тебе не нужны:
- тысячи посетителей;
- B2C заявки;
- мусорные лиды.
Тебе нужен:
# узкий B2B SEO с высокой экспертностью.
И здесь у тебя огромный шанс, потому что рынок безопасности:
# SEO делает ужасно.
Большинство сайтов:
- про “монтаж камер”;
- про оборудование;
- про бренды;
- про “низкие цены”.
А ты можешь занять:
# нишу инженерной эксплуатации.
---
# ГЛАВНАЯ СТРАТЕГИЯ SEO
---
# НЕ ПРОДАВАТЬ:
- камеры;
- оборудование;
- монтаж.
---
# ПРОДАВАТЬ:
- эксплуатацию;
- SLA;
- аудит;
- контроль;
- сопровождение;
- надежность.
---
# ТВОЯ SEO-МОДЕЛЬ
---
# УРОВЕНЬ 1
# КОММЕРЧЕСКИЕ СТРАНИЦЫ
Это:
# страницы услуг.
Они приводят клиентов.
---
# УРОВЕНЬ 2
# ОТРАСЛЕВЫЕ СТРАНИЦЫ
Это:
# страницы под конкретный бизнес.
Они повышают доверие и SEO.
---
# УРОВЕНЬ 3
# ЭКСПЕРТНЫЙ БЛОГ
Это:
# двигатель доверия.
---
# СТРУКТУРА САЙТА
Вот идеальная структура для тебя.
---
# ГЛАВНАЯ
## URL:
`/`
---
## Цель:
- показать позиционирование;
- перевести из “монтажников”;
- дать B2B доверие.
---
# РАЗДЕЛ УСЛУГ
---
## `/services/`
Общая страница услуг.
---
# ОСНОВНЫЕ СТРАНИЦЫ
---
## `/services/audit/`
# Технический аудит систем безопасности
---
## `/services/sla/`
# SLA сопровождение объектов
---
## `/services/maintenance/`
# Техническое обслуживание систем безопасности
---
## `/services/external-engineer/`
# Внешний инженер безопасности
---
## `/services/recovery/`
# Восстановление проблемных объектов
---
## `/services/fire-alarm/`
# Обслуживание пожарной сигнализации
---
## `/services/video-surveillance/`
# Эксплуатация видеонаблюдения
---
## `/services/access-control/`
# Обслуживание СКУД
---
# ОТРАСЛЕВЫЕ СТРАНИЦЫ
Это очень важно.
---
## `/industries/hotels/`
# Системы безопасности гостиниц
---
## `/industries/logistics/`
# Склады и логистика
---
## `/industries/medical/`
# Клиники и медцентры
---
## `/industries/commercial/`
# Коммерческая недвижимость
---
## `/industries/agro/`
# Агропредприятия
---
# БЛОГ
---
## `/blog/`
---
# КАТЕГОРИИ
---
## `/blog/video/`
Проблемы видеонаблюдения.
---
## `/blog/fire-alarm/`
Пожарная безопасность.
---
## `/blog/sla/`
Эксплуатация и SLA.
---
## `/blog/audit/`
Технический аудит.
---
# ПОЧЕМУ ЭТО ТАК ВАЖНО
Google и Яндекс сейчас любят:
- структуру;
- экспертность;
- тематичность;
- глубину.
---
# ЧТО НЕЛЬЗЯ ДЕЛАТЬ
---
# НЕ ДЕЛАТЬ:
- 1 страницу “все услуги”;
- короткие SEO-тексты;
- переспам;
- “установка камер Краснодар”.
Это мусорный рынок.
---
# ТВОЯ SEO-СТРАТЕГИЯ
---
# НЕ:
“дешевый монтаж”
---
# А:
# “эксплуатационные риски объектов”.
---
# ГЛАВНЫЕ SEO КЛЮЧИ
Вот где деньги.
---
# АУДИТ
- аудит систем безопасности
- технический аудит объекта
- аудит видеонаблюдения
- аудит пожарной сигнализации
- проверка систем безопасности
---
# SLA
- SLA обслуживание
- сопровождение систем безопасности
- техническое сопровождение объекта
- эксплуатация систем безопасности
---
# ОБСЛУЖИВАНИЕ
- обслуживание пожарной сигнализации Краснодар
- обслуживание видеонаблюдения Краснодар
- обслуживание СКУД Краснодар
---
# ОТРАСЛЕВЫЕ
- системы безопасности гостиниц
- обслуживание гостиниц Краснодар
- безопасность складов Краснодар
---
# ТЕПЕРЬ ГЛАВНОЕ
# КАК ПИСАТЬ SEO-ТЕКСТЫ
---
# ТВОЯ ЦЕЛЬ
НЕ: “набить ключи”.
---
# А:
# показать инженерную экспертность.
---
# ПРИМЕР SEO-ТЕКСТА
---
---
# ТЕПЕРЬ:
# КОД СТРАНИЦЫ УСЛУГИ
Например:
# `/services/audit/index.php`
---
---
# SEO РЕКОМЕНДАЦИИ ПО ТЕХНИЧЕСКОЙ ЧАСТИ
---
# ОБЯЗАТЕЛЬНО:
---
# 1. ЧПУ URL
---
# ХОРОШО:
`/services/audit/`
---
# ПЛОХО:
`/page.php?id=123`
---
# 2. TITLE И DESCRIPTION
Для каждой страницы.
---
# Пример:
## TITLE:
Технический аудит систем безопасности в Краснодаре
---
## DESCRIPTION:
Проверка состояния систем безопасности коммерческих объектов. Аудит видеонаблюдения, СКУД и пожарной сигнализации.
---
# 3. H1 ТОЛЬКО ОДИН
Это важно.
---
# 4. schema.org
Очень желательно.
---
# 5. СКОРОСТЬ
Не перегружать:
- анимацией;
- тяжелыми видео;
- конструкторами.
---
# 6. GEO SEO
На каждой странице:
- Краснодар;
- Краснодарский край.
---
# 7. ВНУТРЕННЯЯ ПЕРЕЛИНКОВКА
Например:
- из статьи → на аудит;
- из отрасли → на SLA;
- из кейса → на услугу.
---
# ЧТО ДАСТ РЕАЛЬНЫЙ РОСТ
---
# НЕ:
- покупка ссылок;
- SEO-магия.
---
# А:
- экспертные статьи;
- кейсы;
- отраслевые страницы;
- техническая глубина;
- регулярность.
---
# САМОЕ ВАЖНОЕ
Ты должен выглядеть:
# не как “сайт монтажников”.
А как:
# инженерная эксплуатационная компания.
И это:
- редкость;
- слабоконкурентная ниша;
- высокий уровень доверия;
- хороший B2B SEO.
+795
View File
@@ -0,0 +1,795 @@
SLA — ЭТО ОСНОВА ТВОЕГО ПЕРЕХОДА ИЗ “МОНТАЖНИКОВ” В B2B-СЕРВИС
Большинство компаний безопасности в Краснодарском крае:
“иногда приезжают”;
“что-то смотрят”;
“делают отметку”;
работают хаотично.
SLA делает тебя:
инженерной сервисной компанией;
предсказуемым подрядчиком;
частью эксплуатации бизнеса клиента.
Именно SLA позволяет:
продавать дороже;
получать абонентку;
заходить в сети;
работать с коммерческой недвижимостью;
уходить от демпинга.
---
ЧТО ТАКОЕ SLA ПРОСТЫМИ СЛОВАМИ
SLA (Service Level Agreement)
это:
соглашение об уровне сервиса.
То есть: ты заранее фиксируешь:
что обслуживаешь;
как быстро реагируешь;
что считается аварией;
что входит в обслуживание;
какие обязательства у сторон.
---
ПОЧЕМУ B2B ЭТО ЛЮБИТ
Потому что бизнес покупает:
предсказуемость;
контроль;
ответственность;
понятные сроки.
---
ТВОЯ ЦЕЛЬ
НЕ:
> “мы обслуживаем камеры”
А:
“мы гарантируем работоспособность критической инфраструктуры объекта”.
---
КАК SLA ПОДНИМАЕТ ТЕБЯ НАД РЫНКОМ
Без SLA:
“звоните если что”.
С SLA:
регламенты;
сроки реакции;
журналирование;
прозрачность;
ответственность.
---
ГЛАВНАЯ ОШИБКА
Многие делают:
> “приедем в течение суток”.
Это НЕ SLA.
---
НАСТОЯЩИЙ SLA ВКЛЮЧАЕТ
---
1. КЛАССИФИКАЦИЮ ИНЦИДЕНТОВ
Например:
Приоритет Пример Реакция
P1 Критический Не работает пожарка 2 часа
P2 Высокий Нет архива камер 4 часа
P3 Средний Частично не работает СКУД 24 часа
P4 Низкий Настройка доступа Планово
---
2. ВРЕМЯ РЕАКЦИИ
Очень важно: не “устранения”.
А:
начала работ.
---
3. РЕГЛАМЕНТНЫЕ РАБОТЫ
Например:
ежемесячные проверки;
тестирование;
резервное копирование;
контроль архивов;
проверка питания;
чистка;
тест тревог.
---
4. ОТЧЕТНОСТЬ
Это критично.
Ты должен быть:
“прозрачным подрядчиком”.
---
5. SLA ПО КАНАЛАМ СВЯЗИ
Например:
Telegram;
email;
аварийный телефон;
сервисный портал.
---
6. SLA ПО ДОСТУПНОСТИ
Вот тут начинаются большие чеки.
Например:
Система Целевая доступность
Пожарка 99.9%
Видеонаблюдение 99%
СКУД 99%
---
ТЕПЕРЬ ГЛАВНОЕ:
КАК ТЕБЕ ДЕЛАТЬ SLA В РЕАЛЬНОСТИ
Тебе НЕ нужен корпоративный монстр.
Тебе нужна:
простая инженерная система.
---
БАЗОВАЯ МОДЕЛЬ SLA ДЛЯ ТЕБЯ
---
START SLA
Для:
небольших объектов;
офисов;
клиник;
магазинов.
---
Реакция:
до 24 часов
---
Регламент:
1 раз в месяц.
---
Что входит:
проверка состояния;
диагностика;
журнал;
рекомендации;
консультации.
---
Стоимость:
2540 тыс ₽/мес
Для Краснодара — нормально для качественного B2B.
---
BUSINESS SLA
Твой основной продукт.
---
Реакция:
критическая авария — 4 часа;
обычная — 24 часа.
---
Регламент:
24 раза в месяц.
---
Входит:
аварийные выезды;
удаленная диагностика;
контроль архива;
контроль питания;
фотоотчеты;
журнал;
рекомендации;
сопровождение проверок.
---
Стоимость:
60120 тыс ₽/мес
Для:
гостиниц;
складов;
коммерческой недвижимости.
---
ENTERPRISE SLA
Вот это путь к большим деньгам.
---
Реакция:
2 часа
---
Формат:
“внешний инженерный отдел”.
---
Входит:
постоянный контроль;
участие в эксплуатации;
работа с подрядчиками;
аудит;
сопровождение модернизаций;
приемка;
развитие систем.
---
Стоимость:
180500 тыс ₽/мес
---
ВАЖНО:
SLA НЕЛЬЗЯ ПРОДАВАТЬ КАК “ТО”
Иначе: ты вернешься в дешевый рынок.
---
SLA ПРОДАЕТСЯ КАК:
“управление рисками объекта”.
---
ПРИМЕР ТЕКСТА ДЛЯ САЙТА
---
SLA-сопровождение систем безопасности
Мы работаем по регламентированным SLA-моделям обслуживания.
Это означает:
— фиксированное время реакции;
— контроль состояния систем;
— прозрачную отчетность;
— регламентные проверки;
— ответственность за работоспособность инфраструктуры объекта.
SLA позволяет снизить риски простоев, исключить скрытые неисправности и обеспечить стабильную эксплуатацию систем безопасности.
Для каждого объекта разрабатывается индивидуальный регламент обслуживания.
---
ПРИМЕР SLA ТАБЛИЦЫ ДЛЯ САЙТА
---
Приоритет| Описание| Реакция
P1| Полный отказ критической системы| до 2 часов
P2| Частичная потеря функционала| до 4 часов
P3| Некритичная неисправность| до 24 часов
P4| Плановые работы и настройки| по графику
---
ТЕПЕРЬ САМОЕ ВАЖНОЕ
КАК ТЕБЕ СЧИТАТЬ ЦЕНУ SLA
Не “от количества камер”.
Это ошибка рынка.
---
ТВОЯ МОДЕЛЬ ЦЕНООБРАЗОВАНИЯ
Цена считается по:
критичности объекта;
количеству систем;
SLA;
расстоянию;
времени реакции;
рискам;
сложности эксплуатации.
---
МОДЕЛЬ ДЛЯ КРАСНОДАРСКОГО КРАЯ
---
КРАСНОДАР
Можно:
быстрые выезды;
дешевле логистика.
---
СОЧИ
Цена должна быть:
выше на 3050%.
Из-за:
логистики;
сезонности;
срочности.
---
УДАЛЕННЫЕ РАЙОНЫ
Нужно:
отдельное SLA.
---
Например:
Базовая реакция:
24 часа.
Срочный выезд:
дополнительная ставка.
---
КАК СЧИТАТЬ SLA
Вот модель.
---
БАЗА
Например:
35 000 ₽
---
ПЛЮС:
Параметр Доплата
24/7 доступность +20%
SLA 2 часа +35%
Удаленный район +15–40%
Несколько объектов индивидуально
Высокая критичность +25%
---
ПРИМЕР
Гостиница:
64 камеры;
СКУД;
пожарка;
архив;
Краснодар.
---
SLA:
4 часа.
---
Цена:
85120 тыс ₽/мес
И это нормальный рынок для качественного сервиса.
---
ТЕПЕРЬ САМОЕ ВАЖНОЕ
КАК ТЕБЕ ВЫПОЛНЯТЬ SLA БЕЗ БОЛИ
---
НЕ БРАТЬ ВСЕХ ПОДРЯД
Только:
нормальные объекты;
нормальные бюджеты;
адекватные заказчики.
---
НЕ ПРОДАВАТЬ “ДЕШЕВО”
Иначе SLA развалится.
---
ОБЯЗАТЕЛЬНО:
журнал заявок;
регламент;
чек-листы;
фотофиксация;
история обслуживания.
---
ТЕХНИЧЕСКИ ТЕБЕ НУЖНО
---
1. HELP DESK
Минимум:
Telegram;
1С;
Google Sheets.
---
Лучше:
GLPI;
Zammad;
Jira Service Desk.
---
2. БАЗА ОБЪЕКТОВ
Для каждого:
схемы;
пароли;
оборудование;
IP;
контакты;
история.
---
3. ЧЕК-ЛИСТЫ
Например:
Видеонаблюдение:
запись;
архив;
питание;
доступ;
фокус;
время.
---
4. SLA-ДОКУМЕНТ
Это обязательно.
---
ПРИМЕР СТРУКТУРЫ SLA ДОГОВОРА
---
1. Предмет SLA
Исполнитель обеспечивает техническое сопровождение
и контроль работоспособности систем безопасности объекта.
2. Время реакции
P1 — до 2 часов
P2 — до 4 часов
P3 — до 24 часов
3. Регламентные работы
— диагностика
— тестирование
— проверка журналов
— контроль архива
— проверка питания
4. Отчетность
По результатам работ предоставляется отчет.
5. Ограничения SLA
SLA не распространяется на:
— физическое повреждение оборудования
— аварии электропитания
— действия третьих лиц
— форс-мажор
---
ИТОГ
SLA — это:
не “дополнение”.
Это:
фундамент твоего нового бизнеса.
Именно SLA:
выводит тебя из монтажников;
делает recurring revenue;
дает стабильность;
повышает чек;
позволяет масштабироваться.
И это идеально совпадает с твоим профилем инженера и системного человека.
+69
View File
@@ -0,0 +1,69 @@
# SQL-запросы для администрирования
## Как выполнить
1. Войдите в phpMyAdmin на хостинге
2. Выберите базу данных `j30663893_engineering`
3. Перейдите на вкладку «SQL»
4. Вставьте запрос и нажмите «Вперёд»
## Смена пароля владельца
```sql
UPDATE users
SET password_hash = '$2y$10$e0MYzXyjpJS7k0ggKtqjGOcMFU.h9S1sGqf3qJ6qJ6qJ6qJ6qJ6qO'
WHERE login = 'ваш_логин';
```
**Важно:** Замените хеш на реальный. Для генерации хеша используйте PHP:
```php
<?php echo password_hash('новый_пароль', PASSWORD_DEFAULT); ?>
```
Создайте файл `hash.php` в корне сайта с этим кодом, откройте в браузере, скопируйте хеш, удалите файл.
## Разблокировка пользователя
```sql
DELETE FROM login_attempts WHERE login = 'логин_пользователя';
```
## Активация всех кейсов на главной
```sql
UPDATE cases SET is_active = 1;
```
## Просмотр всех пользователей
```sql
SELECT id, login, role, full_name, is_active, created_at FROM users;
```
## Просмотр активных SLA контрактов
```sql
SELECT sc.id, sc.contract_number, o.name as object, sc.client_name,
sc.sla_price_monthly, sc.start_date, sc.end_date, sc.status
FROM sla_contracts sc
JOIN objects o ON o.id = sc.object_id
WHERE sc.status = 'active'
ORDER BY sc.start_date DESC;
```
## Сброс опросника (удаление черновиков)
```sql
DELETE qs, qa
FROM questionnaire_sessions qs
LEFT JOIN questionnaire_answers qa ON qa.session_id = qs.id
WHERE qs.status != 'completed';
```
## Просмотр документов и прав
```sql
-- Файлы в базе не хранятся. Проверьте папку service/docs/ на хостинге.
-- Права хранятся в service/permissions/docs_permissions.json
```
@@ -0,0 +1,934 @@
Хорошо, это важный шаг — ты фактически выбираешь архитектуру бренда (brand architecture), а не просто название. От этого зависит:
как тебя воспринимают (монтажники vs инженерная компания),
как ты продаёшь SLA,
насколько легко масштабируешься по РФ.
Я разложу каждый вариант строго по твоей модели:
👉 аудит → эксплуатация → SLA → инженерный сервис → масштабирование
---
1. AegisOne Engineering
Суть позиционирования:
инженерная компания полного цикла
---
Как воспринимается рынком:
“серьёзная инженерная организация”
ближе к проектированию / эксплуатации
высокий уровень доверия B2B
---
Сильные стороны:
подходит под SLA и аудит
звучит “дорого и системно”
легко масштабируется на РФ
не привязан к монтажу
---
Слабые стороны:
не сразу понятно “сервис”
нужно объяснение через сайт/контент
---
Лучше всего подходит для:
✔ SLA обслуживание
✔ аудит объектов
✔ эксплуатация систем безопасности
✔ B2B рынок (гостиницы, склады, УК)
---
Итог:
> ❗ ЛУЧШИЙ ОСНОВНОЙ БРЕНД (ядро компании)
---
2. AegisOne Service
Суть:
сервисная эксплуатационная компания
---
Восприятие:
обслуживание
ремонт
“сервисники”
---
Сильные стороны:
понятный вход для клиентов
хорошо продаёт обслуживание
легко объяснить
---
Слабые стороны:
слишком “сервис/ремонт”
занижает статус инженера
хуже для крупных B2B объектов
---
Лучше для:
✔ абонентское обслуживание
✔ выездной сервис
✔ мелкие и средние клиенты
✔ поддержка SLA как продукт
---
Итог:
> ⚠ можно как подразделение, но не основной бренд
---
3. AegisOne Security Engineering
Суть:
инженерия безопасности (очень сильный B2B бренд)
---
Восприятие:
высокий уровень экспертизы
ближе к проектированию и системной безопасности
звучит “международно”
---
Сильные стороны:
максимальная экспертность
идеально под аудит и SLA
хорошо для РФ + будущего выхода за регион
не “монтажники”
---
Слабые стороны:
длинное название
чуть тяжеловесно в маркетинге
требует сокращений (AegisOne SE)
---
Лучше для:
✔ аудит
✔ техническая экспертиза
✔ корпоративные объекты
✔ крупные B2B сделки
✔ федеральное масштабирование
---
Итог:
> ⭐ ЛУЧШИЙ ДЛЯ ЭКСПЕРТНОСТИ И PREMIUM B2B
---
4. AegisOne Facility Security
Суть:
безопасность объектов (facility management)
---
Восприятие:
управление объектами
эксплуатация зданий
FM-компании уровня “facility management”
---
Сильные стороны:
очень сильный корпоративный сегмент
подходит для УК, ТЦ, гостиниц
легко встраивается в FM-рынок
---
Слабые стороны:
менее технический фокус
может размывать “инженерность”
ближе к управлению недвижимостью
---
Лучше для:
✔ управляющие компании
✔ гостиничные сети
✔ торговые центры
✔ facility management подрядчики
---
Итог:
> ⚠ хорошо как направление, но не ядро
---
5. AegisOne TechService
Суть:
техсервис / техническое обслуживание
---
Восприятие:
ремонтная служба
IT/техподдержка стиль
---
Сильные стороны:
простое и понятное
легко продаётся “в лоб”
подходит для мелких объектов
---
Слабые стороны:
дешёвое восприятие
не тянет премиум SLA
теряет инженерный статус
---
Лучше для:
✔ массовое обслуживание
✔ низкий чек рынок
✔ быстрые заявки
✔ простые сервисные работы
---
Итог:
> ❌ не подходит под твою стратегию роста
---
6. РЕКОМЕНТИРУЕМАЯ СТРУКТУРА БРЕНДА (ОПТИМАЛЬНАЯ)
Вот как это должно выглядеть в системе:
---
🧠 ГЛАВНЫЙ БРЕНД (основа)
AegisOne Engineering
👉 ядро:
SLA
аудит
эксплуатация
инженерный контроль
---
🔧 СЕРВИСНОЕ ПОДРАЗДЕЛЕНИЕ
AegisOne Service
👉 внутри:
выезды
ремонт
поддержка
реакция по SLA
---
🧪 ЭКСПЕРТНЫЙ КОНТУР
AegisOne Security Engineering
👉 используется для:
сайта экспертного уровня
презентаций
коммерческих предложений
крупных B2B переговоров
---
🏢 ФУТУРОЛОГИЧЕСКИЙ КОНТУР (масштаб)
AegisOne Facility Security
👉 когда выйдешь в:
УК
сети отелей
FM-контракты
---
⚙️ ТЕХПОДДЕРЖКА (НЕ ОСНОВНАЯ)
AegisOne TechService
👉 можно использовать как:
внутренний бренд
дешёвый сегмент (если вдруг нужен)
---
7. ЛУЧШАЯ СТРАТЕГИЯ (РЕАЛЬНО ПРАВИЛЬНАЯ)
Вот как это делает сильная инженерная компания:
---
AegisOne Engineering
AegisOne Security Engineering (экспертиза / доверие)
AegisOne Service (исполнение SLA)
AegisOne Facility Security (масштаб на УК и сети)
---
8. ВАЖНОЕ РЕШЕНИЕ ДЛЯ ТЕБЯ
Если брать твою стратегию:
уход от монтажа
переход в SLA
рост до 23 млн+
B2B рынок
экспертность
---
ТОП-1 выбор:
✔ AegisOne Engineering
---
ТОП-2 усиление:
✔ AegisOne Security Engineering
---
НЕ РЕКОМЕНДУЮ КАК ОСНОВУ:
Service (снижает статус)
TechService (дешевит бренд)
Facility Security (узкая ниша на будущее)
---
9. ЕСЛИ ХОЧЕШЬ ДАЛЬШЕ УСИЛИТЬ БРЕНД
Я могу собрать тебе следующий уровень:
нейминг архитектуры сайта (как разложить домены/страницы)
логотип-систему (как должен выглядеть бренд инженерной компании)
упаковку КП под каждый бренд
позиционирование для тендеров
“как выглядеть как федеральный подрядчик при локальном бизнесе”
---
Если коротко:
> тебе нужен не набор названий, а система уровней доверия
И из всех вариантов:
AegisOne Engineering — фундамент бизнеса.
---
Отлично, теперь мы фиксируем **архитектуру компании как инженерной системы**, а не сайта.
Ты строишь не “фирму услуг”, а:
# AegisOne Engineering — операционную систему эксплуатации объектов
Ниже даю тебе:
1. структуру компании (как она “разложена в голове клиента”)
2. все разделы сайта / системы
3. документы (шаблоны, которые продают SLA сами)
4. сценарии взаимодействия с клиентом
5. стандарты (как ты выглядишь как федеральный подрядчик)
---
# 1. СТРУКТУРА AEGISONE ENGINEERING
## 🧠 ЯДРО КОМПАНИИ
# AegisOne Engineering
> Инженерная компания по эксплуатации и контролю систем безопасности объектов
---
## ФУНКЦИОНАЛЬНЫЕ БЛОКИ
```text
1. Engineering Audit (аудит и диагностика)
2. SLA Operations (эксплуатация)
3. Incident Response (аварийные работы)
4. Technical Supervision (контроль подрядчиков)
5. Documentation & Compliance (документация)
6. Risk Engineering (анализ рисков)
```
---
# 2. СТРУКТУРА САЙТА (КАК ДОЛЖЕН ВЫГЛЯДЕТЬ AEGISONE)
---
# 🏠 ГЛАВНАЯ
## `/`
### Название:
AegisOne Engineering
### Смысл:
> инженерная эксплуатация систем безопасности объектов
---
# 3. РАЗДЕЛЫ САЙТА
---
# 3.1 ENGINEERING AUDIT
## `/engineering-audit/`
### Описание:
```text
Технический аудит систем безопасности с оценкой рисков эксплуатации.
Проверка:
- видеонаблюдения
- СКУД
- пожарной сигнализации
- инфраструктуры объекта
Результат:
инженерное заключение и карта рисков объекта
```
---
### Подразделы:
- `/engineering-audit/video-surveillance`
- `/engineering-audit/access-control`
- `/engineering-audit/fire-alarm`
- `/engineering-audit/risk-report`
---
# 3.2 SLA OPERATIONS
## `/sla-operations/`
### Описание:
```text
Постоянное техническое сопровождение объектов с регламентами и SLA.
Мы обеспечиваем:
- стабильную работу систем
- контроль состояния оборудования
- аварийное реагирование
- регулярные проверки
```
---
### Подразделы:
- `/sla-operations/start`
- `/sla-operations/business`
- `/sla-operations/enterprise`
- `/sla-operations/sla-regulations`
---
# 3.3 INCIDENT RESPONSE
## `/incident-response/`
### Описание:
```text
Экстренное устранение неисправностей систем безопасности.
Сценарии:
- отказ видеонаблюдения
- потеря архива
- сбой СКУД
- проблемы пожарной сигнализации
```
---
### Подразделы:
- `/incident-response/critical`
- `/incident-response/emergency-call`
- `/incident-response/post-incident-report`
---
# 3.4 TECHNICAL SUPERVISION
## `/technical-supervision/`
### Описание:
```text
Контроль подрядчиков и проверка качества работ сторонних организаций.
Мы выступаем как независимый инженерный контроль объекта.
```
---
### Подразделы:
- `/technical-supervision/contractor-check`
- `/technical-supervision/acceptance-testing`
- `/technical-supervision/project-review`
---
# 3.5 DOCUMENTATION & COMPLIANCE
## `/documentation-compliance/`
### Описание:
```text
Ведение и восстановление инженерной документации объектов.
Системы:
- схемы
- журналы
- паспорта оборудования
- регламенты эксплуатации
```
---
### Подразделы:
- `/documentation-compliance/passport`
- `/documentation-compliance/regulations`
- `/documentation-compliance/reporting`
---
# 3.6 RISK ENGINEERING
## `/risk-engineering/`
### Описание:
```text
Анализ рисков эксплуатации инженерных систем безопасности объекта.
Мы оцениваем не оборудование, а последствия его отказа.
```
---
### Подразделы:
- `/risk-engineering/video-risk`
- `/risk-engineering/fire-risk`
- `/risk-engineering/access-risk`
---
# 4. ШАБЛОНЫ ДОКУМЕНТОВ (ЭТО ВАЖНЕЕ САЙТА)
Это твой “продукт доверия”.
---
# 4.1 ИНЖЕНЕРНОЕ ЗАКЛЮЧЕНИЕ
```text
AegisOne Engineering
Инженерное заключение по состоянию систем безопасности
Объект: __________
Дата: __________
1. Проведённые проверки:
- видеонаблюдение
- СКУД
- пожарная сигнализация
- инфраструктура
2. Выявленные отклонения:
- __________
- __________
3. Критические риски:
- __________
4. Оценка состояния системы:
[ ] стабильная
[ ] частично стабильная
[ ] требует вмешательства
5. Заключение инженера:
Система безопасности объекта требует/не требует
технического сопровождения по SLA модели эксплуатации.
```
---
# 4.2 SLA СОГЛАШЕНИЕ
```text
AegisOne Engineering
Service Level Agreement (SLA)
1. Объект обслуживания
2. Перечень систем
3. Время реакции:
P1 — 2 часа
P2 — 4 часа
P3 — 24 часа
4. Регламент обслуживания:
- ежемесячные проверки
- отчётность
- диагностика
5. Зоны ответственности:
- заказчик
- инженерная компания
6. Исключения:
- повреждение третьими лицами
- форс-мажор
```
---
# 4.3 АКТ ТЕХНИЧЕСКОГО АУДИТА
```text
Проверено:
- видеонаблюдение
- СКУД
- пожарная система
Результаты:
- __________
Рекомендации:
- критические
- важные
- плановые
```
---
# 4.4 ПАСПОРТ ОБЪЕКТА
```text
AegisOne Engineering
Паспорт инженерных систем объекта
Содержит:
- схема систем
- оборудование
- IP-адреса
- точки отказа
- история обслуживания
```
---
# 4.5 ОТЧЁТ ПО ИНЦИДЕНТУ
```text
Описание инцидента:
Время реакции:
Причина:
Последствия:
Устранение:
Рекомендации:
```
---
# 5. СЦЕНАРИИ ВЗАИМОДЕЙСТВИЯ
---
## СЦЕНАРИЙ 1 — ВХОД
```text
Контакт → мини-диагностика → аудит → отчет → SLA
```
---
## СЦЕНАРИЙ 2 — АВАРИЯ
```text
Инцидент → выезд → устранение → отчет → предложение SLA
```
---
## СЦЕНАРИЙ 3 — ПАРТНЁРСКИЙ ВХОД
```text
Партнёр → объект → аудит → сопровождение
```
---
## СЦЕНАРИЙ 4 — ТЕНДЕР / КОММЕРЧЕСКИЙ ВХОД
```text
Запрос → аудит → КП → SLA контракт
```
---
# 6. КАК ЭТО ВОСПРИНИМАЕТСЯ РЫНКОМ
Ты становишься не:
❌ монтажной фирмой
❌ сервисниками
❌ подрядчиком
---
А:
# 🧠 инженерной эксплуатационной системой объектов
---
# 7. ГЛАВНЫЙ ЭФФЕКТ ЭТОЙ УПАКОВКИ
## Клиент думает:
> “Это не монтажники. Это те, кто контролирует безопасность объекта как систему.”
---
# 8. СЛЕДУЮЩИЙ УРОВЕНЬ (ЕСЛИ ПРОДОЛЖАЕМ)
Я могу собрать тебе дальше:
- автоматическую CRM (Notion / 1C модель под эти разделы)
- шаблон коммерческого предложения под каждый блок
- визуальную архитектуру сайта (UI/UX)
- систему KPI инженеров
- модель масштабирования до федерального уровня
---
# ИТОГ
Ты только что получил:
> полноценную архитектуру инженерной компании уровня enterprise
И если это внедрить — ты перестаёшь быть “подрядчиком”.
Ты становишься:
# операционной системой эксплуатации объектов безопасности.
@@ -0,0 +1,459 @@
Отлично, теперь мы фиксируем архитектуру компании как инженерной системы, а не сайта.
Ты строишь не “фирму услуг”, а:
AegisOne Engineering — операционную систему эксплуатации объектов
Ниже даю тебе:
1. структуру компании (как она “разложена в голове клиента”)
2. все разделы сайта / системы
3. документы (шаблоны, которые продают SLA сами)
4. сценарии взаимодействия с клиентом
5. стандарты (как ты выглядишь как федеральный подрядчик)
---
1. СТРУКТУРА AEGISONE ENGINEERING
🧠 ЯДРО КОМПАНИИ
AegisOne Engineering
> Инженерная компания по эксплуатации и контролю систем безопасности объектов
---
ФУНКЦИОНАЛЬНЫЕ БЛОКИ
1. Engineering Audit (аудит и диагностика)
2. SLA Operations (эксплуатация)
3. Incident Response (аварийные работы)
4. Technical Supervision (контроль подрядчиков)
5. Documentation & Compliance (документация)
6. Risk Engineering (анализ рисков)
---
2. СТРУКТУРА САЙТА (КАК ДОЛЖЕН ВЫГЛЯДЕТЬ AEGISONE)
---
🏠 ГЛАВНАЯ
/
Название:
AegisOne Engineering
Смысл:
> инженерная эксплуатация систем безопасности объектов
---
3. РАЗДЕЛЫ САЙТА
---
3.1 ENGINEERING AUDIT
/engineering-audit/
Описание:
Технический аудит систем безопасности с оценкой рисков эксплуатации.
Проверка:
- видеонаблюдения
- СКУД
- пожарной сигнализации
- инфраструктуры объекта
Результат:
инженерное заключение и карта рисков объекта
---
Подразделы:
/engineering-audit/video-surveillance
/engineering-audit/access-control
/engineering-audit/fire-alarm
/engineering-audit/risk-report
---
3.2 SLA OPERATIONS
/sla-operations/
Описание:
Постоянное техническое сопровождение объектов с регламентами и SLA.
Мы обеспечиваем:
- стабильную работу систем
- контроль состояния оборудования
- аварийное реагирование
- регулярные проверки
---
Подразделы:
/sla-operations/start
/sla-operations/business
/sla-operations/enterprise
/sla-operations/sla-regulations
---
3.3 INCIDENT RESPONSE
/incident-response/
Описание:
Экстренное устранение неисправностей систем безопасности.
Сценарии:
- отказ видеонаблюдения
- потеря архива
- сбой СКУД
- проблемы пожарной сигнализации
---
Подразделы:
/incident-response/critical
/incident-response/emergency-call
/incident-response/post-incident-report
---
3.4 TECHNICAL SUPERVISION
/technical-supervision/
Описание:
Контроль подрядчиков и проверка качества работ сторонних организаций.
Мы выступаем как независимый инженерный контроль объекта.
---
Подразделы:
/technical-supervision/contractor-check
/technical-supervision/acceptance-testing
/technical-supervision/project-review
---
3.5 DOCUMENTATION & COMPLIANCE
/documentation-compliance/
Описание:
Ведение и восстановление инженерной документации объектов.
Системы:
- схемы
- журналы
- паспорта оборудования
- регламенты эксплуатации
---
Подразделы:
/documentation-compliance/passport
/documentation-compliance/regulations
/documentation-compliance/reporting
---
3.6 RISK ENGINEERING
/risk-engineering/
Описание:
Анализ рисков эксплуатации инженерных систем безопасности объекта.
Мы оцениваем не оборудование, а последствия его отказа.
---
Подразделы:
/risk-engineering/video-risk
/risk-engineering/fire-risk
/risk-engineering/access-risk
---
4. ШАБЛОНЫ ДОКУМЕНТОВ (ЭТО ВАЖНЕЕ САЙТА)
Это твой “продукт доверия”.
---
4.1 ИНЖЕНЕРНОЕ ЗАКЛЮЧЕНИЕ
AegisOne Engineering
Инженерное заключение по состоянию систем безопасности
Объект: __________
Дата: __________
1. Проведённые проверки:
- видеонаблюдение
- СКУД
- пожарная сигнализация
- инфраструктура
2. Выявленные отклонения:
- __________
- __________
3. Критические риски:
- __________
4. Оценка состояния системы:
[ ] стабильная
[ ] частично стабильная
[ ] требует вмешательства
5. Заключение инженера:
Система безопасности объекта требует/не требует
технического сопровождения по SLA модели эксплуатации.
---
4.2 SLA СОГЛАШЕНИЕ
AegisOne Engineering
Service Level Agreement (SLA)
1. Объект обслуживания
2. Перечень систем
3. Время реакции:
P1 — 2 часа
P2 — 4 часа
P3 — 24 часа
4. Регламент обслуживания:
- ежемесячные проверки
- отчётность
- диагностика
5. Зоны ответственности:
- заказчик
- инженерная компания
6. Исключения:
- повреждение третьими лицами
- форс-мажор
---
4.3 АКТ ТЕХНИЧЕСКОГО АУДИТА
Проверено:
- видеонаблюдение
- СКУД
- пожарная система
Результаты:
- __________
Рекомендации:
- критические
- важные
- плановые
---
4.4 ПАСПОРТ ОБЪЕКТА
AegisOne Engineering
Паспорт инженерных систем объекта
Содержит:
- схема систем
- оборудование
- IP-адреса
- точки отказа
- история обслуживания
---
4.5 ОТЧЁТ ПО ИНЦИДЕНТУ
Описание инцидента:
Время реакции:
Причина:
Последствия:
Устранение:
Рекомендации:
---
5. СЦЕНАРИИ ВЗАИМОДЕЙСТВИЯ
---
СЦЕНАРИЙ 1 — ВХОД
Контакт → мини-диагностика → аудит → отчет → SLA
---
СЦЕНАРИЙ 2 — АВАРИЯ
Инцидент → выезд → устранение → отчет → предложение SLA
---
СЦЕНАРИЙ 3 — ПАРТНЁРСКИЙ ВХОД
Партнёр → объект → аудит → сопровождение
---
СЦЕНАРИЙ 4 — ТЕНДЕР / КОММЕРЧЕСКИЙ ВХОД
Запрос → аудит → КП → SLA контракт
---
6. КАК ЭТО ВОСПРИНИМАЕТСЯ РЫНКОМ
Ты становишься не:
❌ монтажной фирмой
❌ сервисниками
❌ подрядчиком
---
А:
🧠 инженерной эксплуатационной системой объектов
---
7. ГЛАВНЫЙ ЭФФЕКТ ЭТОЙ УПАКОВКИ
Клиент думает:
> “Это не монтажники. Это те, кто контролирует безопасность объекта как систему.”
---
8. СЛЕДУЮЩИЙ УРОВЕНЬ (ЕСЛИ ПРОДОЛЖАЕМ)
Я могу собрать тебе дальше:
автоматическую CRM (Notion / 1C модель под эти разделы)
шаблон коммерческого предложения под каждый блок
визуальную архитектуру сайта (UI/UX)
систему KPI инженеров
модель масштабирования до федерального уровня
---
ИТОГ
Ты только что получил:
> полноценную архитектуру инженерной компании уровня enterprise
И если это внедрить — ты перестаёшь быть “подрядчиком”.
Ты становишься:
операционной системой эксплуатации объектов безопасности.
+543
View File
@@ -0,0 +1,543 @@
Хороший вопрос — блог в твоей нише (системы безопасности + SLA + B2B + обслуживание) это не “контент-маркетинг”, а:
> инструмент продажи доверия, SLA и инженерной компетентности
Если блог сделан правильно, он заменяет:
отдел продаж (частично)
коммерческие предложения (частично)
презентации
кейсы
и даже тендерные обоснования
---
1. ГЛАВНАЯ ОШИБКА БОЛЬШИНСТВА БЛОГОВ В ТВОЕЙ НИШЕ
Они делают:
“мы установили систему”
“мы молодцы”
“как выбрать камеру”
“новости компании”
👉 директору (заказчику B2B) это НЕ интересно
---
2. ЧТО ХОЧЕТ ВИДЕТЬ ДИРЕКТОР (ЗАКАЗЧИК B2B)
Директору не нужен контент.
Ему нужно:
1. Снижение риска
“у меня не сломается объект”
“меня не оштрафуют”
“у меня будет доказательная база”
---
2. Управляемость
SLA
контроль
прозрачность
ответственность
---
3. Деньги и потери
сколько стоит простой
сколько стоит отказ системы
где риски потерь
---
4. Компетентность подрядчика
понимает ли подрядчик реальные риски
умеет ли он управлять системой, а не “чинить камеры”
---
3. КАК ДОЛЖЕН БЫТЬ УСТРОЕН БЛОГ (АРХИТЕКТУРА)
BLOG AEGISONE
1. Risk Engineering (риски объектов)
2. SLA & Service (обслуживание)
3. Incident Cases (разбор аварий)
4. Infrastructure Deep Dive (техническая экспертиза)
5. Compliance / MCHS / Audit (регуляторика)
6. Economics of Security (экономика потерь)
7. Case Studies (реальные объекты)
---
4. КАТЕГОРИИ БЛОГА (ПОЛНАЯ СТРУКТУРА)
---
4.1 RISK ENGINEERING (САМАЯ ВАЖНАЯ)
Суть:
Показывает директору:
> где он уже теряет деньги, даже если всё “работает”
---
Примеры статей:
“Почему 70% объектов теряют видеодоказательства и не знают об этом”
“Что происходит, когда SLA пожарной системы формальный”
“7 скрытых точек отказа в СКУД”
---
Формат:
риск
последствия
реальные сценарии
стоимость потерь
---
Почему это продаёт:
👉 вызывает страх + осознание потерь
---
4.2 SLA & SERVICE (ТВОЯ ДЕНЕЖНАЯ КАТЕГОРИЯ)
Суть:
объясняет, почему SLA = бизнес-стабильность
---
Примеры:
“Почему обслуживание пожарной сигнализации — это не обслуживание, а юридическая ответственность”
“Как SLA снижает риск остановки бизнеса”
“Почему разовые выезды не работают”
---
Формат:
объяснение SLA
цифры
последствия без SLA
кейсы
---
4.3 INCIDENT CASES (СИЛЬНЕЙШИЙ ДОВЕРИТЕЛЬНЫЙ БЛОК)
Суть:
реальные аварии
---
Примеры:
“Как один отказ СКУД остановил склад на 14 часов”
“Пожарная система без регламента: разбор инцидента”
“Почему не работал архив видеонаблюдения 3 месяца”
---
Структура:
1. Что случилось
2. Почему это произошло
3. Какие были последствия
4. Как это выявили
5. Как исправили
6. Как избежать
---
Это:
👉 главный доверительный инструмент
---
4.4 INFRASTRUCTURE DEEP DIVE
Суть:
показывает инженерную глубину
---
Примеры:
“Как устроена современная система видеонаблюдения на объекте 10 000 м²”
“Почему сеть — это главный риск безопасности”
“UPS как критический элемент безопасности”
---
4.5 COMPLIANCE / MCHS
Суть:
работа с нормативкой
---
Примеры:
“Что проверяет МЧС в 2026 году”
“Типовые ошибки объектов при проверках”
“Почему формальное ТО приводит к штрафам”
---
4.6 ECONOMICS OF SECURITY
Суть:
деньги, потери, риск
---
Примеры:
“Сколько стоит 1 час простоя склада”
“Стоимость потери видеодоказательства”
“Почему дешёвый подрядчик обходится дороже”
---
4.7 CASE STUDIES (ОСНОВА ПРОДАЖ)
Суть:
твои реальные объекты
---
Формат:
объект
проблемы
что сделали
результат
SLA модель
---
5. КАК ДОЛЖЕН ВЫГЛЯДЕТЬ БЛОГ (UI/UX)
---
5.1 НЕ ДОЛЖНО БЫТЬ:
❌ “новости компании”
❌ “мы молодцы”
❌ маркетинговых текстов
❌ воды
---
5.2 ДОЛЖНО БЫТЬ:
Главная страница блога:
“инженерные риски”
“разбор инцидентов”
“стоимость отказов”
“кейсы объектов”
---
5.3 Визуально:
строгий технический стиль
таблицы
схемы
диаграммы
риск-блоки
---
6. КАЖДАЯ СТАТЬЯ ДОЛЖНА ПРОДАВАТЬ
---
структура статьи (обязательная):
1. Проблема
2. Реальный сценарий
3. Что происходит технически
4. Риски
5. Финансовые последствия
6. Как решается
7. Как это делает AegisOne
---
7. ПРИМЕР “ПРОДАЮЩЕЙ” СТАТЬИ
---
Заголовок:
> “Почему 80% систем видеонаблюдения не дают доказательств в момент инцидента”
---
Внутри:
потеря архива
сбои HDD
неправильная настройка
отсутствие контроля
---
Финал:
> “в AegisOne это решается через SLA мониторинг и инженерный контроль”
---
8. КАК БЛОГ ПРИВОДИТ КЛИЕНТА
---
Статья → Осознание риска → Страх потерь → Аудит → SLA контракт
---
9. ГЛАВНАЯ ЛОГИКА БЛОГА
Ты не пишешь статьи.
Ты создаёшь:
> систему формирования потребности в SLA
---
10. ЧТО ДЕЛАЕТ БЛОГ СИЛЬНЫМ (КРИТИЧНО)
---
1. РЕАЛЬНЫЕ ЦИФРЫ
потери
простои
риски
---
2. РЕАЛЬНЫЕ ИНЦИДЕНТЫ
аварии
сбои
последствия
---
3. ИНЖЕНЕРНАЯ ГЛУБИНА
схемы
логика систем
причины отказов
---
4. ЭКОНОМИКА
сколько стоит ошибка
---
11. ЧТО НЕЛЬЗЯ ДЕЛАТЬ
---
❌ SEO-статьи “для трафика”
❌ поверхностные обзоры
❌ “лайфхаки безопасности”
❌ маркетинг
---
12. ИТОГ
Правильный блог в твоей модели — это:
> “система инженерного страха + доверия + экспертизы”
---
13. ЕСЛИ РАЗВИВАТЬ ДАЛЬШЕ
Я могу дальше собрать:
30 готовых статей под твой блог (с заголовками + структурами)
SEO-структуру под Краснодарский край
Telegram-воронку из блога
автоматическое превращение статей в заявки на аудит
систему “контент → SHS влияние”
---
Если коротко:
👉 блог в твоей модели — это не контент
👉 это “машина продажи SLA через понимание риска”
+445
View File
@@ -0,0 +1,445 @@
Ок, здесь мы уже собираем не “маркетинг”, а полноценную B2B-машину:
# вход → доверие → аудит → SLA → долгий повторяющийся доход
Ты как раз в нише, где деньги делаются не на разовых монтажах, а на:
> эксплуатации + рисках + ответственности
---
# ЧАСТЬ 1. ПЕРВАЯ ВОРОНКА КЛИЕНТОВ (КРАСНОДАРСКИЙ КРАЙ)
---
# 0. ЛОГИКА ВОРОНКИ
Твоя воронка НЕ классическая:
❌ “реклама → заявка → продажа”
У тебя:
# контент → доверие → аудит → выявление проблем → SLA
---
# 1. ВХОД В ВОРОНКУ (ТОЧКИ ПРИКОСНОВЕНИЯ)
## 1.1. Локальный B2B рынок (основа)
Ты работаешь не по всей РФ, а:
- Краснодар
- Сочи
- Новороссийск
- Анапа
- Армавир
- промзоны + логистика + гостиницы
---
## 1.2. Каналы входа (без агрессивного маркетинга)
### 1) Telegram (основной канал доверия)
Контент + кейсы + разборы
---
### 2) Яндекс / Google SEO
Страницы:
- аудит систем безопасности
- SLA обслуживание
- обслуживание видеонаблюдения Краснодар
- обслуживание пожарной сигнализации
---
### 3) Партнеры (очень важно)
- электрики
- IT-аутсорс
- управляющие компании
- проектировщики
- пожарники
- строители
---
### 4) Прямые инженерные выходы
НЕ продажи.
Формат:
> “если нужен технический аудит или проверка систем — можем подключиться”
---
# 2. ПЕРВЫЙ ПРОДУКТ (ВХОД В ДЕНЬГИ)
# 🔥 Технический аудит объекта
---
## Почему это ядро воронки
Потому что:
- легко продать без давления
- низкий чек
- высокая ценность
- выявляет проблемы
- логично ведёт в SLA
---
## Цена (Краснодарский край)
- малый объект: 15–25 тыс
- средний: 25–50 тыс
- крупный: 50–100 тыс
---
## Что важно
Аудит НЕ продаёт услуги.
Он:
# показывает проблемы → создаёт необходимость SLA
---
# 3. ВТОРОЙ ЭТАП
# 🔥 “Диагностика + отчет”
После аудита:
Ты выдаешь документ:
- риски
- неисправности
- слабые места
- последствия
- рекомендации
---
## Ключевая мысль отчета:
> “Система формально работает, но не защищает объект”
---
# 4. ТРЕТИЙ ЭТАП
# 🔥 ПЕРЕХОД В SLA
---
## Это главный момент денег
Ты НЕ продаешь обслуживание.
Ты продаешь:
# “устранение рисков эксплуатации”
---
## Как выглядит оффер
> Мы берем объект на техническое сопровождение с SLA контролем состояния систем безопасности.
---
## Конверсия
Из аудита → в SLA:
- 30% слабые объекты
- 50% средние
- 10–20% сильные (очень хорошие деньги)
---
# 5. SLA (ПРОДУКТ ДЕНЕГ)
---
## Структура:
### START
2540k
---
### BUSINESS
60120k
---
### ENTERPRISE
150400k
---
## Что ты продаешь на самом деле:
НЕ обслуживание
А:
# “инженерную стабильность объекта”
---
# 6. ПОЛНАЯ ВОРОНКА В ВИДЕ СХЕМЫ
```
Контент / SEO / партнеры
Лид (запрос)
Мини-аудит (или первичная диагностика)
Полный технический аудит
Отчет с рисками
SLA предложение
Долгосрочный контракт
```
---
# ЧАСТЬ 2. МОДЕЛЬ РОСТА ДО 2–3 МЛН ₽/МЕС RECURRENT
---
# 1. КЛЮЧЕВОЕ ПРАВИЛО
Ты не масштабируешь монтаж.
Ты масштабируешь:
# количество объектов на SLA
---
# 2. МАТЕМАТИКА МОДЕЛИ
---
## Вариант реалистичный (Краснодар → край → РФ)
### Средний чек SLA:
80 000 ₽
---
## Тебе нужно:
### 2535 объектов
---
## Доход:
```
30 объектов × 80 000 ₽ = 2 400 000 ₽/мес
```
---
# 3. КАК ЭТО ДОСТИГАЕТСЯ
---
## ЭТАП 1 — 0500k
Фокус:
- аудит
- первые SLA
- 57 объектов
---
## ЭТАП 2 — 500k1.5M
Фокус:
- стабильные SLA
- отбор клиентов
- отказ от токсичных объектов
- первые крупные клиенты
---
## ЭТАП 3 — 1.5M3M
Фокус:
- стандартизация
- регламенты
- техподдержка
- 23 инженера
- удаленный контроль
---
# 4. КЛЮЧЕВОЙ РЫЧАГ РОСТА
---
# НЕ реклама
# НЕ продажи
# А:
## “плотность объектов на инженера”
---
# 5. СИСТЕМА РОСТА
---
## 1 инженер = 8–12 объектов
---
## значит:
|Кол-во инженеров|Доход|
|---|---|
|1|0.81.2M|
|2|1.62.4M|
|3|2.43.6M|
---
# 6. ГЛАВНЫЕ РЫЧАГИ МАСШТАБА
---
## 1. Аудит как продукт входа
(дешево / легко / продается сам)
---
## 2. SLA как подписка
(деньги каждый месяц)
---
## 3. Контент как доверие
(заменяет рекламу)
---
## 4. Партнерки
(дают теплые объекты)
---
## 5. Отказ от дешевых клиентов
(самый важный фактор)
---
# 7. КРИТИЧЕСКИЙ МОМЕНТ
---
Если ты не сделаешь это:
> “мы не берем монтаж без SLA”
ты останешься в рынке:
- демпинга
- разовых работ
- кассовых разрывов
---
# 8. ИДЕАЛЬНАЯ ЭВОЛЮЦИЯ БИЗНЕСА
---
## Было:
монтажники
---
## Стало:
SLA + эксплуатация
---
## Будет:
# внешний инженерный отдел объектов
---
# 9. СИЛЬНЫЙ СДВИГ МЫШЛЕНИЯ
---
Ты больше не продаешь:
❌ камеры
❌ монтаж
❌ оборудование
---
Ты продаешь:
# “спокойствие собственника за техническую инфраструктуру объекта”
---
# ЕСЛИ СЖАТЬ В ОДНУ ФОРМУЛУ:
---
> Аудит → выявление рисков → SLA → долгосрочный инженерный доход
---
# ЕСЛИ ДАЛЬШЕ ДВИГАТЬСЯ
Я могу следующим шагом собрать тебе:
- точную систему продаж аудита (скрипты + сообщения)
- упаковку Telegram как лидогенератора
- CRM воронку под SLA
- регламент инженера (чтобы масштабироваться)
- финансовую модель до 5–10 млн/мес
Это уже будет не “бизнес идея”, а:
# операционная система инженерной компании.
@@ -0,0 +1,397 @@
Отлично, это уже уровень, где бизнес начинает “дышать сам”:
Telegram → доверие → аудит → SLA → регулярный доход.
Разобьём на две системы:
# 1) Telegram как лидогенератор (не блог, а воронка)
# 2) CRM-воронка под SLA (повторяемые деньги)
---
# 1. TELEGRAM КАК ЛИДОГЕНЕРАТОР (ИНЖЕНЕРНАЯ МОДЕЛЬ)
---
## 1.1. Главная ошибка всех
Обычно делают:
- “посты про монтаж”
- “новости компании”
- “фото объектов”
👉 это НЕ генерирует SLA
---
## 1.2. Твоя роль канала
Твой Telegram — это:
# “инженерный журнал рисков объектов”
Не маркетинг. Не реклама. Не блог.
---
## 1.3. Главная цель Telegram
Не подписки.
А:
> заявки на аудит
---
# 2. СТРУКТУРА TELEGRAM-КАНАЛА
---
## 2.1. Закреп (ОБЯЗАТЕЛЬНО)
```text
Мы занимаемся инженерной эксплуатацией систем безопасности коммерческих объектов.
Перед обслуживанием всегда проводим технический аудит:
— видеонаблюдение
— СКУД
— пожарная сигнализация
— инфраструктура
Цель — выявить скрытые риски, которые не видны в ежедневной работе.
📌 Если нужно — можем провести аудит объекта и выдать инженерное заключение.
Написать: @your_contact
```
---
## 2.2. 3 типа контента (ВСЕГДА)
---
### 1) РИСКИ (60%)
👉 основа роста
Примеры:
- “Почему архив видеонаблюдения исчезает незаметно”
- “Почему пожарка работает, но не защищает”
- “3 скрытые ошибки СКУД”
---
### 2) КЕЙСЫ (25%)
👉 доверие
Примеры:
- “Аудит склада: 12 критических ошибок”
- “Гостиница без резервного питания”
---
### 3) ЭКСПЕРТИЗА (15%)
👉 статус
Примеры:
- “Как должен выглядеть нормальный SLA”
- “Что проверяет инженер, а не монтажник”
---
# 2.3. ФОРМАТ ПОСТОВ (СТАНДАРТ)
```text
1. Проблема
2. Инженерное объяснение
3. Последствия для бизнеса
4. Спокойный вывод
```
---
## Пример готового поста
```text
Большинство объектов уверены, что видеонаблюдение работает корректно.
На практике часто есть скрытая проблема — архив.
Камеры могут показывать картинку, но запись:
— не сохраняется полностью
— перезаписывается раньше срока
— теряет часть каналов
— сбоит из-за питания или дисков
Это не видно в ежедневной работе.
Проблема проявляется только после инцидента.
По сути, система существует, но не выполняет свою функцию хранения доказательной базы.
Именно поэтому видеонаблюдение нужно рассматривать как систему эксплуатации, а не просто установленное оборудование.
```
---
# 2.4. CTA (ОЧЕНЬ ВАЖНО)
Каждый 34 пост:
```text
Если нужно — можем провести технический аудит объекта и проверить состояние систем безопасности.
```
---
# 2.5. ЛИД-МАГНИТ (ОЧЕНЬ СИЛЬНЫЙ РЫЧАГ)
---
## PDF:
# “Чек-лист скрытых рисков систем безопасности”
или
# “10 ошибок эксплуатации объектов”
---
## Telegram пост:
```text
Мы собрали чек-лист из 10 критических ошибок, которые встречаются на большинстве объектов.
Если хотите — отправим бесплатно.
```
👉 это даёт первые контакты без давления
---
# 3. TELEGRAM → ВОРОНКА
```text
Контент
Интерес
Чек-лист / диалог
Диагностика (мини-аудит)
Полный аудит
SLA
```
---
# 4. CRM-ВОРОНКА ПОД SLA
Теперь самое важное.
Ты не продаёшь “услуги”. Ты ведёшь объекты по состояниям.
---
# 4.1. СТРУКТУРА CRM (СТАДИИ)
---
## 1. ЛИД
Источник:
- Telegram
- сайт
- партнер
- холодный контакт
---
## 2. ПЕРВИЧНЫЙ КОНТАКТ
- написал / ответил
- уточнение объекта
---
## 3. КВАЛИФИКАЦИЯ
Вопросы:
- тип объекта
- системы
- есть ли подрядчик
- проблемы
---
## 4. ДИАГНОСТИКА (мини-аудит)
- быстрый осмотр
- удаленно или выезд
- выявление боли
---
## 5. ПОЛНЫЙ АУДИТ
- инженерный выезд
- отчет
- риски
---
## 6. ПРЕДЛОЖЕНИЕ SLA
- тариф
- регламент
- реакция
---
## 7. ПЕРЕГОВОРЫ
- согласование цены
- условия
---
## 8. ДОГОВОР SLA
- подписан
- объект активен
---
## 9. АКТИВНЫЙ SLA
- обслуживание
- отчёты
---
## 10. РАСШИРЕНИЕ
- новые объекты
- доп. системы
---
# 4.2. ВИЗУАЛЬНАЯ CRM-СХЕМА
```text
ЛИД
КОНТАКТ
КВАЛИФИКАЦИЯ
МИНИ-ДИАГНОСТИКА
АУДИТ
SLA ОФФЕР
ДОГОВОР
ОБСЛУЖИВАНИЕ
РАСШИРЕНИЕ
```
---
# 5. ПРОСТАЯ CRM В ТАБЛИЦЕ (СТАРТ)
Если без софта:
|Клиент|Объект|Статус|Проблема|Этап|Потенциал|
|---|---|---|---|---|---|
|X|гостиница|аудит|нет архива|прогрев|80k/мес|
---
# 6. КЛЮЧЕВОЙ ПРИНЦИП CRM
Ты не ведёшь “продажи”.
Ты ведёшь:
# инженерные объекты
---
# 7. МЕТРИКА УСПЕХА (ВАЖНО)
Не:
- подписчики
- заявки
А:
## 1 объект = SLA контракт
---
# 8. РОЛЬ TELEGRAM В МОДЕЛИ ДЕНЕГ
```text
Telegram = генератор доверия
Аудит = генератор денег входа
SLA = генератор повторного дохода
```
---
# 9. СИСТЕМА РОСТА (СВЯЗКА ВСЕГО)
```text
Контент (Telegram)
Диалог
Аудит
SLA контракт
Постоянный доход
Масштаб через инженеров
```
---
# 10. СЛЕДУЮЩИЙ УРОВЕНЬ (если хочешь усилить систему)
Я могу дальше собрать тебе:
### 1. Полную CRM в Notion / 1C структуру под SLA
### 2. Скрипт “закрытия в SLA после аудита”
### 3. Таблицу квалификации клиента (кого брать/кого нет)
### 4. Финмодель роста до 3 млн+
### 5. Регламент инженера (чтобы масштабировать команду)
---
Если коротко: ты сейчас строишь не маркетинг, а:
> инженерную подписочную сервисную компанию
И Telegram + CRM — это её нервная система.
@@ -0,0 +1,537 @@
Ниже — полноценная модель **“Дашборд инженера (тимлид / руководитель техников)”** для AegisOne Engineering.
Это ключевой слой между:
- техник → (исполнение)
- директор → (управление системой)
Инженер тут не “старший техник”, а:
> операционный контроллер SLA, качества и рисков на объектах
---
# 1. РОЛЬ ИНЖЕНЕРА В СИСТЕМЕ
## 1.1 Основная функция
```text
Инженер = управление качеством исполнения SLA + распределение нагрузки + контроль рисков объектов
```
---
## 1.2 Он НЕ делает:
- не чинит оборудование (это техник)
- не продаёт SLA (это коммерция)
- не ведёт бухгалтерию
---
## 1.3 Он ДЕЛАЕТ:
- распределяет заявки
- контролирует SLA
- проверяет качество работ
- снижает инциденты
- управляет техниками
- влияет на SHS
---
# 2. АРХИТЕКТУРА DASHBOARD ИНЖЕНЕРА
```text
CEO DASHBOARD
ENGINEER DASHBOARD
TECHNICIAN DASHBOARD
OBJECT SYSTEMS
```
---
# 3. DASHBOARD ИНЖЕНЕРА (REAL-TIME)
## 3.1 Главные блоки интерфейса
### Блок 1 — SLA контроль региона
- SLA compliance (%)
- просроченные заявки
- риск нарушения SLA
- критические объекты
---
### Блок 2 — команда техников
- загрузка каждого техника
- эффективность
- ошибки / reopen rate
- скорость реакции
---
### Блок 3 — инциденты
- P1 / P2 / P3
- открытые критические
- повторяющиеся аварии
---
### Блок 4 — объекты
- риск-уровень (Risk Score)
- SHS влияние объекта
- проблемные системы
---
### Блок 5 — KPI инженера
- качество управления SLA
- распределение нагрузки
- снижение инцидентов
- эффективность команды
---
# 4. PHP АРХИТЕКТУРА
---
## 4.1 API инженера
```php
<?php
header('Content-Type: application/json');
$pdo = new PDO("mysql:host=localhost;dbname=aegisone","user","pass");
$data = $pdo->query("
SELECT
e.id,
e.name,
AVG(t.sla_compliance) as sla,
COUNT(i.id) as incidents,
AVG(t.response_time) as response_time
FROM engineers e
LEFT JOIN tasks t ON t.engineer_id = e.id
LEFT JOIN incidents i ON i.engineer_id = e.id
GROUP BY e.id
")->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($data);
?>
```
---
## 4.2 SHS ENGINEER IMPACT API
```php
<?php
header('Content-Type: application/json');
$pdo = new PDO("mysql:host=localhost;dbname=aegisone","user","pass");
$k = $pdo->query("
SELECT
AVG(sla_compliance) as sla,
AVG(response_time) as rt,
AVG(reopen_rate) as rr
FROM tasks
")->fetch(PDO::FETCH_ASSOC);
/* Engineer Impact on SHS */
$EIS =
(0.5 * $k['sla']) +
(0.3 * (1 - $k['rr'])) +
(0.2 * (1 - $k['rt']/10));
echo json_encode([
"ENGINEER_IMPACT_SCORE" => round($EIS * 100, 2)
]);
?>
```
---
# 5. ФОРМУЛЫ ИНЖЕНЕРА (КРИТИЧЕСКИЕ KPI)
---
## 5.1 ENGINEER CONTROL SCORE (ECS)
```text
ECS =
(0.30 × SLA Compliance Control)
+ (0.25 × Task Distribution Efficiency)
+ (0.20 × Incident Reduction Rate)
+ (0.15 × Team Performance Score)
+ (0.10 × Response Coordination Speed)
```
---
## 5.2 SLA CONTROL INDEX
```text
SCI = closed_tasks_in_SLA / total_tasks
```
---
## 5.3 TEAM LOAD BALANCE
```text
TLB = std_dev(tasks_per_engineer)
```
👉 чем меньше → тем лучше распределение
---
## 5.4 INCIDENT REDUCTION RATE
```text
IRR = (incidents_previous - incidents_current) / incidents_previous
```
---
## 5.5 ESCALATION RATE
```text
ER = escalated_tasks / total_tasks
```
---
# 6. DASHBOARD (HTML + JS)
---
## 6.1 HTML
```html
<!DOCTYPE html>
<html>
<head>
<title>Engineer Control Panel</title>
<style>
body { background:#0f172a; color:white; font-family:Arial; }
.grid {
display:grid;
grid-template-columns: repeat(3, 1fr);
gap:15px;
padding:20px;
}
.card {
background:#1e293b;
padding:15px;
border-radius:12px;
}
.big { font-size:28px; font-weight:bold; }
.green { color:#22c55e; }
.yellow { color:#facc15; }
.red { color:#ef4444; }
</style>
</head>
<body>
<h2 style="padding:20px;">Engineer Live Dashboard</h2>
<div class="grid">
<div class="card">
<div>SLA Control</div>
<div id="sla" class="big">--</div>
</div>
<div class="card">
<div>Engineer Impact (SHS)</div>
<div id="shs" class="big">--</div>
</div>
<div class="card">
<div>Incidents</div>
<div id="inc" class="big">--</div>
</div>
</div>
<div id="team"></div>
<script src="engineer.js"></script>
</body>
</html>
```
---
## 6.2 JS realtime
```javascript
async function loadEngineer(){
const res = await fetch('/api/engineer.php');
const data = await res.json();
let sla = 0;
let inc = 0;
let html = "";
data.forEach(e => {
sla += parseFloat(e.sla);
inc += parseInt(e.incidents);
let color =
e.sla > 0.9 ? "green" :
e.sla > 0.75 ? "yellow" : "red";
html += `
<div class="card">
<h3>${e.name}</h3>
<p>SLA: <b class="${color}">
${(e.sla * 100).toFixed(1)}%
</b></p>
<p>Incidents: ${e.incidents}</p>
<p>Response: ${e.response_time}</p>
</div>
`;
});
document.getElementById("team").innerHTML = html;
document.getElementById("sla").innerText =
((sla / data.length) * 100).toFixed(1) + "%";
document.getElementById("inc").innerText = inc;
let shs = (sla / data.length) * 100 - inc * 0.5;
document.getElementById("shs").innerText =
shs.toFixed(1);
}
setInterval(loadEngineer, 15000);
loadEngineer();
```
---
# 7. ДОКУМЕНТЫ ИНЖЕНЕРА (ОБЯЗАТЕЛЬНЫЕ)
---
## 7.1 РЕГЛАМЕНТ ИНЖЕНЕРА
```text
AegisOne Engineering
Engineer Operational Standard
1. Назначение
Инженер отвечает за выполнение SLA и контроль качества работ техников.
---
2. Обязанности
- распределение заявок
- контроль SLA выполнения
- контроль качества диагностики
- управление инцидентами
- предотвращение повторных аварий
---
3. Запрещено
- закрывать задачи без проверки
- игнорировать SLA нарушения
- делегировать без фиксации
```
---
## 7.2 ESCALATION MATRIX
```text
P1 → инженер → директор (немедленно)
P2 → инженер → инженер контроль
P3 → техник
Если SLA риск > 80%:
→ обязательная эскалация
```
---
## 7.3 ЧЕК-ЛИСТ ИНЖЕНЕРА
```text
AegisOne Engineering
Engineer Control Checklist
[ ] Все заявки распределены
[ ] SLA риск оценён
[ ] Техники назначены
[ ] P1 инциденты закрыты
[ ] Повторные аварии проанализированы
[ ] Отчёты проверены
```
---
## 7.4 ОТЧЁТ ИНЖЕНЕРА
```text
Engineer Report
Период:
Инженер:
---
1. SLA состояние:
-
2. Проблемные объекты:
-
3. Инциденты:
-
4. Перегруз команды:
-
5. Принятые меры:
-
6. Риски:
-
```
---
# 8. ЧТО ИНЖЕНЕР ДОЛЖЕН ИМЕТЬ В СИСТЕМЕ
---
## ОБЯЗАТЕЛЬНО:
- SLA статус всех объектов
- список техников
- инциденты P1P3
- риск объектов
- SHS влияние
---
## РЕКОМЕНДУЕМО:
- история объектов
- схемы систем
- фото оборудования
- прошлые ремонты
- нагрузка по часам
---
# 9. ЧТО ЕМУ НЕЛЬЗЯ ДАВАТЬ
---
## КАТЕГОРИЧЕСКИ НЕ НУЖНО:
- прямые продажи
- финансовые данные прибыли
- доступ к CRM сделкам
- доступ к договорам без SLA слоя
- административные функции директора
---
# 10. РИСКИ СИСТЕМЫ
---
## РИСК 1 — инженер превращается в диспетчера
👉 решение:
- оставить KPI, а не “ручное управление”
---
## РИСК 2 — перегруз инженера
👉 решение:
- TLB (load balance)
---
## РИСК 3 — потеря SLA контроля
👉 решение:
- SHS alert system
---
# 11. ГЛАВНАЯ ЛОГИКА
```text
Техник → выполняет
Инженер → управляет качеством
Директор → управляет системой
```
---
# 12. ИТОГ
Ты получаешь:
> управляемый слой контроля SLA между исполнением и бизнесом
---
# 13. ЕСЛИ РАЗВИВАТЬ ДАЛЬШЕ
Я могу собрать:
- автораспределение заявок (AI dispatch)
- прогноз поломок объектов
- система “умного SLA риска”
- мобильный кабинет инженера
- интеграция с Telegram уведомлениями
---
Если коротко:
👉 это уже не “дашборд”
👉 это уровень операционной системы инженерного бизнеса (mini-SAP под твою нишу)
@@ -0,0 +1,424 @@
Ниже — полноценная система **“Дашборд техника в реальном времени” + пакет документов и инструкций**, который превращает техника не в “исполнителя заявок”, а в **узел управляемой SLA-системы**.
Я разделю на 3 уровня:
1. Дашборд техника (web / PHP система)
2. Документы и регламенты (полные тексты)
3. Шаблоны (акты, чек-листы, отчёты)
---
# 1. DASHBOARD ТЕХНИКА (REAL-TIME)
## 1.1 Логика системы
```text
Заявка → Назначение → Выезд → Диагностика → Исполнение → Закрытие → SLA фиксация → SHS влияние
```
---
## 1.2 Что должен видеть техник (главное)
### Блок 1 — текущая нагрузка
- активные заявки
- срочность (P1P3)
- дедлайн SLA
- расстояние до объекта
- статус выполнения
---
### Блок 2 — SLA контроль
- осталось времени до нарушения SLA
- критичность заявки
- штрафной риск
---
### Блок 3 — оборудование объекта
- камеры / СКУД / пожарка
- история неисправностей
- последние ремонты
---
### Блок 4 — чек-лист работ
- пошаговый регламент
- обязательные проверки
- фотофиксация
---
### Блок 5 — результат
- закрытие заявки
- причина
- подтверждение клиента
- влияние на KPI
---
# 2. PHP ДАШБОРД ТЕХНИКА (ГОТОВЫЙ ШАБЛОН)
---
## 2.1 API задач техника
```php
<?php
header('Content-Type: application/json');
$pdo = new PDO("mysql:host=localhost;dbname=aegisone","user","pass");
$stmt = $pdo->query("
SELECT
t.id,
t.title,
t.priority,
t.status,
t.deadline,
t.address,
t.system_type,
t.sla_remaining_hours
FROM tasks t
WHERE t.status != 'closed'
ORDER BY t.priority DESC, t.deadline ASC
");
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
?>
```
---
## 2.2 FRONTEND (ТЕХНИК DASHBOARD)
```html
<!DOCTYPE html>
<html>
<head>
<title>Technician Panel - AegisOne</title>
<style>
body { background:#0f172a; color:white; font-family:Arial; }
.card { background:#1e293b; padding:15px; margin:10px; border-radius:10px; }
.grid { display:grid; grid-template-columns: repeat(2, 1fr); gap:10px; }
.red { color:#ef4444; }
.yellow { color:#facc15; }
.green { color:#22c55e; }
</style>
</head>
<body>
<h2 style="padding:10px;">Technician Live Dashboard</h2>
<div id="tasks"></div>
<script src="tech.js"></script>
</body>
</html>
```
---
## 2.3 JS realtime обновление
```javascript
async function loadTasks(){
const res = await fetch('/api/tech_tasks.php');
const data = await res.json();
let html = "";
data.forEach(t => {
let color =
t.sla_remaining_hours < 2 ? "red" :
t.sla_remaining_hours < 6 ? "yellow" : "green";
html += `
<div class="card">
<h3>${t.title}</h3>
<p><b>Priority:</b> ${t.priority}</p>
<p><b>Address:</b> ${t.address}</p>
<p><b>System:</b> ${t.system_type}</p>
<p><b>SLA:</b>
<span class="${color}">
${t.sla_remaining_hours}h left
</span>
</p>
<button onclick="startTask(${t.id})">Start</button>
<button onclick="closeTask(${t.id})">Close</button>
</div>
`;
});
document.getElementById("tasks").innerHTML = html;
}
setInterval(loadTasks, 10000);
loadTasks();
```
---
# 3. ДОКУМЕНТЫ ДЛЯ ТЕХНИКА (ОБЯЗАТЕЛЬНЫЕ)
---
# 3.1 РЕГЛАМЕНТ РАБОТЫ ТЕХНИКА
```text
AegisOne Engineering
Регламент работы инженера-техника
1. Общие положения
Техник обязан выполнять работы строго по SLA и чек-листу объекта.
Любое отклонение фиксируется в системе.
---
2. Прибытие на объект
- зарегистрировать прибытие в системе
- подтвердить время начала работ
- сделать фото объекта до начала работ
---
3. Диагностика
- проверить систему по чек-листу
- зафиксировать неисправности
- определить причину (если возможно)
---
4. Выполнение работ
- устранить неисправность
- не изменять конфигурацию без согласования
- использовать только разрешённые материалы
---
5. Завершение работ
- тестирование системы
- фото после выполнения
- подтверждение работоспособности
---
6. Закрытие заявки
- заполнить отчет
- указать причину неисправности
- получить подтверждение клиента (если возможно)
```
---
# 3.2 ЧЕК-ЛИСТ ВЫЕЗДА
```text
AegisOne Engineering
Checklist Technician Visit
[ ] Прибытие на объект зафиксировано
[ ] Фото “до”
[ ] Проверка питания системы
[ ] Проверка камер / датчиков
[ ] Проверка СКУД
[ ] Проверка пожарной панели
[ ] Локализация неисправности
[ ] Устранение проблемы
[ ] Тестирование системы
[ ] Фото “после”
[ ] Закрытие заявки
```
---
# 3.3 СТАНДАРТ ДИАГНОСТИКИ
```text
AegisOne Engineering
Diagnostic Standard
1. Неисправность фиксируется только после проверки:
- питания
- сети
- оборудования
- конфигурации
2. Причина должна быть классифицирована:
- Hardware
- Software
- Network
- Human error
- External factor
3. Запрещено:
- закрывать заявку без причины
- менять систему без фиксации
```
---
# 3.4 ОТЧЁТ ТЕХНИКА (ШАБЛОН)
```text
AegisOne Engineering
Technician Report
Объект:
Дата:
Техник:
---
1. Выполненные работы:
-
2. Выявленные проблемы:
-
3. Причина неисправности:
(Hardware / Software / Network / Other)
4. Что было сделано:
-
5. Результат:
[ ] Исправлено
[ ] Частично исправлено
[ ] Требует повторного выезда
6. Фото до/после:
(вложение)
7. Комментарий:
-
```
---
# 4. ОБЯЗАТЕЛЬНЫЕ ДАННЫЕ ТЕХНИКА В СИСТЕМЕ
---
## 4.1 Минимум данных
- текущие заявки
- SLA таймер
- адрес объекта
- тип системы
- приоритет
---
## 4.2 Рекомендуемые данные
- история объекта
- прошлые неисправности
- схема оборудования
- контакт ответственного
- тип неисправности по базе
---
# 5. КЛЮЧЕВАЯ ЛОГИКА СИСТЕМЫ
```text
Техник не “чинит оборудование”
Техник:
→ закрывает SLA
→ влияет на SHS
→ формирует retention
```
---
# 6. КОНТРОЛЬ РИСКОВ
---
## РИСК 1 — “закрытие без факта”
👉 решение:
- фотофиксация обязательна
---
## РИСК 2 — “ложное устранение”
👉 решение:
- reopen rate KPI
---
## РИСК 3 — перегруз техника
👉 решение:
- utilization tracking
---
## РИСК 4 — нарушение SLA
👉 решение:
- live SLA countdown
---
# 7. ИТОГОВАЯ МОДЕЛЬ
```text
CRM → SLA → TASK → TECH DASHBOARD → EXECUTION → KPI → SHS
```
---
# 8. СУТЬ СИСТЕМЫ
Ты создаёшь не “работу техника”:
> ты создаёшь управляемый процесс физической эксплуатации инфраструктуры бизнеса
---
# 9. ЕСЛИ ДАЛЬШЕ РАЗВИВАТЬ
Можно собрать:
- мобильное приложение техника (PWA)
- GPS контроль выездов
- автоакты выполненных работ
- голосовые отчёты
- AI диагностику неисправностей
- автоматическое распределение заявок
---
Если хочешь следующий шаг:
👉 я могу объединить дашборд техника + инженера + CEO в одну единую систему управления AegisOne (как ERP уровня SAP, но для твоей ниши)
@@ -0,0 +1,574 @@
Ниже — система, которая делает тебя не “компанией по безопасности”, а:
доказательным инженерным экспертом
Это ключевой переход.
Потому что рынок верит не словам, а:
> измерениям, проверкам и воспроизводимым результатам
---
0. ЧТО ТАКОЕ “ДОКАЗАТЕЛЬНАЯ ЭКСПЕРТИЗА”
Это подход, где каждое твоё утверждение:
проверяется тестом
фиксируется
документируется
повторяется на любом объекте
подтверждается результатом
---
1. ГЛАВНАЯ ИДЕЯ СИСТЕМЫ
Ты перестаёшь говорить:
❌ “у вас плохое обслуживание”
❌ “система работает неправильно”
Ты начинаешь говорить:
✔ “мы провели тест №X”
✔ “результат измерен”
✔ “отклонение от нормы”
✔ “риск подтверждён”
---
2. СТРУКТУРА СИСТЕМЫ ДОКАЗАТЕЛЬНОСТИ
3. Тест (что проверяем)
4. Методика (как проверяем)
5. Норма (как должно быть)
6. Фактический результат
7. Отклонение
8. Риск для бизнеса
9. Вывод инженера
---
3. БАЗОВЫЕ ТЕСТЫ (ЯДРО ТВОЕЙ ЭКСПЕРТИЗЫ)
---
ТЕСТ №1 — РЕАЛЬНОСТЬ АРХИВА ВИДЕОНАБЛЮДЕНИЯ
---
🔧 Методика
Выборочно проверяется запись с камер за последние:
- 1 день
- 7 дней
- 14 дней
- 30 дней
Проверяется:
- наличие записи
- непрерывность
- пропуски
- доступность воспроизведения
---
📏 Норма
запись 100% камер
непрерывность без пропусков
доступность архива согласно заявленному сроку
---
📉 Частая реальность
часть камер не пишет
архив “дыры”
перезапись раньше срока
сбои HDD
---
⚠️ Риск
> Потеря доказательной базы при инциденте
---
🧠 Заключение от тебя
> “На объекте система видеонаблюдения формально функционирует, но не гарантирует сохранность событийного архива.”
---
ТЕСТ №2 — СИНХРОНИЗАЦИЯ ВРЕМЕНИ
---
Методика
Сравнение времени:
- на камерах
- на регистраторе
- на сервере СКУД
- фактическое время события
---
Норма
отклонение ≤ 1–2 секунды
---
Часто
разрыв 215 минут
разные часовые зоны
нет NTP синхронизации
---
Риск
> невозможность юридически доказать момент события
---
Вывод
> “Система не обеспечивает юридически корректную фиксацию времени событий.”
---
ТЕСТ №3 — РЕЗЕРВНОЕ ПИТАНИЕ
---
Методика
Проверка:
- UPS
- время автономной работы
- отключение питания
- поведение систем
---
Норма
15–60 минут автономии минимум
---
Часто
UPS “для галочки”
не держит нагрузку
отсутствует тестирование
---
Риск
> полная остановка системы при отключении электричества
---
Вывод
> “Инфраструктура объекта не защищена от отключения электропитания.”
---
ТЕСТ №4 — ПОЛНОТА КАМЕРНОГО ПОКРЫТИЯ
---
Методика
Проверка зон:
- входы
- кассы
- периметр
- слепые зоны
---
Норма
отсутствие “мертвых зон”
---
Часто
перекрытия нет
камеры направлены неправильно
часть зон не контролируется
---
Риск
> невозможность фиксации инцидентов
---
Вывод
> “Фактическое покрытие объекта не соответствует заявленной системе безопасности.”
---
ТЕСТ №5 — ЖУРНАЛ СОБЫТИЙ СКУД
---
Методика
Проверка:
- записи проходов
- корректность пользователей
- история событий
---
Норма
полный лог всех проходов
---
Часто
потери логов
сбои базы
отключённый журнал
---
Риск
> невозможность отследить перемещения персонала
---
Вывод
> “СКУД не выполняет функцию контроля доступа в полном объёме.”
---
ТЕСТ №6 — СОСТОЯНИЕ ПИТАНИЯ СИСТЕМ
---
Методика
Проверка:
- напряжения
- нагрузки
- перегрева
- стабильности питания оборудования
---
Часто
перегрузка линий
дешёвые блоки питания
нестабильное напряжение
---
Риск
> деградация оборудования и внезапные отказы
---
Вывод
> “Система имеет скрытую деградацию по питанию.”
---
ТЕСТ №7 — СОСТОЯНИЕ ХРАНЕНИЯ ДАННЫХ
---
Методика
Проверка:
- HDD/SSD
- заполнение
- циклы перезаписи
- ошибки записи
---
Часто
диски в деградации
нет мониторинга
потеря данных без уведомлений
---
Риск
> потеря архива без внешних признаков
---
Вывод
> “Система хранения не контролируется и не мониторится.”
---
4. КАК ТЫ ПРЕЗЕНТУЕШЬ ЭТО КЛИЕНТУ
---
НЕ так:
❌ “у вас проблемы”
---
А так:
✔ “мы провели тест №3 и зафиксировали отклонение”
---
5. ШАБЛОН ТВОЕГО ЭКСПЕРТНОГО ЗАКЛЮЧЕНИЯ
ИНЖЕНЕРНОЕ ЗАКЛЮЧЕНИЕ
Объект: ________
Проведены тесты:
- архив видеонаблюдения
- синхронизация времени
- резервное питание
- СКУД
- хранение данных
Выявленные отклонения:
1. ________
2. ________
3. ________
Риски:
- ________
- ________
Заключение инженера:
Система безопасности объекта требует технического сопровождения
для обеспечения стабильной эксплуатации и снижения рисков отказов.
---
6. СИЛА ЭТОЙ СИСТЕМЫ
Ты становишься:
не монтажником
не продавцом услуг
не подрядчиком
---
А ЧЕМ?
инженером, который доказывает состояние объекта
---
7. ПОЧЕМУ ЭТО ПРОДАЁТ SLA
Потому что клиент видит:
факты
отклонения
риски
И сам приходит к выводу:
> “это нужно сопровождать постоянно”
---
8. КАК ЭТО МАСШТАБИРУЕТСЯ
Ты можешь:
обучить инженеров тестам
стандартизировать отчёты
превратить это в продукт аудита
автоматизировать CRM
---
9. СЛЕДУЮЩИЙ УРОВЕНЬ (если продолжим)
Я могу дальше собрать:
автоматический шаблон отчёта (PDF + PHP генерация)
систему балльной оценки объекта (risk scoring)
SLA-переход на основе результатов тестов
“паспорт объекта безопасности”
стандарты для инженеров в команде
---
Если коротко:
> ты превращаешь “услуги безопасности” в инженерную измеряемую систему контроля риска
И это уже не рынок монтажников.
Это рынок эксплуатационных инженеров уровня enterprise.
@@ -0,0 +1,85 @@
# Инструкция по админ-панели AegisOne
## Вход в систему
1. Перейдите на `https://aegisone.ru/service/`
2. Введите логин и пароль, выданные владельцем
3. При 3 неверных попытках — блокировка на 3 минуты
## Дашборд
Главная страница после входа. Карточки показывают ключевые метрики:
- **Активные объекты** — количество обслуживаемых объектов
- **Активные задачи** — открытые и в работе
- **Открытые инциденты** — текущие инциденты
- **SLA контракты** — активные договоры
- **SLA Compliance** — процент соблюдения SLA
- **SHS** — общий индекс здоровья системы
Нажмите на любую карточку для перехода в соответствующий раздел.
## Управление сотрудниками (только владелец)
- Создание учётных записей с ролями: Владелец / Инженер / Техник
- Блокировка/разблокировка сотрудников
- Сброс пароля
## Объекты
- Добавление объектов с параметрами: тип, площадь, регион
- Автоматическое создание из опросника
- Просмотр Risk Score, Object Index, SLA цены
## Назначения
Привязка сотрудников к объектам. Роль сотрудника определяется автоматически из его профиля.
## SLA контракты
- Создание контрактов с привязкой к объекту
- Номер договора (для синхронизации с 1С)
- Уровень обслуживания: Start / Business / Enterprise
- Гибкое время реакции (в часах)
## Опросник объекта
Пошаговое обследование объекта:
1. **Коммерческий** — название, тип, адрес, контакты
2. **Технический** — видеонаблюдение, СКУД, пожарная сигнализация, инфраструктура
3. **Эксплуатация** — текущее обслуживание, регламенты, проблемы
4. **Риски** — критические риски объекта
5. **Расчёт SLA** — уровень обслуживания, дополнительные требования
После завершения автоматически рассчитываются: Risk Score, Complexity Index, Object Index, SLA цена.
## Блог
- Создание/редактирование статей
- **Slug** — адрес статьи, генерируется автоматически из заголовка (можно изменить вручную)
- **Категории**: Аудит, SLA, Инциденты, Технадзор, Документация, Риск-инжиниринг, Кейсы
- **Содержание** — HTML-формат. Используйте тулбар над редактором для вставки форматирования
- Статусы: Черновик → Опубликовать → Архив
## Документация
- Файлы `.md` в папке `service/docs/` автоматически обнаруживаются
- Владелец управляет доступом инженеров к каждому документу
- Инженер видит только разрешённые документы
## Примеры из практики (карусель на главной)
- Добавление кейсов с заголовком, описанием и эффектом
- Поле «Порядок» определяет порядок в карусели
- Флажок «Активен» — показывает/скрывает кейс на главной
## CEO дашборд (только владелец)
Обзор бизнес-метрик: MRR, удержание клиентов, конверсия, производительность.
## Коэффициенты формул
Все расчётные коэффициенты можно редактировать. Изменения применяются ко всем новым расчётам.
## Паспорта объектов
Автоматически генерируются при завершении опросника. Содержат полный расчёт рисков и рекомендаций.
@@ -0,0 +1,589 @@
Ниже — уже не просто “текст КП”, а фундамент твоей новой модели бизнеса.
Я делаю это в формате:
инженерного B2B;
без “маркетингового мусора”;
с акцентом на доверие, SLA и эксплуатацию.
Это можно:
сразу использовать;
конвертировать в PDF;
вставить на сайт;
генерировать автоматически через CRM.
---
1. ПОЛНОЦЕННЫЙ ШАБЛОН КП (.md)
# КОММЕРЧЕСКОЕ ПРЕДЛОЖЕНИЕ
## Техническое сопровождение систем безопасности
---
# AEGISONE ENGINEERING
### Эксплуатация и сопровождение систем безопасности коммерческих объектов
---
## Контакты
Телефон: +7 (XXX) XXX-XX-XX
Email: info@aegisone.ru
Сайт: https://aegisone.ru
---
# О компании
AEGISONE ENGINEERING — инженерная сервисная компания,
специализирующаяся на техническом сопровождении
и эксплуатации систем безопасности коммерческих объектов.
Мы обеспечиваем:
- стабильную работу систем;
- контроль инфраструктуры;
- регламентное обслуживание;
- SLA и контроль сроков реакции;
- технический аудит;
- сопровождение эксплуатации объектов.
---
# Какие проблемы мы решаем
Большинство неисправностей систем безопасности
выявляются только после возникновения проблем:
- отсутствует архив видеонаблюдения;
- системы работают нестабильно;
- часть оборудования неисправна;
- отсутствует документация;
- неисправности копятся месяцами;
- подрядчики не несут ответственности;
- проверки выявляют критические нарушения.
Это приводит:
- к рискам простоев;
- потере контроля;
- проблемам при проверках;
- финансовым потерям.
---
# Решение
Мы берем на себя техническое сопровождение
и контроль работоспособности систем безопасности объекта.
В рамках сопровождения обеспечиваем:
- регламентное обслуживание;
- диагностику;
- контроль состояния оборудования;
- аварийное реагирование;
- сопровождение эксплуатации;
- фотоотчетность;
- рекомендации по модернизации;
- технический контроль подрядчиков.
---
# Состав услуг
| Услуга | Описание |
|---|---|
| Регламентные проверки | Контроль состояния систем |
| Диагностика | Поиск неисправностей |
| Контроль архива | Проверка записи и хранения |
| Проверка питания | Бесперебойность работы |
| Аварийные выезды | Реагирование по SLA |
| Фотоотчетность | Подтверждение работ |
| Ведение журналов | История обслуживания |
| Консультации | Поддержка эксплуатации |
---
# SLA
| Приоритет | Описание | Время реакции |
|---|---|---|
| P1 | Полный отказ критической системы | до 2 часов |
| P2 | Частичная потеря функционала | до 4 часов |
| P3 | Некритичная неисправность | до 24 часов |
| P4 | Плановые работы | по графику |
---
# Почему выбирают нас
- более 20 лет инженерного опыта;
- лицензия МЧС;
- работа по SLA и регламентам;
- опыт эксплуатации коммерческих объектов;
- прозрачная отчетность;
- несем ответственность за результат;
- не работаем по принципу «сделали и забыли».
---
# Тарифы
## START
Для небольших объектов.
Включает:
- ежемесячный регламент;
- диагностику;
- удаленную поддержку;
- отчетность.
Стоимость:
от 25 000 ₽ / месяц
---
## BUSINESS
Для коммерческих объектов.
Включает:
- SLA;
- аварийные выезды;
- контроль архива;
- сопровождение проверок;
- фотоотчетность.
Стоимость:
от 60 000 ₽ / месяц
---
## ENTERPRISE
Формат внешнего инженерного отдела.
Включает:
- постоянное сопровождение;
- участие в эксплуатации;
- контроль подрядчиков;
- развитие инфраструктуры;
- аудит объектов.
Стоимость:
индивидуально
---
# Этапы работы
1. Предварительная консультация
2. Технический аудит объекта
3. Подготовка отчета и рекомендаций
4. Формирование SLA и регламентов
5. Постановка объекта на сопровождение
6. Регулярное обслуживание и отчетность
---
# Технический аудит
Перед постановкой объекта на сопровождение
рекомендуем проведение технического аудита.
Аудит позволяет:
- выявить скрытые проблемы;
- оценить состояние систем;
- определить риски;
- подготовить рекомендации.
---
# Контакты
AEGISONE ENGINEERING
Телефон:
+7 (XXX) XXX-XX-XX
Email:
info@aegisone.ru
Сайт:
https://aegisone.ru
---
2. СТРУКТУРА ПЕРВОГО АУДИТА
Вот здесь начинается твоя реальная экспертная модель.
ЦЕЛЬ АУДИТА
НЕ:
“найти поломку”.
А:
показать уровень инженерной зрелости объекта.
---
ЧТО ДОЛЖЕН ДАВАТЬ АУДИТ
После него клиент должен понять:
где риски;
что не работает;
что может привести к проблемам;
насколько объект управляем;
почему нужен SLA.
---
ИДЕАЛЬНАЯ СТРУКТУРА АУДИТА
---
ЭТАП 1
ВВОДНАЯ ИНФОРМАЦИЯ
---
Собираем:
тип объекта;
площадь;
количество систем;
ответственные лица;
история проблем;
подрядчики;
наличие документации.
---
ЭТАП 2
ВИЗУАЛЬНЫЙ ОСМОТР
---
Проверяем:
шкафы;
коммутацию;
маркировку;
кабельные трассы;
питание;
серверные;
доступ к оборудованию.
---
ЭТАП 3
ПРОВЕРКА РАБОТОСПОСОБНОСТИ
---
Видеонаблюдение:
запись;
архив;
синхронизация времени;
качество изображения;
доступность камер;
питание.
---
СКУД:
проходы;
журналы;
права доступа;
аварийное открытие;
контроллеры.
---
Пожарка:
индикация;
ошибки;
оповещение;
связь;
резервное питание.
---
ЭТАП 4
ПРОВЕРКА ДОКУМЕНТАЦИИ
---
Проверяем:
схемы;
пароли;
IP;
журналы;
проекты;
исполнительную документацию.
---
ЭТАП 5
ОЦЕНКА РИСКОВ
Это важнейшая часть.
---
Например:
Риск Последствие
Не пишется архив Потеря доказательств
Нет резервного питания Полный отказ
Нет документации Долгое восстановление
Ошибки пожарки Риски проверок
---
ЭТАП 6
РЕКОМЕНДАЦИИ
---
Разделяем:
Критические
Исправить срочно.
---
Важные
В течение 30 дней.
---
Плановые
При модернизации.
---
ЭТАП 7
ПРЕДЛОЖЕНИЕ SLA
Вот тут ты переводишь аудит:
в абонентское сопровождение.
---
3. ЧЕК-ЛИСТ АУДИТА
Это уже реальный инструмент продаж.
---
# ЧЕК-ЛИСТ ТЕХНИЧЕСКОГО АУДИТА
## Общая информация
- [ ] Тип объекта
- [ ] Адрес
- [ ] Ответственное лицо
- [ ] Количество систем
- [ ] Наличие документации
- [ ] Наличие подрядчиков
---
# Видеонаблюдение
## Сервер / регистратор
- [ ] Проверка записи
- [ ] Проверка архива
- [ ] Проверка HDD
- [ ] Проверка времени
- [ ] Проверка сети
- [ ] Проверка питания
## Камеры
- [ ] Наличие сигнала
- [ ] Качество изображения
- [ ] Наличие загрязнений
- [ ] Корректность обзора
- [ ] Наличие ИК подсветки
---
# СКУД
- [ ] Проверка контроллеров
- [ ] Проверка считывателей
- [ ] Проверка журналов
- [ ] Проверка доступа
- [ ] Проверка аварийного открытия
- [ ] Проверка резервного питания
---
# Пожарная сигнализация
- [ ] Проверка панели
- [ ] Проверка ошибок
- [ ] Проверка шлейфов
- [ ] Проверка оповещения
- [ ] Проверка резервного питания
- [ ] Проверка журналов событий
---
# Сетевая инфраструктура
- [ ] Проверка коммутаторов
- [ ] Проверка UPS
- [ ] Проверка кабельной инфраструктуры
- [ ] Проверка интернет-каналов
- [ ] Проверка температурного режима
---
# Документация
- [ ] Схемы
- [ ] IP-адресация
- [ ] Пароли
- [ ] Журналы обслуживания
- [ ] Исполнительная документация
---
# Риски
- [ ] Отсутствие записи
- [ ] Отказ резервного питания
- [ ] Отсутствие документации
- [ ] Ошибки оборудования
- [ ] Нестабильность сети
---
# Рекомендации
## Критические:
_________________________________
## Важные:
_________________________________
## Плановые:
_________________________________
---
САМОЕ ВАЖНОЕ
Вот что тебе нужно понять стратегически.
АУДИТ — ЭТО НЕ “ДОП. УСЛУГА”.
Это:
твоя система продаж.
Потому что:
не надо “впаривать”;
ты показываешь реальность;
ты становишься экспертом;
клиент сам понимает необходимость SLA.
И это идеально подходит под твою модель инженерного B2B.
+503
View File
@@ -0,0 +1,503 @@
Хорошо. Здесь ты фактически строишь не “контент”, а:
инженерную медийную репутацию компании
Это важно различать.
Контент в твоей нише выполняет 3 функции:
1. формирует доверие у B2B (директора, УК, собственники)
2. объясняет, почему эксплуатация важнее монтажа
3. подводит к SLA и аудитам (деньгам)
---
0. СТРАТЕГИЯ ЭКСПЕРТНОСТИ (ОСНОВА)
Твоя роль в контенте:
> “Спокойный инженер, который объясняет, где у бизнеса скрытые риски в системах безопасности”
НЕ:
продавец
маркетолог
“лиды”
“акции”
А:
диагност
эксплуатационный инженер
человек, который видит риски до аварии
---
1. КОНТЕНТ-СТОЛПЫ (PILLARS)
У тебя должно быть 5 контент-направлений:
---
1. Эксплуатационные проблемы (самый важный)
👉 “Что ломается в реальности”
Примеры:
камеры не пишут архив
пожарка работает “на бумаге”
СКУД пропускает ошибки
нет резервного питания
подрядчики исчезают
---
2. Разбор реальных кейсов (без раскрытия клиента)
👉 “Что мы нашли на объектах”
---
3. Ошибки монтажа и эксплуатации
👉 “Почему дешево = дорого”
---
4. Инженерные разборы систем
👉 “как это должно работать правильно”
---
5. Управление рисками (B2B язык)
👉 “что теряет бизнес”
---
2. ЧАСТОТА ПУБЛИКАЦИЙ
Идеально:
2 поста в неделю (Telegram / VK / VC)
1 длинная статья в неделю
1 кейс в неделю (может совпадать)
Итого: 👉 8–10 единиц контента / месяц
---
3. КОНТЕНТ-ПЛАН НА 3 МЕСЯЦА
---
МЕСЯЦ 1 — “ПРОБЛЕМЫ РЫНКА”
Цель: 👉 показать, что рынок систем безопасности работает плохо
---
Неделя 1
Пост 1
Почему камеры видеонаблюдения не защищают бизнес
Пост 2
Что происходит, когда не ведется обслуживание систем безопасности
Статья:
ТОП-7 скрытых проблем систем безопасности на коммерческих объектах
---
Неделя 2
Пост:
Почему 80% объектов не имеют реального архива видеонаблюдения
Пост:
Как подрядчики “сдают” объект и исчезают
Кейс:
“Объект без архива 4 месяца — как это обнаруживается”
---
Неделя 3
Пост:
Почему пожарная сигнализация чаще всего существует “на бумаге”
Пост:
3 ошибки, которые делают монтажники при установке СКУД
Статья:
Почему системы безопасности не работают в момент инцидента
---
Неделя 4
Пост:
Что на самом деле означает “обслуживание систем безопасности”
Пост:
Почему дешёвый монтаж всегда превращается в дорогое обслуживание
Кейс:
“Склад с критическими ошибками в системе доступа”
---
МЕСЯЦ 2 — “ЭКСПЕРТНОСТЬ”
Цель: 👉 показать, что ты знаешь, как должно быть правильно
---
Неделя 1
Пост:
Как правильно проверять видеонаблюдение на объекте
Статья:
Чек-лист эксплуатации систем безопасности для бизнеса
Кейс:
“Восстановление системы видеонаблюдения после 2 лет хаоса”
---
Неделя 2
Пост:
Что должен контролировать собственник объекта
Пост:
Почему отсутствие SLA = потеря контроля
Статья:
Как устроена правильная эксплуатация систем безопасности
---
Неделя 3
Пост:
Как проверить, работает ли ваша система безопасности реально
Пост:
Почему регламент важнее оборудования
Кейс:
“Объект после смены подрядчика”
---
Неделя 4
Пост:
Что такое инженерная эксплуатация (а не монтаж)
Статья:
Почему рынок безопасности живет в иллюзии надежности
---
МЕСЯЦ 3 — “ДОВЕРИЕ И ПЕРЕХОД В ПРОДАЖИ”
Цель: 👉 мягко переводить в аудит и SLA
---
Неделя 1
Пост:
Что показывает технический аудит объекта
Статья:
Как мы выявляем скрытые проблемы систем безопасности
Кейс:
“Аудит коммерческого объекта: 12 критических ошибок”
---
Неделя 2
Пост:
Почему бизнесу нужен внешний инженер безопасности
Пост:
Что происходит, когда нет технического контроля
Статья:
SLA в системах безопасности: зачем он нужен
---
Неделя 3
Пост:
Как выглядит нормальное техническое сопровождение
Кейс:
“Перевод объекта на SLA обслуживание”
Пост:
Почему обслуживание = защита бизнеса, а не ремонт
---
Неделя 4
Пост:
Когда нужно делать аудит (и почему его откладывают)
Статья:
Как мы снижаем эксплуатационные риски объектов
---
4. ФОРМАТ КОНТЕНТА (ВАЖНО)
Каждый пост должен иметь структуру:
---
1. Проблема
2. Реальный инженерный разбор
3. Последствия для бизнеса
4. Вывод (очень спокойный)
---
5. ПРИМЕР ЭКСПЕРТНОГО ПОСТА (ГОТОВЫЙ)
---
Тема:
“Почему камеры видеонаблюдения не защищают бизнес”
---
Видеонаблюдение часто воспринимается как система безопасности.
На практике это не всегда так.
Основная проблема заключается не в оборудовании, а в эксплуатации.
На большинстве объектов мы наблюдаем типовые ситуации:
— часть камер не записывает архив;
— запись есть, но не сохраняется нужный период;
— часть каналов отключена;
— время на устройствах не синхронизировано;
— отсутствует резервирование питания;
— система не тестируется регулярно.
В результате при инциденте система формально есть, но фактически не выполняет свою функцию.
Бизнес узнаёт об этом только постфактум.
Правильная эксплуатация видеонаблюдения — это не установка оборудования, а регулярный контроль:
— состояния записи;
— целостности архива;
— доступности камер;
— корректности времени;
— работоспособности хранения.
Без этого система превращается в формальность.
Именно поэтому в инженерной эксплуатации видеонаблюдение рассматривается как сервис, а не как установленное оборудование.
---
6. КОД ДЛЯ КОНТЕНТ-СТРАНИЦЫ (PHP БЛОГ)
---
<?php include '../header.php'; ?>
<section class="blog-post">
<div class="container">
<h1>Почему камеры видеонаблюдения не защищают бизнес</h1>
<p class="meta">Инженерный разбор систем безопасности</p>
<p>
Видеонаблюдение часто воспринимается как система безопасности...
</p>
<h2>Типовые проблемы эксплуатации</h2>
<ul>
<li>нет записи архива</li>
<li>сбои хранения</li>
<li>отсутствие резервирования</li>
<li>рассинхронизация времени</li>
</ul>
<h2>Почему это критично</h2>
<p>
Система формально работает, но не выполняет функцию защиты бизнеса.
</p>
<h2>Вывод</h2>
<p>
Без регламентной эксплуатации система безопасности превращается в формальность.
</p>
</div>
</section>
<?php include '../footer.php'; ?>
---
7. ГЛАВНЫЙ ПРИНЦИП ВСЕГО КОНТЕНТА
Ты должен повторять одну мысль:
> “Без эксплуатации система безопасности не существует”
---
8. ЧТО ЭТО ДАСТ ЧЕРЕЗ 2–3 МЕСЯЦА
Если делать системно:
тебя начнут воспринимать как эксперта;
появятся входящие заявки;
снизится необходимость продавать;
аудит станет естественным входом;
SLA станет логичным продолжением;
монтаж уйдет в фон.
---
9. СЛЕДУЮЩИЙ ШАГ (если хочешь дальше усилить)
Я могу дальше собрать тебе:
контент-воронку (как пост → аудит → SLA)
шаблоны кейсов (очень мощно работает)
систему “доказательной экспертизы”
сценарий продаж через контент
упаковку Telegram-канала под лидогенерацию
Это уже уровень:
построения сервисной B2B-машины, а не просто сайта.
+673
View File
@@ -0,0 +1,673 @@
Вот здесь начинается настоящий B2B.
И это как раз твоя сильная сторона.
Потому что:
# тебе НЕ нужен агрессивный маркетинг.
Ты не:
- франшиза,
- массовый монтажник,
- “льем лиды”.
Твой рынок:
# доверительный инженерный B2B.
А там продают:
- компетенция;
- спокойствие;
- экспертность;
- репутация;
- предсказуемость.
---
# САМАЯ БОЛЬШАЯ ОШИБКА
Если ты сейчас:
- запустишь рекламу “монтаж камер”;
- начнешь конкурировать ценой;
- пойдешь в массовый трафик;
ты попадешь обратно:
# в болото монтажного рынка.
---
# ТВОЯ ИДЕАЛЬНАЯ СТРАТЕГИЯ
# “ИНЖЕНЕРНАЯ ЭКСПЕРТНОСТЬ + ЛОКАЛЬНОЕ ДОВЕРИЕ”
---
# ЧТО ТЕБЕ НУЖНО НА СТАРТЕ
НЕ:
- 100 клиентов.
А:
# 3–5 правильных объектов.
Это принципиально.
---
# ТВОЯ ЦЕЛЬ НА ПЕРВЫЕ 6 МЕСЯЦЕВ
Собрать:
- 5–10 объектов на SLA;
- с чеком 50–150 тыс.
Это уже:
# 500 тыс 1.5 млн recurring revenue.
И это достижимо без рекламы.
---
# СТРАТЕГИЯ ПЕРВЫХ ПРОДАЖ
---
# ЭТАП 1
# НЕ ПРОДАВАТЬ ОБСЛУЖИВАНИЕ
Это критично.
Потому что: “ТО” воспринимается как:
- обязаловка;
- минималка;
- формальность.
---
# ПРОДАВАТЬ НУЖНО:
# АУДИТ И СНИЖЕНИЕ РИСКОВ.
---
# ТВОЙ ИДЕАЛЬНЫЙ ВХОД
НЕ:
> “давайте мы вас обслужим”.
А:
# “давайте проверим текущее состояние систем”.
---
# ПОЧЕМУ ЭТО РАБОТАЕТ
Ты:
- не впариваешь;
- не навязываешься;
- не демпингуешь.
Ты:
# инженер-эксперт.
---
# ЧТО ПРОДАВАТЬ ПЕРВЫМ
---
# ПРОДУКТ №1
# “ТЕХНИЧЕСКИЙ АУДИТ ОБЪЕКТА”
---
## Стоимость:
### 1550 тыс ₽
Зависит:
- от объекта;
- площади;
- систем.
---
# ЧТО ВХОДИТ
- диагностика;
- проверка архива;
- проверка питания;
- тестирование;
- проверка документации;
- оценка рисков;
- рекомендации.
---
# РЕЗУЛЬТАТ:
PDF-отчет.
---
# ПОЧЕМУ ЭТО ГЕНИАЛЬНО ДЛЯ ТЕБЯ
Ты:
- умеешь находить проблемы;
- не любишь “продажи”;
- инженер.
То есть:
# это идеальная модель продаж под тебя.
---
# ЭТАП 2
# ПРОДАЖА SLA
После аудита.
---
# СХЕМА
## Ты показываешь:
### Сейчас:
- риски;
- неисправности;
- слабые места.
---
## Потом:
> “Чтобы это не накапливалось — нужен регламент эксплуатации.”
---
# И ТУТ ПОЯВЛЯЕТСЯ SLA
---
# КОМУ ИДТИ ПЕРВЫМИ
---
# НЕ:
- застройщики;
- тендеры;
- госка.
Это болото.
---
# ИДЕАЛЬНЫЕ ПЕРВЫЕ КЛИЕНТЫ
---
# 1. ГОСТИНИЦЫ / АПАРТЫ
(лучшая ниша для тебя)
---
## Почему:
- бардак;
- текучка;
- подрядчики пропадают;
- владельцы устали.
---
# 2. НЕБОЛЬШИЕ СКЛАДЫ
Очень хорошая ниша.
---
# 3. КОММЕРЧЕСКИЕ УК
Золото.
---
# 4. ЧАСТНЫЕ КЛИНИКИ
Отличная ниша.
---
# 5. СЕТЕВЫЕ КОМПАНИИ
Например:
- автомойки;
- рестораны;
- магазины;
- стоматологии.
---
# КАК ИСКАТЬ КЛИЕНТОВ БЕЗ РЕКЛАМЫ
Вот тут твой рынок.
---
# МЕТОД №1
# “ИНЖЕНЕРНЫЕ ЗНАКОМСТВА”
Ты недооцениваешь:
# насколько рынок маленький.
---
# ТЕБЕ НУЖНО:
- управляющие;
- инженеры эксплуатации;
- техдиры;
- собственники;
- УК.
---
# НЕ ПРОДАВАТЬ
А:
# знакомиться.
---
# ФОРМАТ
---
## Пример:
> “Мы занимаемся технической эксплуатацией систем безопасности коммерческих объектов. Если будет нужна диагностика или помощь — обращайтесь.”
---
# ВСЕ.
Никаких:
- впариваний;
- “акций”;
- скидок.
---
# МЕТОД №2
# АУДИТ КАК ВХОД
---
# ОЧЕНЬ МОЩНАЯ СХЕМА
## Предлагаешь:
# “экспресс-проверку объекта”.
---
## Например:
- проверка архива;
- отказоустойчивость;
- питание;
- документация.
---
# ПОЧЕМУ ЭТО РАБОТАЕТ
Большинство объектов:
# реально в плохом состоянии.
---
# МЕТОД №3
# LINKEDIN / TELEGRAM / VC / ДЗЕН
Это твоя золотая жила.
---
# ПОЧЕМУ
Рынок безопасности:
# чудовищно слаб по экспертности.
---
# А ТЫ:
- умеешь говорить по делу;
- инженер;
- системный.
---
# ТВОЙ КОНТЕНТ БУДЕТ ВЫДЕЛЯТЬСЯ
---
# ТЕПЕРЬ:
# КОНТЕНТ-ПЛАН НА 3 МЕСЯЦА
---
# ГЛАВНАЯ ЦЕЛЬ КОНТЕНТА
НЕ:
- “лайки”.
А:
# доверие B2B.
---
# ТВОЯ РОЛЬ В КОНТЕНТЕ
НЕ: “маркетолог”.
А:
# “спокойный инженер-эксперт”.
---
# СТИЛЬ КОНТЕНТА
---
# НЕ:
- хайп;
- кликбейт;
- “ТОП-5 камер”.
---
# А:
- реальные проблемы;
- эксплуатация;
- ошибки;
- риски;
- практика.
---
# КАНАЛЫ
---
# ОБЯЗАТЕЛЬНО:
- Telegram;
- сайт/блог;
- Яндекс Бизнес;
- VC.ru;
- Дзен.
---
# МОЖНО:
- YouTube Shorts;
- Rutube.
---
# ГЛАВНОЕ:
# не количество.
А:
# экспертность.
---
# КОНТЕНТ-ПЛАН
---
# МЕСЯЦ 1
# “ПОКАЗАТЬ ПРОБЛЕМЫ РЫНКА”
---
## Неделя 1
### Статья:
# Почему камеры не помогают в момент инцидента
---
## Пост:
5 причин потери архива видеонаблюдения.
---
## Короткое видео:
“Почему регистратор — не гарантия записи.”
---
## Кейс:
“На объекте архив не записывался 4 месяца.”
---
# Неделя 2
### Статья:
# Что реально проверяет МЧС
---
## Пост:
Почему ложные срабатывания — это не “нормально”.
---
## Видео:
3 ошибки обслуживания пожарки.
---
# Неделя 3
### Статья:
# Почему дешевый монтаж обходится дорого
---
## Пост:
Что чаще всего ломают монтажники.
---
## Фото-кейс:
“Как выглядит отсутствие эксплуатации через 2 года.”
---
# Неделя 4
### Статья:
# Что должно входить в нормальное SLA
---
## Пост:
Разница между “ТО для галочки” и эксплуатацией.
---
## Видео:
“Почему подрядчики исчезают после монтажа.”
---
# МЕСЯЦ 2
# “ПОКАЗАТЬ ЭКСПЕРТНОСТЬ”
---
# Темы:
- как проверить архив;
- как проверить подрядчика;
- как принимать объект;
- чек-лист эксплуатации;
- ошибки СКУД;
- проблемы гостиниц;
- проблемы складов.
---
# МЕСЯЦ 3
# “ФОРМИРОВАНИЕ ДОВЕРИЯ”
---
# Темы:
- реальные кейсы;
- аудиты;
- сравнения;
- “что нашли на объекте”;
- эксплуатационные риски;
- инженерные решения.
---
# КАК ЧАСТО ПУБЛИКОВАТЬ
---
# ИДЕАЛЬНО:
## 2–3 поста в неделю.
---
# НЕ НУЖНО:
- каждый день;
- “активность ради активности”.
---
# САМОЕ ВАЖНОЕ
Контент должен вызывать мысль:
> “Эти люди реально понимают эксплуатацию объектов.”
---
# ЧТО ДАСТ РЕЗУЛЬТАТ БЫСТРЕЕ ВСЕГО
---
# 1. АУДИТЫ
---
# 2. КЕЙСЫ
---
# 3. РАЗБОРЫ ОШИБОК
---
# 4. УЗКАЯ НИША
Например:
- гостиницы;
- склады.
---
# 5. ЛИЧНЫЙ БРЕНД ИНЖЕНЕРА
Вот это критично.
---
# ТЕБЕ НЕ НУЖНО БЫТЬ “ПРОДАВЦОМ”
Тебе нужно быть:
# человеком, которому доверяют сложный объект.
---
# ВАЖНОЕ ПОНИМАНИЕ
Твои клиенты:
- не ищут “самых дешевых”;
- не хотят “маркетинг”.
Они хотят:
# “нормального ответственного подрядчика”.
И рынок сейчас чудовищно голоден на таких людей.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+340
View File
@@ -0,0 +1,340 @@
Ниже — тебе не “скрипты продаж”, а инженерная система входа в аудит, адаптированная под Краснодарский край и B2B-реальность (УК, гостиницы, склады, собственники).
Главная идея:
> Ты не продаёшь аудит. Ты предлагаешь проверку рисков объекта.
---
1. ПСИХОЛОГИЯ ПРОДАЖ АУДИТА (ВАЖНО)
Что продаёт аудит на самом деле:
снижение рисков проверок (МЧС, внутренние, страховые)
уверенность, что система реально работает
снятие ответственности с менеджера/собственника
“независимое мнение инженера”
---
Что НЕ работает:
“проверим камеры”
“сделаем диагностику”
“обслуживание дешевле”
---
Рабочая позиция:
> “Мы делаем техническую проверку систем безопасности объекта с выдачей инженерного заключения по рискам эксплуатации.”
---
2. ВХОД В ПРОДАЖУ (3 КАНАЛА)
---
2.1 Холодный вход (самый важный в регионе)
Кому пишем:
гостиницы
апарт-отели
склады
УК
коммерческая недвижимость
---
2.2 Тёплый вход (через знакомства)
электрики
IT
строители
подрядчики
проектировщики
---
2.3 Вход через контент
Telegram / статьи / кейсы
---
3. СТРУКТУРА ПРОДАЖИ АУДИТА
---
1. Контакт
2. Короткое позиционирование
3. Выявление боли
4. Предложение аудита
5. Подтверждение логики
6. Назначение выезда
---
4. ХОЛОДНЫЙ СКРИПТ (ТЕКСТ + WA / TG)
Вариант 1 — максимально инженерный (основной)
Добрый день.
Мы занимаемся техническим сопровождением систем безопасности коммерческих объектов в Краснодарском крае.
Перед тем как брать объекты на обслуживание, обычно делаем технический аудит:
— проверка видеонаблюдения (архив, запись, питание)
— проверка СКУД
— проверка пожарной сигнализации
— оценка рисков эксплуатации
По практике, на большинстве объектов выявляются скрытые проблемы, которые не видны в ежедневной работе.
Если у вас есть задача понять текущее состояние систем — можем провести аудит и дать инженерное заключение по рискам.
Подскажите, у вас сейчас системы на обслуживании или “как есть работают”?
---
Почему это работает:
нет продажи
есть диагностика
есть статус эксперта
есть вопрос (диалог)
---
5. ВТОРОЙ СООБЩЕНИЕ (если отвечают “работает/есть подрядчик”)
Понял.
В таких случаях чаще всего проблема не в “работает/не работает”, а в том, что:
— архив не проверяется регулярно
— резервирование не тестируется
— часть оборудования работает нестабильно
— нет объективного контроля состояния
А это всплывает обычно в момент инцидента или проверки.
Мы как раз делаем аудит, который показывает такие скрытые вещи до проблем.
Если интересно — можем сделать экспресс-проверку объекта с коротким отчётом по рискам.
---
6. КОНВЕРСИЯ В АУДИТ (КЛЮЧЕВОЙ МОМЕНТ)
Можем сделать так:
— выезд инженера
— проверка всех систем
— фиксация состояния
— короткий инженерный отчёт
— список рисков и рекомендаций
Обычно это занимает 1–2 дня в зависимости от объекта.
По стоимости — от 15 до 50 тыс, зависит от объёма систем.
Если удобно — могу уточнить объект и сказать точнее по формату.
---
7. СКРИПТ ДЛЯ ГОЛОСА (ЕСЛИ ЗВОНЯТ)
структура:
1. Позиционирование (10–15 сек)
Мы занимаемся техническим сопровождением систем безопасности коммерческих объектов.
Перед обслуживанием всегда делаем аудит состояния систем.
---
2. Боль
На большинстве объектов, которые мы смотрим, есть скрытые проблемы — архив, питание, доступ, пожарка.
Обычно это не видно в ежедневной работе.
---
3. Переход
Поэтому предлагаем сначала сделать техническую проверку объекта и дать инженерное заключение.
---
4. Закрытие
Если вам актуально — можем согласовать выезд инженера и формат проверки.
---
8. ОБРАБОТКА ВОЗРАЖЕНИЙ
---
❌ “У нас уже есть подрядчик”
Понял.
Аудит как раз часто показывает, что объект обслуживается формально, но без проверки ключевых параметров:
архив, резервирование, ошибки, питание.
Мы не конкурируем с подрядчиком — мы даём независимую оценку состояния систем.
---
❌ “Дорого”
Аудит не про “сделать дешевле”.
Это проверка рисков, которые могут стоить дороже:
— потеря архива
— отказ системы
— проблемы при проверках
Поэтому стоимость зависит от масштаба объекта, но обычно это разовая диагностика, а не постоянные расходы.
---
❌ “Нам не нужно”
Понял.
На практике аудит нужен не всегда “сейчас”, а когда появляются сомнения в стабильности систем или перед проверками.
Если актуализируется — можем подключиться.
---
9. СИСТЕМА ДОГРЕВА (ЕСЛИ НЕ КУПИЛИ СРАЗУ)
Через 35 дней:
Добрый день.
Подскажите, актуально ли сейчас техническое обследование систем безопасности?
По опыту, на объектах часто всплывают вопросы по архиву и резервированию, которые лучше проверить заранее, чем в момент инцидента.
---
10. МИКРО-CRM ЛОГИКА (ВАЖНО)
Каждый контакт делишь на:
не ответил
думает
есть подрядчик
отказ
сделал аудит
ушёл в SLA
---
11. КЛЮЧЕВОЙ МЕХАНИЗМ ПРОДАЖ
Твоя система должна работать так:
контакт → инженерная диагностика → выявление рисков → логичный переход в SLA
---
12. ПОЧЕМУ ЭТО РАБОТАЕТ ИМЕННО В КРАСНОДАРСКОМ КРАЕ
Рынок региона:
много частных объектов
слабая эксплуатация систем
подрядчики “формальные”
мало инженерных компаний уровня SLA
👉 значит: ты не конкурируешь по цене — ты создаёшь новый тип услуги
---
13. ЕСЛИ ДАЛЬШЕ УСИЛИВАТЬ СИСТЕМУ
Следующий уровень, который я могу тебе собрать:
скрипт “закрытия в SLA после аудита”
таблица квалификации клиента (кого брать/кого нет)
CRM-воронка под SLA (по стадиям)
шаблон отчёта аудита (который продаёт сам себя)
шаблон договора SLA (который не торгуется)
И это уже будет не продажи.
Это будет:
инженерная коммерческая система повторяемого дохода.
+556
View File
@@ -0,0 +1,556 @@
Сейчас мы выходим на ключевой этап.
Потому что:
УТП — это не “красивый текст”.
Это:
твоя позиция на рынке.
Именно здесь определяется:
будешь ли ты конкурировать ценой;
попадешь ли в “монтажников”;
или станешь инженерным B2B подрядчиком.
---
ГЛАВНАЯ ОШИБКА РЫНКА
Все пишут:
“качественный монтаж”;
“20 лет опыта”;
“индивидуальный подход”.
Это не УТП.
Это информационный шум.
---
ТВОЕ РЕАЛЬНОЕ ПРЕИМУЩЕСТВО
Ты НЕ:
продавец оборудования;
бригада монтажников;
дешевый подрядчик.
Ты:
инженерная сервисная компания.
И продавать нужно:
ответственность;
стабильность;
контроль;
снижение рисков;
эксплуатацию;
прозрачность.
---
ТВОЕ ОСНОВНОЕ УТП
Вот сильная база.
---
AEGISONE — инженерное сопровождение систем безопасности коммерческих объектов.
Мы не просто устанавливаем оборудование.
Мы берем на себя ответственность за стабильную работу систем безопасности объекта.
Работаем с коммерческой недвижимостью, гостиницами, складами, производствами и сетевыми объектами.
Обеспечиваем:
— регламентное обслуживание;
— SLA и контроль сроков реакции;
— технический аудит;
— восстановление проблемных объектов;
— сопровождение эксплуатации;
— прозрачную отчетность и контроль инфраструктуры.
Если система безопасности нужна не «для галочки», а для реальной работы объекта — мы подходим.
---
ПОЧЕМУ ЭТО СИЛЬНО
Тут нет:
“лучших цен”;
“монтажа под ключ”;
“любых работ”.
Это:
позиция эксперта.
---
ДОПОЛНИТЕЛЬНЫЕ УТП ДЛЯ РАЗНЫХ СЕГМЕНТОВ
---
ДЛЯ ГОСТИНИЦ
Обеспечиваем стабильную работу систем безопасности гостиниц и апарт-отелей без постоянного контроля со стороны управляющего.
Берем на себя техническое сопровождение:
— видеонаблюдения;
— СКУД;
— пожарной сигнализации;
— сетевой инфраструктуры.
Работаем по SLA и регламентам.
---
ДЛЯ СКЛАДОВ И ЛОГИСТИКИ
Снижаем риски потери архива, отказов систем и простоев инфраструктуры складских объектов.
Контролируем работоспособность:
— видеонаблюдения;
— контроля доступа;
— периметра;
— сетевой инфраструктуры.
Обеспечиваем регламентное обслуживание и аварийное реагирование.
---
ТЕПЕРЬ САМОЕ ВАЖНОЕ
СТРУКТУРА КОММЕРЧЕСКОГО ПРЕДЛОЖЕНИЯ
Большинство КП на рынке — мусор.
Там:
список оборудования;
цены;
таблицы;
“надежные решения”.
B2B это НЕ читает.
---
ТВОЕ КП ДОЛЖНО ПРОДАВАТЬ:
СПОКОЙСТВИЕ И КОНТРОЛЬ.
---
ИДЕАЛЬНАЯ СТРУКТУРА КП ДЛЯ ТЕБЯ
---
1. ТИТУЛЬНАЯ СТРАНИЦА
---
Заголовок:
Коммерческое предложение
по техническому сопровождению систем безопасности
---
Подзаголовок:
Для:
гостиницы;
склада;
коммерческого объекта;
производства.
---
Внизу:
логотип;
контакты;
дата.
---
2. КРАТКО О ПРОБЛЕМЕ
Это критически важно.
---
Пример:
---
Большинство неисправностей систем безопасности выявляются только после возникновения проблем:
— отсутствует архив видеонаблюдения;
— часть оборудования не функционирует;
— отсутствует документация;
— подрядчики не несут ответственности;
— неисправности накапливаются месяцами.
Это приводит к рискам простоев, потере контроля и проблемам при проверках.
Для снижения этих рисков необходимо регулярное техническое сопровождение и контроль состояния инфраструктуры объекта.
---
ПОЧЕМУ ЭТО СИЛЬНО
Ты:
не “продаешь услуги”;
а показываешь проблему бизнеса.
---
3. ЧТО ТЫ ПРЕДЛАГАЕШЬ
---
Заголовок:
Решение
---
Пример:
---
AEGISONE обеспечивает техническое сопровождение и контроль работоспособности систем безопасности объекта.
Мы берем на себя:
— регламентное обслуживание;
— диагностику;
— аварийное реагирование;
— технический контроль;
— сопровождение эксплуатации;
— контроль подрядчиков;
— отчетность и рекомендации.
---
4. ЧТО ВХОДИТ В ОБСЛУЖИВАНИЕ
Вот здесь уже конкретика.
---
Пример структуры:
Услуга Описание
Регламентные проверки Проверка состояния оборудования
Контроль архива Проверка записи и хранения
Аварийные выезды Реагирование по SLA
Диагностика Поиск неисправностей
Отчетность Фото и рекомендации
Поддержка Консультации персонала
---
5. SLA
Очень важно.
---
Пример:
Приоритет Время реакции
Критическая авария до 2 часов
Частичный отказ до 4 часов
Плановая заявка до 24 часов
---
6. ПОЧЕМУ ИМЕННО ВЫ
Вот тут нельзя писать банальности.
---
ПРАВИЛЬНО:
---
Почему заказчики работают с нами:
— более 20 лет инженерного опыта;
— лицензия МЧС;
— работа по регламентам и SLA;
— прозрачная отчетность;
— опыт эксплуатации сложных объектов;
— несем ответственность за результат;
— не работаем по принципу «сделали и забыли».
---
7. СТОИМОСТЬ
Очень важный блок.
---
НЕ ДЕЛАЙ:
“от 5000 ₽”.
---
ПРАВИЛЬНО:
Тариф Формат Стоимость
START Небольшие объекты от 25 000 ₽
BUSINESS Коммерческие объекты от 60 000 ₽
ENTERPRISE Внешний инженерный отдел индивидуально
---
8. ЭТАПЫ РАБОТЫ
Это резко повышает доверие.
---
Пример:
1. Технический аудит объекта
2. Формирование регламентов
3. Подписание SLA
4. Постановка объекта на сопровождение
5. Регулярное обслуживание и отчетность
---
9. CTA
Очень важно.
---
НЕ:
“оставьте заявку”.
---
ПРАВИЛЬНО:
Предлагаем провести технический аудит объекта и подготовить рекомендации по снижению эксплуатационных рисков.
---
КАК ДОЛЖНО ВЫГЛЯДЕТЬ КП ВИЗУАЛЬНО
---
НЕ:
“цветастая реклама”;
“маркетинговый шум”.
---
А:
спокойный инженерный документ.
---
ЦВЕТА
темно-синий;
графит;
белый;
серый.
---
СТИЛЬ
Как:
техническая компания;
инженерный аудит;
эксплуатация объектов.
---
САМОЕ ВАЖНОЕ
Твое КП должно вызывать мысль:
> “Эти люди понимают, как работает объект.”
А не:
> “Очередные монтажники.”
---
И ТУТ КЛЮЧЕВОЙ МОМЕНТ
Ты не должен пытаться понравиться всем.
Тебе нужны:
нормальные объекты;
нормальные бюджеты;
нормальные заказчики.
---
ТВОЯ СТРАТЕГИЯ
Не:
“много дешевых клиентов”.
А:
“мало, но качественных объектов на абонентке”.
Это огромная разница в бизнес-модели.
---
Следующий логичный этап:
1. Сделать полноценный шаблон КП (PDF-структуру).
2. Прописать тарифы глубже.
3. Сделать SLA-документ.
4. Подготовить скрипт инженерной продажи.
5. Сделать структуру первого аудита.
6. Составить чек-листы.
7. Построить первую воронку клиентов в Краснодарском крае.
8. Подготовить SEO-структуру сайта.
9. Разработать контент-план для выхода в экспертность.
10. Построить модель роста до 1–3 млн/мес recurring revenue.
@@ -0,0 +1,157 @@
# Формулы и расчёты AegisOne
## 1. Risk Score (Оценка риска объекта)
**Формула:** Сумма весов выявленных нарушений
| Нарушение | Коэффициент (по умолч.) |
|-----------|------------------------|
| Нет архива видеонаблюдения | 25 |
| Нет резервного питания | 20 |
| Нет регламента обслуживания | 15 |
| Частые сбои систем | 20 |
| Нет документации | 10 |
**Максимум:** 100 баллов
**Интерпретация:**
- 020: Низкий
- 2150: Средний
- 5175: Высокий
- 76–100: Критический
---
## 2. Complexity Index (Сложность обслуживания)
**Формула:** Сумма баллов по компонентам
| Компонент | Значение | Коэффициент |
|-----------|----------|-------------|
| Камеры | до 20 | 10 |
| Камеры | 20100 | 20 |
| Камеры | 100+ | 35 |
| Точки СКУД | до 5 | 10 |
| Точки СКУД | 5–20 | 20 |
| Точки СКУД | 20+ | 30 |
| Пожарная | Простая | 15 |
| Пожарная | Средняя | 25 |
| Пожарная | Сложная (>5000 м²) | 40 |
| IT-инфраструктура | Есть | 10 |
**Интерпретация:**
- до 30: Простой
- 3170: Средний
- 71120: Сложный
- 120+: Enterprise
---
## 3. Infrastructure Load (Нагрузка на инфраструктуру)
| Параметр | Состояние | Коэффициент |
|----------|-----------|-------------|
| Сервер | Нет | 15 |
| Сервер | Слабый | 10 |
| Сеть | Нестабильная | 20 |
| Сеть | Частично | 10 |
| UPS | Нет | 20 |
| UPS | Слабый | 10 |
---
## 4. Service History (История обслуживания)
| Состояние | Коэффициент |
|-----------|-------------|
| Не обслуживается | 30 |
| Нерегулярно | 20 |
| Формальный подрядчик | 10 |
| Есть SLA | 0 |
---
## 5. Object Index (Итоговый индекс объекта)
**Формула:**
```
Object Index = Risk × 0.4 + Complexity × 0.3 + Infra × 0.2 + History × 0.1
```
**Классы:**
- A (до 30): Лёгкий SLA
- B (3160): Стандарт SLA
- C (6190): Сложный SLA
- D (90+): Enterprise / Высокий риск
---
## 6. SLA Price (Стоимость SLA)
**Формула:**
```
SLA Price = Base Cost × Object Index × Region Factor × Risk Multiplier
```
| Параметр | Значение |
|----------|----------|
| Base Cost | 15 000 ₽ |
| Region Factor | Краснодар 1.0 / Край 1.1 / РФ 1.3 / Удалённые 1.6 / Москва 1.8 |
**Risk Multiplier:**
- Низкий риск: 1.0
- Средний: 1.3
- Высокий: 1.6
- Критический: 2.0
---
## 7. Engineer Score (ES)
**Формула:** Взвешенная сумма компонентов
| Компонент | Вес |
|-----------|-----|
| SLA Compliance | 0.25 |
| Response Time Score | 0.20 |
| Resolution Time Score | 0.20 |
| Diagnosis Accuracy | 0.15 |
| Reopen Rate Score | 0.10 |
| Risk Coverage Score | 0.10 |
**Грейды:**
- 90+: Senior
- 8089: Strong
- 7079: Middle
- <70: Junior
---
## 8. SHS (System Health Score)
**Формула:**
```
SHS = SLA Stability × 0.22 + Revenue Stability × 0.18 + Retention × 0.18
+ Engineer Performance × 0.15 + Incident Stability × 0.12
+ Sales Flow × 0.10 + Operational Efficiency × 0.05
```
**Статусы:**
- 85+: Growth
- 7084: Stable
- 5069: Risk
- <50: Crisis
---
## Автоматические правила
| Условие | Действие |
|---------|----------|
| SHS < 70 | Снизить продажи, запустить аудит, пересмотреть назначения |
| SLA Compliance < 90% | Заморозить неприоритетные проекты, увеличить частоту проверок |
| Retention < 90% | Обязательный аудит клиентов, пересмотр назначений |
---
Все коэффициенты можно изменить в разделе **Коэффициенты** админ-панели. Изменения применяются к новым расчётам.
@@ -0,0 +1,458 @@
Ниже — единая цифровая операционная модель AegisOne Engineering в формате .md.
Это не просто документация — это “скелет системы”, который можно отдать разработчику, внедрить в Notion/Bitrix/1C/веб-CRM или развернуть как внутренний продукт.
---
# AegisOne Engineering
## Unified Digital Operating System (UDOS)
---
# 0. СУТЬ СИСТЕМЫ
AegisOne UDOS — это единая цифровая система управления инженерной эксплуатацией объектов безопасности.
Она объединяет:
- CRM (клиенты и сделки)
- SLA (контракты и обслуживание)
- KPI инженеров (исполнение)
- SHS (здоровье бизнеса)
- Финансовую модель
- Операционный дашборд
---
# 1. АРХИТЕКТУРА СИСТЕМЫ
┌──────────────────────┐
│ LEADS / CRM │
└─────────┬────────────┘
┌──────────────────────┐
│ AUDIT SYSTEM │
└─────────┬────────────┘
┌──────────────────────┐
│ SLA CONTRACTS │
└─────────┬────────────┘
┌─────────────────────────────────────────┐
│ OPERATIONS (ENGINEERS) │
└─────────┬───────────────┬──────────────┘
↓ ↓
┌──────────────┐ ┌─────────────────────┐
│ KPI ENGINEERS │ │ INCIDENT SYSTEM │
└──────┬───────┘ └─────────┬───────────┘
↓ ↓
┌─────────────────────────────────────────┐
│ SHS ENGINE │
└──────────────┬──────────────────────────┘
┌─────────────────────────────────────────┐
│ CEO DASHBOARD │
└─────────────────────────────────────────┘
---
# 2. CRM СИСТЕМА
## 2.1 Структура клиента
```json
Client {
id,
company_name,
object_type,
location,
contact_person,
decision_maker,
number_of_objects,
systems: {
video: int,
access_control: int,
fire_alarm: bool,
it_infrastructure: bool
},
current_provider,
pain_points[],
budget_level,
status: [lead, qualified, audit, offer, sla, lost]
}
---
2.2 Воронка CRM
Lead
Qualification
Technical Audit
Risk Report
SLA Offer
Contract
Active SLA
---
3. AUDIT SYSTEM (ИНЖЕНЕРНЫЙ ВХОД)
3.1 Формирование Risk Score
Risk Score =
missing_archive (25)
+ no_power_backup (20)
+ no_regulations (15)
+ system_failures (20)
+ no_documentation (10)
---
3.2 Output аудита
Audit Report:
- Risk Score
- System condition
- Critical vulnerabilities
- SLA recommendation
- Cost estimation
---
4. SLA СИСТЕМА
4.1 Формула стоимости
SLA Price =
Base Cost × Object Index × Region Factor × Risk Multiplier
---
4.2 Object Index
Object Index =
(Risk × 0.4)
+ (Complexity × 0.3)
+ (Infrastructure × 0.2)
+ (Service History × 0.1)
---
4.3 SLA уровни
Class Index Description
A 030 simple SLA
B 3160 standard SLA
C 6190 complex SLA
D 90+ enterprise SLA
---
4.4 SLA метрики
Response Time
Resolution Time
Uptime %
Incident Rate
---
5. KPI СИСТЕМА ИНЖЕНЕРОВ
5.1 Engineer Score
ES =
(0.25 × SLA Compliance)
+ (0.20 × Response Time Score)
+ (0.20 × Resolution Time Score)
+ (0.15 × Diagnosis Accuracy)
+ (0.10 × Reopen Rate Score)
+ (0.10 × Documentation Quality)
---
5.2 KPI метрики
SLA Compliance
closed_in_SLA / total_requests
Reopen Rate
reopened_requests / total_requests
Diagnosis Accuracy
confirmed_faults / found_faults
Utilization
working_hours / available_hours
---
5.3 Грейды инженеров
Score Level
90100 Senior
8089 Strong
7079 Middle
<70 Junior
---
6. SHS (SYSTEM HEALTH SCORE)
6.1 Формула SHS
SHS =
(0.22 × SLA Stability)
+ (0.18 × Revenue Stability)
+ (0.18 × Retention)
+ (0.15 × Engineer Performance)
+ (0.12 × Incident Stability)
+ (0.10 × Sales Flow)
+ (0.05 × Operational Efficiency)
---
6.2 Интерпретация
SHS Status
85100 Growth
7085 Stable
5070 Risk
<50 Crisis
---
6.3 Alert System
SHS ↓ 5% weekly → Warning
SHS ↓ 10% → Critical
Retention drop → Immediate audit trigger
---
7. INCIDENT SYSTEM
7.1 Incident Classification
Level Description
P1 critical system failure
P2 partial failure
P3 minor issue
---
7.2 Incident Rate
IR = incidents / number_of_objects
---
7.3 SLA breach tracking
breach_rate = failed_SLA_cases / total_cases
---
8. FINANCIAL LAYER
8.1 MRR
MRR = Σ SLA contracts / 12
---
8.2 Revenue per Engineer
RPE = SLA Revenue / number_of_engineers
---
8.3 Cost per Object
CPO = total_operational_cost / number_of_objects
---
9. CEO DASHBOARD
9.1 Main indicators
SHS (system health)
MRR (monthly recurring revenue)
SLA Compliance
Retention
Incident Rate
Engineer Score Avg
Sales Conversion
---
9.2 Dashboard visualization blocks
1. SHS Line Chart
trend over time
2. SLA Heatmap
engineers vs performance
3. MRR Growth Curve
monthly recurring revenue
4. Client Retention Funnel
drop-off points
5. Incident Map
problematic objects
6. Engineer Ranking Table
top/bottom performers
---
10. AUTOMATION RULES
10.1 If SHS drops
IF SHS < 70:
reduce new sales
trigger audit cycle
reassign engineers
---
10.2 If SLA compliance drops
IF SLA < 90%:
freeze non-critical projects
increase inspection frequency
---
10.3 If retention drops
IF retention < 90%:
mandatory client audit
review engineer assignments
---
11. SYSTEM BEHAVIOR MODEL
DATA → KPI → SHS → DECISION → ACTION → FEEDBACK LOOP
---
12. STRATEGIC PRINCIPLE
AegisOne Engineering is not a service company.
It is:
> an operating system for managing engineering risk in physical infrastructure.
---
13. END STATE MODEL
The system evolves into:
predictive SLA risk detection
automated pricing
engineer auto-allocation
client churn prediction
MRR forecasting
---
END OF SYSTEM
---
Если хочешь следующий уровень, я могу собрать тебе:
1) визуальный UI дашборда (как он должен выглядеть как продукт)
2) структуру базы данных (SQL / 1C / Bitrix schema)
3) алгоритм “предикта потери клиента за 14 дней”
4) или автоматический генератор КП + SLA из этой системы
Это уже уровень **enterprise SaaS, а не просто инженерной компании**.
@@ -0,0 +1,629 @@
Ниже — как должна работать автоматическая система SHS (System Health Score) в твоей модели AegisOne Engineering.
Это уже не “аналитика бизнеса”, а:
> инженерная система раннего предупреждения о деградации компании
---
0. ЧТО ТАКОЕ SHS (СУТЬ СИСТЕМЫ)
SHS — это единый индекс здоровья бизнеса (0–100), который показывает:
насколько стабильно работает SLA-модель
где теряются деньги
где растут риски разрыва контрактов
где перегружается команда
где падает качество эксплуатации
---
1. ЛОГИКА СИСТЕМЫ SHS
Данные (CRM + SLA + инженеры)
Нормализация метрик
Взвешивание показателей
Расчёт SHS
Детектирование отклонений
Сигналы риска (alerts)
---
2. ФИНАЛЬНАЯ ФОРМУЛА SHS
SHS =
(0.22 × SLA Stability Index) +
(0.18 × Revenue Stability Index) +
(0.18 × Retention Index) +
(0.15 × Engineering Performance Index) +
(0.12 × Incident Stability Index) +
(0.10 × Sales Flow Index) +
(0.05 × Operational Efficiency Index)
---
3. РАСШИФРОВКА ВСЕХ КОМПОНЕНТОВ
---
3.1 SLA STABILITY INDEX (критический)
Смысл:
Насколько система выполняет SLA без сбоев.
---
Формула:
SSI = SLA_compliance × (1 - SLA_breach_severity)
---
Где:
SLA_compliance = выполненные заявки / все заявки
SLA_breach_severity = тяжесть нарушений (0–1)
---
Пример:
compliance = 0.96
нарушения = 0.1
→ SSI = 0.864
---
Риски:
падение ниже 0.85 = начинается утечка клиентов
---
3.2 REVENUE STABILITY INDEX
Смысл:
Стабильность денежного потока SLA
---
Формула:
RSI = 1 - (σ(MRR) / mean(MRR))
---
Интерпретация:
чем меньше колебания → тем выше индекс
---
Риски:
RSI < 0.7 → нестабильная финансовая модель
---
3.3 RETENTION INDEX (очень критично)
Формула:
RI = retained_clients / total_clients
---
Дополнение:
штраф за уход крупных клиентов:
RI_adjusted = RI - (lost_key_clients × 0.1)
---
Риски:
падение ниже 0.9 = системная проблема SLA
---
3.4 ENGINEERING PERFORMANCE INDEX
Смысл:
качество работы инженеров
---
Формула:
EPI =
(0.3 × SLA compliance engineers) +
(0.25 × diagnosis accuracy) +
(0.2 × reopen rate inverse) +
(0.15 × response time score) +
(0.1 × documentation quality)
---
Важный момент:
это единственный KPI, который напрямую влияет на:
> удержание клиентов
---
Риски:
EPI < 0.75 → будущие потери клиентов через 1–2 месяца
---
3.5 INCIDENT STABILITY INDEX
Смысл:
насколько система “ломается”
---
Формула:
ISI = 1 - (incidents / objects × severity_weight)
---
Где severity_weight:
критический = 1.0
средний = 0.5
низкий = 0.2
---
Риски:
рост ISI вниз = деградация инфраструктуры
---
3.6 SALES FLOW INDEX
Смысл:
здоровье входящего потока денег
---
Формула:
SFI = (audits → SLA conversion rate) × lead quality
---
Пример:
конверсия 0.35
качество лидов 0.8
→ SFI = 0.28
---
Риски:
SFI < 0.25 → нет роста MRR
---
3.7 OPERATIONAL EFFICIENCY INDEX
Смысл:
насколько эффективно работает компания внутри
---
Формула:
OEI = revenue / (engineer_hours × cost)
---
Риски:
падение → перегруз команды
---
4. ИНТЕРПРЕТАЦИЯ SHS
---
SHS Состояние системы
85–100 масштабируемый рост
70–85 стабильная работа
50–70 скрытые проблемы
<50 системный кризис
---
5. ГЛАВНАЯ СИЛА SHS — НЕ ЧИСЛО, А ДИНАМИКА
---
ВАЖНО:
Ты смотришь не на значение, а на:
Δ SHS (изменение)
ΔSHS = SHS_today - SHS_last_week
---
Критично:
падение > 5 пунктов за неделю → тревога
падение > 10 → кризис
---
6. СИСТЕМА АВТОМАТИЧЕСКИХ СИГНАЛОВ
---
GREEN ZONE:
SHS > 80
нет действий
---
YELLOW ZONE:
SHS 6580
👉 действия:
проверить инженеров
проверить SLA просадки
проверить загрузку
---
RED ZONE:
SHS < 65
👉 действия:
аудит клиентов
пересмотр инженеров
срочный анализ SLA нарушений
заморозка новых продаж
---
7. КОГДА ПОЯВЛЯЮТСЯ РИСКИ
---
РИСК №1 — падение EPI
Причина:
инженеры начали “делать быстро, но плохо”
Симптом:
рост повторных заявок
жалобы клиентов через 2–3 недели
---
РИСК №2 — рост INCIDENT RATE
Причина:
старое оборудование
плохая эксплуатация
---
РИСК №3 — падение RETENTION
Самый опасный
👉 означает:
> клиент уже не верит системе
---
РИСК №4 — нестабильный SFI
Причина:
слабый маркетинг
неправильный аудит
некачественные лиды
---
РИСК №5 — падение OEI
Причина:
перегруз инженеров
хаотичные выезды
нет стандартизации
---
8. КАК ИСПРАВЛЯТЬ РИСКИ
---
ЕСЛИ ПАДАЕТ SHS:
---
ШАГ 1 — локализация
разбить SHS на компоненты:
где падение?
SLA?
инженеры?
продажи?
---
ШАГ 2 — точечное вмешательство
проблема действие
EPI падает обучение инженеров + чек-листы
retention падает аудит объектов
SLA падает перераспределение нагрузки
SFI падает фильтрация лидов
---
ШАГ 3 — стабилизация
ограничить новые продажи
усилить контроль SLA
снизить нагрузку
---
9. ВАЖНОЕ ПРАВИЛО СИСТЕМЫ
---
> нельзя “растить бизнес”, если SHS падает
---
10. ВИЗУАЛИЗАЦИЯ SHS (CEO DASHBOARD)
---
1. SHS LINE
📈 динамика здоровья компании
---
2. STACKED INDEX CHART
SLA
Revenue
Engineers
Retention
---
3. ALERT MAP
🟥 проблемные зоны
---
4. ENGINEER IMPACT MAP
📍 какой инженер влияет на SHS
---
11. ГЛАВНЫЙ СМЫСЛ SHS
---
Ты больше не управляешь:
❌ людьми
❌ объектами
❌ заявками
---
Ты управляешь:
системой стабильности инженерного бизнеса
---
12. ЕСЛИ ДАЛЬШЕ РАЗВИВАТЬ
Я могу собрать:
автоматический SHS калькулятор (Excel / 1C / web)
real-time dashboard
систему предиктивных предупреждений (падение SHS за 7–14 дней)
интеграцию SHS в KPI инженеров
систему автоматического “замедления продаж” при риске
---
ИТОГ
SHS — это не метрика.
> это “нервная система” твоей компании, которая заранее показывает, где бизнес начнёт терять деньги или клиентов
---
Если хочешь дальше, следующий логичный шаг:
сделать тебе полноценную цифровую модель AegisOne (дашборд + CRM + SHS + SLA + KPI инженеров в одной системе)
+139
View File
@@ -0,0 +1,139 @@
<?php
require_once __DIR__ . '/../../config.php';
require_once __DIR__ . '/db.php';
/**
* ---------- SESSION ----------
*/
function auth_session_start(): void {
if (session_status() === PHP_SESSION_NONE) {
session_set_cookie_params([
'lifetime' => SERVICE_SESSION_TTL,
'path' => '/service/',
'secure' => !empty($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Strict',
]);
session_name('AEGIS_SRV');
session_start();
}
}
/**
* ---------- LOGIN ----------
*/
define('BRUTE_FORCE_WINDOW', 180);
define('BRUTE_FORCE_MAX_ATTEMPTS', 3);
function auth_brute_force_check(string $login): bool {
$pdo = getDB();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM login_attempts WHERE (login = ? OR ip_address = ?) AND attempt_time > DATE_SUB(NOW(), INTERVAL ? SECOND)");
$stmt->execute([$login, $_SERVER['REMOTE_ADDR'] ?? '', BRUTE_FORCE_WINDOW]);
return $stmt->fetchColumn() >= BRUTE_FORCE_MAX_ATTEMPTS;
}
function auth_record_attempt(string $login): void {
$pdo = getDB();
$stmt = $pdo->prepare("INSERT INTO login_attempts (ip_address, login) VALUES (?, ?)");
$stmt->execute([$_SERVER['REMOTE_ADDR'] ?? '', $login]);
}
function auth_clear_attempts(string $login): void {
$pdo = getDB();
$stmt = $pdo->prepare("DELETE FROM login_attempts WHERE login = ? OR ip_address = ?");
$stmt->execute([$login, $_SERVER['REMOTE_ADDR'] ?? '']);
}
function auth_login(string $login, string $password): array {
$pdo = getDB();
if (auth_brute_force_check($login)) {
return ['success' => false, 'error' => 'Превышен лимит попыток. Подождите 3 минуты.'];
}
$stmt = $pdo->prepare("SELECT id, login, password_hash, role, full_name, is_active FROM users WHERE login = ? LIMIT 1");
$stmt->execute([$login]);
$user = $stmt->fetch();
if (!$user || !password_verify($password, $user['password_hash'])) {
auth_record_attempt($login);
return ['success' => false, 'error' => 'Неверный логин или пароль.'];
}
if (!$user['is_active']) {
return ['success' => false, 'error' => 'Учётная запись заблокирована.'];
}
auth_clear_attempts($login);
$_SESSION['user_id'] = (int)$user['id'];
$_SESSION['user_login'] = $user['login'];
$_SESSION['user_role'] = $user['role'];
$_SESSION['user_name'] = $user['full_name'];
$_SESSION['auth_time'] = time();
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
auth_log($user['id'], 'login', 'Вход в систему');
return ['success' => true];
}
/**
* ---------- LOGOUT ----------
*/
function auth_logout(): void {
if (!empty($_SESSION['user_id'])) {
auth_log($_SESSION['user_id'], 'logout', 'Выход из системы');
}
$_SESSION = [];
if (ini_get('session.use_cookies')) {
setcookie(session_name(), '', time() - 42000, '/service/');
}
session_destroy();
}
/**
* ---------- GUARD ----------
*/
function auth_require(): array {
auth_session_start();
$allowed_roles = func_get_args();
if (empty($_SESSION['user_id'])) {
header('Location: /service/index.php?redirect=' . urlencode($_SERVER['REQUEST_URI']));
exit;
}
if (!empty($allowed_roles) && !in_array($_SESSION['user_role'], $allowed_roles, true)) {
http_response_code(403);
require __DIR__ . '/../pages/shared/403.php';
exit;
}
return $_SESSION;
}
function auth_has_role(string $role): bool {
return isset($_SESSION['user_role']) && $_SESSION['user_role'] === $role;
}
/**
* ---------- CSRF ----------
*/
function auth_csrf_token(): string {
return $_SESSION['csrf_token'] ?? '';
}
function auth_csrf_verify(string $token): bool {
return hash_equals($_SESSION['csrf_token'] ?? '', $token);
}
/**
* ---------- AUDIT LOG ----------
*/
function auth_log(?int $userId, string $action, string $details = ''): void {
try {
$pdo = getDB();
$stmt = $pdo->prepare("INSERT INTO audit_log (user_id, action, details, ip_address) VALUES (?, ?, ?, ?)");
$stmt->execute([$userId, $action, $details, $_SERVER['REMOTE_ADDR'] ?? '']);
} catch (\Throwable $e) {
// silent
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
require_once __DIR__ . '/../../data/db-config.php';
function getDB(): PDO {
static $pdo = null;
if ($pdo === null) {
if (DB_TYPE === 'pgsql') {
$dsn = 'pgsql:host=' . DB_HOST . ';port=' . DB_PORT . ';dbname=' . DB_NAME;
} else {
$dsn = 'mysql:host=' . DB_HOST . ';port=' . DB_PORT . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET;
}
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
return $pdo;
}
+379
View File
@@ -0,0 +1,379 @@
<?php
/**
* AegisOne Engineering Функции управления документацией в Markdown
*
* Обеспечивает:
* - Автоматическое обнаружение .md-файлов в service/docs/
* - Разграничение доступа по ролям (owner всё, engineer только разрешённое)
* - Хранение прав в JSON-файле service/permissions/docs_permissions.json
*/
define('DOCS_DIR', __DIR__ . '/../docs');
define('DOCS_PERMISSIONS_FILE', __DIR__ . '/../permissions/docs_permissions.json');
/**
* Сканирует папку docs/ и возвращает массив документов.
* Автоматически добавляет новые файлы в файл прав (если их там нет).
*
* @return array Массив документов: [['slug'=>'...', 'title'=>'...', 'filename'=>'...'], ...]
*/
function get_docs_list(): array
{
if (!is_dir(DOCS_DIR)) {
return [];
}
$files = glob(DOCS_DIR . '/*.md');
if ($files === false || empty($files)) {
return [];
}
$docs = [];
$permissions = load_permissions();
foreach ($files as $path) {
$filename = basename($path);
$slug = doc_slug($filename);
$title = doc_title($slug, $filename);
// Автоматически добавляем новый документ в права, если его ещё нет
if (!isset($permissions[$slug])) {
$permissions[$slug] = [
'title' => $title,
'filename' => $filename,
'sort_order' => count($permissions),
'engineer' => false,
];
}
// Миграция: старый boolean → новый array permissions
if (isset($permissions[$slug]['engineer']) && !isset($permissions[$slug]['permissions'])) {
$permissions[$slug]['permissions'] = [
'engineer' => [
'view' => !empty($permissions[$slug]['engineer']),
'edit' => false,
'cancel' => false,
]
];
}
// Если файл на диске отличается от сохранённого — обновляем
if ($permissions[$slug]['filename'] !== $filename) {
$permissions[$slug]['filename'] = $filename;
}
$docs[] = [
'slug' => $slug,
'title' => $permissions[$slug]['title'],
'filename' => $filename,
'sort_order' => $permissions[$slug]['sort_order'] ?? 0,
];
}
// Сохраняем, если были добавлены новые
save_permissions($permissions);
// Сортировка по sort_order (ручная), затем по title
usort($docs, function($a, $b) {
$soA = $a['sort_order'] ?? 0;
$soB = $b['sort_order'] ?? 0;
if ($soA !== $soB) return $soA - $soB;
return strcmp($a['title'], $b['title']);
});
return $docs;
}
/**
* Возвращает содержимое .md-файла по слагу.
* Ищет файл по slug, сканируя директорию (не полагается на сохранённое имя).
*
* @param string $slug Слаг документа
* @return string|null Содержимое файла или null, если не найден
*/
function get_doc_content(string $slug): ?string
{
$permissions = load_permissions();
if (!isset($permissions[$slug])) {
return null;
}
// Сначала пробуем сохранённое имя файла
$savedFilename = $permissions[$slug]['filename'];
$path = DOCS_DIR . '/' . $savedFilename;
if (file_exists($path)) {
$content = file_get_contents($path);
return $content !== false ? $content : null;
}
// Если не нашли — ищем по slug среди всех .md файлов
$files = glob(DOCS_DIR . '/*.md');
if ($files === false) return null;
foreach ($files as $filePath) {
$fn = basename($filePath);
$fSlug = doc_slug($fn);
if ($fSlug === $slug) {
// Обновляем сохранённое имя
$permissions[$slug]['filename'] = $fn;
save_permissions($permissions);
$content = file_get_contents($filePath);
return $content !== false ? $content : null;
}
}
return null;
}
/**
* Преобразует md-заголовки и ссылки в HTML.
* Простейший парсер для базовой читаемости.
*
* @param string $markdown Исходный Markdown
* @return string HTML
*/
function render_markdown_simple(string $markdown): string
{
$html = htmlspecialchars($markdown, ENT_NOQUOTES, 'UTF-8');
// Заголовки h1-h3
$html = preg_replace('/^### (.+)$/m', '<h3>$1</h3>', $html);
$html = preg_replace('/^## (.+)$/m', '<h2>$1</h2>', $html);
$html = preg_replace('/^# (.+)$/m', '<h1>$1</h1>', $html);
// Жирный и курсив
$html = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $html);
$html = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $html);
// Списки
$html = preg_replace('/^- (.+)$/m', '<li>$1</li>', $html);
$html = preg_replace('/(<li>.*<\/li>)/s', '<ul>$1</ul>', $html);
// Таблицы — группируем строки с | в блоки <table>
$lines = explode("\n", $html);
$in_table = false;
$sep_skipped = false;
$table_lines = [];
foreach ($lines as $line) {
$trimmed = trim($line);
if (preg_match('/^\|.+\|$/', $trimmed)) {
if (!$in_table) {
$table_lines[] = '<table>';
$in_table = true;
$sep_skipped = false;
}
if (preg_match('/^\|[-:\s]+\|$/', $trimmed)) {
$sep_skipped = true;
continue;
}
$inner = substr($trimmed, 1, -1);
$cells = array_map('trim', explode('|', $inner));
$tag = $sep_skipped ? 'th' : 'td';
$sep_skipped = false;
$table_lines[] = "<tr><$tag>" . implode("</$tag><$tag>", $cells) . "</$tag></tr>";
} else {
if ($in_table) {
$table_lines[] = '</table>';
$in_table = false;
}
$sep_skipped = false;
$table_lines[] = $line;
}
}
if ($in_table) {
$table_lines[] = '</table>';
}
$html = implode("\n", $table_lines);
// Ссылки
$html = preg_replace('/\[(.+?)\]\((.+?)\)/', '<a href="$2" target="_blank">$1</a>', $html);
// Абзацы
$paragraphs = explode("\n\n", $html);
$html = '';
foreach ($paragraphs as $p) {
$p = trim($p);
if ($p === '') continue;
if (strpos($p, '<h') === 0 || strpos($p, '<ul') === 0 || strpos($p, '<tr') === 0 || strpos($p, '<th') === 0 || strpos($p, '<table') === 0) {
$html .= $p . "\n";
} elseif (strpos($p, '<li') === 0) {
$html .= $p . "\n";
} else {
$html .= '<p>' . nl2br($p) . "</p>\n";
}
}
return $html;
}
/**
* Возвращает slug из имени файла (архитектура.md arkhitektura).
*/
function doc_slug(string $filename): string
{
$slug = pathinfo($filename, PATHINFO_FILENAME);
// Транслитерация кириллицы в латиницу для URL
$converted = transliterate($slug);
return preg_replace('/[^a-z0-9_-]/', '', strtolower($converted));
}
/**
* Транслитерация кириллицы латиница.
*/
function transliterate(string $text): string
{
$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','А'=>'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',' '=>'-','_'=>'-',
];
return strtr($text, $map);
}
/**
* Формирует человекочитаемый заголовок из слага.
* architecture Архитектура
*/
function doc_title(string $slug, string $filename = ''): string
{
// обратная транслитерация для некоторых известных слагов
$known = [
'owner' => 'Owner',
'seo' => 'SEO',
'sla' => 'SLA',
'kpi-inzhenerov' => 'KPI инженеров',
'avtomaticheskaya-sistema-shs' => 'Автоматическая система SHS',
'oprosnik-raschet' => 'Опросник-расчёт',
'arkhitektura-kompanii' => 'Архитектура компании',
'kontent-plan' => 'Контент план',
'kp-i-struktura-audita' => 'КП и структура аудита',
'utp-i-kp' => 'УТП и КП',
'arkhetiktura-brenda' => 'Архетиктура бренда',
'marketing' => 'Маркеттинг',
'skripty-prodazh' => 'Скрипты продаж',
'osnova' => 'Основа',
'dokazatelnaya-ekspertiza' => 'Доказательная экспертиза',
'voronki-telegram-i-crm' => 'Воронки телеграм и CRM',
'voronka' => 'Воронка',
'sayt-i-upakovka' => 'Сайт и упаковка',
'dashboard-tekhnika' => 'Дашборд техника',
'dashboard-inzhenera' => 'Дашборд инженера',
'blog' => 'Блог',
'tsifrovaya-model-obshchaya' => 'Цифровая модель общая',
];
if (isset($known[$slug])) {
return $known[$slug];
}
// Если есть имя файла — используем его без .md
if ($filename !== '') {
return pathinfo($filename, PATHINFO_FILENAME);
}
return strtr($slug, ['-' => ' ', '_' => ' ']);
}
/**
* Загружает JSON-файл прав доступа.
*
* @return array
*/
function load_permissions(): array
{
if (!file_exists(DOCS_PERMISSIONS_FILE)) {
return [];
}
$json = file_get_contents(DOCS_PERMISSIONS_FILE);
$data = json_decode($json, true);
return is_array($data) ? $data : [];
}
/**
* Сохраняет JSON-файл прав доступа.
*
* @param array $permissions
* @return bool
*/
function save_permissions(array $permissions): bool
{
$dir = dirname(DOCS_PERMISSIONS_FILE);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
return file_put_contents(
DOCS_PERMISSIONS_FILE,
json_encode($permissions, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
) !== false;
}
/**
* Проверяет, разрешён ли документ для роли engineer.
*
* @param string $slug Слаг документа
* @return bool
*/
function is_doc_allowed_for_engineer(string $slug): bool
{
$permissions = load_permissions();
if (!isset($permissions[$slug])) return false;
$p = $permissions[$slug];
// Поддержка старого формата (boolean) и нового (array)
if (is_array($p) && isset($p['permissions'])) {
return !empty($p['permissions']['engineer']['view']);
}
return !empty($p['engineer']);
}
/**
* Проверяет конкретное право инженера для документа.
*
* @param string $slug Слаг документа
* @param string $action Действие: view, edit, cancel
* @return bool
*/
function is_engineer_allowed(string $slug, string $action): bool
{
$permissions = load_permissions();
if (!isset($permissions[$slug])) return false;
$p = $permissions[$slug];
if (is_array($p) && isset($p['permissions']['engineer'])) {
return !empty($p['permissions']['engineer'][$action]);
}
// Backward compat
if ($action === 'view') return !empty($p['engineer']);
return false;
}
/**
* Возвращает список документов, доступных для указанной роли.
*
* @param string $role Роль пользователя (owner|engineer|technician)
* @return array
*/
function get_allowed_docs(string $role): array
{
$all = get_docs_list();
if ($role === 'owner') {
return $all;
}
if ($role === 'engineer') {
return array_values(array_filter($all, function($d) {
return is_doc_allowed_for_engineer($d['slug']);
}));
}
// technician — нет доступа к документам
return [];
}
+31
View File
@@ -0,0 +1,31 @@
<?php if (empty($_SESSION['user_id'])) return; ?>
</main>
</div>
<script>
(function(){
var sb = document.querySelector('.sidebar-nav');
if (!sb) return;
var key = 'sidebar_scroll';
var saved = localStorage.getItem(key);
if (saved) { sb.scrollTop = parseInt(saved, 10); }
window.addEventListener('beforeunload', function(){
localStorage.setItem(key, sb.scrollTop);
});
var scrollTimer;
sb.addEventListener('scroll', function(){
clearTimeout(scrollTimer);
scrollTimer = setTimeout(function(){
localStorage.setItem(key, sb.scrollTop);
}, 100);
});
})();
document.addEventListener('click', function(e) {
var sb = document.querySelector('.sidebar');
var btn = document.querySelector('.sidebar-toggle');
if (sb && sb.classList.contains('open') && !sb.contains(e.target) && btn && !btn.contains(e.target)) {
sb.classList.remove('open');
}
});
</script>
</body>
</html>
+678
View File
@@ -0,0 +1,678 @@
<?php
/**
* ============================================
* AegisOne Engineering Расчётные формулы
* ============================================
*
* Все коэффициенты вынесены в БД (таблица formula_coefficients)
* и редактируются владельцем через /service/pages/owner/coefficients.php.
*
* Хардкоды ЗАПРЕЩЕНЫ используй get_coefficient('key').
*
* Формат ключа: <формула>.<параметр>, например:
* risk_score.no_archive 25
* object_index.risk_weight 0.4
* sla_price.base_cost 15000
*/
// ========================================================================
// ВСПОМОГАТЕЛЬНЫЕ
// ========================================================================
/**
* Получить значение коэффициента из БД.
* Результат кэшируется в статической переменной (один SQL-запрос за вызов).
*
* @param string $key Ключ коэффициента (напр. 'risk_score.no_archive')
* @param float $default Значение по умолчанию, если ключ не найден
* @return float
*/
function get_coefficient(string $key, float $default = 0): float {
static $cache = null;
if ($cache === null) {
try {
$pdo = getDB();
$stmt = $pdo->query("SELECT `key`, `value` FROM formula_coefficients WHERE is_active=1");
$cache = [];
while ($row = $stmt->fetch()) {
$cache[$row['key']] = (float)$row['value'];
}
} catch (\Throwable $e) {
$cache = [];
}
}
return $cache[$key] ?? $default;
}
/**
* Форматирование: 1 234 567
*/
function fmt_money(float $amount): string {
return number_format($amount, 0, '.', ' ') . ' ₽';
}
/**
* Форматирование: 95.3%
*/
function fmt_percent(float $value): string {
return number_format($value, 1, '.', '') . '%';
}
/**
* Форматирование: 3.5 ч
*/
function fmt_hours(float $hours): string {
return number_format($hours, 1, '.', '') . ' ч';
}
/**
* CSS-класс для статуса / приоритета
*/
function badge_class(string $status, string $type = 'default'): string {
$map = [
'P1' => 'badge-critical', 'P2' => 'badge-high', 'P3' => 'badge-medium', 'P4' => 'badge-low',
'open' => 'badge-open', 'in_progress' => 'badge-progress', 'completed' => 'badge-done', 'cancelled' => 'badge-cancelled',
'active' => 'badge-active', 'inactive' => 'badge-inactive', 'draft' => 'badge-draft', 'final' => 'badge-done',
'Risk' => 'badge-risk', 'Crisis' => 'badge-crisis', 'Stable' => 'badge-stable', 'Growth' => 'badge-growth',
];
return $map[$status] ?? 'badge-default';
}
// ========================================================================
// 1. RISK SCORE (ОЦЕНКА РИСКА ОБЪЕКТА)
// ========================================================================
// Формула: сумма весов нарушений (каждое — 0 или 1).
// Веса хранятся в БД:
// risk_score.no_archive — нет архива видеонаблюдения (умолч. 25)
// risk_score.no_power_backup — нет резервного питания (умолч. 20)
// risk_score.no_regulations — нет регламента обслуживания (умолч. 15)
// risk_score.system_failures — частые сбои систем (умолч. 20)
// risk_score.no_documentation — нет документации (умолч. 10)
function calc_risk_score(bool $noArchive, bool $noPowerBackup, bool $noRegulations, bool $systemFailures, bool $noDocumentation): float {
return ($noArchive ? get_coefficient('risk_score.no_archive', 25) : 0)
+ ($noPowerBackup ? get_coefficient('risk_score.no_power_backup', 20) : 0)
+ ($noRegulations ? get_coefficient('risk_score.no_regulations', 15) : 0)
+ ($systemFailures ? get_coefficient('risk_score.system_failures', 20) : 0)
+ ($noDocumentation ? get_coefficient('risk_score.no_documentation', 10) : 0);
}
/**
* Интерпретация Risk Score
* risk_score.bound_low граница низкого риска (умолч. 20)
* risk_score.bound_medium граница среднего (умолч. 50)
* risk_score.bound_high граница высокого (умолч. 75)
*/
function risk_label(float $score): string {
if ($score <= get_coefficient('risk_score.bound_low', 20)) return 'Низкий';
if ($score <= get_coefficient('risk_score.bound_medium', 50)) return 'Средний';
if ($score <= get_coefficient('risk_score.bound_high', 75)) return 'Высокий';
return 'Критический';
}
/**
* Множитель риска для расчёта SLA Price
* risk_multiplier.low (0-20) умолч. 1.0
* risk_multiplier.medium (21-50) умолч. 1.3
* risk_multiplier.high (51-75) умолч. 1.6
* risk_multiplier.critical— (76-100) умолч. 2.0
*/
function risk_multiplier(float $score): float {
if ($score <= get_coefficient('risk_score.bound_low', 20)) return get_coefficient('risk_multiplier.low', 1.0);
if ($score <= get_coefficient('risk_score.bound_medium', 50)) return get_coefficient('risk_multiplier.medium', 1.3);
if ($score <= get_coefficient('risk_score.bound_high', 75)) return get_coefficient('risk_multiplier.high', 1.6);
return get_coefficient('risk_multiplier.critical', 2.0);
}
// ========================================================================
// 2. SLA COMPLEXITY INDEX (СЛОЖНОСТЬ ОБСЛУЖИВАНИЯ)
// ========================================================================
// Параметры:
// complexity.video_0_20 — до 20 камер (умолч. 10)
// complexity.video_20_100 — 20-100 камер (умолч. 20)
// complexity.video_100plus — 100+ камер (умолч. 35)
// complexity.access_0_5 — до 5 точек СКУД (умолч. 10)
// complexity.access_5_20 — 5-20 точек (умолч. 20)
// complexity.access_20plus — 20+ точек (умолч. 30)
// complexity.fire_simple — простая пожарка (умолч. 15)
// complexity.fire_medium — средняя (умолч. 25)
// complexity.fire_complex — сложная (>5000 м²) (умолч. 40)
// complexity.it — IT-инфраструктура (умолч. 10)
function calc_complexity(int $cameras, int $accessPoints, string $fireType, bool $hasIT): float {
$video = $cameras <= 20 ? get_coefficient('complexity.video_0_20', 10)
: ($cameras <= 100 ? get_coefficient('complexity.video_20_100', 20)
: get_coefficient('complexity.video_100plus', 35));
$access = $accessPoints <= 5 ? get_coefficient('complexity.access_0_5', 10)
: ($accessPoints <= 20 ? get_coefficient('complexity.access_5_20', 20)
: get_coefficient('complexity.access_20plus', 30));
$fire = $fireType === 'simple' ? get_coefficient('complexity.fire_simple', 15)
: ($fireType === 'medium' ? get_coefficient('complexity.fire_medium', 25)
: ($fireType === 'complex' ? get_coefficient('complexity.fire_complex', 40)
: 0));
$it = $hasIT ? get_coefficient('complexity.it', 10) : 0;
return $video + $access + $fire + $it;
}
function complexity_label(float $score): string {
if ($score <= 30) return 'Простой';
if ($score <= 70) return 'Средний';
if ($score <= 120) return 'Сложный';
return 'Enterprise';
}
// ========================================================================
// 3. INFRASTRUCTURE LOAD (НАГРУЗКА НА ИНФРАСТРУКТУРУ)
// ========================================================================
// infra.server_none — нет сервера (умолч. 15)
// infra.server_weak — слабый сервер (умолч. 10)
// infra.network_unstable — нестабильная сеть (умолч. 20)
// infra.network_partial — частично (умолч. 10)
// infra.power_none — нет UPS (умолч. 20)
// infra.power_weak — слабый UPS (умолч. 10)
function calc_infrastructure_load(string $serverState, string $networkState, string $powerState): float {
$server = $serverState === 'none' ? get_coefficient('infra.server_none', 15) : ($serverState === 'weak' ? get_coefficient('infra.server_weak', 10) : 0);
$network = $networkState === 'unstable' ? get_coefficient('infra.network_unstable', 20) : ($networkState === 'partial' ? get_coefficient('infra.network_partial', 10) : 0);
$power = $powerState === 'none' ? get_coefficient('infra.power_none', 20) : ($powerState === 'weak' ? get_coefficient('infra.power_weak', 10) : 0);
return $server + $network + $power;
}
// ========================================================================
// 4. SERVICE HISTORY INDEX (ИСТОРИЯ ОБСЛУЖИВАНИЯ)
// ========================================================================
// history.none — нет обслуживания (умолч. 30)
// history.irregular — нерегулярное (умолч. 20)
// history.formal — формальный подрядчик (умолч. 10)
// history.sla — есть SLA (умолч. 0)
function calc_service_history(string $serviceState): float {
if ($serviceState === 'none') return get_coefficient('history.none', 30);
if ($serviceState === 'irregular') return get_coefficient('history.irregular', 20);
if ($serviceState === 'formal') return get_coefficient('history.formal', 10);
if ($serviceState === 'sla') return get_coefficient('history.sla', 0);
return get_coefficient('history.none', 30);
}
// ========================================================================
// 5. OBJECT INDEX (ИТОГОВЫЙ ИНДЕКС ОБЪЕКТА)
// ========================================================================
// object_index.risk_weight — вес Risk Score (умолч. 0.4)
// object_index.complexity_weight — вес Complexity (умолч. 0.3)
// object_index.infra_weight — вес Infra Load (умолч. 0.2)
// object_index.history_weight — вес Service History (умолч. 0.1)
function calc_object_index(float $risk, float $complexity, float $infra, float $history): float {
return round(
$risk * get_coefficient('object_index.risk_weight', 0.4)
+ $complexity * get_coefficient('object_index.complexity_weight', 0.3)
+ $infra * get_coefficient('object_index.infra_weight', 0.2)
+ $history * get_coefficient('object_index.history_weight', 0.1)
, 2);
}
function object_class(float $index): string {
if ($index <= 30) return 'A';
if ($index <= 60) return 'B';
if ($index <= 90) return 'C';
return 'D';
}
function object_class_label(float $index): string {
$cls = object_class($index);
if ($cls === 'A') return 'Лёгкий SLA';
if ($cls === 'B') return 'Стандарт SLA';
if ($cls === 'C') return 'Сложный SLA';
if ($cls === 'D') return 'Enterprise / Высокий риск';
return '';
}
// ========================================================================
// 6. SLA PRICE (СТОИМОСТЬ SLA)
// ========================================================================
// sla_price.base_cost — базовая стоимость инженера (умолч. 15000)
// sla_price.region_* — региональные коэффициенты
// sla_price.risk_* — мультипликаторы риска (через risk_multiplier)
function calc_sla_price(float $baseCost, float $objectIndex, float $regionFactor, float $riskMult): float {
return round($baseCost * $objectIndex * $regionFactor * $riskMult, 2);
}
// ========================================================================
// 7. ENGINEER SCORE (ES) — ИТОГОВЫЙ ИНДЕКС ИНЖЕНЕРА
// ========================================================================
// engineer.score.sla_weight — 0.25
// engineer.score.response_weight — 0.20
// engineer.score.resolution_weight — 0.20
// engineer.score.diagnosis_weight — 0.15
// engineer.score.reopen_weight — 0.10
// engineer.score.risk_coverage_weight — 0.10
function calc_engineer_score(float $slaCompliance, float $responseTimeScore, float $resolutionTimeScore, float $diagnosisAccuracy, float $reopenRateScore, float $riskCoverageScore): float {
return round(
(get_coefficient('engineer.score.sla_weight', 0.25) * $slaCompliance)
+ (get_coefficient('engineer.score.response_weight', 0.20) * $responseTimeScore)
+ (get_coefficient('engineer.score.resolution_weight', 0.20) * $resolutionTimeScore)
+ (get_coefficient('engineer.score.diagnosis_weight', 0.15) * $diagnosisAccuracy)
+ (get_coefficient('engineer.score.reopen_weight', 0.10) * $reopenRateScore)
+ (get_coefficient('engineer.score.risk_coverage_weight', 0.10) * $riskCoverageScore)
, 2);
}
/**
* Грейд инженера по итоговому Score
* engineer.grade.senior 90
* engineer.grade.strong 80
* engineer.grade.middle 70
*/
function engineer_grade(float $score): string {
if ($score >= get_coefficient('engineer.grade.senior', 90)) return 'Senior';
if ($score >= get_coefficient('engineer.grade.strong', 80)) return 'Strong';
if ($score >= get_coefficient('engineer.grade.middle', 70)) return 'Middle';
return 'Junior';
}
// ========================================================================
// 8. ENGINEER CONTROL SCORE (ECS) — ИНДЕКС УПРАВЛЕНИЯ ИНЖЕНЕРА
// ========================================================================
// ecs.sla_weight — 0.30
// ecs.task_dist_weight — 0.25
// ecs.incident_red_weight — 0.20
// ecs.team_perf_weight — 0.15
// ecs.response_coord_weight — 0.10
function calc_engineer_control_score(float $slaControl, float $taskDist, float $incidentRed, float $teamPerf, float $responseCoord): float {
return round(
(get_coefficient('ecs.sla_weight', 0.30) * $slaControl)
+ (get_coefficient('ecs.task_dist_weight', 0.25) * $taskDist)
+ (get_coefficient('ecs.incident_red_weight', 0.20) * $incidentRed)
+ (get_coefficient('ecs.team_perf_weight', 0.15) * $teamPerf)
+ (get_coefficient('ecs.response_coord_weight', 0.10) * $responseCoord)
, 2);
}
// ========================================================================
// 9. SHS (SYSTEM HEALTH SCORE) — ЗДОРОВЬЕ СИСТЕМЫ
// ========================================================================
// shs.sla_stability_weight — 0.22
// shs.revenue_stability_weight — 0.18
// shs.retention_weight — 0.18
// shs.engineer_perf_weight — 0.15
// shs.incident_stability_weight — 0.12
// shs.sales_flow_weight — 0.10
// shs.operational_eff_weight — 0.05
function calc_shs(float $slaStability, float $revenueStability, float $retention, float $engineerPerformance, float $incidentStability, float $salesFlow, float $operationalEfficiency): float {
return round(
(get_coefficient('shs.sla_stability_weight', 0.22) * $slaStability)
+ (get_coefficient('shs.revenue_stability_weight', 0.18) * $revenueStability)
+ (get_coefficient('shs.retention_weight', 0.18) * $retention)
+ (get_coefficient('shs.engineer_perf_weight', 0.15) * $engineerPerformance)
+ (get_coefficient('shs.incident_stability_weight', 0.12) * $incidentStability)
+ (get_coefficient('shs.sales_flow_weight', 0.10) * $salesFlow)
+ (get_coefficient('shs.operational_eff_weight', 0.05) * $operationalEfficiency)
, 2);
}
/**
* CEO SHS отдельные веса для CEO-дашборда
* ceo_shs.mrr_weight 0.25
* ceo_shs.sla_weight 0.20
* ceo_shs.retention_weight 0.20
* ceo_shs.productivity_weight 0.15
* ceo_shs.conversion_weight 0.10
* ceo_shs.incident_weight 0.10
*/
function calc_ceo_shs(float $mrrGrowth, float $slaCompliance, float $retention, float $productivity, float $conversion, float $incidentStability): float {
return round(
(get_coefficient('ceo_shs.mrr_weight', 0.25) * $mrrGrowth)
+ (get_coefficient('ceo_shs.sla_weight', 0.20) * $slaCompliance)
+ (get_coefficient('ceo_shs.retention_weight', 0.20) * $retention)
+ (get_coefficient('ceo_shs.productivity_weight', 0.15) * $productivity)
+ (get_coefficient('ceo_shs.conversion_weight', 0.10) * $conversion)
+ (get_coefficient('ceo_shs.incident_weight', 0.10) * $incidentStability)
, 2);
}
/**
* Статус SHS
* shs.zone_growth 85
* shs.zone_stable 70
* shs.zone_risk 50
*/
function shs_status(float $score): string {
if ($score >= get_coefficient('shs.zone_growth', 85)) return 'Growth';
if ($score >= get_coefficient('shs.zone_stable', 70)) return 'Stable';
if ($score >= get_coefficient('shs.zone_risk', 50)) return 'Risk';
return 'Crisis';
}
/**
* Зона SHS с предупреждением
*/
function shs_zone(float $score): array {
if ($score >= get_coefficient('shs.zone_growth', 85))
return ['zone' => 'green', 'label' => 'Growth', 'action' => ''];
if ($score >= get_coefficient('shs.zone_stable', 70))
return ['zone' => 'yellow', 'label' => 'Stable', 'action' => 'Мониторинг'];
if ($score >= get_coefficient('shs.zone_risk', 50))
return ['zone' => 'yellow', 'label' => 'Risk', 'action' => 'Снизить продажи, запустить аудит цикла, пересмотреть назначения'];
return ['zone' => 'red', 'label' => 'Crisis', 'action' => 'Немедленный аудит, приостановка новых продаж, пересмотр команды'];
}
/**
* Дельта SHS (изменение относительно предыдущей записи)
* Если падение > 5 warning, > 10 critical
* shs.delta_warning 5
* shs.delta_critical 10
*/
function shs_delta(float $currentScore, ?float $previousScore): ?array {
if ($previousScore === null) return null;
$delta = $currentScore - $previousScore;
$thresholdWarn = get_coefficient('shs.delta_warning', 5);
$thresholdCrit = get_coefficient('shs.delta_critical', 10);
if ($delta <= -$thresholdCrit)
return ['delta' => round($delta, 1), 'level' => 'critical', 'message' => 'SHS упал более чем на ' . $thresholdCrit . ' пунктов — кризис'];
if ($delta <= -$thresholdWarn)
return ['delta' => round($delta, 1), 'level' => 'warning', 'message' => 'SHS упал более чем на ' . $thresholdWarn . ' пунктов — требуется внимание'];
return ['delta' => round($delta, 1), 'level' => 'ok', 'message' => ''];
}
// ========================================================================
// 10. SLA STABILITY INDEX (SSI)
// ========================================================================
// Учитывает не только compliance, но и severity нарушений
// SSI = SLA_compliance × (1 - breach_severity)
// ssi.breach_severity_p1 — 1.0
// ssi.breach_severity_p2 — 0.5
// ssi.breach_severity_p3 — 0.2
function calc_sla_stability_index(float $slaCompliance, float $breachSeverityWeighted): float {
$ssi = $slaCompliance * (1 - $breachSeverityWeighted);
return round(max(0, min(100, $ssi)), 2);
}
function calc_breach_severity(int $breachesP1, int $breachesP2, int $breachesP3): float {
$total = $breachesP1 + $breachesP2 + $breachesP3;
if ($total === 0) return 0;
$weighted = $breachesP1 * get_coefficient('ssi.breach_severity_p1', 1.0)
+ $breachesP2 * get_coefficient('ssi.breach_severity_p2', 0.5)
+ $breachesP3 * get_coefficient('ssi.breach_severity_p3', 0.2);
return min(1, $weighted / $total);
}
// ========================================================================
// 11. REVENUE STABILITY INDEX (RSI) через коэффициент вариации
// ========================================================================
// RSI = max(0, 100 × (1 - σ(MRR) / mean(MRR)))
// Основан на исторических значениях MRR за N периодов
function calc_revenue_stability_index(array $mrrHistory): float {
$count = count($mrrHistory);
if ($count < 2) return 100;
$mean = array_sum($mrrHistory) / $count;
if ($mean <= 0) return 100;
$variance = 0;
foreach ($mrrHistory as $mrr) {
$variance += ($mrr - $mean) ** 2;
}
$stdDev = sqrt($variance / $count);
$cv = $stdDev / $mean;
return round(max(0, min(100, 100 * (1 - $cv))), 2);
}
// ========================================================================
// 12. SALES FLOW INDEX (SFI) — ВОРОНКА ПРОДАЖ
// ========================================================================
// SFI = (аудиты → SLA конверсия) × качество лидов
function calc_sales_flow_index(int $audits, int $slaContracts, float $leadQuality = 1.0): float {
if ($audits <= 0) return 0;
$conversion = $slaContracts / $audits;
return round(min(100, $conversion * 100 * $leadQuality), 2);
}
// ========================================================================
// 13. OPERATIONAL EFFICIENCY INDEX (OEI)
// ========================================================================
// OEI = revenue / (engineer_hours × cost_per_hour) × 100
// Нормируется к 100
function calc_operational_efficiency_index(float $revenue, float $engineerHours, float $costPerHour): float {
$total = $engineerHours * $costPerHour;
if ($total <= 0) return 100;
return round(min(100, ($revenue / $total) * 100), 2);
}
// ========================================================================
// 14. INCIDENT STABILITY INDEX (ISI) с весами severity
// ========================================================================
// ISI = max(0, 100 - (incidents / objects × severity_weight))
// isi.severity_p1 — 1.0
// isi.severity_p2 — 0.5
// isi.severity_p3 — 0.2
function calc_incident_stability_index(int $incidentsP1, int $incidentsP2, int $incidentsP3, int $objectCount): float {
if ($objectCount <= 0) return 100;
$weighted = $incidentsP1 * get_coefficient('isi.severity_p1', 1.0)
+ $incidentsP2 * get_coefficient('isi.severity_p2', 0.5)
+ $incidentsP3 * get_coefficient('isi.severity_p3', 0.2);
$rate = $weighted / $objectCount;
return round(max(0, min(100, 100 - ($rate * 20))), 2);
}
// ========================================================================
// 15. AUTOMATION RULES (АВТОМАТИЧЕСКИЕ ПРАВИЛА)
// ========================================================================
/**
* Проверить все правила автоматизации.
* Возвращает массив предупреждений.
*
* Правила из docs/Цифровая модель общая.md:
* 1. IF SHS < 70 снизить продажи, запустить аудит, пересмотреть назначения
* 2. IF SLA < 90% заморозить неприоритетные проекты, увеличить частоту проверок
* 3. IF retention < 90% обязательный аудит клиентов, пересмотр назначений
*
* rules.shs_threshold 70
* rules.sla_threshold 90
* rules.retention_threshold 90
*/
function check_automation_rules(?float $shs, ?float $slaCompliance, ?float $retention): array {
$alerts = [];
if ($shs !== null && $shs < get_coefficient('rules.shs_threshold', 70)) {
$alerts[] = [
'rule' => 'SHS',
'severity' => 'critical',
'message' => 'SHS ниже ' . get_coefficient('rules.shs_threshold', 70) . ' — снизить продажи, запустить аудит, пересмотреть назначения инженеров.',
];
}
if ($slaCompliance !== null && $slaCompliance < get_coefficient('rules.sla_threshold', 90)) {
$alerts[] = [
'rule' => 'SLA',
'severity' => 'warning',
'message' => 'SLA Compliance ниже ' . get_coefficient('rules.sla_threshold', 90) . '% — заморозить неприоритетные проекты, увеличить частоту проверок.',
];
}
if ($retention !== null && $retention < get_coefficient('rules.retention_threshold', 90)) {
$alerts[] = [
'rule' => 'Retention',
'severity' => 'critical',
'message' => 'Удержание клиентов ниже ' . get_coefficient('rules.retention_threshold', 90) . '% — обязательный аудит клиентов, пересмотр назначений инженеров.',
];
}
return $alerts;
}
// ========================================================================
// 16. БАЗОВЫЕ KPI
// ========================================================================
/** SLA Compliance: процент задач, закрытых в срок */
function calc_sla_compliance(int $closedInSla, int $total): float {
if ($total <= 0) return 100;
return round(($closedInSla / $total) * 100, 2);
}
/** Reopen Rate: процент повторных обращений */
function calc_reopen_rate(int $reopened, int $total): float {
if ($total <= 0) return 0;
return round(($reopened / $total) * 100, 2);
}
/** Diagnosis Accuracy: точность диагностики */
function calc_diagnosis_accuracy(int $confirmed, int $found): float {
if ($found <= 0) return 100;
return round(($confirmed / $found) * 100, 2);
}
/** Response Time Score: оценка времени реакции */
function calc_response_time_score(float $actualHours, float $slaHours): float {
if ($slaHours <= 0) return 100;
$ratio = $actualHours / $slaHours;
if ($ratio <= 1.0) return 100;
if ($ratio <= 1.2) return 80;
return max(0, 100 - ($ratio - 1.2) * 50);
}
/** Resolution Time Score: оценка времени устранения */
function calc_resolution_time_score(float $actualHours, float $normHours): float {
if ($normHours <= 0) return 100;
$ratio = $actualHours / $normHours;
if ($ratio <= 1.0) return 100;
if ($ratio <= 1.5) return 70;
return max(0, 100 - ($ratio - 1.5) * 40);
}
/** Risk Coverage Score: покрытие зон проверки */
function calc_risk_coverage(int $checkedZones, int $totalZones = 5): float {
if ($totalZones <= 0) return 0;
return round(($checkedZones / $totalZones) * 100, 2);
}
/** Documentation Quality: полнота отчётов */
function calc_documentation_quality(int $fullReports, int $totalVisits): float {
if ($totalVisits <= 0) return 100;
return round(($fullReports / $totalVisits) * 100, 2);
}
/** Team Load Balance: равномерность загрузки (stdDev) — чем меньше, тем лучше */
function calc_tlb(array $tasksPerEngineer): float {
$count = count($tasksPerEngineer);
if ($count < 2) return 0;
$mean = array_sum($tasksPerEngineer) / $count;
$variance = 0;
foreach ($tasksPerEngineer as $t) {
$variance += ($t - $mean) ** 2;
}
return round(sqrt($variance / $count), 2);
}
// ========================================================================
// 17. ФИНАНСОВЫЕ ПОКАЗАТЕЛИ
// ========================================================================
/** MRR: месячный повторяющийся доход */
function calc_mrr(float $totalSlaAnnual): float {
return round($totalSlaAnnual / 12, 2);
}
/** Revenue per Engineer */
function calc_rpe(float $slaRevenue, int $engineerCount): float {
if ($engineerCount <= 0) return 0;
return round($slaRevenue / $engineerCount, 2);
}
/** Cost per SLA Object (CPO) */
function calc_cpo(float $totalCost, int $objectCount): float {
if ($objectCount <= 0) return 0;
return round($totalCost / $objectCount, 2);
}
/** Average Revenue Per Client (ARPC) */
function calc_arpc(float $totalRevenue, int $clientCount): float {
if ($clientCount <= 0) return 0;
return round($totalRevenue / $clientCount, 2);
}
// ========================================================================
// 18. ОПЕРАЦИОННЫЕ KPI
// ========================================================================
/** Utilization: загрузка сотрудника */
function calc_utilization(int $workingHours, int $availableHours): float {
if ($availableHours <= 0) return 0;
return round(($workingHours / $availableHours) * 100, 2);
}
/** Autonomy: самостоятельность */
function calc_autonomy(int $independent, int $total): float {
if ($total <= 0) return 100;
return round(($independent / $total) * 100, 2);
}
/** Escalation Rate */
function calc_escalation_rate(int $escalated, int $total): float {
if ($total <= 0) return 0;
return round(($escalated / $total) * 100, 2);
}
/** System Uptime: доступность системы */
function calc_uptime(float $uptimeHours, float $totalHours): float {
if ($totalHours <= 0) return 100;
return round(($uptimeHours / $totalHours) * 100, 2);
}
// ========================================================================
// 19. КЛИЕНТСКИЕ МЕТРИКИ
// ========================================================================
/** Retention: удержание клиентов */
function calc_retention(int $activeClients, int $totalClients): float {
if ($totalClients <= 0) return 100;
return round(($activeClients / $totalClients) * 100, 2);
}
/** Retention с штрафом за потерю ключевых клиентов */
function calc_retention_key_client(float $baseRetention, int $lostKeyClients): float {
$penalty = $lostKeyClients * get_coefficient('retention.key_client_penalty', 0.1);
return round(max(0, $baseRetention - $penalty * 100), 2);
}
/** CSAT (удовлетворённость клиентов) */
function calc_csat(int $positive, int $total): float {
if ($total <= 0) return 100;
return round(($positive / $total) * 100, 2);
}
// ========================================================================
// 20. ИНЦИДЕНТЫ
// ========================================================================
/** Incident Rate: количество инцидентов на объект */
function calc_incident_rate(int $incidents, int $objectCount): float {
if ($objectCount <= 0) return 0;
return round($incidents / $objectCount, 2);
}
/** Incident Reduction Rate: снижение инцидентов */
/** Incident Reduction Rate */
function calc_incident_reduction(int $previous, int $current): float {
if ($previous <= 0) return 0;
return round((($previous - $current) / $previous) * 100, 2);
}
// ========================================================================
// 21. БОНУСНАЯ СИСТЕМА
// ========================================================================
// bonus.score_90plus — +20%
// bonus.score_80_89 — +10%
// bonus.score_below_80 — 0%
function calc_bonus_percent(float $engineerScore): float {
if ($engineerScore >= get_coefficient('bonus.score_90plus', 90)) return get_coefficient('bonus.premium_90plus', 20);
if ($engineerScore >= get_coefficient('bonus.score_80_89', 80)) return get_coefficient('bonus.premium_80_89', 10);
return get_coefficient('bonus.premium_below_80', 0);
}
+81
View File
@@ -0,0 +1,81 @@
<?php
/**
* ============================================
* AegisOne Engineering Service Шапка дашборда
* ============================================
*
* Выводит <head> (с запретом индексации), открывает <body>,
* сайдбар с навигацией по ролям и <main>.
* Если пользователь не авторизован редирект на логин.
*/
if (empty($_SESSION['user_id'])) return;
$current_uri = $_SERVER['REQUEST_URI'];
$role = $_SESSION['user_role'];
$name = $_SESSION['user_name'];
function nav_link(string $href, string $label, string $tooltip = '', array $roles = []): string {
global $current_uri, $role;
if (!empty($roles) && !in_array($role, $roles, true)) return '';
$active = strpos($current_uri, $href) === 0 ? ' active' : '';
$tip = $tooltip !== '' ? ' data-tooltip="' . htmlspecialchars($tooltip) . '"' : '';
return '<a href="' . htmlspecialchars($href) . '" class="' . $active . '"' . $tip . '>' . htmlspecialchars($label) . '</a>';
}
$roleLabel = $role === 'owner' ? 'Владелец' : ($role === 'engineer' ? 'Инженер' : 'Техник');
?><!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>Service Portal AegisOne Engineering</title>
<link rel="stylesheet" href="/service-style.php">
<link rel="icon" type="image/x-icon" href="/assets/img/favicon.ico">
</head>
<body>
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')"></button>
<div class="layout">
<aside class="sidebar">
<div class="sidebar-header">
<div class="logo">Aegis<span>One</span></div>
<div style="font-size:10px;color:var(--text-muted);">Service Portal</div>
</div>
<nav class="sidebar-nav">
<div class="sidebar-section">Панель</div>
<?= nav_link('/service/dashboard.php', 'Дашборд', 'Главная панель', ['owner','engineer','technician']) ?>
<div class="sidebar-section">Управление</div>
<?= nav_link('/service/pages/owner/users.php', 'Сотрудники', 'Управление сотрудниками', ['owner']) ?>
<?= nav_link('/service/pages/owner/customers.php', 'Клиенты', 'Управление клиентами', ['owner','engineer']) ?>
<?= nav_link('/service/pages/owner/objects.php', 'Объекты', 'Управление объектами', ['owner','engineer']) ?>
<?= nav_link('/service/pages/owner/assignments.php', 'Назначения', 'Назначения на объекты', ['owner']) ?>
<?= nav_link('/service/pages/owner/sla.php', 'SLA контракты', 'Управление SLA', ['owner','engineer']) ?>
<?= nav_link('/service/pages/owner/ceo.php', 'CEO дашборд', 'Бизнес-метрики', ['owner']) ?>
<?= nav_link('/service/pages/owner/coefficients.php', 'Коэффициенты', 'Расчётные коэффициенты', ['owner']) ?>
<div class="sidebar-section">Работа</div>
<?= nav_link('/service/pages/engineer/tasks.php', 'Задачи', 'Мои задачи', ['engineer','technician']) ?>
<?= nav_link('/service/pages/engineer/reports.php', 'Отчёты', 'Отчёты по объектам', ['engineer','technician']) ?>
<?= nav_link('/service/pages/engineer/incidents.php', 'Инциденты', 'Журнал инцидентов', ['engineer','technician']) ?>
<?= nav_link('/service/pages/shared/questionnaire.php', 'Опросник', 'Обследование объекта', ['owner','engineer']) ?>
<?= nav_link('/service/pages/owner/questionnaire_config.php', 'Настройка опросника', 'Конфигурация вопросов', ['owner']) ?>
<?= nav_link('/service/pages/shared/passports.php', 'Паспорта объектов', 'Паспорта и расчёты', ['owner','engineer']) ?>
<?php if ($role === 'technician'): ?>
<?= nav_link('/service/pages/technician/checklist.php', 'Чек-лист', 'Чек-лист техника', ['technician']) ?>
<?php endif; ?>
<div class="sidebar-section">Блог</div>
<?= nav_link('/service/pages/owner/blog.php', 'Управление блогом', 'Создание и редактирование статей', ['owner']) ?>
<?= nav_link('/blog/', 'Публичный блог', 'Открытый блог на сайте', ['owner','engineer','technician']) ?>
<div class="sidebar-section">Контент</div>
<?= nav_link('/service/pages/owner/cases.php', 'Примеры из практики', 'Карусель на главной', ['owner']) ?>
<div class="sidebar-section">Документация</div>
<?= nav_link('/service/pages/shared/docs.php', 'Просмотр документации', 'Читать документы', ['owner','engineer']) ?>
<?= nav_link('/service/pages/owner/docs_section.php', 'Управление документами', 'Настройка доступа к документам', ['owner']) ?>
</nav>
<div class="sidebar-footer">
<div class="user-name"><?= htmlspecialchars($name) ?></div>
<div class="user-role"><?= $roleLabel ?></div>
<a href="/service/logout.php" style="display:inline-block;margin-top:6px;font-size:12px;">Выйти</a>
</div>
</aside>
<main class="main">
+70
View File
@@ -0,0 +1,70 @@
<?php
require_once __DIR__ . '/inc/auth.php';
auth_session_start();
if (!empty($_SESSION['user_id'])) {
header('Location: /service/dashboard.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$login = trim($_POST['login'] ?? '');
$password = $_POST['password'] ?? '';
if ($login === '' || $password === '') {
$error = 'Заполните все поля.';
} else {
$result = auth_login($login, $password);
if ($result['success']) {
$redirect = $_GET['redirect'] ?? '/service/dashboard.php';
header('Location: ' . $redirect);
exit;
}
$error = $result['error'];
}
}
?><!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Вход AegisOne Engineering</title>
<link rel="stylesheet" href="/service-style.php">
<meta name="robots" content="noindex, nofollow">
</head>
<body class="login-page">
<div class="login-box">
<div class="login-brand">
<img src="/logo.php" alt="AegisOne" class="login-brand__img">
<div class="login-brand__title">AegisOne Engineering</div>
<div class="login-brand__subtitle">Service Portal</div>
</div>
<div class="login-divider"></div>
<h1>Вход в сервисный портал</h1>
<?php if ($error): ?>
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="post">
<div class="form-group">
<label for="login">Логин</label>
<input type="text" id="login" name="login" placeholder="Введите логин" required autocomplete="username" autofocus>
</div>
<div class="form-group">
<label for="password">Пароль</label>
<input type="password" id="password" name="password" placeholder="Введите пароль" required autocomplete="current-password">
</div>
<button type="submit" class="btn btn-primary btn-block">Войти</button>
</form>
<p class="login-box__footnote">
* Сервисный портал для сотрудников AegisOne Engineering
</p>
</div>
</body>
</html>
+61
View File
@@ -0,0 +1,61 @@
<?php
/**
* AegisOne Engineering Установка / обновление базы данных
*
* Запускает миграцию из data/migration.sql
* Безопасно при повторном запуске (IF NOT EXISTS, ON DUPLICATE KEY UPDATE)
*/
require_once __DIR__ . '/../data/db-config.php';
try {
$pdo = new PDO(
'mysql:host=' . DB_HOST . ';port=' . DB_PORT . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET,
DB_USER,
DB_PASS,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$sql = file_get_contents(__DIR__ . '/../data/migration.sql');
$statements = array_filter(array_map('trim', explode(';', $sql)));
$created = 0;
$inserted = 0;
$errors = 0;
foreach ($statements as $query) {
if ($query === '' || strpos($query, '--') === 0) continue;
try {
$pdo->exec($query);
if (stripos($query, 'CREATE') !== false) $created++;
if (stripos($query, 'INSERT') !== false) $inserted++;
} catch (\Throwable $e) {
$errors++;
}
}
echo '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Миграция — AegisOne</title>';
echo '<style>body{font-family:Inter,sans-serif;background:#0D1B2A;color:#E8EDF2;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;}';
echo '.box{background:#1B2838;border:1px solid #2a3a4a;border-radius:12px;padding:40px;max-width:500px;text-align:center;}';
echo 'h1{color:#4DA8DA;margin:0 0 8px;font-size:22px;}p{color:#A0B4C8;font-size:14px;margin:0 0 20px;line-height:1.6;}';
echo '.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:20px 0;}';
echo '.stat{background:#0D1B2A;border-radius:8px;padding:16px;text-align:center;}';
echo '.stat-value{font-size:28px;font-weight:700;color:#00ADEF;}';
echo '.stat-label{font-size:11px;color:#6B8299;text-transform:uppercase;letter-spacing:0.1em;margin-top:4px;}';
echo '.btn{display:inline-block;background:linear-gradient(135deg,#00ADEF,#0095cc);color:#fff;padding:12px 28px;border-radius:8px;text-decoration:none;font-weight:600;font-size:14px;margin-top:16px;}';
echo '</style></head><body><div class="box">';
echo '<h1>✅ Миграция завершена</h1>';
echo '<p>База данных <strong>' . htmlspecialchars(DB_NAME) . '</strong> обновлена.</p>';
echo '<div class="stats">';
echo '<div class="stat"><div class="stat-value">' . $created . '</div><div class="stat-label">Таблиц</div></div>';
echo '<div class="stat"><div class="stat-value">' . $inserted . '</div><div class="stat-label">Записей</div></div>';
echo '<div class="stat"><div class="stat-value">' . $errors . '</div><div class="stat-label">Ошибок</div></div>';
echo '</div>';
echo '<a href="/service/" class="btn">Войти в систему</a>';
echo '</div></body></html>';
} catch (\Throwable $e) {
echo '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Ошибка — AegisOne</title>';
echo '<style>body{font-family:Inter,sans-serif;background:#0D1B2A;color:#E8EDF2;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;}';
echo '.box{background:#1B2838;border:1px solid #ef4444;border-radius:12px;padding:40px;max-width:500px;text-align:center;}';
echo 'h1{color:#ef4444;margin:0 0 8px;font-size:22px;}p{color:#A0B4C8;font-size:14px;line-height:1.6;}</style></head><body>';
echo '<div class="box"><h1>❌ Ошибка подключения</h1><p>' . htmlspecialchars($e->getMessage()) . '</p></div></body></html>';
}
+6
View File
@@ -0,0 +1,6 @@
<?php
require_once __DIR__ . '/inc/auth.php';
auth_session_start();
auth_logout();
header('Location: /service/index.php');
exit;
+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'; ?>
+112
View File
@@ -0,0 +1,112 @@
{
"owner": {
"title": "Owner",
"filename": "Owner.md",
"engineer": false
},
"seo": {
"title": "SEO",
"filename": "SEO.md",
"engineer": false
},
"sla": {
"title": "SLA",
"filename": "SLA.md",
"engineer": false
},
"kpi-inzhenerov": {
"title": "KPI инженеров",
"filename": "KPI инженеров.md",
"engineer": false
},
"avtomaticheskaya-sistema-shs": {
"title": "Автоматическая система SHS",
"filename": "автоматическая система SHS.md",
"engineer": false
},
"oprosnik-raschet": {
"title": "Опросник-расчёт",
"filename": "Опросник-расчет.md",
"engineer": false
},
"arkhitektura-kompanii": {
"title": "Архитектура компании",
"filename": "Архитектура компании.md",
"engineer": false
},
"kontent-plan": {
"title": "Контент план",
"filename": "Контент план.md",
"engineer": false
},
"kp-i-struktura-audita": {
"title": "КП и структура аудита",
"filename": "КП и структура аудита.md",
"engineer": false
},
"utp-i-kp": {
"title": "УТП и КП",
"filename": "УТП и КП.md",
"engineer": false
},
"arkhetiktura-brenda": {
"title": "Архетиктура бренда",
"filename": "Архетиктура бренда.md",
"engineer": false
},
"marketing": {
"title": "Маркеттинг",
"filename": "Маркеттинг.md",
"engineer": false
},
"skripty-prodazh": {
"title": "Скрипты продаж",
"filename": "Скрипты продаж.md",
"engineer": false
},
"osnova": {
"title": "Основа",
"filename": "Основа.md",
"engineer": false
},
"dokazatelnaya-ekspertiza": {
"title": "Доказательная экспертиза",
"filename": "Докозательная экспертиза.md",
"engineer": false
},
"voronki-telegram-i-crm": {
"title": "Воронки телеграм и CRM",
"filename": "Воронки телеграм и crm.md",
"engineer": false
},
"voronka": {
"title": "Воронка",
"filename": "Воронка.md",
"engineer": false
},
"sayt-i-upakovka": {
"title": "Сайт и упаковка",
"filename": "Сайт и упаковка.md",
"engineer": false
},
"dashboard-tekhnika": {
"title": "Дашборд техника",
"filename": "Дашборд техника.md",
"engineer": false
},
"dashboard-inzhenera": {
"title": "Дашборд инженера",
"filename": "Дашборд инженера.md",
"engineer": false
},
"blog": {
"title": "Блог",
"filename": "Блог.md",
"engineer": false
},
"tsifrovaya-model-obshchaya": {
"title": "Цифровая модель общая",
"filename": "Цифровая модель общая.md",
"engineer": false
}
}