Commit 0402e2b4 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: align remaining modules to actual Supabase column schemas

- ads: remove non-existent columns (advertiser, placement, cpm, title, body, click_url, image_url), use actual schema (campaign_type, target_slots, target_games, budget_daily)
- moderation: resolution→action_taken, resolved_by→reviewed_by (UUID), resolved_at→reviewed_at
- tournaments: el3ab_tournament_players→tournament_registrations, swiss_tournament_id→swiss_api_tournament_id
- players: remove email from insert (not in profiles), banned_at removed, use ban_expires_at
- notifications: remove is_broadcast (not in schema), detect broadcast by null user_id
- settings: is_editable→is_secret, handle JSONB value column
- auth: last_login→last_login_at
- Created el3ab_tournament_rounds table on server
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 367e98e5
......@@ -273,7 +273,7 @@ class Auth
try {
$db = Database::getInstance();
$db->update('admin_users', ['id' => 'eq.' . $userId], [
'last_login' => date('c'),
'last_login_at' => date('c'),
]);
} catch (\Throwable $e) {
// Non-critical — don't break login if this fails
......
......@@ -42,9 +42,7 @@ class AdsController
Auth::requireCsrf();
$validator = Validator::make($_POST)
->required('name', 'اسم الحملة')
->required('advertiser', 'المعلن')
->required('placement', 'الموضع');
->required('name', 'اسم الحملة');
if ($validator->fails()) {
Response::error($validator->firstError(), '/ads/create');
......@@ -53,21 +51,17 @@ class AdsController
$data = [
'name' => trim($_POST['name']),
'advertiser' => trim($_POST['advertiser']),
'placement' => $_POST['placement'],
'name_ar' => trim($_POST['name_ar'] ?? ''),
'campaign_type' => $_POST['campaign_type'] ?? 'cpm',
'status' => 'draft',
'title' => trim($_POST['title'] ?? ''),
'title_ar' => trim($_POST['title_ar'] ?? ''),
'body' => trim($_POST['body'] ?? ''),
'body_ar' => trim($_POST['body_ar'] ?? ''),
'click_url' => trim($_POST['click_url'] ?? ''),
'image_url' => trim($_POST['image_url'] ?? ''),
'target_countries' => $_POST['target_countries'] ?? '[]',
'target_games' => $_POST['target_games'] ?? '[]',
'target_slots' => !empty($_POST['target_slots']) ? '{' . implode(',', (array)$_POST['target_slots']) . '}' : '{}',
'target_games' => !empty($_POST['target_games']) ? '{' . implode(',', (array)$_POST['target_games']) . '}' : '{}',
'target_countries' => !empty($_POST['target_countries']) ? '{' . implode(',', (array)$_POST['target_countries']) . '}' : '{}',
'starts_at' => $_POST['starts_at'] ?: null,
'ends_at' => $_POST['ends_at'] ?: null,
'budget_total' => (int)($_POST['budget_total'] ?? 0),
'cpm' => (float)($_POST['cpm'] ?? 0),
'budget_daily' => (int)($_POST['budget_daily'] ?? 0),
'created_by' => Auth::user()['id'] ?? null,
];
$this->db->insert('ad_campaigns', $data);
......@@ -103,18 +97,15 @@ class AdsController
$data = [
'name' => trim($_POST['name']),
'advertiser' => trim($_POST['advertiser']),
'placement' => $_POST['placement'],
'title' => trim($_POST['title'] ?? ''),
'title_ar' => trim($_POST['title_ar'] ?? ''),
'body' => trim($_POST['body'] ?? ''),
'body_ar' => trim($_POST['body_ar'] ?? ''),
'click_url' => trim($_POST['click_url'] ?? ''),
'image_url' => trim($_POST['image_url'] ?? ''),
'name_ar' => trim($_POST['name_ar'] ?? ''),
'campaign_type' => $_POST['campaign_type'] ?? 'cpm',
'target_slots' => !empty($_POST['target_slots']) ? '{' . implode(',', (array)$_POST['target_slots']) . '}' : '{}',
'target_games' => !empty($_POST['target_games']) ? '{' . implode(',', (array)$_POST['target_games']) . '}' : '{}',
'target_countries' => !empty($_POST['target_countries']) ? '{' . implode(',', (array)$_POST['target_countries']) . '}' : '{}',
'starts_at' => $_POST['starts_at'] ?: null,
'ends_at' => $_POST['ends_at'] ?: null,
'budget_total' => (int)($_POST['budget_total'] ?? 0),
'cpm' => (float)($_POST['cpm'] ?? 0),
'budget_daily' => (int)($_POST['budget_daily'] ?? 0),
'updated_at' => date('c'),
];
......
......@@ -9,31 +9,47 @@
<form method="POST" action="<?= $isEdit ? "/ads/{$campaign['id']}/update" : '/ads/store' ?>" data-validate>
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<div class="grid grid-2 gap-4">
<div class="form-group"><label class="form-label">اسم الحملة *</label><input type="text" name="name" class="form-input" value="<?= View::e($campaign['name'] ?? '') ?>" required><span class="form-error"></span></div>
<div class="form-group"><label class="form-label">المعلن *</label><input type="text" name="advertiser" class="form-input" value="<?= View::e($campaign['advertiser'] ?? '') ?>" required></div>
<div class="form-group"><label class="form-label">اسم الحملة (English) *</label><input type="text" name="name" class="form-input" value="<?= View::e($campaign['name'] ?? '') ?>" required dir="ltr"><span class="form-error"></span></div>
<div class="form-group"><label class="form-label">اسم الحملة (عربي)</label><input type="text" name="name_ar" class="form-input" value="<?= View::e($campaign['name_ar'] ?? '') ?>"></div>
</div>
<div class="form-group">
<label class="form-label">الموضع *</label>
<select name="placement" class="form-select" required>
<?php $placements = ['banner_top' => 'بانر علوي', 'banner_bottom' => 'بانر سفلي', 'interstitial' => 'بيني', 'sidebar' => 'جانبي', 'in_game' => 'داخل اللعبة', 'reward_video' => 'فيديو مكافأة']; ?>
<?php foreach ($placements as $val => $label): ?>
<option value="<?= $val ?>" <?= ($campaign['placement'] ?? '') === $val ? 'selected' : '' ?>><?= $label ?></option>
<label class="form-label">نوع الحملة</label>
<select name="campaign_type" class="form-select">
<?php $types = ['cpm' => 'CPM - التكلفة لكل 1000 ظهور', 'cpc' => 'CPC - التكلفة لكل نقرة', 'flat' => 'ثابت - سعر ثابت']; ?>
<?php foreach ($types as $val => $label): ?>
<option value="<?= $val ?>" <?= ($campaign['campaign_type'] ?? 'cpm') === $val ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="grid grid-2 gap-4">
<div class="form-group"><label class="form-label">العنوان (English)</label><input type="text" name="title" class="form-input" value="<?= View::e($campaign['title'] ?? '') ?>" dir="ltr"></div>
<div class="form-group"><label class="form-label">العنوان (عربي)</label><input type="text" name="title_ar" class="form-input" value="<?= View::e($campaign['title_ar'] ?? '') ?>"></div>
<div class="form-group">
<label class="form-label">المواضع المستهدفة</label>
<select name="target_slots[]" class="form-select" multiple>
<?php
$slots = ['banner_top' => 'بانر علوي', 'banner_bottom' => 'بانر سفلي', 'interstitial' => 'بيني', 'sidebar' => 'جانبي', 'in_game' => 'داخل اللعبة', 'reward_video' => 'فيديو مكافأة'];
$selectedSlots = $campaign['target_slots'] ?? [];
foreach ($slots as $val => $label): ?>
<option value="<?= $val ?>" <?= in_array($val, (array)$selectedSlots) ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if (!empty($games)): ?>
<div class="form-group">
<label class="form-label">الألعاب المستهدفة</label>
<select name="target_games[]" class="form-select" multiple>
<?php $selectedGames = $campaign['target_games'] ?? []; ?>
<?php foreach ($games as $game): ?>
<option value="<?= View::e($game['game_key']) ?>" <?= in_array($game['game_key'], (array)$selectedGames) ? 'selected' : '' ?>><?= View::e($game['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group"><label class="form-label">رابط الصورة</label><input type="url" name="image_url" class="form-input" value="<?= View::e($campaign['image_url'] ?? '') ?>" dir="ltr"></div>
<div class="form-group"><label class="form-label">رابط النقر</label><input type="url" name="click_url" class="form-input" value="<?= View::e($campaign['click_url'] ?? '') ?>" dir="ltr"></div>
<?php endif; ?>
<div class="grid grid-2 gap-4">
<div class="form-group"><label class="form-label">تاريخ البدء</label><input type="datetime-local" name="starts_at" class="form-input" value="<?= $campaign['starts_at'] ? date('Y-m-d\TH:i', strtotime($campaign['starts_at'])) : '' ?>"></div>
<div class="form-group"><label class="form-label">تاريخ الانتهاء</label><input type="datetime-local" name="ends_at" class="form-input" value="<?= $campaign['ends_at'] ? date('Y-m-d\TH:i', strtotime($campaign['ends_at'])) : '' ?>"></div>
<div class="form-group"><label class="form-label">تاريخ البدء</label><input type="datetime-local" name="starts_at" class="form-input" value="<?= !empty($campaign['starts_at']) ? date('Y-m-d\TH:i', strtotime($campaign['starts_at'])) : '' ?>"></div>
<div class="form-group"><label class="form-label">تاريخ الانتهاء</label><input type="datetime-local" name="ends_at" class="form-input" value="<?= !empty($campaign['ends_at']) ? date('Y-m-d\TH:i', strtotime($campaign['ends_at'])) : '' ?>"></div>
</div>
<div class="grid grid-2 gap-4">
<div class="form-group"><label class="form-label">الميزانية الكلية</label><input type="number" name="budget_total" class="form-input" value="<?= $campaign['budget_total'] ?? 0 ?>" min="0"></div>
<div class="form-group"><label class="form-label">CPM</label><input type="number" name="cpm" class="form-input" value="<?= $campaign['cpm'] ?? 0 ?>" min="0" step="0.01"></div>
<div class="form-group"><label class="form-label">الميزانية اليومية</label><input type="number" name="budget_daily" class="form-input" value="<?= $campaign['budget_daily'] ?? 0 ?>" min="0"></div>
</div>
<div class="flex gap-3 mt-6">
<button type="submit" class="btn btn-primary"><?= $isEdit ? 'حفظ التعديلات' : 'إنشاء الحملة' ?></button>
......
......@@ -34,16 +34,16 @@
<tbody>
<?php foreach ($campaigns as $c): ?>
<tr>
<td class="font-medium"><?= View::e($c['name']) ?></td>
<td class="text-secondary"><?= View::e($c['advertiser']) ?></td>
<td><span class="badge badge-info"><?= View::e($c['placement']) ?></span></td>
<td class="font-medium"><?= View::e($c['name'] ?? '') ?></td>
<td class="text-secondary"><?= View::e($c['name_ar'] ?? '') ?></td>
<td><span class="badge badge-info"><?= View::e($c['campaign_type'] ?? 'cpm') ?></span></td>
<td>
<?php $sb = ['active' => 'success', 'paused' => 'warning', 'draft' => 'default', 'completed' => 'info', 'expired' => 'danger']; ?>
<span class="badge badge-<?= $sb[$c['status']] ?? 'default' ?> badge-dot"><?= View::e($c['status']) ?></span>
</td>
<td class="tabular-nums"><?= number_format($c['impressions'] ?? 0) ?></td>
<td class="tabular-nums"><?= number_format($c['clicks'] ?? 0) ?></td>
<td class="tabular-nums text-xs"><?= number_format($c['budget_spent'] ?? 0) ?> / <?= number_format($c['budget_total'] ?? 0) ?></td>
<td class="tabular-nums"><?= number_format($c['weight'] ?? 0) ?></td>
<td class="tabular-nums"><?= number_format($c['priority'] ?? 0) ?></td>
<td class="tabular-nums text-xs"><?= number_format($c['spent_total'] ?? 0) ?> / <?= number_format($c['budget_total'] ?? 0) ?></td>
<td>
<div class="flex gap-2">
<a href="/ads/<?= $c['id'] ?>/edit" class="btn btn-ghost btn-sm">تعديل</a>
......
......@@ -72,9 +72,9 @@ class ModerationController
$this->db->update('cheat_reports', ['id' => "eq.{$id}"], [
'status' => 'resolved',
'resolution' => $resolution,
'resolved_by' => Auth::user()['username'],
'resolved_at' => date('c'),
'action_taken' => $resolution,
'reviewed_by' => Auth::user()['id'],
'reviewed_at' => date('c'),
]);
AuditLog::log('resolve', 'report', $id, null, ['resolution' => $resolution]);
......@@ -89,9 +89,9 @@ class ModerationController
$this->db->update('cheat_reports', ['id' => "eq.{$id}"], [
'status' => 'dismissed',
'resolution' => $resolution,
'resolved_by' => Auth::user()['username'],
'resolved_at' => date('c'),
'action_taken' => $resolution,
'reviewed_by' => Auth::user()['id'],
'reviewed_at' => date('c'),
]);
AuditLog::log('dismiss', 'report', $id);
......@@ -107,9 +107,9 @@ class ModerationController
foreach ($ids as $id) {
$this->db->update('cheat_reports', ['id' => "eq.{$id}"], [
'status' => 'dismissed',
'resolution' => $reason,
'resolved_by' => Auth::user()['username'],
'resolved_at' => date('c'),
'action_taken' => $reason,
'reviewed_by' => Auth::user()['id'],
'reviewed_at' => date('c'),
]);
}
......
......@@ -46,7 +46,6 @@ class NotificationsController
'title_ar' => trim($_POST['title_ar'] ?? ''),
'body' => trim($_POST['body'] ?? ''),
'body_ar' => trim($_POST['body_ar'] ?? ''),
'is_broadcast' => false,
]);
AuditLog::log('send', 'notification', $_POST['user_id']);
......@@ -74,7 +73,6 @@ class NotificationsController
'title_ar' => trim($_POST['title_ar'] ?? ''),
'body' => trim($_POST['body'] ?? ''),
'body_ar' => trim($_POST['body_ar'] ?? ''),
'is_broadcast' => true,
]);
AuditLog::log('broadcast', 'notification', null, null, ['title' => $_POST['title']]);
......
......@@ -18,7 +18,7 @@
<td><span class="badge badge-info"><?= View::e($n['type']) ?></span></td>
<td><?= View::e($n['title_ar'] ?: $n['title']) ?></td>
<td class="text-xs"><?= $n['user_id'] ? substr($n['user_id'], 0, 8) . '...' : '-' ?></td>
<td><?= $n['is_broadcast'] ? '<span class="badge badge-purple">بث عام</span>' : '' ?></td>
<td><?= empty($n['user_id']) ? '<span class="badge badge-purple">بث عام</span>' : '' ?></td>
<td class="text-xs tabular-nums"><?= date('m/d H:i', strtotime($n['created_at'])) ?></td>
<td><button class="btn btn-ghost btn-sm" style="color:var(--danger);" onclick="confirmDelete('/notifications/<?= $n['id'] ?>/delete','إشعار')">حذف</button></td>
</tr>
......
......@@ -109,7 +109,6 @@ class PlayersController
'username' => trim($_POST['username']),
'display_name' => trim($_POST['display_name'] ?? ''),
'display_name_ar' => trim($_POST['display_name_ar'] ?? ''),
'email' => trim($_POST['email'] ?? ''),
'country_code' => trim($_POST['country_code'] ?? ''),
'city' => trim($_POST['city'] ?? ''),
'bio' => trim($_POST['bio'] ?? ''),
......@@ -191,8 +190,7 @@ class PlayersController
$this->db->update('profiles', ['id' => "eq.{$id}"], [
'is_banned' => true,
'ban_reason' => $reason,
'banned_at' => date('c'),
'banned_by' => Auth::user()['username'],
'banned_by' => Auth::user()['id'],
]);
AuditLog::log('ban', 'player', $id, null, ['reason' => $reason]);
......@@ -207,7 +205,7 @@ class PlayersController
$this->db->update('profiles', ['id' => "eq.{$id}"], [
'is_banned' => false,
'ban_reason' => null,
'banned_at' => null,
'ban_expires_at' => null,
'banned_by' => null,
]);
......
......@@ -102,7 +102,7 @@
<h3 class="card-title">معلومات</h3>
</div>
<div class="flex flex-col gap-3 text-sm">
<div class="flex justify-between"><span class="text-secondary">البريد</span><span><?= View::e($player['email'] ?? '-') ?></span></div>
<div class="flex justify-between"><span class="text-secondary">اللغة</span><span><?= View::e($player['preferred_language'] ?? '-') ?></span></div>
<div class="flex justify-between"><span class="text-secondary">الدولة</span><span><?= View::e($player['country_code'] ?? '-') ?></span></div>
<div class="flex justify-between"><span class="text-secondary">المدينة</span><span><?= View::e($player['city'] ?? '-') ?></span></div>
<div class="flex justify-between"><span class="text-secondary">تاريخ الانضمام</span><span class="tabular-nums"><?= $player['created_at'] ? date('Y-m-d', strtotime($player['created_at'])) : '-' ?></span></div>
......
......@@ -48,14 +48,14 @@ class SettingsController
return;
}
if (!$setting['is_editable']) {
Response::error('هذا الإعداد غير قابل للتعديل', '/settings');
if ($setting['is_secret'] ?? false) {
Response::error('هذا الإعداد محمي', '/settings');
return;
}
$old = $setting['value'];
$this->db->update('system_config', ['key' => "eq.{$key}"], [
'value' => $value,
'value' => json_encode($value),
'updated_at' => date('c'),
]);
......
......@@ -32,13 +32,17 @@
<?php endif; ?>
</div>
<div class="flex items-center gap-3">
<?php if ($setting['value_type'] === 'boolean'): ?>
<?php
$val = is_array($setting['value']) ? json_encode($setting['value']) : (string)($setting['value'] ?? '');
$editable = !($setting['is_secret'] ?? false);
?>
<?php if ($val === 'true' || $val === 'false'): ?>
<form method="POST" action="/settings/update">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<input type="hidden" name="key" value="<?= View::e($setting['key']) ?>">
<input type="hidden" name="value" value="<?= $setting['value'] === 'true' ? 'false' : 'true' ?>">
<input type="hidden" name="value" value="<?= $val === 'true' ? 'false' : 'true' ?>">
<label class="toggle">
<input type="checkbox" <?= $setting['value'] === 'true' ? 'checked' : '' ?> <?= !$setting['is_editable'] ? 'disabled' : '' ?> onchange="this.closest('form').submit()">
<input type="checkbox" <?= $val === 'true' ? 'checked' : '' ?> <?= !$editable ? 'disabled' : '' ?> onchange="this.closest('form').submit()">
<span class="toggle-track"></span>
</label>
</form>
......@@ -46,8 +50,8 @@
<form method="POST" action="/settings/update" class="flex items-center gap-2">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<input type="hidden" name="key" value="<?= View::e($setting['key']) ?>">
<input type="<?= $setting['value_type'] === 'number' ? 'number' : 'text' ?>" name="value" class="form-input" style="width: 200px; padding: var(--space-2) var(--space-3);" value="<?= View::e($setting['value']) ?>" <?= !$setting['is_editable'] ? 'disabled' : '' ?> dir="ltr">
<?php if ($setting['is_editable']): ?>
<input type="text" name="value" class="form-input" style="width: 200px; padding: var(--space-2) var(--space-3);" value="<?= View::e($val) ?>" <?= !$editable ? 'disabled' : '' ?> dir="ltr">
<?php if ($editable): ?>
<button type="submit" class="btn btn-primary btn-sm">حفظ</button>
<?php endif; ?>
</form>
......
......@@ -60,15 +60,15 @@ class TournamentsController
// Fetch standings from Swiss API if tournament is in progress or completed
$standings = [];
if (in_array($tournament['status'], ['in_progress', 'completed']) && !empty($tournament['swiss_tournament_id'])) {
$response = ApiProxy::swiss('GET', '/tournaments/' . $tournament['swiss_tournament_id'] . '/standings');
if (in_array($tournament['status'], ['in_progress', 'completed']) && !empty($tournament['swiss_api_tournament_id'])) {
$response = ApiProxy::swiss('GET', '/tournaments/' . $tournament['swiss_api_tournament_id'] . '/standings');
if ($response['status'] === 200 && is_array($response['body'])) {
$standings = $response['body'];
}
}
// Fetch registered players
$players = $this->db->select('el3ab_tournament_players', [
$players = $this->db->select('tournament_registrations', [
'tournament_id' => "eq.{$id}",
'order' => 'registered_at.asc',
]);
......@@ -186,7 +186,7 @@ class TournamentsController
'status' => 'draft',
'swiss_org_id' => $swissOrgId,
'swiss_event_id' => $swissEventId,
'swiss_tournament_id' => $swissTournamentId,
'swiss_api_tournament_id' => $swissTournamentId,
'created_by' => $_SESSION['user']['username'] ?? 'system',
];
......@@ -284,16 +284,17 @@ class TournamentsController
}
// Register players in Swiss API
if (!empty($tournament['swiss_tournament_id'])) {
$players = $this->db->select('el3ab_tournament_players', [
if (!empty($tournament['swiss_api_tournament_id'])) {
$players = $this->db->select('tournament_registrations', [
'tournament_id' => "eq.{$id}",
]);
foreach ($players as $player) {
ApiProxy::swiss('POST', '/tournaments/' . $tournament['swiss_tournament_id'] . '/players', [
$profile = $this->db->selectOne('profiles', ['id' => "eq.{$player['player_id']}"]);
ApiProxy::swiss('POST', '/tournaments/' . $tournament['swiss_api_tournament_id'] . '/players', [
'id' => $player['player_id'],
'name' => $player['player_name'] ?? $player['player_id'],
'rating' => $player['rating'] ?? 1500,
'name' => $profile['display_name'] ?? $profile['username'] ?? $player['player_id'],
'rating' => $profile['elo_blitz'] ?? 1500,
]);
}
}
......@@ -375,13 +376,13 @@ class TournamentsController
return;
}
if (empty($tournament['swiss_tournament_id'])) {
if (empty($tournament['swiss_api_tournament_id'])) {
Response::error('لا يوجد ربط مع نظام السويسري', '/tournaments/' . $id);
return;
}
// Call Swiss API to generate round
$response = ApiProxy::swiss('POST', '/tournaments/' . $tournament['swiss_tournament_id'] . '/rounds/generate');
$response = ApiProxy::swiss('POST', '/tournaments/' . $tournament['swiss_api_tournament_id'] . '/rounds/generate');
if ($response['status'] !== 200 && $response['status'] !== 201) {
$errorMsg = $response['body']['message'] ?? 'فشل في إنشاء الجولة';
......@@ -495,12 +496,12 @@ class TournamentsController
return;
}
if (empty($tournament['swiss_tournament_id'])) {
if (empty($tournament['swiss_api_tournament_id'])) {
Response::json(['error' => 'لا يوجد ربط مع نظام السويسري'], 400);
return;
}
$response = ApiProxy::swiss('GET', '/tournaments/' . $tournament['swiss_tournament_id'] . '/standings');
$response = ApiProxy::swiss('GET', '/tournaments/' . $tournament['swiss_api_tournament_id'] . '/standings');
if ($response['status'] === 200) {
Response::json([
......
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