diff --git a/admin.php b/admin.php new file mode 100644 index 0000000..e98cf45 --- /dev/null +++ b/admin.php @@ -0,0 +1,292 @@ + + + + + + + <?= htmlspecialchars($meta['title']) ?> + + + + + + + + + + + + + + + + +
+
+ + + + + +
+
+
+
Strefa zabezpieczona
+

Logowanie administratora

+

Panel służy do przeglądu prób, analizy wyników oraz eksportu danych z diagnozy dojrzałości procesowej.

+
+ +
+ + +
+
+ + +
+ +
+ + + +
+
+
+ +
+
+
+
Panel administratora
+

Operacje i wyniki diagnozy

+

Przeglądaj próby respondentów, śledź skuteczność ukończenia i analizuj wyniki w poszczególnych obszarach.

+
+ Eksportuj CSV +
+
+ +
+
Wszystkie próby
+
Ukończone
+
%Wskaźnik ukończenia
+
%Średni wynik
+
+ +
+
+
+
+
+
Lista prób
+

Ostatnie odpowiedzi

+
+ rekordów +
+ +
+

Brak zapisanych prób

+

Po pierwszym wypełnieniu diagnozy przez użytkownika wyniki pojawią się tutaj automatycznie.

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDE-mailStatusTelefonWynikSegmentRaportSzczegóły
#Zobacz
+
+ +
+
+
+
+ +
+

Brak wybranej próby

+

Wybierz odpowiedź z listy, aby zobaczyć szczegóły, rozkład sekcji i pełny zestaw odpowiedzi.

+
+ + +
+
+
Szczegóły próby #
+

+

Rozpoczęto:

+
+
+ Wynik + +
+
+
+
+
Status
+
Raport e-mail
+
Zgoda marketingowa
+
Telefon kontaktowy
+
Prośba o kontakt
+
Zakończono
+
+
+ + +
+

Podsumowanie wyniku

+

+
+ +
+
+ + % +
+
+
+
+
+ +
+
+ + +
+

Odpowiedzi respondenta

+
    + $question): ?> + +
  • + + + +
  • + +
+
+ +
+
+
+ +
+
+
Zakres diagnozy
+

Obszary oceniane w narzędziu

