Commit a6d47855 authored by Administrator's avatar Administrator

Update 12 files via Son of Anton

parent 78514f08
Pipeline #27 canceled with stage
......@@ -2,9 +2,9 @@
return [
'name' => 'AL-ARCADE HR Platform',
'version' => '3.0.0',
'url' => 'https://hrsystem.caprover.al-arcade.com',
'debug' => false,
'url' => getenv('APP_URL') ?: 'https://hrsystem.caprover.al-arcade.com',
'debug' => (bool)(getenv('APP_DEBUG') ?: false),
'timezone' => 'Africa/Cairo',
'locale' => 'en',
'key' => 'al-arcade-hr-v3-2025-secure-key-do-not-share-with-anyone-ever',
'key' => getenv('APP_KEY') ?: 'al-arcade-hr-v3-2025-secure-key-do-not-share-with-anyone-ever',
];
\ No newline at end of file
<?php
return [
'host' => 'srv-captain--mysql-db',
'port' => 3306,
'database' => 'al_arcade_hr',
'username' => 'root',
'password' => 'Alarcade123#',
'host' => getenv('DB_HOST') ?: 'srv-captain--mysql-db',
'port' => (int)(getenv('DB_PORT') ?: 3306),
'database' => getenv('DB_NAME') ?: 'al_arcade_hr',
'username' => getenv('DB_USER') ?: 'root',
'password' => getenv('DB_PASS') ?: 'Alarcade123#',
'charset' => 'utf8mb4',
'collation'=> 'utf8mb4_unicode_ci',
'options' => [
......@@ -12,6 +12,5 @@ return [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false,
],
];
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html/public
ServerName localhost
<Directory /var/www/html/public>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
Options -Indexes +FollowSymLinks
# Route everything through index.php
FallbackResource /index.php
# Disable MultiViews to prevent Apache from doing content negotiation
Options -MultiViews
</Directory>
# Deny access to sensitive directories
<DirectoryMatch "/var/www/html/(engine|modules|config|database|cli|bootstrap|storage|docker|templates)">
<DirectoryMatch "^/var/www/html/(engine|config|database|cli|bootstrap|storage|modules|middleware|templates)">
Require all denied
</DirectoryMatch>
# Allow storage/uploads for file serving
<Directory /var/www/html/storage/uploads>
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
\ No newline at end of file
#!/bin/bash
set -e
echo "=== AL-ARCADE HR Platform v3.0 — Starting ==="
echo "Timestamp: $(date)"
echo ""
# ─── ALL VALUES HARDCODED — NO ENV VARS NEEDED ───
DB_HOST="srv-captain--mysql-db"
DB_PORT="3306"
DB_NAME="al_arcade_hr"
DB_USER="root"
DB_PASS="Alarcade123#"
echo "Configuration:"
echo " DB_HOST: ${DB_HOST}"
echo " DB_PORT: ${DB_PORT}"
echo " DB_NAME: ${DB_NAME}"
echo " DB_USER: ${DB_USER}"
echo " DB_PASS: [REDACTED]"
echo ""
# ─── Common MySQL flags (disable SSL for internal Docker networking) ───
MYSQL_FLAGS="--ssl-mode=DISABLED"
# ─── DNS Resolution Check ───
echo "Attempting DNS resolution for ${DB_HOST}..."
if getent hosts "${DB_HOST}" > /dev/null 2>&1; then
RESOLVED_IP=$(getent hosts "${DB_HOST}" | awk '{ print $1 }')
echo " ✅ ${DB_HOST} resolves to ${RESOLVED_IP}"
else
echo " ❌ CANNOT RESOLVE ${DB_HOST}"
echo " Starting Apache anyway..."
exec "$@"
fi
echo ""
echo "==========================================="
echo " AL-ARCADE HR Platform v3.0 — Entrypoint"
echo "==========================================="
# ─── Wait for MySQL ───
echo "Waiting for MySQL at ${DB_HOST}:${DB_PORT}..."
MAX_ATTEMPTS=60
ATTEMPT=0
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
ATTEMPT=$((ATTEMPT + 1))
if mysqladmin ping -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASS}" ${MYSQL_FLAGS} --silent 2>/dev/null; then
echo "✅ MySQL is ready! (attempt ${ATTEMPT}/${MAX_ATTEMPTS})"
break
fi
if mysqladmin ping -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASS}" --skip-ssl --silent 2>/dev/null; then
echo "✅ MySQL is ready via --skip-ssl! (attempt ${ATTEMPT}/${MAX_ATTEMPTS})"
MYSQL_FLAGS="--skip-ssl"
echo "[INIT] Waiting for MySQL at ${DB_HOST:-srv-captain--mysql-db}:${DB_PORT:-3306}..."
MAX_TRIES=30
COUNT=0
until mysql -h "${DB_HOST:-srv-captain--mysql-db}" -P "${DB_PORT:-3306}" -u "${DB_USER:-root}" -p"${DB_PASS:-Alarcade123#}" -e "SELECT 1" &>/dev/null; do
COUNT=$((COUNT+1))
if [ $COUNT -ge $MAX_TRIES ]; then
echo "[ERROR] MySQL not available after ${MAX_TRIES} attempts. Starting Apache anyway..."
break
fi
if [ $((ATTEMPT % 10)) -eq 0 ]; then
echo " MySQL not ready... (attempt ${ATTEMPT}/${MAX_ATTEMPTS})"
mysqladmin ping -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASS}" ${MYSQL_FLAGS} 2>&1 || true
else
echo " MySQL not ready... (attempt ${ATTEMPT}/${MAX_ATTEMPTS})"
fi
echo "[INIT] MySQL not ready yet (attempt $COUNT/$MAX_TRIES)..."
sleep 2
done
if [ $ATTEMPT -ge $MAX_ATTEMPTS ]; then
echo "❌ FATAL: Could not connect to MySQL after ${MAX_ATTEMPTS} attempts"
echo " Starting Apache anyway..."
exec "$@"
fi
# ─── Helper functions ───
run_mysql() {
mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASS}" ${MYSQL_FLAGS} "$@" 2>&1
}
run_mysql_silent() {
mysql -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASS}" ${MYSQL_FLAGS} -N "$@" 2>/dev/null
}
# ─── Create Database ───
echo ""
echo "Ensuring database '${DB_NAME}' exists..."
run_mysql -e "CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
echo "✅ Database ensured."
echo "[INIT] MySQL is available."
# ─── Run Schema ───
TABLE_COUNT=$(run_mysql_silent -e "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='${DB_NAME}' AND TABLE_TYPE='BASE TABLE';" || echo "0")
DB_NAME="${DB_NAME:-al_arcade_hr}"
DB_EXISTS=$(mysql -h "${DB_HOST:-srv-captain--mysql-db}" -P "${DB_PORT:-3306}" -u "${DB_USER:-root}" -p"${DB_PASS:-Alarcade123#}" -se "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='${DB_NAME}'" 2>/dev/null || echo "0")
echo "Current table count: ${TABLE_COUNT}"
if [ "$DB_EXISTS" = "0" ] || [ -z "$DB_EXISTS" ]; then
echo "[INIT] Database empty or not found. Running schema..."
mysql -h "${DB_HOST:-srv-captain--mysql-db}" -P "${DB_PORT:-3306}" -u "${DB_USER:-root}" -p"${DB_PASS:-Alarcade123#}" < /var/www/html/database/schema.sql 2>&1 || echo "[WARN] Schema import had warnings"
echo "[INIT] Schema imported."
if [ "${TABLE_COUNT}" -lt "70" ] 2>/dev/null; then
echo "Running schema migration (expected 73 tables, found ${TABLE_COUNT})..."
SCHEMA_FILE=""
for path in "/var/www/html/database/schema.sql" "/var/www/html/public/../database/schema.sql"; do
if [ -f "$path" ]; then
SCHEMA_FILE="$path"
break
fi
done
if [ -n "${SCHEMA_FILE}" ]; then
echo "Found schema at: ${SCHEMA_FILE}"
run_mysql "${DB_NAME}" < "${SCHEMA_FILE}"
echo "✅ Schema applied."
# Seed data
if [ -f /var/www/html/database/seed.sql ]; then
echo "[INIT] Running seed SQL..."
mysql -h "${DB_HOST:-srv-captain--mysql-db}" -P "${DB_PORT:-3306}" -u "${DB_USER:-root}" -p"${DB_PASS:-Alarcade123#}" "${DB_NAME}" < /var/www/html/database/seed.sql 2>&1 || echo "[WARN] Seed had warnings"
fi
NEW_COUNT=$(run_mysql_silent -e "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='${DB_NAME}' AND TABLE_TYPE='BASE TABLE';" || echo "0")
echo "Tables after migration: ${NEW_COUNT}"
else
echo "❌ schema.sql not found!"
echo " Files in /var/www/html/database/:"
ls -la /var/www/html/database/ 2>/dev/null || echo " (directory does not exist)"
# Run PHP seed
if [ -f /var/www/html/cli/seed.php ]; then
echo "[INIT] Running PHP seed..."
php /var/www/html/cli/seed.php 2>&1 || echo "[WARN] PHP seed had errors"
fi
# Seed data
if [ -f "/var/www/html/database/seed.sql" ]; then
echo "Running seed data..."
run_mysql "${DB_NAME}" < "/var/www/html/database/seed.sql" || echo "⚠️ Some seed data may already exist."
echo "✅ Seed data applied."
# Create super admin
if [ -f /var/www/html/cli/create-superadmin.php ]; then
echo "[INIT] Creating super admin..."
php /var/www/html/cli/create-superadmin.php 2>&1 || echo "[WARN] Superadmin creation had errors"
fi
else
echo "✅ Schema already exists (${TABLE_COUNT} tables). Skipping migration."
fi
echo "[INIT] Database already has ${DB_EXISTS} tables. Skipping schema import."
# ─── Create Super Admin ───
SA_EXISTS=$(run_mysql_silent -e "SELECT COUNT(*) FROM \`${DB_NAME}\`.users WHERE role='super_admin';" || echo "0")
if [ "${SA_EXISTS}" = "0" ]; then
echo "Creating Super Admin..."
if [ -f "/var/www/html/cli/create-superadmin.php" ]; then
php /var/www/html/cli/create-superadmin.php
# Still try to create superadmin if it doesn't exist
if [ -f /var/www/html/cli/create-superadmin.php ]; then
php /var/www/html/cli/create-superadmin.php 2>&1 || true
fi
else
echo "✅ Super Admin already exists. Skipping."
fi
# ─── Fix Permissions ───
echo ""
echo "Setting file permissions..."
# ─── Fix permissions ───
echo "[INIT] Setting file permissions..."
chown -R www-data:www-data /var/www/html/storage 2>/dev/null || true
chmod -R 775 /var/www/html/storage 2>/dev/null || true
echo ""
echo "╔══════════════════════════════════════════════════════╗"
echo "║ ✅ AL-ARCADE HR Platform v3.0 — Ready! ║"
echo "║ Starting Apache... ║"
echo "╚══════════════════════════════════════════════════════╝"
echo ""
# ─── Setup cron ───
echo "[INIT] Setting up cron job..."
echo "*/5 * * * * cd /var/www/html && php cron/runner.php >> /var/www/html/storage/logs/cron.log 2>&1" | crontab - 2>/dev/null || true
service cron start 2>/dev/null || true
echo "==========================================="
echo " AL-ARCADE HR Platform — Ready!"
echo "==========================================="
# Start Apache
exec "$@"
\ No newline at end of file
; AL-ARCADE HR Platform PHP Configuration
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/www/html/storage/logs/php-error.log
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
; Upload limits
upload_max_filesize = 25M
post_max_size = 30M
memory_limit = 512M
max_file_uploads = 10
; Memory & execution
memory_limit = 256M
max_execution_time = 120
max_input_time = 60
display_errors = Off
log_errors = On
error_log = /var/www/html/storage/logs/php-error.log
date.timezone = Africa/Cairo
max_input_vars = 5000
; Session
session.gc_maxlifetime = 28800
session.cookie_httponly = 1
session.cookie_secure = 0
session.cookie_samesite = Lax
session.use_strict_mode = 1
; OPcache
opcache.enable = 1
opcache.memory_consumption = 128
opcache.max_accelerated_files = 10000
opcache.validate_timestamps = 0
\ No newline at end of file
opcache.revalidate_freq = 60
opcache.validate_timestamps = 1
; Timezone
date.timezone = Africa/Cairo
; Output buffering (SSE needs this off for streaming)
output_buffering = 4096
zlib.output_compression = Off
\ No newline at end of file
<?php
declare(strict_types=1);
use Engine\Core\Container;
use Engine\Core\Router;
$router = Container::getInstance()->resolve(Engine\Core\Router::class);
// SSE stream — only auth middleware, NO CSRF, NO audit (would flood the audit trail)
$router->get('/sse/stream', Engine\RealTime\SSEController::class, 'stream')
->middleware([Middleware\AuthenticationMiddleware::class]);
\ No newline at end of file
// SSE stream — only auth middleware, NO CSRF, NO audit
Router::group('', ['middleware' => ['auth']], function () {
Router::get('/sse/stream', [\Engine\RealTime\SSEController::class, 'stream']);
});
\ No newline at end of file
RewriteEngine On
RewriteBase /
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# Block direct access to sensitive dirs
RewriteRule ^(bootstrap|config|engine|modules|templates|storage|database|cli|cron)/ - [F,L]
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Serve existing files/dirs directly
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Redirect Trailing Slashes (except root)
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Route everything else through index.php
RewriteRule ^(.*)$ index.php [QSA,L]
# Send Requests To Front Controller
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>
# Security headers
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
\ No newline at end of file
# Security Headers
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
# Prevent directory listing
Options -Indexes
# Deny access to hidden files
<FilesMatch "^\.">
Require all denied
</FilesMatch>
\ No newline at end of file
<?php
/** @var array $user */
/** @var array $months */
/** @var array $payroll_trend */
/** @var array $deduction_trend */
/** @var array $bounty_trend */
/** @var array $headcount_trend */
/** @var array $productivity_index */
/** @var array $top_performers */
/** @var array $at_risk_contractors */
/** @var array $deduction_breakdown */
/** @var array $evaluation_distribution */
/** @var array $retention_stats */
/** @var array $report_compliance */
/** @var array $average_cost_per_contractor */
?>
<div class="dashboard-header">
<h1>📈 Analytics Dashboard</h1>
<a href="/analytics/report-builder" class="btn btn-primary btn-sm">🔧 Report Builder</a>
</div>
<!-- KPI Row -->
<div class="grid grid-4 mb-3">
<div class="stat-card stat-primary">
<span class="stat-label">Active Contractors</span>
<span class="stat-value"><?= $retention_stats['active'] ?? 0 ?></span>
<span class="stat-sub">of <?= $retention_stats['total'] ?? 0 ?> total</span>
</div>
<div class="stat-card stat-success">
<span class="stat-label">Report Compliance</span>
<span class="stat-value"><?= $report_compliance['rate'] ?? 0 ?>%</span>
<span class="stat-sub"><?= $report_compliance['on_time'] ?? 0 ?>/<?= $report_compliance['total'] ?? 0 ?> on time</span>
</div>
<div class="stat-card stat-info">
<span class="stat-label">3-Month Retention</span>
<span class="stat-value"><?= $retention_stats['retention_3mo'] ?? 0 ?>%</span>
</div>
<div class="stat-card stat-warning">
<span class="stat-label">Terminated</span>
<span class="stat-value"><?= $retention_stats['terminated'] ?? 0 ?></span>
</div>
</div>
<div class="grid grid-2 mb-3">
<!-- Payroll Trend -->
<div class="card">
<div class="card-header"><span class="card-title">💰 Payroll Trend (6 months)</span></div>
<table class="data-table">
<thead><tr><th>Month</th><th class="right">Total</th><th class="right">Records</th></tr></thead>
<tbody>
<?php foreach ($payroll_trend ?? [] as $pt): ?>
<tr>
<td><?= $pt['month'] ?></td>
<td class="right mono"><?= number_format($pt['total'], 0) ?> EGP</td>
<td class="right"><?= $pt['count'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Deduction Trend -->
<div class="card">
<div class="card-header"><span class="card-title">⚠️ Deduction Trend (6 months)</span></div>
<table class="data-table">
<thead><tr><th>Month</th><th class="right">Total</th><th class="right">Count</th></tr></thead>
<tbody>
<?php foreach ($deduction_trend ?? [] as $dt): ?>
<tr>
<td><?= $dt['month'] ?></td>
<td class="right mono text-danger"><?= number_format($dt['total'], 0) ?> EGP</td>
<td class="right"><?= $dt['count'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="grid grid-2 mb-3">
<!-- Bounty Trend -->
<div class="card">
<div class="card-header"><span class="card-title">🏆 Bounty Trend</span></div>
<table class="data-table">
<thead><tr><th>Month</th><th class="right">Total</th><th class="right">Count</th></tr></thead>
<tbody>
<?php foreach ($bounty_trend ?? [] as $bt): ?>
<tr>
<td><?= $bt['month'] ?></td>
<td class="right mono text-success"><?= number_format($bt['total'], 0) ?> EGP</td>
<td class="right"><?= $bt['count'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Productivity Index -->
<div class="card">
<div class="card-header"><span class="card-title">📊 Productivity Index</span></div>
<table class="data-table">
<thead><tr><th>Month</th><th class="right">Tasks Done</th><th class="right">Per Contractor</th></tr></thead>
<tbody>
<?php foreach ($productivity_index ?? [] as $pi): ?>
<tr>
<td><?= $pi['month'] ?></td>
<td class="right"><?= $pi['tasks_completed'] ?></td>
<td class="right mono"><?= $pi['per_contractor'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="grid grid-2 mb-3">
<!-- Top Performers -->
<div class="card">
<div class="card-header"><span class="card-title">⭐ Top Performers (This Month)</span></div>
<?php if (!empty($top_performers)): ?>
<table class="data-table">
<thead><tr><th>Name</th><th class="right">Bounties</th><th class="right">Tasks Done</th><th class="right">Eval</th></tr></thead>
<tbody>
<?php foreach ($top_performers as $tp): ?>
<tr>
<td><a href="/users/<?= $tp['id'] ?>"><?= htmlspecialchars($tp['full_name_en']) ?></a></td>
<td class="right mono text-success"><?= number_format($tp['bounties'] ?? 0, 0) ?></td>
<td class="right"><?= $tp['tasks_done'] ?? 0 ?></td>
<td class="right mono"><?= $tp['eval_score'] ?? '—' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<div class="empty-state"><div class="empty-state-icon"></div><div>No data yet</div></div>
<?php endif; ?>
</div>
<!-- At-Risk Contractors -->
<div class="card">
<div class="card-header"><span class="card-title">🚨 At-Risk Contractors</span></div>
<?php if (!empty($at_risk_contractors)): ?>
<table class="data-table">
<thead><tr><th>Name</th><th class="right">Deductions</th><th>PIP</th></tr></thead>
<tbody>
<?php foreach ($at_risk_contractors as $ar): ?>
<tr>
<td><a href="/users/<?= $ar['id'] ?>"><?= htmlspecialchars($ar['full_name_en']) ?></a></td>
<td class="right mono text-danger"><?= number_format($ar['total_deductions'] ?? 0, 0) ?> EGP</td>
<td><?= $ar['pip_status'] ? '<span class="badge badge-danger">' . ucfirst($ar['pip_status']) . '</span>' : '—' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<div class="empty-state"><div class="empty-state-icon"></div><div>No at-risk contractors</div></div>
<?php endif; ?>
</div>
</div>
<div class="grid grid-2">
<!-- Deduction Breakdown by Category -->
<div class="card">
<div class="card-header"><span class="card-title">📊 Deduction Breakdown (This Month)</span></div>
<?php if (!empty($deduction_breakdown)): ?>
<?php
$catNames = ['A' => 'Deadline', 'B' => 'Reporting', 'C' => 'Quality', 'D' => 'Communication'];
$catColors = ['A' => 'var(--danger)', 'B' => 'var(--warning)', 'C' => 'var(--info)', 'D' => 'var(--accent-primary)'];
?>
<?php foreach ($deduction_breakdown as $db_row): ?>
<div class="stat-row">
<span>
<strong style="color:<?= $catColors[$db_row['category']] ?? 'inherit' ?>">Cat <?= $db_row['category'] ?></strong>
<?= $catNames[$db_row['category']] ?? '' ?>
</span>
<strong><?= $db_row['cnt'] ?> (<?= number_format($db_row['total'], 0) ?> EGP)</strong>
</div>
<?php endforeach; ?>
<?php else: ?>
<p class="text-muted">No deductions this month.</p>
<?php endif; ?>
</div>
<!-- Evaluation Distribution -->
<div class="card">
<div class="card-header"><span class="card-title">📊 Evaluation Distribution (Latest Cycle)</span></div>
<?php if (!empty($evaluation_distribution)): ?>
<?php
$ratingColors = [
'exceptional' => 'badge-success', 'strong' => 'badge-info',
'adequate' => 'badge-warning', 'below_expectations' => 'badge-danger', 'unacceptable' => 'badge-danger',
];
?>
<?php foreach ($evaluation_distribution as $ed): ?>
<div class="stat-row">
<span class="badge <?= $ratingColors[$ed['rating']] ?? 'badge-muted' ?>"><?= ucfirst(str_replace('_', ' ', $ed['rating'])) ?></span>
<strong><?= $ed['cnt'] ?></strong>
</div>
<?php endforeach; ?>
<?php else: ?>
<p class="text-muted">No evaluation data.</p>
<?php endif; ?>
</div>
</div>
\ No newline at end of file
<?php $__engine->extend('layouts/app'); ?>
<?php $__engine->section('title'); ?>Project Leader Dashboard<?php $__engine->endSection(); ?>
<?php
/** @var array $user */
/** @var array $team_members */
/** @var int $pending_reviews */
/** @var array $at_risk_tasks */
/** @var array $notifications */
$month = date('F Y');
?>
<div class="dashboard">
<h1>Team Dashboard</h1>
<div class="dashboard-grid">
<div class="card card-wide">
<h3>📊 Team Report Status — Today</h3>
<div class="team-status-list">
<?php foreach ($team_members ?? [] as $member): ?>
<div class="team-member-row">
<span class="member-name"><?= $__engine->e($member['full_name_en']) ?></span>
<?php if ($member['report_status'] && in_array($member['report_status'], ['submitted','approved','approved_auto'])): ?>
<span class="badge badge-success">✅ Reported</span>
<?php elseif ($member['report_status'] === 'draft'): ?>
<span class="badge badge-warning">📝 Draft</span>
<?php else: ?>
<span class="badge badge-danger">⏳ Pending</span>
<?php endif; ?>
</div>
<div class="dashboard-header">
<h1>📊 Team Dashboard</h1>
<span class="dashboard-date"><?= date('l, F j, Y') ?></span>
</div>
<div class="welcome-banner" style="background:linear-gradient(135deg, #059669, #047857);">
<h2>Welcome back, <?= htmlspecialchars($user['full_name_en'] ?? 'PL') ?></h2>
<p>Your team overview for <?= $month ?>.</p>
<div class="quick-actions">
<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>
<a href="/boards" class="btn btn-sm" style="background:rgba(255,255,255,0.2);color:white;border:1px solid rgba(255,255,255,0.3);">📋 Boards</a>
</div>
</div>
<div class="grid grid-3 mb-3">
<div class="stat-card stat-primary">
<span class="stat-label">Team Members</span>
<span class="stat-value"><?= count($team_members ?? []) ?></span>
</div>
<div class="stat-card stat-warning">
<span class="stat-label">Pending Reviews</span>
<span class="stat-value"><?= $pending_reviews ?? 0 ?></span>
<?php if (($pending_reviews ?? 0) > 0): ?>
<a href="/reports/review" class="btn btn-sm btn-warning mt-1">Review Now</a>
<?php endif; ?>
</div>
<div class="stat-card stat-danger">
<span class="stat-label">At-Risk Tasks</span>
<span class="stat-value"><?= count($at_risk_tasks ?? []) ?></span>
</div>
</div>
<div class="grid grid-2">
<!-- Team Report Status -->
<div class="card">
<div class="card-header">
<span class="card-title">📊 Team Report Status — Today</span>
</div>
<?php if (!empty($team_members)): ?>
<div>
<?php foreach ($team_members as $member): ?>
<div class="task-item">
<a href="/users/<?= $member['id'] ?>" style="font-weight:600;"><?= htmlspecialchars($member['full_name_en']) ?></a>
<?php
$status = $member['report_status'] ?? null;
if ($status && in_array($status, ['submitted','approved','approved_auto'])):
?>
<span class="badge badge-success">✅ Reported</span>
<?php elseif ($status === 'draft'): ?>
<span class="badge badge-warning">📝 Draft</span>
<?php elseif ($status === 'late'): ?>
<span class="badge badge-warning">⏰ Late</span>
<?php else: ?>
<span class="badge badge-danger">⏳ Pending</span>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="card stat-card">
<h3>📋 Pending Reviews</h3>
<div class="stat-number"><?= $pending_reviews ?? 0 ?></div>
<a href="/reports/review" class="btn btn-sm btn-primary">Review Now</a>
</div>
<div class="card card-wide">
<h3>⚠️ At-Risk Tasks</h3>
<?php foreach ($at_risk_tasks ?? [] as $task): ?>
<div class="task-item">
<span class="task-key"><?= $__engine->e($task['card_key'] ?? '') ?></span>
<span class="task-title"><?= $__engine->e($task['title']) ?></span>
<span class="overdue"><?= date('M j', strtotime($task['deadline'])) ?></span>
<?php else: ?>
<div class="empty-state">
<div class="empty-state-icon">👥</div>
<div>No team members found</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<!-- At-Risk Tasks -->
<div class="card">
<div class="card-header">
<span class="card-title">⚠️ At-Risk Tasks (Due within 48h)</span>
</div>
<?php if (!empty($at_risk_tasks)): ?>
<ul class="task-list">
<?php foreach ($at_risk_tasks as $task): ?>
<li class="task-item">
<span class="priority-dot priority-<?= $task['priority'] ?? 'none' ?>"></span>
<a href="/cards/<?= $task['id'] ?>" class="task-key"><?= htmlspecialchars($task['card_key'] ?? '') ?></a>
<span class="task-title"><?= htmlspecialchars($task['title']) ?></span>
<span class="task-meta" style="color:var(--danger)"><?= date('M j', strtotime($task['deadline'])) ?></span>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<div class="empty-state">
<div class="empty-state-icon"></div>
<div>No at-risk tasks — looking good!</div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Recent Notifications -->
<?php if (!empty($notifications)): ?>
<div class="card mt-3">
<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>
\ No newline at end of file
<ul class="task-list">
<?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>
</div>
<?php endif; ?>
\ No newline at end of file
<?php
/** @var array $user */
/** @var array $invites */
/** @var array $stats */
/** @var array $project_leaders */
?>
<div class="dashboard-header">
<h1>📨 Invite Management</h1>
<button class="btn btn-primary" onclick="document.getElementById('create-invite-modal').style.display='flex'">+ New Invite</button>
</div>
<!-- Stats -->
<div class="grid grid-4 mb-3">
<div class="stat-card stat-success"><span class="stat-label">Active</span><span class="stat-value"><?= $stats['active'] ?? 0 ?></span></div>
<div class="stat-card stat-info"><span class="stat-label">Used</span><span class="stat-value"><?= $stats['used'] ?? 0 ?></span></div>
<div class="stat-card stat-warning"><span class="stat-label">Expired</span><span class="stat-value"><?= $stats['expired'] ?? 0 ?></span></div>
<div class="stat-card stat-danger"><span class="stat-label">Revoked</span><span class="stat-value"><?= $stats['revoked'] ?? 0 ?></span></div>
</div>
<!-- Invite List -->
<div class="card">
<table class="data-table">
<thead>
<tr>
<th>Code</th>
<th>Type</th>
<th>Status</th>
<th>Created By</th>
<th>Expires</th>
<th>Used By</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($invites as $inv): ?>
<tr>
<td class="mono font-bold"><?= htmlspecialchars($inv['code']) ?></td>
<td><span class="badge badge-info"><?= ucfirst(str_replace('_', ' ', $inv['contractor_type'])) ?></span></td>
<td>
<span class="badge <?= match($inv['status']) {
'active' => 'badge-success', 'used' => 'badge-info',
'expired' => 'badge-warning', 'revoked' => 'badge-danger', default => 'badge-muted'
} ?>"><?= ucfirst($inv['status']) ?></span>
</td>
<td><?= htmlspecialchars($inv['created_by_name'] ?? '') ?></td>
<td><?= date('M j, Y H:i', strtotime($inv['expires_at'])) ?></td>
<td><?= htmlspecialchars($inv['used_by_name'] ?? '—') ?></td>
<td>
<?php if ($inv['status'] === 'active'): ?>
<form method="POST" action="/invites/<?= $inv['id'] ?>/revoke" style="display:inline;">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>">
<button class="btn btn-sm btn-danger" onclick="return confirm('Revoke this invite?')">Revoke</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if (empty($invites)): ?>
<div class="empty-state"><div class="empty-state-icon">📨</div><div>No invites yet</div></div>
<?php endif; ?>
</div>
<!-- Create Invite Modal -->
<div id="create-invite-modal" class="modal-overlay" style="display:none;" onclick="if(event.target===this)this.style.display='none'">
<div class="modal">
<div class="modal-header">
<h3>📨 Create New Invite</h3>
<button class="modal-close" onclick="this.closest('.modal-overlay').style.display='none'">×</button>
</div>
<form id="create-invite-form" onsubmit="return createInvite(event)">
<div class="form-group">
<label class="form-label">Contractor Type *</label>
<select name="contractor_type" class="form-control" required>
<option value="full_timer">Full Timer</option>
<option value="intern">Intern</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Assigned Project Leader</label>
<select name="assigned_pl_id" class="form-control">
<option value="">— None —</option>
<?php foreach ($project_leaders ?? [] as $pl): ?>
<option value="<?= $pl['id'] ?>"><?= htmlspecialchars($pl['full_name_en']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">Expiration (days)</label>
<input type="number" name="expiration_days" class="form-control" value="7" min="1" max="30">
</div>
<div class="form-group">
<label class="form-label">Welcome Note</label>
<textarea name="custom_welcome_note" class="form-control" placeholder="Optional welcome message..."></textarea>
</div>
<button type="submit" class="btn btn-primary btn-block btn-lg">Create Invite</button>
</form>
<div id="invite-result" style="display:none;margin-top:16px;padding:16px;background:var(--success-bg);border-radius:var(--radius);"></div>
</div>
</div>
<script>
async function createInvite(e) {
e.preventDefault();
const form = e.target;
const data = Object.fromEntries(new FormData(form));
try {
const resp = await window.api.post('/invites', data);
const result = await resp.json();
if (result.success) {
const el = document.getElementById('invite-result');
el.style.display = 'block';
el.innerHTML = `<strong>✅ Invite Created!</strong><br>
<span class="mono font-bold" style="font-size:1.2rem;">${result.code}</span><br>
<a href="${result.invite_link}" target="_blank" style="word-break:break-all;">${result.invite_link}</a><br>
<small>Expires: ${result.expires_at}</small>`;
window.toast.success('Invite created: ' + result.code);
} else {
window.toast.error(result.error || 'Failed to create invite');
}
} catch (err) {
window.toast.error('Network error');
}
}
</script>
\ No newline at end of file
This diff is collapsed.
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