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

250 lines
13 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/okr_bootstrap.php';
okr_ensure_schema();
$user = okr_current_user();
$scopeClause = okr_scope_clause();
$scopeParams = okr_scope_params($user);
$id = (int) ($_GET['id'] ?? 0);
if ($id <= 0) {
okr_flash('danger', 'Select a valid OKR record.');
header('Location: index.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && (string) ($_POST['action'] ?? '') === 'review_okr') {
try {
okr_verify_csrf();
if (!okr_is_approver($user['role'])) {
throw new RuntimeException('Your current role cannot approve or reject OKRs in this release.');
}
$decision = trim((string) ($_POST['decision'] ?? 'update'));
$currentValue = (float) ($_POST['current_value'] ?? 0);
$targetValue = (float) ($_POST['target_value'] ?? 0);
$managerComment = trim((string) ($_POST['manager_comment'] ?? ''));
if ($targetValue <= 0) {
throw new RuntimeException('Target value must stay above 0.');
}
if ($currentValue < 0) {
throw new RuntimeException('Current value cannot be negative.');
}
$scorePercent = okr_calculate_score($currentValue, $targetValue);
$approvalState = 'pending_manager';
$status = 'submitted';
if ($decision === 'approve') {
$approvalState = 'approved';
$status = $scorePercent >= 100 ? 'completed' : 'active';
if ($managerComment === '') {
$managerComment = 'Approved and scored by ' . $user['role'] . '.';
}
} elseif ($decision === 'reject') {
$approvalState = 'rejected';
$status = 'needs_revision';
if ($managerComment === '') {
$managerComment = 'Rejected with feedback from ' . $user['role'] . '.';
}
} elseif ($managerComment === '') {
$managerComment = 'Progress updated by ' . $user['role'] . '.';
}
$sql = 'UPDATE okr_items SET current_value = :current_value, target_value = :target_value, score_percent = :score_percent, approval_state = :approval_state, status = :status, manager_comment = :manager_comment WHERE id = :id AND ' . $scopeClause;
$stmt = db()->prepare($sql);
$stmt->bindValue(':current_value', $currentValue);
$stmt->bindValue(':target_value', $targetValue);
$stmt->bindValue(':score_percent', $scorePercent);
$stmt->bindValue(':approval_state', $approvalState);
$stmt->bindValue(':status', $status);
$stmt->bindValue(':manager_comment', $managerComment);
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
foreach ($scopeParams as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->execute();
okr_flash('success', 'OKR updated successfully.');
header('Location: okr_detail.php?id=' . $id);
exit;
} catch (Throwable $exception) {
okr_flash('danger', $exception->getMessage());
header('Location: okr_detail.php?id=' . $id);
exit;
}
}
$sql = 'SELECT * FROM okr_items WHERE id = :id AND ' . $scopeClause . ' LIMIT 1';
$stmt = db()->prepare($sql);
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
foreach ($scopeParams as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->execute();
$item = $stmt->fetch();
if (!$item) {
okr_flash('danger', 'That OKR could not be found in your current scope.');
header('Location: index.php');
exit;
}
$projectName = okr_app_name();
$projectDescription = okr_meta_description();
$projectImageUrl = env_value('PROJECT_IMAGE_URL');
$flash = okr_pull_flash();
$csrfToken = okr_csrf_token();
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= e($projectName) ?> · OKR detail</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="detail-shell">
<div class="container py-4 py-lg-5">
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap mb-4">
<div>
<p class="tiny-label mb-2">OKR detail</p>
<h1 class="h3 mb-1"><?= e($item['objective_title']) ?></h1>
<p class="text-secondary mb-0"><?= e($item['organization_name']) ?> · <?= e($item['department_name']) ?> · <?= e($item['period_name']) ?></p>
</div>
<div class="d-flex gap-2 flex-wrap">
<a class="btn btn-outline-secondary" href="index.php">Back to workspace</a>
<form method="post" action="logout.php">
<button class="btn btn-outline-danger" type="submit">Log out</button>
</form>
</div>
</div>
<?php if ($flash): ?>
<div class="alert alert-<?= e($flash['type']) ?> border-0 shadow-sm" role="alert">
<?= e($flash['message']) ?>
</div>
<?php endif; ?>
<div class="row g-4">
<div class="col-xl-8">
<section class="surface-card h-100">
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap mb-4">
<div>
<p class="tiny-label mb-2">Key result</p>
<h2 class="h5 mb-1"><?= e($item['key_result_title']) ?></h2>
<p class="small text-secondary mb-0">Owner: <?= e($item['owner_name']) ?> · <?= e($item['owner_role']) ?> · <?= e($item['owner_email']) ?></p>
</div>
<span class="badge <?= e(okr_badge_class((string) $item['approval_state'])) ?>"><?= e($item['approval_state']) ?></span>
</div>
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="surface-muted p-3 rounded-3 h-100">
<div class="small text-secondary mb-1">Current score</div>
<div class="h4 mb-0"><?= e((string) $item['score_percent']) ?>%</div>
</div>
</div>
<div class="col-md-4">
<div class="surface-muted p-3 rounded-3 h-100">
<div class="small text-secondary mb-1">Current value</div>
<div class="h4 mb-0"><?= e((string) $item['current_value']) ?></div>
</div>
</div>
<div class="col-md-4">
<div class="surface-muted p-3 rounded-3 h-100">
<div class="small text-secondary mb-1">Target value</div>
<div class="h4 mb-0"><?= e((string) $item['target_value']) ?></div>
</div>
</div>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between small mb-1">
<span class="fw-semibold text-dark">Progress to target</span>
<span class="text-secondary"><?= e((string) $item['score_percent']) ?>%</span>
</div>
<div class="progress large-progress" role="progressbar" aria-valuenow="<?= e((string) $item['score_percent']) ?>" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar bg-success" style="width: <?= e((string) $item['score_percent']) ?>%"></div>
</div>
</div>
<div class="mb-4">
<div class="small text-secondary mb-2">Objective notes</div>
<div class="surface-muted rounded-3 p-3 small">
<?= nl2br(e($item['description'] ?: 'No additional notes supplied.')) ?>
</div>
</div>
<div>
<div class="small text-secondary mb-2">Latest reviewer comment</div>
<div class="surface-muted rounded-3 p-3 small">
<?= nl2br(e($item['manager_comment'] ?: 'No comments yet.')) ?>
</div>
</div>
</section>
</div>
<div class="col-xl-4">
<section class="surface-card mb-4">
<p class="tiny-label mb-2">Approval workflow</p>
<h2 class="h5 mb-3">Review and score</h2>
<form method="post" class="row g-3" id="okrReviewForm">
<input type="hidden" name="action" value="review_okr">
<input type="hidden" name="csrf_token" value="<?= e($csrfToken) ?>">
<div class="col-12">
<label class="form-label" for="detail_target_value">Target value</label>
<input class="form-control js-score-target" id="detail_target_value" name="target_value" type="number" min="1" step="0.1" value="<?= e((string) $item['target_value']) ?>" <?= okr_is_approver($user['role']) ? '' : 'disabled' ?> >
</div>
<div class="col-12">
<label class="form-label" for="detail_current_value">Current value</label>
<input class="form-control js-score-current" id="detail_current_value" name="current_value" type="number" min="0" step="0.1" value="<?= e((string) $item['current_value']) ?>" <?= okr_is_approver($user['role']) ? '' : 'disabled' ?> >
</div>
<div class="col-12">
<label class="form-label" for="manager_comment">Comment</label>
<textarea class="form-control" id="manager_comment" name="manager_comment" rows="4" <?= okr_is_approver($user['role']) ? '' : 'disabled' ?>><?= e((string) ($item['manager_comment'] ?? '')) ?></textarea>
</div>
<div class="col-12">
<label class="form-label">Projected score</label>
<div class="score-preview surface-muted">
<strong class="js-score-output"><?= e((string) $item['score_percent']) ?>%</strong>
<span class="small text-secondary">Recomputed live in the browser</span>
</div>
</div>
<?php if (okr_is_approver($user['role'])): ?>
<div class="col-12 d-grid gap-2">
<button class="btn btn-brand" type="submit" name="decision" value="approve">Approve and score</button>
<button class="btn btn-outline-secondary" type="submit" name="decision" value="update">Save progress only</button>
<button class="btn btn-outline-danger" type="submit" name="decision" value="reject">Reject with feedback</button>
</div>
<?php else: ?>
<div class="alert alert-light border small mb-0">You can view this record, but only leadership roles can change approval status in this first release.</div>
<?php endif; ?>
</form>
</section>
<section class="surface-card">
<p class="tiny-label mb-2">Audit snapshot</p>
<ul class="list-unstyled small mb-0 vstack gap-2 text-secondary">
<li><strong class="text-dark">Created:</strong> <?= e(date('M j, Y H:i', strtotime((string) $item['created_at']))) ?> UTC</li>
<li><strong class="text-dark">Updated:</strong> <?= e(date('M j, Y H:i', strtotime((string) $item['updated_at']))) ?> UTC</li>
<li><strong class="text-dark">Status:</strong> <?= e((string) $item['status']) ?></li>
<li><strong class="text-dark">Approval:</strong> <?= e((string) $item['approval_state']) ?></li>
</ul>
</section>
</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>