Commit 952aa695 authored by DevPilot's avatar DevPilot

fix(sql): correct column names that crash screens at runtime, add a schema checker

كشف حساب العضو كان بيقع بـ Unknown column 'phone' — جدول members فيه
phone_mobile/phone_home مش phone. ولما دورت على الغلط ده في باقي الكود
لقيت نفس النوع في 20+ مكان تاني (coaches.name_ar، players.name_ar،
members.member_number، hr_attendance.employee_id، وغيرهم).

tools/sql_schema_check.py بيقارن كل SQL في app/ بالـ schema الحقيقي
ويقع بـ exit 1 لو لقى عمود مش موجود، عشان النوع ده ما يوصلش للـ production تاني.
parent 9c814464
...@@ -48,7 +48,7 @@ final class SettlementService ...@@ -48,7 +48,7 @@ final class SettlementService
WHERE sc.academy_id = ? WHERE sc.academy_id = ?
AND sc.coach_type = 'academy' AND sc.coach_type = 'academy'
AND pr.status IN ('paid', 'completed') AND pr.status IN ('paid', 'completed')
AND DATE_FORMAT(pr.paid_at, '%Y-%m') = ?", AND DATE_FORMAT(pr.processed_at, '%Y-%m') = ?",
[$academyId, $month] [$academyId, $month]
); );
......
...@@ -541,8 +541,11 @@ class ReportController extends Controller ...@@ -541,8 +541,11 @@ class ReportController extends Controller
); );
if (!$found) { if (!$found) {
$found = $db->selectOne( $found = $db->selectOne(
"SELECT id FROM members WHERE full_name_ar LIKE ? OR full_name_en LIKE ? OR phone LIKE ? LIMIT 1", "SELECT id FROM members
["%{$memberSearch}%", "%{$memberSearch}%", "%{$memberSearch}%"] WHERE full_name_ar LIKE ? OR full_name_en LIKE ?
OR phone_mobile LIKE ? OR phone_home LIKE ?
LIMIT 1",
["%{$memberSearch}%", "%{$memberSearch}%", "%{$memberSearch}%", "%{$memberSearch}%"]
); );
} }
if ($found) { if ($found) {
......
...@@ -69,7 +69,7 @@ final class AlertProcessorService ...@@ -69,7 +69,7 @@ final class AlertProcessorService
LEFT JOIN players p ON p.id = pmr.player_id LEFT JOIN players p ON p.id = pmr.player_id
WHERE pmr.expiry_date IS NOT NULL WHERE pmr.expiry_date IS NOT NULL
AND pmr.expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY) AND pmr.expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY)
AND pmr.is_current = 1", AND pmr.approval_status = 'approved'",
[$days] [$days]
); );
...@@ -133,8 +133,9 @@ final class AlertProcessorService ...@@ -133,8 +133,9 @@ final class AlertProcessorService
"SELECT sa.player_id, p.full_name_ar AS player_name, COUNT(*) AS absence_count "SELECT sa.player_id, p.full_name_ar AS player_name, COUNT(*) AS absence_count
FROM session_attendance sa FROM session_attendance sa
LEFT JOIN players p ON p.id = sa.player_id LEFT JOIN players p ON p.id = sa.player_id
INNER JOIN training_sessions ts ON ts.id = sa.session_id
WHERE sa.status = 'absent' WHERE sa.status = 'absent'
AND sa.session_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) AND ts.session_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY sa.player_id GROUP BY sa.player_id
HAVING absence_count >= ? HAVING absence_count >= ?
ORDER BY absence_count DESC", ORDER BY absence_count DESC",
......
...@@ -89,7 +89,7 @@ class GroupWaitlist extends Model ...@@ -89,7 +89,7 @@ class GroupWaitlist extends Model
if (!empty($filters['q'])) { if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%'; $search = '%' . $filters['q'] . '%';
$where[] = '(m.name_ar LIKE ? OR m.membership_number LIKE ?)'; $where[] = '(m.full_name_ar LIKE ? OR m.membership_number LIKE ?)';
$params[] = $search; $params[] = $search;
$params[] = $search; $params[] = $search;
} }
...@@ -111,7 +111,7 @@ class GroupWaitlist extends Model ...@@ -111,7 +111,7 @@ class GroupWaitlist extends Model
// Fetch data // Fetch data
$sql = "SELECT gw.*, $sql = "SELECT gw.*,
m.name_ar AS player_name, m.membership_number, m.full_name_ar AS player_name, m.membership_number,
tg.name_ar AS group_name tg.name_ar AS group_name
FROM group_waitlist gw FROM group_waitlist gw
LEFT JOIN members m ON m.id = gw.player_id LEFT JOIN members m ON m.id = gw.player_id
......
...@@ -30,7 +30,7 @@ final class FacilityAttendanceController extends Controller ...@@ -30,7 +30,7 @@ final class FacilityAttendanceController extends Controller
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$coaches = $db->select( $coaches = $db->select(
"SELECT DISTINCT fzs.coach_id, c.name_ar "SELECT DISTINCT fzs.coach_id, c.full_name_ar
FROM facility_zone_schedules fzs FROM facility_zone_schedules fzs
INNER JOIN facility_grid_zones fgz ON fgz.id = fzs.zone_id INNER JOIN facility_grid_zones fgz ON fgz.id = fzs.zone_id
INNER JOIN coaches c ON c.id = fzs.coach_id INNER JOIN coaches c ON c.id = fzs.coach_id
...@@ -39,7 +39,7 @@ final class FacilityAttendanceController extends Controller ...@@ -39,7 +39,7 @@ final class FacilityAttendanceController extends Controller
); );
$trainees = $db->select( $trainees = $db->select(
"SELECT DISTINCT fzt.player_id, p.name_ar "SELECT DISTINCT fzt.player_id, p.full_name_ar
FROM facility_zone_trainees fzt FROM facility_zone_trainees fzt
INNER JOIN facility_grid_zones fgz ON fgz.id = fzt.zone_id INNER JOIN facility_grid_zones fgz ON fgz.id = fzt.zone_id
INNER JOIN players p ON p.id = fzt.player_id INNER JOIN players p ON p.id = fzt.player_id
......
...@@ -99,7 +99,7 @@ final class FacilityAttendanceService ...@@ -99,7 +99,7 @@ final class FacilityAttendanceService
public function sendPaymentReminders(int $gridId): int public function sendPaymentReminders(int $gridId): int
{ {
$trainees = $this->db->select( $trainees = $this->db->select(
"SELECT DISTINCT fzt.player_id, p.name_ar, p.phone "SELECT DISTINCT fzt.player_id, p.full_name_ar, p.phone
FROM facility_zone_trainees fzt FROM facility_zone_trainees fzt
INNER JOIN facility_grid_zones fgz ON fgz.id = fzt.zone_id INNER JOIN facility_grid_zones fgz ON fgz.id = fzt.zone_id
INNER JOIN players p ON p.id = fzt.player_id INNER JOIN players p ON p.id = fzt.player_id
......
...@@ -14,9 +14,15 @@ final class AttendanceViolationService ...@@ -14,9 +14,15 @@ final class AttendanceViolationService
$violations = []; $violations = [];
$attendanceRecords = $db->select( $attendanceRecords = $db->select(
"SELECT a.*, e.id as emp_id, e.full_name_ar "SELECT a.*,
ep.employee_id AS employee_id,
a.check_in_time AS check_in,
a.check_out_time AS check_out,
e.id AS emp_id,
e.full_name_ar
FROM hr_attendance a FROM hr_attendance a
JOIN employees e ON e.id = a.employee_id JOIN hr_employee_profiles ep ON ep.id = a.employee_profile_id
JOIN employees e ON e.id = ep.employee_id
WHERE a.attendance_date = ?", WHERE a.attendance_date = ?",
[$date] [$date]
); );
...@@ -132,13 +138,16 @@ final class AttendanceViolationService ...@@ -132,13 +138,16 @@ final class AttendanceViolationService
if ($isHoliday) return []; if ($isHoliday) return [];
$onLeave = $db->select( $onLeave = $db->select(
"SELECT employee_id FROM hr_leave_requests WHERE status = 'approved' AND ? BETWEEN start_date AND end_date", "SELECT ep.employee_id
FROM hr_leave_requests lr
JOIN hr_employee_profiles ep ON ep.id = lr.employee_profile_id
WHERE lr.status = 'approved' AND ? BETWEEN lr.start_date AND lr.end_date",
[$date] [$date]
); );
$onLeaveIds = array_column($onLeave, 'employee_id'); $onLeaveIds = array_column($onLeave, 'employee_id');
$allActive = $db->select( $allActive = $db->select(
"SELECT id as employee_id FROM employees WHERE is_archived = 0 AND employment_status = 'active'" "SELECT id as employee_id FROM employees WHERE is_archived = 0 AND is_active = 1"
); );
$absentees = []; $absentees = [];
......
...@@ -96,7 +96,7 @@ final class PlayerProgressionService ...@@ -96,7 +96,7 @@ final class PlayerProgressionService
$targetDate = date('Y-m-d', strtotime('+' . $daysAhead . ' days')); $targetDate = date('Y-m-d', strtotime('+' . $daysAhead . ' days'));
return $db->select( return $db->select(
"SELECT pmr.*, p.full_name AS player_name, p.phone AS player_phone "SELECT pmr.*, p.full_name_ar AS player_name, p.phone AS player_phone
FROM player_medical_records pmr FROM player_medical_records pmr
LEFT JOIN players p ON p.id = pmr.player_id LEFT JOIN players p ON p.id = pmr.player_id
WHERE pmr.expiry_date IS NOT NULL WHERE pmr.expiry_date IS NOT NULL
......
...@@ -93,7 +93,7 @@ final class EnrollmentController extends ApiController ...@@ -93,7 +93,7 @@ final class EnrollmentController extends ApiController
$enrollments = $db->select( $enrollments = $db->select(
"SELECT gm.id, gm.status, gm.joined_at, "SELECT gm.id, gm.status, gm.joined_at,
tg.name_ar as group_name, tg.group_type, tg.day_of_week, tg.start_time, tg.end_time, tg.name_ar as group_name, tg.group_type, tg.day_of_week, tg.start_time, tg.end_time,
c.name_ar as coach_name, c.full_name_ar as coach_name,
f.name_ar as facility_name, f.name_ar as facility_name,
sd.name_ar as discipline_name sd.name_ar as discipline_name
FROM group_memberships gm FROM group_memberships gm
...@@ -118,7 +118,7 @@ final class EnrollmentController extends ApiController ...@@ -118,7 +118,7 @@ final class EnrollmentController extends ApiController
"SELECT pe.id, pe.skill_level, pe.scores_json, pe.strengths_ar, pe.weaknesses_ar, "SELECT pe.id, pe.skill_level, pe.scores_json, pe.strengths_ar, pe.weaknesses_ar,
pe.status, pe.created_at, pe.status, pe.created_at,
sd.name_ar as discipline_name, sd.name_ar as discipline_name,
c.name_ar as evaluator_name c.full_name_ar as evaluator_name
FROM player_evaluations pe FROM player_evaluations pe
LEFT JOIN sport_disciplines sd ON sd.id = pe.discipline_id LEFT JOIN sport_disciplines sd ON sd.id = pe.discipline_id
LEFT JOIN coaches c ON c.id = pe.evaluator_coach_id LEFT JOIN coaches c ON c.id = pe.evaluator_coach_id
......
...@@ -23,7 +23,7 @@ final class ProfileController extends ApiController ...@@ -23,7 +23,7 @@ final class ProfileController extends ApiController
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$full = $db->selectOne( $full = $db->selectOne(
"SELECT id, name_ar, name_en, phone, date_of_birth, gender, national_id, "SELECT id, full_name_ar AS name_ar, full_name_en AS name_en, phone, date_of_birth, gender, national_id,
member_id, player_type, medical_status, medical_expiry_date, member_id, player_type, medical_status, medical_expiry_date,
guardian_name, guardian_phone, guardian_relationship, guardian_name, guardian_phone, guardian_relationship,
card_status, created_at card_status, created_at
......
...@@ -65,7 +65,7 @@ final class SportBrowseController extends ApiController ...@@ -65,7 +65,7 @@ final class SportBrowseController extends ApiController
"SELECT tg.id, tg.name_ar, tg.group_type, tg.day_of_week, tg.start_time, tg.end_time, "SELECT tg.id, tg.name_ar, tg.group_type, tg.day_of_week, tg.start_time, tg.end_time,
tg.min_capacity, tg.max_capacity, tg.current_count, tg.gender_restriction, tg.min_capacity, tg.max_capacity, tg.current_count, tg.gender_restriction,
tg.age_from, tg.age_to, tg.pricing_tier, tg.age_from, tg.age_to, tg.pricing_tier,
c.name_ar as coach_name, c.full_name_ar as coach_name,
f.name_ar as facility_name, f.name_ar as facility_name,
al.name_ar as level_name al.name_ar as level_name
FROM training_groups tg FROM training_groups tg
...@@ -86,7 +86,7 @@ final class SportBrowseController extends ApiController ...@@ -86,7 +86,7 @@ final class SportBrowseController extends ApiController
$groupId = (int) $id; $groupId = (int) $id;
$group = $db->selectOne( $group = $db->selectOne(
"SELECT tg.*, c.name_ar as coach_name, c.phone as coach_phone, "SELECT tg.*, c.full_name_ar as coach_name, c.phone as coach_phone,
f.name_ar as facility_name, f.facility_type, f.name_ar as facility_name, f.facility_type,
al.name_ar as level_name, al.name_ar as level_name,
a.name_ar as academy_name a.name_ar as academy_name
......
...@@ -18,7 +18,7 @@ final class PlayerAuthService ...@@ -18,7 +18,7 @@ final class PlayerAuthService
public function findPlayerByPhone(string $phone): ?array public function findPlayerByPhone(string $phone): ?array
{ {
return $this->db->selectOne( return $this->db->selectOne(
"SELECT id, name_ar, phone, password_hash, is_archived FROM players WHERE phone = ? AND is_archived = 0", "SELECT id, full_name_ar AS name_ar, phone, password_hash, is_archived FROM players WHERE phone = ? AND is_archived = 0",
[$phone] [$phone]
); );
} }
......
...@@ -30,7 +30,7 @@ final class PlaygroundAttendanceController extends Controller ...@@ -30,7 +30,7 @@ final class PlaygroundAttendanceController extends Controller
$date = $request->get('date', date('Y-m-d')); $date = $request->get('date', date('Y-m-d'));
$slots = $db->select( $slots = $db->select(
"SELECT pss.*, c.name_ar as coach_name "SELECT pss.*, c.full_name_ar as coach_name
FROM playground_schedule_slots pss FROM playground_schedule_slots pss
INNER JOIN playground_monthly_schedules pms ON pms.id = pss.schedule_id INNER JOIN playground_monthly_schedules pms ON pms.id = pss.schedule_id
LEFT JOIN coaches c ON c.id = pss.coach_id LEFT JOIN coaches c ON c.id = pss.coach_id
...@@ -108,7 +108,7 @@ final class PlaygroundAttendanceController extends Controller ...@@ -108,7 +108,7 @@ final class PlaygroundAttendanceController extends Controller
} }
$players = $db->select( $players = $db->select(
"SELECT DISTINCT pss.coach_id, c.name_ar, c.phone "SELECT DISTINCT pss.coach_id, c.full_name_ar, c.phone
FROM playground_schedule_slots pss FROM playground_schedule_slots pss
INNER JOIN coaches c ON c.id = pss.coach_id INNER JOIN coaches c ON c.id = pss.coach_id
WHERE pss.schedule_id = ? AND pss.coach_id IS NOT NULL", WHERE pss.schedule_id = ? AND pss.coach_id IS NOT NULL",
......
...@@ -142,7 +142,7 @@ final class PlaygroundScheduleController extends Controller ...@@ -142,7 +142,7 @@ final class PlaygroundScheduleController extends Controller
$playground = $db->selectOne("SELECT * FROM playground_configurations WHERE id = ?", [(int) $id]); $playground = $db->selectOne("SELECT * FROM playground_configurations WHERE id = ?", [(int) $id]);
$slots = PlaygroundScheduleSlot::getForDateAndHour((int) $schedId, $date, $hour . ':00'); $slots = PlaygroundScheduleSlot::getForDateAndHour((int) $schedId, $date, $hour . ':00');
$coaches = $db->select("SELECT id, name_ar FROM coaches WHERE is_archived = 0 ORDER BY name_ar"); $coaches = $db->select("SELECT id, full_name_ar AS name_ar FROM coaches WHERE is_archived = 0 ORDER BY full_name_ar");
$academies = $db->select("SELECT id, name_ar FROM academies WHERE is_active = 1 ORDER BY name_ar"); $academies = $db->select("SELECT id, name_ar FROM academies WHERE is_active = 1 ORDER BY name_ar");
return $this->view('PlaygroundAdmin.Views.slot_editor', [ return $this->view('PlaygroundAdmin.Views.slot_editor', [
......
...@@ -20,7 +20,7 @@ final class PlaygroundScheduleSlot extends Model ...@@ -20,7 +20,7 @@ final class PlaygroundScheduleSlot extends Model
{ {
$db = \App\Core\App::getInstance()->db(); $db = \App\Core\App::getInstance()->db();
return $db->select( return $db->select(
"SELECT pss.*, c.name_ar as coach_name, a.name_ar as academy_name "SELECT pss.*, c.full_name_ar as coach_name, a.name_ar as academy_name
FROM playground_schedule_slots pss FROM playground_schedule_slots pss
LEFT JOIN coaches c ON c.id = pss.coach_id LEFT JOIN coaches c ON c.id = pss.coach_id
LEFT JOIN academies a ON a.id = pss.academy_id LEFT JOIN academies a ON a.id = pss.academy_id
...@@ -34,7 +34,7 @@ final class PlaygroundScheduleSlot extends Model ...@@ -34,7 +34,7 @@ final class PlaygroundScheduleSlot extends Model
{ {
$db = \App\Core\App::getInstance()->db(); $db = \App\Core\App::getInstance()->db();
return $db->select( return $db->select(
"SELECT pss.*, c.name_ar as coach_name, a.name_ar as academy_name "SELECT pss.*, c.full_name_ar as coach_name, a.name_ar as academy_name
FROM playground_schedule_slots pss FROM playground_schedule_slots pss
LEFT JOIN coaches c ON c.id = pss.coach_id LEFT JOIN coaches c ON c.id = pss.coach_id
LEFT JOIN academies a ON a.id = pss.academy_id LEFT JOIN academies a ON a.id = pss.academy_id
......
...@@ -26,8 +26,8 @@ final class PlaygroundMirrorService ...@@ -26,8 +26,8 @@ final class PlaygroundMirrorService
$reservations = $this->db->select( $reservations = $this->db->select(
"SELECT r.id, r.start_time, r.end_time, r.booker_type, COALESCE(r.player_id, r.member_id) AS booker_id, r.status, r.total_amount, "SELECT r.id, r.start_time, r.end_time, r.booker_type, COALESCE(r.player_id, r.member_id) AS booker_id, r.status, r.total_amount,
CASE WHEN r.booker_type = 'player' THEN p.name_ar CASE WHEN r.booker_type = 'player' THEN p.full_name_ar
WHEN r.booker_type = 'member' THEN m.name_ar WHEN r.booker_type = 'member' THEN m.full_name_ar
ELSE 'ضيف' END as booker_name ELSE 'ضيف' END as booker_name
FROM reservations r FROM reservations r
LEFT JOIN players p ON r.booker_type = 'player' AND p.id = r.player_id LEFT JOIN players p ON r.booker_type = 'player' AND p.id = r.player_id
...@@ -52,7 +52,7 @@ final class PlaygroundMirrorService ...@@ -52,7 +52,7 @@ final class PlaygroundMirrorService
); );
if ($activeSched) { if ($activeSched) {
$scheduledSlots = $this->db->select( $scheduledSlots = $this->db->select(
"SELECT pss.*, c.name_ar as coach_name, a.name_ar as academy_name "SELECT pss.*, c.full_name_ar as coach_name, a.name_ar as academy_name
FROM playground_schedule_slots pss FROM playground_schedule_slots pss
LEFT JOIN coaches c ON c.id = pss.coach_id LEFT JOIN coaches c ON c.id = pss.coach_id
LEFT JOIN academies a ON a.id = pss.academy_id LEFT JOIN academies a ON a.id = pss.academy_id
......
...@@ -182,7 +182,7 @@ final class RegulatoryDiscountService ...@@ -182,7 +182,7 @@ final class RegulatoryDiscountService
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$member = $db->selectOne( $member = $db->selectOne(
"SELECT m.id, m.member_number, m.full_name_ar, m.branch_id, b.name_ar as branch_name FROM members m LEFT JOIN branches b ON b.id = m.branch_id WHERE m.id = ? AND m.branch_id = ? AND m.status = 'active'", "SELECT m.id, m.membership_number, m.full_name_ar, m.branch_id, b.name_ar as branch_name FROM members m LEFT JOIN branches b ON b.id = m.branch_id WHERE m.id = ? AND m.branch_id = ? AND m.status = 'active'",
[$memberId, $sourceBranchId] [$memberId, $sourceBranchId]
); );
......
...@@ -138,7 +138,7 @@ final class DashboardMetricsService ...@@ -138,7 +138,7 @@ final class DashboardMetricsService
$db = App::getInstance()->db(); $db = App::getInstance()->db();
return $db->select(" return $db->select("
SELECT DISTINCT c.id, c.name_ar, c.name_en, SELECT DISTINCT c.id, c.full_name_ar, c.full_name_en,
(SELECT COUNT(*) FROM facility_zone_schedules fzs2 WHERE fzs2.coach_id = c.id AND fzs2.schedule_date BETWEEN ? AND ?) AS session_count (SELECT COUNT(*) FROM facility_zone_schedules fzs2 WHERE fzs2.coach_id = c.id AND fzs2.schedule_date BETWEEN ? AND ?) AS session_count
FROM coaches c FROM coaches c
INNER JOIN facility_zone_schedules fzs ON fzs.coach_id = c.id INNER JOIN facility_zone_schedules fzs ON fzs.coach_id = c.id
......
...@@ -30,7 +30,7 @@ final class CoachAuthController extends ApiController ...@@ -30,7 +30,7 @@ final class CoachAuthController extends ApiController
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$coach = $db->selectOne( $coach = $db->selectOne(
"SELECT id, name_ar, phone, password_hash, is_archived FROM coaches WHERE phone = ? AND is_archived = 0", "SELECT id, full_name_ar AS name_ar, phone, password_hash, is_archived FROM coaches WHERE phone = ? AND is_archived = 0",
[$data['phone']] [$data['phone']]
); );
......
...@@ -104,13 +104,13 @@ final class CoachScheduleController extends ApiController ...@@ -104,13 +104,13 @@ final class CoachScheduleController extends ApiController
} }
$players = $db->select( $players = $db->select(
"SELECT p.id, p.name_ar, p.phone, p.date_of_birth, p.gender, "SELECT p.id, p.full_name_ar, p.phone, p.date_of_birth, p.gender,
p.medical_status, p.medical_expiry_date, p.medical_status, p.medical_expiry_date,
gm.joined_at, gm.status as membership_status gm.joined_at, gm.status as membership_status
FROM group_memberships gm FROM group_memberships gm
INNER JOIN players p ON p.id = gm.player_id INNER JOIN players p ON p.id = gm.player_id
WHERE gm.group_id = ? AND gm.status = 'active' WHERE gm.group_id = ? AND gm.status = 'active'
ORDER BY p.name_ar", ORDER BY p.full_name_ar",
[(int) $id] [(int) $id]
); );
......
...@@ -44,7 +44,7 @@ final class CoachDayOffService ...@@ -44,7 +44,7 @@ final class CoachDayOffService
$requestId = (int) $this->db->selectOne("SELECT LAST_INSERT_ID() as id")['id']; $requestId = (int) $this->db->selectOne("SELECT LAST_INSERT_ID() as id")['id'];
$coach = $this->db->selectOne("SELECT name_ar FROM coaches WHERE id = ?", [$coachId]); $coach = $this->db->selectOne("SELECT full_name_ar AS name_ar FROM coaches WHERE id = ?", [$coachId]);
EventBus::dispatch('coach.day_off_requested', [ EventBus::dispatch('coach.day_off_requested', [
'coach_id' => $coachId, 'coach_id' => $coachId,
...@@ -75,7 +75,7 @@ final class CoachDayOffService ...@@ -75,7 +75,7 @@ final class CoachDayOffService
public function getRequestDetails(int $requestId, int $coachId): ?array public function getRequestDetails(int $requestId, int $coachId): ?array
{ {
$request = $this->db->selectOne( $request = $this->db->selectOne(
"SELECT dor.*, c.name_ar as replacement_name "SELECT dor.*, c.full_name_ar as replacement_name
FROM coach_day_off_requests dor FROM coach_day_off_requests dor
LEFT JOIN coaches c ON c.id = dor.replacement_coach_id LEFT JOIN coaches c ON c.id = dor.replacement_coach_id
WHERE dor.id = ? AND dor.coach_id = ?", WHERE dor.id = ? AND dor.coach_id = ?",
......
...@@ -19,7 +19,7 @@ final class ReplacementService ...@@ -19,7 +19,7 @@ final class ReplacementService
{ {
return $this->db->select( return $this->db->select(
"SELECT dor.id, dor.coach_id, dor.request_date, dor.affected_sessions_json, dor.created_at, "SELECT dor.id, dor.coach_id, dor.request_date, dor.affected_sessions_json, dor.created_at,
c.name_ar as coach_name c.full_name_ar as coach_name
FROM coach_day_off_requests dor FROM coach_day_off_requests dor
INNER JOIN coaches c ON c.id = dor.coach_id INNER JOIN coaches c ON c.id = dor.coach_id
WHERE dor.status = 'pending' AND dor.coach_id != ? AND dor.replacement_coach_id IS NULL WHERE dor.status = 'pending' AND dor.coach_id != ? AND dor.replacement_coach_id IS NULL
...@@ -68,8 +68,8 @@ final class ReplacementService ...@@ -68,8 +68,8 @@ final class ReplacementService
], 'id = ? AND status = ?', [(int) $session['id'], 'scheduled']); ], 'id = ? AND status = ?', [(int) $session['id'], 'scheduled']);
} }
$volunteerCoach = $this->db->selectOne("SELECT name_ar FROM coaches WHERE id = ?", [$volunteerCoachId]); $volunteerCoach = $this->db->selectOne("SELECT full_name_ar AS name_ar FROM coaches WHERE id = ?", [$volunteerCoachId]);
$originalCoach = $this->db->selectOne("SELECT name_ar FROM coaches WHERE id = ?", [(int) $request['coach_id']]); $originalCoach = $this->db->selectOne("SELECT full_name_ar AS name_ar FROM coaches WHERE id = ?", [(int) $request['coach_id']]);
EventBus::dispatch('coach.replacement_confirmed', [ EventBus::dispatch('coach.replacement_confirmed', [
'original_coach_id' => (int) $request['coach_id'], 'original_coach_id' => (int) $request['coach_id'],
......
...@@ -32,7 +32,7 @@ class SessionFeedback extends Model ...@@ -32,7 +32,7 @@ class SessionFeedback extends Model
$db = App::getInstance()->db(); $db = App::getInstance()->db();
return $db->select( return $db->select(
"SELECT sf.*, m.name_ar AS player_name "SELECT sf.*, m.full_name_ar AS player_name
FROM session_feedback sf FROM session_feedback sf
LEFT JOIN members m ON m.id = sf.player_id AND sf.is_anonymous = 0 LEFT JOIN members m ON m.id = sf.player_id AND sf.is_anonymous = 0
WHERE sf.session_id = ? WHERE sf.session_id = ?
......
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""اتصال بقاعدة البيانات الحية بالإعدادات اللي في .env — لأدوات الفحص."""
import os
import pymysql
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def env(path=None):
path = path or os.path.join(ROOT, '.env')
out = {}
with open(path, encoding='utf-8') as fh:
for line in fh:
line = line.strip()
if not line or line.startswith('#') or '=' not in line:
continue
k, v = line.split('=', 1)
out[k.strip()] = v.strip().strip('"').strip("'")
return out
def connect():
e = env()
return pymysql.connect(
host=e['DB_HOST'], port=int(e.get('DB_PORT', 3306)),
user=e['DB_USER'], password=e.get('DB_PASS', ''),
database=e['DB_NAME'], charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor,
)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
فاحص أعمدة الـ SQL — بيقارن كل `alias.column` في كود PHP بالـ schema الحقيقي.
الفكرة: أغلب الـ 500 اللي بتطلع فجأة في الشاشات سببها عمود اتغير اسمه أو
ما اتعملش أصلًا، والكود بيفضل شايل الاسم القديم لحد ما حد يفتح الشاشة.
الفحص ده بيمسك النوع ده من الغلط من غير ما نفتح كل شاشة بإيدينا.
python3 tools/sql_schema_check.py # يفحص كل الكود
python3 tools/sql_schema_check.py app/Modules/Accounting
بيرجّع exit code 1 لو لقى مشاكل، عشان ينفع في أي CI بعدين.
"""
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from db_conn import connect # noqa: E402
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# كلمات محجوزة ما ينفعش نعتبرها أسماء جداول/أعمدة
SQL_KEYWORDS = {
'select', 'from', 'where', 'and', 'or', 'not', 'in', 'is', 'null', 'as', 'on',
'join', 'left', 'right', 'inner', 'outer', 'cross', 'group', 'by', 'order',
'having', 'limit', 'offset', 'union', 'all', 'distinct', 'case', 'when',
'then', 'else', 'end', 'insert', 'into', 'values', 'update', 'set', 'delete',
'count', 'sum', 'avg', 'min', 'max', 'coalesce', 'ifnull', 'if', 'concat',
'date', 'now', 'curdate', 'year', 'month', 'day', 'desc', 'asc', 'like',
'between', 'exists', 'interval', 'cast', 'convert', 'round', 'abs', 'nullif',
'greatest', 'least', 'date_format', 'date_add', 'date_sub', 'datediff',
'timestampdiff', 'lower', 'upper', 'trim', 'substring', 'replace', 'length',
'group_concat', 'json_extract', 'json_unquote', 'row_number', 'over',
'partition', 'with', 'recursive', 'true', 'false', 'default', 'duplicate',
'key', 'ignore', 'straight_join', 'using', 'natural', 'for', 'share', 'lock',
'asc', 'binary', 'collate', 'div', 'mod', 'xor', 'rlike', 'regexp', 'last_insert_id',
# وحدات الوقت اللي بتيجي بعد INTERVAL
'second', 'minute', 'hour', 'week', 'quarter', 'microsecond', 'dayofweek',
'unsigned', 'signed', 'char', 'decimal', 'integer', 'separator', 'both', 'leading', 'trailing',
}
def load_schema(cur):
"""{table: {column, ...}} من الـ schema الحقيقي."""
cur.execute("""SELECT TABLE_NAME, COLUMN_NAME
FROM information_schema.columns
WHERE table_schema = DATABASE()""")
schema = {}
for r in cur.fetchall():
schema.setdefault(r['TABLE_NAME'].lower(), set()).add(r['COLUMN_NAME'].lower())
return schema
def php_strings(src):
"""كل الـ string literals في ملف PHP (مفرد/مزدوج/heredoc)."""
out = []
for m in re.finditer(r'<<<[\'"]?(\w+)[\'"]?\r?\n(.*?)\r?\n\s*\1', src, re.S):
out.append((m.start(), m.group(2)))
for m in re.finditer(r'"((?:[^"\\]|\\.)*)"', src, re.S):
out.append((m.start(), m.group(1)))
for m in re.finditer(r"'((?:[^'\\]|\\.)*)'", src, re.S):
out.append((m.start(), m.group(1)))
return out
def looks_like_sql(s):
"""لازم يبقى SQL فعلي — جمل إنجليزي فيها كلمة from كانت بتتحسب غلط."""
if len(s) < 18:
return False
if not re.search(r'^\s*\(?\s*(SELECT|INSERT|UPDATE|DELETE|REPLACE)\b', s, re.I):
return False
return bool(re.search(r'\b(FROM|JOIN|INSERT\s+INTO|UPDATE)\s+[`\w]', s, re.I))
def alias_map(sql, schema):
"""
alias -> table لكل جدول حقيقي في الاستعلام.
الجداول المشتقة (SELECT ...) t بترجع None عشان نتجاهل أعمدتها.
"""
aliases = {}
derived = set()
# (SELECT ...) alias → جدول مشتق، أعمدته محسوبة مش من الـ schema
for m in re.finditer(r'\)\s*(?:AS\s+)?([a-zA-Z_]\w*)', sql, re.I):
name = m.group(1).lower()
if name not in SQL_KEYWORDS:
derived.add(name)
# ON DUPLICATE KEY UPDATE / ON UPDATE CURRENT_TIMESTAMP مش أسماء جداول
sql = re.sub(r'ON\s+DUPLICATE\s+KEY\s+UPDATE', ' ', sql, flags=re.I)
sql = re.sub(r'ON\s+UPDATE\s+CURRENT_TIMESTAMP', ' ', sql, flags=re.I)
pattern = r'\b(?:FROM|JOIN|UPDATE|INSERT\s+INTO)\s+`?([a-zA-Z_]\w*)`?(?:\s+(?:AS\s+)?`?([a-zA-Z_]\w*)`?)?'
for m in re.finditer(pattern, sql, re.I):
table = m.group(1).lower()
alias = (m.group(2) or '').lower()
if table in SQL_KEYWORDS or table not in schema:
continue
aliases[table] = table
if alias and alias not in SQL_KEYWORDS:
aliases[alias] = table
for d in derived:
aliases.pop(d, None)
return aliases, derived
def check_sql(sql, schema):
"""بيرجّع list من (alias.column, table) اللي مش موجودة."""
problems = []
aliases, derived = alias_map(sql, schema)
if not aliases:
return problems
for m in re.finditer(r'\b([a-zA-Z_]\w*)\.([a-zA-Z_]\w*)\b', sql):
alias, col = m.group(1).lower(), m.group(2).lower()
if alias in derived or alias in SQL_KEYWORDS or col in SQL_KEYWORDS:
continue
table = aliases.get(alias)
if table is None:
continue # alias مش معروف (subquery أو CTE) — نتجاهله
if col not in schema.get(table, set()):
problems.append((f'{alias}.{col}', table))
# الأعمدة من غير alias: نتأكد منها بس لما يكون في جدول واحد بالظبط
# في الاستعلام، وساعتها ما فيش لبس في اسم العمود.
tables = set(aliases.values())
if len(tables) == 1 and not derived and not re.search(r'\bSELECT\b.*\bSELECT\b', sql, re.I | re.S):
table = next(iter(tables))
cols = schema.get(table, set())
body = re.sub(r"'[^']*'", "''", sql) # شيل النصوص عشان ما نقراش جواها
body = re.sub(r'\b\w+\s*\(', ' ( ', body) # شيل أسماء الدوال
for m in re.finditer(r'\b([a-zA-Z_]\w*)\b', body):
col = m.group(1).lower()
# الاختصارات القصيرة دي بتبقى aliases لجداول تانية غالبًا، مش أعمدة
if len(col) <= 2:
continue
if col in SQL_KEYWORDS or col == table or col in aliases:
continue
if col in cols:
continue
# بس اللي في سياق عمود فعلي (بعد SELECT/WHERE/AND/ON/BY/SET/,)
before = body[max(0, m.start() - 14):m.start()]
if not re.search(r'(?:SELECT|WHERE|AND|OR|ON|BY|SET|,|\()\s*$', before, re.I):
continue
problems.append((f'(bare) {col}', table))
return problems
def unknown_tables(sql, schema):
out = []
sql = re.sub(r'ON\s+DUPLICATE\s+KEY\s+UPDATE', ' ', sql, flags=re.I)
sql = re.sub(r'ON\s+UPDATE\s+CURRENT_TIMESTAMP', ' ', sql, flags=re.I)
pattern = r'\b(?:FROM|JOIN|INSERT\s+INTO|UPDATE)\s+`?([a-zA-Z_]\w*)`?'
for m in re.finditer(pattern, sql, re.I):
t = m.group(1).lower()
if t in SQL_KEYWORDS or t in schema:
continue
# الجداول المؤقتة والمشتقة بتتكتب كده كتير — نتأكد إنه اسم جدول فعلًا
if re.match(r'^(dual|information_schema)$', t):
continue
out.append(t)
return out
def line_of(src, pos):
return src.count('\n', 0, pos) + 1
def main():
targets = sys.argv[1:] or ['app']
with connect() as conn, conn.cursor() as cur:
schema = load_schema(cur)
print(f'schema: {len(schema)} tables\n')
files = []
for t in targets:
p = os.path.join(ROOT, t) if not os.path.isabs(t) else t
if os.path.isfile(p):
files.append(p)
continue
for dirpath, _dirs, names in os.walk(p):
for n in names:
if n.endswith('.php'):
files.append(os.path.join(dirpath, n))
total = 0
seen = set()
for f in sorted(files):
try:
src = open(f, encoding='utf-8').read()
except Exception:
continue
rel = os.path.relpath(f, ROOT)
for pos, s in php_strings(src):
if not looks_like_sql(s):
continue
probs = check_sql(s, schema)
for tbl in unknown_tables(s, schema):
probs.append((f'(table) {tbl}', '—'))
for ref, table in probs:
key = (rel, ref, table)
if key in seen:
continue
seen.add(key)
total += 1
print(f'{rel}:{line_of(src, pos)} {ref} [{table}]')
print(f'\n{total} suspicious reference(s)')
return 1 if total else 0
if __name__ == '__main__':
sys.exit(main())
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