Commit b60d3139 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix SessionFeedbackController coach column bug and add missing view

- SessionFeedbackController used CONCAT(first_name_ar, last_name_ar) but
  coaches table has full_name_ar — causes 500 on feedback pages
- Add missing feedback_coach_summary.php view (controller referenced it)
- Enhance smoke_test.php with better error extraction and Claude-pasteable output
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent a4c5d9e5
......@@ -15,7 +15,7 @@ class SessionFeedbackController extends Controller
{
$db = App::getInstance()->db();
$session = $db->selectOne(
"SELECT ts.*, tg.name_ar AS group_name, CONCAT(c.first_name_ar, ' ', c.last_name_ar) AS coach_name
"SELECT ts.*, tg.name_ar AS group_name, c.full_name_ar AS coach_name
FROM training_sessions ts
LEFT JOIN training_groups tg ON tg.id = ts.group_id
LEFT JOIN coaches c ON c.id = ts.coach_id
......@@ -53,7 +53,7 @@ class SessionFeedbackController extends Controller
$db = App::getInstance()->db();
$session = $db->selectOne(
"SELECT ts.*, tg.name_ar AS group_name, CONCAT(c.first_name_ar, ' ', c.last_name_ar) AS coach_name
"SELECT ts.*, tg.name_ar AS group_name, c.full_name_ar AS coach_name
FROM training_sessions ts
LEFT JOIN training_groups tg ON tg.id = ts.group_id
LEFT JOIN coaches c ON c.id = ts.coach_id
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>ملخص تقييمات المدرب — <?= e($coach['full_name_ar'] ?? '') ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="padding:15px;margin-bottom:20px;display:flex;justify-content:space-between;align-items:center;">
<div>
<strong style="font-size:18px;"><?= e($coach['full_name_ar'] ?? 'مدرب') ?></strong>
</div>
<form method="get" style="display:flex;gap:10px;align-items:center;">
<label style="font-size:13px;">من:</label>
<input type="date" name="from" value="<?= e($dateFrom) ?>" class="form-control" style="width:auto;">
<label style="font-size:13px;">إلى:</label>
<input type="date" name="to" value="<?= e($dateTo) ?>" class="form-control" style="width:auto;">
<button type="submit" class="btn btn-sm btn-primary">تصفية</button>
</form>
</div>
<?php if (!empty($summary) && (int) ($summary['total_responses'] ?? 0) > 0): ?>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#F59E0B;"><?= $summary['avg_rating'] ?? '0' ?></div>
<div style="font-size:12px;color:#6B7280;">متوسط التقييم</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#059669;"><?= $summary['avg_enjoyment'] ?? '0' ?></div>
<div style="font-size:12px;color:#6B7280;">متوسط الاستمتاع</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#7C3AED;"><?= $summary['avg_difficulty'] ?? '0' ?></div>
<div style="font-size:12px;color:#6B7280;">متوسط الصعوبة</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#0284C7;"><?= $summary['total_responses'] ?? '0' ?></div>
<div style="font-size:12px;color:#6B7280;">عدد التقييمات</div>
</div>
</div>
<?php else: ?>
<div class="card" style="padding:40px;text-align:center;color:#6B7280;">
لا توجد تقييمات في هذه الفترة
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php
/**
* ERP Smoke Tester — UI-level automated testing
*
* Crawls every route, loads every page, checks for PHP errors,
* SQL errors, broken views, and slow pages. Outputs a structured
* report you can paste to Claude for automated fixing.
*
* Usage:
* php tools/smoke_test.php [base_url] [email] [password]
* Example: php tools/smoke_test.php https://your-erp.com admin@club.com admin123
*/
declare(strict_types=1);
// ─── Configuration ──────────────────────────────────────────────────────────
$baseUrl = $argv[1] ?? getenv('APP_URL') ?: 'http://localhost';
$baseUrl = rtrim($baseUrl, '/');
$adminEmail = $argv[2] ?? getenv('ADMIN_EMAIL') ?: 'admin@club.com';
$adminPassword = $argv[3] ?? getenv('ADMIN_PASSWORD') ?: 'admin123';
$timeout = 15;
$maxPages = 500;
// ─── Results Collection ─────────────────────────────────────────────────────
$results = [
'total_routes' => 0,
'tested' => 0,
'passed' => 0,
'failed' => 0,
'skipped' => 0,
'errors' => [],
'warnings' => [],
'start_time' => microtime(true),
];
$isCli = php_sapi_name() === 'cli';
function output(string $msg, string $type = 'info'): void {
global $isCli;
if ($isCli) {
$colors = ['info' => "\033[0m", 'success' => "\033[32m", 'error' => "\033[31m", 'warning' => "\033[33m", 'header' => "\033[36m"];
echo ($colors[$type] ?? '') . $msg . "\033[0m\n";
}
}
// ─── Step 1: Discover All Routes ────────────────────────────────────────────
output("═══════════════════════════════════════════════", 'header');
output(" ERP SMOKE TESTER", 'header');
output(" Base URL: {$baseUrl}", 'header');
output("═══════════════════════════════════════════════", 'header');
output("");
$modulesDir = __DIR__ . '/../app/Modules';
$routeFiles = glob($modulesDir . '/*/Routes.php');
$allRoutes = [];
foreach ($routeFiles as $file) {
$routes = require $file;
if (!is_array($routes)) continue;
foreach ($routes as $route) {
if (!is_array($route) || count($route) < 3) continue;
$allRoutes[] = [
'method' => $route[0],
'path' => $route[1],
'handler'=> $route[2],
'module' => basename(dirname($file)),
];
}
}
$results['total_routes'] = count($allRoutes);
output("Found " . count($allRoutes) . " routes across " . count($routeFiles) . " modules\n");
// ─── Step 2: Authenticate ───────────────────────────────────────────────────
output("Authenticating as {$adminEmail}...", 'info');
$cookieFile = sys_get_temp_dir() . '/erp_smoke_test_cookies_' . getmypid() . '.txt';
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $baseUrl . '/login',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_COOKIEJAR => $cookieFile,
CURLOPT_COOKIEFILE => $cookieFile,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_SSL_VERIFYPEER => false,
]);
$loginPage = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (!$loginPage || $httpCode >= 400) {
output("FATAL: Cannot reach login page (HTTP {$httpCode})", 'error');
exit(1);
}
$csrfToken = '';
if (preg_match('/name=["\']_csrf_token["\']\s+value=["\']([^"\']+)["\']/', $loginPage, $m)) {
$csrfToken = $m[1];
} elseif (preg_match('/name=["\']csrf_token["\']\s+value=["\']([^"\']+)["\']/', $loginPage, $m)) {
$csrfToken = $m[1];
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $baseUrl . '/login',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_COOKIEJAR => $cookieFile,
CURLOPT_COOKIEFILE => $cookieFile,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'email' => $adminEmail,
'password' => $adminPassword,
'_csrf_token' => $csrfToken,
]),
CURLOPT_TIMEOUT => $timeout,
CURLOPT_SSL_VERIFYPEER => false,
]);
$loginResult = curl_exec($ch);
$loginCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$loginUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
curl_close($ch);
$isLoggedIn = ($loginCode < 400 && strpos($loginUrl, '/login') === false);
if (!$isLoggedIn) {
output("WARNING: Login may have failed (landed on: {$loginUrl}). Continuing anyway...", 'warning');
$results['warnings'][] = "Authentication may have failed — some pages may return 403/401";
}
output("Auth complete. Testing routes...\n", 'success');
// ─── Step 3: Test Each GET Route ────────────────────────────────────────────
$getRoutes = array_filter($allRoutes, fn($r) => $r['method'] === 'GET');
$tested = 0;
foreach ($getRoutes as $route) {
if ($tested >= $maxPages) break;
$path = $route['path'];
$testPath = preg_replace('/\{[^}]+:\\\d\+\}/', '1', $path);
$testPath = preg_replace('/\{[^}]+\}/', 'test', $testPath);
$url = $baseUrl . $testPath;
$tested++;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_COOKIEJAR => $cookieFile,
CURLOPT_COOKIEFILE => $cookieFile,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HEADER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$totalTime = round(curl_getinfo($ch, CURLINFO_TOTAL_TIME), 2);
$curlError = curl_error($ch);
curl_close($ch);
$headerSize = strpos($response, "\r\n\r\n");
$body = $headerSize !== false ? substr($response, $headerSize + 4) : $response;
$hasError = false;
$errorDetails = [];
if ($curlError) {
$hasError = true;
$errorDetails[] = "CURL: {$curlError}";
}
if ($httpCode >= 500) {
$hasError = true;
$errorDetails[] = "HTTP {$httpCode} Server Error";
if (preg_match('/Exception.*?Message:\s*(.+?)(?:<|$)/s', $body, $em)) {
$errorDetails[] = "Exception: " . trim(strip_tags($em[1]));
} elseif (preg_match('/<h1[^>]*>(.+?)<\/h1>/i', $body, $em)) {
$errorDetails[] = "Page: " . trim(strip_tags($em[1]));
}
if (preg_match('/File:\s*(.+?\.php):?(\d+)?/i', $body, $fm)) {
$errorDetails[] = "File: " . trim($fm[0]);
}
if (preg_match('/SQLSTATE\[.+?\]:.+/i', $body, $sqlm)) {
$errorDetails[] = "SQL: " . trim(substr($sqlm[0], 0, 200));
}
if (preg_match('/Stack trace:(.+?)(?:<\/pre>|<br|$)/s', $body, $stm)) {
$trace = trim(strip_tags($stm[1]));
$traceLines = array_filter(explode("\n", $trace));
$errorDetails[] = "Trace: " . implode(' → ', array_slice($traceLines, 0, 3));
}
}
if ($httpCode === 403) {
$results['skipped']++;
continue;
}
if ($httpCode === 404) {
if (strpos($body, 'غير موجود') !== false || strpos($body, 'not found') !== false) {
$results['passed']++;
output(" ✓ [{$httpCode}] {$testPath} (entity not found - OK) [{$totalTime}s]", 'success');
continue;
}
}
$phpErrors = [];
if (preg_match_all('/(Fatal error|Parse error|Warning|Notice|Deprecated):(.+?)(?:in\s+\/[^\s]+)?(?:\s+on\s+line\s+\d+)?/i', $body, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$phpErrors[] = trim($match[0]);
}
}
if (preg_match('/PDOException|RuntimeException|TypeError|Error\b/', $body, $exMatch)) {
$phpErrors[] = "Uncaught: " . $exMatch[0];
if (preg_match('/Message:\s*(.+?)(<|\n)/s', $body, $msgMatch)) {
$phpErrors[] = trim(strip_tags($msgMatch[1]));
}
}
if (preg_match('/Undefined (variable|index|array key|property)/', $body, $undMatch)) {
$phpErrors[] = $undMatch[0];
}
if (!empty($phpErrors)) {
$hasError = true;
$errorDetails = array_merge($errorDetails, $phpErrors);
}
if ($totalTime > 5.0) {
$results['warnings'][] = "SLOW [{$totalTime}s]: {$testPath}";
}
if ($hasError) {
$results['failed']++;
$results['errors'][] = [
'route' => $route['path'],
'url' => $testPath,
'module' => $route['module'],
'handler' => $route['handler'],
'http' => $httpCode,
'time' => $totalTime,
'details' => $errorDetails,
];
output(" ✗ [{$httpCode}] {$testPath} — " . implode(' | ', array_slice($errorDetails, 0, 2)), 'error');
} else {
$results['passed']++;
if ($httpCode >= 200 && $httpCode < 400) {
output(" ✓ [{$httpCode}] {$testPath} [{$totalTime}s]", 'success');
} else {
output(" ? [{$httpCode}] {$testPath} [{$totalTime}s]", 'warning');
}
}
}
$results['tested'] = $tested;
$results['duration'] = round(microtime(true) - $results['start_time'], 1);
// ─── Step 4: Output Report ──────────────────────────────────────────────────
output("\n═══════════════════════════════════════════════", 'header');
output(" SMOKE TEST REPORT", 'header');
output("═══════════════════════════════════════════════", 'header');
output("");
output("Total Routes: {$results['total_routes']}");
output("GET Tested: {$results['tested']}");
output("Passed: {$results['passed']}", 'success');
output("Failed: {$results['failed']}", $results['failed'] > 0 ? 'error' : 'success');
output("Skipped(403): {$results['skipped']}");
output("Duration: {$results['duration']}s");
output("");
if (!empty($results['errors'])) {
output("─── ERRORS (" . count($results['errors']) . ") ───────────────────────────────", 'error');
output("");
foreach ($results['errors'] as $i => $err) {
$num = $i + 1;
output(" [{$num}] {$err['module']} :: {$err['handler']}", 'error');
output(" Route: {$err['route']}");
output(" URL: {$err['url']} (HTTP {$err['http']})");
foreach ($err['details'] as $detail) {
output(" → {$detail}", 'warning');
}
output("");
}
}
if (!empty($results['warnings'])) {
output("─── WARNINGS (" . count($results['warnings']) . ") ─────────────────────────────", 'warning');
foreach ($results['warnings'] as $w) {
output(" ⚠ {$w}", 'warning');
}
output("");
}
// ─── Save JSON report ───────────────────────────────────────────────────────
$reportFile = __DIR__ . '/smoke_test_report.json';
file_put_contents($reportFile, json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
output("Full report saved to: {$reportFile}");
// ─── Compact output for Claude ──────────────────────────────────────────────
if (!empty($results['errors'])) {
output("\n─── COPY BELOW TO CLAUDE ────────────────────────", 'header');
output("```");
output("SMOKE TEST: {$results['failed']} errors / {$results['passed']} passed / {$results['tested']} tested");
foreach ($results['errors'] as $i => $err) {
$num = $i + 1;
output("[{$num}] {$err['module']}::{$err['handler']} HTTP={$err['http']} PATH={$err['url']}");
foreach ($err['details'] as $detail) {
output(" {$detail}");
}
}
if (!empty($results['warnings'])) {
output("WARNINGS:");
foreach ($results['warnings'] as $w) {
output(" {$w}");
}
}
output("```");
output("─── END COPY ───────────────────────────────────", 'header');
}
@unlink($cookieFile);
exit($results['failed'] > 0 ? 1 : 0);
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