1426 lines
55 KiB
PHP
1426 lines
55 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,
|
|
report_subject VARCHAR(255) NULL,
|
|
report_html LONGTEXT NULL,
|
|
report_text LONGTEXT NULL,
|
|
report_generated_at DATETIME NULL,
|
|
ai_summary_text TEXT NULL,
|
|
ai_summary_generated_at DATETIME 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', 'report_subject')) {
|
|
db()->exec("ALTER TABLE diagnostic_attempts ADD COLUMN report_subject VARCHAR(255) NULL AFTER result_json");
|
|
}
|
|
|
|
if (!diagnostic_schema_column_exists('diagnostic_attempts', 'report_html')) {
|
|
db()->exec("ALTER TABLE diagnostic_attempts ADD COLUMN report_html LONGTEXT NULL AFTER report_subject");
|
|
}
|
|
|
|
if (!diagnostic_schema_column_exists('diagnostic_attempts', 'report_text')) {
|
|
db()->exec("ALTER TABLE diagnostic_attempts ADD COLUMN report_text LONGTEXT NULL AFTER report_html");
|
|
}
|
|
|
|
if (!diagnostic_schema_column_exists('diagnostic_attempts', 'report_generated_at')) {
|
|
db()->exec("ALTER TABLE diagnostic_attempts ADD COLUMN report_generated_at DATETIME NULL AFTER report_text");
|
|
}
|
|
|
|
if (!diagnostic_schema_column_exists('diagnostic_attempts', 'ai_summary_text')) {
|
|
db()->exec("ALTER TABLE diagnostic_attempts ADD COLUMN ai_summary_text TEXT NULL AFTER report_generated_at");
|
|
}
|
|
|
|
if (!diagnostic_schema_column_exists('diagnostic_attempts', 'ai_summary_generated_at')) {
|
|
db()->exec("ALTER TABLE diagnostic_attempts ADD COLUMN ai_summary_generated_at DATETIME NULL AFTER ai_summary_text");
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
db()->exec(
|
|
"CREATE TABLE IF NOT EXISTS diagnostic_settings (
|
|
setting_key VARCHAR(100) NOT NULL PRIMARY KEY,
|
|
setting_value VARCHAR(255) NULL,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
|
);
|
|
}
|
|
|
|
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_admin_entry_filename(): string
|
|
{
|
|
return 'admin-vr0heltmao.php';
|
|
}
|
|
|
|
function diagnostic_admin_path(array $query = []): string
|
|
{
|
|
$path = '/' . diagnostic_admin_entry_filename();
|
|
if ($query === []) {
|
|
return $path;
|
|
}
|
|
|
|
$queryString = http_build_query($query);
|
|
return $queryString !== '' ? $path . '?' . $queryString : $path;
|
|
}
|
|
|
|
function diagnostic_brand_home_url(): string
|
|
{
|
|
return 'https://doktorbiznes.pl/';
|
|
}
|
|
|
|
function diagnostic_brand_logo_public_path(): string
|
|
{
|
|
return '/assets/pasted-20260411-194222-a875bba4.png';
|
|
}
|
|
|
|
function diagnostic_brand_logo_file_path(): string
|
|
{
|
|
return __DIR__ . '/../assets/pasted-20260411-194222-a875bba4.png';
|
|
}
|
|
|
|
function diagnostic_brand_logo_alt(): string
|
|
{
|
|
return 'DoktorBiznes.pl — Gdy Twój biznes potrzebuje trafnej diagnozy';
|
|
}
|
|
|
|
function diagnostic_brand_logo_anchor(string $className = '', bool $openInNewTab = true): string
|
|
{
|
|
$classes = trim('doctor-biznes-logo-link ' . preg_replace('/[^a-zA-Z0-9_\- ]+/', '', $className));
|
|
$target = $openInNewTab ? ' target="_blank" rel="noopener noreferrer"' : '';
|
|
|
|
return '<a class="' . diagnostic_email_escape($classes) . '" href="' . diagnostic_email_escape(diagnostic_brand_home_url()) . '"' . $target . ' aria-label="Przejdź do bloga DoktorBiznes.pl">'
|
|
. '<img class="doctor-biznes-logo" src="' . diagnostic_email_escape(diagnostic_brand_logo_public_path()) . '" alt="' . diagnostic_email_escape(diagnostic_brand_logo_alt()) . '" width="391" height="121">'
|
|
. '</a>';
|
|
}
|
|
|
|
function diagnostic_brand_logo_email_src(): string
|
|
{
|
|
static $src = null;
|
|
if ($src !== null) {
|
|
return $src;
|
|
}
|
|
|
|
$path = diagnostic_brand_logo_file_path();
|
|
if (!is_file($path) || !is_readable($path)) {
|
|
$src = '';
|
|
return $src;
|
|
}
|
|
|
|
$data = file_get_contents($path);
|
|
if ($data === false || $data === '') {
|
|
$src = '';
|
|
return $src;
|
|
}
|
|
|
|
$src = 'data:image/png;base64,' . base64_encode($data);
|
|
return $src;
|
|
}
|
|
|
|
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,
|
|
report_subject = NULL,
|
|
report_html = NULL,
|
|
report_text = NULL,
|
|
report_generated_at = NULL,
|
|
ai_summary_text = NULL,
|
|
ai_summary_generated_at = NULL,
|
|
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);
|
|
diagnostic_store_report_delivery($attemptId, $emailResult);
|
|
|
|
return diagnostic_get_attempt($attemptId);
|
|
}
|
|
|
|
function diagnostic_store_report_delivery(int $attemptId, array $emailResult): void
|
|
{
|
|
$emailStmt = db()->prepare(
|
|
'UPDATE diagnostic_attempts
|
|
SET email_report_status = :status, email_report_error = :error, updated_at = NOW()
|
|
WHERE id = :id'
|
|
);
|
|
$emailStmt->execute([
|
|
':status' => !empty($emailResult['success']) ? 'sent' : 'failed',
|
|
':error' => !empty($emailResult['success']) ? null : ($emailResult['error'] ?? 'Nieznany błąd wysyłki e-mail'),
|
|
':id' => $attemptId,
|
|
]);
|
|
}
|
|
|
|
function diagnostic_attempt_can_send_report(array $attempt): bool
|
|
{
|
|
return ($attempt['status'] ?? '') === 'completed'
|
|
&& !empty($attempt['email'])
|
|
&& filter_var((string)$attempt['email'], FILTER_VALIDATE_EMAIL)
|
|
&& !empty($attempt['result']);
|
|
}
|
|
|
|
function diagnostic_admin_resend_report(int $attemptId): array
|
|
{
|
|
$attempt = diagnostic_get_attempt($attemptId);
|
|
if (!$attempt) {
|
|
return ['success' => false, 'error' => 'Nie znaleziono wskazanej próby.'];
|
|
}
|
|
|
|
if (!diagnostic_attempt_can_send_report($attempt)) {
|
|
return ['success' => false, 'error' => 'Raport można wysłać ponownie tylko dla ukończonej próby z poprawnym adresem e-mail i wynikiem.'];
|
|
}
|
|
|
|
$emailResult = diagnostic_send_report_email($attempt);
|
|
diagnostic_store_report_delivery($attemptId, $emailResult);
|
|
|
|
return $emailResult;
|
|
}
|
|
|
|
function diagnostic_email_escape(?string $value): string
|
|
{
|
|
return htmlspecialchars((string)$value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
}
|
|
|
|
function diagnostic_email_brand_marker_html(): string
|
|
{
|
|
$src = diagnostic_brand_logo_email_src();
|
|
if ($src === '') {
|
|
return '<div style="display:inline-block;font-size:24px;line-height:1.2;font-weight:800;color:#ffffff;">DoktorBiznes<span style="color:#ff9c57;">.pl</span></div>';
|
|
}
|
|
|
|
return '<div style="margin:0 0 14px;">'
|
|
. '<a href="' . diagnostic_email_escape(diagnostic_brand_home_url()) . '" style="display:inline-block;text-decoration:none;" aria-label="Przejdź do bloga DoktorBiznes.pl">'
|
|
. '<img src="' . diagnostic_email_escape($src) . '" alt="' . diagnostic_email_escape(diagnostic_brand_logo_alt()) . '" width="235" height="73" style="display:block;width:235px;max-width:100%;height:auto;border:0;">'
|
|
. '</a>'
|
|
. '</div>';
|
|
}
|
|
|
|
function diagnostic_email_document(string $subject, string $preheader, string $contentHtml): string
|
|
{
|
|
$safeSubject = diagnostic_email_escape($subject);
|
|
$safePreheader = diagnostic_email_escape($preheader);
|
|
|
|
return '<!doctype html>'
|
|
. '<html lang="pl"><head><meta charset="UTF-8"><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>' . $safeSubject . '</title></head>'
|
|
. '<body style="margin:0;padding:0;background-color:#f3f4f6;color:#111827;font-family:Inter,Segoe UI,Arial,sans-serif;">'
|
|
. '<div style="display:none;max-height:0;overflow:hidden;opacity:0;color:transparent;">' . $safePreheader . '</div>'
|
|
. '<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="width:100%;border-collapse:collapse;background-color:#f3f4f6;">'
|
|
. '<tr><td align="center" style="padding:24px 12px;">'
|
|
. '<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width:680px;width:100%;border-collapse:collapse;background-color:#ffffff;border:1px solid #d8e3f3;border-radius:20px;overflow:hidden;box-shadow:0 12px 32px rgba(15,23,42,0.08);">'
|
|
. '<tr><td style="padding:32px;background:linear-gradient(135deg,#38a7e8 0%,#1d88d4 46%,#0e5fb5 100%);color:#ffffff;">'
|
|
. diagnostic_email_brand_marker_html()
|
|
. '</td></tr>'
|
|
. '<tr><td style="padding:32px;">' . $contentHtml . '</td></tr>'
|
|
. '<tr><td style="padding:0 32px 32px;color:#6b7280;font-size:13px;line-height:1.6;">Ta wiadomość została wygenerowana automatycznie przez narzędzie diagnozy dojrzałości procesowej.</td></tr>'
|
|
. '</table>'
|
|
. '</td></tr></table>'
|
|
. '</body></html>';
|
|
}
|
|
|
|
function diagnostic_email_data_table(array $rows): string
|
|
{
|
|
$html = '<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="width:100%;border-collapse:collapse;margin:16px 0 0;">';
|
|
foreach ($rows as $label => $value) {
|
|
$html .= '<tr>'
|
|
. '<td style="padding:12px 0;border-bottom:1px solid #e5e7eb;width:220px;color:#6b7280;font-size:14px;font-weight:600;vertical-align:top;">' . diagnostic_email_escape((string)$label) . '</td>'
|
|
. '<td style="padding:12px 0;border-bottom:1px solid #e5e7eb;color:#111827;font-size:14px;">' . diagnostic_email_escape((string)$value) . '</td>'
|
|
. '</tr>';
|
|
}
|
|
return $html . '</table>';
|
|
}
|
|
|
|
|
|
function diagnostic_email_list(array $items, string $accentColor = '#111827'): string
|
|
{
|
|
$html = '<ul style="margin:0;padding:0 0 0 20px;color:#374151;font-size:15px;line-height:1.7;">';
|
|
foreach ($items as $item) {
|
|
$html .= '<li style="margin:0 0 10px;color:' . diagnostic_email_escape($accentColor) . ';"><span style="color:#374151;">' . diagnostic_email_escape((string)$item) . '</span></li>';
|
|
}
|
|
return $html . '</ul>';
|
|
}
|
|
|
|
function diagnostic_report_limit(string $value, int $maxLength): string
|
|
{
|
|
$value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/u', '', $value) ?? $value;
|
|
$value = str_replace(["\r\n", "\r"], "\n", $value);
|
|
$value = preg_replace('/[ \t]+/u', ' ', $value) ?? $value;
|
|
$value = preg_replace("/\n{3,}/u", "\n\n", $value) ?? $value;
|
|
$value = trim($value);
|
|
|
|
if ($maxLength > 0) {
|
|
if (function_exists('mb_substr')) {
|
|
$value = mb_substr($value, 0, $maxLength, 'UTF-8');
|
|
} else {
|
|
$value = substr($value, 0, $maxLength);
|
|
}
|
|
$value = trim($value);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
function diagnostic_report_list(array $items, int $maxItems = 3, int $maxLength = 220): array
|
|
{
|
|
$normalized = [];
|
|
foreach ($items as $item) {
|
|
if (!is_scalar($item)) {
|
|
continue;
|
|
}
|
|
$line = diagnostic_report_limit((string)$item, $maxLength);
|
|
if ($line !== '') {
|
|
$normalized[] = $line;
|
|
}
|
|
if (count($normalized) >= $maxItems) {
|
|
break;
|
|
}
|
|
}
|
|
return $normalized;
|
|
}
|
|
|
|
function diagnostic_question_answer_rows(array $attempt): array
|
|
{
|
|
$rows = [];
|
|
$questions = diagnostic_flat_questions();
|
|
$answers = $attempt['answers'] ?? [];
|
|
foreach ($questions as $question) {
|
|
$value = $answers[$question['id']] ?? null;
|
|
if ($value === null || $value === '') {
|
|
continue;
|
|
}
|
|
$rows[] = [
|
|
'section' => $question['section_name'],
|
|
'question' => $question['text'],
|
|
'answer_value' => (int)$value,
|
|
'answer_label' => diagnostic_answer_label((int)$value),
|
|
'reverse' => !empty($question['reverse']),
|
|
];
|
|
}
|
|
return $rows;
|
|
}
|
|
|
|
function diagnostic_default_report_copy(array $attempt): array
|
|
{
|
|
$result = $attempt['result'] ?? [];
|
|
$strengths = [];
|
|
foreach (($result['strongest_areas'] ?? []) as $area) {
|
|
$strengths[] = sprintf(
|
|
'%s (%d%%) — to obszar, który już daje Ci relatywnie stabilny fundament operacyjny.',
|
|
(string)($area['section_name'] ?? 'Mocny obszar'),
|
|
(int)($area['percentage'] ?? 0)
|
|
);
|
|
}
|
|
|
|
$priorities = [];
|
|
foreach (($result['weakest_areas'] ?? []) as $area) {
|
|
$priorities[] = sprintf(
|
|
'%s (%d%%) — tutaj najszybciej pojawiają się tarcia, opóźnienia lub zależność od ręcznego nadzoru.',
|
|
(string)($area['section_name'] ?? 'Priorytet'),
|
|
(int)($area['percentage'] ?? 0)
|
|
);
|
|
}
|
|
|
|
$nextSteps = [];
|
|
foreach (($result['recommendations'] ?? []) as $item) {
|
|
$nextSteps[] = (string)$item;
|
|
}
|
|
|
|
$segmentLabel = (string)($result['segment_label'] ?? 'Diagnoza zakończona');
|
|
$segmentSummary = diagnostic_report_limit((string)($result['segment_summary'] ?? ''), 420);
|
|
$priorityNames = array_map(
|
|
static fn(array $area): string => (string)($area['section_name'] ?? ''),
|
|
(array)($result['weakest_areas'] ?? [])
|
|
);
|
|
$priorityNames = array_values(array_filter($priorityNames, static fn(string $name): bool => trim($name) !== ''));
|
|
$whatItMeans = 'Największy potencjał poprawy widać obecnie w obszarach: ' . ($priorityNames ? implode(', ', $priorityNames) : 'organizacji codziennej pracy') . '.';
|
|
|
|
return [
|
|
'subject' => 'Twój raport diagnozy dojrzałości procesowej',
|
|
'preheader' => 'Gotowe: wynik diagnozy i rekomendowane kolejne kroki dla Twojej firmy.',
|
|
'intro' => 'Dziękujemy za wypełnienie diagnozy. Poniżej znajdziesz krótkie podsumowanie wyniku oraz rekomendowane następne kroki.',
|
|
'executive_summary' => $segmentSummary !== '' ? $segmentSummary : 'Masz już gotowy wynik diagnozy oraz wskazanie obszarów, które warto uporządkować w pierwszej kolejności.',
|
|
'what_it_means' => $whatItMeans,
|
|
'strengths' => diagnostic_report_list($strengths),
|
|
'priorities' => diagnostic_report_list($priorities),
|
|
'next_steps' => diagnostic_report_list($nextSteps),
|
|
'closing' => '',
|
|
'result_overview' => diagnostic_default_result_overview($attempt),
|
|
];
|
|
}
|
|
|
|
function diagnostic_default_result_overview(array $attempt): string
|
|
{
|
|
$result = $attempt['result'] ?? [];
|
|
$score = (int)($result['percentage_score'] ?? 0);
|
|
$segmentLabel = (string)($result['segment_label'] ?? 'Diagnoza zakończona');
|
|
$segmentSummary = diagnostic_report_limit((string)($result['segment_summary'] ?? ''), 380);
|
|
|
|
$strongest = array_map(
|
|
static fn(array $area): string => sprintf('%s (%d%%)', (string)($area['section_name'] ?? 'Mocny obszar'), (int)($area['percentage'] ?? 0)),
|
|
array_slice((array)($result['strongest_areas'] ?? []), 0, 2)
|
|
);
|
|
$strongest = array_values(array_filter($strongest, static fn(string $item): bool => trim($item) !== ''));
|
|
|
|
$weakest = array_map(
|
|
static fn(array $area): string => sprintf('%s (%d%%)', (string)($area['section_name'] ?? 'Priorytet'), (int)($area['percentage'] ?? 0)),
|
|
array_slice((array)($result['weakest_areas'] ?? []), 0, 2)
|
|
);
|
|
$weakest = array_values(array_filter($weakest, static fn(string $item): bool => trim($item) !== ''));
|
|
|
|
$recommendations = diagnostic_report_list((array)($result['recommendations'] ?? []), 2, 180);
|
|
|
|
$parts = [];
|
|
$parts[] = 'Wynik ' . $score . '% klasyfikuje firmę w segmencie „' . $segmentLabel . '”.';
|
|
if ($segmentSummary !== '') {
|
|
$parts[] = $segmentSummary;
|
|
}
|
|
if ($strongest !== []) {
|
|
$parts[] = 'Najmocniej wypadają obecnie: ' . implode(', ', $strongest) . '.';
|
|
}
|
|
if ($weakest !== []) {
|
|
$parts[] = 'Najwięcej uporządkowania wymagają: ' . implode(', ', $weakest) . '.';
|
|
}
|
|
if ($recommendations !== []) {
|
|
$parts[] = 'Najbardziej praktyczny kolejny krok to: ' . implode(' ', $recommendations);
|
|
}
|
|
|
|
return diagnostic_report_limit(implode(' ', $parts), 1000);
|
|
}
|
|
|
|
function diagnostic_ai_report_prompt_payload(array $attempt): array
|
|
{
|
|
$result = $attempt['result'] ?? [];
|
|
|
|
return [
|
|
'quiz' => [
|
|
'title' => diagnostic_quiz_definition()['quiz']['title'] ?? 'Diagnoza dojrzałości procesowej',
|
|
'language' => 'pl',
|
|
],
|
|
'recipient' => [
|
|
'email' => (string)($attempt['email'] ?? ''),
|
|
],
|
|
'result' => [
|
|
'score_percent' => (int)($result['percentage_score'] ?? 0),
|
|
'segment_label' => (string)($result['segment_label'] ?? ''),
|
|
'segment_summary' => (string)($result['segment_summary'] ?? ''),
|
|
'recommendations' => array_values((array)($result['recommendations'] ?? [])),
|
|
'strongest_areas' => array_values((array)($result['strongest_areas'] ?? [])),
|
|
'weakest_areas' => array_values((array)($result['weakest_areas'] ?? [])),
|
|
'section_scores' => array_values((array)($result['section_scores'] ?? [])),
|
|
],
|
|
'answers' => diagnostic_question_answer_rows($attempt),
|
|
];
|
|
}
|
|
|
|
function diagnostic_normalize_report_copy(array $candidate, array $defaults): array
|
|
{
|
|
$copy = $defaults;
|
|
foreach (['subject' => 140, 'preheader' => 180, 'intro' => 420, 'executive_summary' => 650, 'what_it_means' => 520, 'closing' => 320, 'result_overview' => 1000] as $field => $maxLength) {
|
|
if (!empty($candidate[$field]) && is_scalar($candidate[$field])) {
|
|
$normalized = diagnostic_report_limit((string)$candidate[$field], $maxLength);
|
|
if ($normalized !== '') {
|
|
$copy[$field] = $normalized;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (['strengths', 'priorities', 'next_steps'] as $field) {
|
|
if (!empty($candidate[$field]) && is_array($candidate[$field])) {
|
|
$normalizedList = diagnostic_report_list($candidate[$field]);
|
|
if ($normalizedList !== []) {
|
|
$copy[$field] = $normalizedList;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $copy;
|
|
}
|
|
|
|
function diagnostic_generate_ai_report_copy(array $attempt): array
|
|
{
|
|
$defaults = diagnostic_default_report_copy($attempt);
|
|
$payload = diagnostic_ai_report_prompt_payload($attempt);
|
|
|
|
try {
|
|
$response = LocalAIApi::createResponse(
|
|
[
|
|
'input' => [
|
|
[
|
|
'role' => 'system',
|
|
'content' => 'Jesteś konsultantem operacyjnym B2B. Tworzysz krótkie, konkretne i profesjonalne podsumowania e-mail po polsku na podstawie danych z quizu. Odpowiadaj wyłącznie poprawnym JSON-em UTF-8 bez markdownu i bez HTML. Zachowaj rzeczowy ton. Nie wymyślaj faktów wykraczających poza dane. Zwróć dokładnie pola: subject, preheader, intro, executive_summary, what_it_means, strengths, priorities, next_steps, closing, result_overview. strengths/priorities/next_steps mają być tablicami 2-3 krótkich punktów. result_overview ma być jednym spójnym opisem wyniku na stronę podsumowania, maksymalnie 1000 znaków.'
|
|
],
|
|
[
|
|
'role' => 'user',
|
|
'content' => "Przygotuj treść raportu e-mail dla klienta na podstawie tych danych quizu. Dane wejściowe JSON:\n" . json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
|
|
],
|
|
],
|
|
],
|
|
[
|
|
'poll_interval' => 3,
|
|
'poll_timeout' => 120,
|
|
]
|
|
);
|
|
|
|
if (!empty($response['success'])) {
|
|
$decoded = LocalAIApi::decodeJsonFromResponse($response);
|
|
if (is_array($decoded)) {
|
|
return [
|
|
'copy' => diagnostic_normalize_report_copy($decoded, $defaults),
|
|
'source' => 'ai',
|
|
];
|
|
}
|
|
|
|
error_log('Diagnostic AI report generation returned unparsable payload.');
|
|
} else {
|
|
error_log('Diagnostic AI report generation failed: ' . (string)($response['error'] ?? $response['message'] ?? 'unknown_error'));
|
|
}
|
|
} catch (Throwable $exception) {
|
|
error_log('Diagnostic AI report generation failed: ' . $exception->getMessage());
|
|
}
|
|
|
|
return [
|
|
'copy' => $defaults,
|
|
'source' => 'fallback',
|
|
];
|
|
}
|
|
|
|
function diagnostic_build_report_result_overview_html(string $overview): string
|
|
{
|
|
$overview = diagnostic_report_limit(trim($overview), 1000);
|
|
if ($overview === '') {
|
|
return '';
|
|
}
|
|
|
|
return '<div style="margin:0 0 24px;padding:20px 22px;border-radius:18px;background:linear-gradient(135deg,#eff6ff 0%,#f8fbff 100%);border:1px solid #bfdbfe;">'
|
|
. '<div style="font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#2563eb;margin-bottom:8px;">Co ten wynik mówi o Twojej organizacji</div>'
|
|
. '<div style="font-size:15px;line-height:1.75;color:#1f2937;">' . diagnostic_email_escape($overview) . '</div>'
|
|
. '</div>';
|
|
}
|
|
|
|
function diagnostic_build_report_result_overview_text(string $overview): string
|
|
{
|
|
$overview = diagnostic_report_limit(trim($overview), 1000);
|
|
if ($overview === '') {
|
|
return '';
|
|
}
|
|
|
|
return "Co ten wynik mówi o Twojej organizacji:\n" . $overview;
|
|
}
|
|
|
|
function diagnostic_build_report_html(array $attempt, array $copy): string
|
|
{
|
|
$result = $attempt['result'] ?? [];
|
|
|
|
$strengthsHtml = diagnostic_email_list($copy['strengths'] ?? []);
|
|
$prioritiesHtml = diagnostic_email_list($copy['priorities'] ?? []);
|
|
$nextStepsHtml = diagnostic_email_list($copy['next_steps'] ?? []);
|
|
$resultOverviewHtml = diagnostic_build_report_result_overview_html((string)($copy['result_overview'] ?? ''));
|
|
|
|
$content = '<p style="margin:0 0 18px;font-size:16px;line-height:1.7;color:#374151;">' . diagnostic_email_escape((string)($copy['intro'] ?? '')) . '</p>'
|
|
. '<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="width:100%;border-collapse:collapse;margin:0 0 24px;">'
|
|
. '<tr>'
|
|
. '<td style="padding:20px;border-radius:18px;background:linear-gradient(135deg,#f9fafb 0%,#eef2f7 100%);border:1px solid #e5e7eb;">'
|
|
. '<div style="font-size:13px;letter-spacing:0.08em;text-transform:uppercase;color:#6b7280;margin-bottom:8px;">Wynik łączny</div>'
|
|
. '<div style="font-size:36px;line-height:1;font-weight:700;color:#111827;margin-bottom:8px;">' . (int)($result['percentage_score'] ?? 0) . '%</div>'
|
|
. '<div style="font-size:18px;line-height:1.4;font-weight:700;color:#111827;margin-bottom:10px;">' . diagnostic_email_escape((string)($result['segment_label'] ?? 'Diagnoza zakończona')) . '</div>'
|
|
. '<div style="font-size:15px;line-height:1.7;color:#374151;">' . diagnostic_email_escape((string)($copy['executive_summary'] ?? '')) . '</div>'
|
|
. '</td>'
|
|
. '</tr>'
|
|
. '</table>'
|
|
. $resultOverviewHtml
|
|
. '<div style="margin:0 0 24px;padding:18px 20px;border:1px solid #e5e7eb;border-radius:16px;background:#ffffff;">'
|
|
. '<div style="font-size:12px;letter-spacing:0.08em;text-transform:uppercase;color:#6b7280;margin-bottom:8px;">Co ten wynik oznacza</div>'
|
|
. '<div style="font-size:15px;line-height:1.7;color:#374151;">' . diagnostic_email_escape((string)($copy['what_it_means'] ?? '')) . '</div>'
|
|
. '</div>'
|
|
. '<h2 style="font-size:18px;line-height:1.3;margin:0 0 12px;color:#111827;">Mocne strony</h2>'
|
|
. $strengthsHtml
|
|
. '<h2 style="font-size:18px;line-height:1.3;margin:28px 0 12px;color:#111827;">Priorytety na teraz</h2>'
|
|
. $prioritiesHtml
|
|
. '<h2 style="font-size:18px;line-height:1.3;margin:28px 0 12px;color:#111827;">Rekomendowane kolejne kroki</h2>'
|
|
. $nextStepsHtml;
|
|
|
|
return diagnostic_email_document(
|
|
(string)($copy['subject'] ?? 'Raport z diagnozy dojrzałości procesowej'),
|
|
(string)($copy['preheader'] ?? 'Twój wynik diagnozy i rekomendowane kolejne kroki są gotowe.'),
|
|
$content
|
|
);
|
|
}
|
|
|
|
function diagnostic_build_report_text(array $attempt, array $copy): string
|
|
{
|
|
$result = $attempt['result'] ?? [];
|
|
$resultOverviewText = diagnostic_build_report_result_overview_text((string)($copy['result_overview'] ?? ''));
|
|
$lines = [];
|
|
$lines[] = 'DoktorBiznes.pl';
|
|
$lines[] = diagnostic_brand_home_url();
|
|
$lines[] = '';
|
|
$lines[] = (string)($copy['subject'] ?? 'Raport z diagnozy dojrzałości procesowej');
|
|
$lines[] = 'Wynik: ' . ($result['segment_label'] ?? 'Diagnoza zakończona') . ' (' . (int)($result['percentage_score'] ?? 0) . '%)';
|
|
$lines[] = '';
|
|
$lines[] = (string)($copy['intro'] ?? '');
|
|
$lines[] = '';
|
|
$lines[] = (string)($copy['executive_summary'] ?? '');
|
|
if ($resultOverviewText !== '') {
|
|
$lines[] = '';
|
|
$lines[] = $resultOverviewText;
|
|
}
|
|
$lines[] = '';
|
|
$lines[] = 'Co ten wynik oznacza:';
|
|
$lines[] = (string)($copy['what_it_means'] ?? '');
|
|
$lines[] = '';
|
|
$lines[] = 'Mocne strony:';
|
|
foreach (($copy['strengths'] ?? []) as $item) {
|
|
$lines[] = '- ' . $item;
|
|
}
|
|
$lines[] = '';
|
|
$lines[] = 'Priorytety na teraz:';
|
|
foreach (($copy['priorities'] ?? []) as $item) {
|
|
$lines[] = '- ' . $item;
|
|
}
|
|
$lines[] = '';
|
|
$lines[] = 'Rekomendowane kolejne kroki:';
|
|
foreach (($copy['next_steps'] ?? []) as $item) {
|
|
$lines[] = '- ' . $item;
|
|
}
|
|
return diagnostic_report_limit(rtrim(implode("\n", $lines)), 10000);
|
|
}
|
|
|
|
function diagnostic_strip_legacy_report_cta(array $package): array
|
|
{
|
|
$subject = diagnostic_report_limit((string)($package['subject'] ?? ''), 255);
|
|
$html = (string)($package['html'] ?? '');
|
|
$text = (string)($package['text'] ?? '');
|
|
|
|
if ($html !== '') {
|
|
$html = preg_replace(
|
|
'~<div style="margin:28px 0 0;padding:18px 20px;border-radius:16px;background:#111827;color:#ffffff;">.*?Otwórz podsumowanie diagnozy</a></div>~su',
|
|
'',
|
|
$html
|
|
) ?? $html;
|
|
$html = str_replace(
|
|
'background:linear-gradient(135deg,#111827 0%,#1f2937 100%);color:#ffffff;',
|
|
'background:linear-gradient(135deg,#38a7e8 0%,#1d88d4 46%,#0e5fb5 100%);color:#ffffff;',
|
|
$html
|
|
);
|
|
$html = preg_replace(
|
|
'~<li style="margin:0 0 10px;"><span style="color:[^"]*;">•</span>\s*([^<].*?)</li>~u',
|
|
'<li style="margin:0 0 10px;color:#111827;"><span style="color:#374151;">$1</span></li>',
|
|
$html
|
|
) ?? $html;
|
|
$html = preg_replace(
|
|
'~<div style="display:inline-block;padding:8px 14px;border-radius:999px;background:rgba\(255,255,255,0\.16\);border:1px solid rgba\(255,255,255,0\.22\);font-size:12px;font-weight:800;letter-spacing:0\.16em;text-transform:uppercase;color:#ffffff;">DOKTOR <span style="color:#ff9c57;">BIZNES</span></div>\s*~u',
|
|
'',
|
|
$html,
|
|
1
|
|
) ?? $html;
|
|
$html = preg_replace(
|
|
'~<div style="margin:14px 0 10px;font-size:13px;line-height:1\.5;color:rgba\(255,255,255,0\.88\);font-weight:600;">Gdy Twój biznes potrzebuje trafnej diagnozy</div>\s*~u',
|
|
'',
|
|
$html,
|
|
1
|
|
) ?? $html;
|
|
$html = preg_replace(
|
|
'~<div style="font-size:12px;letter-spacing:0\.12em;text-transform:uppercase;opacity:(?:0\.72|0\.82);margin-bottom:12px;">Przegląd procesów</div>\s*~u',
|
|
'',
|
|
$html,
|
|
1
|
|
) ?? $html;
|
|
$html = str_replace(
|
|
'<tr><td style="padding:32px 32px 12px;background:linear-gradient(135deg,#38a7e8 0%,#1d88d4 46%,#0e5fb5 100%);color:#ffffff;">',
|
|
'<tr><td style="padding:32px;background:linear-gradient(135deg,#38a7e8 0%,#1d88d4 46%,#0e5fb5 100%);color:#ffffff;">',
|
|
$html
|
|
);
|
|
$html = str_replace(
|
|
'<tr><td style="padding:32px 32px 18px;background:linear-gradient(135deg,#38a7e8 0%,#1d88d4 46%,#0e5fb5 100%);color:#ffffff;">',
|
|
'<tr><td style="padding:32px;background:linear-gradient(135deg,#38a7e8 0%,#1d88d4 46%,#0e5fb5 100%);color:#ffffff;">',
|
|
$html
|
|
);
|
|
$html = preg_replace(
|
|
'~<div style="margin:0 0 14px;">\s*<a href="[^"]*" style="display:inline-block;text-decoration:none;" aria-label="Przejdź do bloga DoktorBiznes\.pl">\s*<img[^>]+>\s*</a>\s*</div>~u',
|
|
'',
|
|
$html
|
|
) ?? $html;
|
|
$html = preg_replace(
|
|
'~<div style="display:inline-block;font-size:24px;line-height:1\.2;font-weight:800;color:#ffffff;">DoktorBiznes(?:<span style="color:#ff9c57;">\.pl</span>)?</div>~u',
|
|
'',
|
|
$html
|
|
) ?? $html;
|
|
$html = preg_replace(
|
|
'~(<tr><td style="padding:32px;background:[^"]*;color:#ffffff;">)(?:\s*<div[^>]*>\s*</div>)*~u',
|
|
'$1' . diagnostic_email_brand_marker_html(),
|
|
$html,
|
|
1
|
|
) ?? $html;
|
|
if (strpos($html, 'Przejdź do bloga DoktorBiznes.pl') === false && strpos($html, 'DoktorBiznes<span style="color:#ff9c57;">.pl</span>') === false) {
|
|
$html = preg_replace(
|
|
'~(<tr><td style="padding:32px(?: 32px (?:12|18)px)?;background:[^"]*;color:#ffffff;">)~u',
|
|
'$1' . diagnostic_email_brand_marker_html(),
|
|
$html,
|
|
1
|
|
) ?? $html;
|
|
}
|
|
}
|
|
|
|
if ($text !== '') {
|
|
$text = preg_replace('/^.*omówić wynik z zespołem.*$/miu', '', $text) ?? $text;
|
|
$text = preg_replace('/^DOKTOR BIZNES\s*$/miu', '', $text) ?? $text;
|
|
$text = preg_replace('/^DoktorBiznes\.pl\s*$/miu', '', $text) ?? $text;
|
|
$text = preg_replace('/^Gdy Twój biznes potrzebuje trafnej diagnozy\s*$/miu', '', $text) ?? $text;
|
|
$text = preg_replace('~^https?://(?:www\.)?doktorbiznes\.pl/?\s*$~miu', '', $text) ?? $text;
|
|
$text = 'DoktorBiznes.pl' . "\n" . diagnostic_brand_home_url() . "\n\n" . ltrim($text);
|
|
$text = preg_replace('/\n{3,}/', "\n\n", $text) ?? $text;
|
|
$text = trim($text);
|
|
}
|
|
|
|
return [
|
|
'subject' => $subject,
|
|
'html' => $html,
|
|
'text' => $text,
|
|
];
|
|
}
|
|
|
|
function diagnostic_store_report_content(int $attemptId, array $package): void
|
|
{
|
|
$stmt = db()->prepare(
|
|
"UPDATE diagnostic_attempts
|
|
SET report_subject = :report_subject,
|
|
report_html = :report_html,
|
|
report_text = :report_text,
|
|
report_generated_at = NOW(),
|
|
ai_summary_text = :ai_summary_text,
|
|
ai_summary_generated_at = CASE WHEN :ai_summary_text = '' THEN ai_summary_generated_at ELSE NOW() END,
|
|
updated_at = NOW()
|
|
WHERE id = :id"
|
|
);
|
|
$summary = diagnostic_report_limit((string)($package['result_overview'] ?? ''), 1000);
|
|
|
|
$stmt->execute([
|
|
':report_subject' => (string)($package['subject'] ?? ''),
|
|
':report_html' => (string)($package['html'] ?? ''),
|
|
':report_text' => (string)($package['text'] ?? ''),
|
|
':ai_summary_text' => $summary,
|
|
':id' => $attemptId,
|
|
]);
|
|
}
|
|
|
|
function diagnostic_cached_report_content(array $attempt): ?array
|
|
{
|
|
$package = diagnostic_strip_legacy_report_cta([
|
|
'subject' => (string)($attempt['report_subject'] ?? ''),
|
|
'html' => (string)($attempt['report_html'] ?? ''),
|
|
'text' => (string)($attempt['report_text'] ?? ''),
|
|
]);
|
|
|
|
$subject = $package['subject'];
|
|
$html = $package['html'];
|
|
$text = $package['text'];
|
|
$resultOverview = diagnostic_report_limit((string)($attempt['ai_summary_text'] ?? ''), 1000);
|
|
|
|
if ($subject === '' || $html === '' || $text === '') {
|
|
return null;
|
|
}
|
|
|
|
if ($resultOverview === '') {
|
|
return null;
|
|
}
|
|
|
|
$overviewHtml = diagnostic_build_report_result_overview_html($resultOverview);
|
|
$overviewText = diagnostic_build_report_result_overview_text($resultOverview);
|
|
|
|
if ($overviewHtml !== '' && strpos($html, 'Co ten wynik mówi o Twojej organizacji') === false) {
|
|
$needle = '<div style="margin:0 0 24px;padding:18px 20px;border:1px solid #e5e7eb;border-radius:16px;background:#ffffff;">';
|
|
if (strpos($html, $needle) !== false) {
|
|
$html = str_replace($needle, $overviewHtml . $needle, $html);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if ($overviewText !== '' && strpos($text, 'Co ten wynik mówi o Twojej organizacji:') === false) {
|
|
$needle = "\n\nCo ten wynik oznacza:\n";
|
|
if (strpos($text, $needle) !== false) {
|
|
$text = str_replace($needle, "\n\n" . $overviewText . $needle, $text);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (!empty($attempt['id']) && (
|
|
$subject !== (string)($attempt['report_subject'] ?? '')
|
|
|| $html !== (string)($attempt['report_html'] ?? '')
|
|
|| $text !== (string)($attempt['report_text'] ?? '')
|
|
)) {
|
|
diagnostic_store_report_content((int)$attempt['id'], [
|
|
'subject' => $subject,
|
|
'html' => $html,
|
|
'text' => $text,
|
|
'result_overview' => $resultOverview,
|
|
]);
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'subject' => $subject,
|
|
'html' => $html,
|
|
'text' => $text,
|
|
'result_overview' => $resultOverview,
|
|
'source' => 'cache',
|
|
];
|
|
}
|
|
|
|
function diagnostic_generate_report_content(array $attempt): array
|
|
{
|
|
if (empty($attempt['result'])) {
|
|
return ['success' => false, 'error' => 'Brak wyniku potrzebnego do wygenerowania raportu.'];
|
|
}
|
|
|
|
$ai = diagnostic_generate_ai_report_copy($attempt);
|
|
$copy = $ai['copy'] ?? diagnostic_default_report_copy($attempt);
|
|
|
|
return [
|
|
'success' => true,
|
|
'subject' => (string)($copy['subject'] ?? 'Raport z diagnozy dojrzałości procesowej'),
|
|
'html' => diagnostic_build_report_html($attempt, $copy),
|
|
'text' => diagnostic_build_report_text($attempt, $copy),
|
|
'result_overview' => diagnostic_report_limit((string)($copy['result_overview'] ?? ''), 1000),
|
|
'source' => (string)($ai['source'] ?? 'fallback'),
|
|
];
|
|
}
|
|
|
|
function diagnostic_resolve_result_overview(array $attempt): string
|
|
{
|
|
$cached = diagnostic_report_limit((string)($attempt['ai_summary_text'] ?? ''), 1000);
|
|
if ($cached !== '') {
|
|
return $cached;
|
|
}
|
|
|
|
$report = diagnostic_resolve_report_content($attempt, true);
|
|
if (!empty($report['success'])) {
|
|
$generated = diagnostic_report_limit((string)($report['result_overview'] ?? ''), 1000);
|
|
if ($generated !== '') {
|
|
return $generated;
|
|
}
|
|
}
|
|
|
|
return diagnostic_default_result_overview($attempt);
|
|
}
|
|
|
|
function diagnostic_resolve_report_content(array $attempt, bool $forceRegenerate = false): array
|
|
{
|
|
if (!$forceRegenerate) {
|
|
$cached = diagnostic_cached_report_content($attempt);
|
|
if ($cached !== null) {
|
|
return $cached;
|
|
}
|
|
}
|
|
|
|
$generated = diagnostic_generate_report_content($attempt);
|
|
if (!empty($generated['success']) && !empty($attempt['id'])) {
|
|
diagnostic_store_report_content((int)$attempt['id'], $generated);
|
|
}
|
|
|
|
return $generated;
|
|
}
|
|
|
|
function diagnostic_send_report_email(array $attempt, bool $forceRegenerate = false): array
|
|
{
|
|
if (empty($attempt['email']) || !filter_var((string)$attempt['email'], FILTER_VALIDATE_EMAIL)) {
|
|
return ['success' => false, 'error' => 'Brak poprawnego adresu e-mail klienta'];
|
|
}
|
|
|
|
$report = diagnostic_resolve_report_content($attempt, $forceRegenerate);
|
|
if (empty($report['success'])) {
|
|
return $report;
|
|
}
|
|
|
|
return MailService::sendMail(
|
|
$attempt['email'],
|
|
(string)$report['subject'],
|
|
(string)$report['html'],
|
|
(string)$report['text']
|
|
);
|
|
}
|
|
|
|
function diagnostic_admin_setting_get(string $key): ?string
|
|
{
|
|
$stmt = db()->prepare('SELECT setting_value FROM diagnostic_settings WHERE setting_key = :setting_key LIMIT 1');
|
|
$stmt->execute([':setting_key' => $key]);
|
|
$value = $stmt->fetchColumn();
|
|
if ($value === false || $value === null) {
|
|
return null;
|
|
}
|
|
return trim((string)$value);
|
|
}
|
|
|
|
function diagnostic_admin_setting_set(string $key, ?string $value): void
|
|
{
|
|
$stmt = db()->prepare(
|
|
'INSERT INTO diagnostic_settings (setting_key, setting_value)
|
|
VALUES (:setting_key, :setting_value)
|
|
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), updated_at = CURRENT_TIMESTAMP'
|
|
);
|
|
$stmt->bindValue(':setting_key', $key, PDO::PARAM_STR);
|
|
if ($value === null || $value === '') {
|
|
$stmt->bindValue(':setting_value', null, PDO::PARAM_NULL);
|
|
} else {
|
|
$stmt->bindValue(':setting_value', $value, PDO::PARAM_STR);
|
|
}
|
|
$stmt->execute();
|
|
}
|
|
|
|
function diagnostic_admin_notification_config(): array
|
|
{
|
|
$configuredEmail = diagnostic_admin_setting_get('admin_notification_email') ?? '';
|
|
if ($configuredEmail !== '' && !filter_var($configuredEmail, FILTER_VALIDATE_EMAIL)) {
|
|
$configuredEmail = '';
|
|
}
|
|
|
|
$fallbackEmail = trim((string)(getenv('MAIL_TO') ?: ''));
|
|
if ($fallbackEmail !== '' && !filter_var($fallbackEmail, FILTER_VALIDATE_EMAIL)) {
|
|
$fallbackEmail = '';
|
|
}
|
|
|
|
$effectiveEmail = $configuredEmail !== '' ? $configuredEmail : $fallbackEmail;
|
|
$source = $configuredEmail !== '' ? 'panel' : ($fallbackEmail !== '' ? 'env' : 'none');
|
|
|
|
return [
|
|
'configured_email' => $configuredEmail,
|
|
'fallback_email' => $fallbackEmail,
|
|
'effective_email' => $effectiveEmail,
|
|
'source' => $source,
|
|
];
|
|
}
|
|
|
|
function diagnostic_admin_notification_email(): ?string
|
|
{
|
|
$config = diagnostic_admin_notification_config();
|
|
return $config['effective_email'] !== '' ? $config['effective_email'] : null;
|
|
}
|
|
|
|
function diagnostic_mask_secret(string $value, int $visibleTail = 4): string
|
|
{
|
|
$normalized = preg_replace('/\s+/', '', trim($value)) ?? '';
|
|
if ($normalized === '') {
|
|
return '';
|
|
}
|
|
|
|
$length = function_exists('mb_strlen') ? mb_strlen($normalized) : strlen($normalized);
|
|
if ($length <= $visibleTail) {
|
|
return str_repeat('•', max($length, 8));
|
|
}
|
|
|
|
$tail = function_exists('mb_substr') ? mb_substr($normalized, -$visibleTail) : substr($normalized, -$visibleTail);
|
|
return str_repeat('•', max($length - $visibleTail, 8)) . $tail;
|
|
}
|
|
|
|
function diagnostic_openai_api_key_is_valid(string $value): bool
|
|
{
|
|
$normalized = preg_replace('/\s+/', '', trim($value)) ?? '';
|
|
if ($normalized === '' || strlen($normalized) < 20) {
|
|
return false;
|
|
}
|
|
|
|
return preg_match('/^[A-Za-z0-9_\-]+$/', $normalized) === 1;
|
|
}
|
|
|
|
function diagnostic_admin_openai_key_config(): array
|
|
{
|
|
$configuredKey = diagnostic_admin_setting_get('openai_api_key') ?? '';
|
|
$configuredKey = preg_replace('/\s+/', '', $configuredKey) ?? '';
|
|
if ($configuredKey !== '' && !diagnostic_openai_api_key_is_valid($configuredKey)) {
|
|
$configuredKey = '';
|
|
}
|
|
|
|
return [
|
|
'configured' => $configuredKey !== '',
|
|
'masked_key' => $configuredKey !== '' ? diagnostic_mask_secret($configuredKey) : '',
|
|
];
|
|
}
|
|
|
|
function diagnostic_admin_openai_api_key(): ?string
|
|
{
|
|
$config = diagnostic_admin_openai_key_config();
|
|
if (empty($config['configured'])) {
|
|
return null;
|
|
}
|
|
|
|
$key = diagnostic_admin_setting_get('openai_api_key') ?? '';
|
|
$key = preg_replace('/\s+/', '', $key) ?? '';
|
|
return $key !== '' ? $key : null;
|
|
}
|
|
|
|
function diagnostic_send_consultation_notification(array $attempt): array
|
|
{
|
|
$recipient = diagnostic_admin_notification_email();
|
|
if ($recipient === null) {
|
|
return ['success' => false, 'error' => 'Brak skonfigurowanego adresu powiadomień administratora', 'reason' => 'missing_recipient'];
|
|
}
|
|
|
|
$phone = trim((string)($attempt['contact_phone'] ?? ''));
|
|
if ($phone === '') {
|
|
return ['success' => false, 'error' => 'Brak numeru telefonu do przekazania', 'reason' => 'missing_phone'];
|
|
}
|
|
|
|
$result = $attempt['result'] ?? [];
|
|
$score = isset($result['percentage_score']) ? (int)$result['percentage_score'] . '%' : '—';
|
|
$segment = (string)($result['segment_label'] ?? '—');
|
|
$requestedAt = (string)($attempt['consultation_requested_at'] ?? date('Y-m-d H:i:s'));
|
|
$respondentEmail = trim((string)($attempt['email'] ?? ''));
|
|
$marketingConsent = ((int)($attempt['marketing_consent'] ?? 0)) === 1 ? 'Tak' : 'Nie';
|
|
|
|
$subject = 'Nowa prośba o konsultację po diagnozie';
|
|
$html = diagnostic_email_document(
|
|
$subject,
|
|
'Użytkownik zakończył diagnozę i poprosił o kontakt telefoniczny.',
|
|
'<p style="margin:0 0 18px;font-size:16px;line-height:1.7;color:#374151;">Użytkownik zakończył diagnozę i poprosił o dalszą konsultację.</p>'
|
|
. '<div style="padding:18px 20px;border:1px solid #dbeafe;border-radius:16px;background:#eff6ff;color:#1d4ed8;margin-bottom:20px;">'
|
|
. '<div style="font-size:13px;letter-spacing:0.08em;text-transform:uppercase;opacity:0.72;margin-bottom:6px;">Telefon kontaktowy</div>'
|
|
. '<div style="font-size:28px;line-height:1.1;font-weight:700;color:#111827;">' . diagnostic_email_escape($phone) . '</div>'
|
|
. '</div>'
|
|
. diagnostic_email_data_table([
|
|
'ID próby' => (string)(int)($attempt['id'] ?? 0),
|
|
'E-mail respondenta' => $respondentEmail !== '' ? $respondentEmail : '—',
|
|
'Data zgłoszenia' => $requestedAt,
|
|
'Wynik diagnozy' => $score,
|
|
'Segment' => $segment,
|
|
'Zgoda marketingowa' => $marketingConsent,
|
|
])
|
|
);
|
|
|
|
$text = "Nowa prośba o kontakt
|
|
|
|
"
|
|
. "ID próby: " . (int)($attempt['id'] ?? 0) . "
|
|
"
|
|
. "E-mail respondenta: " . ($respondentEmail !== '' ? $respondentEmail : '—') . "
|
|
"
|
|
. "Telefon kontaktowy: {$phone}
|
|
"
|
|
. "Data zgłoszenia: {$requestedAt}
|
|
"
|
|
. "Wynik diagnozy: {$score}
|
|
"
|
|
. "Segment: {$segment}
|
|
"
|
|
. "Zgoda marketingowa: {$marketingConsent}
|
|
";
|
|
|
|
$opts = [];
|
|
if ($respondentEmail !== '' && filter_var($respondentEmail, FILTER_VALIDATE_EMAIL)) {
|
|
$opts['reply_to'] = $respondentEmail;
|
|
}
|
|
|
|
return MailService::sendMail($recipient, $subject, $html, $text, $opts);
|
|
}
|
|
|
|
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;
|
|
}
|