Commit 0188bd9e authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: comprehensive push notification management system

Adds campaign management, demographic targeting, individual player
notifications, reusable templates, scheduling, and campaign history.
All notifications delivered via Supabase Realtime (insert into
notifications table).
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 62273d44
...@@ -189,11 +189,25 @@ return [ ...@@ -189,11 +189,25 @@ return [
// Analytics // Analytics
'analytics' => ['module' => 'analytics', 'action' => 'index'], 'analytics' => ['module' => 'analytics', 'action' => 'index'],
// Notifications // Notifications / Push Management
'notifications' => ['module' => 'notifications', 'action' => 'list'], 'notifications' => ['module' => 'notifications', 'action' => 'list'],
'notifications/send' => ['module' => 'notifications', 'action' => 'send'], 'notifications/compose' => ['module' => 'notifications', 'action' => 'compose'],
'notifications/broadcast' => ['module' => 'notifications', 'action' => 'broadcast'], 'notifications/compose/preview' => ['module' => 'notifications', 'action' => 'previewCount'],
'notifications/compose/send' => ['module' => 'notifications', 'action' => 'send'],
'notifications/compose/schedule' => ['module' => 'notifications', 'action' => 'schedule'],
'notifications/individual' => ['module' => 'notifications', 'action' => 'individual'],
'notifications/individual/send' => ['module' => 'notifications', 'action' => 'sendIndividual'],
'notifications/templates' => ['module' => 'notifications', 'action' => 'templates'],
'notifications/templates/create' => ['module' => 'notifications', 'action' => 'createTemplate'],
'notifications/templates/store' => ['module' => 'notifications', 'action' => 'storeTemplate'],
'notifications/templates/{id}/edit' => ['module' => 'notifications', 'action' => 'editTemplate'],
'notifications/templates/{id}/update' => ['module' => 'notifications', 'action' => 'updateTemplate'],
'notifications/templates/{id}/delete' => ['module' => 'notifications', 'action' => 'deleteTemplate'],
'notifications/scheduled' => ['module' => 'notifications', 'action' => 'scheduled'],
'notifications/scheduled/{id}/cancel' => ['module' => 'notifications', 'action' => 'cancelScheduled'],
'notifications/{id}' => ['module' => 'notifications', 'action' => 'show'],
'notifications/{id}/delete' => ['module' => 'notifications', 'action' => 'delete'], 'notifications/{id}/delete' => ['module' => 'notifications', 'action' => 'delete'],
'api/notifications/search-players' => ['module' => 'notifications', 'action' => 'searchPlayers'],
// Audit Log // Audit Log
'audit-log' => ['module' => 'audit-log', 'action' => 'index'], 'audit-log' => ['module' => 'audit-log', 'action' => 'index'],
......
-- Push notification campaign management tables
-- These tables track push notification campaigns sent from the management panel
CREATE TABLE IF NOT EXISTS push_campaigns (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
title_ar TEXT NOT NULL,
body_ar TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'broadcast' CHECK (type IN ('broadcast', 'demographic', 'individual')),
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'sending', 'sent', 'scheduled', 'cancelled', 'failed')),
notification_type TEXT NOT NULL DEFAULT 'announcement',
filters_json JSONB NULL,
target_count INT NOT NULL DEFAULT 0,
sent_count INT NOT NULL DEFAULT 0,
failed_count INT NOT NULL DEFAULT 0,
scheduled_at TIMESTAMPTZ NULL,
sent_at TIMESTAMPTZ NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_push_campaigns_status ON push_campaigns(status);
CREATE INDEX idx_push_campaigns_scheduled ON push_campaigns(scheduled_at) WHERE status = 'scheduled';
CREATE TABLE IF NOT EXISTS push_campaign_recipients (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
campaign_id UUID NOT NULL REFERENCES push_campaigns(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
display_name TEXT NULL,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed')),
error_message TEXT NULL,
sent_at TIMESTAMPTZ NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_push_recipients_campaign ON push_campaign_recipients(campaign_id);
CREATE INDEX idx_push_recipients_user ON push_campaign_recipients(user_id);
CREATE TABLE IF NOT EXISTS push_templates (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
title_ar TEXT NOT NULL,
body_ar TEXT NOT NULL,
notification_type TEXT NOT NULL DEFAULT 'announcement',
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Seed default templates
INSERT INTO push_templates (name, title_ar, body_ar, notification_type) VALUES
('تحديث جديد', 'تحديث جديد! 🎮', 'تم إضافة ميزات جديدة للتطبيق، تعال شوف!', 'update'),
('بطولة جديدة', 'بطولة جديدة! 🏆', 'بطولة جديدة متاحة الآن، سجّل واربح جوائز!', 'tournament_start'),
('مكافأة يومية', 'مكافأتك اليومية جاهزة! 🎁', 'ادخل واستلم مكافأتك اليومية قبل ما تنتهي.', 'daily_reward'),
('عرض خاص', 'عرض خاص لفترة محدودة! 💎', 'خصم حصري في المتجر، لا تفوّت الفرصة!', 'promotion'),
('صيانة', 'صيانة مجدولة ⚙️', 'سيتم إجراء صيانة للخوادم. نعتذر عن أي إزعاج.', 'maintenance');
-- RLS policies (service role bypasses, but good practice)
ALTER TABLE push_campaigns ENABLE ROW LEVEL SECURITY;
ALTER TABLE push_campaign_recipients ENABLE ROW LEVEL SECURITY;
ALTER TABLE push_templates ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Service role full access on push_campaigns" ON push_campaigns FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY "Service role full access on push_campaign_recipients" ON push_campaign_recipients FOR ALL USING (auth.role() = 'service_role');
CREATE POLICY "Service role full access on push_templates" ON push_templates FOR ALL USING (auth.role() = 'service_role');
This diff is collapsed.
<div class="content-header">
<h1><?= View::e($campaign['title_ar'] ?? 'تفاصيل الحملة') ?></h1>
<a href="/notifications" class="btn btn-ghost">← العودة</a>
</div>
<div class="card p-6 mb-5">
<div class="flex justify-between items-start mb-5">
<div>
<p class="text-secondary text-sm">
<?= match($campaign['type'] ?? '') { 'broadcast' => 'بث عام', 'demographic' => 'فئة محددة', 'individual' => 'فردي', default => $campaign['type'] ?? '-' } ?>
— أُنشئت <?= date('Y/m/d H:i', strtotime($campaign['created_at'])) ?>
</p>
</div>
<?php $statusClass = match($campaign['status'] ?? '') { 'sent' => 'badge-success', 'scheduled' => 'badge-warning', 'failed' => 'badge-danger', default => 'badge-info' }; ?>
<span class="badge <?= $statusClass ?>"><?= match($campaign['status'] ?? '') { 'sent' => 'تم الإرسال', 'scheduled' => 'مجدول', 'sending' => 'جاري', 'failed' => 'فشل', 'cancelled' => 'ملغي', default => $campaign['status'] ?? '-' } ?></span>
</div>
<div class="card-inner p-4 mb-4" style="background:var(--bg-elevated);border-radius:10px;">
<div class="text-xs text-secondary mb-1">نص الإشعار</div>
<div class="text-sm"><?= nl2br(View::e($campaign['body_ar'] ?? '')) ?></div>
</div>
<div class="stats-row">
<div class="stat-card stat-card-sm">
<div class="stat-value" style="color:var(--success)"><?= number_format($campaign['sent_count'] ?? 0) ?></div>
<div class="stat-label">مرسل</div>
</div>
<div class="stat-card stat-card-sm">
<div class="stat-value" style="color:var(--danger)"><?= number_format($campaign['failed_count'] ?? 0) ?></div>
<div class="stat-label">فشل</div>
</div>
<div class="stat-card stat-card-sm">
<div class="stat-value" style="color:var(--info)"><?= number_format($campaign['target_count'] ?? 0) ?></div>
<div class="stat-label">المستهدف</div>
</div>
<div class="stat-card stat-card-sm">
<div class="stat-value text-sm"><?= View::e($campaign['notification_type'] ?? '-') ?></div>
<div class="stat-label">النوع</div>
</div>
</div>
<?php if (!empty($campaign['scheduled_at'])): ?>
<div class="alert alert-warning mt-4">مجدول في: <?= View::e($campaign['scheduled_at']) ?></div>
<?php endif; ?>
<?php if (!empty($campaign['filters_json'])): ?>
<div class="mt-4">
<div class="text-xs text-secondary mb-2">الفلاتر المستخدمة:</div>
<div class="flex gap-2 flex-wrap">
<?php foreach (json_decode($campaign['filters_json'], true) ?? [] as $k => $v): ?>
<span class="badge badge-purple"><?= View::e($k) ?>: <?= View::e((string) $v) ?></span>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<div class="flex gap-3 mt-5">
<?php if (($campaign['status'] ?? '') === 'scheduled'): ?>
<form method="POST" action="/notifications/scheduled/<?= $campaign['id'] ?>/cancel">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('إلغاء؟')">إلغاء الجدولة</button>
</form>
<?php endif; ?>
<form method="POST" action="/notifications/<?= $campaign['id'] ?>/delete">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-ghost btn-sm" style="color:var(--danger)" onclick="return confirm('حذف نهائي؟')">حذف</button>
</form>
</div>
</div>
<?php if (!empty($recipients)): ?>
<div class="card">
<div class="card-header"><h3 class="text-sm font-bold">المستلمون (<?= count($recipients) ?>)</h3></div>
<table class="data-table">
<thead><tr><th>اللاعب</th><th>UUID</th><th>الحالة</th><th>وقت الإرسال</th></tr></thead>
<tbody>
<?php foreach ($recipients as $r): ?>
<tr>
<td><?= View::e($r['display_name'] ?? '—') ?></td>
<td class="text-xs tabular-nums text-secondary"><?= View::e(substr($r['user_id'] ?? '', 0, 12)) ?>...</td>
<td><span class="badge <?= ($r['status'] ?? '') === 'sent' ? 'badge-success' : 'badge-danger' ?>"><?= ($r['status'] ?? '') === 'sent' ? 'مرسل' : 'فشل' ?></span></td>
<td class="text-xs"><?= $r['sent_at'] ? date('m/d H:i', strtotime($r['sent_at'])) : '—' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<style>.stat-card-sm{padding:12px!important;}.stat-card-sm .stat-value{font-size:20px!important;}</style>
<div class="content-header">
<h1>إشعارات التطبيق</h1>
<div class="flex gap-3">
<a href="/notifications/compose" class="btn btn-primary">إرسال إشعار</a>
<a href="/notifications/individual" class="btn btn-ghost">إرسال لفرد</a>
</div>
</div>
<!-- Stats Cards -->
<div class="stats-row mb-5">
<div class="stat-card">
<div class="stat-value"><?= number_format($stats['total']) ?></div>
<div class="stat-label">إجمالي اللاعبين</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color:var(--success)"><?= number_format($stats['active_today']) ?></div>
<div class="stat-label">نشط اليوم</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color:var(--info)"><?= number_format($stats['active_week']) ?></div>
<div class="stat-label">نشط هذا الأسبوع</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color:var(--purple)"><?= number_format($stats['active_month']) ?></div>
<div class="stat-label">نشط هذا الشهر</div>
</div>
</div>
<!-- Filter Pills -->
<div class="filter-pills mb-5">
<a href="/notifications" class="filter-pill <?= empty($statusFilter) ? 'active' : '' ?>">الكل</a>
<a href="/notifications?status=sent" class="filter-pill <?= $statusFilter === 'sent' ? 'active' : '' ?>">مرسل</a>
<a href="/notifications?status=scheduled" class="filter-pill <?= $statusFilter === 'scheduled' ? 'active' : '' ?>">مجدول</a>
<a href="/notifications?status=failed" class="filter-pill <?= $statusFilter === 'failed' ? 'active' : '' ?>">فشل</a>
</div>
<div class="data-table-wrapper">
<?php if (empty($campaigns)): ?>
<div class="empty-state">
<h3 class="empty-state-title">لا توجد حملات</h3>
<p class="empty-state-text">ابدأ بإرسال أول إشعار لللاعبين</p>
<a href="/notifications/compose" class="btn btn-primary mt-4">إرسال إشعار</a>
</div>
<?php else: ?>
<table class="data-table">
<thead>
<tr>
<th>التاريخ</th>
<th>العنوان</th>
<th>النوع</th>
<th>المستهدف</th>
<th>المرسل</th>
<th>الحالة</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($campaigns as $c): ?>
<tr>
<td class="text-xs tabular-nums"><?= date('m/d H:i', strtotime($c['created_at'])) ?></td>
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"><?= View::e($c['title_ar']) ?></td>
<td>
<?php $typeBadge = match($c['type'] ?? '') { 'broadcast' => 'badge-info', 'demographic' => 'badge-warning', 'individual' => 'badge-purple', default => '' }; ?>
<span class="badge <?= $typeBadge ?>"><?= match($c['type'] ?? '') { 'broadcast' => 'بث عام', 'demographic' => 'فئة', 'individual' => 'فردي', default => $c['type'] ?? '-' } ?></span>
</td>
<td class="tabular-nums"><?= number_format($c['target_count'] ?? 0) ?></td>
<td class="tabular-nums"><?= number_format($c['sent_count'] ?? 0) ?></td>
<td>
<?php $statusClass = match($c['status'] ?? '') { 'sent' => 'badge-success', 'scheduled' => 'badge-warning', 'sending' => 'badge-info', 'failed' => 'badge-danger', default => '' }; ?>
<span class="badge <?= $statusClass ?>"><?= match($c['status'] ?? '') { 'sent' => 'مرسل', 'scheduled' => 'مجدول', 'sending' => 'جاري', 'failed' => 'فشل', 'cancelled' => 'ملغي', default => $c['status'] ?? '-' } ?></span>
</td>
<td><a href="/notifications/<?= $c['id'] ?>" class="btn btn-ghost btn-sm">تفاصيل</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<div class="table-footer">
<span><?= $pagination->rangeText() ?></span>
<div class="pagination">
<?php foreach ($pagination->pages() as $p): ?>
<a href="?page=<?= $p ?>&status=<?= View::e($statusFilter) ?>" class="pagination-btn <?= $p === $pagination->page ? 'active' : '' ?>"><?= $p ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
This diff is collapsed.
<div class="content-header">
<h1>إرسال إشعار لفرد</h1>
<a href="/notifications" class="btn btn-ghost">← العودة</a>
</div>
<div style="max-width:700px;">
<div class="card p-6">
<form method="POST" action="/notifications/individual/send">
<?= Auth::csrfField() ?>
<!-- Player Search -->
<div class="form-group mb-4">
<label class="form-label">ابحث عن اللاعب <span class="text-danger">*</span></label>
<div style="position:relative;">
<input type="text" id="player-search" class="form-input" placeholder="اسم اللاعب أو UUID..." autocomplete="off">
<div id="search-results" style="display:none;position:absolute;top:100%;left:0;right:0;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;box-shadow:var(--shadow-lg);max-height:250px;overflow-y:auto;z-index:50;"></div>
</div>
<input type="hidden" name="user_id" id="selected-user-id">
<input type="hidden" name="display_name" id="selected-display-name">
</div>
<!-- Selected Player -->
<div id="selected-player" class="alert alert-success mb-4" style="display:none;">
<div class="flex justify-between items-center">
<div>
<strong id="sel-name"></strong>
<div id="sel-info" class="text-xs text-secondary mt-1"></div>
</div>
<button type="button" onclick="clearSelection()" class="btn btn-ghost btn-sm" style="color:var(--danger);"></button>
</div>
</div>
<!-- Template -->
<div class="form-group mb-4">
<label class="form-label">القالب (اختياري)</label>
<select id="template-select" class="form-select" onchange="applyTemplate()">
<option value="">— إشعار مخصص —</option>
<?php foreach ($templates as $t): ?>
<option value="<?= View::e($t['id']) ?>" data-title="<?= View::e($t['title_ar']) ?>" data-body="<?= View::e($t['body_ar']) ?>" data-type="<?= View::e($t['notification_type']) ?>"><?= View::e($t['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- Title -->
<div class="form-group mb-4">
<label class="form-label">عنوان الإشعار <span class="text-danger">*</span></label>
<input type="text" name="title_ar" id="title_ar" class="form-input" required maxlength="200" placeholder="عنوان الإشعار...">
</div>
<!-- Body -->
<div class="form-group mb-4">
<label class="form-label">نص الإشعار <span class="text-danger">*</span></label>
<textarea name="body_ar" id="body_ar" class="form-textarea" rows="3" required maxlength="500" placeholder="نص الإشعار..."></textarea>
</div>
<!-- Type -->
<div class="form-group mb-5">
<label class="form-label">نوع الإشعار</label>
<select name="notification_type" class="form-select">
<option value="announcement">إعلان</option>
<option value="match_invite">دعوة لعب</option>
<option value="daily_reward">مكافأة</option>
<option value="promotion">عرض خاص</option>
<option value="tournament_start">بطولة</option>
<option value="update">تحديث</option>
<option value="maintenance">صيانة</option>
</select>
</div>
<button type="submit" class="btn btn-primary" onclick="return validateForm()">إرسال الإشعار</button>
</form>
</div>
</div>
<script>
let searchTimeout;
const searchInput=document.getElementById('player-search'),resultsDiv=document.getElementById('search-results');
searchInput.addEventListener('input',function(){
clearTimeout(searchTimeout);const q=this.value.trim();
if(q.length<2){resultsDiv.style.display='none';return;}
searchTimeout=setTimeout(async()=>{
try{
const r=await fetch('/api/notifications/search-players?q='+encodeURIComponent(q));const d=await r.json();
if(d.players&&d.players.length>0){
resultsDiv.innerHTML=d.players.map(p=>`<div onclick="selectPlayer('${p.id}','${(p.display_name||'').replace(/'/g,"\\'")}','${p.level||0}','${p.games_played||0}','${p.country||''}')" style="padding:10px 14px;cursor:pointer;border-bottom:1px solid var(--border);" onmouseover="this.style.background='var(--bg-elevated)'" onmouseout="this.style.background=''"><div class="font-bold text-sm">${p.display_name||'بدون اسم'}</div><div class="text-xs text-secondary">${p.id.substring(0,8)}... • Lv.${p.level||0}${p.games_played||0} لعبة${p.country?' • '+p.country:''}</div></div>`).join('');
resultsDiv.style.display='block';
}else{resultsDiv.innerHTML='<div style="padding:14px;text-align:center;" class="text-secondary text-sm">لم يتم العثور على لاعبين</div>';resultsDiv.style.display='block';}
}catch(e){resultsDiv.style.display='none';}
},400);
});
document.addEventListener('click',e=>{if(!e.target.closest('#player-search')&&!e.target.closest('#search-results'))resultsDiv.style.display='none';});
function selectPlayer(id,name,level,games,country){
document.getElementById('selected-user-id').value=id;document.getElementById('selected-display-name').value=name;
document.getElementById('sel-name').textContent=name||'بدون اسم';
document.getElementById('sel-info').textContent=`${id} • Lv.${level}${games} لعبة`+(country?` • ${country}`:'');
document.getElementById('selected-player').style.display='block';resultsDiv.style.display='none';searchInput.value=name;
}
function clearSelection(){document.getElementById('selected-user-id').value='';document.getElementById('selected-player').style.display='none';searchInput.value='';}
function applyTemplate(){const s=document.getElementById('template-select'),o=s.options[s.selectedIndex];if(o.value){document.getElementById('title_ar').value=o.dataset.title||'';document.getElementById('body_ar').value=o.dataset.body||'';}}
function validateForm(){if(!document.getElementById('selected-user-id').value){alert('يرجى اختيار لاعب أولاً');return false;}return confirm('تأكيد إرسال الإشعار؟');}
</script>
<div class="content-header">
<h1>الإشعارات المجدولة</h1>
<a href="/notifications/compose" class="btn btn-primary">جدولة إشعار</a>
</div>
<?php if (!empty($upcoming)): ?>
<div class="card mb-5">
<div class="card-header" style="background:var(--warning-bg);border-bottom:1px solid var(--border);">
<h3 class="text-sm font-bold" style="color:var(--warning);">قادمة (<?= count($upcoming) ?>)</h3>
</div>
<table class="data-table">
<thead><tr><th>وقت الإرسال</th><th>العنوان</th><th>النوع</th><th></th></tr></thead>
<tbody>
<?php foreach ($upcoming as $s): ?>
<tr>
<td class="font-bold tabular-nums"><?= date('Y/m/d H:i', strtotime($s['scheduled_at'])) ?></td>
<td><?= View::e($s['title_ar']) ?></td>
<td><span class="badge badge-info"><?= match($s['type'] ?? '') { 'broadcast' => 'بث عام', 'demographic' => 'فئة', default => $s['type'] ?? '-' } ?></span></td>
<td>
<form method="POST" action="/notifications/scheduled/<?= $s['id'] ?>/cancel" style="display:inline;">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-ghost btn-sm" style="color:var(--danger)" onclick="return confirm('إلغاء؟')">إلغاء</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div class="card p-8 text-center mb-5">
<div style="font-size:40px;margin-bottom:12px;">📅</div>
<p class="text-secondary">لا توجد إشعارات مجدولة</p>
</div>
<?php endif; ?>
<?php if (!empty($past)): ?>
<div class="card">
<div class="card-header"><h3 class="text-sm font-bold text-secondary">سابقة</h3></div>
<table class="data-table">
<thead><tr><th>التاريخ</th><th>العنوان</th><th>الحالة</th><th>المرسل</th></tr></thead>
<tbody>
<?php foreach ($past as $p): ?>
<tr>
<td class="text-xs tabular-nums"><?= date('m/d H:i', strtotime($p['scheduled_at'])) ?></td>
<td><?= View::e($p['title_ar']) ?></td>
<td><span class="badge <?= ($p['status'] ?? '') === 'sent' ? 'badge-success' : '' ?>"><?= ($p['status'] ?? '') === 'sent' ? 'مرسل' : 'ملغي' ?></span></td>
<td class="tabular-nums"><?= number_format($p['sent_count'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<div class="content-header">
<h1><?= $template ? 'تعديل القالب' : 'قالب جديد' ?></h1>
<a href="/notifications/templates" class="btn btn-ghost">← العودة</a>
</div>
<div style="max-width:600px;">
<div class="card p-6">
<form method="POST" action="<?= $template ? "/notifications/templates/{$template['id']}/update" : '/notifications/templates/store' ?>">
<?= Auth::csrfField() ?>
<div class="form-group mb-4">
<label class="form-label">اسم القالب <span class="text-danger">*</span></label>
<input type="text" name="name" class="form-input" required value="<?= View::e($template['name'] ?? '') ?>" placeholder="مثال: إعلان تحديث">
</div>
<div class="form-group mb-4">
<label class="form-label">عنوان الإشعار <span class="text-danger">*</span></label>
<input type="text" name="title_ar" class="form-input" required maxlength="200" value="<?= View::e($template['title_ar'] ?? '') ?>">
</div>
<div class="form-group mb-4">
<label class="form-label">نص الإشعار <span class="text-danger">*</span></label>
<textarea name="body_ar" class="form-textarea" rows="4" required maxlength="500"><?= View::e($template['body_ar'] ?? '') ?></textarea>
</div>
<div class="form-group mb-4">
<label class="form-label">نوع الإشعار</label>
<select name="notification_type" class="form-select">
<?php $types = ['announcement' => 'إعلان', 'update' => 'تحديث', 'tournament_start' => 'بطولة', 'daily_reward' => 'مكافأة', 'promotion' => 'عرض خاص', 'maintenance' => 'صيانة', 'match_invite' => 'دعوة لعب']; ?>
<?php foreach ($types as $val => $label): ?>
<option value="<?= $val ?>" <?= ($template['notification_type'] ?? '') === $val ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if ($template): ?>
<div class="form-group mb-4">
<label class="form-label">الحالة</label>
<select name="is_active" class="form-select">
<option value="1" <?= ($template['is_active'] ?? true) ? 'selected' : '' ?>>نشط</option>
<option value="0" <?= !($template['is_active'] ?? true) ? 'selected' : '' ?>>معطّل</option>
</select>
</div>
<?php endif; ?>
<button type="submit" class="btn btn-primary"><?= $template ? 'حفظ التعديلات' : 'إنشاء القالب' ?></button>
</form>
</div>
</div>
<div class="content-header">
<h1>قوالب الإشعارات</h1>
<a href="/notifications/templates/create" class="btn btn-primary">قالب جديد</a>
</div>
<div class="data-table-wrapper">
<?php if (empty($templates)): ?>
<div class="empty-state">
<h3 class="empty-state-title">لا توجد قوالب</h3>
<p class="empty-state-text">أنشئ قوالب لتسريع إرسال الإشعارات المتكررة</p>
</div>
<?php else: ?>
<table class="data-table">
<thead><tr><th>الاسم</th><th>العنوان</th><th>النوع</th><th>الحالة</th><th></th></tr></thead>
<tbody>
<?php foreach ($templates as $t): ?>
<tr>
<td class="font-bold"><?= View::e($t['name']) ?></td>
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"><?= View::e($t['title_ar']) ?></td>
<td><span class="badge badge-info"><?= View::e($t['notification_type']) ?></span></td>
<td><span class="badge <?= ($t['is_active'] ?? false) ? 'badge-success' : 'badge-danger' ?>"><?= ($t['is_active'] ?? false) ? 'نشط' : 'معطّل' ?></span></td>
<td class="flex gap-2">
<a href="/notifications/templates/<?= $t['id'] ?>/edit" class="btn btn-ghost btn-sm">تعديل</a>
<form method="POST" action="/notifications/templates/<?= $t['id'] ?>/delete" style="display:inline;">
<?= Auth::csrfField() ?>
<button type="submit" class="btn btn-ghost btn-sm" style="color:var(--danger)" onclick="return confirm('حذف؟')">حذف</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
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