Commit 2a77ae3a authored by DevPilot's avatar DevPilot

fix(accounting, sa): missing LC/LG show screens, debtors report repeating每...

fix(accounting, sa): missing LC/LG show screens, debtors report repeating每 debtor, player link fix screen

- شاشتي عرض الاعتماد المستندي وخطاب الضمان مكانوش موجودين أصلاً (View not
  found) — أي حد يدوس «عرض» كان بياخد استثناء. اتعملوا كاملين: البيانات
  والغطاء والعمولة والمستندات، وكمان فورم تحديث الحالة اللي كان متعرّف في
  الكنترولر من غير أي واجهة توصّله (التجديد بيطلب تاريخ انتهاء جديد).

- تقرير المدينين كان بيطلع سطر لكل مطالبة، فالعضو الواحد يتكرر 53 مرة
  (1219 سطر لكل المدينين). بقى سطر واحد لكل مدين بإجمالي مديونيته وتوزيع
  التقادم، وبالضغط عليه بتتفتح تفاصيل مطالباته — من غير ما نخسر أي تفصيلة.

- «صلّح الربط» في شاشة الفجوات كان بيفتح فورم فيه خانة رقم مجرّدة، والأسوأ
  إن الفورم أصلاً بيبعت على /sa/players/{id}/update واللي مش موجود كـ
  route — يعني الحفظ ما كانش بيشتغل خالص. دلوقتي:
  • الفورم بيبعت على المسار الصح.
  • خانة «العضو المرتبط» بقت بحث بالاسم/رقم العضوية وبتختار من النتايج.
  • بتوضّح العضو المربوط حالياً، ولو الرقم المسجّل مش عضو حقيقي بتقول كده
    صراحة — وده بالظبط الحالة اللي الفجوة بتبلّغ عنها.
  • الكنترولر بقى يرفض أي member_id مش رقم عضو موجود، في الإضافة والتعديل —
    عشان الفجوة دي ما تتكررش من أصلها.

- tools/route_smoke.py: كان بيحط id=1 دايماً، والكنترولر بيعمل redirect لما
  الصف مش موجود، فالشاشة ما بتتفتحش والفحص بيعدّي وهو فاضي — وده اللي خلّى
  شاشات ناقصة view تعدّي الفحص وتقع عند المستخدم. بقى بيجيب ID حقيقي من
  قاعدة البيانات لكل نوع، وبيبلّغ عن أي شاشة ما اتفتحتش بدل ما يعتبرها نجاح.
