39564-vm/app/diagnostic_functions.php
2026-04-11 19:06:55 +00:00

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