629 lines
34 KiB
PHP
629 lines
34 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/okr_bootstrap.php';
|
|
okr_ensure_schema();
|
|
|
|
if (empty($_SESSION['okr_user'])) {
|
|
require __DIR__ . '/login.php';
|
|
exit;
|
|
}
|
|
|
|
$user = okr_current_user();
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string) ($_POST['action'] ?? '') === 'create_okr') {
|
|
try {
|
|
okr_verify_csrf();
|
|
|
|
$departmentName = trim((string) ($_POST['department_name'] ?? ''));
|
|
$periodName = trim((string) ($_POST['period_name'] ?? ''));
|
|
$objectiveTitle = trim((string) ($_POST['objective_title'] ?? ''));
|
|
$keyResultTitle = trim((string) ($_POST['key_result_title'] ?? ''));
|
|
$description = trim((string) ($_POST['description'] ?? ''));
|
|
$targetValue = (float) ($_POST['target_value'] ?? 0);
|
|
$currentValue = (float) ($_POST['current_value'] ?? 0);
|
|
|
|
$errors = [];
|
|
if ($departmentName === '') {
|
|
$errors[] = 'Department is required.';
|
|
}
|
|
if ($periodName === '') {
|
|
$errors[] = 'OKR period is required.';
|
|
}
|
|
if ($objectiveTitle === '' || strlen($objectiveTitle) < 8) {
|
|
$errors[] = 'Objective title must be at least 8 characters.';
|
|
}
|
|
if ($keyResultTitle === '' || strlen($keyResultTitle) < 8) {
|
|
$errors[] = 'Key result title must be at least 8 characters.';
|
|
}
|
|
if ($targetValue <= 0) {
|
|
$errors[] = 'Target value must be greater than 0.';
|
|
}
|
|
if ($currentValue < 0) {
|
|
$errors[] = 'Current value cannot be negative.';
|
|
}
|
|
|
|
if ($errors !== []) {
|
|
throw new RuntimeException(implode(' ', $errors));
|
|
}
|
|
|
|
$scorePercent = okr_calculate_score($currentValue, $targetValue);
|
|
$approvalState = okr_is_approver($user['role']) ? 'approved' : 'pending_manager';
|
|
$status = $approvalState === 'approved' ? ($scorePercent >= 100 ? 'completed' : 'active') : 'submitted';
|
|
$managerComment = $approvalState === 'approved' ? 'Auto-approved on submission by leadership role.' : null;
|
|
|
|
$stmt = db()->prepare(
|
|
'INSERT INTO okr_items (
|
|
organization_name,
|
|
organization_slug,
|
|
owner_name,
|
|
owner_email,
|
|
owner_role,
|
|
department_name,
|
|
period_name,
|
|
objective_title,
|
|
key_result_title,
|
|
description,
|
|
target_value,
|
|
current_value,
|
|
score_percent,
|
|
status,
|
|
approval_state,
|
|
manager_comment,
|
|
created_by_email
|
|
) VALUES (
|
|
:organization_name,
|
|
:organization_slug,
|
|
:owner_name,
|
|
:owner_email,
|
|
:owner_role,
|
|
:department_name,
|
|
:period_name,
|
|
:objective_title,
|
|
:key_result_title,
|
|
:description,
|
|
:target_value,
|
|
:current_value,
|
|
:score_percent,
|
|
:status,
|
|
:approval_state,
|
|
:manager_comment,
|
|
:created_by_email
|
|
)'
|
|
);
|
|
$stmt->execute([
|
|
':organization_name' => $user['organization_name'],
|
|
':organization_slug' => $user['organization_slug'],
|
|
':owner_name' => $user['name'],
|
|
':owner_email' => $user['email'],
|
|
':owner_role' => $user['role'],
|
|
':department_name' => $departmentName,
|
|
':period_name' => $periodName,
|
|
':objective_title' => $objectiveTitle,
|
|
':key_result_title' => $keyResultTitle,
|
|
':description' => $description !== '' ? $description : null,
|
|
':target_value' => $targetValue,
|
|
':current_value' => $currentValue,
|
|
':score_percent' => $scorePercent,
|
|
':status' => $status,
|
|
':approval_state' => $approvalState,
|
|
':manager_comment' => $managerComment,
|
|
':created_by_email' => $user['email'],
|
|
]);
|
|
|
|
okr_flash('success', 'Objective created and routed into the workflow.');
|
|
header('Location: index.php#my-okrs');
|
|
exit;
|
|
} catch (Throwable $exception) {
|
|
okr_flash('danger', $exception->getMessage());
|
|
header('Location: index.php#my-okrs');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
$projectName = okr_app_name();
|
|
$projectDescription = okr_meta_description();
|
|
$projectImageUrl = env_value('PROJECT_IMAGE_URL');
|
|
$flash = okr_pull_flash();
|
|
$csrfToken = okr_csrf_token();
|
|
$scopeParams = okr_scope_params($user);
|
|
$scopeClause = okr_scope_clause();
|
|
|
|
$summaryStmt = db()->prepare(
|
|
'SELECT
|
|
COUNT(*) AS total_items,
|
|
SUM(approval_state = "pending_manager") AS pending_items,
|
|
SUM(approval_state = "approved") AS approved_items,
|
|
ROUND(COALESCE(AVG(score_percent), 0), 1) AS average_score,
|
|
SUM(status = "completed") AS completed_items
|
|
FROM okr_items
|
|
WHERE ' . $scopeClause
|
|
);
|
|
foreach ($scopeParams as $key => $value) {
|
|
$summaryStmt->bindValue($key, $value);
|
|
}
|
|
$summaryStmt->execute();
|
|
$summary = $summaryStmt->fetch() ?: ['total_items' => 0, 'pending_items' => 0, 'approved_items' => 0, 'average_score' => 0, 'completed_items' => 0];
|
|
|
|
$recentStmt = db()->prepare(
|
|
'SELECT id, organization_name, owner_name, owner_role, department_name, objective_title, score_percent, approval_state, updated_at
|
|
FROM okr_items
|
|
WHERE ' . $scopeClause . '
|
|
ORDER BY updated_at DESC
|
|
LIMIT 6'
|
|
);
|
|
foreach ($scopeParams as $key => $value) {
|
|
$recentStmt->bindValue($key, $value);
|
|
}
|
|
$recentStmt->execute();
|
|
$recentItems = $recentStmt->fetchAll();
|
|
|
|
$listStmt = db()->prepare(
|
|
'SELECT id, owner_name, owner_role, department_name, period_name, objective_title, key_result_title, score_percent, status, approval_state, updated_at
|
|
FROM okr_items
|
|
WHERE ' . $scopeClause . '
|
|
ORDER BY created_at DESC
|
|
LIMIT 24'
|
|
);
|
|
foreach ($scopeParams as $key => $value) {
|
|
$listStmt->bindValue($key, $value);
|
|
}
|
|
$listStmt->execute();
|
|
$okrItems = $listStmt->fetchAll();
|
|
|
|
$myStmt = db()->prepare(
|
|
'SELECT id, objective_title, key_result_title, score_percent, approval_state, updated_at
|
|
FROM okr_items
|
|
WHERE ' . $scopeClause . ' AND owner_email = :owner_email
|
|
ORDER BY created_at DESC
|
|
LIMIT 6'
|
|
);
|
|
foreach ($scopeParams as $key => $value) {
|
|
$myStmt->bindValue($key, $value);
|
|
}
|
|
$myStmt->bindValue(':owner_email', $user['email']);
|
|
$myStmt->execute();
|
|
$myItems = $myStmt->fetchAll();
|
|
|
|
$approvalInbox = [];
|
|
if (okr_is_approver($user['role'])) {
|
|
$approvalStmt = db()->prepare(
|
|
'SELECT id, owner_name, department_name, objective_title, key_result_title, score_percent, updated_at
|
|
FROM okr_items
|
|
WHERE ' . $scopeClause . ' AND approval_state = :approval_state
|
|
ORDER BY updated_at DESC
|
|
LIMIT 5'
|
|
);
|
|
foreach ($scopeParams as $key => $value) {
|
|
$approvalStmt->bindValue($key, $value);
|
|
}
|
|
$approvalStmt->bindValue(':approval_state', 'pending_manager');
|
|
$approvalStmt->execute();
|
|
$approvalInbox = $approvalStmt->fetchAll();
|
|
}
|
|
|
|
$departmentStmt = db()->prepare(
|
|
'SELECT department_name, COUNT(*) AS item_count, ROUND(COALESCE(AVG(score_percent), 0), 1) AS department_score
|
|
FROM okr_items
|
|
WHERE ' . $scopeClause . '
|
|
GROUP BY department_name
|
|
ORDER BY item_count DESC, department_name ASC
|
|
LIMIT 4'
|
|
);
|
|
foreach ($scopeParams as $key => $value) {
|
|
$departmentStmt->bindValue($key, $value);
|
|
}
|
|
$departmentStmt->execute();
|
|
$departmentRows = $departmentStmt->fetchAll();
|
|
|
|
$pendingCount = okr_notification_count($user);
|
|
$completionRate = ((int) ($summary['total_items'] ?? 0)) > 0 ? round(((int) ($summary['completed_items'] ?? 0) / (int) $summary['total_items']) * 100) : 0;
|
|
?>
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title><?= e($projectName) ?> · Workspace</title>
|
|
<meta name="description" content="<?= e($projectDescription) ?>">
|
|
<?php if ($projectDescription !== ''): ?>
|
|
<meta property="og:description" content="<?= e($projectDescription) ?>">
|
|
<meta property="twitter:description" content="<?= e($projectDescription) ?>">
|
|
<?php endif; ?>
|
|
<?php if ($projectImageUrl !== ''): ?>
|
|
<meta property="og:image" content="<?= e($projectImageUrl) ?>">
|
|
<meta property="twitter:image" content="<?= e($projectImageUrl) ?>">
|
|
<?php endif; ?>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
|
|
</head>
|
|
<body class="app-shell">
|
|
<div class="app-layout">
|
|
<aside class="sidebar border-end" id="sidebarMenu">
|
|
<div class="sidebar-top">
|
|
<a href="index.php" class="brand-mark">Aligned OKR</a>
|
|
<div class="small text-secondary mt-2">Tenant: <?= e($user['organization_name']) ?></div>
|
|
</div>
|
|
<nav class="nav flex-column gap-1 sidebar-nav">
|
|
<a class="nav-link active" href="#dashboard">Dashboard</a>
|
|
<a class="nav-link" href="#corporate-okrs">Corporate OKRs</a>
|
|
<a class="nav-link" href="#department-okrs">Department OKRs</a>
|
|
<a class="nav-link" href="#staff-okrs">Staff OKRs</a>
|
|
<a class="nav-link" href="#my-okrs">My OKRs</a>
|
|
</nav>
|
|
<div class="surface-card p-3 mt-auto">
|
|
<div class="small text-secondary mb-2">Current access</div>
|
|
<div class="fw-semibold"><?= e($user['role']) ?></div>
|
|
<div class="small text-secondary"><?= e($user['email']) ?></div>
|
|
<div class="small text-secondary">Org key: <?= e($user['organization_slug']) ?></div>
|
|
</div>
|
|
</aside>
|
|
|
|
<div class="main-panel">
|
|
<header class="topbar border-bottom">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<button class="btn btn-outline-secondary btn-sm d-lg-none" type="button" id="sidebarToggle">Menu</button>
|
|
<div>
|
|
<div class="small text-secondary">Operational strategy workspace</div>
|
|
<h1 class="h4 mb-0">Dashboard</h1>
|
|
</div>
|
|
</div>
|
|
<div class="topbar-actions">
|
|
<div class="search-wrap">
|
|
<input type="search" id="tableSearch" class="form-control" placeholder="Search OKRs, people, departments">
|
|
</div>
|
|
<button class="btn btn-notify position-relative" type="button" data-bs-toggle="offcanvas" data-bs-target="#notificationsDrawer" aria-controls="notificationsDrawer">
|
|
Notifications
|
|
<?php if ($pendingCount > 0): ?>
|
|
<span class="badge rounded-pill text-bg-dark notification-badge"><?= e((string) $pendingCount) ?></span>
|
|
<?php endif; ?>
|
|
</button>
|
|
<div class="dropdown">
|
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
<?= e($user['name']) ?>
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
|
|
<li><span class="dropdown-item-text small text-secondary"><?= e($user['organization_name']) ?></span></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li>
|
|
<form method="post" action="logout.php" class="px-2">
|
|
<button class="btn btn-sm btn-outline-danger w-100" type="submit">Log out</button>
|
|
</form>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="content-area">
|
|
<section class="hero-panel surface-card" id="dashboard">
|
|
<div>
|
|
<p class="tiny-label mb-2">First MVP delivery</p>
|
|
<h2 class="h3 mb-2">A working OKR workflow for one tenant-aware organization at a time.</h2>
|
|
<p class="text-secondary mb-0">Create a personal objective, view organization-wide progress, and move items through manager review with auto-scored key results.</p>
|
|
</div>
|
|
<div class="hero-meta-grid">
|
|
<div>
|
|
<div class="small text-secondary">Workspace</div>
|
|
<div class="fw-semibold"><?= e($user['organization_name']) ?></div>
|
|
</div>
|
|
<div>
|
|
<div class="small text-secondary">Role</div>
|
|
<div class="fw-semibold"><?= e($user['role']) ?></div>
|
|
</div>
|
|
<div>
|
|
<div class="small text-secondary">Version</div>
|
|
<div class="fw-semibold">0.1 MVP</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<?php if ($flash): ?>
|
|
<div class="toast-stack">
|
|
<div class="alert alert-<?= e($flash['type']) ?> border-0 shadow-sm" role="alert" data-auto-dismiss="true">
|
|
<?= e($flash['message']) ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<section class="stats-grid mt-4">
|
|
<article class="metric-card surface-card">
|
|
<div class="small text-secondary">Total OKRs</div>
|
|
<div class="metric-value"><?= e((string) ($summary['total_items'] ?? 0)) ?></div>
|
|
<div class="small text-secondary">Scoped to <?= okr_is_super_admin() ? 'all organizations' : 'your organization' ?></div>
|
|
</article>
|
|
<article class="metric-card surface-card">
|
|
<div class="small text-secondary">Pending approvals</div>
|
|
<div class="metric-value"><?= e((string) ($summary['pending_items'] ?? 0)) ?></div>
|
|
<div class="small text-secondary">Queue for line-manager review</div>
|
|
</article>
|
|
<article class="metric-card surface-card">
|
|
<div class="small text-secondary">Approved items</div>
|
|
<div class="metric-value"><?= e((string) ($summary['approved_items'] ?? 0)) ?></div>
|
|
<div class="small text-secondary">Includes leadership auto-approvals</div>
|
|
</article>
|
|
<article class="metric-card surface-card">
|
|
<div class="small text-secondary">Average score</div>
|
|
<div class="metric-value"><?= e((string) ($summary['average_score'] ?? 0)) ?>%</div>
|
|
<div class="small text-secondary">Calculated from key result progress</div>
|
|
</article>
|
|
</section>
|
|
|
|
<section class="row g-4 mt-1">
|
|
<div class="col-xl-8">
|
|
<div class="surface-card h-100" id="corporate-okrs">
|
|
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-4">
|
|
<div>
|
|
<p class="tiny-label mb-2">Corporate OKRs</p>
|
|
<h2 class="h5 mb-1">Recent strategic objectives</h2>
|
|
<p class="small text-secondary mb-0">A compact hierarchical view of the latest objectives and key results in scope.</p>
|
|
</div>
|
|
<span class="small text-secondary">Use the search field in the header to filter all table rows.</span>
|
|
</div>
|
|
<?php if ($recentItems === []): ?>
|
|
<div class="empty-state border rounded-3 p-4 text-center">
|
|
<div class="fw-semibold mb-2">No OKRs yet</div>
|
|
<p class="small text-secondary mb-0">Create your first objective in the My OKRs section to populate the dashboard.</p>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="vstack gap-3">
|
|
<?php foreach ($recentItems as $item): ?>
|
|
<a class="list-row text-decoration-none" href="okr_detail.php?id=<?= e((string) $item['id']) ?>">
|
|
<div>
|
|
<div class="fw-semibold text-dark"><?= e($item['objective_title']) ?></div>
|
|
<div class="small text-secondary"><?= e($item['owner_name']) ?> · <?= e($item['owner_role']) ?> · <?= e($item['department_name']) ?></div>
|
|
</div>
|
|
<div class="text-end">
|
|
<span class="badge <?= e(okr_badge_class((string) $item['approval_state'])) ?> mb-2"><?= e($item['approval_state']) ?></span>
|
|
<div class="small fw-semibold"><?= e((string) $item['score_percent']) ?>%</div>
|
|
</div>
|
|
</a>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-4">
|
|
<div class="surface-card h-100" id="department-okrs">
|
|
<p class="tiny-label mb-2">Department OKRs</p>
|
|
<h2 class="h5 mb-3">Distribution by department</h2>
|
|
<?php if ($departmentRows === []): ?>
|
|
<p class="small text-secondary mb-0">Department insights appear after your team creates records.</p>
|
|
<?php else: ?>
|
|
<div class="vstack gap-3">
|
|
<?php foreach ($departmentRows as $departmentRow): ?>
|
|
<div>
|
|
<div class="d-flex justify-content-between small mb-1">
|
|
<span class="fw-semibold text-dark"><?= e($departmentRow['department_name']) ?></span>
|
|
<span class="text-secondary"><?= e((string) $departmentRow['item_count']) ?> OKRs</span>
|
|
</div>
|
|
<div class="progress thin-progress" role="progressbar" aria-valuenow="<?= e((string) $departmentRow['department_score']) ?>" aria-valuemin="0" aria-valuemax="100">
|
|
<div class="progress-bar bg-success" style="width: <?= e((string) $departmentRow['department_score']) ?>%"></div>
|
|
</div>
|
|
<div class="small text-secondary mt-1">Average score <?= e((string) $departmentRow['department_score']) ?>%</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<hr>
|
|
<p class="tiny-label mb-2" id="staff-okrs">Staff OKRs</p>
|
|
<h3 class="h6 mb-2">Workflow completion</h3>
|
|
<div class="progress large-progress mb-2" role="progressbar" aria-valuenow="<?= e((string) $completionRate) ?>" aria-valuemin="0" aria-valuemax="100">
|
|
<div class="progress-bar bg-dark" style="width: <?= e((string) $completionRate) ?>%"></div>
|
|
</div>
|
|
<div class="small text-secondary"><?= e((string) $completionRate) ?>% of in-scope objectives are completed.</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="row g-4 mt-1">
|
|
<div class="col-xl-7">
|
|
<div class="surface-card h-100" id="my-okrs">
|
|
<div class="d-flex justify-content-between align-items-start gap-3 mb-4 flex-wrap">
|
|
<div>
|
|
<p class="tiny-label mb-2">My OKRs</p>
|
|
<h2 class="h5 mb-1">Create a new objective</h2>
|
|
<p class="small text-secondary mb-0">This thin slice covers create → confirmation → list → detail → approval.</p>
|
|
</div>
|
|
<div class="small text-secondary">All writes use PDO prepared statements.</div>
|
|
</div>
|
|
<form method="post" class="row g-3" id="okrCreateForm">
|
|
<input type="hidden" name="action" value="create_okr">
|
|
<input type="hidden" name="csrf_token" value="<?= e($csrfToken) ?>">
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="department_name">Department</label>
|
|
<input class="form-control" id="department_name" name="department_name" type="text" placeholder="Revenue Operations" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" for="period_name">OKR period</label>
|
|
<input class="form-control" id="period_name" name="period_name" type="text" placeholder="Q2 2026" required>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label" for="objective_title">Objective</label>
|
|
<input class="form-control" id="objective_title" name="objective_title" type="text" placeholder="Improve enterprise expansion revenue quality" required>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label" for="key_result_title">Key result</label>
|
|
<input class="form-control" id="key_result_title" name="key_result_title" type="text" placeholder="Increase qualified pipeline conversion from 21% to 33%" required>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label" for="description">Notes</label>
|
|
<textarea class="form-control" id="description" name="description" rows="4" placeholder="Add success criteria, dependencies, and any approval notes."></textarea>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" for="target_value">Target value</label>
|
|
<input class="form-control js-score-target" id="target_value" name="target_value" type="number" min="1" step="0.1" value="100" required>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" for="current_value">Current value</label>
|
|
<input class="form-control js-score-current" id="current_value" name="current_value" type="number" min="0" step="0.1" value="0" required>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Projected score</label>
|
|
<div class="score-preview surface-muted">
|
|
<strong class="js-score-output">0%</strong>
|
|
<span class="small text-secondary">Calculated automatically</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 d-flex gap-2 flex-wrap">
|
|
<button class="btn btn-brand" type="submit">Create objective</button>
|
|
<a class="btn btn-outline-secondary" href="#okr-table">View current records</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="col-xl-5">
|
|
<div class="surface-card h-100">
|
|
<p class="tiny-label mb-2">Approval inbox</p>
|
|
<h2 class="h5 mb-3">Items waiting for review</h2>
|
|
<?php if (!okr_is_approver($user['role'])): ?>
|
|
<div class="alert alert-light border small mb-0">Only Manager, Director, CEO, Admin, and Super Admin roles can approve or reject submitted OKRs in this first release.</div>
|
|
<?php elseif ($approvalInbox === []): ?>
|
|
<div class="empty-state border rounded-3 p-4 text-center">
|
|
<div class="fw-semibold mb-2">Inbox is clear</div>
|
|
<p class="small text-secondary mb-0">Pending approvals will appear here as staff submit new OKRs.</p>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="vstack gap-3">
|
|
<?php foreach ($approvalInbox as $pending): ?>
|
|
<div class="surface-muted p-3 rounded-3">
|
|
<div class="d-flex justify-content-between gap-3">
|
|
<div>
|
|
<div class="fw-semibold"><?= e($pending['objective_title']) ?></div>
|
|
<div class="small text-secondary"><?= e($pending['owner_name']) ?> · <?= e($pending['department_name']) ?></div>
|
|
</div>
|
|
<div class="text-end small">
|
|
<div class="fw-semibold"><?= e((string) $pending['score_percent']) ?>%</div>
|
|
<div class="text-secondary">Current score</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<a class="btn btn-sm btn-outline-dark" href="okr_detail.php?id=<?= e((string) $pending['id']) ?>">Review item</a>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="surface-card mt-4" id="okr-table">
|
|
<div class="d-flex justify-content-between align-items-start gap-3 mb-4 flex-wrap">
|
|
<div>
|
|
<p class="tiny-label mb-2">Shared list</p>
|
|
<h2 class="h5 mb-1">In-scope OKR records</h2>
|
|
<p class="small text-secondary mb-0">Each record opens a detail page for approvals, comments, and score updates.</p>
|
|
</div>
|
|
<div class="small text-secondary">Showing up to 24 most recent records.</div>
|
|
</div>
|
|
<?php if ($okrItems === []): ?>
|
|
<div class="empty-state border rounded-3 p-5 text-center">
|
|
<div class="fw-semibold mb-2">Your workspace is ready for the first OKR</div>
|
|
<p class="small text-secondary mb-0">Create one above to activate the dashboard, approval inbox, and analytics cards.</p>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="table-responsive">
|
|
<table class="table align-middle mb-0" id="okrTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Owner</th>
|
|
<th>Department</th>
|
|
<th>Objective</th>
|
|
<th>Score</th>
|
|
<th>Status</th>
|
|
<th class="text-end">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($okrItems as $item): ?>
|
|
<tr class="js-search-row">
|
|
<td>
|
|
<div class="fw-semibold"><?= e($item['owner_name']) ?></div>
|
|
<div class="small text-secondary"><?= e($item['owner_role']) ?></div>
|
|
</td>
|
|
<td>
|
|
<div><?= e($item['department_name']) ?></div>
|
|
<div class="small text-secondary"><?= e($item['period_name']) ?></div>
|
|
</td>
|
|
<td>
|
|
<div class="fw-semibold"><?= e($item['objective_title']) ?></div>
|
|
<div class="small text-secondary"><?= e($item['key_result_title']) ?></div>
|
|
</td>
|
|
<td>
|
|
<div class="fw-semibold"><?= e((string) $item['score_percent']) ?>%</div>
|
|
<div class="small text-secondary">Updated <?= e(date('M j', strtotime((string) $item['updated_at']))) ?></div>
|
|
</td>
|
|
<td>
|
|
<span class="badge <?= e(okr_badge_class((string) $item['approval_state'])) ?> mb-2"><?= e($item['approval_state']) ?></span>
|
|
<div class="small text-secondary text-capitalize"><?= e($item['status']) ?></div>
|
|
</td>
|
|
<td class="text-end">
|
|
<a class="btn btn-sm btn-outline-dark" href="okr_detail.php?id=<?= e((string) $item['id']) ?>">Open</a>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
</section>
|
|
|
|
<section class="surface-card mt-4">
|
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
|
|
<div>
|
|
<p class="tiny-label mb-2">Personal queue</p>
|
|
<h2 class="h5 mb-1">Your latest submissions</h2>
|
|
<p class="small text-secondary mb-0">Quick access to your own items inside the current organization scope.</p>
|
|
</div>
|
|
<a class="btn btn-outline-secondary btn-sm" href="#my-okrs">Create another</a>
|
|
</div>
|
|
<div class="row g-3 mt-1">
|
|
<?php if ($myItems === []): ?>
|
|
<div class="col-12">
|
|
<div class="empty-state border rounded-3 p-4 text-center small text-secondary">No personal OKRs created yet in this workspace.</div>
|
|
</div>
|
|
<?php else: ?>
|
|
<?php foreach ($myItems as $item): ?>
|
|
<div class="col-md-6 col-xl-4">
|
|
<a class="surface-muted p-3 rounded-3 h-100 d-block text-decoration-none text-dark" href="okr_detail.php?id=<?= e((string) $item['id']) ?>">
|
|
<div class="fw-semibold mb-2"><?= e($item['objective_title']) ?></div>
|
|
<div class="small text-secondary mb-3"><?= e($item['key_result_title']) ?></div>
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span class="badge <?= e(okr_badge_class((string) $item['approval_state'])) ?>"><?= e($item['approval_state']) ?></span>
|
|
<span class="fw-semibold"><?= e((string) $item['score_percent']) ?>%</span>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<footer class="footer-bar border-top">
|
|
<div>Aligned OKR Cloud · Version 0.1 MVP</div>
|
|
<div>© <?= e(date('Y')) ?> <?= e($user['organization_name']) ?> workspace</div>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="offcanvas offcanvas-end" tabindex="-1" id="notificationsDrawer" aria-labelledby="notificationsDrawerLabel">
|
|
<div class="offcanvas-header">
|
|
<h2 class="offcanvas-title h5" id="notificationsDrawerLabel">Notifications</h2>
|
|
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
|
</div>
|
|
<div class="offcanvas-body">
|
|
<div class="surface-muted rounded-3 p-3 mb-3">
|
|
<div class="fw-semibold mb-1">Approval workload</div>
|
|
<div class="small text-secondary"><?= e((string) $pendingCount) ?> item(s) are currently waiting for a line manager or leadership decision.</div>
|
|
</div>
|
|
<div class="small text-secondary">This initial delivery uses lightweight refreshes and contextual alerts. Real-time comment streams and richer notifications can be layered onto the same workflow next.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
|
<script src="assets/js/main.js?v=<?= time() ?>" defer></script>
|
|
</body>
|
|
</html>
|