177 lines
4.9 KiB
PHP
177 lines
4.9 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
@date_default_timezone_set('UTC');
|
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
|
session_start();
|
|
}
|
|
|
|
require_once __DIR__ . '/db/config.php';
|
|
|
|
const OKR_ROLES = ['Super Admin', 'Admin', 'CEO', 'Director', 'Manager', 'Team', 'Staff'];
|
|
const OKR_APPROVER_ROLES = ['Super Admin', 'Admin', 'CEO', 'Director', 'Manager'];
|
|
|
|
function env_value(string $key, string $default = ''): string
|
|
{
|
|
$serverValue = $_SERVER[$key] ?? null;
|
|
if (is_string($serverValue) && $serverValue !== '') {
|
|
return $serverValue;
|
|
}
|
|
|
|
$envValue = getenv($key);
|
|
return is_string($envValue) && $envValue !== '' ? $envValue : $default;
|
|
}
|
|
|
|
function e(mixed $value): string
|
|
{
|
|
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
|
}
|
|
|
|
function okr_app_name(): string
|
|
{
|
|
return env_value('PROJECT_NAME', 'Aligned OKR Cloud');
|
|
}
|
|
|
|
function okr_meta_description(): string
|
|
{
|
|
return env_value('PROJECT_DESCRIPTION', 'Multi-tenant OKR workspace for strategy, approvals, and score tracking.');
|
|
}
|
|
|
|
function okr_roles(): array
|
|
{
|
|
return OKR_ROLES;
|
|
}
|
|
|
|
function okr_is_approver(string $role): bool
|
|
{
|
|
return in_array($role, OKR_APPROVER_ROLES, true);
|
|
}
|
|
|
|
function okr_is_super_admin(): bool
|
|
{
|
|
return (($_SESSION['okr_user']['role'] ?? '') === 'Super Admin');
|
|
}
|
|
|
|
function okr_current_user(): array
|
|
{
|
|
if (empty($_SESSION['okr_user']) || !is_array($_SESSION['okr_user'])) {
|
|
header('Location: login.php');
|
|
exit;
|
|
}
|
|
|
|
return $_SESSION['okr_user'];
|
|
}
|
|
|
|
function okr_flash(string $type, string $message): void
|
|
{
|
|
$_SESSION['okr_flash'] = [
|
|
'type' => $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);
|
|
}
|