39428-vm/index.php
Flatlogic Bot 765d998fa1 V1
2026-04-01 10:36:51 +00:00

586 lines
35 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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>