$type, 'message' => $message, ]; } function okr_pull_flash(): ?array { $flash = $_SESSION['okr_flash'] ?? null; unset($_SESSION['okr_flash']); return is_array($flash) ? $flash : null; } function okr_csrf_token(): string { if (empty($_SESSION['okr_csrf'])) { $_SESSION['okr_csrf'] = bin2hex(random_bytes(16)); } return (string) $_SESSION['okr_csrf']; } function okr_verify_csrf(): void { $sessionToken = $_SESSION['okr_csrf'] ?? ''; $postedToken = $_POST['csrf_token'] ?? ''; if (!is_string($postedToken) || !hash_equals((string) $sessionToken, $postedToken)) { throw new RuntimeException('Security validation failed. Please refresh and try again.'); } } function okr_scope_clause(string $alias = ''): string { $prefix = $alias !== '' ? $alias . '.' : ''; return okr_is_super_admin() ? '1=1' : $prefix . 'organization_slug = :organization_slug'; } function okr_scope_params(array $user): array { return okr_is_super_admin() ? [] : [':organization_slug' => $user['organization_slug']]; } function okr_calculate_score(float $currentValue, float $targetValue): float { if ($targetValue <= 0) { return 0.0; } $score = ($currentValue / $targetValue) * 100; return round(max(0, min(100, $score)), 1); } function okr_badge_class(string $state): string { return match ($state) { 'approved', 'completed', 'active' => 'badge-soft-success', 'pending_manager', 'submitted' => 'badge-soft-warning', 'rejected', 'needs_revision' => 'badge-soft-danger', default => 'badge-soft-neutral', }; } function okr_notification_count(array $user): int { if (!okr_is_approver($user['role'])) { return 0; } $params = okr_scope_params($user); $stmt = db()->prepare('SELECT COUNT(*) FROM okr_items WHERE ' . okr_scope_clause() . ' AND approval_state = :approval_state'); foreach ($params as $key => $value) { $stmt->bindValue($key, $value); } $stmt->bindValue(':approval_state', 'pending_manager'); $stmt->execute(); return (int) $stmt->fetchColumn(); } function okr_ensure_schema(): void { db()->exec(<<<'SQL' CREATE TABLE IF NOT EXISTS okr_items ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, organization_name VARCHAR(120) NOT NULL, organization_slug VARCHAR(120) NOT NULL, owner_name VARCHAR(120) NOT NULL, owner_email VARCHAR(160) NOT NULL, owner_role VARCHAR(40) NOT NULL, department_name VARCHAR(120) NOT NULL, period_name VARCHAR(120) NOT NULL, objective_title VARCHAR(255) NOT NULL, key_result_title VARCHAR(255) NOT NULL, description TEXT NULL, target_value DECIMAL(10,2) NOT NULL DEFAULT 100.00, current_value DECIMAL(10,2) NOT NULL DEFAULT 0.00, score_percent DECIMAL(5,2) NOT NULL DEFAULT 0.00, status VARCHAR(40) NOT NULL DEFAULT 'draft', approval_state VARCHAR(40) NOT NULL DEFAULT 'pending_manager', manager_comment TEXT NULL, created_by_email VARCHAR(160) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_scope (organization_slug, department_name, approval_state), INDEX idx_owner (owner_email, created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci SQL); }