490 lines
30 KiB
PHP
490 lines
30 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
require_once __DIR__ . '/app/bootstrap.php';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$action = $_POST['action'] ?? '';
|
|
if ($action === 'login') {
|
|
$username = trim((string)($_POST['username'] ?? ''));
|
|
$password = (string)($_POST['password'] ?? '');
|
|
if (diagnostic_admin_login($username, $password)) {
|
|
diagnostic_flash_set('success', 'Sesja administratora została rozpoczęta.');
|
|
header('Location: ' . diagnostic_admin_path());
|
|
exit;
|
|
}
|
|
diagnostic_flash_set('danger', 'Nieprawidłowy login lub hasło administratora.');
|
|
header('Location: ' . diagnostic_admin_path());
|
|
exit;
|
|
}
|
|
if ($action === 'logout') {
|
|
diagnostic_admin_logout();
|
|
diagnostic_flash_set('info', 'Sesja administratora została zakończona.');
|
|
header('Location: ' . diagnostic_admin_path());
|
|
exit;
|
|
}
|
|
if ($action === 'save-notification-email') {
|
|
if (!diagnostic_admin_is_authenticated()) {
|
|
diagnostic_flash_set('warning', 'Zaloguj się jako administrator, aby zmienić adres powiadomień.');
|
|
header('Location: ' . diagnostic_admin_path(['tab' => 'settings']));
|
|
exit;
|
|
}
|
|
|
|
$notificationEmail = trim((string)($_POST['notification_email'] ?? ''));
|
|
if (!filter_var($notificationEmail, FILTER_VALIDATE_EMAIL)) {
|
|
diagnostic_flash_set('danger', 'Podaj poprawny adres e-mail administratora do powiadomień o nowych prośbach o kontakt.');
|
|
header('Location: ' . diagnostic_admin_path(['tab' => 'settings']));
|
|
exit;
|
|
}
|
|
|
|
diagnostic_admin_setting_set('admin_notification_email', $notificationEmail);
|
|
diagnostic_flash_set('success', 'Adres e-mail administratora do powiadomień został zapisany.');
|
|
header('Location: ' . diagnostic_admin_path(['tab' => 'settings']));
|
|
exit;
|
|
}
|
|
if ($action === 'save-openai-api-key') {
|
|
if (!diagnostic_admin_is_authenticated()) {
|
|
diagnostic_flash_set('warning', 'Zaloguj się jako administrator, aby zapisać klucz OpenAI.');
|
|
header('Location: ' . diagnostic_admin_path(['tab' => 'settings']));
|
|
exit;
|
|
}
|
|
|
|
$openAiApiKey = preg_replace('/\s+/', '', (string)($_POST['openai_api_key'] ?? '')) ?? '';
|
|
if (!diagnostic_openai_api_key_is_valid($openAiApiKey)) {
|
|
diagnostic_flash_set('danger', 'Podaj poprawny klucz API OpenAI. Klucz powinien być kompletny i bez spacji.');
|
|
header('Location: ' . diagnostic_admin_path(['tab' => 'settings']));
|
|
exit;
|
|
}
|
|
|
|
diagnostic_admin_setting_set('openai_api_key', $openAiApiKey);
|
|
diagnostic_flash_set('success', 'Własny klucz API OpenAI został zapisany. Nowe zapytania AI będą używały tego klucza.');
|
|
header('Location: ' . diagnostic_admin_path(['tab' => 'settings']));
|
|
exit;
|
|
}
|
|
if ($action === 'remove-openai-api-key') {
|
|
if (!diagnostic_admin_is_authenticated()) {
|
|
diagnostic_flash_set('warning', 'Zaloguj się jako administrator, aby usunąć klucz OpenAI.');
|
|
header('Location: ' . diagnostic_admin_path(['tab' => 'settings']));
|
|
exit;
|
|
}
|
|
|
|
diagnostic_admin_setting_set('openai_api_key', null);
|
|
diagnostic_flash_set('info', 'Zapisany klucz API OpenAI został usunięty. System wrócił do domyślnej konfiguracji proxy.');
|
|
header('Location: ' . diagnostic_admin_path(['tab' => 'settings']));
|
|
exit;
|
|
}
|
|
if ($action === 'resend-report') {
|
|
if (!diagnostic_admin_is_authenticated()) {
|
|
diagnostic_flash_set('warning', 'Zaloguj się jako administrator, aby ponownie wysłać raport.');
|
|
header('Location: ' . diagnostic_admin_path());
|
|
exit;
|
|
}
|
|
|
|
$attemptId = (int)($_POST['attempt_id'] ?? 0);
|
|
if ($attemptId <= 0) {
|
|
diagnostic_flash_set('danger', 'Nie udało się ustalić, dla której próby należy ponownie wysłać raport.');
|
|
header('Location: ' . diagnostic_admin_path());
|
|
exit;
|
|
}
|
|
|
|
$result = diagnostic_admin_resend_report($attemptId);
|
|
if (!empty($result['success'])) {
|
|
$attempt = diagnostic_get_attempt($attemptId);
|
|
$recipient = $attempt['email'] ?? '';
|
|
diagnostic_flash_set('success', 'Raport został wysłany ponownie do klienta' . ($recipient !== '' ? ' (' . $recipient . ')' : '') . '.');
|
|
} else {
|
|
diagnostic_flash_set('danger', 'Nie udało się ponownie wysłać raportu: ' . (string)($result['error'] ?? 'Nieznany błąd.'));
|
|
}
|
|
|
|
header('Location: ' . diagnostic_admin_path(['attempt' => $attemptId]));
|
|
exit;
|
|
}
|
|
}
|
|
|
|
if (diagnostic_admin_is_authenticated() && ($_GET['export'] ?? '') === 'csv') {
|
|
header('Content-Type: text/csv; charset=utf-8');
|
|
header('Content-Disposition: attachment; filename="wyniki-diagnozy-' . date('Ymd-His') . '.csv"');
|
|
echo diagnostic_admin_export_csv();
|
|
exit;
|
|
}
|
|
|
|
$flash = diagnostic_flash_get();
|
|
$meta = diagnostic_meta('Panel administratora', 'Zabezpieczony panel do przeglądu prób diagnozy i wyników dojrzałości procesowej.');
|
|
$definition = diagnostic_quiz_definition();
|
|
$attempts = diagnostic_admin_is_authenticated() ? diagnostic_admin_attempts() : [];
|
|
$stats = diagnostic_admin_is_authenticated() ? diagnostic_admin_stats() : [];
|
|
$selectedAttempt = null;
|
|
if (diagnostic_admin_is_authenticated() && isset($_GET['attempt'])) {
|
|
$selectedAttempt = diagnostic_get_attempt((int)$_GET['attempt']);
|
|
}
|
|
if (!$selectedAttempt && !empty($attempts)) {
|
|
$selectedAttempt = diagnostic_get_attempt((int)$attempts[0]['id']);
|
|
}
|
|
$questionMap = diagnostic_question_map();
|
|
$credentials = diagnostic_admin_credentials();
|
|
$notificationConfig = diagnostic_admin_is_authenticated() ? diagnostic_admin_notification_config() : ['configured_email' => '', 'fallback_email' => '', 'effective_email' => '', 'source' => 'none'];
|
|
$openAiConfig = diagnostic_admin_is_authenticated() ? diagnostic_admin_openai_key_config() : ['configured' => false, 'masked_key' => ''];
|
|
$currentTab = 'overview';
|
|
if (diagnostic_admin_is_authenticated()) {
|
|
$requestedTab = (string)($_GET['tab'] ?? 'overview');
|
|
$currentTab = in_array($requestedTab, ['overview', 'settings'], true) ? $requestedTab : 'overview';
|
|
}
|
|
?>
|
|
<!doctype html>
|
|
<html lang="pl">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title><?= htmlspecialchars($meta['title']) ?></title>
|
|
<meta name="description" content="<?= htmlspecialchars($meta['description']) ?>">
|
|
<meta property="og:title" content="<?= htmlspecialchars($meta['title']) ?>">
|
|
<meta property="og:description" content="<?= htmlspecialchars($meta['description']) ?>">
|
|
<meta property="twitter:title" content="<?= htmlspecialchars($meta['title']) ?>">
|
|
<meta property="twitter:description" content="<?= htmlspecialchars($meta['description']) ?>">
|
|
<?php if ($meta['image'] !== ''): ?>
|
|
<meta property="og:image" content="<?= htmlspecialchars($meta['image']) ?>">
|
|
<meta property="twitter:image" content="<?= htmlspecialchars($meta['image']) ?>">
|
|
<?php endif; ?>
|
|
<meta name="robots" content="noindex, nofollow">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="/assets/css/custom.css?v=<?= urlencode((string)@filemtime(__DIR__ . '/assets/css/custom.css')) ?>">
|
|
</head>
|
|
<body class="app-shell">
|
|
<header class="site-header border-bottom bg-white sticky-top">
|
|
<nav class="navbar navbar-expand-lg navbar-light py-3">
|
|
<div class="container">
|
|
<?= diagnostic_brand_logo_anchor('navbar-brand doctor-biznes-logo-link--nav') ?>
|
|
<div class="d-flex align-items-center gap-2 ms-auto">
|
|
<a class="btn btn-sm btn-outline-dark" href="/diagnostic.php">Diagnoza</a>
|
|
<?php if (diagnostic_admin_is_authenticated()): ?>
|
|
<form method="post" class="m-0">
|
|
<input type="hidden" name="action" value="logout">
|
|
<button type="submit" class="btn btn-sm btn-dark">Wyloguj</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
</header>
|
|
|
|
<main class="py-4 py-lg-5">
|
|
<div class="<?= diagnostic_admin_is_authenticated() ? 'container-fluid admin-workspace' : 'container' ?>">
|
|
<?php if ($flash): ?>
|
|
<div class="alert alert-<?= htmlspecialchars($flash['type']) ?> mb-4" role="alert">
|
|
<?= htmlspecialchars($flash['message']) ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!diagnostic_admin_is_authenticated()): ?>
|
|
<section class="row justify-content-center">
|
|
<div class="col-lg-5">
|
|
<div class="surface-card p-4 p-lg-5">
|
|
<div class="eyebrow mb-3">Strefa zabezpieczona</div>
|
|
<h1 class="h3 mb-3">Logowanie administratora</h1>
|
|
<p class="text-secondary mb-4">Panel służy do przeglądu prób, analizy wyników oraz eksportu danych z diagnozy dojrzałości procesowej.</p>
|
|
<form method="post" novalidate>
|
|
<input type="hidden" name="action" value="login">
|
|
<div class="mb-3">
|
|
<label for="username" class="form-label">Login</label>
|
|
<input type="text" class="form-control" id="username" name="username" required>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label for="password" class="form-label">Hasło</label>
|
|
<input type="password" class="form-control" id="password" name="password" required>
|
|
</div>
|
|
<button type="submit" class="btn btn-dark w-100">Wejdź do panelu</button>
|
|
</form>
|
|
<?php if ($credentials['using_default']): ?>
|
|
<div class="alert alert-warning mt-4 mb-0" role="alert">
|
|
Uwaga: aktywne są domyślne dane logowania administratora (<strong>admin / admin123</strong>). Przed użyciem produkcyjnym ustaw własne wartości <code>DIAGNOSTIC_ADMIN_USER</code> i <code>DIAGNOSTIC_ADMIN_PASS</code>.
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<?php else: ?>
|
|
<section class="surface-card p-4 p-lg-5 mb-4">
|
|
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3 align-items-lg-center">
|
|
<div>
|
|
<div class="eyebrow mb-2">Panel administratora</div>
|
|
<h1 class="page-title mb-1">Operacje i wyniki diagnozy</h1>
|
|
<p class="text-secondary mb-0">Przeglądaj próby respondentów, śledź skuteczność ukończenia i analizuj wyniki w poszczególnych obszarach.</p>
|
|
</div>
|
|
<a class="btn btn-outline-dark" href="<?= htmlspecialchars(diagnostic_admin_path(['export' => 'csv'])) ?>">Eksportuj CSV</a>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="row g-3 mb-4">
|
|
<div class="col-sm-6 col-lg-3"><div class="metric-card"><strong><?= (int)$stats['total_attempts'] ?></strong><span>Wszystkie próby</span></div></div>
|
|
<div class="col-sm-6 col-lg-3"><div class="metric-card"><strong><?= (int)$stats['completed_attempts'] ?></strong><span>Ukończone</span></div></div>
|
|
<div class="col-sm-6 col-lg-3"><div class="metric-card"><strong><?= (int)$stats['completion_rate'] ?>%</strong><span>Wskaźnik ukończenia</span></div></div>
|
|
<div class="col-sm-6 col-lg-3"><div class="metric-card"><strong><?= (int)$stats['average_score'] ?>%</strong><span>Średni wynik</span></div></div>
|
|
</section>
|
|
|
|
<section class="surface-card p-3 p-lg-4 mb-4">
|
|
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3">
|
|
<div>
|
|
<div class="eyebrow mb-2">Nawigacja panelu</div>
|
|
<h2 class="h5 mb-1">Wybierz obszar pracy</h2>
|
|
<p class="text-secondary mb-0">Ustawienia wrażliwe zostały przeniesione do osobnej zakładki, aby nie były widoczne od razu po wejściu do panelu.</p>
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<a class="btn <?= $currentTab === 'overview' ? 'btn-dark' : 'btn-outline-dark' ?>" href="<?= htmlspecialchars(diagnostic_admin_path()) ?>">Wyniki i zgłoszenia</a>
|
|
<a class="btn <?= $currentTab === 'settings' ? 'btn-dark' : 'btn-outline-dark' ?>" href="<?= htmlspecialchars(diagnostic_admin_path(['tab' => 'settings'])) ?>">Ustawienia</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<?php if ($currentTab === 'settings'): ?>
|
|
<section class="row g-4 mb-4">
|
|
<div class="col-xl-6">
|
|
<div class="surface-card p-4 h-100">
|
|
<div class="row g-4 align-items-start">
|
|
<div class="col-lg-7">
|
|
<div class="eyebrow mb-2">Powiadomienia o konsultacji</div>
|
|
<h2 class="h4 mb-2">Adres administratora dla nowych numerów telefonu</h2>
|
|
<p class="text-secondary mb-3">Gdy użytkownik poda numer telefonu po ukończeniu diagnozy, system wyśle powiadomienie e-mail właśnie na ten adres.</p>
|
|
<form method="post" class="row g-3" novalidate>
|
|
<input type="hidden" name="action" value="save-notification-email">
|
|
<div class="col-md-8">
|
|
<label for="notificationEmail" class="form-label">Adres e-mail administratora</label>
|
|
<input type="email" class="form-control" id="notificationEmail" name="notification_email" placeholder="np. kontakt@twojafirma.pl" value="<?= htmlspecialchars((string)$notificationConfig['configured_email']) ?>" required>
|
|
</div>
|
|
<div class="col-md-4 d-grid align-self-end">
|
|
<button type="submit" class="btn btn-dark">Zapisz adres</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="col-lg-5">
|
|
<div class="compact-card h-100">
|
|
<strong>Aktywna konfiguracja</strong>
|
|
<?php if (($notificationConfig['source'] ?? 'none') === 'panel'): ?>
|
|
<span>Powiadomienia trafiają na: <strong><?= htmlspecialchars((string)$notificationConfig['effective_email']) ?></strong></span>
|
|
<span class="text-secondary">Źródło: ustawienie zapisane w panelu administracyjnym.</span>
|
|
<?php elseif (($notificationConfig['source'] ?? 'none') === 'env'): ?>
|
|
<span>Powiadomienia tymczasowo trafią na: <strong><?= htmlspecialchars((string)$notificationConfig['effective_email']) ?></strong></span>
|
|
<span class="text-secondary">To fallback testowy z <code>MAIL_TO</code>. Ustaw adres w panelu, aby zarządzać nim bez edycji środowiska.</span>
|
|
<?php else: ?>
|
|
<span>Brak ustawionego adresu do powiadomień.</span>
|
|
<span class="text-secondary">Do czasu zapisania adresu w panelu e-mail o nowej prośbie o kontakt nie zostanie wysłany.</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-6">
|
|
<div class="surface-card p-4 h-100">
|
|
<div class="row g-4 align-items-start">
|
|
<div class="col-lg-7">
|
|
<div class="eyebrow mb-2">Integracja AI</div>
|
|
<h2 class="h4 mb-2">Własny klucz API OpenAI</h2>
|
|
<p class="text-secondary mb-3">Zapisany tutaj klucz będzie automatycznie używany przez wszystkie obecne wywołania AI w aplikacji: diagnozę, czat i webhook Telegrama.</p>
|
|
<form method="post" class="row g-3" novalidate>
|
|
<input type="hidden" name="action" value="save-openai-api-key">
|
|
<div class="col-12">
|
|
<label for="openAiApiKey" class="form-label">Klucz API OpenAI</label>
|
|
<input type="password" class="form-control" id="openAiApiKey" name="openai_api_key" placeholder="wklej pełny klucz, np. sk-proj-..." value="" autocomplete="new-password" spellcheck="false">
|
|
<div class="form-text">Pole pozostaje puste po zapisaniu ze względów bezpieczeństwa. Wprowadź nowy klucz tylko wtedy, gdy chcesz go zmienić.</div>
|
|
</div>
|
|
<div class="col-sm-6 d-grid">
|
|
<button type="submit" class="btn btn-dark">Zapisz klucz</button>
|
|
</div>
|
|
</form>
|
|
<?php if (!empty($openAiConfig['configured'])): ?>
|
|
<form method="post" class="mt-3" onsubmit="return confirm('Usunąć zapisany klucz API OpenAI?');">
|
|
<input type="hidden" name="action" value="remove-openai-api-key">
|
|
<button type="submit" class="btn btn-outline-dark">Usuń zapisany klucz</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="col-lg-5">
|
|
<div class="compact-card h-100">
|
|
<strong>Aktywna konfiguracja</strong>
|
|
<?php if (!empty($openAiConfig['configured'])): ?>
|
|
<span>Zapytania AI używają własnego klucza zapisanego w panelu.</span>
|
|
<span class="text-secondary">Zapisany klucz: <strong><?= htmlspecialchars((string)$openAiConfig['masked_key']) ?></strong></span>
|
|
<span class="text-secondary">Klucz jest wysyłany centralnie przez <code>LocalAIApi</code>, więc nie trzeba osobno zmieniać endpointów.</span>
|
|
<?php else: ?>
|
|
<span>Brak własnego klucza OpenAI w panelu.</span>
|
|
<span class="text-secondary">Do czasu zapisania klucza aplikacja korzysta z domyślnej konfiguracji proxy.</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<?php else: ?>
|
|
<section class="row g-4">
|
|
<div class="col-xl-8 col-xxl-9">
|
|
<div class="surface-card p-4 h-100">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<div>
|
|
<div class="eyebrow mb-2">Lista prób</div>
|
|
<h2 class="h4 mb-0">Ostatnie odpowiedzi</h2>
|
|
</div>
|
|
<span class="pill-badge subdued"><?= count($attempts) ?> rekordów</span>
|
|
</div>
|
|
<?php if (empty($attempts)): ?>
|
|
<div class="empty-state text-center py-5 px-4">
|
|
<h3 class="h5 mb-2">Brak zapisanych prób</h3>
|
|
<p class="text-secondary mb-0">Po pierwszym wypełnieniu diagnozy przez użytkownika wyniki pojawią się tutaj automatycznie.</p>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="table-responsive admin-table-wrap">
|
|
<table class="table admin-table align-middle mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">ID</th>
|
|
<th scope="col">E-mail</th>
|
|
<th scope="col">Status</th>
|
|
<th scope="col">Telefon</th>
|
|
<th scope="col">Wynik</th>
|
|
<th scope="col">Segment</th>
|
|
<th scope="col">Raport</th>
|
|
<th scope="col">Szczegóły</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($attempts as $row): ?>
|
|
<tr>
|
|
<td>#<?= (int)$row['id'] ?></td>
|
|
<td><?= htmlspecialchars((string)$row['email']) ?></td>
|
|
<td><span class="pill-badge<?= ($row['status'] ?? '') === 'completed' ? '' : ' subdued' ?>"><?= ($row['status'] ?? '') === 'completed' ? 'ukończona' : 'w trakcie' ?></span></td>
|
|
<td><?= !empty($row['contact_phone']) ? htmlspecialchars((string)$row['contact_phone']) : '—' ?></td>
|
|
<td><?= isset($row['result']['percentage_score']) ? (int)$row['result']['percentage_score'] . '%' : '—' ?></td>
|
|
<td><?= htmlspecialchars((string)($row['result']['segment_label'] ?? '—')) ?></td>
|
|
<td><?= htmlspecialchars((string)$row['email_report_status']) ?></td>
|
|
<td><a class="btn btn-sm btn-outline-dark" href="<?= htmlspecialchars(diagnostic_admin_path(['attempt' => (int)$row['id']])) ?>">Zobacz</a></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-4 col-xxl-3">
|
|
<div class="surface-card p-4 h-100">
|
|
<?php if (!$selectedAttempt): ?>
|
|
<div class="empty-state text-center py-5 px-4">
|
|
<h3 class="h5 mb-2">Brak wybranej próby</h3>
|
|
<p class="text-secondary mb-0">Wybierz odpowiedź z listy, aby zobaczyć szczegóły, rozkład sekcji i pełny zestaw odpowiedzi.</p>
|
|
</div>
|
|
<?php else: ?>
|
|
<?php $result = $selectedAttempt['result'] ?? []; ?>
|
|
<div class="attempt-header d-flex flex-wrap justify-content-between gap-3 mb-4 align-items-start">
|
|
<div class="attempt-identity">
|
|
<div class="eyebrow mb-2">Szczegóły próby #<?= (int)$selectedAttempt['id'] ?></div>
|
|
<h2 class="h4 mb-1 attempt-email"><?= htmlspecialchars((string)$selectedAttempt['email']) ?></h2>
|
|
<p class="text-secondary mb-0">Rozpoczęto: <?= htmlspecialchars((string)$selectedAttempt['started_at']) ?></p>
|
|
</div>
|
|
<div class="score-chip text-end">
|
|
<span>Wynik</span>
|
|
<strong><?= isset($result['percentage_score']) ? (int)$result['percentage_score'] . '%' : '—' ?></strong>
|
|
</div>
|
|
</div>
|
|
<div class="mb-4">
|
|
<div class="row g-3">
|
|
<div class="col-sm-6"><div class="compact-card"><strong>Status</strong><span><?= ($selectedAttempt['status'] ?? '') === 'completed' ? 'Ukończona' : 'W trakcie' ?></span></div></div>
|
|
<div class="col-sm-6"><div class="compact-card"><strong>Raport e-mail</strong><span><?= htmlspecialchars((string)$selectedAttempt['email_report_status']) ?></span></div></div>
|
|
<div class="col-sm-6"><div class="compact-card"><strong>Zgoda marketingowa</strong><span><?= ((int)$selectedAttempt['marketing_consent']) === 1 ? 'Tak' : 'Nie' ?></span></div></div>
|
|
<div class="col-sm-6"><div class="compact-card"><strong>Telefon kontaktowy</strong><span><?= !empty($selectedAttempt['contact_phone']) ? htmlspecialchars((string)$selectedAttempt['contact_phone']) : '—' ?></span></div></div>
|
|
<div class="col-sm-6"><div class="compact-card"><strong>Prośba o kontakt</strong><span><?= htmlspecialchars((string)($selectedAttempt['consultation_requested_at'] ?? '—')) ?></span></div></div>
|
|
<div class="col-sm-6"><div class="compact-card"><strong>Zakończono</strong><span><?= htmlspecialchars((string)($selectedAttempt['completed_at'] ?? '—')) ?></span></div></div>
|
|
<div class="col-12">
|
|
<div class="compact-card">
|
|
<strong>Ponowna wysyłka raportu</strong>
|
|
<?php if (diagnostic_attempt_can_send_report($selectedAttempt)): ?>
|
|
<span>Wyślij ponownie raport na adres klienta: <?= htmlspecialchars((string)$selectedAttempt['email']) ?></span>
|
|
<form method="post" class="mt-3">
|
|
<input type="hidden" name="action" value="resend-report">
|
|
<input type="hidden" name="attempt_id" value="<?= (int)$selectedAttempt['id'] ?>">
|
|
<button type="submit" class="btn btn-sm btn-dark">Wyślij ponownie</button>
|
|
</form>
|
|
<?php else: ?>
|
|
<span>Ponowna wysyłka będzie dostępna po ukończeniu diagnozy i wygenerowaniu wyniku.</span>
|
|
<?php endif; ?>
|
|
<?php if (!empty($selectedAttempt['email_report_error'])): ?>
|
|
<span class="text-danger small mt-2">Ostatni błąd wysyłki: <?= htmlspecialchars((string)$selectedAttempt['email_report_error']) ?></span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (!empty($result)): ?>
|
|
<div class="mb-4">
|
|
<h3 class="h5 mb-3">Podsumowanie wyniku</h3>
|
|
<p class="text-secondary mb-3"><strong><?= htmlspecialchars((string)($result['segment_label'] ?? '')) ?></strong> — <?= htmlspecialchars((string)($result['segment_summary'] ?? '')) ?></p>
|
|
<div class="row g-3">
|
|
<?php foreach (($result['section_scores'] ?? []) as $section): ?>
|
|
<div class="col-12">
|
|
<div class="section-score-card">
|
|
<span class="small text-secondary d-block mb-2"><?= htmlspecialchars($section['section_name']) ?></span>
|
|
<strong class="d-block mb-2"><?= (int)$section['percentage'] ?>%</strong>
|
|
<div class="progress" style="height: 8px;">
|
|
<div class="progress-bar bg-dark" role="progressbar" style="width: <?= (int)$section['percentage'] ?>%;" aria-valuenow="<?= (int)$section['percentage'] ?>" aria-valuemin="0" aria-valuemax="100"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div>
|
|
<h3 class="h5 mb-3">Odpowiedzi respondenta</h3>
|
|
<ul class="answer-review-list mb-0">
|
|
<?php foreach ($questionMap as $questionId => $question): ?>
|
|
<?php $rawAnswer = $selectedAttempt['answers'][$questionId] ?? null; ?>
|
|
<li>
|
|
<em><?= htmlspecialchars($question['section_name']) ?></em>
|
|
<strong><?= htmlspecialchars($question['text']) ?></strong>
|
|
<span class="text-secondary"><?= $rawAnswer === null ? 'Brak odpowiedzi' : htmlspecialchars(diagnostic_answer_label((int)$rawAnswer)) ?></span>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="surface-card p-4 mt-4">
|
|
<div class="section-intro mb-3">
|
|
<div class="eyebrow">Zakres diagnozy</div>
|
|
<h2 class="h4 mb-0">Obszary oceniane w narzędziu</h2>
|
|
</div>
|
|
<div class="row g-3">
|
|
<?php foreach ($definition['sections'] as $section): ?>
|
|
<div class="col-md-6 col-xl-4">
|
|
<div class="compact-card h-100">
|
|
<strong><?= htmlspecialchars($section['name']) ?></strong>
|
|
<span><?= count($section['questions']) ?> pytań w tej sekcji.</span>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</section>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</main>
|
|
|
|
<footer class="border-top py-4 bg-white">
|
|
<div class="container d-flex flex-column flex-md-row justify-content-between gap-2">
|
|
<span class="text-secondary small">Panel administracyjny diagnozy dojrzałości procesowej.</span>
|
|
<div class="d-flex gap-3 small">
|
|
<a class="text-decoration-none text-secondary" href="/">Strona główna</a>
|
|
<a class="text-decoration-none text-secondary" href="/diagnostic.php">Diagnoza</a>
|
|
<a class="text-decoration-none text-secondary" href="/healthz.php">Status</a>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" defer></script>
|
|
<script src="/assets/js/main.js?v=<?= urlencode((string)@filemtime(__DIR__ . '/assets/js/main.js')) ?>" defer></script>
|
|
</body>
|
|
</html>
|