+
+
+ +
+
+ + pytań w tej sekcji. +
+
+ +
+
+ +
+
+ + + + + + + diff --git a/app/bootstrap.php b/app/bootstrap.php new file mode 100644 index 0000000..8978c12 --- /dev/null +++ b/app/bootstrap.php @@ -0,0 +1,15 @@ + [ + '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.', + ], + ], + ], + ]; +} diff --git a/app/diagnostic_functions.php b/app/diagnostic_functions.php new file mode 100644 index 0000000..6dab6aa --- /dev/null +++ b/app/diagnostic_functions.php @@ -0,0 +1,498 @@ +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 .= '
  • ' . htmlspecialchars($area['section_name']) . ': ' . (int)$area['percentage'] . '% dojrzałości — ten obszar najprawdopodobniej generuje dziś największe tarcia, opóźnienia lub ukryte ryzyka.
  • '; + } + + $recommendations = ''; + foreach (($result['recommendations'] ?? []) as $item) { + $recommendations .= '
  • ' . htmlspecialchars($item) . '
  • '; + } + + return '
    ' + . '

    Raport z diagnozy dojrzałości procesowej

    ' + . '

    Dziękujemy za wypełnienie diagnozy. Obecny poziom dojrzałości operacyjnej Twojej firmy to ' . htmlspecialchars($result['segment_label'] ?? 'Diagnoza zakończona') . ' (' . (int)($result['percentage_score'] ?? 0) . '%).

    ' + . '

    ' . htmlspecialchars($result['segment_summary'] ?? '') . '

    ' + . '

    Priorytetowe obszary

    ' + . '

    Rekomendowane kolejne kroki

    ' + . '

    Jeśli chcesz omówić wynik i ustalić priorytety zmian, wróć do podsumowania: ' . htmlspecialchars($resultUrl) . '.

    ' + . '
    '; +} + +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; +} diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..da9638f 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -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; -} \ No newline at end of file +} + +.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; + } +} diff --git a/assets/js/main.js b/assets/js/main.js index d349598..f5fedf1 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -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); } }); }); diff --git a/diagnostic.php b/diagnostic.php new file mode 100644 index 0000000..324cef2 --- /dev/null +++ b/diagnostic.php @@ -0,0 +1,447 @@ += $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.'); +?> + + + + + + <?= htmlspecialchars($meta['title']) ?> + + + + + + + + + + + + + + + +
    +
    + + + + + +
    +
    +
    +
    Narzędzie diagnostyczne
    +

    +

    +
    +
    Czas wypełnienia
    +
    Format pytań w obszarach
    +
    RezultatWynik na ekranie + raport e-mail
    +
    OdbiorcaWłaściciele i liderzy operacyjni
    +
    +
      +
    • Jedno pytanie na ekranie, bez zbędnego rozproszenia.
    • +
    • Ocena w 7 kluczowych obszarach operacyjnych firmy.
    • +
    • Natychmiastowe podsumowanie z najmocniejszymi i najsłabszymi obszarami.
    • +
    • Raport wysyłany na adres e-mail podany przed rozpoczęciem diagnozy.
    • +
    +
    +
    +
    +
    Krok 1 z 2
    +

    Podaj adres e-mail

    +

    Podsumowanie zobaczysz od razu na ekranie, a rozszerzony raport wyślemy na wskazany adres e-mail. Konto użytkownika nie jest wymagane.

    +
    + +
    + + +
    +

    Podanie adresu e-mail jest wymagane wyłącznie do dostarczenia raportu z diagnozy. Zgoda marketingowa jest opcjonalna i niezależna od wysyłki raportu.

    +
    + + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    Zakres diagnozy
    +

    Co ocenia to narzędzie

    +
    +
    + +
    +
    + + pytań dotyczących dojrzałości operacyjnej w tym obszarze. +
    +
    + +
    +
    +
    +
    +
    +
    +
    Po co to robić
    +

    Dlaczego warto wykonać diagnozę

    +
    +
      +
    • Zobaczysz, które obszary wymagają największej interwencji organizacyjnej.
    • +
    • Łatwiej ustalisz priorytety usprawnień zamiast działać intuicyjnie.
    • +
    • Otrzymasz punkt wyjścia do rozmowy o wdrożeniu zmian lub konsultacji.
    • +
    +
    +
    +
    + + +
    +
    +
    +
    +

    +

    Pytanie z . Odpowiedz zgodnie z aktualnym stanem organizacji, a nie stanem docelowym.

    +
    +
    + Postęp + % +
    +
    + +
    +
    +
    + +
    + + +
    + $label): ?> +
    + +
    + +
    +
    + + +
    +
    +
    + + + +
    +
    +
    +
    Podsumowanie diagnozy
    +

    +

    +
    +
    + Łączny wynik + % +
    +
    + + + + + + + +
    + +
    +
    + + % +
    +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    Najmocniejsze obszary
    +
      + +
    1. + + % dojrzałości w tym obszarze. +
    2. + +
    +
    +
    +
    +
    +
    Priorytety do poprawy
    +
      + +
    1. + + % — tutaj warto rozpocząć porządkowanie i standaryzację. +
    2. + +
    +
    +
    +
    +
    +
    Rekomendowane kolejne kroki
    +
      + +
    • + +
    +
    +
    +
    + +
    +
    +
    +
    Kolejny krok
    +

    Wykorzystaj wynik jako podstawę do rozmowy o usprawnieniach

    +

    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.

    + + + +
    + +
    +
    + + + + +
    +
    Brak aktywnej diagnozy
    +

    Nie znaleziono aktywnego wyniku

    +

    Rozpocznij nowe badanie, aby wygenerować podsumowanie dojrzałości procesowej i raport wysyłany e-mailem.

    + Rozpocznij diagnozę +
    + +
    +
    + + + + + + + + + + diff --git a/healthz.php b/healthz.php new file mode 100644 index 0000000..7615dd0 --- /dev/null +++ b/healthz.php @@ -0,0 +1,16 @@ + '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); diff --git a/index.php b/index.php index 7205f3d..38088aa 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,193 @@ - + - - - New Style - - - - - - - - + + + <?= htmlspecialchars($meta['title']) ?> + + + + + + + + - - - - - - - - - - + + - -
    -
    -

    Analyzing your requirements and generating your website…

    -
    - Loading… -
    -

    AI is collecting your requirements and applying the first changes.

    -

    This page will update automatically as the plan is implemented.

    -

    Runtime: PHP — UTC

    + + + +
    +
    +
    +
    +
    + Narzędzie dla firm B2B i zespołów operacyjnych +

    Sprawdź, na ile procesy w Twojej firmie są gotowe do wzrostu.

    +

    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.

    +
    + Test jest w 100% darmowy + Bez opłaty za uruchomienie i bez konieczności rozmowy handlowej. +
    + +
    +
    Pytania
    +
    Obszary
    +
    Czas
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    Zakres oceny
    +

    Siedem obszarów, które najczęściej decydują o jakości działania firmy

    +
    +
    + +
    +
    + + pytań pomagających ocenić poziom uporządkowania, powtarzalności i kontroli. +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    Jak to działa
    +

    Prosty przebieg dla respondenta, uporządkowane dane dla administratora

    +
    +
    +
    1. E-mail przed startemAdres jest wymagany do wysłania raportu i zapisania próby w bazie.
    +
    2. Wieloetapowa diagnozaUżytkownik przechodzi przez pytania krok po kroku z paskiem postępu.
    +
    3. Wynik natychmiastNa ekranie od razu pojawia się segment dojrzałości i ocena sekcji.
    +
    4. Raport e-mailSzczegółowe podsumowanie trafia na podany adres e-mail.
    +
    +
    +
    +
    +
    Zastosowanie
    +

    Dla kogo powstało to rozwiązanie

    +

    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.

    +
      +
    • Właściciele firm
    • +
    • Zarządy i managing directorzy
    • +
    • Szefowie operacji i menedżerowie działów
    • +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    Praktyczne wnioski
    +

    Jak interpretować wynik diagnozy

    +
    +
    +
    +
    +

    Widoczność operacyjna

    +

    Czy firma działa na podstawie danych, czy głównie intuicji i doraźnych działań?

    +

    Wynik pokaże, czy menedżerowie i właściciel mają realny wgląd w status zadań, opóźnienia, jakość i ryzyka.

    +
    +
    +
    +
    +

    Powtarzalność

    +

    Na ile codzienna praca jest oparta na standardach, a nie na pamięci pojedynczych osób?

    +

    To kluczowy wskaźnik zdolności organizacji do utrzymania jakości i skalowania bez chaosu.

    +
    +
    +
    +
    +

    Rola właściciela

    +

    Czy firma potrafi działać sprawnie bez ciągłej interwencji właściciela?

    +

    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.

    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    Gotowe do użycia
    +

    Uruchom diagnozę jako niezależną podstronę w ramach bloga firmowego

    +

    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.

    +
    + +
    +
    +
    +
    +
    + +
    - + + + +