39424-vm/index.php
Flatlogic Bot c04a6c2d66 Version 1
2026-04-01 08:42:52 +00:00

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>