Wersja początkowa - działający mechanizm

This commit is contained in:
Flatlogic Bot 2026-04-11 19:06:55 +00:00
parent 2b94c80a4e
commit 944cf1608d
9 changed files with 2028 additions and 523 deletions

292
admin.php Normal file
View 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 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
View 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
View 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' => '810 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 35 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.',
],
],
],
];
}

View 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;
}

View File

@ -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;
}
}

View File

@ -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
View 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 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
View 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
View File

@ -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 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>