Commit 08409942 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add Live Match Center with minute-by-minute tracking and News feed module

- MatchCenter: schedule/start/end matches, live pitch-side event entry
- Events auto-increment score on goals, timeline view by minute
- Dashboard shows live (pulsing), scheduled, and finished matches
- News module: full CRUD with cover image upload, publish/unpublish toggle
- Category-based filtering (match/facility/announcement/schedule/general)
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 0a0a15dc
<?php
declare(strict_types=1);
namespace App\Modules\MatchCenter\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\MatchCenter\Models\LiveMatch;
use App\Modules\MatchCenter\Models\MatchEvent;
class MatchCenterController extends Controller
{
/**
* Dashboard: show all matches grouped by status (live first, then scheduled, then finished).
*/
public function dashboard(Request $request): Response
{
$matches = LiveMatch::getDashboardMatches();
return $this->view('MatchCenter.Views.dashboard', [
'liveMatches' => $matches['live'],
'scheduledMatches' => $matches['scheduled'],
'finishedMatches' => $matches['finished'],
]);
}
/**
* Show the form to create/schedule a new match.
*/
public function create(Request $request): Response
{
return $this->view('MatchCenter.Views.create');
}
/**
* Validate and store a new match.
*/
public function store(Request $request): Response
{
$matchTitleAr = trim((string) $request->post('match_title_ar', ''));
$teamAName = trim((string) $request->post('team_a_name', ''));
$teamBName = trim((string) $request->post('team_b_name', ''));
$matchDate = trim((string) $request->post('match_date', ''));
$startTime = trim((string) $request->post('start_time', ''));
$refereeName = trim((string) $request->post('referee_name', ''));
$notes = trim((string) $request->post('notes', ''));
// Validation
$errors = [];
if ($matchTitleAr === '') {
$errors[] = 'عنوان المباراة مطلوب';
}
if ($teamAName === '') {
$errors[] = 'اسم الفريق الأول مطلوب';
}
if ($teamBName === '') {
$errors[] = 'اسم الفريق الثاني مطلوب';
}
if ($matchDate === '') {
$errors[] = 'تاريخ المباراة مطلوب';
}
if ($startTime === '') {
$errors[] = 'وقت البداية مطلوب';
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/match-center/create');
}
$employee = App::getInstance()->currentEmployee();
$match = LiveMatch::create([
'match_title_ar' => $matchTitleAr,
'team_a_name' => $teamAName,
'team_b_name' => $teamBName,
'match_date' => $matchDate,
'start_time' => $startTime,
'referee_name' => $refereeName ?: null,
'notes' => $notes ?: null,
'status' => 'scheduled',
'score_a' => 0,
'score_b' => 0,
'entry_employee_id' => $employee ? ($employee->id ?? ($employee['id'] ?? null)) : null,
]);
return $this->redirect('/match-center/' . $match->id)->withSuccess('تم جدولة المباراة بنجاح');
}
/**
* Display match details with timeline of events.
*/
public function show(Request $request, string $id): Response
{
$match = LiveMatch::find((int) $id);
if (!$match) {
return $this->redirect('/match-center')->withError('المباراة غير موجودة');
}
$events = MatchEvent::getByMatch((int) $id);
return $this->view('MatchCenter.Views.show', [
'match' => $match,
'events' => $events,
'eventTypes' => MatchEvent::getEventTypeLabels(),
]);
}
/**
* Start a match: set status to live.
*/
public function startMatch(Request $request, string $id): Response
{
$match = LiveMatch::find((int) $id);
if (!$match) {
return $this->redirect('/match-center')->withError('المباراة غير موجودة');
}
if ($match->status !== 'scheduled') {
return $this->redirect('/match-center/' . $id)->withError('لا يمكن بدء مباراة غير مجدولة');
}
$match->update([
'status' => 'live',
'started_at' => date('Y-m-d H:i:s'),
]);
return $this->redirect('/match-center/' . $id)->withSuccess('تم بدء المباراة');
}
/**
* End a match: set status to finished.
*/
public function endMatch(Request $request, string $id): Response
{
$match = LiveMatch::find((int) $id);
if (!$match) {
return $this->redirect('/match-center')->withError('المباراة غير موجودة');
}
if ($match->status !== 'live') {
return $this->redirect('/match-center/' . $id)->withError('لا يمكن إنهاء مباراة غير جارية');
}
$match->update([
'status' => 'finished',
'finished_at' => date('Y-m-d H:i:s'),
]);
return $this->redirect('/match-center/' . $id)->withSuccess('تم إنهاء المباراة');
}
/**
* Live entry interface for pitch-side employees.
*/
public function liveEntry(Request $request, string $id): Response
{
$match = LiveMatch::find((int) $id);
if (!$match) {
return $this->redirect('/match-center')->withError('المباراة غير موجودة');
}
$events = MatchEvent::getByMatch((int) $id);
return $this->view('MatchCenter.Views.live_entry', [
'match' => $match,
'events' => $events,
'eventTypes' => MatchEvent::getEventTypeLabels(),
]);
}
/**
* Add an event to a match. If it's a goal, also increment the score.
*/
public function addEvent(Request $request, string $id): Response
{
$match = LiveMatch::find((int) $id);
if (!$match) {
return $this->redirect('/match-center')->withError('المباراة غير موجودة');
}
$minute = (int) $request->post('minute', 0);
$eventType = trim((string) $request->post('event_type', ''));
$team = trim((string) $request->post('team', ''));
$playerName = trim((string) $request->post('player_name', ''));
$descriptionAr = trim((string) $request->post('description_ar', ''));
// Validation
$validTypes = array_keys(MatchEvent::getEventTypeLabels());
if (!in_array($eventType, $validTypes, true)) {
return $this->redirect('/match-center/' . $id . '/live')->withError('نوع الحدث غير صالح');
}
$teamValue = null;
if (in_array($team, ['a', 'b'], true)) {
$teamValue = $team;
}
MatchEvent::create([
'match_id' => (int) $id,
'minute' => $minute,
'event_type' => $eventType,
'team' => $teamValue,
'player_name' => $playerName ?: null,
'description_ar' => $descriptionAr ?: null,
]);
// If it's a goal, increment the appropriate team's score
if ($eventType === 'goal' && $teamValue !== null) {
$db = App::getInstance()->db();
$scoreField = $teamValue === 'a' ? 'score_a' : 'score_b';
$db->query(
"UPDATE live_matches SET {$scoreField} = {$scoreField} + 1, updated_at = NOW() WHERE id = ?",
[(int) $id]
);
}
return $this->redirect('/match-center/' . $id . '/live')->withSuccess('تم إضافة الحدث');
}
/**
* Directly update the score of a match.
*/
public function updateScore(Request $request, string $id): Response
{
$match = LiveMatch::find((int) $id);
if (!$match) {
return $this->redirect('/match-center')->withError('المباراة غير موجودة');
}
$scoreA = max(0, (int) $request->post('score_a', 0));
$scoreB = max(0, (int) $request->post('score_b', 0));
$match->update([
'score_a' => $scoreA,
'score_b' => $scoreB,
]);
return $this->redirect('/match-center/' . $id . '/live')->withSuccess('تم تحديث النتيجة');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\MatchCenter\Models;
use App\Core\Model;
use App\Core\App;
class LiveMatch extends Model
{
protected static string $table = 'live_matches';
protected static bool $softDelete = false;
protected static array $fillable = [
'facility_id',
'discipline_id',
'tournament_match_id',
'match_title_ar',
'team_a_name',
'team_b_name',
'team_a_logo_path',
'team_b_logo_path',
'score_a',
'score_b',
'match_date',
'start_time',
'status',
'started_at',
'finished_at',
'referee_name',
'entry_employee_id',
'notes',
];
/**
* Search matches with optional status and date filters.
*/
public static function search(array $filters = [], int $perPage = 20, int $page = 1): array
{
$qb = static::query();
if (!empty($filters['status'])) {
$qb->where('status', '=', $filters['status']);
}
if (!empty($filters['date_from'])) {
$qb->where('match_date', '>=', $filters['date_from']);
}
if (!empty($filters['date_to'])) {
$qb->where('match_date', '<=', $filters['date_to']);
}
if (!empty($filters['q'])) {
$qb->where('match_title_ar', 'LIKE', '%' . $filters['q'] . '%');
}
$qb->orderBy('match_date', 'DESC')->orderBy('start_time', 'DESC');
return $qb->paginate($perPage, $page);
}
/**
* Get matches grouped by status for the dashboard.
*/
public static function getDashboardMatches(): array
{
$db = App::getInstance()->db();
$live = $db->select(
"SELECT * FROM live_matches WHERE status = 'live' ORDER BY started_at DESC"
);
$scheduled = $db->select(
"SELECT * FROM live_matches WHERE status = 'scheduled' ORDER BY match_date ASC, start_time ASC LIMIT 20"
);
$finished = $db->select(
"SELECT * FROM live_matches WHERE status = 'finished' ORDER BY finished_at DESC LIMIT 20"
);
return [
'live' => $live,
'scheduled' => $scheduled,
'finished' => $finished,
];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\MatchCenter\Models;
use App\Core\Model;
use App\Core\App;
class MatchEvent extends Model
{
protected static string $table = 'match_events';
protected static bool $softDelete = false;
protected static bool $timestamps = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'match_id',
'minute',
'event_type',
'team',
'player_name',
'description_ar',
];
/**
* Get all events for a given match, sorted by minute.
*/
public static function getByMatch(int $matchId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM match_events WHERE match_id = ? ORDER BY minute ASC, created_at ASC",
[$matchId]
);
}
/**
* Get event type labels in Arabic.
*/
public static function getEventTypeLabels(): array
{
return [
'goal' => 'هدف',
'foul' => 'خطأ',
'yellow_card' => 'بطاقة صفراء',
'red_card' => 'بطاقة حمراء',
'substitution' => 'تبديل',
'penalty' => 'ركلة جزاء',
'injury' => 'إصابة',
'half_time' => 'نهاية الشوط الأول',
'full_time' => 'نهاية المباراة',
'other' => 'أخرى',
];
}
/**
* Get event type icon (Lucide icon name).
*/
public static function getEventTypeIcon(string $type): string
{
return match ($type) {
'goal' => 'circle-dot',
'foul' => 'alert-triangle',
'yellow_card' => 'square',
'red_card' => 'square',
'substitution' => 'repeat',
'penalty' => 'target',
'injury' => 'heart-pulse',
'half_time' => 'pause',
'full_time' => 'flag-checkered',
default => 'info',
};
}
/**
* Get event type color.
*/
public static function getEventTypeColor(string $type): string
{
return match ($type) {
'goal' => '#10B981',
'foul' => '#F59E0B',
'yellow_card' => '#EAB308',
'red_card' => '#DC2626',
'substitution' => '#6366F1',
'penalty' => '#8B5CF6',
'injury' => '#EF4444',
'half_time' => '#6B7280',
'full_time' => '#6B7280',
default => '#9CA3AF',
};
}
}
<?php
declare(strict_types=1);
return [
['GET', '/match-center', 'MatchCenter\Controllers\MatchCenterController@dashboard', ['auth'], 'match_center.view'],
['GET', '/match-center/create', 'MatchCenter\Controllers\MatchCenterController@create', ['auth'], 'match_center.manage'],
['POST', '/match-center', 'MatchCenter\Controllers\MatchCenterController@store', ['auth', 'csrf'], 'match_center.manage'],
['GET', '/match-center/{id:\d+}', 'MatchCenter\Controllers\MatchCenterController@show', ['auth'], 'match_center.view'],
['POST', '/match-center/{id:\d+}/start', 'MatchCenter\Controllers\MatchCenterController@startMatch', ['auth', 'csrf'], 'match_center.live_entry'],
['POST', '/match-center/{id:\d+}/end', 'MatchCenter\Controllers\MatchCenterController@endMatch', ['auth', 'csrf'], 'match_center.live_entry'],
['GET', '/match-center/{id:\d+}/live', 'MatchCenter\Controllers\MatchCenterController@liveEntry', ['auth'], 'match_center.live_entry'],
['POST', '/match-center/{id:\d+}/event', 'MatchCenter\Controllers\MatchCenterController@addEvent', ['auth', 'csrf'], 'match_center.live_entry'],
['POST', '/match-center/{id:\d+}/score', 'MatchCenter\Controllers\MatchCenterController@updateScore',['auth', 'csrf'], 'match_center.live_entry'],
];
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>جدولة مباراة جديدة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/match-center" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة لمركز المباريات</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/match-center">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="trophy" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات المباراة</h3>
</div>
<div style="padding:20px;">
<div class="form-group" style="margin-bottom:15px;">
<label class="form-label">عنوان المباراة <span style="color:#DC2626;">*</span></label>
<input type="text" name="match_title_ar" value="<?= e(old('match_title_ar')) ?>" class="form-input" required placeholder="مثال: مباراة دوري الناشئين - الجولة الأولى">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:15px;">
<div class="form-group">
<label class="form-label">الفريق الأول <span style="color:#DC2626;">*</span></label>
<input type="text" name="team_a_name" value="<?= e(old('team_a_name')) ?>" class="form-input" required placeholder="اسم الفريق الأول">
</div>
<div class="form-group">
<label class="form-label">الفريق الثاني <span style="color:#DC2626;">*</span></label>
<input type="text" name="team_b_name" value="<?= e(old('team_b_name')) ?>" class="form-input" required placeholder="اسم الفريق الثاني">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-bottom:15px;">
<div class="form-group">
<label class="form-label">تاريخ المباراة <span style="color:#DC2626;">*</span></label>
<input type="date" name="match_date" value="<?= e(old('match_date')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">وقت البداية <span style="color:#DC2626;">*</span></label>
<input type="time" name="start_time" value="<?= e(old('start_time')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">اسم الحكم</label>
<input type="text" name="referee_name" value="<?= e(old('referee_name')) ?>" class="form-input" placeholder="اختياري">
</div>
</div>
<div class="form-group">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="3" placeholder="ملاحظات إضافية..."><?= e(old('notes')) ?></textarea>
</div>
</div>
</div>
<div style="display:flex;justify-content:flex-start;gap:10px;">
<button type="submit" class="btn btn-primary"><i data-lucide="calendar-plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> جدولة المباراة</button>
<a href="/match-center" class="btn btn-outline">إلغاء</a>
</div>
</form>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>مركز المباريات<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/match-center/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> جدولة مباراة جديدة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusLabels = [
'live' => 'مباشر',
'scheduled' => 'مجدولة',
'finished' => 'منتهية',
'cancelled' => 'ملغاة',
];
$statusColors = [
'live' => '#DC2626',
'scheduled' => '#2563EB',
'finished' => '#10B981',
'cancelled' => '#6B7280',
];
?>
<!-- Live Matches -->
<?php if (!empty($liveMatches)): ?>
<div style="margin-bottom:30px;">
<h2 style="font-size:18px;font-weight:700;color:#DC2626;margin-bottom:15px;display:flex;align-items:center;gap:8px;">
<span style="width:10px;height:10px;border-radius:50%;background:#DC2626;animation:pulse 1.5s infinite;"></span>
مباريات مباشرة (<?= count($liveMatches) ?>)
</h2>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(380px, 1fr));gap:20px;">
<?php foreach ($liveMatches as $m): ?>
<a href="/match-center/<?= (int) $m['id'] ?>" style="text-decoration:none;color:inherit;">
<div class="card" style="border:2px solid #FCA5A5;background:linear-gradient(135deg, #FEF2F2, #FFFFFF);transition:transform 0.2s;">
<div style="padding:20px;text-align:center;">
<div style="font-size:13px;color:#6B7280;margin-bottom:8px;"><?= e($m['match_title_ar']) ?></div>
<div style="display:flex;align-items:center;justify-content:center;gap:20px;">
<div style="flex:1;text-align:center;">
<div style="font-size:16px;font-weight:700;color:#1A1A2E;"><?= e($m['team_a_name']) ?></div>
</div>
<div style="font-size:32px;font-weight:900;color:#DC2626;min-width:80px;">
<?= (int) $m['score_a'] ?> - <?= (int) $m['score_b'] ?>
</div>
<div style="flex:1;text-align:center;">
<div style="font-size:16px;font-weight:700;color:#1A1A2E;"><?= e($m['team_b_name']) ?></div>
</div>
</div>
<div style="margin-top:10px;">
<span style="display:inline-block;padding:3px 12px;border-radius:12px;font-size:11px;font-weight:700;background:#FEE2E2;color:#DC2626;animation:pulse 1.5s infinite;">
مباشر
</span>
</div>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- Scheduled Matches -->
<?php if (!empty($scheduledMatches)): ?>
<div style="margin-bottom:30px;">
<h2 style="font-size:18px;font-weight:700;color:#2563EB;margin-bottom:15px;">
مباريات مجدولة (<?= count($scheduledMatches) ?>)
</h2>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(380px, 1fr));gap:20px;">
<?php foreach ($scheduledMatches as $m): ?>
<a href="/match-center/<?= (int) $m['id'] ?>" style="text-decoration:none;color:inherit;">
<div class="card" style="border:1px solid #BFDBFE;transition:transform 0.2s;">
<div style="padding:20px;text-align:center;">
<div style="font-size:13px;color:#6B7280;margin-bottom:8px;"><?= e($m['match_title_ar']) ?></div>
<div style="display:flex;align-items:center;justify-content:center;gap:20px;">
<div style="flex:1;text-align:center;">
<div style="font-size:16px;font-weight:700;color:#1A1A2E;"><?= e($m['team_a_name']) ?></div>
</div>
<div style="font-size:14px;font-weight:600;color:#2563EB;min-width:80px;">
VS
</div>
<div style="flex:1;text-align:center;">
<div style="font-size:16px;font-weight:700;color:#1A1A2E;"><?= e($m['team_b_name']) ?></div>
</div>
</div>
<div style="margin-top:10px;font-size:12px;color:#6B7280;">
<?= e($m['match_date']) ?> - <?= e($m['start_time']) ?>
</div>
<div style="margin-top:8px;">
<span style="display:inline-block;padding:3px 12px;border-radius:12px;font-size:11px;font-weight:600;background:#DBEAFE;color:#2563EB;">
مجدولة
</span>
</div>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- Finished Matches -->
<?php if (!empty($finishedMatches)): ?>
<div style="margin-bottom:30px;">
<h2 style="font-size:18px;font-weight:700;color:#10B981;margin-bottom:15px;">
مباريات منتهية (<?= count($finishedMatches) ?>)
</h2>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(380px, 1fr));gap:20px;">
<?php foreach ($finishedMatches as $m): ?>
<a href="/match-center/<?= (int) $m['id'] ?>" style="text-decoration:none;color:inherit;">
<div class="card" style="border:1px solid #A7F3D0;transition:transform 0.2s;">
<div style="padding:20px;text-align:center;">
<div style="font-size:13px;color:#6B7280;margin-bottom:8px;"><?= e($m['match_title_ar']) ?></div>
<div style="display:flex;align-items:center;justify-content:center;gap:20px;">
<div style="flex:1;text-align:center;">
<div style="font-size:16px;font-weight:700;color:#1A1A2E;"><?= e($m['team_a_name']) ?></div>
</div>
<div style="font-size:28px;font-weight:900;color:#10B981;min-width:80px;">
<?= (int) $m['score_a'] ?> - <?= (int) $m['score_b'] ?>
</div>
<div style="flex:1;text-align:center;">
<div style="font-size:16px;font-weight:700;color:#1A1A2E;"><?= e($m['team_b_name']) ?></div>
</div>
</div>
<div style="margin-top:10px;">
<span style="display:inline-block;padding:3px 12px;border-radius:12px;font-size:11px;font-weight:600;background:#D1FAE5;color:#10B981;">
منتهية
</span>
</div>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php if (empty($liveMatches) && empty($scheduledMatches) && empty($finishedMatches)): ?>
<div class="card" style="padding:60px;text-align:center;">
<i data-lucide="trophy" style="width:48px;height:48px;color:#D1D5DB;margin:0 auto 15px;display:block;"></i>
<p style="color:#6B7280;font-size:16px;margin-bottom:15px;">لا توجد مباريات حتى الآن</p>
<a href="/match-center/create" class="btn btn-primary">جدولة أول مباراة</a>
</div>
<?php endif; ?>
<style>
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
<?php $__template->endSection(); ?>
This diff is collapsed.
<?php
use App\Modules\MatchCenter\Models\MatchEvent;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>تفاصيل المباراة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/match-center" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة لمركز المباريات</a>
<?php if ($match->status === 'live'): ?>
<a href="/match-center/<?= (int) $match->id ?>/live" class="btn btn-primary"><i data-lucide="radio" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> الإدخال المباشر</a>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusLabels = [
'live' => 'مباشر',
'scheduled' => 'مجدولة',
'finished' => 'منتهية',
'cancelled' => 'ملغاة',
];
$statusColors = [
'live' => '#DC2626',
'scheduled' => '#2563EB',
'finished' => '#10B981',
'cancelled' => '#6B7280',
];
$currentStatus = $match->status ?? 'scheduled';
?>
<!-- Score Header -->
<div class="card" style="margin-bottom:20px;overflow:hidden;">
<div style="padding:30px;text-align:center;background:linear-gradient(135deg, #1A1A2E, #16213E);color:#fff;">
<div style="font-size:14px;color:#9CA3AF;margin-bottom:10px;"><?= e($match->match_title_ar) ?></div>
<div style="display:flex;align-items:center;justify-content:center;gap:30px;">
<div style="flex:1;text-align:center;">
<div style="font-size:22px;font-weight:700;"><?= e($match->team_a_name) ?></div>
</div>
<div style="font-size:48px;font-weight:900;min-width:120px;letter-spacing:4px;">
<?= (int) $match->score_a ?> - <?= (int) $match->score_b ?>
</div>
<div style="flex:1;text-align:center;">
<div style="font-size:22px;font-weight:700;"><?= e($match->team_b_name) ?></div>
</div>
</div>
<div style="margin-top:15px;">
<span style="display:inline-block;padding:4px 16px;border-radius:12px;font-size:12px;font-weight:700;background:<?= $statusColors[$currentStatus] ?>20;color:<?= $statusColors[$currentStatus] ?>;border:1px solid <?= $statusColors[$currentStatus] ?>50;<?= $currentStatus === 'live' ? 'animation:pulse 1.5s infinite;' : '' ?>">
<?= $statusLabels[$currentStatus] ?>
</span>
</div>
</div>
</div>
<!-- Match Info -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:15px;color:#374151;">معلومات المباراة</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:repeat(auto-fill, minmax(200px, 1fr));gap:15px;">
<div>
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">التاريخ</div>
<div style="font-size:14px;font-weight:600;color:#1A1A2E;"><?= e($match->match_date) ?></div>
</div>
<div>
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">وقت البداية</div>
<div style="font-size:14px;font-weight:600;color:#1A1A2E;"><?= e($match->start_time) ?></div>
</div>
<?php if ($match->referee_name): ?>
<div>
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">الحكم</div>
<div style="font-size:14px;font-weight:600;color:#1A1A2E;"><?= e($match->referee_name) ?></div>
</div>
<?php endif; ?>
<?php if ($match->started_at): ?>
<div>
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">بدأت في</div>
<div style="font-size:14px;font-weight:600;color:#1A1A2E;"><?= e($match->started_at) ?></div>
</div>
<?php endif; ?>
<?php if ($match->finished_at): ?>
<div>
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">انتهت في</div>
<div style="font-size:14px;font-weight:600;color:#1A1A2E;"><?= e($match->finished_at) ?></div>
</div>
<?php endif; ?>
</div>
<?php if ($match->notes): ?>
<div style="padding:0 20px 20px;">
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">ملاحظات</div>
<div style="font-size:14px;color:#374151;"><?= e($match->notes) ?></div>
</div>
<?php endif; ?>
</div>
<!-- Action Buttons -->
<?php if ($currentStatus === 'scheduled'): ?>
<div class="card" style="margin-bottom:20px;padding:20px;">
<form method="POST" action="/match-center/<?= (int) $match->id ?>/start" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('هل تريد بدء المباراة الآن؟')">
<i data-lucide="play" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> بدء المباراة
</button>
</form>
</div>
<?php elseif ($currentStatus === 'live'): ?>
<div class="card" style="margin-bottom:20px;padding:20px;display:flex;gap:10px;">
<a href="/match-center/<?= (int) $match->id ?>/live" class="btn btn-primary">
<i data-lucide="radio" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> الإدخال المباشر
</a>
<form method="POST" action="/match-center/<?= (int) $match->id ?>/end" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-outline" style="color:#DC2626;border-color:#DC2626;" onclick="return confirm('هل تريد إنهاء المباراة؟')">
<i data-lucide="square" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إنهاء المباراة
</button>
</form>
</div>
<?php endif; ?>
<!-- Events Timeline -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:15px;color:#374151;">أحداث المباراة</h3>
</div>
<div style="padding:20px;">
<?php if (empty($events)): ?>
<div style="text-align:center;padding:30px;color:#9CA3AF;">
<i data-lucide="clock" style="width:32px;height:32px;margin:0 auto 10px;display:block;"></i>
<p>لا توجد أحداث مسجلة حتى الآن</p>
</div>
<?php else: ?>
<div style="position:relative;padding-right:30px;">
<!-- Timeline line -->
<div style="position:absolute;right:12px;top:0;bottom:0;width:2px;background:#E5E7EB;"></div>
<?php foreach ($events as $event):
$evColor = MatchEvent::getEventTypeColor($event['event_type']);
$evIcon = MatchEvent::getEventTypeIcon($event['event_type']);
$evLabel = $eventTypes[$event['event_type']] ?? $event['event_type'];
$teamLabel = $event['team'] === 'a' ? $match->team_a_name : ($event['team'] === 'b' ? $match->team_b_name : '');
?>
<div style="position:relative;margin-bottom:20px;padding-right:25px;">
<!-- Timeline dot -->
<div style="position:absolute;right:-24px;top:4px;width:16px;height:16px;border-radius:50%;background:<?= $evColor ?>;display:flex;align-items:center;justify-content:center;">
<div style="width:6px;height:6px;border-radius:50%;background:#fff;"></div>
</div>
<div style="display:flex;align-items:start;gap:12px;background:#F9FAFB;border-radius:8px;padding:12px 15px;">
<div style="min-width:40px;text-align:center;">
<span style="font-size:18px;font-weight:700;color:<?= $evColor ?>;"><?= (int) $event['minute'] ?>'</span>
</div>
<div style="flex:1;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">
<i data-lucide="<?= e($evIcon) ?>" style="width:14px;height:14px;color:<?= $evColor ?>;"></i>
<span style="font-weight:600;font-size:13px;color:#374151;"><?= e($evLabel) ?></span>
<?php if ($teamLabel): ?>
<span style="font-size:11px;color:#6B7280;background:#E5E7EB;padding:2px 8px;border-radius:8px;"><?= e($teamLabel) ?></span>
<?php endif; ?>
</div>
<?php if ($event['player_name']): ?>
<div style="font-size:12px;color:#374151;"><?= e($event['player_name']) ?></div>
<?php endif; ?>
<?php if ($event['description_ar']): ?>
<div style="font-size:12px;color:#6B7280;margin-top:2px;"><?= e($event['description_ar']) ?></div>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<style>
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
// ────────────────────────────────────────────────────────────
// Match Center — Live match tracking & score entry
// ────────────────────────────────────────────────────────────
MenuRegistry::register('match_center', [
'label_ar' => 'مركز المباريات',
'label_en' => 'Match Center',
'icon' => 'futbol',
'route' => '/match-center',
'permission' => 'match_center.view',
'parent' => 'sports_activities',
'order' => 720,
]);
PermissionRegistry::register('match_center', [
'match_center.view' => ['ar' => 'عرض مركز المباريات', 'en' => 'View Match Center'],
'match_center.manage' => ['ar' => 'إدارة المباريات', 'en' => 'Manage Matches'],
'match_center.live_entry' => ['ar' => 'إدخال بيانات مباشرة', 'en' => 'Live Match Entry'],
]);
<?php
declare(strict_types=1);
namespace App\Modules\News\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\News\Models\NewsArticle;
class NewsController extends Controller
{
/**
* List articles with filters.
*/
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'category' => trim((string) $request->get('category', '')),
'is_published' => $request->get('is_published', ''),
];
$page = max(1, (int) $request->get('page', 1));
$result = NewsArticle::search($filters, 20, $page);
return $this->view('News.Views.index', [
'articles' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'categories' => NewsArticle::getCategoryLabels(),
]);
}
/**
* Show create article form.
*/
public function create(Request $request): Response
{
return $this->view('News.Views.create', [
'categories' => NewsArticle::getCategoryLabels(),
]);
}
/**
* Store a new article.
*/
public function store(Request $request): Response
{
$titleAr = trim((string) $request->post('title_ar', ''));
$bodyAr = trim((string) $request->post('body_ar', ''));
$category = trim((string) $request->post('category', 'general'));
// Validation
$errors = [];
if ($titleAr === '') {
$errors[] = 'عنوان الخبر مطلوب';
}
if ($bodyAr === '') {
$errors[] = 'محتوى الخبر مطلوب';
}
$validCategories = array_keys(NewsArticle::getCategoryLabels());
if (!in_array($category, $validCategories, true)) {
$errors[] = 'فئة الخبر غير صالحة';
}
// Handle cover image upload
$coverImagePath = null;
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
$coverImagePath = $this->handleImageUpload($_FILES['cover_image'], $errors);
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/news/create');
}
$employee = App::getInstance()->currentEmployee();
$article = NewsArticle::create([
'title_ar' => $titleAr,
'body_ar' => $bodyAr,
'category' => $category,
'cover_image_path' => $coverImagePath,
'is_published' => 0,
'created_by' => $employee ? ($employee->id ?? ($employee['id'] ?? null)) : null,
]);
return $this->redirect('/news/' . $article->id)->withSuccess('تم إنشاء الخبر بنجاح');
}
/**
* Show a single article.
*/
public function show(Request $request, string $id): Response
{
$article = NewsArticle::find((int) $id);
if (!$article) {
return $this->redirect('/news')->withError('الخبر غير موجود');
}
return $this->view('News.Views.show', [
'article' => $article,
'categories' => NewsArticle::getCategoryLabels(),
]);
}
/**
* Show edit form.
*/
public function edit(Request $request, string $id): Response
{
$article = NewsArticle::find((int) $id);
if (!$article) {
return $this->redirect('/news')->withError('الخبر غير موجود');
}
return $this->view('News.Views.edit', [
'article' => $article,
'categories' => NewsArticle::getCategoryLabels(),
]);
}
/**
* Update an article.
*/
public function update(Request $request, string $id): Response
{
$article = NewsArticle::find((int) $id);
if (!$article) {
return $this->redirect('/news')->withError('الخبر غير موجود');
}
$titleAr = trim((string) $request->post('title_ar', ''));
$bodyAr = trim((string) $request->post('body_ar', ''));
$category = trim((string) $request->post('category', 'general'));
// Validation
$errors = [];
if ($titleAr === '') {
$errors[] = 'عنوان الخبر مطلوب';
}
if ($bodyAr === '') {
$errors[] = 'محتوى الخبر مطلوب';
}
$updateData = [
'title_ar' => $titleAr,
'body_ar' => $bodyAr,
'category' => $category,
];
// Handle cover image upload
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
$coverImagePath = $this->handleImageUpload($_FILES['cover_image'], $errors);
if ($coverImagePath !== null) {
$updateData['cover_image_path'] = $coverImagePath;
}
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/news/' . $id . '/edit');
}
$article->update($updateData);
return $this->redirect('/news/' . $id)->withSuccess('تم تحديث الخبر بنجاح');
}
/**
* Toggle publish status.
*/
public function togglePublish(Request $request, string $id): Response
{
$article = NewsArticle::find((int) $id);
if (!$article) {
return $this->redirect('/news')->withError('الخبر غير موجود');
}
$isPublished = (int) $article->is_published;
if ($isPublished) {
// Unpublish
$article->update([
'is_published' => 0,
'published_at' => null,
]);
$message = 'تم إلغاء نشر الخبر';
} else {
// Publish
$article->update([
'is_published' => 1,
'published_at' => date('Y-m-d H:i:s'),
]);
$message = 'تم نشر الخبر بنجاح';
}
return $this->redirect('/news/' . $id)->withSuccess($message);
}
/**
* Handle image upload. Returns the saved path or null.
*/
private function handleImageUpload(array $file, array &$errors): ?string
{
$allowedTypes = ['image/jpeg', 'image/png', 'image/jpg'];
$maxSize = 5 * 1024 * 1024; // 5MB
if (!in_array($file['type'], $allowedTypes, true)) {
$errors[] = 'نوع الصورة غير مدعوم (يُسمح فقط بـ JPG و PNG)';
return null;
}
if ($file['size'] > $maxSize) {
$errors[] = 'حجم الصورة يتجاوز الحد المسموح (5 ميجابايت)';
return null;
}
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$filename = 'news_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$uploadDir = rtrim((string) ($_ENV['UPLOAD_PATH'] ?? 'public/uploads'), '/') . '/news';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$destPath = $uploadDir . '/' . $filename;
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
$errors[] = 'فشل في رفع الصورة';
return null;
}
return '/uploads/news/' . $filename;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\News\Models;
use App\Core\Model;
use App\Core\App;
class NewsArticle extends Model
{
protected static string $table = 'news_articles';
protected static bool $softDelete = false;
protected static array $fillable = [
'title_ar',
'body_ar',
'category',
'cover_image_path',
'related_match_id',
'related_facility_id',
'is_published',
'published_at',
'created_by',
];
/**
* Search articles with optional filters.
*/
public static function search(array $filters = [], int $perPage = 20, int $page = 1): array
{
$qb = static::query();
if (!empty($filters['category'])) {
$qb->where('category', '=', $filters['category']);
}
if (isset($filters['is_published']) && $filters['is_published'] !== '') {
$qb->where('is_published', '=', (int) $filters['is_published']);
}
if (!empty($filters['q'])) {
$qb->where('title_ar', 'LIKE', '%' . $filters['q'] . '%');
}
$qb->orderBy('created_at', 'DESC');
return $qb->paginate($perPage, $page);
}
/**
* Get category labels in Arabic.
*/
public static function getCategoryLabels(): array
{
return [
'match' => 'مباريات',
'facility' => 'مرافق',
'announcement' => 'إعلانات',
'schedule' => 'جداول',
'general' => 'عام',
];
}
/**
* Get category color.
*/
public static function getCategoryColor(string $category): string
{
return match ($category) {
'match' => '#DC2626',
'facility' => '#2563EB',
'announcement' => '#F59E0B',
'schedule' => '#8B5CF6',
'general' => '#6B7280',
default => '#9CA3AF',
};
}
}
<?php
declare(strict_types=1);
return [
['GET', '/news', 'News\Controllers\NewsController@index', ['auth'], 'news.view'],
['GET', '/news/create', 'News\Controllers\NewsController@create', ['auth'], 'news.manage'],
['POST', '/news', 'News\Controllers\NewsController@store', ['auth', 'csrf'], 'news.manage'],
['GET', '/news/{id:\d+}', 'News\Controllers\NewsController@show', ['auth'], 'news.view'],
['GET', '/news/{id:\d+}/edit', 'News\Controllers\NewsController@edit', ['auth'], 'news.manage'],
['POST', '/news/{id:\d+}', 'News\Controllers\NewsController@update', ['auth', 'csrf'], 'news.manage'],
['POST', '/news/{id:\d+}/toggle-publish','News\Controllers\NewsController@togglePublish',['auth', 'csrf'], 'news.manage'],
];
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إضافة خبر جديد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/news" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للأخبار</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/news" enctype="multipart/form-data">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="newspaper" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات الخبر</h3>
</div>
<div style="padding:20px;">
<div class="form-group" style="margin-bottom:15px;">
<label class="form-label">العنوان <span style="color:#DC2626;">*</span></label>
<input type="text" name="title_ar" value="<?= e(old('title_ar')) ?>" class="form-input" required placeholder="عنوان الخبر">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:15px;">
<div class="form-group">
<label class="form-label">الفئة <span style="color:#DC2626;">*</span></label>
<select name="category" class="form-select" required>
<?php foreach ($categories as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('category') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">صورة الغلاف</label>
<input type="file" name="cover_image" class="form-input" accept="image/jpeg,image/png" style="padding:8px;">
<small style="color:#9CA3AF;font-size:11px;">JPG أو PNG، الحد الأقصى 5 ميجابايت</small>
</div>
</div>
<div class="form-group">
<label class="form-label">المحتوى <span style="color:#DC2626;">*</span></label>
<textarea name="body_ar" class="form-input" rows="12" required placeholder="اكتب محتوى الخبر هنا..." style="line-height:1.8;"><?= e(old('body_ar')) ?></textarea>
</div>
</div>
</div>
<div style="display:flex;justify-content:flex-start;gap:10px;">
<button type="submit" class="btn btn-primary"><i data-lucide="save" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حفظ كمسودة</button>
<a href="/news" class="btn btn-outline">إلغاء</a>
</div>
</form>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل الخبر<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/news/<?= (int) $article->id ?>" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للخبر</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/news/<?= (int) $article->id ?>" enctype="multipart/form-data">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="edit" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">تعديل الخبر</h3>
</div>
<div style="padding:20px;">
<div class="form-group" style="margin-bottom:15px;">
<label class="form-label">العنوان <span style="color:#DC2626;">*</span></label>
<input type="text" name="title_ar" value="<?= e(old('title_ar', $article->title_ar)) ?>" class="form-input" required placeholder="عنوان الخبر">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:15px;">
<div class="form-group">
<label class="form-label">الفئة <span style="color:#DC2626;">*</span></label>
<select name="category" class="form-select" required>
<?php foreach ($categories as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('category', $article->category)) === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">صورة الغلاف</label>
<?php if ($article->cover_image_path): ?>
<div style="margin-bottom:8px;">
<img src="<?= e($article->cover_image_path) ?>" alt="الغلاف الحالي" style="max-width:200px;max-height:100px;border-radius:8px;border:1px solid #E5E7EB;">
</div>
<?php endif; ?>
<input type="file" name="cover_image" class="form-input" accept="image/jpeg,image/png" style="padding:8px;">
<small style="color:#9CA3AF;font-size:11px;">اترك فارغاً للإبقاء على الصورة الحالية | JPG أو PNG، الحد الأقصى 5 ميجابايت</small>
</div>
</div>
<div class="form-group">
<label class="form-label">المحتوى <span style="color:#DC2626;">*</span></label>
<textarea name="body_ar" class="form-input" rows="12" required placeholder="اكتب محتوى الخبر هنا..." style="line-height:1.8;"><?= e(old('body_ar', $article->body_ar)) ?></textarea>
</div>
</div>
</div>
<div style="display:flex;justify-content:flex-start;gap:10px;">
<button type="submit" class="btn btn-primary"><i data-lucide="save" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حفظ التعديلات</button>
<a href="/news/<?= (int) $article->id ?>" class="btn btn-outline">إلغاء</a>
</div>
</form>
<?php $__template->endSection(); ?>
<?php
use App\Modules\News\Models\NewsArticle;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>الأخبار<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/news/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة خبر جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px 20px;">
<form method="GET" action="/news" style="display:flex;flex-wrap:wrap;gap:10px;align-items:end;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="ابحث بالعنوان..." class="form-input">
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">الفئة</label>
<select name="category" class="form-select">
<option value="">الكل</option>
<?php foreach ($categories as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['category'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">حالة النشر</label>
<select name="is_published" class="form-select">
<option value="">الكل</option>
<option value="1" <?= ($filters['is_published'] ?? '') === '1' ? 'selected' : '' ?>>منشور</option>
<option value="0" <?= ($filters['is_published'] ?? '') === '0' ? 'selected' : '' ?>>مسودة</option>
</select>
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/news" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Articles Grid -->
<?php if (!empty($articles)): ?>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(350px, 1fr));gap:20px;margin-bottom:20px;">
<?php foreach ($articles as $article):
$catColor = NewsArticle::getCategoryColor($article['category'] ?? 'general');
$catLabel = $categories[$article['category'] ?? 'general'] ?? 'عام';
$isPublished = (int) ($article['is_published'] ?? 0);
?>
<a href="/news/<?= (int) $article['id'] ?>" style="text-decoration:none;color:inherit;">
<div class="card" style="overflow:hidden;transition:transform 0.2s,box-shadow 0.2s;height:100%;">
<?php if (!empty($article['cover_image_path'])): ?>
<div style="height:180px;background:url('<?= e($article['cover_image_path']) ?>') center/cover no-repeat;"></div>
<?php else: ?>
<div style="height:80px;background:linear-gradient(135deg, <?= $catColor ?>20, <?= $catColor ?>05);display:flex;align-items:center;justify-content:center;">
<i data-lucide="newspaper" style="width:32px;height:32px;color:<?= $catColor ?>;opacity:0.5;"></i>
</div>
<?php endif; ?>
<div style="padding:20px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $catColor ?>15;color:<?= $catColor ?>;"><?= e($catLabel) ?></span>
<?php if ($isPublished): ?>
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;background:#D1FAE5;color:#10B981;">منشور</span>
<?php else: ?>
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;background:#F3F4F6;color:#6B7280;">مسودة</span>
<?php endif; ?>
</div>
<h3 style="margin:0 0 8px;font-size:16px;font-weight:700;color:#1A1A2E;line-height:1.4;"><?= e($article['title_ar']) ?></h3>
<p style="margin:0;font-size:13px;color:#6B7280;line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">
<?= e(mb_substr(strip_tags($article['body_ar']), 0, 120)) ?>...
</p>
<div style="margin-top:12px;font-size:11px;color:#9CA3AF;">
<?= e($article['created_at'] ?? '') ?>
</div>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
<!-- Pagination -->
<?php if (isset($pagination) && $pagination['last_page'] > 1): ?>
<div style="display:flex;justify-content:center;gap:5px;margin-top:20px;">
<?php for ($p = 1; $p <= $pagination['last_page']; $p++): ?>
<a href="/news?page=<?= $p ?><?= ($filters['q'] ?? '') !== '' ? '&q=' . urlencode($filters['q']) : '' ?><?= ($filters['category'] ?? '') !== '' ? '&category=' . urlencode($filters['category']) : '' ?><?= ($filters['is_published'] ?? '') !== '' ? '&is_published=' . urlencode((string) $filters['is_published']) : '' ?>"
class="btn btn-sm <?= $p === ($pagination['current_page'] ?? 1) ? 'btn-primary' : 'btn-outline' ?>"
style="min-width:36px;text-align:center;">
<?= $p ?>
</a>
<?php endfor; ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px;text-align:center;">
<i data-lucide="newspaper" style="width:48px;height:48px;color:#D1D5DB;margin:0 auto 15px;display:block;"></i>
<p style="color:#6B7280;font-size:16px;margin-bottom:15px;">لا توجد أخبار حتى الآن</p>
<a href="/news/create" class="btn btn-primary">إضافة أول خبر</a>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php
use App\Modules\News\Models\NewsArticle;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?><?= e($article->title_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/news" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للأخبار</a>
<a href="/news/<?= (int) $article->id ?>/edit" class="btn btn-outline"><i data-lucide="edit" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعديل</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$catColor = NewsArticle::getCategoryColor($article->category ?? 'general');
$catLabel = $categories[$article->category ?? 'general'] ?? 'عام';
$isPublished = (int) $article->is_published;
?>
<!-- Cover Image -->
<?php if ($article->cover_image_path): ?>
<div class="card" style="margin-bottom:20px;overflow:hidden;">
<img src="<?= e($article->cover_image_path) ?>" alt="<?= e($article->title_ar) ?>" style="width:100%;max-height:400px;object-fit:cover;display:block;">
</div>
<?php endif; ?>
<!-- Article Content -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:30px;">
<!-- Meta -->
<div style="display:flex;align-items:center;gap:10px;margin-bottom:15px;">
<span style="display:inline-block;padding:3px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $catColor ?>15;color:<?= $catColor ?>;"><?= e($catLabel) ?></span>
<?php if ($isPublished): ?>
<span style="display:inline-block;padding:3px 12px;border-radius:10px;font-size:12px;font-weight:600;background:#D1FAE5;color:#10B981;">منشور</span>
<?php else: ?>
<span style="display:inline-block;padding:3px 12px;border-radius:10px;font-size:12px;font-weight:600;background:#F3F4F6;color:#6B7280;">مسودة</span>
<?php endif; ?>
<span style="font-size:12px;color:#9CA3AF;"><?= e($article->created_at ?? '') ?></span>
<?php if ($article->published_at): ?>
<span style="font-size:12px;color:#9CA3AF;">| نُشر: <?= e($article->published_at) ?></span>
<?php endif; ?>
</div>
<!-- Title -->
<h1 style="margin:0 0 20px;font-size:24px;font-weight:700;color:#1A1A2E;line-height:1.5;"><?= e($article->title_ar) ?></h1>
<!-- Body -->
<div style="font-size:15px;line-height:2;color:#374151;white-space:pre-wrap;"><?= e($article->body_ar) ?></div>
</div>
</div>
<!-- Action Buttons -->
<div class="card" style="padding:20px;display:flex;gap:10px;">
<form method="POST" action="/news/<?= (int) $article->id ?>/toggle-publish" style="display:inline;">
<?= csrf_field() ?>
<?php if ($isPublished): ?>
<button type="submit" class="btn btn-outline" style="color:#F59E0B;border-color:#F59E0B;">
<i data-lucide="eye-off" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إلغاء النشر
</button>
<?php else: ?>
<button type="submit" class="btn btn-primary" style="background:#10B981;">
<i data-lucide="send" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> نشر الخبر
</button>
<?php endif; ?>
</form>
<a href="/news/<?= (int) $article->id ?>/edit" class="btn btn-outline">
<i data-lucide="edit" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> تعديل
</a>
</div>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
// ────────────────────────────────────────────────────────────
// News — Club news articles and announcements
// ────────────────────────────────────────────────────────────
MenuRegistry::register('news', [
'label_ar' => 'الأخبار',
'label_en' => 'News',
'icon' => 'newspaper',
'route' => '/news',
'permission' => 'news.view',
'parent' => 'sports_activities',
'order' => 730,
]);
PermissionRegistry::register('news', [
'news.view' => ['ar' => 'عرض الأخبار', 'en' => 'View News'],
'news.manage' => ['ar' => 'إدارة الأخبار', 'en' => 'Manage News'],
]);
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$db->raw("
CREATE TABLE live_matches (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
facility_id BIGINT UNSIGNED NULL,
discipline_id BIGINT UNSIGNED NULL,
tournament_match_id BIGINT UNSIGNED NULL,
match_title_ar VARCHAR(200) NOT NULL,
team_a_name VARCHAR(100) NOT NULL,
team_b_name VARCHAR(100) NOT NULL,
team_a_logo_path VARCHAR(500) NULL,
team_b_logo_path VARCHAR(500) NULL,
score_a INT NOT NULL DEFAULT 0,
score_b INT NOT NULL DEFAULT 0,
match_date DATE NOT NULL,
start_time TIME NOT NULL,
status ENUM('scheduled','live','finished','cancelled') NOT NULL DEFAULT 'scheduled',
started_at DATETIME NULL,
finished_at DATETIME NULL,
referee_name VARCHAR(100) NULL,
entry_employee_id BIGINT UNSIGNED NULL,
notes TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_live_match_status (status),
INDEX idx_live_match_date (match_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
$db->raw("
CREATE TABLE match_events (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
match_id BIGINT UNSIGNED NOT NULL,
minute INT UNSIGNED NOT NULL DEFAULT 0,
event_type ENUM('goal','foul','yellow_card','red_card','substitution','penalty','injury','half_time','full_time','other') NOT NULL,
team ENUM('a','b') NULL,
player_name VARCHAR(100) NULL,
description_ar TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_match_event_match (match_id),
FOREIGN KEY (match_id) REFERENCES live_matches(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
};
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$db->raw("
CREATE TABLE news_articles (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title_ar VARCHAR(300) NOT NULL,
body_ar TEXT NOT NULL,
category ENUM('match','facility','announcement','schedule','general') NOT NULL DEFAULT 'general',
cover_image_path VARCHAR(500) NULL,
related_match_id BIGINT UNSIGNED NULL,
related_facility_id BIGINT UNSIGNED NULL,
is_published TINYINT(1) NOT NULL DEFAULT 0,
published_at DATETIME NULL,
created_by BIGINT UNSIGNED NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_news_published (is_published, published_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
};
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