586 lines
35 KiB
PHP
586 lines
35 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
require_once __DIR__ . '/includes/okr_app.php';
|
||
|
||
okr_require_schema();
|
||
|
||
function e(string $value): string
|
||
{
|
||
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
|
||
}
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
try {
|
||
okr_verify_csrf();
|
||
$profile = okr_current_profile();
|
||
$action = (string) ($_POST['action_type'] ?? '');
|
||
|
||
switch ($action) {
|
||
case 'switch_profile':
|
||
okr_set_profile((string) ($_POST['profile_key'] ?? 'staff'));
|
||
okr_flash('success', 'Workspace role preview switched successfully.');
|
||
okr_redirect('index.php');
|
||
break;
|
||
|
||
case 'create_okr':
|
||
$entryId = okr_create_entry($_POST, $profile);
|
||
okr_flash('success', 'Objective saved as draft. You can now refine scores or submit it for approval.');
|
||
okr_redirect('okr_detail.php?id=' . $entryId);
|
||
break;
|
||
|
||
case 'submit_okr':
|
||
okr_submit_entry((int) ($_POST['id'] ?? 0), $profile);
|
||
okr_flash('success', 'Objective moved into the approval workflow.');
|
||
okr_redirect((string) ($_POST['redirect_to'] ?? 'index.php'));
|
||
break;
|
||
|
||
case 'delete_okr':
|
||
okr_delete_entry((int) ($_POST['id'] ?? 0), $profile);
|
||
okr_flash('success', 'Draft objective deleted.');
|
||
okr_redirect('index.php');
|
||
break;
|
||
|
||
default:
|
||
throw new RuntimeException('Unknown action.');
|
||
}
|
||
} catch (Throwable $exception) {
|
||
okr_flash('danger', $exception->getMessage());
|
||
okr_redirect('index.php');
|
||
}
|
||
}
|
||
|
||
$profile = okr_current_profile();
|
||
$allEntries = okr_fetch_entries();
|
||
$metrics = okr_dashboard_metrics($allEntries);
|
||
$notifications = okr_collect_notifications($allEntries, 10);
|
||
$reviewQueue = array_values(array_filter($allEntries, static fn(array $entry): bool => $entry['status'] === 'Pending'));
|
||
$myEntries = array_values(array_filter($allEntries, static fn(array $entry): bool => okr_can_edit_owner($entry, okr_current_profile()) || okr_is_admin(okr_current_profile())));
|
||
$departments = $metrics['departments'];
|
||
$flash = okr_flash();
|
||
$projectName = project_name();
|
||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? project_description();
|
||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||
$cssVersion = is_file(__DIR__ . '/assets/css/custom.css') ? (string) filemtime(__DIR__ . '/assets/css/custom.css') : (string) time();
|
||
$jsVersion = is_file(__DIR__ . '/assets/js/main.js') ? (string) filemtime(__DIR__ . '/assets/js/main.js') : (string) time();
|
||
$approvedPercent = $metrics['total'] > 0 ? (int) round(($metrics['approved'] / $metrics['total']) * 100) : 0;
|
||
$pendingPercent = $metrics['total'] > 0 ? (int) round(($metrics['pending'] / $metrics['total']) * 100) : 0;
|
||
$draftPercent = $metrics['total'] > 0 ? max(0, 100 - $approvedPercent - $pendingPercent) : 0;
|
||
$roleLabels = [
|
||
'Staff' => 'Staff',
|
||
'Approver' => 'Approver',
|
||
'Admin' => 'Admin',
|
||
];
|
||
?>
|
||
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title><?= e($projectName) ?> · OKR Workspace</title>
|
||
<?php if ($projectDescription): ?>
|
||
<meta name="description" content="<?= e((string) $projectDescription) ?>" />
|
||
<meta property="og:description" content="<?= e((string) $projectDescription) ?>" />
|
||
<meta property="twitter:description" content="<?= e((string) $projectDescription) ?>" />
|
||
<?php endif; ?>
|
||
<?php if ($projectImageUrl): ?>
|
||
<meta property="og:image" content="<?= e((string) $projectImageUrl) ?>" />
|
||
<meta property="twitter:image" content="<?= e((string) $projectImageUrl) ?>" />
|
||
<?php endif; ?>
|
||
<meta name="theme-color" content="#68BB59" />
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<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=<?= e($cssVersion) ?>">
|
||
</head>
|
||
<body>
|
||
<div class="app-shell">
|
||
<aside class="sidebar-panel">
|
||
<div>
|
||
<a href="index.php" class="brand-mark text-decoration-none">
|
||
<span class="brand-dot"></span>
|
||
<div>
|
||
<strong><?= e($projectName) ?></strong>
|
||
<small>OKR Workspace</small>
|
||
</div>
|
||
</a>
|
||
<p class="sidebar-copy">Simple, seamless objective planning with one-click submission, approval, scoring, and transparent updates.</p>
|
||
</div>
|
||
|
||
<nav class="sidebar-nav" aria-label="Primary navigation">
|
||
<a class="sidebar-link active" href="#dashboard">Dashboard</a>
|
||
<a class="sidebar-link" href="#my-okrs">My OKRs</a>
|
||
<a class="sidebar-link" href="#review-queue">Review Queue</a>
|
||
<a class="sidebar-link" href="#department-okrs">Department OKRs</a>
|
||
<a class="sidebar-link" href="#staff-okrs">Staff OKRs</a>
|
||
<a class="sidebar-link" href="#activity">Notifications</a>
|
||
</nav>
|
||
|
||
<div class="surface-card compact-card mt-3">
|
||
<div class="section-kicker">Workspace role preview</div>
|
||
<h2 class="section-title h6 mb-2">Act as any role in the workflow</h2>
|
||
<form method="post" class="vstack gap-2">
|
||
<input type="hidden" name="csrf_token" value="<?= e(okr_csrf_token()) ?>">
|
||
<input type="hidden" name="action_type" value="switch_profile">
|
||
<label for="profile_key" class="form-label small text-uppercase text-muted mb-0">Current role</label>
|
||
<select class="form-select form-select-sm" id="profile_key" name="profile_key">
|
||
<?php foreach (okr_profiles() as $key => $workspaceProfile): ?>
|
||
<option value="<?= e($key) ?>" <?= $key === $profile['key'] ? 'selected' : '' ?>>
|
||
<?= e($workspaceProfile['name']) ?> · <?= e($workspaceProfile['role']) ?><?= $workspaceProfile['level'] !== $workspaceProfile['role'] ? ' · ' . e($workspaceProfile['level']) : '' ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<button class="btn btn-dark btn-sm" type="submit">Switch preview</button>
|
||
</form>
|
||
<div class="meta-list mt-3">
|
||
<span><strong><?= e($profile['name']) ?></strong></span>
|
||
<span><?= e($profile['department']) ?> · <?= e($roleLabels[$profile['role']] ?? $profile['role']) ?></span>
|
||
<span><?= e($profile['email']) ?></span>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<div class="app-main">
|
||
<header class="topbar">
|
||
<div>
|
||
<div class="section-kicker">Initial MVP slice</div>
|
||
<h1 class="page-title mb-0">One-button OKR workflow</h1>
|
||
</div>
|
||
<div class="topbar-tools">
|
||
<div class="search-shell">
|
||
<label class="visually-hidden" for="globalSearch">Search objectives</label>
|
||
<input id="globalSearch" type="search" class="form-control" placeholder="Search objectives, people, departments" data-search-input>
|
||
</div>
|
||
<a href="#activity" class="notification-pill text-decoration-none">
|
||
Notifications
|
||
<span class="badge text-bg-dark" id="notificationCount"><?= count($notifications) ?></span>
|
||
</a>
|
||
<div class="profile-chip">
|
||
<span class="profile-avatar"><?= e(substr($profile['name'], 0, 1)) ?></span>
|
||
<div>
|
||
<strong><?= e($profile['name']) ?></strong>
|
||
<small><?= e($profile['role']) ?><?= $profile['level'] !== $profile['role'] ? ' · ' . e($profile['level']) : '' ?></small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="app-content">
|
||
<section id="dashboard" class="page-section">
|
||
<div class="hero-card surface-card">
|
||
<div class="hero-copy">
|
||
<div class="section-kicker">Secure workflow preview</div>
|
||
<h2>Draft, submit, score, comment, and notify from one calm workspace.</h2>
|
||
<p>This first delivery gives you a functional OKR path: staff can create objectives and key results, submit once, and approvers can score and approve with comments and visible activity.</p>
|
||
<div class="hero-actions">
|
||
<a href="#create-okr" class="btn btn-success">Create OKR</a>
|
||
<a href="#review-queue" class="btn btn-outline-dark">Open queue</a>
|
||
</div>
|
||
</div>
|
||
<div class="hero-aside">
|
||
<div class="metric-inline">
|
||
<span>Average objective score</span>
|
||
<strong><?= e(number_format($metrics['average_score'], 1)) ?>%</strong>
|
||
</div>
|
||
<div class="metric-inline">
|
||
<span>Approval rate</span>
|
||
<strong><?= e(number_format($metrics['approval_rate'], 1)) ?>%</strong>
|
||
</div>
|
||
<div class="metric-inline">
|
||
<span>Active departments</span>
|
||
<strong><?= e((string) count($departments)) ?></strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stats-grid mt-4">
|
||
<article class="surface-card stat-card">
|
||
<div class="stat-label">Total objectives</div>
|
||
<div class="stat-value"><?= e((string) $metrics['total']) ?></div>
|
||
<div class="progress slim-progress"><div class="progress-bar bg-dark" style="width: 100%"></div></div>
|
||
</article>
|
||
<article class="surface-card stat-card">
|
||
<div class="stat-label">Draft</div>
|
||
<div class="stat-value"><?= e((string) $metrics['draft']) ?></div>
|
||
<div class="progress slim-progress"><div class="progress-bar" style="width: <?= $draftPercent ?>%"></div></div>
|
||
</article>
|
||
<article class="surface-card stat-card">
|
||
<div class="stat-label">Pending approval</div>
|
||
<div class="stat-value"><?= e((string) $metrics['pending']) ?></div>
|
||
<div class="progress slim-progress"><div class="progress-bar bg-warning" style="width: <?= $pendingPercent ?>%"></div></div>
|
||
</article>
|
||
<article class="surface-card stat-card">
|
||
<div class="stat-label">Approved</div>
|
||
<div class="stat-value"><?= e((string) $metrics['approved']) ?></div>
|
||
<div class="progress slim-progress"><div class="progress-bar bg-success" style="width: <?= $approvedPercent ?>%"></div></div>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="page-section" id="workflow-overview">
|
||
<div class="row g-4 align-items-stretch">
|
||
<div class="col-xl-7" id="create-okr">
|
||
<div class="surface-card h-100">
|
||
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
|
||
<div>
|
||
<div class="section-kicker">Create & submit</div>
|
||
<h2 class="section-title">New objective draft</h2>
|
||
</div>
|
||
<span class="status-pill status-draft">Saved as Draft first</span>
|
||
</div>
|
||
<form method="post" class="vstack gap-3">
|
||
<input type="hidden" name="csrf_token" value="<?= e(okr_csrf_token()) ?>">
|
||
<input type="hidden" name="action_type" value="create_okr">
|
||
<div class="row g-3">
|
||
<div class="col-md-8">
|
||
<label class="form-label" for="objective_title">Objective title</label>
|
||
<input class="form-control" id="objective_title" name="objective_title" maxlength="190" placeholder="Increase team execution confidence" required>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label" for="period_label">Period</label>
|
||
<input class="form-control" id="period_label" name="period_label" placeholder="Q2 2026" required>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label" for="owner_name">Owner name</label>
|
||
<input class="form-control" id="owner_name" name="owner_name" maxlength="120" value="<?= e($profile['name']) ?>" required>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label" for="owner_email">Owner email</label>
|
||
<input class="form-control" id="owner_email" type="email" name="owner_email" maxlength="160" value="<?= e($profile['email']) ?>" required>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label" for="department_name">Department</label>
|
||
<input class="form-control" id="department_name" name="department_name" maxlength="120" value="<?= e($profile['department']) ?>" required>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<label class="form-label" for="approver_name">Line manager</label>
|
||
<input class="form-control" id="approver_name" name="approver_name" maxlength="120" placeholder="David Manager" required>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<label class="form-label" for="approver_level">Approver level</label>
|
||
<select class="form-select" id="approver_level" name="approver_level" required>
|
||
<option value="Manager">Manager</option>
|
||
<option value="Director">Director</option>
|
||
<option value="CEO">CEO</option>
|
||
<option value="Team">Team</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="surface-subtle p-3">
|
||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||
<div>
|
||
<div class="section-kicker">Key results</div>
|
||
<div class="small text-muted">Set measurable outcomes. CEO-level approvals will auto-approve on submit.</div>
|
||
</div>
|
||
<button class="btn btn-outline-dark btn-sm" type="button" data-add-key-result>Add key result</button>
|
||
</div>
|
||
<div class="vstack gap-3" id="keyResultsContainer">
|
||
<?php for ($i = 0; $i < 2; $i++): ?>
|
||
<div class="key-result-row">
|
||
<div class="row g-2 align-items-end">
|
||
<div class="col-md-8">
|
||
<label class="form-label">Key result</label>
|
||
<input class="form-control" name="key_result_title[]" placeholder="Ship approval-ready OKR review in under 2 clicks" <?= $i === 0 ? 'required' : '' ?>>
|
||
</div>
|
||
<div class="col-md-3">
|
||
<label class="form-label">Due date</label>
|
||
<input class="form-control" type="date" name="key_result_due[]">
|
||
</div>
|
||
<div class="col-md-1">
|
||
<button class="btn btn-outline-secondary w-100" type="button" data-remove-key-result aria-label="Remove key result">×</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endfor; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
|
||
<div class="small text-muted">Server-side validation, PDO writes, and activity logging are enabled for this workflow slice.</div>
|
||
<button type="submit" class="btn btn-success">Save draft</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="col-xl-5" id="activity">
|
||
<div class="surface-card h-100 d-flex flex-column">
|
||
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
|
||
<div>
|
||
<div class="section-kicker">Live activity</div>
|
||
<h2 class="section-title">Notifications and recent actions</h2>
|
||
</div>
|
||
<span class="notification-pill small">Visible to all users</span>
|
||
</div>
|
||
<div class="activity-list" id="activityFeed" data-feed-url="feed.php">
|
||
<?php if ($notifications === []): ?>
|
||
<div class="empty-state">
|
||
<strong>No notifications yet.</strong>
|
||
<span>Create the first OKR draft to start the activity stream.</span>
|
||
</div>
|
||
<?php else: ?>
|
||
<?php foreach ($notifications as $notification): ?>
|
||
<a href="okr_detail.php?id=<?= (int) $notification['objective_id'] ?>" class="activity-item text-decoration-none">
|
||
<div class="activity-topline">
|
||
<strong><?= e($notification['actor_name']) ?></strong>
|
||
<span><?= e(okr_time_label((string) $notification['time'])) ?></span>
|
||
</div>
|
||
<div class="activity-text"><?= e($notification['message']) ?></div>
|
||
<div class="activity-meta"><?= e($notification['objective_title']) ?></div>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="my-okrs" class="page-section">
|
||
<div class="section-header">
|
||
<div>
|
||
<div class="section-kicker">Personal workspace</div>
|
||
<h2 class="section-title">My OKRs</h2>
|
||
</div>
|
||
<div class="small text-muted">List, detail, submit, and delete draft objectives.</div>
|
||
</div>
|
||
<div class="surface-card table-card">
|
||
<div class="table-responsive">
|
||
<table class="table align-middle app-table" data-search-table>
|
||
<thead>
|
||
<tr>
|
||
<th>Objective</th>
|
||
<th>Department</th>
|
||
<th>Period</th>
|
||
<th>Status</th>
|
||
<th>Score</th>
|
||
<th>Approver</th>
|
||
<th class="text-end">Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if ($myEntries === []): ?>
|
||
<tr>
|
||
<td colspan="7">
|
||
<div class="empty-state compact-empty">
|
||
<strong>No personal OKRs yet.</strong>
|
||
<span>Use the draft form above to create your first objective.</span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php else: ?>
|
||
<?php foreach ($myEntries as $entry): ?>
|
||
<tr>
|
||
<td>
|
||
<strong><?= e($entry['objective_title']) ?></strong>
|
||
<div class="table-subtext"><?= e($entry['key_result_count'] . ' key results') ?></div>
|
||
</td>
|
||
<td><?= e($entry['department_name']) ?></td>
|
||
<td><?= e($entry['period_label']) ?></td>
|
||
<td><span class="status-pill status-<?= strtolower($entry['status']) ?>"><?= e($entry['status']) ?></span></td>
|
||
<td><?= e(number_format((float) $entry['objective_score'], 1)) ?>%</td>
|
||
<td>
|
||
<?= e($entry['approver_name']) ?>
|
||
<div class="table-subtext"><?= e($entry['approver_level']) ?></div>
|
||
</td>
|
||
<td class="text-end">
|
||
<div class="table-actions">
|
||
<a href="okr_detail.php?id=<?= (int) $entry['id'] ?>" class="btn btn-sm btn-outline-dark">View</a>
|
||
<?php if ($entry['status'] === 'Draft'): ?>
|
||
<form method="post" class="d-inline">
|
||
<input type="hidden" name="csrf_token" value="<?= e(okr_csrf_token()) ?>">
|
||
<input type="hidden" name="action_type" value="submit_okr">
|
||
<input type="hidden" name="id" value="<?= (int) $entry['id'] ?>">
|
||
<input type="hidden" name="redirect_to" value="index.php#my-okrs">
|
||
<button type="submit" class="btn btn-sm btn-success">Submit</button>
|
||
</form>
|
||
<form method="post" class="d-inline" onsubmit="return confirm('Delete this draft objective?');">
|
||
<input type="hidden" name="csrf_token" value="<?= e(okr_csrf_token()) ?>">
|
||
<input type="hidden" name="action_type" value="delete_okr">
|
||
<input type="hidden" name="id" value="<?= (int) $entry['id'] ?>">
|
||
<button type="submit" class="btn btn-sm btn-outline-secondary">Delete</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="review-queue" class="page-section">
|
||
<div class="section-header">
|
||
<div>
|
||
<div class="section-kicker">Approval workflow</div>
|
||
<h2 class="section-title">Review queue</h2>
|
||
</div>
|
||
<div class="small text-muted">Approvers can approve, reject, and rescore from the detail screen.</div>
|
||
</div>
|
||
<div class="surface-card table-card">
|
||
<div class="table-responsive">
|
||
<table class="table align-middle app-table" data-search-table>
|
||
<thead>
|
||
<tr>
|
||
<th>Objective</th>
|
||
<th>Owner</th>
|
||
<th>Department</th>
|
||
<th>Submitted</th>
|
||
<th>Approver level</th>
|
||
<th class="text-end">Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if ($reviewQueue === []): ?>
|
||
<tr>
|
||
<td colspan="6">
|
||
<div class="empty-state compact-empty">
|
||
<strong>Queue is clear.</strong>
|
||
<span>Once an objective is submitted it will appear here for line-manager review.</span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php else: ?>
|
||
<?php foreach ($reviewQueue as $entry): ?>
|
||
<tr>
|
||
<td>
|
||
<strong><?= e($entry['objective_title']) ?></strong>
|
||
<div class="table-subtext"><?= e(number_format((float) $entry['objective_score'], 1)) ?>% owner score</div>
|
||
</td>
|
||
<td>
|
||
<?= e($entry['owner_name']) ?>
|
||
<div class="table-subtext"><?= e($entry['owner_email']) ?></div>
|
||
</td>
|
||
<td><?= e($entry['department_name']) ?></td>
|
||
<td><?= e(okr_time_label((string) $entry['submitted_at'])) ?></td>
|
||
<td><?= e($entry['approver_level']) ?></td>
|
||
<td class="text-end">
|
||
<a href="okr_detail.php?id=<?= (int) $entry['id'] ?>" class="btn btn-sm btn-dark">Review</a>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="department-okrs" class="page-section">
|
||
<div class="section-header">
|
||
<div>
|
||
<div class="section-kicker">Department OKRs</div>
|
||
<h2 class="section-title">Portfolio overview by department</h2>
|
||
</div>
|
||
<div class="small text-muted">A thin corporate view to show coverage, approval health, and average scoring.</div>
|
||
</div>
|
||
<div class="row g-3">
|
||
<?php if ($departments === []): ?>
|
||
<div class="col-12">
|
||
<div class="surface-card empty-state compact-empty">
|
||
<strong>No departments yet.</strong>
|
||
<span>Departments will appear automatically as OKRs are created.</span>
|
||
</div>
|
||
</div>
|
||
<?php else: ?>
|
||
<?php foreach ($departments as $department): ?>
|
||
<div class="col-md-6 col-xl-4">
|
||
<article class="surface-card department-card h-100">
|
||
<div class="department-header">
|
||
<strong><?= e($department['name']) ?></strong>
|
||
<span><?= e((string) $department['count']) ?> objectives</span>
|
||
</div>
|
||
<dl class="meta-list meta-grid mt-3">
|
||
<div>
|
||
<dt>Approved</dt>
|
||
<dd><?= e((string) $department['approved']) ?></dd>
|
||
</div>
|
||
<div>
|
||
<dt>Average score</dt>
|
||
<dd><?= e(number_format((float) $department['average_score'], 1)) ?>%</dd>
|
||
</div>
|
||
</dl>
|
||
<div class="progress slim-progress mt-3">
|
||
<div class="progress-bar bg-success" style="width: <?= $department['count'] > 0 ? (int) round(($department['approved'] / $department['count']) * 100) : 0 ?>%"></div>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="staff-okrs" class="page-section">
|
||
<div class="section-header">
|
||
<div>
|
||
<div class="section-kicker">Staff OKRs</div>
|
||
<h2 class="section-title">All objectives</h2>
|
||
</div>
|
||
<div class="small text-muted">Search, scan statuses, and open detail pages from one place.</div>
|
||
</div>
|
||
<div class="surface-card table-card">
|
||
<div class="table-responsive">
|
||
<table class="table align-middle app-table" data-search-table>
|
||
<thead>
|
||
<tr>
|
||
<th>Objective</th>
|
||
<th>Owner</th>
|
||
<th>Status</th>
|
||
<th>Score</th>
|
||
<th>Updated</th>
|
||
<th class="text-end">Detail</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if ($allEntries === []): ?>
|
||
<tr>
|
||
<td colspan="6">
|
||
<div class="empty-state compact-empty">
|
||
<strong>Nothing to review yet.</strong>
|
||
<span>Create the first objective to populate the staff view.</span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php else: ?>
|
||
<?php foreach ($allEntries as $entry): ?>
|
||
<tr>
|
||
<td>
|
||
<strong><?= e($entry['objective_title']) ?></strong>
|
||
<div class="table-subtext"><?= e($entry['department_name']) ?> · <?= e($entry['period_label']) ?></div>
|
||
</td>
|
||
<td><?= e($entry['owner_name']) ?></td>
|
||
<td><span class="status-pill status-<?= strtolower($entry['status']) ?>"><?= e($entry['status']) ?></span></td>
|
||
<td><?= e(number_format((float) $entry['objective_score'], 1)) ?>%</td>
|
||
<td><?= e(okr_time_label((string) $entry['updated_at'])) ?></td>
|
||
<td class="text-end"><a href="okr_detail.php?id=<?= (int) $entry['id'] ?>" class="btn btn-sm btn-outline-dark">Open</a></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<footer class="footer-bar">
|
||
<span>Version MVP-0.1 · OKR workflow slice</span>
|
||
<span>© <?= date('Y') ?> <?= e($projectName) ?></span>
|
||
</footer>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if ($flash): ?>
|
||
<div class="app-flash" data-flash-type="<?= e((string) $flash['type']) ?>" data-flash-message="<?= e((string) $flash['message']) ?>"></div>
|
||
<?php endif; ?>
|
||
|
||
<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=<?= e($jsVersion) ?>" defer></script>
|
||
</body>
|
||
</html>
|