Commit 223727b5 authored by Administrator's avatar Administrator

Update 5 files via Son of Anton

parent 7b9f7368
Pipeline #24 canceled with stage
...@@ -3,113 +3,93 @@ declare(strict_types=1); ...@@ -3,113 +3,93 @@ declare(strict_types=1);
namespace Engine\Template; namespace Engine\Template;
/**
* Simple PHP template engine with layout support.
*
* Usage:
* $engine->render('dashboard/super_admin', ['user' => $user]);
*
* Templates can specify layout at the top:
* (no explicit call needed — all templates use app layout by default for logged-in users)
*/
final class TemplateEngine final class TemplateEngine
{ {
private string $templateDir; private string $basePath;
private string $cacheDir; private string $cachePath;
private array $globals = []; private string $defaultLayout = 'layouts/app';
private array $sections = [];
private array $sectionStack = [];
private ?string $layout = null;
private string $childContent = '';
public function __construct(string $templateDir, string $cacheDir) public function __construct(string $basePath = '', string $cachePath = '')
{ {
$this->templateDir = rtrim($templateDir, '/'); $this->basePath = $basePath ?: (defined('ROOT_PATH') ? ROOT_PATH . '/templates' : __DIR__ . '/../../templates');
$this->cacheDir = rtrim($cacheDir, '/'); $this->cachePath = $cachePath ?: (defined('ROOT_PATH') ? ROOT_PATH . '/storage/cache/templates' : '/tmp');
if (!is_dir($this->cacheDir)) {
mkdir($this->cacheDir, 0755, true);
}
} }
public function addGlobal(string $key, mixed $value): void /**
* Render a template with data and wrap in layout.
*/
public function render(string $template, array $data = [], ?string $layout = null): string
{ {
$this->globals[$key] = $value; $templateFile = $this->basePath . '/' . $template . '.php';
}
public function render(string $template, array $data = []): string if (!file_exists($templateFile)) {
{ throw new \RuntimeException("Template not found: {$template} (looked in {$templateFile})");
$this->sections = []; }
$this->layout = null;
// Render the child template first
$content = $this->renderFile($templateFile, $data);
// Determine layout
$layoutName = $layout;
if ($layoutName === null) {
// Auto-detect: use 'auth' layout for auth pages, 'app' for everything else
if (str_starts_with($template, 'auth/') || str_starts_with($template, 'errors/')) {
$layoutName = 'layouts/auth';
} else {
$layoutName = $this->defaultLayout;
}
}
$content = $this->renderFile($template, $data); if ($layoutName === 'none' || $layoutName === false || $layoutName === '') {
return $content;
}
if ($this->layout) { $layoutFile = $this->basePath . '/' . $layoutName . '.php';
$layoutTemplate = $this->layout; if (!file_exists($layoutFile)) {
$this->childContent = $content; // No layout file? Just return the content directly.
$this->layout = null; return $content;
$content = $this->renderFile($layoutTemplate, $data);
} }
return $content; // Render layout with content injected
$layoutData = array_merge($data, ['content' => $content]);
return $this->renderFile($layoutFile, $layoutData);
} }
private function renderFile(string $template, array $data): string /**
* Render a partial (no layout wrapping).
*/
public function partial(string $template, array $data = []): string
{ {
$file = $this->templateDir . '/' . str_replace('.', '/', $template) . '.php'; $file = $this->basePath . '/' . $template . '.php';
if (!file_exists($file)) { if (!file_exists($file)) {
throw new \RuntimeException("Template not found: {$file}"); return "<!-- partial not found: {$template} -->";
} }
return $this->renderFile($file, $data);
}
extract(array_merge($this->globals, $data)); /**
$__engine = $this; * Render a PHP file with extracted data and return output as string.
*/
private function renderFile(string $file, array $data): string
{
extract($data, EXTR_SKIP);
ob_start(); ob_start();
try { try {
include $file; require $file;
} catch (\Throwable $e) { } catch (\Throwable $e) {
ob_end_clean(); ob_end_clean();
throw $e; throw new \RuntimeException("Template render error in {$file}: " . $e->getMessage(), 0, $e);
} }
return ob_get_clean(); return ob_get_clean();
} }
public function extend(string $layout): void
{
$this->layout = $layout;
}
public function section(string $name): void
{
$this->sectionStack[] = $name;
ob_start();
}
public function endSection(): void
{
$name = array_pop($this->sectionStack);
$this->sections[$name] = ob_get_clean();
}
public function yield(string $name, string $default = ''): string
{
return $this->sections[$name] ?? $default;
}
public function content(): string
{
return $this->childContent;
}
public function partial(string $template, array $data = []): string
{
return $this->renderFile($template, $data);
}
public function e(mixed $value): string
{
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
}
public function csrf(): string
{
$token = $_SESSION['csrf_token'] ?? ($_COOKIE['csrf_token'] ?? '');
return '<input type="hidden" name="_csrf_token" value="' . $this->e($token) . '">';
}
public function csrfMeta(): string
{
$token = $_SESSION['csrf_token'] ?? ($_COOKIE['csrf_token'] ?? '');
return '<meta name="csrf-token" content="' . $this->e($token) . '">';
}
} }
\ No newline at end of file
This diff is collapsed.
<?php $__engine->extend('layouts/app'); ?> <?php
<?php $__engine->section('title'); ?>Admin Dashboard<?php $__engine->endSection(); ?> /** @var array $user */
/** @var array $notifications */
/** @var int $unread_count */
/** @var int $active_contractors */
/** @var int $onboarding_count */
/** @var array $month_deductions */
/** @var array $month_bounties */
/** @var int $active_pips */
<div class="dashboard"> $month = date('F Y');
<h1>Admin Dashboard</h1> ?>
<div class="dashboard-grid">
<div class="card stat-card"> <div class="dashboard-header">
<h3>👥 Active Contractors</h3> <h1>📊 Dashboard</h1>
<div class="stat-number"><?= $active_contractors ?? 0 ?></div> <span class="dashboard-date"><?= date('l, F j, Y') ?></span>
</div> </div>
<div class="card stat-card">
<h3>📋 Onboarding</h3> <div class="welcome-banner">
<div class="stat-number"><?= $onboarding_count ?? 0 ?></div> <h2>Welcome back, <?= htmlspecialchars($user['full_name_en'] ?? 'Admin') ?></h2>
</div> <p>Administration overview for <?= $month ?>.</p>
<div class="card stat-card"> </div>
<h3>⚠️ Deductions This Month</h3>
<div class="stat-number"><?= $month_deductions['cnt'] ?? 0 ?></div> <div class="grid grid-4 mb-3">
<div class="stat-sub">EGP <?= number_format((float)($month_deductions['total'] ?? 0), 0) ?></div> <div class="stat-card stat-primary">
</div> <span class="stat-label">Active Contractors</span>
<div class="card stat-card"> <span class="stat-value"><?= $active_contractors ?? 0 ?></span>
<h3>💰 Bounties This Month</h3> <span class="stat-sub"><?= $onboarding_count ?? 0 ?> onboarding</span>
<div class="stat-number"><?= $month_bounties['cnt'] ?? 0 ?></div> </div>
<div class="stat-sub">EGP <?= number_format((float)($month_bounties['total'] ?? 0), 0) ?></div> <div class="stat-card stat-danger">
<span class="stat-label">Deductions (<?= date('M') ?>)</span>
<span class="stat-value font-mono"><?= number_format($month_deductions['total'] ?? 0, 0) ?></span>
<span class="stat-sub"><?= $month_deductions['cnt'] ?? 0 ?> issued</span>
</div>
<div class="stat-card stat-success">
<span class="stat-label">Bounties (<?= date('M') ?>)</span>
<span class="stat-value font-mono"><?= number_format($month_bounties['total'] ?? 0, 0) ?></span>
<span class="stat-sub"><?= $month_bounties['cnt'] ?? 0 ?> paid</span>
</div>
<div class="stat-card stat-warning">
<span class="stat-label">Active PIPs</span>
<span class="stat-value"><?= $active_pips ?? 0 ?></span>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-header">
<span class="card-title">🔔 Notifications</span>
<a href="/notifications" class="btn btn-sm btn-ghost">View All</a>
</div> </div>
<div class="card stat-card"> <?php if (!empty($notifications)): ?>
<h3>📊 Active PIPs</h3> <ul class="task-list">
<div class="stat-number"><?= $active_pips ?? 0 ?></div> <?php foreach ($notifications as $n): ?>
<li class="task-item">
<span class="badge <?= $n['tier'] === 'blocking' ? 'badge-danger' : 'badge-info' ?>"><?= strtoupper($n['tier']) ?></span>
<span class="task-title"><?= htmlspecialchars($n['title'] ?? '') ?></span>
<span class="task-meta"><?= date('M j', strtotime($n['created_at'] ?? 'now')) ?></span>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<div class="empty-state"><div class="empty-state-icon">🔕</div><div>No recent notifications</div></div>
<?php endif; ?>
</div>
<div class="card">
<div class="card-header"><span class="card-title">⚡ Quick Access</span></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<a href="/boards" class="btn btn-ghost btn-block">📋 Boards</a>
<a href="/users" class="btn btn-ghost btn-block">👥 Directory</a>
<a href="/reports/review" class="btn btn-ghost btn-block">📝 Review Reports</a>
<a href="/deductions" class="btn btn-ghost btn-block">⚠️ Deductions</a>
<a href="/payroll" class="btn btn-ghost btn-block">💰 Payroll</a>
<a href="/invites" class="btn btn-ghost btn-block">📨 Invites</a>
</div> </div>
</div> </div>
</div> </div>
\ No newline at end of file
<?php $__engine->extend('layouts/app'); ?> <?php
<?php $__engine->section('title'); ?>Super Admin Dashboard<?php $__engine->endSection(); ?> /** @var array $user */
/** @var array $notifications */
<div class="dashboard"> /** @var int $unread_count */
<h1>Super Admin Dashboard</h1> /** @var int $active_contractors */
<div class="dashboard-grid"> /** @var int $onboarding_count */
<div class="card stat-card"> /** @var array $month_deductions */
<h3>👥 Active Contractors</h3> /** @var array $month_bounties */
<div class="stat-number"><?= $active_contractors ?? 0 ?></div> /** @var array $payroll_status */
</div> /** @var int $active_pips */
<div class="card stat-card"> /** @var float $total_payroll */
<h3>💵 Total Payroll</h3> /** @var int $expiring_contracts */
<div class="stat-number">EGP <?= number_format($total_payroll ?? 0, 0) ?></div>
</div> $month = date('F Y');
<div class="card stat-card"> $greeting = match(true) {
<h3>⚠️ Deductions</h3> date('H') < 12 => 'Good morning',
<div class="stat-number"><?= $month_deductions['cnt'] ?? 0 ?></div> date('H') < 18 => 'Good afternoon',
<div class="stat-sub">EGP <?= number_format((float)($month_deductions['total'] ?? 0), 0) ?></div> default => 'Good evening',
</div> };
<div class="card stat-card"> ?>
<h3>💰 Bounties</h3>
<div class="stat-number"><?= $month_bounties['cnt'] ?? 0 ?></div> <div class="dashboard-header">
<div class="stat-sub">EGP <?= number_format((float)($month_bounties['total'] ?? 0), 0) ?></div> <h1>📊 Dashboard</h1>
</div> <span class="dashboard-date"><?= date('l, F j, Y') ?></span>
<div class="card stat-card"> </div>
<h3>📋 Onboarding</h3>
<div class="stat-number"><?= $onboarding_count ?? 0 ?></div> <div class="welcome-banner">
<h2><?= $greeting ?>, <?= htmlspecialchars($user['full_name_en'] ?? 'Admin') ?>!</h2>
<p>Here's your operational overview for <?= $month ?>.</p>
<div class="quick-actions">
<a href="/invites" class="btn btn-sm" style="background:rgba(255,255,255,0.2);color:white;border:1px solid rgba(255,255,255,0.3);">📨 Create Invite</a>
<a href="/payroll?month=<?= date('Y-m') ?>" class="btn btn-sm" style="background:rgba(255,255,255,0.2);color:white;border:1px solid rgba(255,255,255,0.3);">💰 View Payroll</a>
<a href="/reports/review?date=<?= date('Y-m-d') ?>" class="btn btn-sm" style="background:rgba(255,255,255,0.2);color:white;border:1px solid rgba(255,255,255,0.3);">📝 Review Reports</a>
</div>
</div>
<!-- Key Metrics -->
<div class="grid grid-4 mb-3">
<div class="stat-card stat-primary">
<span class="stat-label">Active Contractors</span>
<span class="stat-value"><?= $active_contractors ?? 0 ?></span>
<span class="stat-sub"><?= $onboarding_count ?? 0 ?> onboarding</span>
</div>
<div class="stat-card stat-bounty">
<span class="stat-label">Total Payroll (<?= date('M') ?>)</span>
<span class="stat-value font-mono"><?= number_format($total_payroll ?? 0, 0) ?></span>
<span class="stat-sub">EGP this month</span>
</div>
<div class="stat-card stat-danger">
<span class="stat-label">Deductions (<?= date('M') ?>)</span>
<span class="stat-value font-mono"><?= number_format($month_deductions['total'] ?? 0, 0) ?></span>
<span class="stat-sub"><?= $month_deductions['cnt'] ?? 0 ?> deductions issued</span>
</div>
<div class="stat-card stat-success">
<span class="stat-label">Bounties (<?= date('M') ?>)</span>
<span class="stat-value font-mono"><?= number_format($month_bounties['total'] ?? 0, 0) ?></span>
<span class="stat-sub"><?= $month_bounties['cnt'] ?? 0 ?> bounties paid</span>
</div>
</div>
<div class="grid grid-3 mb-3">
<div class="stat-card stat-warning">
<span class="stat-label">Active PIPs</span>
<span class="stat-value"><?= $active_pips ?? 0 ?></span>
<span class="stat-sub">Under performance improvement</span>
</div>
<div class="stat-card stat-info">
<span class="stat-label">Expiring Contracts</span>
<span class="stat-value"><?= $expiring_contracts ?? 0 ?></span>
<span class="stat-sub">Within 90 days</span>
</div>
<div class="stat-card stat-primary">
<span class="stat-label">Payroll Status</span>
<span class="stat-value" style="font-size:1.2rem"><?= ucfirst(str_replace('_', ' ', $payroll_status['status'] ?? 'N/A')) ?></span>
<span class="stat-sub"><?= $payroll_status['cnt'] ?? 0 ?> records</span>
</div>
</div>
<!-- Recent Notifications -->
<div class="grid grid-2">
<div class="card">
<div class="card-header">
<span class="card-title">🔔 Recent Notifications</span>
<a href="/notifications" class="btn btn-sm btn-ghost">View All</a>
</div> </div>
<div class="card stat-card"> <?php if (!empty($notifications)): ?>
<h3>📊 Active PIPs</h3> <ul class="task-list">
<div class="stat-number"><?= $active_pips ?? 0 ?></div> <?php foreach ($notifications as $n): ?>
<li class="task-item">
<span class="badge <?= $n['tier'] === 'blocking' ? 'badge-danger' : ($n['tier'] === 'important' ? 'badge-warning' : 'badge-muted') ?>"><?= strtoupper($n['tier']) ?></span>
<span class="task-title"><?= htmlspecialchars($n['title'] ?? '') ?></span>
<span class="task-meta"><?= date('M j, g:ia', strtotime($n['created_at'] ?? 'now')) ?></span>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<div class="empty-state">
<div class="empty-state-icon">🔕</div>
<div class="empty-state-text">No recent notifications</div>
</div>
<?php endif; ?>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">⚡ Quick Access</span>
</div> </div>
<div class="card stat-card"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<h3>📄 Expiring Contracts</h3> <a href="/boards" class="btn btn-ghost btn-block">📋 Boards</a>
<div class="stat-number"><?= $expiring_contracts ?? 0 ?></div> <a href="/users" class="btn btn-ghost btn-block">👥 Directory</a>
<div class="stat-sub">within 90 days</div> <a href="/deductions" class="btn btn-ghost btn-block">⚠️ Deductions</a>
<a href="/evaluations" class="btn btn-ghost btn-block">📊 Evaluations</a>
<a href="/analytics" class="btn btn-ghost btn-block">📈 Analytics</a>
<a href="/control-panel" class="btn btn-ghost btn-block">⚙️ Control Panel</a>
<a href="/settings" class="btn btn-ghost btn-block">🔧 Settings</a>
<a href="/system-health" class="btn btn-ghost btn-block">🏥 System Health</a>
</div> </div>
</div> </div>
</div> </div>
\ No newline at end of file
<?php /** @var string $content */ ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $__engine->yield('title', 'AL-ARCADE HR Platform - Login') ?></title> <title>The Grind — AL-ARCADE HR</title>
<link rel="stylesheet" href="/assets/css/app.css"> <link rel="stylesheet" href="/assets/css/app.css">
</head> </head>
<body class="auth-page"> <body class="auth-page">
<div class="auth-container"> <div class="auth-container">
<div class="auth-logo">🎮 The Grind</div> <?= $content ?? '' ?>
<?= $__engine->content() ?>
</div> </div>
</body> </body>
</html> </html>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment