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

394 lines
17 KiB
PHP

<?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 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'; ?>