parent 366460fd
...@@ -502,8 +502,17 @@ class ReportController extends Controller ...@@ -502,8 +502,17 @@ class ReportController extends Controller
$outstanding = AccountReceivable::getOutstanding($memberId); $outstanding = AccountReceivable::getOutstanding($memberId);
$aging = AccountReceivable::getAgingSummary(); $aging = AccountReceivable::getAgingSummary();
// مدين واحد في كل سطر. التفاصيل بتتجمّع تحت كل مدين عشان ما يتكررش.
$byMember = AccountReceivable::getOutstandingByMember();
$details = [];
foreach ($outstanding as $row) {
$details[(int) ($row['member_id'] ?? 0)][] = $row;
}
return $this->view('Accounting/Views/reports/accounts_receivable', [ return $this->view('Accounting/Views/reports/accounts_receivable', [
'outstanding' => $outstanding, 'outstanding' => $outstanding,
'byMember' => $byMember,
'details' => $details,
'aging' => $aging, 'aging' => $aging,
'member_id' => $memberId, 'member_id' => $memberId,
]); ]);
......
...@@ -39,6 +39,41 @@ class AccountReceivable extends Model ...@@ -39,6 +39,41 @@ class AccountReceivable extends Model
); );
} }
/**
* صف واحد لكل مدين بإجمالي المديونية وتوزيعها على فترات التقادم.
*
* تقرير المدينين المفروض يبقى مدين في كل سطر — مش سطر لكل فاتورة.
* التفاصيل بتتعرض بالتوسيع من getOutstanding().
*/
public static function getOutstandingByMember(): array
{
$db = App::getInstance()->db();
$today = date('Y-m-d');
return $db->select(
"SELECT ar.member_id,
COALESCE(m.full_name_ar, 'غير مرتبط بعضو') AS member_name,
m.form_number,
m.membership_number,
COUNT(*) AS claims,
SUM(ar.total_amount) AS total_amount,
SUM(ar.paid_amount) AS paid_amount,
SUM(ar.balance) AS balance,
MIN(ar.due_date) AS oldest_due,
SUM(CASE WHEN ar.due_date >= ? THEN ar.balance ELSE 0 END) AS current_amount,
SUM(CASE WHEN ar.due_date < ? AND ar.due_date >= DATE_SUB(?, INTERVAL 30 DAY) THEN ar.balance ELSE 0 END) AS days_30,
SUM(CASE WHEN ar.due_date < DATE_SUB(?, INTERVAL 30 DAY) AND ar.due_date >= DATE_SUB(?, INTERVAL 60 DAY) THEN ar.balance ELSE 0 END) AS days_60,
SUM(CASE WHEN ar.due_date < DATE_SUB(?, INTERVAL 60 DAY) AND ar.due_date >= DATE_SUB(?, INTERVAL 90 DAY) THEN ar.balance ELSE 0 END) AS days_90,
SUM(CASE WHEN ar.due_date < DATE_SUB(?, INTERVAL 90 DAY) THEN ar.balance ELSE 0 END) AS over_90
FROM accounts_receivable ar
LEFT JOIN members m ON m.id = ar.member_id
WHERE ar.is_archived = 0 AND ar.status IN ('pending','partial','overdue')
GROUP BY ar.member_id, m.full_name_ar, m.form_number, m.membership_number
ORDER BY balance DESC",
[$today, $today, $today, $today, $today, $today, $today, $today]
);
}
public static function getAgingSummary(): array public static function getAgingSummary(): array
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تفاصيل الاعتماد المستندي<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statuses = [
'opened' => 'مفتوح',
'shipped' => 'تم الشحن',
'documents_presented' => 'المستندات مقدمة',
'partial_paid' => 'مسدد جزئياً',
'paid' => 'مسدد',
'closed' => 'مقفل',
'cancelled' => 'ملغي',
];
$statusColors = [
'opened' => '#0284C7',
'shipped' => '#7C3AED',
'documents_presented' => '#D97706',
'partial_paid' => '#F59E0B',
'paid' => '#059669',
'closed' => '#6B7280',
'cancelled' => '#DC2626',
];
$st = $credit['status'] ?? '';
$statusLabel = $statuses[$st] ?? ($st ?: '—');
$statusColor = $statusColors[$st] ?? '#374151';
$docTypes = [
'invoice' => 'فاتورة',
'packing_list' => 'قائمة تعبئة',
'bill_of_lading' => 'بوليصة شحن',
'insurance' => 'وثيقة تأمين',
'certificate_of_origin' => 'شهادة منشأ',
'inspection' => 'شهادة فحص',
'other' => 'أخرى',
];
$docStatuses = [
'pending' => ['نتظر', '#6B7280'],
'received' => ['مستلم', '#0284C7'],
'verified' => ['مطابق', '#059669'],
'discrepant' => ['به مخالفة', '#DC2626'],
];
$currency = $credit['currency_code'] ?? 'EGP';
?>
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;">
<h3 style="margin:0;">اعتماد مستندي رقم: <span style="direction:ltr;display:inline-block;"><?= e($credit['lc_number'] ?? '') ?></span></h3>
<span style="background:<?= $statusColor ?>1A;color:<?= $statusColor ?>;font-weight:700;font-size:13px;padding:5px 14px;border-radius:20px;"><?= e($statusLabel) ?></span>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:15px;">
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">البنك المُصدر</div>
<div style="font-size:15px;font-weight:700;margin-top:4px;"><?= e($credit['bank_name'] ?? '—') ?></div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">قيمة الاعتماد</div>
<div style="font-size:18px;font-weight:700;direction:ltr;margin-top:4px;"><?= money($credit['amount'] ?? 0) ?> <span style="font-size:12px;color:#6B7280;"><?= e($currency) ?></span></div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">مبلغ الغطاء</div>
<div style="font-size:18px;font-weight:700;direction:ltr;margin-top:4px;color:#0284C7;"><?= money($credit['margin_amount'] ?? 0) ?></div>
<div style="font-size:11px;color:#9CA3AF;"><?= e($credit['margin_percentage'] ?? 0) ?>%</div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">إجمالي المصروفات</div>
<div style="font-size:18px;font-weight:700;direction:ltr;margin-top:4px;color:#D97706;"><?= money($credit['total_expenses'] ?? 0) ?></div>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-top:18px;">
<div>
<span style="font-size:12px;color:#6B7280;">المستفيد:</span>
<span style="font-weight:600;"><?= e($credit['supplier_name'] ?? $credit['beneficiary_name'] ?? '—') ?></span>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">تاريخ الفتح:</span>
<span style="font-weight:600;direction:ltr;display:inline-block;"><?= e($credit['opening_date'] ?? '—') ?></span>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">تاريخ الشحن:</span>
<span style="font-weight:600;direction:ltr;display:inline-block;"><?= e($credit['shipment_date'] ?? '—') ?></span>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">تاريخ الانتهاء:</span>
<?php
$expired = !empty($credit['expiry_date']) && strtotime($credit['expiry_date']) < strtotime('today')
&& !in_array($st, ['paid', 'closed', 'cancelled'], true);
?>
<span style="font-weight:600;direction:ltr;display:inline-block;<?= $expired ? 'color:#DC2626;' : '' ?>"><?= e($credit['expiry_date'] ?? '—') ?></span>
<?php if ($expired): ?><span style="color:#DC2626;font-size:11px;">(منتهي)</span><?php endif; ?>
</div>
</div>
<?php if (!empty($credit['terms'])): ?>
<div style="margin-top:16px;padding:12px 15px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">شروط الاعتماد</div>
<div style="font-size:13px;line-height:1.9;white-space:pre-wrap;"><?= e($credit['terms']) ?></div>
</div>
<?php endif; ?>
<?php if (!empty($credit['notes'])): ?>
<div style="margin-top:10px;padding:12px 15px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">ملاحظات</div>
<div style="font-size:13px;line-height:1.9;white-space:pre-wrap;"><?= e($credit['notes']) ?></div>
</div>
<?php endif; ?>
</div>
</div>
<?php if (can('accounting.lc.manage')): ?>
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:15px;">تحديث حالة الاعتماد</h3>
</div>
<form method="POST" action="/accounting/documentary-credits/<?= (int) $credit['id'] ?>/status" style="padding:20px;display:flex;gap:12px;align-items:end;flex-wrap:wrap;">
<?= csrf_field() ?>
<div style="min-width:220px;">
<label class="form-label" style="font-size:12px;">الحالة الجديدة</label>
<select name="status" class="form-select" required>
<?php foreach ($statuses as $val => $label): ?>
<option value="<?= e($val) ?>" <?= $st === $val ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary">حفظ الحالة</button>
</form>
</div>
<?php endif; ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:15px;">المستندات</h3>
</div>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>نوع المستند</th>
<th>رقم المستند</th>
<th>تاريخ المستند</th>
<th>تاريخ الاستلام</th>
<th>الحالة</th>
<th>ملاحظات المخالفة</th>
</tr>
</thead>
<tbody>
<?php foreach (($documents ?? []) as $doc): ?>
<?php
$ds = $doc['status'] ?? '';
[$dsLabel, $dsColor] = $docStatuses[$ds] ?? [$ds ?: '—', '#374151'];
?>
<tr>
<td><?= e($docTypes[$doc['document_type'] ?? ''] ?? ($doc['document_type'] ?? '—')) ?></td>
<td style="direction:ltr;text-align:right;"><?= e($doc['document_number'] ?? '—') ?></td>
<td style="direction:ltr;text-align:right;"><?= e($doc['document_date'] ?? '—') ?></td>
<td style="direction:ltr;text-align:right;"><?= e($doc['received_date'] ?? '—') ?></td>
<td><span style="color:<?= $dsColor ?>;font-weight:600;"><?= e($dsLabel) ?></span></td>
<td style="font-size:12px;color:#6B7280;"><?= e($doc['discrepancy_notes'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($documents)): ?>
<tr><td colspan="6" style="text-align:center;color:#6B7280;padding:30px;">لا توجد مستندات مسجلة على هذا الاعتماد</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div style="margin-top:15px;">
<a href="/accounting/documentary-credits" class="btn btn-outline">رجوع للقائمة</a>
</div>
<?php $__template->endSection(); ?>
...@@ -189,8 +189,10 @@ foreach ($brokenLinks as $bl) { ...@@ -189,8 +189,10 @@ foreach ($brokenLinks as $bl) {
مش لاقي حد، وبالتالي محدش بيتابعه. مش لاقي حد، وبالتالي محدش بيتابعه.
</p> </p>
<p style="margin:6px 0 0;color:#6B7280;font-size:12px;line-height:1.9;"> <p style="margin:6px 0 0;color:#6B7280;font-size:12px;line-height:1.9;">
ده مش قرار محاسبي — محدش غير اللي يعرف اللاعب يقدر يقول هو مين. صلّح الربط من ده مش قرار محاسبي — محدش غير اللي يعرف اللاعب يقدر يقول هو مين. زرار
ملف اللاعب، وأول جولة للماسح بعد كده هتظبّط حساب العضو لوحدها. <strong>«صلّح الربط»</strong> بيفتح ملف اللاعب نفسه (مش شاشة إضافة جديدة)،
وهتلاقي خانة «العضو المرتبط» <strong>بتدوّر بالاسم</strong> — اكتب اسم العضو
واختاره من النتائج واحفظ. أول جولة للماسح بعد كده هتظبّط حساب العضو لوحدها.
</p> </p>
<div class="table-responsive" style="margin-top:12px;"> <div class="table-responsive" style="margin-top:12px;">
...@@ -221,10 +223,15 @@ foreach ($brokenLinks as $bl) { ...@@ -221,10 +223,15 @@ foreach ($brokenLinks as $bl) {
<span style="color:#9CA3AF;">مفيش عضو برقم العضوية ده</span> <span style="color:#9CA3AF;">مفيش عضو برقم العضوية ده</span>
<?php endif; ?> <?php endif; ?>
</td> </td>
<td> <td style="white-space:nowrap;">
<a class="btn btn-sm btn-secondary" href="/sa/players/<?= (int) $bl['player_id'] ?>/edit"> <a class="btn btn-sm btn-secondary" href="/sa/players/<?= (int) $bl['player_id'] ?>/edit#member_id_field"
title="يفتح ملف اللاعب نفسه — دوّر بالاسم واختر العضو الصح">
صلّح الربط صلّح الربط
</a> </a>
<a class="btn btn-sm btn-outline" href="/sa/players/<?= (int) $bl['player_id'] ?>"
style="margin-inline-start:4px;" title="عرض ملف اللاعب">
الملف
</a>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تفاصيل خطاب الضمان<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statuses = [
'requested' => 'مطلوب',
'issued' => 'صادر',
'active' => 'ساري',
'released' => 'مُفرج عنه',
'called' => 'تم تسييله',
'expired' => 'منتهي',
'renewed' => 'مُجدد',
];
$statusColors = [
'requested' => '#6B7280',
'issued' => '#0284C7',
'active' => '#059669',
'released' => '#6B7280',
'called' => '#DC2626',
'expired' => '#991B1B',
'renewed' => '#7C3AED',
];
$types = [
'tender' => 'ابتدائي (مناقصة)',
'performance' => 'نهائي (حسن تنفيذ)',
'advance_payment' => 'دفعة مقدمة',
'maintenance' => 'صيانة',
'customs' => 'جمركي',
'other' => 'أخرى',
];
$st = $guarantee['status'] ?? '';
$statusLabel = $statuses[$st] ?? ($st ?: '—');
$statusColor = $statusColors[$st] ?? '#374151';
$currency = $guarantee['currency_code'] ?? 'EGP';
$expired = !empty($guarantee['expiry_date']) && strtotime($guarantee['expiry_date']) < strtotime('today')
&& !in_array($st, ['released', 'called', 'expired'], true);
?>
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;">
<h3 style="margin:0;">خطاب ضمان رقم: <span style="direction:ltr;display:inline-block;"><?= e($guarantee['guarantee_number'] ?? '') ?></span></h3>
<span style="background:<?= $statusColor ?>1A;color:<?= $statusColor ?>;font-weight:700;font-size:13px;padding:5px 14px;border-radius:20px;"><?= e($statusLabel) ?></span>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:15px;">
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">البنك</div>
<div style="font-size:15px;font-weight:700;margin-top:4px;"><?= e($guarantee['bank_name'] ?? '—') ?></div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">قيمة الخطاب</div>
<div style="font-size:18px;font-weight:700;direction:ltr;margin-top:4px;"><?= money($guarantee['amount'] ?? 0) ?> <span style="font-size:12px;color:#6B7280;"><?= e($currency) ?></span></div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">مبلغ الغطاء</div>
<div style="font-size:18px;font-weight:700;direction:ltr;margin-top:4px;color:#0284C7;"><?= money($guarantee['margin_amount'] ?? 0) ?></div>
<div style="font-size:11px;color:#9CA3AF;"><?= e($guarantee['margin_percentage'] ?? 0) ?>%</div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">العمولة</div>
<div style="font-size:18px;font-weight:700;direction:ltr;margin-top:4px;color:#D97706;"><?= money($guarantee['commission_amount'] ?? 0) ?></div>
<div style="font-size:11px;color:#9CA3AF;"><?= e($guarantee['commission_rate'] ?? 0) ?>%</div>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-top:18px;">
<div>
<span style="font-size:12px;color:#6B7280;">المستفيد:</span>
<span style="font-weight:600;"><?= e($guarantee['beneficiary_name'] ?? '—') ?></span>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">نوع الخطاب:</span>
<span style="font-weight:600;"><?= e($types[$guarantee['guarantee_type'] ?? ''] ?? ($guarantee['guarantee_type'] ?? '—')) ?></span>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">تاريخ الإصدار:</span>
<span style="font-weight:600;direction:ltr;display:inline-block;"><?= e($guarantee['issue_date'] ?? '—') ?></span>
</div>
<div>
<span style="font-size:12px;color:#6B7280;">تاريخ الانتهاء:</span>
<span style="font-weight:600;direction:ltr;display:inline-block;<?= $expired ? 'color:#DC2626;' : '' ?>"><?= e($guarantee['expiry_date'] ?? '—') ?></span>
<?php if ($expired): ?><span style="color:#DC2626;font-size:11px;">(منتهي)</span><?php endif; ?>
</div>
<?php if (!empty($guarantee['renewal_date'])): ?>
<div>
<span style="font-size:12px;color:#6B7280;">تاريخ التجديد:</span>
<span style="font-weight:600;direction:ltr;display:inline-block;"><?= e($guarantee['renewal_date']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($guarantee['related_contract'])): ?>
<div>
<span style="font-size:12px;color:#6B7280;">العقد المرتبط:</span>
<span style="font-weight:600;"><?= e($guarantee['related_contract']) ?></span>
</div>
<?php endif; ?>
</div>
<?php if (!empty($guarantee['notes'])): ?>
<div style="margin-top:16px;padding:12px 15px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">ملاحظات</div>
<div style="font-size:13px;line-height:1.9;white-space:pre-wrap;"><?= e($guarantee['notes']) ?></div>
</div>
<?php endif; ?>
</div>
</div>
<?php if (can('accounting.guarantee.manage')): ?>
<div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:15px;">تحديث حالة الخطاب</h3>
</div>
<form method="POST" action="/accounting/guarantees/<?= (int) $guarantee['id'] ?>/status" style="padding:20px;display:flex;gap:12px;align-items:end;flex-wrap:wrap;">
<?= csrf_field() ?>
<div style="min-width:220px;">
<label class="form-label" style="font-size:12px;">الحالة الجديدة</label>
<select name="status" id="lgStatus" class="form-select" required>
<?php foreach ($statuses as $val => $label): ?>
<option value="<?= e($val) ?>" <?= $st === $val ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div id="lgRenewalWrap" style="min-width:220px;display:none;">
<label class="form-label" style="font-size:12px;">تاريخ الانتهاء الجديد <span style="color:#DC2626;">*</span></label>
<input type="date" name="new_expiry_date" id="lgNewExpiry" class="form-input">
</div>
<button type="submit" class="btn btn-primary">حفظ الحالة</button>
</form>
</div>
<script>
(function(){
var sel = document.getElementById('lgStatus');
var wrap = document.getElementById('lgRenewalWrap');
var inp = document.getElementById('lgNewExpiry');
if (!sel || !wrap) return;
function sync(){
var isRenew = sel.value === 'renewed';
wrap.style.display = isRenew ? '' : 'none';
if (inp) inp.required = isRenew;
}
sel.addEventListener('change', sync);
sync();
})();
</script>
<?php endif; ?>
<div style="margin-top:15px;">
<a href="/accounting/guarantees" class="btn btn-outline">رجوع للقائمة</a>
</div>
<?php $__template->endSection(); ?>
...@@ -3,6 +3,14 @@ ...@@ -3,6 +3,14 @@
<?php $__template->section('title'); ?>تقرير المدينين<?php $__template->endSection(); ?> <?php $__template->section('title'); ?>تقرير المدينين<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?> <?php $__template->section('content'); ?>
<?php
$typeLabels = [
'installment' => 'أقساط',
'subscription' => 'اشتراك',
'fine' => 'غرامة',
'membership_fee' => 'عضوية',
];
?>
<!-- Aging Summary --> <!-- Aging Summary -->
<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin-bottom:20px;"> <div style="display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin-bottom:20px;">
<div class="card" style="padding:15px;text-align:center;"> <div class="card" style="padding:15px;text-align:center;">
...@@ -35,41 +43,70 @@ ...@@ -35,41 +43,70 @@
<div class="card" style="margin-bottom:15px;"> <div class="card" style="margin-bottom:15px;">
<div style="padding:15px 20px;"> <div style="padding:15px 20px;">
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;"> <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div style="flex:2;"> <div style="flex:2;min-width:220px;">
<label class="form-label" style="font-size:12px;">بحث</label> <label class="form-label" style="font-size:12px;">بحث باسم المدين أو رقم العضوية</label>
<input type="text" id="ar-search" class="form-input" placeholder="اسم العضو، النوع، الوصف..."> <input type="text" id="ar-search" class="form-input" placeholder="اكتب اسم العضو أو رقم العضوية...">
</div>
<div>
<label class="form-label" style="font-size:12px;">النوع</label>
<select id="ar-type" class="form-select">
<option value="">الكل</option>
<option value="installment">أقساط</option>
<option value="subscription">اشتراك</option>
<option value="fine">غرامة</option>
<option value="membership_fee">عضوية</option>
</select>
</div> </div>
<div> <div>
<label class="form-label" style="font-size:12px;">الحالة</label> <label class="form-label" style="font-size:12px;">الحالة</label>
<select id="ar-status" class="form-select"> <select id="ar-status" class="form-select">
<option value="">الكل</option> <option value="">الكل</option>
<option value="overdue">متأخر فقط</option> <option value="overdue">عليه متأخرات فقط</option>
</select> </select>
</div> </div>
<div style="margin-inline-start:auto;font-size:12px;color:#6B7280;padding-bottom:8px;">
اضغط على أي مدين لعرض تفاصيل مطالباته
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Outstanding List --> <!-- Debtors (one row per member) -->
<div class="card"> <div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;"> <div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;">المدينين المستحقة <span id="ar-count" style="font-size:13px;color:#6B7280;font-weight:400;"></span></h3> <h3 style="margin:0;">المدينون <span id="ar-count" style="font-size:13px;color:#6B7280;font-weight:400;">(<?= number_format(count($byMember ?? [])) ?> مدين)</span></h3>
<span style="font-size:12px;color:#6B7280;">إجمالي المطالبات: <?= number_format(count($outstanding ?? [])) ?></span>
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="data-table" style="width:100%;"> <table class="data-table" style="width:100%;">
<thead> <thead>
<tr> <tr>
<th>العضو</th> <th style="width:28px;"></th>
<th>المدين</th>
<th>رقم العضوية</th>
<th>عدد المطالبات</th>
<th>أقدم استحقاق</th>
<th>جاري</th>
<th>متأخر +90</th>
<th>إجمالي المستحق</th>
</tr>
</thead>
<tbody id="ar-tbody">
<?php foreach (($byMember ?? []) as $row): ?>
<?php
$mid = (int) ($row['member_id'] ?? 0);
$hasOverdue = bccomp((string) (($row['days_30'] ?? 0) + ($row['days_60'] ?? 0) + ($row['days_90'] ?? 0) + ($row['over_90'] ?? 0)), '0', 2) > 0;
$rowDetails = $details[$mid] ?? [];
?>
<tr class="ar-member-row" data-member="<?= $mid ?>"
data-search="<?= e(($row['member_name'] ?? '') . ' ' . ($row['membership_number'] ?? '') . ' ' . ($row['form_number'] ?? '')) ?>"
data-overdue="<?= $hasOverdue ? '1' : '0' ?>"
style="cursor:pointer;">
<td style="text-align:center;color:#9CA3AF;"><span class="ar-toggle"></span></td>
<td style="font-weight:600;"><?= e($row['member_name'] ?? '—') ?></td>
<td style="direction:ltr;text-align:right;font-size:12px;"><?= e($row['membership_number'] ?? $row['form_number'] ?? '—') ?></td>
<td style="text-align:center;"><?= number_format((int) ($row['claims'] ?? 0)) ?></td>
<td style="direction:ltr;text-align:right;font-size:12px;<?= $hasOverdue ? 'color:#DC2626;font-weight:600;' : '' ?>"><?= e($row['oldest_due'] ?? '—') ?></td>
<td style="direction:ltr;text-align:right;color:#059669;"><?= money($row['current_amount'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;color:#991B1B;font-weight:<?= bccomp((string) ($row['over_90'] ?? 0), '0', 2) > 0 ? '700' : '400' ?>;"><?= money($row['over_90'] ?? 0) ?></td>
<td style="direction:ltr;text-align:right;font-weight:700;color:#DC2626;"><?= money($row['balance'] ?? 0) ?></td>
</tr>
<tr class="ar-detail-row" data-detail-for="<?= $mid ?>" style="display:none;background:#F9FAFB;">
<td></td>
<td colspan="7" style="padding:0 15px 14px;">
<table class="data-table" style="width:100%;font-size:12.5px;background:#fff;">
<thead>
<tr>
<th>النوع</th> <th>النوع</th>
<th>الوصف</th> <th>الوصف</th>
<th>تاريخ الاستحقاق</th> <th>تاريخ الاستحقاق</th>
...@@ -79,30 +116,28 @@ ...@@ -79,30 +116,28 @@
<th>الحالة</th> <th>الحالة</th>
</tr> </tr>
</thead> </thead>
<tbody id="ar-tbody"> <tbody>
<?php foreach ($outstanding as $ar): ?> <?php foreach ($rowDetails as $ar): ?>
<?php <?php $isOverdue = !empty($ar['due_date']) && strtotime($ar['due_date']) < time(); ?>
$typeLabel = match($ar['document_type'] ?? '') { <tr>
'installment' => 'أقساط', <td><?= e($typeLabels[$ar['document_type'] ?? ''] ?? ($ar['document_type'] ?? '—')) ?></td>
'subscription' => 'اشتراك', <td><?= e($ar['description_ar'] ?? '—') ?></td>
'fine' => 'غرامة', <td style="direction:ltr;text-align:right;<?= $isOverdue ? 'color:#DC2626;font-weight:600;' : '' ?>"><?= e($ar['due_date'] ?? '—') ?></td>
'membership_fee' => 'عضوية', <td style="direction:ltr;text-align:right;"><?= money($ar['total_amount'] ?? 0) ?></td>
default => $ar['document_type'] ?? '—', <td style="direction:ltr;text-align:right;"><?= money($ar['paid_amount'] ?? 0) ?></td>
}; <td style="direction:ltr;text-align:right;font-weight:600;"><?= money($ar['balance'] ?? 0) ?></td>
$isOverdue = !empty($ar['due_date']) && strtotime($ar['due_date']) < time(); <td style="color:<?= $isOverdue ? '#DC2626' : '#F59E0B' ?>;"><?= $isOverdue ? 'متأخر' : 'مستحق' ?></td>
?>
<tr data-search="<?= e(($ar['member_name'] ?? '') . ' ' . ($ar['description_ar'] ?? '') . ' ' . $typeLabel) ?>" data-type="<?= e($ar['document_type'] ?? '') ?>" data-overdue="<?= $isOverdue ? '1' : '0' ?>">
<td><?= e($ar['member_name'] ?? '—') ?></td>
<td style="font-size:12px;"><?= $typeLabel ?></td>
<td style="font-size:13px;"><?= e($ar['description_ar']) ?></td>
<td style="<?= $isOverdue ? 'color:#DC2626;font-weight:600;' : '' ?>"><?= e($ar['due_date']) ?></td>
<td style="direction:ltr;text-align:right;"><?= money($ar['total_amount']) ?></td>
<td style="direction:ltr;text-align:right;"><?= money($ar['paid_amount']) ?></td>
<td style="direction:ltr;text-align:right;font-weight:600;"><?= money($ar['balance']) ?></td>
<td style="font-size:12px;color:<?= $isOverdue ? '#DC2626' : '#F59E0B' ?>;"><?= $isOverdue ? 'متأخر' : 'مستحق' ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (empty($outstanding)): ?> <?php if (empty($rowDetails)): ?>
<tr><td colspan="7" style="text-align:center;color:#6B7280;padding:14px;">لا توجد تفاصيل</td></tr>
<?php endif; ?>
</tbody>
</table>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($byMember)): ?>
<tr><td colspan="8" style="text-align:center;color:#6B7280;padding:30px;">لا توجد مديونيات مستحقة</td></tr> <tr><td colspan="8" style="text-align:center;color:#6B7280;padding:30px;">لا توجد مديونيات مستحقة</td></tr>
<?php endif; ?> <?php endif; ?>
</tbody> </tbody>
...@@ -114,15 +149,38 @@ function __m(q,h){return !q||(window.ArabicSearch?window.ArabicSearch.score(q,wi ...@@ -114,15 +149,38 @@ function __m(q,h){return !q||(window.ArabicSearch?window.ArabicSearch.score(q,wi
function __n(v){return window.ArabicSearch?window.ArabicSearch.normalize(v):(v||'').toLowerCase();} function __n(v){return window.ArabicSearch?window.ArabicSearch.normalize(v):(v||'').toLowerCase();}
(function(){ (function(){
var s=document.getElementById('ar-search'),t=document.getElementById('ar-type'),st=document.getElementById('ar-status'),c=document.getElementById('ar-count'); var s=document.getElementById('ar-search'),st=document.getElementById('ar-status'),c=document.getElementById('ar-count');
var rows=document.querySelectorAll('#ar-tbody tr[data-search]'); var rows=document.querySelectorAll('#ar-tbody tr.ar-member-row');
function f(){var q=__n(s.value),tv=t.value,sv=st.value,n=0;
rows.forEach(function(r){var ms=__m(q,r.getAttribute('data-search')); function detailRow(id){return document.querySelector('#ar-tbody tr.ar-detail-row[data-detail-for="'+id+'"]');}
var mt=!tv||r.getAttribute('data-type')===tv;
rows.forEach(function(r){
r.addEventListener('click',function(){
var d=detailRow(r.getAttribute('data-member'));
if(!d)return;
var open=d.style.display!=='none';
d.style.display=open?'none':'';
var t=r.querySelector('.ar-toggle');
if(t)t.textContent=open?'▸':'▾';
});
});
function f(){
var q=__n(s.value),sv=st.value,n=0;
rows.forEach(function(r){
var ms=__m(q,r.getAttribute('data-search'));
var mo=sv!=='overdue'||r.getAttribute('data-overdue')==='1'; var mo=sv!=='overdue'||r.getAttribute('data-overdue')==='1';
if(ms&&mt&&mo){r.style.display='';n++;}else{r.style.display='none';}}); var d=detailRow(r.getAttribute('data-member'));
c.textContent=(q||tv||sv)?'('+n+' نتيجة)':'';} if(ms&&mo){r.style.display='';n++;}
if(s)s.addEventListener('input',f);if(t)t.addEventListener('change',f);if(st)st.addEventListener('change',f); else{
r.style.display='none';
if(d){d.style.display='none';var t=r.querySelector('.ar-toggle');if(t)t.textContent='▸';}
}
});
c.textContent='('+n+' مدين)';
}
if(s)s.addEventListener('input',f);
if(st)st.addEventListener('change',f);
})(); })();
</script> </script>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
...@@ -159,7 +159,14 @@ class PlayerController extends Controller ...@@ -159,7 +159,14 @@ class PlayerController extends Controller
$errors[] = 'نوع اللاعب مطلوب'; $errors[] = 'نوع اللاعب مطلوب';
} }
if ($playerType === 'member' && $memberId <= 0) { if ($playerType === 'member' && $memberId <= 0) {
$errors[] = 'رقم العضوية مطلوب للأعضاء'; $errors[] = 'يجب اختيار العضو المرتبط من نتائج البحث';
} elseif ($playerType === 'member' && $memberId > 0) {
// لازم يبقى رقم صف عضو حقيقي — ده اللي بيمنع تكرار فجوة الربط
// اللي بتظهر في /accounting/gaps لما حد يكتب رقم عضوية بدل رقم العضو.
$memberExists = $db->selectOne("SELECT id FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$memberExists) {
$errors[] = 'العضو المختار غير موجود — ابحث بالاسم واختر من النتائج';
}
} }
$validTypes = ['member', 'non_member']; $validTypes = ['member', 'non_member'];
...@@ -353,8 +360,21 @@ class PlayerController extends Controller ...@@ -353,8 +360,21 @@ class PlayerController extends Controller
return $this->redirect('/sa/players')->withError('اللاعب غير موجود'); return $this->redirect('/sa/players')->withError('اللاعب غير موجود');
} }
// العضو المربوط حالياً — بنجيبه بالاسم عشان اللي بيصلّح يشوف هو مربوط بمين
// فعلاً، مش رقم مجرّد. لو الرقم المكتوب مش رقم عضو حقيقي هيرجع null وده
// بالظبط الحالة اللي شاشة الفجوات بتبلّغ عنها.
$linkedMember = null;
if (!empty($player['member_id'])) {
$linkedMember = $db->selectOne(
"SELECT id, full_name_ar, membership_number, form_number, status
FROM members WHERE id = ? AND is_archived = 0",
[(int) $player['member_id']]
);
}
return $this->view('SportsActivity.Views.players.edit', [ return $this->view('SportsActivity.Views.players.edit', [
'player' => $player, 'player' => $player,
'linkedMember' => $linkedMember,
]); ]);
} }
...@@ -406,7 +426,14 @@ class PlayerController extends Controller ...@@ -406,7 +426,14 @@ class PlayerController extends Controller
$errors[] = 'نوع اللاعب مطلوب'; $errors[] = 'نوع اللاعب مطلوب';
} }
if ($playerType === 'member' && $memberId <= 0) { if ($playerType === 'member' && $memberId <= 0) {
$errors[] = 'رقم العضوية مطلوب للأعضاء'; $errors[] = 'يجب اختيار العضو المرتبط من نتائج البحث';
} elseif ($playerType === 'member' && $memberId > 0) {
// لازم يبقى رقم صف عضو حقيقي — ده اللي بيمنع تكرار فجوة الربط
// اللي بتظهر في /accounting/gaps لما حد يكتب رقم عضوية بدل رقم العضو.
$memberExists = $db->selectOne("SELECT id FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$memberExists) {
$errors[] = 'العضو المختار غير موجود — ابحث بالاسم واختر من النتائج';
}
} }
$validTypes = ['member', 'non_member']; $validTypes = ['member', 'non_member'];
......
...@@ -9,7 +9,7 @@ $__template->layout('Layout.main'); ...@@ -9,7 +9,7 @@ $__template->layout('Layout.main');
<?php $__template->section('content'); ?> <?php $__template->section('content'); ?>
<form method="POST" action="/sa/players/<?= (int) $player['id'] ?>/update"> <form method="POST" action="/sa/players/<?= (int) $player['id'] ?>">
<?= csrf_field() ?> <?= csrf_field() ?>
<!-- Basic Information --> <!-- Basic Information -->
...@@ -27,8 +27,35 @@ $__template->layout('Layout.main'); ...@@ -27,8 +27,35 @@ $__template->layout('Layout.main');
</select> </select>
</div> </div>
<div id="member_id_field" style="<?= ($player['player_type'] ?? '') !== 'member' ? 'display:none;' : '' ?>"> <div id="member_id_field" style="<?= ($player['player_type'] ?? '') !== 'member' ? 'display:none;' : '' ?>">
<label class="form-label">رقم العضوية <span style="color:#DC2626;">*</span></label> <label class="form-label">العضو المرتبط <span style="color:#DC2626;">*</span></label>
<input type="number" name="member_id" value="<?= e(old('member_id') ?? $player['member_id'] ?? '') ?>" class="form-input" placeholder="أدخل رقم العضوية">
<?php $rawMemberId = $player['member_id'] ?? ''; ?>
<?php if (!empty($rawMemberId) && empty($linkedMember)): ?>
<div style="background:#FEF2F2;border:1px solid #FECACA;color:#991B1B;border-radius:8px;padding:9px 12px;font-size:12.5px;line-height:1.8;margin-bottom:8px;">
الرقم المسجّل حالياً (<code style="direction:ltr;display:inline-block;"><?= e((string) $rawMemberId) ?></code>)
مش رقم عضو صحيح — الدين مش بيظهر على حساب أي عضو.
دوّر بالاسم تحت واختر العضو الصح.
</div>
<?php endif; ?>
<input type="hidden" name="member_id" id="memberIdHidden" value="<?= e((string) (old('member_id') ?? $rawMemberId)) ?>">
<div id="memberPicked" style="<?= empty($linkedMember) ? 'display:none;' : '' ?>background:#ECFDF5;border:1px solid #A7F3D0;border-radius:8px;padding:9px 12px;font-size:13px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;gap:10px;">
<span>
<strong id="memberPickedName"><?= e($linkedMember['full_name_ar'] ?? '') ?></strong>
<span id="memberPickedNo" style="color:#6B7280;font-size:12px;direction:ltr;display:inline-block;">
<?= $linkedMember ? e((string) ($linkedMember['membership_number'] ?? $linkedMember['form_number'] ?? '')) : '' ?>
</span>
</span>
<button type="button" id="memberClear" class="btn btn-sm btn-outline" style="font-size:11px;padding:2px 10px;">تغيير</button>
</div>
<div id="memberSearchWrap" style="<?= empty($linkedMember) ? '' : 'display:none;' ?>position:relative;">
<input type="text" id="memberSearchInput" class="form-input" autocomplete="off"
placeholder="ابحث باسم العضو أو رقم العضوية أو الرقم القومي...">
<div id="memberResults" style="display:none;position:absolute;z-index:40;inset-inline:0;top:100%;margin-top:4px;background:#fff;border:1px solid #E5E7EB;border-radius:8px;box-shadow:0 6px 18px rgba(0,0,0,.1);max-height:260px;overflow:auto;"></div>
<small style="color:#6B7280;font-size:11px;">اكتب حرفين على الأقل — اختر العضو من النتائج.</small>
</div>
</div> </div>
<div> <div>
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label> <label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
...@@ -126,6 +153,80 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -126,6 +153,80 @@ document.addEventListener('DOMContentLoaded', function() {
memberField.style.display = playerType.value === 'member' ? '' : 'none'; memberField.style.display = playerType.value === 'member' ? '' : 'none';
} }
playerType.addEventListener('change', toggleMemberId); playerType.addEventListener('change', toggleMemberId);
// ── ربط اللاعب بعضو: بحث بالاسم بدل كتابة رقم ────────────────────────
// الفجوة اللي بتظهر في شاشة الفجوات سببها إن حد كتب رقم عضوية في خانة
// عايزة رقم صف العضو. البحث بالاسم بيمنع ده من أصله.
var hidden = document.getElementById('memberIdHidden');
var picked = document.getElementById('memberPicked');
var pName = document.getElementById('memberPickedName');
var pNo = document.getElementById('memberPickedNo');
var clearBtn= document.getElementById('memberClear');
var wrap = document.getElementById('memberSearchWrap');
var input = document.getElementById('memberSearchInput');
var results = document.getElementById('memberResults');
var timer = null;
function showPicked(name, no) {
if (pName) pName.textContent = name || '';
if (pNo) pNo.textContent = no || '';
if (picked) picked.style.display = 'flex';
if (wrap) wrap.style.display = 'none';
if (results) results.style.display = 'none';
}
function showSearch() {
if (picked) picked.style.display = 'none';
if (wrap) wrap.style.display = '';
if (input) { input.value = ''; input.focus(); }
if (results) { results.innerHTML = ''; results.style.display = 'none'; }
}
if (clearBtn) clearBtn.addEventListener('click', function(){ if (hidden) hidden.value=''; showSearch(); });
if (input) {
input.addEventListener('input', function(){
var q = this.value.trim();
clearTimeout(timer);
if (q.length < 2) { results.style.display='none'; return; }
timer = setTimeout(function(){
fetch('/api/members/search?q=' + encodeURIComponent(q), {
headers: {'X-Requested-With':'XMLHttpRequest'}
})
.then(function(r){ return r.json(); })
.then(function(rows){
results.innerHTML = '';
if (!rows || !rows.length) {
results.innerHTML = '<div style="padding:10px 12px;color:#6B7280;font-size:12.5px;">مفيش نتائج مطابقة</div>';
results.style.display = '';
return;
}
rows.forEach(function(m){
var no = m.membership_number || m.national_id || '';
var d = document.createElement('div');
d.style.cssText = 'padding:9px 12px;cursor:pointer;border-bottom:1px solid #F3F4F6;font-size:13px;';
d.innerHTML = '<strong>' + (m.full_name_ar || '') + '</strong>'
+ '<span style="color:#6B7280;font-size:11.5px;margin-inline-start:8px;direction:ltr;display:inline-block;">' + no + '</span>';
d.addEventListener('mouseenter', function(){ d.style.background = '#F9FAFB'; });
d.addEventListener('mouseleave', function(){ d.style.background = ''; });
d.addEventListener('click', function(){
if (hidden) hidden.value = m.id;
showPicked(m.full_name_ar, no);
});
results.appendChild(d);
});
results.style.display = '';
})
.catch(function(){
results.innerHTML = '<div style="padding:10px 12px;color:#DC2626;font-size:12.5px;">تعذّر البحث — حاول تاني</div>';
results.style.display = '';
});
}, 300);
});
}
document.addEventListener('click', function(e){
if (results && wrap && !wrap.contains(e.target)) results.style.display = 'none';
});
}); });
</script> </script>
......
...@@ -48,6 +48,75 @@ def fill_params(path, ids): ...@@ -48,6 +48,75 @@ def fill_params(path, ids):
return re.sub(r'\{(\w+)(?::[^}]+)?\}', repl, path) return re.sub(r'\{(\w+)(?::[^}]+)?\}', repl, path)
# أول جزء في المسار -> الجدول اللي بنجيب منه ID حقيقي.
# من غير ده الـ {id} بتتحط 1، والكنترولر بيعمل redirect لما الصف مش موجود،
# فالشاشة نفسها ما بتتفتحش أصلاً والفحص بيعدّي وهو فاضي — وده اللي خلّى
# شاشات ناقصة view تعدّي من الفحص وتقع عند المستخدم.
ID_TABLES = {
'documentary-credits': 'documentary_credits',
'guarantees': 'letters_of_guarantee',
'loans': 'bank_loans',
'members': 'members',
'players': 'players',
'employees': 'employees',
'suppliers': 'suppliers',
'payments': 'payments',
'receipts': 'receipts',
'fines': 'fines',
'journal-entries': 'journal_entries',
'accounts': 'chart_of_accounts',
'facility-grids': 'facility_grids',
'groups': 'sa_groups',
'bookings': 'sa_bookings',
'invoices': 'vendor_invoices',
}
def load_real_ids():
"""بيجيب أقل ID موجود فعلاً من كل جدول معروف، عشان الشاشات تتفتح بجد."""
try:
import pymysql
except ImportError:
return {}
env = {}
envpath = os.path.join(ROOT, '.env')
if not os.path.exists(envpath):
return {}
for line in open(envpath, encoding='utf-8'):
line = line.strip()
if line and not line.startswith('#') and '=' in line:
k, v = line.split('=', 1)
env[k.strip()] = v.strip().strip('"').strip("'")
try:
conn = pymysql.connect(
host=env.get('DB_HOST', '127.0.0.1'), port=int(env.get('DB_PORT', 3306)),
user=env.get('DB_USER', ''), password=env.get('DB_PASS', ''),
database=env.get('DB_NAME', ''), charset='utf8mb4', connect_timeout=10)
except Exception as e:
print(f'(real-id lookup skipped: {e})', file=sys.stderr)
return {}
out = {}
cur = conn.cursor()
for seg, table in ID_TABLES.items():
try:
cur.execute(f'SELECT MIN(id) FROM `{table}`')
row = cur.fetchone()
if row and row[0]:
out[seg] = int(row[0])
except Exception:
pass
conn.close()
return out
def path_ids(path, real_ids, fallback=1):
"""بيختار ID مناسب للمسار ده حسب أول segment معروف فيه."""
for seg in path.strip('/').split('/'):
if seg in real_ids:
return {'id': real_ids[seg]}
return {'id': fallback}
def login(session, base, username, password): def login(session, base, username, password):
r = session.get(f'{base}/login', timeout=30) r = session.get(f'{base}/login', timeout=30)
token = '' token = ''
...@@ -108,14 +177,16 @@ def main(): ...@@ -108,14 +177,16 @@ def main():
if args.limit: if args.limit:
routes = routes[:args.limit] routes = routes[:args.limit]
ids = {'id': 1} real_ids = load_real_ids()
failures, checked, skipped = [], 0, 0 if real_ids:
print(f'real ids for {len(real_ids)} resource(s)\n')
failures, untested, checked, skipped = [], [], 0, 0
for path, module in routes: for path, module in routes:
if SKIP.search(path): if SKIP.search(path):
skipped += 1 skipped += 1
continue continue
url = args.base + fill_params(path, ids) url = args.base + fill_params(path, path_ids(path, real_ids))
try: try:
r = s.get(url, timeout=45, allow_redirects=True) r = s.get(url, timeout=45, allow_redirects=True)
except Exception as e: except Exception as e:
...@@ -125,8 +196,16 @@ def main(): ...@@ -125,8 +196,16 @@ def main():
if r.status_code >= 500: if r.status_code >= 500:
failures.append((module, path, r.status_code, error_summary(r.text))) failures.append((module, path, r.status_code, error_summary(r.text)))
print(f' [{r.status_code}] {module:22} {path}\n {error_summary(r.text)}') print(f' [{r.status_code}] {module:22} {path}\n {error_summary(r.text)}')
elif '{' in path and r.history:
# اتحوّل لصفحة تانية = الصف مش موجود = الشاشة دي ما اتفتحتش فعلاً.
# لازم نعرف ده، مش نعتبره نجاح.
untested.append((module, path))
print(f'\nchecked {checked}, skipped {skipped}, failures {len(failures)}') print(f'\nchecked {checked}, skipped {skipped}, failures {len(failures)}')
if untested:
print(f'\n⚠ {len(untested)} شاشة ما اتفتحتش (الصف مش موجود — اتحوّلت لقائمة):')
for module, path in untested:
print(f' {module:22} {path}')
if failures: if failures:
print('\n--- summary ---') print('\n--- summary ---')
for module, path, code, msg in failures: for module, path, code, msg in failures:
......
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