Commit 952e2ce6 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: writes that named columns the database does not have

Every one of these was rejected by PostgREST and reported as success
(found by checking each insert/update/select against the live schema):

- workflows: approval_requests has reviewed_by/review_notes, not
  reviewer_id/review_note; reviewers and requesters are staff uuids, so the
  pages now show their names, "my requests" filters by the account id, and
  search no longer runs ilike on a uuid. workflow_rules has event_type, not
  event; created_by is a uuid; conditions/actions go in as jsonb lists.
- RuleEngine: event_type, and its notify action writes a real notifications
  row (user_id, title_ar/body_ar) only when addressed to a player.
- moderation: cheat_reports.reviewed_by references profiles, so a staff
  reviewer is stored in the new reviewed_by_admin column.
- users: admin_users has no created_by.
- org-transfers, org-frames, org-chat: org_members has player_id (not
  user_id) and no status column. Membership logic is unchanged.
- org-achievements: an org achievement cannot be stored in
  player_achievements (its achievement_id references the game's
  achievements); granting now says so instead of failing the insert and
  still paying the reward. Progress reads completed/completed_at.
- org-loyalty, org-partnerships: those tables have no updated_at.
- live pages: phases are in tournament_phases ordered by phase_number; the
  arena tables never existed.
Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
parent ef9854c7
......@@ -69,7 +69,7 @@ class RuleEngine
try {
$db = Database::getInstance();
return $db->select('workflow_rules', [
'event' => 'eq.' . $event,
'event_type' => 'eq.' . $event,
'is_active' => 'eq.true',
'order' => 'priority.desc',
]);
......@@ -106,7 +106,9 @@ class RuleEngine
private static function evaluateSingleCondition(array $condition, array $context): bool
{
$field = $condition['field'] ?? null;
$op = $condition['op'] ?? '=';
// The rule form saves 'operator' with eq/neq/gt..., older rules 'op' with symbols.
$op = $condition['op'] ?? $condition['operator'] ?? '=';
$op = ['eq' => '=', 'neq' => '!=', 'gt' => '>', 'lt' => '<', 'gte' => '>=', 'lte' => '<='][$op] ?? $op;
$expected = $condition['value'] ?? null;
if ($field === null) {
......@@ -179,20 +181,24 @@ class RuleEngine
*/
private static function actionNotify(array $params, string $event, array $context): void
{
try {
$db = Database::getInstance();
$db->insert('notifications', [
'recipient' => $params['to'] ?? 'admin',
'title' => $params['title'] ?? 'Rule triggered: ' . $event,
'message' => self::interpolate($params['message'] ?? '', $context),
'type' => 'rule_engine',
'event' => $event,
'is_read' => false,
'created_at' => date('c'),
]);
} catch (\Throwable $e) {
// Non-critical
// notifications is the players' in-game bell: it needs a player id
// (user_id), and the game reads title_ar/body_ar. Staff have no inbox
// there, so a rule addressed to 'admin' has nowhere to land.
$to = self::interpolate((string)($params['to'] ?? ''), $context);
if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $to)) {
return;
}
$title = $params['title'] ?? 'Rule triggered: ' . $event;
$body = self::interpolate($params['message'] ?? '', $context);
Database::getInstance()->soft(fn(Database $db) => $db->insert('notifications', [
'user_id' => $to,
'type' => 'system',
'title' => $title,
'title_ar' => $params['title_ar'] ?? $title,
'body' => $body,
'body_ar' => $body,
'data' => ['event' => $event, 'source' => 'rule_engine'],
]));
}
/**
......
......@@ -293,26 +293,21 @@ class LiveDataService
public function getPhases(string $tournamentId): array
{
return $this->db->select('el3ab_tournament_phases', [
// The table is tournament_phases, ordered by phase_number.
return $this->db->select('tournament_phases', [
'tournament_id' => "eq.{$tournamentId}",
'order' => 'phase_order.asc',
'order' => 'phase_number.asc',
]);
}
/**
* Arena games and standings. There are no arena tables (el3ab_arena_games
* and el3ab_arena_standings never existed) and the player app has no arena
* mode, so there is nothing to show.
*/
public function getArenaData(string $tournamentId): array
{
$games = $this->db->select('el3ab_arena_games', [
'tournament_id' => "eq.{$tournamentId}",
'order' => 'created_at.desc',
'limit' => 100,
]);
$standings = $this->db->select('el3ab_arena_standings', [
'tournament_id' => "eq.{$tournamentId}",
'order' => 'score.desc',
]);
return ['games' => $games, 'standings' => $standings];
return ['games' => [], 'standings' => []];
}
public function getStats(array $tournament): array
......
......@@ -73,7 +73,7 @@ class ModerationController
$this->db->update('cheat_reports', ['id' => "eq.{$id}"], [
'status' => 'resolved',
'action_taken' => $resolution,
'reviewed_by' => Auth::user()['id'],
'reviewed_by_admin' => Auth::user()['id'] ?? null, // reviewed_by references profiles; staff are admin_users
'reviewed_at' => date('c'),
]);
......@@ -90,7 +90,7 @@ class ModerationController
$this->db->update('cheat_reports', ['id' => "eq.{$id}"], [
'status' => 'dismissed',
'action_taken' => $resolution,
'reviewed_by' => Auth::user()['id'],
'reviewed_by_admin' => Auth::user()['id'] ?? null, // reviewed_by references profiles; staff are admin_users
'reviewed_at' => date('c'),
]);
......@@ -108,7 +108,7 @@ class ModerationController
$this->db->update('cheat_reports', ['id' => "eq.{$id}"], [
'status' => 'dismissed',
'action_taken' => $reason,
'reviewed_by' => Auth::user()['id'],
'reviewed_by_admin' => Auth::user()['id'] ?? null, // reviewed_by references profiles; staff are admin_users
'reviewed_at' => date('c'),
]);
}
......
......@@ -210,57 +210,12 @@ class OrgAchievementsController
return;
}
$existing = $this->db->selectOne('player_achievements', [
'player_id' => "eq.{$playerId}",
'achievement_id' => "eq.{$achievementId}",
]);
if ($existing && !($achievement['is_repeatable'] ?? false)) {
Response::error('اللاعب يملك هذا الإنجاز بالفعل', '/org-achievements');
return;
}
if (!$existing) {
$this->db->insert('player_achievements', [
'player_id' => $playerId,
'achievement_id' => $achievementId,
'org_id' => $achievement['org_id'],
'is_complete' => true,
'progress' => ($achievement['criteria'] ? json_decode($achievement['criteria'], true)['threshold'] ?? 1 : 1),
]);
} else {
$this->db->update('player_achievements', [
'player_id' => "eq.{$playerId}",
'achievement_id' => "eq.{$achievementId}",
], ['is_complete' => true, 'earned_at' => date('c')]);
}
if (($achievement['reward_coins'] ?? 0) > 0 || ($achievement['reward_gems'] ?? 0) > 0) {
$player = $this->db->selectOne('profiles', ['id' => "eq.{$playerId}"]);
$updates = [];
if ($achievement['reward_coins'] > 0) {
$updates['coins'] = ($player['coins'] ?? 0) + $achievement['reward_coins'];
}
if ($achievement['reward_gems'] > 0) {
$updates['gems'] = ($player['gems'] ?? 0) + $achievement['reward_gems'];
}
if (!empty($updates)) {
$this->db->update('profiles', ['id' => "eq.{$playerId}"], $updates);
}
}
$this->db->insert('notifications', [
'user_id' => $playerId,
'title' => 'إنجاز جديد!',
'body' => "حصلت على إنجاز: {$achievement['name']}",
'type' => 'social',
]);
AuditLog::log('grant_achievement', 'player_achievement', null, null, [
'player_id' => $playerId,
'achievement_id' => $achievementId,
]);
Response::success('تم منح الإنجاز', '/org-achievements');
// player_achievements.achievement_id references the game's achievements
// table (text ids such as "first_win"); an org achievement's uuid can never
// be stored there, so this used to fail the insert and still pay the
// reward and send the notification. There is no table for granted org
// achievements yet, so refuse rather than pretend.
Response::error('منح إنجازات المنظمات غير متاح بعد: جدول إنجازات اللاعبين يقبل إنجازات اللعبة فقط، ولا يوجد جدول لإنجازات المنظمات الممنوحة', '/org-achievements');
}
public function playerProgress(array $params, string $method): void
......@@ -268,13 +223,11 @@ class OrgAchievementsController
$playerId = $_GET['player_id'] ?? '';
$orgId = $_GET['org_id'] ?? '';
$queryParams = ['select' => '*', 'order' => 'earned_at.desc'];
// player_achievements has completed_at (no earned_at) and no org_id.
$queryParams = ['select' => '*', 'order' => 'completed_at.desc.nullslast'];
if ($playerId) {
$queryParams['player_id'] = "eq.{$playerId}";
}
if ($orgId) {
$queryParams['org_id'] = "eq.{$orgId}";
}
$total = $this->db->count('player_achievements', $queryParams);
$pagination = Pagination::fromRequest($total);
......
......@@ -87,15 +87,15 @@
</div>
</td>
<td>
<?php if ($item['is_complete'] ?? false): ?>
<?php if ($item['completed'] ?? false): ?>
<span class="badge badge-success badge-dot">مكتمل</span>
<?php else: ?>
<span class="badge badge-warning badge-dot">قيد التقدم</span>
<?php endif; ?>
</td>
<td>
<?php if (!empty($item['earned_at'])): ?>
<span class="text-xs tabular-nums"><?= date('Y-m-d H:i', strtotime($item['earned_at'])) ?></span>
<?php if (!empty($item['completed_at'])): ?>
<span class="text-xs tabular-nums"><?= date('Y-m-d H:i', strtotime($item['completed_at'])) ?></span>
<?php else: ?>
<span class="text-muted text-xs">-</span>
<?php endif; ?>
......
......@@ -262,7 +262,7 @@ class OrgChatController
}
$mutedMembers = $this->db->select('org_members', [
'select' => 'user_id,display_name,is_muted,muted_until',
'select' => 'player_id,display_name,is_muted,muted_until',
'org_id' => "eq.{$orgId}",
'is_muted' => 'eq.true',
]);
......@@ -290,7 +290,7 @@ class OrgChatController
$this->db->update('org_members', [
'org_id' => "eq.{$orgId}",
'user_id' => "eq.{$playerId}",
'player_id' => "eq.{$playerId}",
], [
'is_muted' => true,
'muted_until' => $expiresAt,
......@@ -318,7 +318,7 @@ class OrgChatController
$this->db->update('org_members', [
'org_id' => "eq.{$orgId}",
'user_id' => "eq.{$playerId}",
'player_id' => "eq.{$playerId}",
], [
'is_muted' => false,
'muted_until' => null,
......@@ -349,7 +349,7 @@ class OrgChatController
$this->db->update('org_members', [
'org_id' => "eq.{$orgId}",
'user_id' => "eq.{$playerId}",
'player_id' => "eq.{$playerId}",
], [
'is_muted' => true,
'muted_until' => '2099-12-31T23:59:59Z',
......
......@@ -319,22 +319,22 @@ class OrgFramesController
return;
}
// org_members: player_id, and every row is an active membership.
$members = $this->db->select('org_members', [
'select' => 'user_id',
'select' => 'player_id',
'org_id' => "eq.{$orgId}",
'status' => 'eq.active',
]);
$assigned = 0;
foreach ($members as $member) {
$existing = $this->db->selectOne('player_frames', [
'player_id' => "eq.{$member['user_id']}",
'player_id' => "eq.{$member['player_id']}",
'frame_id' => "eq.{$frameId}",
]);
if (!$existing) {
$this->db->insert('player_frames', [
'player_id' => $member['user_id'],
'player_id' => $member['player_id'],
'frame_id' => $frameId,
'acquisition_type' => 'org_membership',
'source_id' => "org:{$orgId}",
......
......@@ -139,7 +139,6 @@ class OrgLoyaltyController
'reward_frame_id' => !empty($_POST['reward_frame_id']) ? $_POST['reward_frame_id'] : null,
'reward_title' => trim($_POST['reward_title'] ?? ''),
'sort_order' => (int) ($_POST['sort_order'] ?? 0),
'updated_at' => date('c'),
];
$this->db->update('org_loyalty_rewards', ['id' => "eq.{$rewardId}"], $data);
......@@ -167,7 +166,6 @@ class OrgLoyaltyController
$newStatus = !($reward['is_active'] ?? false);
$this->db->update('org_loyalty_rewards', ['id' => "eq.{$rewardId}"], [
'is_active' => $newStatus,
'updated_at' => date('c'),
]);
AuditLog::log('toggle', 'org_loyalty_reward', $rewardId, $reward, ['is_active' => $newStatus]);
......
......@@ -113,7 +113,6 @@ class OrgPartnershipsController
'status' => 'active',
'started_at' => date('c'),
'accepted_by' => $user['id'],
'updated_at' => date('c'),
]);
AuditLog::log('approve', 'org_partnership', $id, $partnership, ['status' => 'active']);
......@@ -140,7 +139,6 @@ class OrgPartnershipsController
$this->db->update('org_partnerships', ['id' => "eq.{$id}"], [
'status' => 'rejected',
'updated_at' => date('c'),
]);
AuditLog::log('reject', 'org_partnership', $id, $partnership, ['status' => 'rejected']);
......@@ -168,7 +166,6 @@ class OrgPartnershipsController
$this->db->update('org_partnerships', ['id' => "eq.{$id}"], [
'status' => 'dissolved',
'ended_at' => date('c'),
'updated_at' => date('c'),
]);
AuditLog::log('dissolve', 'org_partnership', $id, $partnership, ['status' => 'dissolved']);
......
......@@ -89,9 +89,10 @@ class OrgTransfersController
}
if (empty($fromOrgId)) {
// org_members keys a member by player_id and has no status column:
// a row is a membership.
$currentMembership = $this->db->selectOne('org_members', [
'user_id' => "eq.{$playerId}",
'status' => 'eq.active',
'player_id' => "eq.{$playerId}",
]);
$fromOrgId = $currentMembership['org_id'] ?? null;
}
......@@ -119,15 +120,15 @@ class OrgTransfersController
if ($fromOrgId) {
$this->db->delete('org_members', [
'org_id' => "eq.{$fromOrgId}",
'user_id' => "eq.{$playerId}",
'player_id' => "eq.{$playerId}",
]);
}
$this->db->insert('org_members', [
'org_id' => $toOrgId,
'user_id' => $playerId,
'player_id' => $playerId,
'role' => 'member',
'status' => 'active',
'joined_via' => 'transfer',
'joined_at' => date('c'),
]);
}
......@@ -206,21 +207,21 @@ class OrgTransfersController
if ($transfer['from_org_id']) {
$this->db->delete('org_members', [
'org_id' => "eq.{$transfer['from_org_id']}",
'user_id' => "eq.{$transfer['player_id']}",
'player_id' => "eq.{$transfer['player_id']}",
]);
}
$existing = $this->db->selectOne('org_members', [
'org_id' => "eq.{$transfer['to_org_id']}",
'user_id' => "eq.{$transfer['player_id']}",
'player_id' => "eq.{$transfer['player_id']}",
]);
if (!$existing) {
$this->db->insert('org_members', [
'org_id' => $transfer['to_org_id'],
'user_id' => $transfer['player_id'],
'player_id' => $transfer['player_id'],
'role' => 'member',
'status' => 'active',
'joined_via' => 'transfer',
'joined_at' => date('c'),
]);
}
......@@ -231,10 +232,13 @@ class OrgTransfersController
'updated_at' => date('c'),
]);
// The game's bell shows title_ar/body_ar.
$this->db->insert('notifications', [
'user_id' => $transfer['player_id'],
'title' => 'تم إتمام انتقالك',
'body' => 'تم نقلك بنجاح إلى المنظمة الجديدة',
'title' => 'Your transfer is complete',
'title_ar' => 'تم إتمام انتقالك',
'body' => 'You have moved to your new organisation.',
'body_ar' => 'تم نقلك بنجاح إلى المنظمة الجديدة',
'type' => 'social',
]);
......
......@@ -129,7 +129,8 @@ class UsersController
'org_id' => !empty($_POST['org_id']) ? $_POST['org_id'] : null,
'is_active' => isset($_POST['is_active']) ? true : false,
'avatar_url' => trim($_POST['avatar_url'] ?? ''),
'created_by' => Auth::user()['username'],
// admin_users has no created_by column; who created the account is
// recorded in the audit log below.
];
$result = $this->db->insert('admin_users', $data);
......
......@@ -35,6 +35,24 @@ class WorkflowsController
$this->db = Database::getInstance();
}
/** The signed-in staff account's id; null for the built-in superadmin. */
private static function adminId(): ?string
{
$id = Auth::user()['id'] ?? null;
return is_string($id) && preg_match('/^[0-9a-f-]{36}$/i', $id) ? $id : null;
}
/** Usernames for a set of staff account ids, keyed by id. */
private function adminNames(array $ids): array
{
$ids = array_values(array_unique(array_filter($ids, fn($v) => is_string($v) && preg_match('/^[0-9a-f-]{36}$/i', $v))));
if (!$ids) return [];
$rows = $this->db->select('admin_users', ['select' => 'id,username,display_name', 'id' => 'in.(' . implode(',', $ids) . ')']);
$out = [];
foreach ($rows as $r) $out[$r['id']] = $r['display_name'] ?: $r['username'];
return $out;
}
public function list(array $params, string $method): void
{
$status = $_GET['status'] ?? '';
......@@ -52,7 +70,9 @@ class WorkflowsController
}
if ($search) {
$queryParams['or'] = "(data->>name.ilike.*{$search}*,requester_id.ilike.*{$search}*)";
// requester_id is a uuid; ilike on it fails the whole query.
$search = str_replace(['*', ',', '(', ')'], '', $search);
$queryParams['or'] = "(title.ilike.*{$search}*,data->>name.ilike.*{$search}*)";
}
$countParams = $queryParams;
......@@ -64,6 +84,9 @@ class WorkflowsController
$queryParams['limit'] = $pagination->perPage;
$requests = $this->db->select('approval_requests', $queryParams);
$names = $this->adminNames(array_column($requests, 'requester_id'));
foreach ($requests as &$r) $r['requester_name'] = $names[$r['requester_id'] ?? ''] ?? ($r['requester_id'] ?? null);
unset($r);
$pendingCount = $this->db->count('approval_requests', ['status' => 'eq.pending']);
......@@ -88,6 +111,10 @@ class WorkflowsController
return;
}
$names = $this->adminNames([$request['requester_id'] ?? null, $request['reviewed_by'] ?? null]);
$request['requester_name'] = $names[$request['requester_id'] ?? ''] ?? ($request['requester_id'] ?? null);
$request['reviewer_name'] = $names[$request['reviewed_by'] ?? ''] ?? null;
$pageTitle = 'تفاصيل الطلب';
$moduleCSS = 'workflows';
$moduleJS = 'workflows';
......@@ -116,9 +143,10 @@ class WorkflowsController
$note = trim($_POST['review_note'] ?? '');
$data = [
'status' => 'approved',
'reviewer_id' => Auth::user()['username'],
'review_note' => $note,
'reviewed_by' => self::adminId(),
'review_notes' => $note,
'reviewed_at' => date('c'),
'updated_at' => date('c'),
];
$this->db->update('approval_requests', ['id' => "eq.{$id}"], $data);
......@@ -152,9 +180,10 @@ class WorkflowsController
$data = [
'status' => 'rejected',
'reviewer_id' => Auth::user()['username'],
'review_note' => $note,
'reviewed_by' => self::adminId(),
'review_notes' => $note,
'reviewed_at' => date('c'),
'updated_at' => date('c'),
];
$this->db->update('approval_requests', ['id' => "eq.{$id}"], $data);
......@@ -187,9 +216,10 @@ class WorkflowsController
$data = [
'status' => 'approved',
'reviewer_id' => Auth::user()['username'],
'review_note' => $note,
'reviewed_by' => self::adminId(),
'review_notes' => $note,
'reviewed_at' => date('c'),
'updated_at' => date('c'),
];
$this->db->update('approval_requests', ['id' => "eq.{$id}"], $data);
......@@ -257,14 +287,17 @@ class WorkflowsController
$actions = $_POST['actions'] ?? [];
// workflow_rules.event_type (there is no event column); conditions and
// actions are jsonb, sent as lists rather than pre-encoded strings;
// created_by is a uuid, so the staff account's id, not its username.
$data = [
'name' => trim($_POST['name']),
'event' => trim($_POST['event']),
'conditions' => json_encode($conditions, JSON_UNESCAPED_UNICODE),
'actions' => json_encode($actions, JSON_UNESCAPED_UNICODE),
'event_type' => trim($_POST['event']),
'conditions' => $conditions,
'actions' => array_values((array)$actions),
'is_active' => isset($_POST['is_active']),
'priority' => (int)($_POST['priority'] ?? 0),
'created_by' => Auth::user()['username'],
'created_by' => self::adminId(),
];
$result = $this->db->insert('workflow_rules', $data);
......@@ -313,7 +346,8 @@ class WorkflowsController
public function myRequests(array $params, string $method): void
{
$userId = Auth::user()['username'];
// requester_id is the staff account's uuid, never its username.
$userId = self::adminId() ?? '00000000-0000-0000-0000-000000000000';
$queryParams = [
'select' => '*',
......@@ -328,6 +362,9 @@ class WorkflowsController
$queryParams['limit'] = $pagination->perPage;
$requests = $this->db->select('approval_requests', $queryParams);
$names = $this->adminNames(array_column($requests, 'reviewed_by'));
foreach ($requests as &$r) $r['reviewer_name'] = $names[$r['reviewed_by'] ?? ''] ?? null;
unset($r);
$pageTitle = 'طلباتي';
$moduleCSS = 'workflows';
......
......@@ -127,9 +127,9 @@
</td>
<td>
<div class="flex items-center gap-2">
<div class="avatar avatar-sm"><?= mb_substr($req['requester_id'] ?? '?', 0, 1) ?></div>
<div class="avatar avatar-sm"><?= View::e(mb_substr($req['requester_name'] ?? '?', 0, 1)) ?></div>
<div>
<div class="font-medium text-sm"><?= View::e($req['requester_id'] ?? '-') ?></div>
<div class="font-medium text-sm"><?= View::e($req['requester_name'] ?? '-') ?></div>
<span class="badge badge-default text-xs"><?= $roleLabels[$req['requester_role'] ?? ''] ?? ($req['requester_role'] ?? '-') ?></span>
</div>
</div>
......
......@@ -82,23 +82,23 @@
<?php endif; ?>
</div>
<?php if ($reqStatus === 'rejected' && !empty($req['review_note'])): ?>
<?php if ($reqStatus === 'rejected' && !empty($req['review_notes'])): ?>
<div class="my-request-note my-request-note-danger">
<div class="text-xs font-medium mb-1">سبب الرفض:</div>
<p class="text-sm"><?= View::e($req['review_note']) ?></p>
<p class="text-sm"><?= View::e($req['review_notes']) ?></p>
</div>
<?php elseif ($reqStatus === 'approved' && !empty($req['review_note'])): ?>
<?php elseif ($reqStatus === 'approved' && !empty($req['review_notes'])): ?>
<div class="my-request-note my-request-note-success">
<div class="text-xs font-medium mb-1">ملاحظة المراجع:</div>
<p class="text-sm"><?= View::e($req['review_note']) ?></p>
<p class="text-sm"><?= View::e($req['review_notes']) ?></p>
</div>
<?php endif; ?>
<?php if (!empty($req['reviewed_at'])): ?>
<div class="text-xs text-muted mt-2">
تمت المراجعة: <?= date('Y-m-d H:i', strtotime($req['reviewed_at'])) ?>
<?php if (!empty($req['reviewer_id'])): ?>
بواسطة <?= View::e($req['reviewer_id']) ?>
<?php if (!empty($req['reviewer_name'])): ?>
بواسطة <?= View::e($req['reviewer_name']) ?>
<?php endif; ?>
</div>
<?php endif; ?>
......
......@@ -68,7 +68,7 @@
<select id="ruleEvent" name="event" class="form-select" required>
<option value="">اختر الحدث...</option>
<?php foreach ($eventLabels as $value => $label): ?>
<option value="<?= $value ?>" <?= ($rule['event'] ?? '') === $value ? 'selected' : '' ?>><?= $label ?></option>
<option value="<?= $value ?>" <?= ($rule['event_type'] ?? '') === $value ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
</div>
......
......@@ -72,7 +72,7 @@
<!-- Event -->
<div class="rule-section">
<div class="rule-section-label">الحدث</div>
<span class="badge badge-info"><?= $eventLabels[$rule['event'] ?? ''] ?? ($rule['event'] ?? '-') ?></span>
<span class="badge badge-info"><?= $eventLabels[$rule['event_type'] ?? ''] ?? View::e($rule['event_type'] ?? '-') ?></span>
</div>
<!-- Conditions -->
......
......@@ -100,9 +100,9 @@
</div>
<div class="flex flex-col gap-3 text-sm p-4">
<div class="flex items-center gap-3 mb-3">
<div class="avatar avatar-lg"><?= mb_substr($request['requester_id'] ?? '?', 0, 1) ?></div>
<div class="avatar avatar-lg"><?= View::e(mb_substr($request['requester_name'] ?? '?', 0, 1)) ?></div>
<div>
<div class="font-semibold"><?= View::e($request['requester_id'] ?? '-') ?></div>
<div class="font-semibold"><?= View::e($request['requester_name'] ?? '-') ?></div>
<span class="badge badge-default"><?= $roleLabels[$request['requester_role'] ?? ''] ?? ($request['requester_role'] ?? '-') ?></span>
</div>
</div>
......@@ -176,7 +176,7 @@
<div class="timeline-content">
<div class="timeline-title">تم تقديم الطلب</div>
<div class="timeline-meta">
<span><?= View::e($request['requester_id'] ?? '-') ?></span>
<span><?= View::e($request['requester_name'] ?? '-') ?></span>
<span class="timeline-date"><?= !empty($request['created_at']) ? date('Y-m-d H:i', strtotime($request['created_at'])) : '-' ?></span>
</div>
</div>
......@@ -189,11 +189,11 @@
<div class="timeline-content">
<div class="timeline-title">تمت الموافقة</div>
<div class="timeline-meta">
<span>بواسطة: <?= View::e($request['reviewer_id'] ?? '-') ?></span>
<span>بواسطة: <?= View::e($request['reviewer_name'] ?? '-') ?></span>
<span class="timeline-date"><?= !empty($request['reviewed_at']) ? date('Y-m-d H:i', strtotime($request['reviewed_at'])) : '-' ?></span>
</div>
<?php if (!empty($request['review_note'])): ?>
<div class="timeline-note"><?= View::e($request['review_note']) ?></div>
<?php if (!empty($request['review_notes'])): ?>
<div class="timeline-note"><?= View::e($request['review_notes']) ?></div>
<?php endif; ?>
</div>
</div>
......@@ -204,11 +204,11 @@
<div class="timeline-content">
<div class="timeline-title">تم الرفض</div>
<div class="timeline-meta">
<span>بواسطة: <?= View::e($request['reviewer_id'] ?? '-') ?></span>
<span>بواسطة: <?= View::e($request['reviewer_name'] ?? '-') ?></span>
<span class="timeline-date"><?= !empty($request['reviewed_at']) ? date('Y-m-d H:i', strtotime($request['reviewed_at'])) : '-' ?></span>
</div>
<?php if (!empty($request['review_note'])): ?>
<div class="timeline-note timeline-note-danger"><?= View::e($request['review_note']) ?></div>
<?php if (!empty($request['review_notes'])): ?>
<div class="timeline-note timeline-note-danger"><?= View::e($request['review_notes']) ?></div>
<?php endif; ?>
</div>
</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