499 lines
18 KiB
PHP
499 lines
18 KiB
PHP
<?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;
|
|
}
|