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 ...@@ -69,7 +69,7 @@ class RuleEngine
try { try {
$db = Database::getInstance(); $db = Database::getInstance();
return $db->select('workflow_rules', [ return $db->select('workflow_rules', [
'event' => 'eq.' . $event, 'event_type' => 'eq.' . $event,
'is_active' => 'eq.true', 'is_active' => 'eq.true',
'order' => 'priority.desc', 'order' => 'priority.desc',
]); ]);
...@@ -106,7 +106,9 @@ class RuleEngine ...@@ -106,7 +106,9 @@ class RuleEngine
private static function evaluateSingleCondition(array $condition, array $context): bool private static function evaluateSingleCondition(array $condition, array $context): bool
{ {
$field = $condition['field'] ?? null; $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; $expected = $condition['value'] ?? null;
if ($field === null) { if ($field === null) {
...@@ -179,20 +181,24 @@ class RuleEngine ...@@ -179,20 +181,24 @@ class RuleEngine
*/ */
private static function actionNotify(array $params, string $event, array $context): void private static function actionNotify(array $params, string $event, array $context): void
{ {
try { // notifications is the players' in-game bell: it needs a player id
$db = Database::getInstance(); // (user_id), and the game reads title_ar/body_ar. Staff have no inbox
$db->insert('notifications', [ // there, so a rule addressed to 'admin' has nowhere to land.
'recipient' => $params['to'] ?? 'admin', $to = self::interpolate((string)($params['to'] ?? ''), $context);
'title' => $params['title'] ?? 'Rule triggered: ' . $event, 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)) {
'message' => self::interpolate($params['message'] ?? '', $context), return;
'type' => 'rule_engine',
'event' => $event,
'is_read' => false,
'created_at' => date('c'),
]);
} catch (\Throwable $e) {
// Non-critical
} }
$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 ...@@ -293,26 +293,21 @@ class LiveDataService
public function getPhases(string $tournamentId): array 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}", '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 public function getArenaData(string $tournamentId): array
{ {
$games = $this->db->select('el3ab_arena_games', [ return ['games' => [], 'standings' => []];
'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];
} }
public function getStats(array $tournament): array public function getStats(array $tournament): array
......
...@@ -73,7 +73,7 @@ class ModerationController ...@@ -73,7 +73,7 @@ class ModerationController
$this->db->update('cheat_reports', ['id' => "eq.{$id}"], [ $this->db->update('cheat_reports', ['id' => "eq.{$id}"], [
'status' => 'resolved', 'status' => 'resolved',
'action_taken' => $resolution, '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'), 'reviewed_at' => date('c'),
]); ]);
...@@ -90,7 +90,7 @@ class ModerationController ...@@ -90,7 +90,7 @@ class ModerationController
$this->db->update('cheat_reports', ['id' => "eq.{$id}"], [ $this->db->update('cheat_reports', ['id' => "eq.{$id}"], [
'status' => 'dismissed', 'status' => 'dismissed',
'action_taken' => $resolution, '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'), 'reviewed_at' => date('c'),
]); ]);
...@@ -108,7 +108,7 @@ class ModerationController ...@@ -108,7 +108,7 @@ class ModerationController
$this->db->update('cheat_reports', ['id' => "eq.{$id}"], [ $this->db->update('cheat_reports', ['id' => "eq.{$id}"], [
'status' => 'dismissed', 'status' => 'dismissed',
'action_taken' => $reason, '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'), 'reviewed_at' => date('c'),
]); ]);
} }
......
...@@ -210,57 +210,12 @@ class OrgAchievementsController ...@@ -210,57 +210,12 @@ class OrgAchievementsController
return; return;
} }
$existing = $this->db->selectOne('player_achievements', [ // player_achievements.achievement_id references the game's achievements
'player_id' => "eq.{$playerId}", // table (text ids such as "first_win"); an org achievement's uuid can never
'achievement_id' => "eq.{$achievementId}", // 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.
if ($existing && !($achievement['is_repeatable'] ?? false)) { Response::error('منح إنجازات المنظمات غير متاح بعد: جدول إنجازات اللاعبين يقبل إنجازات اللعبة فقط، ولا يوجد جدول لإنجازات المنظمات الممنوحة', '/org-achievements');
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');
} }
public function playerProgress(array $params, string $method): void public function playerProgress(array $params, string $method): void
...@@ -268,13 +223,11 @@ class OrgAchievementsController ...@@ -268,13 +223,11 @@ class OrgAchievementsController
$playerId = $_GET['player_id'] ?? ''; $playerId = $_GET['player_id'] ?? '';
$orgId = $_GET['org_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) { if ($playerId) {
$queryParams['player_id'] = "eq.{$playerId}"; $queryParams['player_id'] = "eq.{$playerId}";
} }
if ($orgId) {
$queryParams['org_id'] = "eq.{$orgId}";
}
$total = $this->db->count('player_achievements', $queryParams); $total = $this->db->count('player_achievements', $queryParams);
$pagination = Pagination::fromRequest($total); $pagination = Pagination::fromRequest($total);
......
...@@ -87,15 +87,15 @@ ...@@ -87,15 +87,15 @@
</div> </div>
</td> </td>
<td> <td>
<?php if ($item['is_complete'] ?? false): ?> <?php if ($item['completed'] ?? false): ?>
<span class="badge badge-success badge-dot">مكتمل</span> <span class="badge badge-success badge-dot">مكتمل</span>
<?php else: ?> <?php else: ?>
<span class="badge badge-warning badge-dot">قيد التقدم</span> <span class="badge badge-warning badge-dot">قيد التقدم</span>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td> <td>
<?php if (!empty($item['earned_at'])): ?> <?php if (!empty($item['completed_at'])): ?>
<span class="text-xs tabular-nums"><?= date('Y-m-d H:i', strtotime($item['earned_at'])) ?></span> <span class="text-xs tabular-nums"><?= date('Y-m-d H:i', strtotime($item['completed_at'])) ?></span>
<?php else: ?> <?php else: ?>
<span class="text-muted text-xs">-</span> <span class="text-muted text-xs">-</span>
<?php endif; ?> <?php endif; ?>
......
...@@ -262,7 +262,7 @@ class OrgChatController ...@@ -262,7 +262,7 @@ class OrgChatController
} }
$mutedMembers = $this->db->select('org_members', [ $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}", 'org_id' => "eq.{$orgId}",
'is_muted' => 'eq.true', 'is_muted' => 'eq.true',
]); ]);
...@@ -290,7 +290,7 @@ class OrgChatController ...@@ -290,7 +290,7 @@ class OrgChatController
$this->db->update('org_members', [ $this->db->update('org_members', [
'org_id' => "eq.{$orgId}", 'org_id' => "eq.{$orgId}",
'user_id' => "eq.{$playerId}", 'player_id' => "eq.{$playerId}",
], [ ], [
'is_muted' => true, 'is_muted' => true,
'muted_until' => $expiresAt, 'muted_until' => $expiresAt,
...@@ -318,7 +318,7 @@ class OrgChatController ...@@ -318,7 +318,7 @@ class OrgChatController
$this->db->update('org_members', [ $this->db->update('org_members', [
'org_id' => "eq.{$orgId}", 'org_id' => "eq.{$orgId}",
'user_id' => "eq.{$playerId}", 'player_id' => "eq.{$playerId}",
], [ ], [
'is_muted' => false, 'is_muted' => false,
'muted_until' => null, 'muted_until' => null,
...@@ -349,7 +349,7 @@ class OrgChatController ...@@ -349,7 +349,7 @@ class OrgChatController
$this->db->update('org_members', [ $this->db->update('org_members', [
'org_id' => "eq.{$orgId}", 'org_id' => "eq.{$orgId}",
'user_id' => "eq.{$playerId}", 'player_id' => "eq.{$playerId}",
], [ ], [
'is_muted' => true, 'is_muted' => true,
'muted_until' => '2099-12-31T23:59:59Z', 'muted_until' => '2099-12-31T23:59:59Z',
......
...@@ -319,22 +319,22 @@ class OrgFramesController ...@@ -319,22 +319,22 @@ class OrgFramesController
return; return;
} }
// org_members: player_id, and every row is an active membership.
$members = $this->db->select('org_members', [ $members = $this->db->select('org_members', [
'select' => 'user_id', 'select' => 'player_id',
'org_id' => "eq.{$orgId}", 'org_id' => "eq.{$orgId}",
'status' => 'eq.active',
]); ]);
$assigned = 0; $assigned = 0;
foreach ($members as $member) { foreach ($members as $member) {
$existing = $this->db->selectOne('player_frames', [ $existing = $this->db->selectOne('player_frames', [
'player_id' => "eq.{$member['user_id']}", 'player_id' => "eq.{$member['player_id']}",
'frame_id' => "eq.{$frameId}", 'frame_id' => "eq.{$frameId}",
]); ]);
if (!$existing) { if (!$existing) {
$this->db->insert('player_frames', [ $this->db->insert('player_frames', [
'player_id' => $member['user_id'], 'player_id' => $member['player_id'],
'frame_id' => $frameId, 'frame_id' => $frameId,
'acquisition_type' => 'org_membership', 'acquisition_type' => 'org_membership',
'source_id' => "org:{$orgId}", 'source_id' => "org:{$orgId}",
......
...@@ -139,7 +139,6 @@ class OrgLoyaltyController ...@@ -139,7 +139,6 @@ class OrgLoyaltyController
'reward_frame_id' => !empty($_POST['reward_frame_id']) ? $_POST['reward_frame_id'] : null, 'reward_frame_id' => !empty($_POST['reward_frame_id']) ? $_POST['reward_frame_id'] : null,
'reward_title' => trim($_POST['reward_title'] ?? ''), 'reward_title' => trim($_POST['reward_title'] ?? ''),
'sort_order' => (int) ($_POST['sort_order'] ?? 0), 'sort_order' => (int) ($_POST['sort_order'] ?? 0),
'updated_at' => date('c'),
]; ];
$this->db->update('org_loyalty_rewards', ['id' => "eq.{$rewardId}"], $data); $this->db->update('org_loyalty_rewards', ['id' => "eq.{$rewardId}"], $data);
...@@ -167,7 +166,6 @@ class OrgLoyaltyController ...@@ -167,7 +166,6 @@ class OrgLoyaltyController
$newStatus = !($reward['is_active'] ?? false); $newStatus = !($reward['is_active'] ?? false);
$this->db->update('org_loyalty_rewards', ['id' => "eq.{$rewardId}"], [ $this->db->update('org_loyalty_rewards', ['id' => "eq.{$rewardId}"], [
'is_active' => $newStatus, 'is_active' => $newStatus,
'updated_at' => date('c'),
]); ]);
AuditLog::log('toggle', 'org_loyalty_reward', $rewardId, $reward, ['is_active' => $newStatus]); AuditLog::log('toggle', 'org_loyalty_reward', $rewardId, $reward, ['is_active' => $newStatus]);
......
...@@ -113,7 +113,6 @@ class OrgPartnershipsController ...@@ -113,7 +113,6 @@ class OrgPartnershipsController
'status' => 'active', 'status' => 'active',
'started_at' => date('c'), 'started_at' => date('c'),
'accepted_by' => $user['id'], 'accepted_by' => $user['id'],
'updated_at' => date('c'),
]); ]);
AuditLog::log('approve', 'org_partnership', $id, $partnership, ['status' => 'active']); AuditLog::log('approve', 'org_partnership', $id, $partnership, ['status' => 'active']);
...@@ -140,7 +139,6 @@ class OrgPartnershipsController ...@@ -140,7 +139,6 @@ class OrgPartnershipsController
$this->db->update('org_partnerships', ['id' => "eq.{$id}"], [ $this->db->update('org_partnerships', ['id' => "eq.{$id}"], [
'status' => 'rejected', 'status' => 'rejected',
'updated_at' => date('c'),
]); ]);
AuditLog::log('reject', 'org_partnership', $id, $partnership, ['status' => 'rejected']); AuditLog::log('reject', 'org_partnership', $id, $partnership, ['status' => 'rejected']);
...@@ -168,7 +166,6 @@ class OrgPartnershipsController ...@@ -168,7 +166,6 @@ class OrgPartnershipsController
$this->db->update('org_partnerships', ['id' => "eq.{$id}"], [ $this->db->update('org_partnerships', ['id' => "eq.{$id}"], [
'status' => 'dissolved', 'status' => 'dissolved',
'ended_at' => date('c'), 'ended_at' => date('c'),
'updated_at' => date('c'),
]); ]);
AuditLog::log('dissolve', 'org_partnership', $id, $partnership, ['status' => 'dissolved']); AuditLog::log('dissolve', 'org_partnership', $id, $partnership, ['status' => 'dissolved']);
......
...@@ -89,9 +89,10 @@ class OrgTransfersController ...@@ -89,9 +89,10 @@ class OrgTransfersController
} }
if (empty($fromOrgId)) { 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', [ $currentMembership = $this->db->selectOne('org_members', [
'user_id' => "eq.{$playerId}", 'player_id' => "eq.{$playerId}",
'status' => 'eq.active',
]); ]);
$fromOrgId = $currentMembership['org_id'] ?? null; $fromOrgId = $currentMembership['org_id'] ?? null;
} }
...@@ -119,15 +120,15 @@ class OrgTransfersController ...@@ -119,15 +120,15 @@ class OrgTransfersController
if ($fromOrgId) { if ($fromOrgId) {
$this->db->delete('org_members', [ $this->db->delete('org_members', [
'org_id' => "eq.{$fromOrgId}", 'org_id' => "eq.{$fromOrgId}",
'user_id' => "eq.{$playerId}", 'player_id' => "eq.{$playerId}",
]); ]);
} }
$this->db->insert('org_members', [ $this->db->insert('org_members', [
'org_id' => $toOrgId, 'org_id' => $toOrgId,
'user_id' => $playerId, 'player_id' => $playerId,
'role' => 'member', 'role' => 'member',
'status' => 'active', 'joined_via' => 'transfer',
'joined_at' => date('c'), 'joined_at' => date('c'),
]); ]);
} }
...@@ -206,21 +207,21 @@ class OrgTransfersController ...@@ -206,21 +207,21 @@ class OrgTransfersController
if ($transfer['from_org_id']) { if ($transfer['from_org_id']) {
$this->db->delete('org_members', [ $this->db->delete('org_members', [
'org_id' => "eq.{$transfer['from_org_id']}", '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', [ $existing = $this->db->selectOne('org_members', [
'org_id' => "eq.{$transfer['to_org_id']}", 'org_id' => "eq.{$transfer['to_org_id']}",
'user_id' => "eq.{$transfer['player_id']}", 'player_id' => "eq.{$transfer['player_id']}",
]); ]);
if (!$existing) { if (!$existing) {
$this->db->insert('org_members', [ $this->db->insert('org_members', [
'org_id' => $transfer['to_org_id'], 'org_id' => $transfer['to_org_id'],
'user_id' => $transfer['player_id'], 'player_id' => $transfer['player_id'],
'role' => 'member', 'role' => 'member',
'status' => 'active', 'joined_via' => 'transfer',
'joined_at' => date('c'), 'joined_at' => date('c'),
]); ]);
} }
...@@ -231,10 +232,13 @@ class OrgTransfersController ...@@ -231,10 +232,13 @@ class OrgTransfersController
'updated_at' => date('c'), 'updated_at' => date('c'),
]); ]);
// The game's bell shows title_ar/body_ar.
$this->db->insert('notifications', [ $this->db->insert('notifications', [
'user_id' => $transfer['player_id'], 'user_id' => $transfer['player_id'],
'title' => 'تم إتمام انتقالك', 'title' => 'Your transfer is complete',
'body' => 'تم نقلك بنجاح إلى المنظمة الجديدة', 'title_ar' => 'تم إتمام انتقالك',
'body' => 'You have moved to your new organisation.',
'body_ar' => 'تم نقلك بنجاح إلى المنظمة الجديدة',
'type' => 'social', 'type' => 'social',
]); ]);
......
...@@ -129,7 +129,8 @@ class UsersController ...@@ -129,7 +129,8 @@ class UsersController
'org_id' => !empty($_POST['org_id']) ? $_POST['org_id'] : null, 'org_id' => !empty($_POST['org_id']) ? $_POST['org_id'] : null,
'is_active' => isset($_POST['is_active']) ? true : false, 'is_active' => isset($_POST['is_active']) ? true : false,
'avatar_url' => trim($_POST['avatar_url'] ?? ''), '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); $result = $this->db->insert('admin_users', $data);
......
...@@ -35,6 +35,24 @@ class WorkflowsController ...@@ -35,6 +35,24 @@ class WorkflowsController
$this->db = Database::getInstance(); $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 public function list(array $params, string $method): void
{ {
$status = $_GET['status'] ?? ''; $status = $_GET['status'] ?? '';
...@@ -52,7 +70,9 @@ class WorkflowsController ...@@ -52,7 +70,9 @@ class WorkflowsController
} }
if ($search) { 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; $countParams = $queryParams;
...@@ -64,6 +84,9 @@ class WorkflowsController ...@@ -64,6 +84,9 @@ class WorkflowsController
$queryParams['limit'] = $pagination->perPage; $queryParams['limit'] = $pagination->perPage;
$requests = $this->db->select('approval_requests', $queryParams); $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']); $pendingCount = $this->db->count('approval_requests', ['status' => 'eq.pending']);
...@@ -88,6 +111,10 @@ class WorkflowsController ...@@ -88,6 +111,10 @@ class WorkflowsController
return; 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 = 'تفاصيل الطلب'; $pageTitle = 'تفاصيل الطلب';
$moduleCSS = 'workflows'; $moduleCSS = 'workflows';
$moduleJS = 'workflows'; $moduleJS = 'workflows';
...@@ -116,9 +143,10 @@ class WorkflowsController ...@@ -116,9 +143,10 @@ class WorkflowsController
$note = trim($_POST['review_note'] ?? ''); $note = trim($_POST['review_note'] ?? '');
$data = [ $data = [
'status' => 'approved', 'status' => 'approved',
'reviewer_id' => Auth::user()['username'], 'reviewed_by' => self::adminId(),
'review_note' => $note, 'review_notes' => $note,
'reviewed_at' => date('c'), 'reviewed_at' => date('c'),
'updated_at' => date('c'),
]; ];
$this->db->update('approval_requests', ['id' => "eq.{$id}"], $data); $this->db->update('approval_requests', ['id' => "eq.{$id}"], $data);
...@@ -152,9 +180,10 @@ class WorkflowsController ...@@ -152,9 +180,10 @@ class WorkflowsController
$data = [ $data = [
'status' => 'rejected', 'status' => 'rejected',
'reviewer_id' => Auth::user()['username'], 'reviewed_by' => self::adminId(),
'review_note' => $note, 'review_notes' => $note,
'reviewed_at' => date('c'), 'reviewed_at' => date('c'),
'updated_at' => date('c'),
]; ];
$this->db->update('approval_requests', ['id' => "eq.{$id}"], $data); $this->db->update('approval_requests', ['id' => "eq.{$id}"], $data);
...@@ -187,9 +216,10 @@ class WorkflowsController ...@@ -187,9 +216,10 @@ class WorkflowsController
$data = [ $data = [
'status' => 'approved', 'status' => 'approved',
'reviewer_id' => Auth::user()['username'], 'reviewed_by' => self::adminId(),
'review_note' => $note, 'review_notes' => $note,
'reviewed_at' => date('c'), 'reviewed_at' => date('c'),
'updated_at' => date('c'),
]; ];
$this->db->update('approval_requests', ['id' => "eq.{$id}"], $data); $this->db->update('approval_requests', ['id' => "eq.{$id}"], $data);
...@@ -257,14 +287,17 @@ class WorkflowsController ...@@ -257,14 +287,17 @@ class WorkflowsController
$actions = $_POST['actions'] ?? []; $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 = [ $data = [
'name' => trim($_POST['name']), 'name' => trim($_POST['name']),
'event' => trim($_POST['event']), 'event_type' => trim($_POST['event']),
'conditions' => json_encode($conditions, JSON_UNESCAPED_UNICODE), 'conditions' => $conditions,
'actions' => json_encode($actions, JSON_UNESCAPED_UNICODE), 'actions' => array_values((array)$actions),
'is_active' => isset($_POST['is_active']), 'is_active' => isset($_POST['is_active']),
'priority' => (int)($_POST['priority'] ?? 0), 'priority' => (int)($_POST['priority'] ?? 0),
'created_by' => Auth::user()['username'], 'created_by' => self::adminId(),
]; ];
$result = $this->db->insert('workflow_rules', $data); $result = $this->db->insert('workflow_rules', $data);
...@@ -313,7 +346,8 @@ class WorkflowsController ...@@ -313,7 +346,8 @@ class WorkflowsController
public function myRequests(array $params, string $method): void 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 = [ $queryParams = [
'select' => '*', 'select' => '*',
...@@ -328,6 +362,9 @@ class WorkflowsController ...@@ -328,6 +362,9 @@ class WorkflowsController
$queryParams['limit'] = $pagination->perPage; $queryParams['limit'] = $pagination->perPage;
$requests = $this->db->select('approval_requests', $queryParams); $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 = 'طلباتي'; $pageTitle = 'طلباتي';
$moduleCSS = 'workflows'; $moduleCSS = 'workflows';
......
...@@ -127,9 +127,9 @@ ...@@ -127,9 +127,9 @@
</td> </td>
<td> <td>
<div class="flex items-center gap-2"> <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>
<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> <span class="badge badge-default text-xs"><?= $roleLabels[$req['requester_role'] ?? ''] ?? ($req['requester_role'] ?? '-') ?></span>
</div> </div>
</div> </div>
......
...@@ -82,23 +82,23 @@ ...@@ -82,23 +82,23 @@
<?php endif; ?> <?php endif; ?>
</div> </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="my-request-note my-request-note-danger">
<div class="text-xs font-medium mb-1">سبب الرفض:</div> <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> </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="my-request-note my-request-note-success">
<div class="text-xs font-medium mb-1">ملاحظة المراجع:</div> <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> </div>
<?php endif; ?> <?php endif; ?>
<?php if (!empty($req['reviewed_at'])): ?> <?php if (!empty($req['reviewed_at'])): ?>
<div class="text-xs text-muted mt-2"> <div class="text-xs text-muted mt-2">
تمت المراجعة: <?= date('Y-m-d H:i', strtotime($req['reviewed_at'])) ?> تمت المراجعة: <?= date('Y-m-d H:i', strtotime($req['reviewed_at'])) ?>
<?php if (!empty($req['reviewer_id'])): ?> <?php if (!empty($req['reviewer_name'])): ?>
بواسطة <?= View::e($req['reviewer_id']) ?> بواسطة <?= View::e($req['reviewer_name']) ?>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
......
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
<select id="ruleEvent" name="event" class="form-select" required> <select id="ruleEvent" name="event" class="form-select" required>
<option value="">اختر الحدث...</option> <option value="">اختر الحدث...</option>
<?php foreach ($eventLabels as $value => $label): ?> <?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; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
......
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
<!-- Event --> <!-- Event -->
<div class="rule-section"> <div class="rule-section">
<div class="rule-section-label">الحدث</div> <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> </div>
<!-- Conditions --> <!-- Conditions -->
......
...@@ -100,9 +100,9 @@ ...@@ -100,9 +100,9 @@
</div> </div>
<div class="flex flex-col gap-3 text-sm p-4"> <div class="flex flex-col gap-3 text-sm p-4">
<div class="flex items-center gap-3 mb-3"> <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>
<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> <span class="badge badge-default"><?= $roleLabels[$request['requester_role'] ?? ''] ?? ($request['requester_role'] ?? '-') ?></span>
</div> </div>
</div> </div>
...@@ -176,7 +176,7 @@ ...@@ -176,7 +176,7 @@
<div class="timeline-content"> <div class="timeline-content">
<div class="timeline-title">تم تقديم الطلب</div> <div class="timeline-title">تم تقديم الطلب</div>
<div class="timeline-meta"> <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> <span class="timeline-date"><?= !empty($request['created_at']) ? date('Y-m-d H:i', strtotime($request['created_at'])) : '-' ?></span>
</div> </div>
</div> </div>
...@@ -189,11 +189,11 @@ ...@@ -189,11 +189,11 @@
<div class="timeline-content"> <div class="timeline-content">
<div class="timeline-title">تمت الموافقة</div> <div class="timeline-title">تمت الموافقة</div>
<div class="timeline-meta"> <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> <span class="timeline-date"><?= !empty($request['reviewed_at']) ? date('Y-m-d H:i', strtotime($request['reviewed_at'])) : '-' ?></span>
</div> </div>
<?php if (!empty($request['review_note'])): ?> <?php if (!empty($request['review_notes'])): ?>
<div class="timeline-note"><?= View::e($request['review_note']) ?></div> <div class="timeline-note"><?= View::e($request['review_notes']) ?></div>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> </div>
...@@ -204,11 +204,11 @@ ...@@ -204,11 +204,11 @@
<div class="timeline-content"> <div class="timeline-content">
<div class="timeline-title">تم الرفض</div> <div class="timeline-title">تم الرفض</div>
<div class="timeline-meta"> <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> <span class="timeline-date"><?= !empty($request['reviewed_at']) ? date('Y-m-d H:i', strtotime($request['reviewed_at'])) : '-' ?></span>
</div> </div>
<?php if (!empty($request['review_note'])): ?> <?php if (!empty($request['review_notes'])): ?>
<div class="timeline-note timeline-note-danger"><?= View::e($request['review_note']) ?></div> <div class="timeline-note timeline-note-danger"><?= View::e($request['review_notes']) ?></div>
<?php endif; ?> <?php endif; ?>
</div> </div>
</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