Wersja początkowa - działający mechanizm
This commit is contained in:
parent
2b94c80a4e
commit
944cf1608d
292
admin.php
Normal file
292
admin.php
Normal file
@ -0,0 +1,292 @@
|
||||
<?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: /admin.php');
|
||||
exit;
|
||||
}
|
||||
diagnostic_flash_set('danger', 'Nieprawidłowy login lub hasło administratora.');
|
||||
header('Location: /admin.php');
|
||||
exit;
|
||||
}
|
||||
if ($action === 'logout') {
|
||||
diagnostic_admin_logout();
|
||||
diagnostic_flash_set('info', 'Sesja administratora została zakończona.');
|
||||
header('Location: /admin.php');
|
||||
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();
|
||||
?>
|
||||
<!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">
|
||||
<a class="navbar-brand fw-semibold text-dark" href="/">Przegląd procesów</a>
|
||||
<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="/admin.php?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="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="/admin.php?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="d-flex justify-content-between gap-3 mb-4 align-items-start">
|
||||
<div>
|
||||
<div class="eyebrow mb-2">Szczegóły próby #<?= (int)$selectedAttempt['id'] ?></div>
|
||||
<h2 class="h4 mb-1"><?= 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>
|
||||
</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; ?>
|
||||
</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>
|
||||
15
app/bootstrap.php
Normal file
15
app/bootstrap.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/../mail/MailService.php';
|
||||
require_once __DIR__ . '/diagnostic_data.php';
|
||||
require_once __DIR__ . '/diagnostic_functions.php';
|
||||
|
||||
ensure_diagnostic_schema();
|
||||
168
app/diagnostic_data.php
Normal file
168
app/diagnostic_data.php
Normal file
@ -0,0 +1,168 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
function diagnostic_quiz_definition(): array
|
||||
{
|
||||
return [
|
||||
'quiz' => [
|
||||
'slug' => 'business-process-diagnostic-v1',
|
||||
'title' => 'Diagnoza dojrzałości procesowej',
|
||||
'estimated_time' => '8–10 minut',
|
||||
'subtitle' => 'Oceń, na ile procesy w Twojej firmie są uporządkowane, mierzalne i gotowe do dalszego wzrostu bez nadmiernej zależności od właściciela.',
|
||||
],
|
||||
'answer_scale' => [
|
||||
0 => 'Zdecydowanie nie',
|
||||
1 => 'Raczej nie',
|
||||
2 => 'Raczej tak',
|
||||
3 => 'Zdecydowanie tak',
|
||||
],
|
||||
'sections' => [
|
||||
[
|
||||
'id' => 'task_management',
|
||||
'name' => 'Zarządzanie zadaniami',
|
||||
'questions' => [
|
||||
['id' => 'q1', 'text' => 'Czy w dowolnym momencie możesz sprawdzić, kto aktualnie pracuje nad jakim zadaniem?'],
|
||||
['id' => 'q2', 'text' => 'Czy wszystkie zadania w firmie są zapisywane w jednym miejscu?'],
|
||||
['id' => 'q3', 'text' => 'Czy zdarza się, że zadania „giną” lub są zapominane?', 'reverse' => true],
|
||||
['id' => 'q4', 'text' => 'Czy pracownicy wiedzą dokładnie, co mają robić bez konieczności dopytywania?'],
|
||||
['id' => 'q5', 'text' => 'Czy masz możliwość sprawdzenia, ile czasu zajmuje realizacja konkretnych zadań?'],
|
||||
['id' => 'q6', 'text' => 'Czy jesteś w stanie szybko zidentyfikować opóźnienia w pracy?'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 'repeatability',
|
||||
'name' => 'Procesy i powtarzalność',
|
||||
'questions' => [
|
||||
['id' => 'q7', 'text' => 'Czy kluczowe działania w firmie mają spisany przebieg krok po kroku?'],
|
||||
['id' => 'q8', 'text' => 'Czy nowy pracownik jest w stanie wykonać zadanie na podstawie instrukcji?'],
|
||||
['id' => 'q9', 'text' => 'Czy sposób realizacji zadań jest powtarzalny między pracownikami?'],
|
||||
['id' => 'q10', 'text' => 'Czy jakość realizacji usług lub produktów jest przewidywalna?'],
|
||||
['id' => 'q11', 'text' => 'Czy w firmie istnieją checklisty lub procedury dla kluczowych działań?'],
|
||||
['id' => 'q12', 'text' => 'Czy procesy są aktualizowane po popełnionych błędach?'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 'communication',
|
||||
'name' => 'Komunikacja wewnętrzna',
|
||||
'questions' => [
|
||||
['id' => 'q13', 'text' => 'Czy informacje w firmie są przekazywane w uporządkowany sposób (np. jedno narzędzie)?'],
|
||||
['id' => 'q14', 'text' => 'Czy zdarza się, że te same tematy są omawiane wielokrotnie?', 'reverse' => true],
|
||||
['id' => 'q15', 'text' => 'Czy pracownicy mają dostęp do wszystkich potrzebnych informacji?'],
|
||||
['id' => 'q16', 'text' => 'Czy decyzje są jasno komunikowane i rozumiane przez zespół?'],
|
||||
['id' => 'q17', 'text' => 'Czy zdarzają się sytuacje, w których różne osoby działają na podstawie sprzecznych informacji?', 'reverse' => true],
|
||||
['id' => 'q18', 'text' => 'Czy ustalenia ze spotkań są zapisywane i przekładane na konkretne działania?'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 'control',
|
||||
'name' => 'Kontrola i nadzór',
|
||||
'questions' => [
|
||||
['id' => 'q19', 'text' => 'Czy osoby zarządzające mają bieżący wgląd w status kluczowych działań?'],
|
||||
['id' => 'q20', 'text' => 'Czy w firmie istnieją jasne wskaźniki pokazujące, czy praca przebiega zgodnie z planem?'],
|
||||
['id' => 'q21', 'text' => 'Czy błędy, opóźnienia lub odchylenia są wychwytywane na wczesnym etapie?'],
|
||||
['id' => 'q22', 'text' => 'Czy potrafisz szybko ustalić przyczynę problemu operacyjnego?'],
|
||||
['id' => 'q23', 'text' => 'Czy regularnie analizowane są wyniki zespołów, procesów lub projektów?'],
|
||||
['id' => 'q24', 'text' => 'Czy odpowiedzialność za wyniki i terminy jest jasno przypisana?'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 'sales_service',
|
||||
'name' => 'Sprzedaż i obsługa klienta',
|
||||
'questions' => [
|
||||
['id' => 'q25', 'text' => 'Czy proces sprzedaży od pierwszego kontaktu do zamknięcia jest uporządkowany i powtarzalny?'],
|
||||
['id' => 'q26', 'text' => 'Czy wiadomo, na jakim etapie znajduje się każda szansa sprzedażowa?'],
|
||||
['id' => 'q27', 'text' => 'Czy oferty handlowe są przygotowywane według spójnego standardu?'],
|
||||
['id' => 'q28', 'text' => 'Czy po sprzedaży klient otrzymuje obsługę według jasno określonego procesu?'],
|
||||
['id' => 'q29', 'text' => 'Czy reklamacje, uwagi i potrzeby klientów są systematycznie rejestrowane?'],
|
||||
['id' => 'q30', 'text' => 'Czy zespół potrafi szybko reagować na problemy klienta bez angażowania właściciela w każdą sprawę?'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 'finance',
|
||||
'name' => 'Finanse i rozliczenia',
|
||||
'questions' => [
|
||||
['id' => 'q31', 'text' => 'Czy masz regularny i aktualny wgląd w przychody, koszty i rentowność?'],
|
||||
['id' => 'q32', 'text' => 'Czy w firmie istnieje uporządkowany proces wystawiania faktur i kontroli płatności?'],
|
||||
['id' => 'q33', 'text' => 'Czy wiesz, które usługi, produkty lub klienci są najbardziej rentowni?'],
|
||||
['id' => 'q34', 'text' => 'Czy opóźnienia w płatnościach są szybko identyfikowane i obsługiwane?'],
|
||||
['id' => 'q35', 'text' => 'Czy decyzje operacyjne są podejmowane z uwzględnieniem danych finansowych?'],
|
||||
['id' => 'q36', 'text' => 'Czy przepływy pieniężne są monitorowane w sposób pozwalający wcześniej reagować na ryzyka?'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'id' => 'owner_dependency',
|
||||
'name' => 'Decyzyjność i rola właściciela',
|
||||
'questions' => [
|
||||
['id' => 'q37', 'text' => 'Czy firma potrafi działać sprawnie podczas Twojej nieobecności przez kilka dni?'],
|
||||
['id' => 'q38', 'text' => 'Czy decyzje operacyjne są podejmowane na odpowiednim poziomie bez eskalowania wszystkiego do właściciela?'],
|
||||
['id' => 'q39', 'text' => 'Czy role i zakres odpowiedzialności w zespole są jasno określone?'],
|
||||
['id' => 'q40', 'text' => 'Czy właściciel ma czas na rozwój firmy, a nie wyłącznie na bieżące gaszenie problemów?'],
|
||||
['id' => 'q41', 'text' => 'Czy kluczowa wiedza operacyjna jest rozproszona w zespole, a nie skupiona wyłącznie u właściciela?'],
|
||||
['id' => 'q42', 'text' => 'Czy firma podejmuje decyzje na podstawie danych i ustalonych zasad, a nie wyłącznie intuicji właściciela?'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'segments' => [
|
||||
[
|
||||
'key' => 'operational_chaos',
|
||||
'label' => 'Chaos operacyjny',
|
||||
'min' => 0,
|
||||
'max' => 25,
|
||||
'summary' => 'Codzienne funkcjonowanie firmy jest w dużej mierze reaktywne. Wiele działań zależy od pamięci, bieżących interwencji i osobistej kontroli właściciela, co zwiększa ryzyko błędów, opóźnień i utraty marży.',
|
||||
'recommendations' => [
|
||||
'Ustal jeden wspólny sposób rejestrowania zadań, odpowiedzialności i terminów.',
|
||||
'Opisz 3–5 najważniejszych procesów operacyjnych w formie prostych instrukcji lub checklist.',
|
||||
'Wprowadź regularny rytm przeglądu zadań i problemów, aby szybciej wychwytywać odchylenia.',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'reactive_company',
|
||||
'label' => 'Reaktywny model działania',
|
||||
'min' => 26,
|
||||
'max' => 45,
|
||||
'summary' => 'W firmie istnieją już pojedyncze elementy porządku, jednak sposób działania nadal opiera się bardziej na reagowaniu niż na przewidywalnym zarządzaniu. Wąskie gardła i zależność od pojedynczych osób są nadal wyraźne.',
|
||||
'recommendations' => [
|
||||
'Uporządkuj przepływ informacji i ogranicz liczbę miejsc, w których prowadzone są ustalenia.',
|
||||
'Zdefiniuj właścicieli procesów i podstawowe wskaźniki dla obszarów o największym wpływie na wynik firmy.',
|
||||
'Ustandaryzuj przekazywanie zadań oraz sposób raportowania statusu prac.',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'partially_organized',
|
||||
'label' => 'Częściowe uporządkowanie',
|
||||
'min' => 46,
|
||||
'max' => 65,
|
||||
'summary' => 'Firma ma już działające fundamenty operacyjne, ale wciąż występują luki w spójności procesów, kontroli i delegowaniu decyzji. To etap, na którym dobre uporządkowanie wybranych obszarów może szybko zwiększyć efektywność.',
|
||||
'recommendations' => [
|
||||
'Uzupełnij brakujące procedury dla powtarzalnych działań o wysokim koszcie błędu.',
|
||||
'Rozszerz kontrolę o cykliczne przeglądy wyników, terminów i jakości wykonania.',
|
||||
'Ogranicz zależność od właściciela poprzez jasne role, decyzje delegowane i standardy pracy.',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'well_managed',
|
||||
'label' => 'Dobrze zarządzana organizacja',
|
||||
'min' => 66,
|
||||
'max' => 85,
|
||||
'summary' => 'Procesy są w dużej mierze przewidywalne, a menedżerowie mają realny wgląd w sytuację operacyjną. Kolejny poziom rozwoju wymaga dalszego wzmacniania mierzalności, ciągłego doskonalenia i redukcji pojedynczych punktów zależności.',
|
||||
'recommendations' => [
|
||||
'Rozwijaj wskaźniki zarządcze i wykorzystuj je do wcześniejszego reagowania, a nie tylko raportowania po fakcie.',
|
||||
'Regularnie aktualizuj procedury po błędach, reklamacjach i zmianach w sposobie pracy.',
|
||||
'Buduj większą samodzielność zespołu w decyzjach operacyjnych i obsłudze klienta.',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'scaling_ready',
|
||||
'label' => 'Gotowość do skalowania',
|
||||
'min' => 86,
|
||||
'max' => 100,
|
||||
'summary' => 'Model operacyjny firmy jest dojrzały, spójny i stosunkowo odporny na chaos dnia codziennego. Organizacja ma dobre podstawy do wzrostu, większej skali oraz pracy właściciela bardziej na poziomie strategicznym niż wykonawczym.',
|
||||
'recommendations' => [
|
||||
'Wykorzystuj dane operacyjne i finansowe do planowania rozwoju, inwestycji i skalowania zespołu.',
|
||||
'Koncentruj się na ciągłym doskonaleniu oraz automatyzacji obszarów o największej powtarzalności.',
|
||||
'Przeglądaj dojrzałość procesową cyklicznie, aby utrzymać jakość działania przy dalszym wzroście firmy.',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
498
app/diagnostic_functions.php
Normal file
498
app/diagnostic_functions.php
Normal file
@ -0,0 +1,498 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
function ensure_diagnostic_schema(): void
|
||||
{
|
||||
db()->exec(
|
||||
"CREATE TABLE IF NOT EXISTS diagnostic_attempts (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
session_token VARCHAR(64) NOT NULL UNIQUE,
|
||||
email VARCHAR(190) NOT NULL,
|
||||
marketing_consent TINYINT(1) NOT NULL DEFAULT 0,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'in_progress',
|
||||
current_step INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
answers_json LONGTEXT NULL,
|
||||
result_json LONGTEXT NULL,
|
||||
email_report_status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
email_report_error TEXT NULL,
|
||||
contact_phone VARCHAR(40) NULL,
|
||||
consultation_requested_at DATETIME NULL,
|
||||
started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at DATETIME NULL,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
||||
);
|
||||
|
||||
if (!diagnostic_schema_column_exists('diagnostic_attempts', 'contact_phone')) {
|
||||
db()->exec("ALTER TABLE diagnostic_attempts ADD COLUMN contact_phone VARCHAR(40) NULL AFTER email_report_error");
|
||||
}
|
||||
|
||||
if (!diagnostic_schema_column_exists('diagnostic_attempts', 'consultation_requested_at')) {
|
||||
db()->exec("ALTER TABLE diagnostic_attempts ADD COLUMN consultation_requested_at DATETIME NULL AFTER contact_phone");
|
||||
}
|
||||
}
|
||||
|
||||
function diagnostic_schema_column_exists(string $tableName, string $columnName): bool
|
||||
{
|
||||
$stmt = db()->prepare(
|
||||
'SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = :schema AND TABLE_NAME = :table_name AND COLUMN_NAME = :column_name'
|
||||
);
|
||||
$stmt->execute([
|
||||
':schema' => DB_NAME,
|
||||
':table_name' => $tableName,
|
||||
':column_name' => $columnName,
|
||||
]);
|
||||
return (int)$stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
function diagnostic_meta(string $pageTitle, ?string $fallbackDescription = null): array
|
||||
{
|
||||
$projectName = $_SERVER['PROJECT_NAME'] ?? 'Przegląd Procesów';
|
||||
$projectDescription = trim((string)($_SERVER['PROJECT_DESCRIPTION'] ?? ''));
|
||||
return [
|
||||
'title' => $pageTitle . ' · ' . $projectName,
|
||||
'description' => $projectDescription !== '' ? $projectDescription : ($fallbackDescription ?? 'Diagnoza dojrzałości procesowej dla właścicieli firm i liderów operacyjnych.'),
|
||||
'image' => $_SERVER['PROJECT_IMAGE_URL'] ?? '',
|
||||
'project_name' => $projectName,
|
||||
];
|
||||
}
|
||||
|
||||
function diagnostic_flat_questions(): array
|
||||
{
|
||||
$definition = diagnostic_quiz_definition();
|
||||
$questions = [];
|
||||
foreach ($definition['sections'] as $sectionIndex => $section) {
|
||||
foreach ($section['questions'] as $questionIndex => $question) {
|
||||
$questions[] = [
|
||||
'id' => $question['id'],
|
||||
'text' => $question['text'],
|
||||
'reverse' => !empty($question['reverse']),
|
||||
'section_id' => $section['id'],
|
||||
'section_name' => $section['name'],
|
||||
'section_order' => $sectionIndex,
|
||||
'question_order' => $questionIndex,
|
||||
];
|
||||
}
|
||||
}
|
||||
return $questions;
|
||||
}
|
||||
|
||||
function diagnostic_question_map(): array
|
||||
{
|
||||
$map = [];
|
||||
foreach (diagnostic_flat_questions() as $question) {
|
||||
$map[$question['id']] = $question;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
function diagnostic_find_question(string $questionId): ?array
|
||||
{
|
||||
$map = diagnostic_question_map();
|
||||
return $map[$questionId] ?? null;
|
||||
}
|
||||
|
||||
function diagnostic_answer_label(int $value): string
|
||||
{
|
||||
$definition = diagnostic_quiz_definition();
|
||||
return $definition['answer_scale'][$value] ?? 'Nieznana odpowiedź';
|
||||
}
|
||||
|
||||
function diagnostic_segment_for_percentage(int $percentage): array
|
||||
{
|
||||
$definition = diagnostic_quiz_definition();
|
||||
foreach ($definition['segments'] as $segment) {
|
||||
if ($percentage >= $segment['min'] && $percentage <= $segment['max']) {
|
||||
return $segment;
|
||||
}
|
||||
}
|
||||
return end($definition['segments']);
|
||||
}
|
||||
|
||||
function diagnostic_create_attempt(string $email, bool $marketingConsent): int
|
||||
{
|
||||
$stmt = db()->prepare(
|
||||
'INSERT INTO diagnostic_attempts (session_token, email, marketing_consent, status, current_step, answers_json, result_json, email_report_status)
|
||||
VALUES (:session_token, :email, :marketing_consent, :status, :current_step, :answers_json, :result_json, :email_report_status)'
|
||||
);
|
||||
$stmt->execute([
|
||||
':session_token' => bin2hex(random_bytes(16)),
|
||||
':email' => $email,
|
||||
':marketing_consent' => $marketingConsent ? 1 : 0,
|
||||
':status' => 'in_progress',
|
||||
':current_step' => 1,
|
||||
':answers_json' => json_encode([], JSON_UNESCAPED_UNICODE),
|
||||
':result_json' => null,
|
||||
':email_report_status' => 'pending',
|
||||
]);
|
||||
$id = (int)db()->lastInsertId();
|
||||
$_SESSION['diagnostic_attempt_id'] = $id;
|
||||
return $id;
|
||||
}
|
||||
|
||||
function diagnostic_clear_attempt_session(): void
|
||||
{
|
||||
unset($_SESSION['diagnostic_attempt_id']);
|
||||
}
|
||||
|
||||
function diagnostic_current_attempt(): ?array
|
||||
{
|
||||
$attemptId = isset($_SESSION['diagnostic_attempt_id']) ? (int)$_SESSION['diagnostic_attempt_id'] : 0;
|
||||
if ($attemptId <= 0) {
|
||||
return null;
|
||||
}
|
||||
return diagnostic_get_attempt($attemptId);
|
||||
}
|
||||
|
||||
function diagnostic_get_attempt(int $attemptId): ?array
|
||||
{
|
||||
$stmt = db()->prepare('SELECT * FROM diagnostic_attempts WHERE id = :id LIMIT 1');
|
||||
$stmt->execute([':id' => $attemptId]);
|
||||
$attempt = $stmt->fetch();
|
||||
if (!$attempt) {
|
||||
return null;
|
||||
}
|
||||
$attempt['answers'] = diagnostic_decode_json_map($attempt['answers_json'] ?? '');
|
||||
$attempt['result'] = diagnostic_decode_json_map($attempt['result_json'] ?? '');
|
||||
return $attempt;
|
||||
}
|
||||
|
||||
function diagnostic_decode_json_map(?string $json): array
|
||||
{
|
||||
if (!$json) {
|
||||
return [];
|
||||
}
|
||||
$decoded = json_decode($json, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
function diagnostic_save_answer(int $attemptId, string $questionId, int $selectedValue, int $currentStep): void
|
||||
{
|
||||
$attempt = diagnostic_get_attempt($attemptId);
|
||||
if (!$attempt) {
|
||||
return;
|
||||
}
|
||||
$answers = $attempt['answers'];
|
||||
$answers[$questionId] = $selectedValue;
|
||||
|
||||
$stmt = db()->prepare(
|
||||
'UPDATE diagnostic_attempts
|
||||
SET answers_json = :answers_json, current_step = :current_step, updated_at = NOW()
|
||||
WHERE id = :id'
|
||||
);
|
||||
$stmt->execute([
|
||||
':answers_json' => json_encode($answers, JSON_UNESCAPED_UNICODE),
|
||||
':current_step' => $currentStep,
|
||||
':id' => $attemptId,
|
||||
]);
|
||||
}
|
||||
|
||||
function diagnostic_update_step(int $attemptId, int $currentStep): void
|
||||
{
|
||||
$stmt = db()->prepare('UPDATE diagnostic_attempts SET current_step = :current_step, updated_at = NOW() WHERE id = :id');
|
||||
$stmt->execute([':current_step' => $currentStep, ':id' => $attemptId]);
|
||||
}
|
||||
|
||||
|
||||
function diagnostic_normalize_phone(string $phone): string
|
||||
{
|
||||
$phone = trim(preg_replace('/\s+/', ' ', $phone) ?? '');
|
||||
return $phone;
|
||||
}
|
||||
|
||||
function diagnostic_phone_is_valid(string $phone): bool
|
||||
{
|
||||
if ($phone === '' || strlen($phone) > 40) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!preg_match('/^[0-9+()\-\s]+$/', $phone)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$digitsOnly = preg_replace('/\D+/', '', $phone) ?? '';
|
||||
$digitCount = strlen($digitsOnly);
|
||||
return $digitCount >= 7 && $digitCount <= 15;
|
||||
}
|
||||
|
||||
function diagnostic_save_contact_phone(int $attemptId, string $phone): void
|
||||
{
|
||||
$stmt = db()->prepare(
|
||||
'UPDATE diagnostic_attempts
|
||||
SET contact_phone = :contact_phone, consultation_requested_at = NOW(), updated_at = NOW()
|
||||
WHERE id = :id'
|
||||
);
|
||||
$stmt->execute([
|
||||
':contact_phone' => $phone,
|
||||
':id' => $attemptId,
|
||||
]);
|
||||
}
|
||||
|
||||
function diagnostic_effective_answer_value(array $question, array $answers): int
|
||||
{
|
||||
$value = (int)($answers[$question['id']] ?? 0);
|
||||
return !empty($question['reverse']) ? 3 - $value : $value;
|
||||
}
|
||||
|
||||
function diagnostic_calculate_result(array $answers): array
|
||||
{
|
||||
$definition = diagnostic_quiz_definition();
|
||||
$maxScore = count(diagnostic_flat_questions()) * 3;
|
||||
$totalScore = 0;
|
||||
$sectionScores = [];
|
||||
|
||||
foreach ($definition['sections'] as $sectionIndex => $section) {
|
||||
$sectionScore = 0;
|
||||
$questionCount = count($section['questions']);
|
||||
foreach ($section['questions'] as $question) {
|
||||
$sectionScore += diagnostic_effective_answer_value($question, $answers);
|
||||
}
|
||||
$sectionMax = $questionCount * 3;
|
||||
$sectionPercentage = $sectionMax > 0 ? (int)round(($sectionScore / $sectionMax) * 100) : 0;
|
||||
$sectionScores[] = [
|
||||
'section_id' => $section['id'],
|
||||
'section_name' => $section['name'],
|
||||
'score' => $sectionScore,
|
||||
'max_score' => $sectionMax,
|
||||
'percentage' => $sectionPercentage,
|
||||
'order' => $sectionIndex,
|
||||
];
|
||||
$totalScore += $sectionScore;
|
||||
}
|
||||
|
||||
$ranked = $sectionScores;
|
||||
usort($ranked, static fn(array $a, array $b): int => $b['percentage'] <=> $a['percentage']);
|
||||
$strongest = array_slice($ranked, 0, 3);
|
||||
$weakest = array_slice(array_reverse($ranked), 0, 3);
|
||||
|
||||
usort($sectionScores, static fn(array $a, array $b): int => $a['order'] <=> $b['order']);
|
||||
foreach ($sectionScores as &$sectionScore) {
|
||||
unset($sectionScore['order']);
|
||||
}
|
||||
unset($sectionScore);
|
||||
foreach ($strongest as &$area) {
|
||||
unset($area['order']);
|
||||
}
|
||||
unset($area);
|
||||
foreach ($weakest as &$area) {
|
||||
unset($area['order']);
|
||||
}
|
||||
unset($area);
|
||||
|
||||
$percentage = $maxScore > 0 ? (int)round(($totalScore / $maxScore) * 100) : 0;
|
||||
$segment = diagnostic_segment_for_percentage($percentage);
|
||||
|
||||
return [
|
||||
'total_score' => $totalScore,
|
||||
'max_score' => $maxScore,
|
||||
'percentage_score' => $percentage,
|
||||
'segment_key' => $segment['key'],
|
||||
'segment_label' => $segment['label'],
|
||||
'segment_summary' => $segment['summary'],
|
||||
'recommendations' => $segment['recommendations'],
|
||||
'section_scores' => $sectionScores,
|
||||
'strongest_areas' => $strongest,
|
||||
'weakest_areas' => $weakest,
|
||||
'completed_questions' => count($answers),
|
||||
'total_questions' => count(diagnostic_flat_questions()),
|
||||
];
|
||||
}
|
||||
|
||||
function diagnostic_finalize_attempt(int $attemptId): ?array
|
||||
{
|
||||
$attempt = diagnostic_get_attempt($attemptId);
|
||||
if (!$attempt) {
|
||||
return null;
|
||||
}
|
||||
$result = diagnostic_calculate_result($attempt['answers']);
|
||||
$stmt = db()->prepare(
|
||||
'UPDATE diagnostic_attempts
|
||||
SET status = :status, current_step = :current_step, result_json = :result_json, completed_at = NOW(), updated_at = NOW()
|
||||
WHERE id = :id'
|
||||
);
|
||||
$stmt->execute([
|
||||
':status' => 'completed',
|
||||
':current_step' => count(diagnostic_flat_questions()),
|
||||
':result_json' => json_encode($result, JSON_UNESCAPED_UNICODE),
|
||||
':id' => $attemptId,
|
||||
]);
|
||||
|
||||
$attempt = diagnostic_get_attempt($attemptId);
|
||||
$emailResult = diagnostic_send_report_email($attempt);
|
||||
|
||||
$emailStmt = db()->prepare('UPDATE diagnostic_attempts SET email_report_status = :status, email_report_error = :error WHERE id = :id');
|
||||
$emailStmt->execute([
|
||||
':status' => $emailResult['success'] ? 'sent' : 'failed',
|
||||
':error' => $emailResult['success'] ? null : ($emailResult['error'] ?? 'Nieznany błąd wysyłki e-mail'),
|
||||
':id' => $attemptId,
|
||||
]);
|
||||
|
||||
return diagnostic_get_attempt($attemptId);
|
||||
}
|
||||
|
||||
function diagnostic_build_report_html(array $attempt): string
|
||||
{
|
||||
$result = $attempt['result'] ?? [];
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||
$resultUrl = $scheme . '://' . $host . '/diagnostic.php?view=result';
|
||||
|
||||
$weakest = '';
|
||||
foreach (($result['weakest_areas'] ?? []) as $area) {
|
||||
$weakest .= '<li><strong>' . htmlspecialchars($area['section_name']) . ':</strong> ' . (int)$area['percentage'] . '% dojrzałości — ten obszar najprawdopodobniej generuje dziś największe tarcia, opóźnienia lub ukryte ryzyka.</li>';
|
||||
}
|
||||
|
||||
$recommendations = '';
|
||||
foreach (($result['recommendations'] ?? []) as $item) {
|
||||
$recommendations .= '<li>' . htmlspecialchars($item) . '</li>';
|
||||
}
|
||||
|
||||
return '<div style="font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#111827;line-height:1.6">'
|
||||
. '<h1 style="font-size:24px;margin:0 0 12px">Raport z diagnozy dojrzałości procesowej</h1>'
|
||||
. '<p style="margin:0 0 16px">Dziękujemy za wypełnienie diagnozy. Obecny poziom dojrzałości operacyjnej Twojej firmy to <strong>' . htmlspecialchars($result['segment_label'] ?? 'Diagnoza zakończona') . '</strong> (' . (int)($result['percentage_score'] ?? 0) . '%).</p>'
|
||||
. '<p style="margin:0 0 16px">' . htmlspecialchars($result['segment_summary'] ?? '') . '</p>'
|
||||
. '<h2 style="font-size:18px;margin:24px 0 8px">Priorytetowe obszary</h2><ul>' . $weakest . '</ul>'
|
||||
. '<h2 style="font-size:18px;margin:24px 0 8px">Rekomendowane kolejne kroki</h2><ul>' . $recommendations . '</ul>'
|
||||
. '<p style="margin:24px 0 0">Jeśli chcesz omówić wynik i ustalić priorytety zmian, wróć do podsumowania: <a href="' . htmlspecialchars($resultUrl) . '">' . htmlspecialchars($resultUrl) . '</a>.</p>'
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
function diagnostic_build_report_text(array $attempt): string
|
||||
{
|
||||
$result = $attempt['result'] ?? [];
|
||||
$lines = [];
|
||||
$lines[] = 'Raport z diagnozy dojrzałości procesowej';
|
||||
$lines[] = 'Wynik: ' . ($result['segment_label'] ?? 'Diagnoza zakończona') . ' (' . (int)($result['percentage_score'] ?? 0) . '%)';
|
||||
$lines[] = (string)($result['segment_summary'] ?? '');
|
||||
$lines[] = '';
|
||||
$lines[] = 'Priorytetowe obszary:';
|
||||
foreach (($result['weakest_areas'] ?? []) as $area) {
|
||||
$lines[] = '- ' . $area['section_name'] . ': ' . (int)$area['percentage'] . '%';
|
||||
}
|
||||
$lines[] = '';
|
||||
$lines[] = 'Rekomendowane działania:';
|
||||
foreach (($result['recommendations'] ?? []) as $item) {
|
||||
$lines[] = '- ' . $item;
|
||||
}
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
function diagnostic_send_report_email(array $attempt): array
|
||||
{
|
||||
if (empty($attempt['email']) || empty($attempt['result'])) {
|
||||
return ['success' => false, 'error' => 'Brak adresu e-mail lub wyniku próby'];
|
||||
}
|
||||
|
||||
$subject = 'Raport z diagnozy dojrzałości procesowej';
|
||||
$html = diagnostic_build_report_html($attempt);
|
||||
$text = diagnostic_build_report_text($attempt);
|
||||
return MailService::sendMail($attempt['email'], $subject, $html, $text);
|
||||
}
|
||||
|
||||
function diagnostic_flash_set(string $type, string $message): void
|
||||
{
|
||||
$_SESSION['diagnostic_flash'] = ['type' => $type, 'message' => $message];
|
||||
}
|
||||
|
||||
function diagnostic_flash_get(): ?array
|
||||
{
|
||||
if (empty($_SESSION['diagnostic_flash'])) {
|
||||
return null;
|
||||
}
|
||||
$flash = $_SESSION['diagnostic_flash'];
|
||||
unset($_SESSION['diagnostic_flash']);
|
||||
return is_array($flash) ? $flash : null;
|
||||
}
|
||||
|
||||
function diagnostic_admin_credentials(): array
|
||||
{
|
||||
return [
|
||||
'username' => getenv('DIAGNOSTIC_ADMIN_USER') ?: 'admin',
|
||||
'password' => getenv('DIAGNOSTIC_ADMIN_PASS') ?: 'admin123',
|
||||
'using_default' => !getenv('DIAGNOSTIC_ADMIN_USER') && !getenv('DIAGNOSTIC_ADMIN_PASS'),
|
||||
];
|
||||
}
|
||||
|
||||
function diagnostic_admin_is_authenticated(): bool
|
||||
{
|
||||
return !empty($_SESSION['diagnostic_admin_authenticated']);
|
||||
}
|
||||
|
||||
function diagnostic_admin_login(string $username, string $password): bool
|
||||
{
|
||||
$credentials = diagnostic_admin_credentials();
|
||||
$valid = hash_equals($credentials['username'], $username) && hash_equals($credentials['password'], $password);
|
||||
if ($valid) {
|
||||
$_SESSION['diagnostic_admin_authenticated'] = true;
|
||||
}
|
||||
return $valid;
|
||||
}
|
||||
|
||||
function diagnostic_admin_logout(): void
|
||||
{
|
||||
unset($_SESSION['diagnostic_admin_authenticated']);
|
||||
}
|
||||
|
||||
function diagnostic_admin_attempts(): array
|
||||
{
|
||||
$stmt = db()->query('SELECT * FROM diagnostic_attempts ORDER BY started_at DESC');
|
||||
$rows = $stmt->fetchAll();
|
||||
foreach ($rows as &$row) {
|
||||
$row['answers'] = diagnostic_decode_json_map($row['answers_json'] ?? '');
|
||||
$row['result'] = diagnostic_decode_json_map($row['result_json'] ?? '');
|
||||
}
|
||||
unset($row);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function diagnostic_admin_stats(): array
|
||||
{
|
||||
$attempts = diagnostic_admin_attempts();
|
||||
$total = count($attempts);
|
||||
$completed = 0;
|
||||
$averageScore = 0;
|
||||
$totalScore = 0;
|
||||
foreach ($attempts as $attempt) {
|
||||
if (($attempt['status'] ?? '') === 'completed') {
|
||||
$completed++;
|
||||
$totalScore += (int)($attempt['result']['percentage_score'] ?? 0);
|
||||
}
|
||||
}
|
||||
$completionRate = $total > 0 ? (int)round(($completed / $total) * 100) : 0;
|
||||
$averageScore = $completed > 0 ? (int)round($totalScore / $completed) : 0;
|
||||
|
||||
return [
|
||||
'total_attempts' => $total,
|
||||
'completed_attempts' => $completed,
|
||||
'completion_rate' => $completionRate,
|
||||
'average_score' => $averageScore,
|
||||
];
|
||||
}
|
||||
|
||||
function diagnostic_admin_export_csv(): string
|
||||
{
|
||||
$rows = diagnostic_admin_attempts();
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fputcsv($stream, ['ID próby', 'E-mail', 'Telefon', 'Data prośby o kontakt', 'Status', 'Rozpoczęto', 'Zakończono', 'Wynik %', 'Segment', 'Zgoda marketingowa', 'Raport e-mail']);
|
||||
foreach ($rows as $row) {
|
||||
fputcsv($stream, [
|
||||
$row['id'],
|
||||
$row['email'],
|
||||
$row['contact_phone'] ?? '',
|
||||
$row['consultation_requested_at'] ?? '',
|
||||
$row['status'],
|
||||
$row['started_at'],
|
||||
$row['completed_at'],
|
||||
$row['result']['percentage_score'] ?? '',
|
||||
$row['result']['segment_label'] ?? '',
|
||||
((int)$row['marketing_consent']) === 1 ? 'Tak' : 'Nie',
|
||||
$row['email_report_status'],
|
||||
]);
|
||||
}
|
||||
rewind($stream);
|
||||
$csv = stream_get_contents($stream) ?: '';
|
||||
fclose($stream);
|
||||
return $csv;
|
||||
}
|
||||
@ -1,403 +1,435 @@
|
||||
:root {
|
||||
--bg: #f5f6f8;
|
||||
--surface: #ffffff;
|
||||
--surface-muted: #f8fafc;
|
||||
--border: #d9dde3;
|
||||
--border-strong: #c3c9d2;
|
||||
--text: #101828;
|
||||
--muted: #667085;
|
||||
--accent: #1d2939;
|
||||
--accent-soft: #eef2f6;
|
||||
--success: #0f766e;
|
||||
--warning: #b45309;
|
||||
--danger: #b42318;
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.04);
|
||||
--shadow-md: 0 12px 24px rgba(16, 24, 40, 0.06);
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
|
||||
background-size: 400% 400%;
|
||||
animation: gradient 15s ease infinite;
|
||||
color: #212529;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
.app-shell {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.site-header,
|
||||
footer {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.navbar-brand,
|
||||
.section-title,
|
||||
.page-title,
|
||||
.display-title,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.display-title {
|
||||
font-size: clamp(2.4rem, 4vw, 4.2rem);
|
||||
line-height: 1.02;
|
||||
font-weight: 700;
|
||||
max-width: 11ch;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: clamp(2rem, 3vw, 3rem);
|
||||
line-height: 1.05;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: clamp(1.7rem, 2vw, 2.3rem);
|
||||
line-height: 1.1;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.surface-card,
|
||||
.compact-card,
|
||||
.metric-card,
|
||||
.metric-inline,
|
||||
.section-score-card,
|
||||
.compact-panel,
|
||||
.empty-state,
|
||||
.score-chip {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.surface-card {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.compact-card,
|
||||
.metric-card,
|
||||
.metric-inline,
|
||||
.section-score-card,
|
||||
.compact-panel,
|
||||
.empty-state {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.hero-section,
|
||||
.border-top,
|
||||
.border-bottom {
|
||||
border-color: var(--border) !important;
|
||||
}
|
||||
|
||||
.metric-card,
|
||||
.metric-inline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.metric-card strong,
|
||||
.metric-inline strong {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.metric-card span,
|
||||
.metric-inline span,
|
||||
.compact-card span {
|
||||
color: var(--muted);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.pill-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 999px;
|
||||
padding: 0.25rem 0.65rem;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
.pill-badge.subdued {
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 20px;
|
||||
.feature-list,
|
||||
.rank-list,
|
||||
.answer-review-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.feature-list li,
|
||||
.rank-list li,
|
||||
.answer-review-list li {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 0.9rem 0;
|
||||
}
|
||||
|
||||
.feature-list li:first-child,
|
||||
.rank-list li:first-child,
|
||||
.answer-review-list li:first-child {
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.rank-list li,
|
||||
.answer-review-list li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 85vh;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
overflow: hidden;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
.rank-list strong,
|
||||
.answer-review-list em {
|
||||
color: var(--accent);
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
.compact-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
gap: 0.4rem;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
.score-chip {
|
||||
padding: 1rem 1.1rem;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 85%;
|
||||
padding: 0.85rem 1.1rem;
|
||||
border-radius: 16px;
|
||||
line-height: 1.5;
|
||||
font-size: 0.95rem;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
||||
animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px) scale(0.95); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
.message.visitor {
|
||||
align-self: flex-end;
|
||||
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
|
||||
color: #fff;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.message.bot {
|
||||
align-self: flex-start;
|
||||
background: #ffffff;
|
||||
color: #212529;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
padding: 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.chat-input-area form {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.chat-input-area input {
|
||||
flex: 1;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1rem;
|
||||
outline: none;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-input-area input:focus {
|
||||
border-color: #23a6d5;
|
||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
|
||||
}
|
||||
|
||||
.chat-input-area button {
|
||||
background: #212529;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-input-area button:hover {
|
||||
background: #000;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Background Animations */
|
||||
.bg-animations {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blob {
|
||||
position: absolute;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
background: rgba(238, 119, 82, 0.4);
|
||||
}
|
||||
|
||||
.blob-2 {
|
||||
bottom: -10%;
|
||||
right: -10%;
|
||||
background: rgba(35, 166, 213, 0.4);
|
||||
animation-delay: -7s;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.blob-3 {
|
||||
top: 40%;
|
||||
left: 30%;
|
||||
background: rgba(231, 60, 126, 0.3);
|
||||
animation-delay: -14s;
|
||||
width: 450px;
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
|
||||
33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
|
||||
66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
|
||||
100% { transform: translate(0, 0) rotate(360deg) scale(1); }
|
||||
}
|
||||
|
||||
.header-link {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.header-link:hover {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Admin Styles */
|
||||
.admin-container {
|
||||
max-width: 900px;
|
||||
margin: 3rem auto;
|
||||
padding: 2.5rem;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.admin-container h1 {
|
||||
margin-top: 0;
|
||||
color: #212529;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0 8px;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 1rem;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.table td {
|
||||
background: #fff;
|
||||
padding: 1rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.table tr td:first-child { border-radius: 12px 0 0 12px; }
|
||||
.table tr td:last-child { border-radius: 0 12px 12px 0; }
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
.score-chip span {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.score-chip strong {
|
||||
font-size: 2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-check-input,
|
||||
.answer-card,
|
||||
.accordion-item,
|
||||
.accordion-button,
|
||||
.btn,
|
||||
.progress {
|
||||
border-radius: var(--radius-sm) !important;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
padding: 0.85rem 0.95rem;
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #23a6d5;
|
||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
|
||||
.form-control:focus,
|
||||
.form-check-input:focus,
|
||||
.btn:focus,
|
||||
.answer-card:focus-within {
|
||||
border-color: #94a3b8;
|
||||
box-shadow: 0 0 0 0.2rem rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.admin-card {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
padding: 2rem;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
margin-bottom: 2.5rem;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.admin-card h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background: #212529;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
background: #0088cc;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
.btn {
|
||||
padding: 0.8rem 1.1rem;
|
||||
font-weight: 600;
|
||||
width: 100%;
|
||||
transition: all 0.3s ease;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.webhook-url {
|
||||
font-size: 0.85em;
|
||||
color: #555;
|
||||
margin-top: 0.5rem;
|
||||
.btn-dark {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.history-table-container {
|
||||
overflow-x: auto;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
.btn-dark:hover,
|
||||
.btn-dark:focus {
|
||||
background: #111827;
|
||||
border-color: #111827;
|
||||
}
|
||||
|
||||
.answer-grid {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.answer-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 68px;
|
||||
padding: 1rem 1rem 1rem 3rem;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: var(--surface);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.18s ease, background-color 0.18s ease, transform 0.18s ease;
|
||||
}
|
||||
|
||||
.answer-card:hover {
|
||||
border-color: #98a2b3;
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.answer-card.is-selected {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
.answer-input {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.answer-label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.question-title {
|
||||
font-size: clamp(1.6rem, 2vw, 2.3rem);
|
||||
line-height: 1.2;
|
||||
max-width: 24ch;
|
||||
}
|
||||
|
||||
.quiz-progress {
|
||||
height: 0.55rem;
|
||||
background: #e8ecf1;
|
||||
}
|
||||
|
||||
.quiz-progress .progress-bar {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.quiz-progress.slim {
|
||||
height: 0.45rem;
|
||||
}
|
||||
|
||||
.section-score-card {
|
||||
padding: 1rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.history-table {
|
||||
.admin-workspace {
|
||||
max-width: 1680px;
|
||||
padding-left: clamp(1rem, 2vw, 2rem);
|
||||
padding-right: clamp(1rem, 2vw, 2rem);
|
||||
}
|
||||
|
||||
.admin-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.admin-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.history-table-time {
|
||||
width: 15%;
|
||||
.admin-table thead th,
|
||||
.admin-table tbody td {
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.admin-table th:nth-child(1),
|
||||
.admin-table td:nth-child(1) {
|
||||
width: 6%;
|
||||
}
|
||||
|
||||
.admin-table th:nth-child(2),
|
||||
.admin-table td:nth-child(2) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.admin-table th:nth-child(3),
|
||||
.admin-table td:nth-child(3) {
|
||||
width: 12%;
|
||||
}
|
||||
|
||||
.admin-table th:nth-child(4),
|
||||
.admin-table td:nth-child(4) {
|
||||
width: 13%;
|
||||
}
|
||||
|
||||
.admin-table th:nth-child(5),
|
||||
.admin-table td:nth-child(5) {
|
||||
width: 8%;
|
||||
}
|
||||
|
||||
.admin-table th:nth-child(6),
|
||||
.admin-table td:nth-child(6) {
|
||||
width: 18%;
|
||||
}
|
||||
|
||||
.admin-table th:nth-child(7),
|
||||
.admin-table td:nth-child(7) {
|
||||
width: 11%;
|
||||
}
|
||||
|
||||
.admin-table th:nth-child(8),
|
||||
.admin-table td:nth-child(8) {
|
||||
width: 12%;
|
||||
white-space: nowrap;
|
||||
font-size: 0.85em;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.history-table-user {
|
||||
width: 35%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
.admin-table thead th {
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
border-bottom-width: 1px;
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.history-table-ai {
|
||||
width: 50%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
.admin-table tbody td {
|
||||
border-color: var(--border);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.no-messages {
|
||||
.accordion-item,
|
||||
.accordion-button {
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.accordion-button:not(.collapsed) {
|
||||
background: var(--accent-soft);
|
||||
color: var(--text);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: var(--radius-sm);
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.py-lg-6 {
|
||||
padding-top: 5rem !important;
|
||||
padding-bottom: 5rem !important;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.admin-table-wrap {
|
||||
overflow-x: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.display-title {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.question-title {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,39 +1,33 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
const answerCards = document.querySelectorAll('.answer-card');
|
||||
|
||||
const appendMessage = (text, sender) => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.classList.add('message', sender);
|
||||
msgDiv.textContent = text;
|
||||
chatMessages.appendChild(msgDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
};
|
||||
answerCards.forEach((card) => {
|
||||
const input = card.querySelector('.answer-input');
|
||||
if (!input) return;
|
||||
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
appendMessage(message, 'visitor');
|
||||
chatInput.value = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('api/chat.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message })
|
||||
const updateSelection = () => {
|
||||
const group = document.querySelectorAll(`input[name="${input.name}"]`);
|
||||
group.forEach((radio) => {
|
||||
const wrapper = radio.closest('.answer-card');
|
||||
if (wrapper) wrapper.classList.toggle('is-selected', radio.checked);
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// Artificial delay for realism
|
||||
setTimeout(() => {
|
||||
appendMessage(data.reply, 'bot');
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
||||
};
|
||||
|
||||
card.addEventListener('click', () => {
|
||||
input.checked = true;
|
||||
updateSelection();
|
||||
});
|
||||
|
||||
input.addEventListener('change', updateSelection);
|
||||
});
|
||||
|
||||
const alerts = document.querySelectorAll('.alert');
|
||||
alerts.forEach((alert) => {
|
||||
if (alert.classList.contains('alert-success') || alert.classList.contains('alert-info')) {
|
||||
window.setTimeout(() => {
|
||||
alert.classList.add('fade');
|
||||
alert.classList.remove('show');
|
||||
}, 4500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
447
diagnostic.php
Normal file
447
diagnostic.php
Normal file
@ -0,0 +1,447 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/app/bootstrap.php';
|
||||
|
||||
$definition = diagnostic_quiz_definition();
|
||||
$questions = diagnostic_flat_questions();
|
||||
$totalQuestions = count($questions);
|
||||
$totalSections = count($definition['sections'] ?? []);
|
||||
$flash = diagnostic_flash_get();
|
||||
|
||||
if (isset($_GET['reset']) && $_GET['reset'] === '1') {
|
||||
diagnostic_clear_attempt_session();
|
||||
diagnostic_flash_set('info', 'Poprzednia, niedokończona diagnoza została wyczyszczona.');
|
||||
header('Location: /diagnostic.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$attempt = diagnostic_current_attempt();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'start') {
|
||||
$email = trim((string)($_POST['email'] ?? ''));
|
||||
$marketingConsent = isset($_POST['marketing_consent']) && $_POST['marketing_consent'] === '1';
|
||||
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
diagnostic_flash_set('danger', 'Podaj poprawny adres e-mail, na który mamy wysłać raport.');
|
||||
header('Location: /diagnostic.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
diagnostic_create_attempt($email, $marketingConsent);
|
||||
header('Location: /diagnostic.php?step=1');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'save-answer') {
|
||||
$attempt = diagnostic_current_attempt();
|
||||
if (!$attempt) {
|
||||
diagnostic_flash_set('warning', 'Sesja wygasła. Rozpocznij diagnozę ponownie.');
|
||||
header('Location: /diagnostic.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$step = max(1, (int)($_POST['step'] ?? 1));
|
||||
$nav = $_POST['nav'] ?? 'next';
|
||||
$question = $questions[$step - 1] ?? null;
|
||||
|
||||
if (!$question) {
|
||||
header('Location: /diagnostic.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($nav === 'back') {
|
||||
if (isset($_POST['answer']) && $_POST['answer'] !== '') {
|
||||
diagnostic_save_answer((int)$attempt['id'], $question['id'], (int)$_POST['answer'], $step);
|
||||
}
|
||||
$targetStep = max(1, $step - 1);
|
||||
diagnostic_update_step((int)$attempt['id'], $targetStep);
|
||||
header('Location: /diagnostic.php?step=' . $targetStep);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($_POST['answer']) || $_POST['answer'] === '' || !in_array((int)$_POST['answer'], [0, 1, 2, 3], true)) {
|
||||
diagnostic_flash_set('danger', 'Wybierz jedną odpowiedź, aby przejść dalej.');
|
||||
header('Location: /diagnostic.php?step=' . $step);
|
||||
exit;
|
||||
}
|
||||
|
||||
diagnostic_save_answer((int)$attempt['id'], $question['id'], (int)$_POST['answer'], $step);
|
||||
|
||||
if ($step >= $totalQuestions) {
|
||||
diagnostic_finalize_attempt((int)$attempt['id']);
|
||||
header('Location: /diagnostic.php?view=result');
|
||||
exit;
|
||||
}
|
||||
|
||||
$targetStep = $step + 1;
|
||||
diagnostic_update_step((int)$attempt['id'], $targetStep);
|
||||
header('Location: /diagnostic.php?step=' . $targetStep);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'request-consultation') {
|
||||
$attempt = diagnostic_current_attempt();
|
||||
if (!$attempt || ($attempt['status'] ?? '') !== 'completed') {
|
||||
diagnostic_flash_set('warning', 'Najpierw ukończ diagnozę, aby zostawić numer telefonu do kontaktu.');
|
||||
header('Location: /diagnostic.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$phone = diagnostic_normalize_phone((string)($_POST['phone'] ?? ''));
|
||||
if (!diagnostic_phone_is_valid($phone)) {
|
||||
diagnostic_flash_set('danger', 'Podaj poprawny numer telefonu, abyśmy mogli wrócić z propozycją dalszej konsultacji.');
|
||||
header('Location: /diagnostic.php?view=result&modal=consultation');
|
||||
exit;
|
||||
}
|
||||
|
||||
diagnostic_save_contact_phone((int)$attempt['id'], $phone);
|
||||
diagnostic_flash_set('success', 'Dziękujemy. Numer telefonu został zapisany — skontaktujemy się w sprawie dalszej konsultacji.');
|
||||
header('Location: /diagnostic.php?view=result');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$attempt = diagnostic_current_attempt();
|
||||
if ($attempt && ($attempt['status'] ?? '') === 'completed' && (!isset($_GET['step']) && ($_GET['view'] ?? '') !== 'result')) {
|
||||
header('Location: /diagnostic.php?view=result');
|
||||
exit;
|
||||
}
|
||||
if ($attempt && ($attempt['status'] ?? '') === 'in_progress' && !isset($_GET['step']) && ($_GET['view'] ?? '') !== 'landing') {
|
||||
header('Location: /diagnostic.php?step=' . (int)$attempt['current_step']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$requestedView = $_GET['view'] ?? '';
|
||||
if ($requestedView === 'result') {
|
||||
$view = 'result';
|
||||
} elseif (isset($_GET['step']) || ($attempt && ($attempt['status'] ?? '') === 'in_progress')) {
|
||||
$view = 'quiz';
|
||||
} else {
|
||||
$view = 'landing';
|
||||
}
|
||||
|
||||
$currentStep = max(1, min($totalQuestions, (int)($_GET['step'] ?? ($attempt['current_step'] ?? 1))));
|
||||
$currentQuestion = $questions[$currentStep - 1] ?? null;
|
||||
$currentAnswer = $attempt['answers'][$currentQuestion['id'] ?? ''] ?? null;
|
||||
$progressPercent = (int)round(($currentStep / max($totalQuestions, 1)) * 100);
|
||||
$openConsultationModal = isset($_GET['modal']) && $_GET['modal'] === 'consultation';
|
||||
$meta = diagnostic_meta('Diagnoza dojrzałości procesowej', 'Profesjonalna diagnoza dojrzałości procesowej z natychmiastowym podsumowaniem i raportem wysyłanym na e-mail.');
|
||||
?>
|
||||
<!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; ?>
|
||||
<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">
|
||||
<a class="navbar-brand fw-semibold text-dark" href="/">Przegląd procesów</a>
|
||||
<div class="d-flex align-items-center gap-2 ms-auto">
|
||||
<a class="btn btn-sm btn-dark" href="/diagnostic.php<?= $attempt ? '?reset=1' : '' ?>"><?= $attempt ? 'Rozpocznij od nowa' : 'Rozpocznij diagnozę' ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="py-4 py-lg-5">
|
||||
<div class="container">
|
||||
<?php if ($flash): ?>
|
||||
<div class="alert alert-<?= htmlspecialchars($flash['type']) ?> mb-4" role="alert">
|
||||
<?= htmlspecialchars($flash['message']) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($view === 'landing'): ?>
|
||||
<section class="surface-card p-4 p-lg-5 mb-4">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-7">
|
||||
<div class="eyebrow mb-3">Narzędzie diagnostyczne</div>
|
||||
<h1 class="page-title mb-3"><?= htmlspecialchars($definition['quiz']['title']) ?></h1>
|
||||
<p class="lead text-secondary mb-4"><?= htmlspecialchars($definition['quiz']['subtitle']) ?></p>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-sm-6"><div class="metric-inline"><span>Czas wypełnienia</span><strong><?= htmlspecialchars($definition['quiz']['estimated_time']) ?></strong></div></div>
|
||||
<div class="col-sm-6"><div class="metric-inline"><span>Format</span><strong><?= $totalQuestions ?> pytań w <?= $totalSections ?> obszarach</strong></div></div>
|
||||
<div class="col-sm-6"><div class="metric-inline"><span>Rezultat</span><strong>Wynik na ekranie + raport e-mail</strong></div></div>
|
||||
<div class="col-sm-6"><div class="metric-inline"><span>Odbiorca</span><strong>Właściciele i liderzy operacyjni</strong></div></div>
|
||||
</div>
|
||||
<ul class="feature-list mb-0">
|
||||
<li>Jedno pytanie na ekranie, bez zbędnego rozproszenia.</li>
|
||||
<li>Ocena w 7 kluczowych obszarach operacyjnych firmy.</li>
|
||||
<li>Natychmiastowe podsumowanie z najmocniejszymi i najsłabszymi obszarami.</li>
|
||||
<li>Raport wysyłany na adres e-mail podany przed rozpoczęciem diagnozy.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="surface-card p-4 bg-white border h-100">
|
||||
<div class="eyebrow mb-3">Krok 1 z 2</div>
|
||||
<h2 class="h4 mb-3">Podaj adres e-mail</h2>
|
||||
<p class="text-secondary mb-4">Podsumowanie zobaczysz od razu na ekranie, a rozszerzony raport wyślemy na wskazany adres e-mail. Konto użytkownika nie jest wymagane.</p>
|
||||
<form method="post" novalidate>
|
||||
<input type="hidden" name="action" value="start">
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Adres e-mail</label>
|
||||
<input type="email" class="form-control form-control-lg" id="email" name="email" placeholder="np. zarzad@twojafirma.pl" required>
|
||||
</div>
|
||||
<p class="small text-secondary mb-3">Podanie adresu e-mail jest wymagane wyłącznie do dostarczenia raportu z diagnozy. Zgoda marketingowa jest opcjonalna i niezależna od wysyłki raportu.</p>
|
||||
<div class="form-check mb-4">
|
||||
<input class="form-check-input" type="checkbox" value="1" id="marketingConsent" name="marketing_consent">
|
||||
<label class="form-check-label text-secondary" for="marketingConsent">
|
||||
Wyrażam zgodę na otrzymywanie dodatkowych materiałów dotyczących usprawniania procesów i zarządzania operacyjnego.
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-dark btn-lg w-100">Rozpocznij diagnozę</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="surface-card p-4 p-lg-5 h-100">
|
||||
<div class="section-intro mb-3">
|
||||
<div class="eyebrow">Zakres diagnozy</div>
|
||||
<h2 class="section-title">Co ocenia to narzędzie</h2>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($definition['sections'] as $section): ?>
|
||||
<div class="col-md-6">
|
||||
<div class="compact-card">
|
||||
<strong><?= htmlspecialchars($section['name']) ?></strong>
|
||||
<span><?= count($section['questions']) ?> pytań dotyczących dojrzałości operacyjnej w tym obszarze.</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="surface-card p-4 p-lg-5 h-100">
|
||||
<div class="section-intro mb-3">
|
||||
<div class="eyebrow">Po co to robić</div>
|
||||
<h2 class="section-title">Dlaczego warto wykonać diagnozę</h2>
|
||||
</div>
|
||||
<ul class="feature-list mb-0">
|
||||
<li>Zobaczysz, które obszary wymagają największej interwencji organizacyjnej.</li>
|
||||
<li>Łatwiej ustalisz priorytety usprawnień zamiast działać intuicyjnie.</li>
|
||||
<li>Otrzymasz punkt wyjścia do rozmowy o wdrożeniu zmian lub konsultacji.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php elseif ($view === 'quiz' && $attempt && ($attempt['status'] ?? '') === 'in_progress' && $currentQuestion): ?>
|
||||
<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 mb-4">
|
||||
<div>
|
||||
<div class="eyebrow mb-2"><?= htmlspecialchars($currentQuestion['section_name']) ?></div>
|
||||
<h1 class="page-title mb-2"><?= htmlspecialchars($currentQuestion['text']) ?></h1>
|
||||
<p class="text-secondary mb-0">Pytanie <?= $currentStep ?> z <?= $totalQuestions ?>. Odpowiedz zgodnie z aktualnym stanem organizacji, a nie stanem docelowym.</p>
|
||||
</div>
|
||||
<div class="score-chip text-end">
|
||||
<span>Postęp</span>
|
||||
<strong><?= $progressPercent ?>%</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress mb-4" style="height: 10px;">
|
||||
<div class="progress-bar bg-dark" role="progressbar" style="width: <?= $progressPercent ?>%;" aria-valuenow="<?= $progressPercent ?>" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
|
||||
<form method="post" novalidate>
|
||||
<input type="hidden" name="action" value="save-answer">
|
||||
<input type="hidden" name="step" value="<?= $currentStep ?>">
|
||||
<div class="row g-3 mb-4">
|
||||
<?php foreach ($definition['answer_scale'] as $value => $label): ?>
|
||||
<div class="col-md-6">
|
||||
<label class="answer-card h-100 <?= ((string)$currentAnswer === (string)$value) ? 'is-selected' : '' ?>">
|
||||
<input class="answer-input" type="radio" name="answer" value="<?= $value ?>" <?= ((string)$currentAnswer === (string)$value) ? 'checked' : '' ?>>
|
||||
<strong><?= htmlspecialchars($label) ?></strong>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-sm-row gap-3 justify-content-between">
|
||||
<button type="submit" name="nav" value="back" class="btn btn-outline-dark btn-lg<?= $currentStep === 1 ? ' disabled' : '' ?>" <?= $currentStep === 1 ? 'disabled' : '' ?>>Wstecz</button>
|
||||
<button type="submit" name="nav" value="next" class="btn btn-dark btn-lg ms-sm-auto"><?= $currentStep === $totalQuestions ? 'Zakończ diagnozę' : 'Następne pytanie' ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<?php elseif ($view === 'result' && $attempt && ($attempt['status'] ?? '') === 'completed'): ?>
|
||||
<?php $result = $attempt['result'] ?? []; ?>
|
||||
<section class="surface-card p-4 p-lg-5 mb-4">
|
||||
<div class="d-flex flex-column flex-lg-row justify-content-between gap-4 align-items-lg-start mb-4">
|
||||
<div>
|
||||
<div class="eyebrow mb-3">Podsumowanie diagnozy</div>
|
||||
<h1 class="page-title mb-2"><?= htmlspecialchars($result['segment_label'] ?? 'Diagnoza zakończona') ?></h1>
|
||||
<p class="lead text-secondary mb-0"><?= htmlspecialchars($result['segment_summary'] ?? '') ?></p>
|
||||
</div>
|
||||
<div class="score-chip text-end">
|
||||
<span>Łączny wynik</span>
|
||||
<strong><?= (int)($result['percentage_score'] ?? 0) ?>%</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (($attempt['email_report_status'] ?? '') !== 'sent'): ?>
|
||||
<div class="alert alert-warning mb-4" role="alert">Podsumowanie na ekranie jest gotowe. Wysyłka raportu e-mail nie została jeszcze potwierdzona, więc przed użyciem produkcyjnym sprawdź konfigurację SMTP.</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-success mb-4" role="alert">Szczegółowy raport został wysłany na adres <?= htmlspecialchars((string)$attempt['email']) ?>.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<?php foreach (($result['section_scores'] ?? []) as $section): ?>
|
||||
<div class="col-md-6 col-xl-4">
|
||||
<div class="section-score-card h-100">
|
||||
<span class="small text-secondary d-block mb-2"><?= htmlspecialchars($section['section_name']) ?></span>
|
||||
<strong class="d-block mb-1"><?= (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>
|
||||
</section>
|
||||
|
||||
<section class="row g-4">
|
||||
<div class="col-lg-4">
|
||||
<div class="surface-card p-4 h-100">
|
||||
<div class="eyebrow mb-3">Najmocniejsze obszary</div>
|
||||
<ol class="rank-list mb-0">
|
||||
<?php foreach (($result['strongest_areas'] ?? []) as $area): ?>
|
||||
<li>
|
||||
<strong><?= htmlspecialchars($area['section_name']) ?></strong>
|
||||
<span><?= (int)$area['percentage'] ?>% dojrzałości w tym obszarze.</span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="surface-card p-4 h-100">
|
||||
<div class="eyebrow mb-3">Priorytety do poprawy</div>
|
||||
<ol class="rank-list mb-0">
|
||||
<?php foreach (($result['weakest_areas'] ?? []) as $area): ?>
|
||||
<li>
|
||||
<strong><?= htmlspecialchars($area['section_name']) ?></strong>
|
||||
<span><?= (int)$area['percentage'] ?>% — tutaj warto rozpocząć porządkowanie i standaryzację.</span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="surface-card p-4 h-100">
|
||||
<div class="eyebrow mb-3">Rekomendowane kolejne kroki</div>
|
||||
<ul class="feature-list mb-0">
|
||||
<?php foreach (($result['recommendations'] ?? []) as $recommendation): ?>
|
||||
<li><?= htmlspecialchars($recommendation) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="surface-card p-4 p-lg-5 mt-4">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="eyebrow mb-3">Kolejny krok</div>
|
||||
<h2 class="section-title mb-3">Wykorzystaj wynik jako podstawę do rozmowy o usprawnieniach</h2>
|
||||
<p class="text-secondary mb-3">Największą wartość przynosi przełożenie diagnozy na plan działań: uporządkowanie odpowiedzialności, standaryzację procesów i wdrożenie prostych mechanizmów kontroli. To naturalny punkt wyjścia do konsultacji operacyjnej lub warsztatu usprawniającego.</p>
|
||||
<?php if (!empty($attempt['contact_phone'])): ?>
|
||||
<div class="alert alert-success mb-0" role="alert">
|
||||
Numer do kontaktu został już zapisany: <strong><?= htmlspecialchars((string)$attempt['contact_phone']) ?></strong>.
|
||||
<?php if (!empty($attempt['consultation_requested_at'])): ?>
|
||||
<span class="d-block small mt-1">Prośba o kontakt została wysłana <?= htmlspecialchars((string)$attempt['consultation_requested_at']) ?>.</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="col-lg-4 d-flex flex-column gap-3">
|
||||
<button type="button" class="btn btn-dark btn-lg" data-bs-toggle="modal" data-bs-target="#consultationModal"><?= !empty($attempt['contact_phone']) ? 'Zaktualizuj numer kontaktowy' : 'Umów kolejny krok' ?></button>
|
||||
<a class="btn btn-outline-dark btn-lg" href="/diagnostic.php?reset=1">Wykonaj diagnozę ponownie</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="modal fade" id="consultationModal" tabindex="-1" aria-labelledby="consultationModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<div class="eyebrow mb-2">Dalsza konsultacja</div>
|
||||
<h2 class="modal-title h4 mb-0" id="consultationModalLabel">Podaj numer telefonu</h2>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||
</div>
|
||||
<form method="post" novalidate>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" value="request-consultation">
|
||||
<p class="text-secondary mb-4">Oddzwonimy, aby omówić wynik diagnozy i zaproponować dalszy zakres konsultacji. Numer telefonu jest wymagany, żebyśmy mogli się z Tobą skontaktować.</p>
|
||||
<div class="mb-3">
|
||||
<label for="contactPhone" class="form-label">Numer telefonu</label>
|
||||
<input type="tel" class="form-control form-control-lg" id="contactPhone" name="phone" inputmode="tel" autocomplete="tel" placeholder="np. +48 500 600 700" value="<?= htmlspecialchars((string)($attempt['contact_phone'] ?? '')) ?>" required>
|
||||
</div>
|
||||
<p class="small text-secondary mb-0">Akceptowane są cyfry oraz znaki <code>+</code>, spacje, myślniki i nawiasy.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-dark" data-bs-dismiss="modal">Anuluj</button>
|
||||
<button type="submit" class="btn btn-dark">Wyślij numer do kontaktu</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<section class="empty-state text-center py-5 px-4">
|
||||
<div class="eyebrow mb-3">Brak aktywnej diagnozy</div>
|
||||
<h1 class="page-title mb-3">Nie znaleziono aktywnego wyniku</h1>
|
||||
<p class="text-secondary mb-4">Rozpocznij nowe badanie, aby wygenerować podsumowanie dojrzałości procesowej i raport wysyłany e-mailem.</p>
|
||||
<a class="btn btn-dark" href="/diagnostic.php">Rozpocznij diagnozę</a>
|
||||
</section>
|
||||
<?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">Profesjonalna diagnoza dojrzałości procesowej dla właścicieli firm i liderów operacyjnych.</span>
|
||||
<div class="d-flex gap-3 small">
|
||||
<a class="text-decoration-none text-secondary" href="/">Strona główna</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" defer></script>
|
||||
<?php if ($openConsultationModal): ?>
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
var consultationModal = document.getElementById('consultationModal');
|
||||
if (consultationModal && window.bootstrap && window.bootstrap.Modal) {
|
||||
window.bootstrap.Modal.getOrCreateInstance(consultationModal).show();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<script src="/assets/js/main.js?v=<?= urlencode((string)@filemtime(__DIR__ . '/assets/js/main.js')) ?>" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
16
healthz.php
Normal file
16
healthz.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
require_once __DIR__ . '/app/bootstrap.php';
|
||||
|
||||
$status = ['status' => 'ok', 'time' => gmdate('c')];
|
||||
try {
|
||||
db()->query('SELECT 1');
|
||||
$status['database'] = 'ok';
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
$status['status'] = 'error';
|
||||
$status['database'] = 'error';
|
||||
}
|
||||
|
||||
echo json_encode($status, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
321
index.php
321
index.php
@ -1,150 +1,193 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
require_once __DIR__ . '/app/bootstrap.php';
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$definition = diagnostic_quiz_definition();
|
||||
$totalQuestions = count(diagnostic_flat_questions());
|
||||
$totalSections = count($definition['sections']);
|
||||
$meta = diagnostic_meta('Diagnoza dojrzałości procesowej', 'Profesjonalne narzędzie do oceny uporządkowania procesów, komunikacji i kontroli operacyjnej w firmie.');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<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; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
<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>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
<body class="app-shell">
|
||||
<header class="site-header border-bottom sticky-top">
|
||||
<nav class="navbar navbar-expand-lg navbar-light py-3">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-semibold text-dark" href="/">Przegląd procesów</a>
|
||||
<div class="d-flex align-items-center gap-2 ms-auto">
|
||||
<a class="btn btn-sm btn-dark" href="/diagnostic.php">Rozpocznij diagnozę</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="hero-section py-5 py-lg-6">
|
||||
<div class="container py-lg-4">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-7">
|
||||
<span class="pill-badge mb-3">Narzędzie dla firm B2B i zespołów operacyjnych</span>
|
||||
<h1 class="display-title mb-4">Sprawdź, na ile procesy w Twojej firmie są gotowe do wzrostu.</h1>
|
||||
<p class="lead text-secondary mb-3">To narzędzie pomaga właścicielom firm, dyrektorom zarządzającym i liderom operacyjnym szybko ocenić dojrzałość operacyjną organizacji, wskazać obszary chaosu i ustalić priorytety zmian.</p>
|
||||
<div class="d-inline-flex align-items-center gap-2 px-3 py-2 mb-4 rounded-3 border bg-white shadow-sm">
|
||||
<span class="fw-semibold text-dark">Test jest w 100% darmowy</span>
|
||||
<span class="text-secondary small">Bez opłaty za uruchomienie i bez konieczności rozmowy handlowej.</span>
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-sm-row gap-3 mb-4">
|
||||
<a class="btn btn-dark btn-lg" href="/diagnostic.php">Rozpocznij diagnozę</a>
|
||||
<a class="btn btn-outline-dark btn-lg" href="#zakres">Zobacz zakres oceny</a>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-4"><div class="metric-inline"><span>Pytania</span><strong><?= $totalQuestions ?></strong></div></div>
|
||||
<div class="col-sm-4"><div class="metric-inline"><span>Obszary</span><strong><?= $totalSections ?></strong></div></div>
|
||||
<div class="col-sm-4"><div class="metric-inline"><span>Czas</span><strong><?= htmlspecialchars($definition['quiz']['estimated_time']) ?></strong></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<aside class="surface-card p-4 p-lg-5 h-100">
|
||||
<div class="eyebrow mb-3">Co otrzymasz</div>
|
||||
<div class="small fw-semibold text-success mb-2">Bezpłatna diagnoza online</div>
|
||||
<h2 class="h4 mb-3">Rzetelne podsumowanie dojrzałości procesowej</h2>
|
||||
<p class="text-secondary mb-4">Diagnoza została zaprojektowana jako profesjonalne narzędzie konsultacyjne — nie jako lekki quiz. Użytkownik otrzymuje natychmiastowy wynik na ekranie oraz rozszerzony raport na e-mail.</p>
|
||||
<ul class="feature-list mb-4">
|
||||
<li>Jedno pytanie na ekranie</li>
|
||||
<li>Ocena w 7 sekcjach biznesowych</li>
|
||||
<li>Wynik procentowy, segment dojrzałości i rekomendacje</li>
|
||||
<li>Panel administratora do przeglądu prób i eksportu danych</li>
|
||||
</ul>
|
||||
<a class="btn btn-dark w-100" href="/diagnostic.php">Uruchom narzędzie</a>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-5 border-top" id="zakres">
|
||||
<div class="container">
|
||||
<div class="section-intro mb-4">
|
||||
<div class="eyebrow">Zakres oceny</div>
|
||||
<h2 class="section-title">Siedem obszarów, które najczęściej decydują o jakości działania firmy</h2>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($definition['sections'] as $section): ?>
|
||||
<div class="col-md-6 col-xl-4">
|
||||
<article class="compact-card h-100">
|
||||
<strong><?= htmlspecialchars($section['name']) ?></strong>
|
||||
<span><?= count($section['questions']) ?> pytań pomagających ocenić poziom uporządkowania, powtarzalności i kontroli.</span>
|
||||
</article>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-5" id="jak-to-dziala">
|
||||
<div class="container">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-7">
|
||||
<div class="section-intro mb-4">
|
||||
<div class="eyebrow">Jak to działa</div>
|
||||
<h2 class="section-title">Prosty przebieg dla respondenta, uporządkowane dane dla administratora</h2>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6"><div class="compact-card"><strong>1. E-mail przed startem</strong><span>Adres jest wymagany do wysłania raportu i zapisania próby w bazie.</span></div></div>
|
||||
<div class="col-sm-6"><div class="compact-card"><strong>2. Wieloetapowa diagnoza</strong><span>Użytkownik przechodzi przez pytania krok po kroku z paskiem postępu.</span></div></div>
|
||||
<div class="col-sm-6"><div class="compact-card"><strong>3. Wynik natychmiast</strong><span>Na ekranie od razu pojawia się segment dojrzałości i ocena sekcji.</span></div></div>
|
||||
<div class="col-sm-6"><div class="compact-card"><strong>4. Raport e-mail</strong><span>Szczegółowe podsumowanie trafia na podany adres e-mail.</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="surface-card p-4 p-lg-5">
|
||||
<div class="eyebrow mb-3">Zastosowanie</div>
|
||||
<h2 class="h4 mb-3">Dla kogo powstało to rozwiązanie</h2>
|
||||
<p class="text-secondary mb-4">Narzędzie jest dopasowane do potrzeb małych i średnich firm, które chcą uporządkować operacje, ograniczyć chaos organizacyjny i przygotować się do dalszego skalowania.</p>
|
||||
<ul class="feature-list mb-0">
|
||||
<li>Właściciele firm</li>
|
||||
<li>Zarządy i managing directorzy</li>
|
||||
<li>Szefowie operacji i menedżerowie działów</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-5 border-top" id="wnioski">
|
||||
<div class="container">
|
||||
<div class="section-intro mb-4">
|
||||
<div class="eyebrow">Praktyczne wnioski</div>
|
||||
<h2 class="section-title">Jak interpretować wynik diagnozy</h2>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<article class="surface-card p-4 h-100">
|
||||
<p class="small text-secondary mb-2">Widoczność operacyjna</p>
|
||||
<h3 class="h5">Czy firma działa na podstawie danych, czy głównie intuicji i doraźnych działań?</h3>
|
||||
<p class="text-secondary mb-0">Wynik pokaże, czy menedżerowie i właściciel mają realny wgląd w status zadań, opóźnienia, jakość i ryzyka.</p>
|
||||
</article>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<article class="surface-card p-4 h-100">
|
||||
<p class="small text-secondary mb-2">Powtarzalność</p>
|
||||
<h3 class="h5">Na ile codzienna praca jest oparta na standardach, a nie na pamięci pojedynczych osób?</h3>
|
||||
<p class="text-secondary mb-0">To kluczowy wskaźnik zdolności organizacji do utrzymania jakości i skalowania bez chaosu.</p>
|
||||
</article>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<article class="surface-card p-4 h-100">
|
||||
<p class="small text-secondary mb-2">Rola właściciela</p>
|
||||
<h3 class="h5">Czy firma potrafi działać sprawnie bez ciągłej interwencji właściciela?</h3>
|
||||
<p class="text-secondary mb-0">Im wyższy poziom dojrzałości, tym większa możliwość przesunięcia uwagi właściciela z bieżących problemów na rozwój.</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-5 border-top" id="start">
|
||||
<div class="container">
|
||||
<div class="surface-card p-4 p-lg-5">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<div class="eyebrow mb-3">Gotowe do użycia</div>
|
||||
<h2 class="section-title mb-3">Uruchom diagnozę jako niezależną podstronę w ramach bloga firmowego</h2>
|
||||
<p class="text-secondary mb-0">Obecna wersja została zbudowana tak, aby mogła działać jako samodzielna podstrona, a w kolejnym etapie zostać osadzona lub podlinkowana z firmowego bloga bez przebudowy silnika oceny i panelu administratora.</p>
|
||||
</div>
|
||||
<div class="col-lg-4 d-flex flex-column gap-3">
|
||||
<a class="btn btn-dark btn-lg" href="/diagnostic.php">Otwórz narzędzie diagnostyczne</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</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">Diagnoza dojrzałości procesowej dla właścicieli firm i liderów operacyjnych.</span>
|
||||
<div class="d-flex gap-3 small">
|
||||
<a class="text-decoration-none text-secondary" href="/diagnostic.php">Diagnoza</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
</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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user