Commit 1be9b982 authored by Mahmoud Aglan's avatar Mahmoud Aglan

sports paymert

parent 00f555fb
......@@ -97,6 +97,7 @@ final class AccountCodes
// Activities & Sports (4105)
const ACTIVITY_SUBSCRIPTION = '410501';
const ACADEMY_REVENUE = '410502';
const HOURLY_BOOKING_REVENUE = '410503';
// Single-level revenue accounts
const FINE_REVENUE = '4106';
......@@ -153,6 +154,7 @@ final class AccountCodes
'annual_subscription' => self::ANNUAL_SUBSCRIPTION,
'development_fee' => self::DEVELOPMENT_FEE,
'activity_subscription' => self::ACTIVITY_SUBSCRIPTION,
'hourly_booking' => self::HOURLY_BOOKING_REVENUE,
'down_payment' => self::ACCOUNTS_RECEIVABLE,
'installment' => self::ACCOUNTS_RECEIVABLE,
'fine' => self::FINE_REVENUE,
......
......@@ -176,9 +176,19 @@ class PlayerController extends Controller
[(int) $id]
);
$medicalHistory = $db->select(
"SELECT * FROM sa_player_documents WHERE player_id = ? AND document_type = 'medical_cert' ORDER BY created_at DESC",
[(int) $id]
);
$employee = App::getInstance()->currentEmployee();
$canApproveMedical = $employee && method_exists($employee, 'hasPermission') && $employee->hasPermission('sa.medical.approve');
return $this->view('SportsActivity.Views.players.show', [
'player' => $player,
'documents' => $documents,
'player' => $player,
'documents' => $documents,
'medicalHistory' => $medicalHistory,
'canApproveMedical' => $canApproveMedical,
]);
}
......
......@@ -186,4 +186,100 @@ class PlayerDocumentController extends Controller
return $this->redirect('/sa/players/' . $pid . '/documents')->withSuccess('تم رفض المستند');
}
/**
* Approve a medical document with conditional status.
*/
public function approveConditional(Request $request, string $pid, string $id): Response
{
$db = App::getInstance()->db();
$session = App::getInstance()->session();
$player = $db->selectOne("SELECT * FROM sa_players WHERE id = ?", [(int) $pid]);
if (!$player) {
return $this->redirect('/sa/players')->withError('اللاعب غير موجود');
}
$document = $db->selectOne(
"SELECT * FROM sa_player_documents WHERE id = ? AND player_id = ?",
[(int) $id, (int) $pid]
);
if (!$document) {
return $this->redirect('/sa/players/' . $pid . '/documents')->withError('المستند غير موجود');
}
$expiryDate = trim((string) $request->post('expiry_date', ''));
$conditionalNotes = trim((string) $request->post('conditional_notes', ''));
if ($expiryDate === '') {
return $this->redirect('/sa/players/' . $pid . '/documents')->withError('تاريخ انتهاء الصلاحية مطلوب');
}
if ($conditionalNotes === '') {
return $this->redirect('/sa/players/' . $pid . '/documents')->withError('ملاحظات الاعتماد المشروط مطلوبة');
}
$db->update('sa_player_documents', [
'approval_status' => 'approved',
'conditional_notes' => $conditionalNotes,
'approved_by' => (int) ($session->get('employee_id') ?? 0),
'approved_at' => now(),
'expiry_date' => $expiryDate,
'updated_at' => now(),
], 'id = ?', [(int) $id]);
$db->update('sa_players', [
'medical_status' => 'conditional',
'medical_expiry_date' => $expiryDate,
'updated_at' => now(),
], 'id = ?', [(int) $pid]);
return $this->redirect('/sa/players/' . $pid . '/documents')->withSuccess('تم الاعتماد المشروط وتحديث الحالة الطبية');
}
/**
* Update medical details directly from player screen.
*/
public function updateMedical(Request $request, string $pid): Response
{
$db = App::getInstance()->db();
$player = $db->selectOne("SELECT * FROM sa_players WHERE id = ?", [(int) $pid]);
if (!$player) {
return $this->redirect('/sa/players')->withError('اللاعب غير موجود');
}
$medicalStatus = trim((string) $request->post('medical_status', ''));
$expiryDate = trim((string) $request->post('medical_expiry_date', ''));
$doctorName = trim((string) $request->post('doctor_name', ''));
$clinicName = trim((string) $request->post('clinic_name', ''));
$examDate = trim((string) $request->post('exam_date', ''));
$validStatuses = ['pending', 'fit', 'conditional', 'unfit', 'expired'];
if (!in_array($medicalStatus, $validStatuses, true)) {
return $this->redirect('/sa/players/' . $pid)->withError('حالة طبية غير صالحة');
}
$db->update('sa_players', [
'medical_status' => $medicalStatus,
'medical_expiry_date' => $expiryDate ?: null,
'updated_at' => now(),
], 'id = ?', [(int) $pid]);
$latestDoc = $db->selectOne(
"SELECT id FROM sa_player_documents WHERE player_id = ? AND document_type = 'medical_cert' ORDER BY created_at DESC LIMIT 1",
[(int) $pid]
);
if ($latestDoc) {
$docUpdate = ['updated_at' => now()];
if ($examDate !== '') $docUpdate['exam_date'] = $examDate;
if ($expiryDate !== '') $docUpdate['expiry_date'] = $expiryDate;
if ($doctorName !== '') $docUpdate['doctor_name'] = $doctorName;
if ($clinicName !== '') $docUpdate['clinic_name'] = $clinicName;
$db->update('sa_player_documents', $docUpdate, 'id = ?', [(int) $latestDoc['id']]);
}
return $this->redirect('/sa/players/' . $pid)->withSuccess('تم تحديث البيانات الطبية');
}
}
......@@ -11,6 +11,7 @@ use App\Core\Pagination;
use App\Modules\SportsActivity\Models\Subscription;
use App\Modules\SportsActivity\Models\Group;
use App\Modules\SportsActivity\Services\SubscriptionGeneratorService;
use App\Modules\SportsActivity\Services\SaPaymentService;
class SubscriptionController extends Controller
{
......@@ -132,29 +133,46 @@ class SubscriptionController extends Controller
}
/**
* Mark subscription as paid.
* Process subscription payment via PaymentService (creates receipt + accounting entry).
*/
public function pay(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$paymentMethod = trim((string) $request->post('payment_method', 'cash'));
if (!in_array($paymentMethod, ['cash', 'check', 'visa', 'bank_transfer'], true)) {
return $this->redirect('/sa/subscriptions/' . $id)->withError('طريقة الدفع غير صالحة');
}
$subscription = $db->selectOne(
"SELECT * FROM sa_subscriptions WHERE id = ?",
[(int) $id]
);
$extra = [
'check_number' => $request->post('check_number'),
'check_bank' => $request->post('check_bank'),
'check_date' => $request->post('check_date'),
'visa_reference' => $request->post('visa_reference'),
'transfer_reference' => $request->post('transfer_reference'),
'transfer_bank' => $request->post('transfer_bank'),
];
if (!$subscription) {
return $this->redirect('/sa/subscriptions')->withError('الاشتراك غير موجود');
$result = SaPaymentService::paySubscription((int) $id, $paymentMethod, $extra);
if (!$result['success']) {
return $this->redirect('/sa/subscriptions/' . $id)->withError($result['error'] ?? 'فشل تسجيل الدفع');
}
$db->update('sa_subscriptions', [
'payment_status' => 'paid',
'paid_at' => date('Y-m-d H:i:s'),
'paid_amount' => (float) $subscription['final_amount'],
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]);
return $this->redirect('/sa/subscriptions/' . $id)
->withSuccess('تم الدفع بنجاح — إيصال رقم: ' . ($result['receipt_number'] ?? ''));
}
/**
* Void a subscription payment.
*/
public function void(Request $request, string $id): Response
{
$result = SaPaymentService::voidSubscriptionPayment((int) $id);
if (!$result['success']) {
return $this->redirect('/sa/subscriptions/' . $id)->withError($result['error'] ?? 'فشل إلغاء الدفع');
}
return $this->redirect('/sa/subscriptions/' . $id)->withSuccess('تم تسجيل الدفع بنجاح');
return $this->redirect('/sa/subscriptions/' . $id)->withSuccess('تم إلغاء الدفع بنجاح');
}
/**
......
......@@ -56,8 +56,11 @@ return [
// Player Documents
['GET', '/sa/players/{pid:\d+}/documents', 'SportsActivity\Controllers\PlayerDocumentController@index', ['auth'], 'sa.player.view'],
['POST', '/sa/players/{pid:\d+}/documents', 'SportsActivity\Controllers\PlayerDocumentController@upload', ['auth', 'csrf'], 'sa.player.manage'],
['POST', '/sa/players/{pid:\d+}/documents/upload', 'SportsActivity\Controllers\PlayerDocumentController@upload', ['auth', 'csrf'], 'sa.player.manage'],
['POST', '/sa/players/{pid:\d+}/documents/{id:\d+}/approve', 'SportsActivity\Controllers\PlayerDocumentController@approve', ['auth', 'csrf'], 'sa.medical.approve'],
['POST', '/sa/players/{pid:\d+}/documents/{id:\d+}/reject', 'SportsActivity\Controllers\PlayerDocumentController@reject', ['auth', 'csrf'], 'sa.medical.approve'],
['POST', '/sa/players/{pid:\d+}/documents/{id:\d+}/approve-conditional', 'SportsActivity\Controllers\PlayerDocumentController@approveConditional', ['auth', 'csrf'], 'sa.medical.approve'],
['POST', '/sa/players/{pid:\d+}/medical/update', 'SportsActivity\Controllers\PlayerDocumentController@updateMedical', ['auth', 'csrf'], 'sa.medical.approve'],
// Academies
['GET', '/sa/academies', 'SportsActivity\Controllers\AcademyController@index', ['auth'], 'sa.academy.view'],
......@@ -120,6 +123,7 @@ return [
['POST', '/sa/subscriptions/generate', 'SportsActivity\Controllers\SubscriptionController@generate', ['auth', 'csrf'], 'sa.subscription.generate'],
['GET', '/sa/subscriptions/{id:\d+}', 'SportsActivity\Controllers\SubscriptionController@show', ['auth'], 'sa.subscription.view'],
['POST', '/sa/subscriptions/{id:\d+}/pay', 'SportsActivity\Controllers\SubscriptionController@pay', ['auth', 'csrf'], 'sa.subscription.collect'],
['POST', '/sa/subscriptions/{id:\d+}/void', 'SportsActivity\Controllers\SubscriptionController@void', ['auth', 'csrf'], 'sa.subscription.collect'],
['POST', '/sa/subscriptions/{id:\d+}/exempt', 'SportsActivity\Controllers\SubscriptionController@exempt', ['auth', 'csrf'], 'sa.subscription.exempt'],
// Attendance
......
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Services;
use App\Core\App;
use App\Core\Logger;
use App\Modules\Payments\Services\PaymentService;
final class SaPaymentService
{
public static function paySubscription(int $subscriptionId, string $paymentMethod, array $extra = []): array
{
$db = App::getInstance()->db();
$subscription = $db->selectOne(
"SELECT s.*, p.full_name_ar as player_name, p.member_id, p.player_type,
g.name_ar as group_name
FROM sa_subscriptions s
LEFT JOIN sa_players p ON p.id = s.player_id
LEFT JOIN sa_groups g ON g.id = s.group_id
WHERE s.id = ?",
[$subscriptionId]
);
if (!$subscription) {
return ['success' => false, 'error' => 'الاشتراك غير موجود'];
}
if ($subscription['payment_status'] === 'paid') {
return ['success' => false, 'error' => 'الاشتراك مدفوع بالفعل'];
}
$amount = (string) ($subscription['final_amount'] ?? '0.00');
if (bccomp($amount, '0.01', 2) < 0) {
return ['success' => false, 'error' => 'مبلغ الاشتراك غير صالح'];
}
$memberId = !empty($subscription['member_id']) ? (int) $subscription['member_id'] : null;
$description = 'اشتراك نشاط رياضي';
if (!empty($subscription['group_name'])) {
$description .= ' — ' . $subscription['group_name'];
}
$paymentData = [
'member_id' => $memberId,
'amount' => $amount,
'payment_type' => 'activity_subscription',
'payment_method' => $paymentMethod,
'related_entity_type' => 'sa_subscriptions',
'related_entity_id' => $subscriptionId,
'description' => $description,
'check_number' => $extra['check_number'] ?? null,
'check_bank' => $extra['check_bank'] ?? null,
'check_date' => $extra['check_date'] ?? null,
'visa_reference' => $extra['visa_reference'] ?? null,
'transfer_reference' => $extra['transfer_reference'] ?? null,
'transfer_bank' => $extra['transfer_bank'] ?? null,
];
if ($memberId === null && !empty($subscription['player_name'])) {
$paymentData['guest_name'] = $subscription['player_name'];
}
$result = PaymentService::processPayment($paymentData);
if (!$result['success']) {
return $result;
}
$db->update('sa_subscriptions', [
'payment_status' => 'paid',
'paid_at' => date('Y-m-d H:i:s'),
'paid_amount' => (float) $amount,
'payment_id' => $result['payment_id'],
'receipt_id' => $result['receipt_id'],
'receipt_number' => $result['receipt_number'],
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$subscriptionId]);
return [
'success' => true,
'payment_id' => $result['payment_id'],
'receipt_id' => $result['receipt_id'],
'receipt_number' => $result['receipt_number'],
'amount' => $amount,
];
}
public static function payBooking(int $bookingId, string $paymentMethod, array $extra = []): array
{
$db = App::getInstance()->db();
$booking = $db->selectOne(
"SELECT b.*, p.full_name_ar as booker_name_resolved, p.member_id, p.player_type
FROM sa_bookings b
LEFT JOIN sa_players p ON p.id = b.booker_id AND b.booker_type = 'player'
WHERE b.id = ?",
[$bookingId]
);
if (!$booking) {
return ['success' => false, 'error' => 'الحجز غير موجود'];
}
if ($booking['payment_status'] === 'paid') {
return ['success' => false, 'error' => 'الحجز مدفوع بالفعل'];
}
$amount = (string) ($booking['total_amount'] ?? '0.00');
if (bccomp($amount, '0.01', 2) < 0) {
return ['success' => false, 'error' => 'مبلغ الحجز غير صالح'];
}
$memberId = !empty($booking['member_id']) ? (int) $booking['member_id'] : null;
$paymentType = ($booking['booking_type'] === 'hourly') ? 'hourly_booking' : 'activity_subscription';
$paymentData = [
'member_id' => $memberId,
'amount' => $amount,
'payment_type' => $paymentType,
'payment_method' => $paymentMethod,
'related_entity_type' => 'sa_bookings',
'related_entity_id' => $bookingId,
'description' => 'حجز رياضي — ' . ($booking['booker_name'] ?? $booking['booker_name_resolved'] ?? ''),
'check_number' => $extra['check_number'] ?? null,
'check_bank' => $extra['check_bank'] ?? null,
'check_date' => $extra['check_date'] ?? null,
'visa_reference' => $extra['visa_reference'] ?? null,
'transfer_reference' => $extra['transfer_reference'] ?? null,
'transfer_bank' => $extra['transfer_bank'] ?? null,
];
if ($memberId === null && !empty($booking['booker_name'])) {
$paymentData['guest_name'] = $booking['booker_name'];
}
$result = PaymentService::processPayment($paymentData);
if (!$result['success']) {
return $result;
}
$db->update('sa_bookings', [
'payment_status' => 'paid',
'payment_id' => $result['payment_id'],
'receipt_id' => $result['receipt_id'],
'receipt_number' => $result['receipt_number'],
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$bookingId]);
return [
'success' => true,
'payment_id' => $result['payment_id'],
'receipt_id' => $result['receipt_id'],
'receipt_number' => $result['receipt_number'],
'amount' => $amount,
];
}
public static function voidSubscriptionPayment(int $subscriptionId): array
{
$db = App::getInstance()->db();
$subscription = $db->selectOne(
"SELECT * FROM sa_subscriptions WHERE id = ?",
[$subscriptionId]
);
if (!$subscription) {
return ['success' => false, 'error' => 'الاشتراك غير موجود'];
}
$paymentId = (int) ($subscription['payment_id'] ?? 0);
if ($paymentId < 1) {
return ['success' => false, 'error' => 'لا يوجد دفعة مرتبطة'];
}
$result = PaymentService::voidPayment($paymentId, 'إلغاء اشتراك نشاط رياضي');
if (!$result['success']) {
return $result;
}
$db->update('sa_subscriptions', [
'payment_status' => 'unpaid',
'paid_at' => null,
'paid_amount' => null,
'payment_id' => null,
'receipt_id' => null,
'receipt_number' => null,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$subscriptionId]);
return ['success' => true];
}
}
......@@ -58,6 +58,9 @@ $approvalColors = ['pending' => '#D97706', 'approved' => '#059669', 'rejected' =
<?php if ($aStatus === 'approved' && !empty($doc['expiry_date'])): ?>
<div style="font-size:11px;color:#059669;margin-top:4px;">صالح حتى: <?= e($doc['expiry_date']) ?></div>
<?php endif; ?>
<?php if ($aStatus === 'approved' && !empty($doc['conditional_notes'])): ?>
<div style="font-size:11px;color:#F59E0B;margin-top:4px;">مشروط: <?= e($doc['conditional_notes']) ?></div>
<?php endif; ?>
</td>
<td style="font-size:13px;"><?= e($doc['created_at'] ?? '') ?></td>
<td>
......@@ -75,16 +78,33 @@ $approvalColors = ['pending' => '#D97706', 'approved' => '#059669', 'rejected' =
<form method="POST" action="/sa/players/<?= (int) $player['id'] ?>/documents/<?= (int) $doc['id'] ?>/approve" style="display:inline-flex;gap:4px;align-items:center;">
<?= csrf_field() ?>
<input type="date" name="expiry_date" class="form-input" style="width:140px;height:30px;font-size:12px;" required title="تاريخ انتهاء الصلاحية">
<button type="submit" class="btn btn-sm" style="background:#059669;color:#fff;padding:4px 8px;font-size:11px;" title="اعتماد">
<button type="submit" class="btn btn-sm" style="background:#059669;color:#fff;padding:4px 8px;font-size:11px;" title="اعتماد لائق">
<i data-lucide="check" style="width:12px;height:12px;"></i>
</button>
</form>
<!-- Reject Form -->
<!-- Conditional Approve Button -->
<button type="button" class="btn btn-sm" style="background:#F59E0B;color:#fff;padding:4px 8px;font-size:11px;" title="اعتماد مشروط"
onclick="document.getElementById('cond_form_<?= (int) $doc['id'] ?>').style.display='block'">
<i data-lucide="alert-triangle" style="width:12px;height:12px;"></i>
</button>
<!-- Reject Button -->
<button type="button" class="btn btn-sm" style="background:#DC2626;color:#fff;padding:4px 8px;font-size:11px;" title="رفض"
onclick="document.getElementById('reject_form_<?= (int) $doc['id'] ?>').style.display='block'">
<i data-lucide="x" style="width:12px;height:12px;"></i>
</button>
</div>
<!-- Hidden Conditional Approve Form -->
<form method="POST" action="/sa/players/<?= (int) $player['id'] ?>/documents/<?= (int) $doc['id'] ?>/approve-conditional" id="cond_form_<?= (int) $doc['id'] ?>" style="display:none;margin-top:8px;padding:10px;background:#FFFBEB;border-radius:6px;">
<?= csrf_field() ?>
<div style="font-size:11px;color:#92400E;font-weight:600;margin-bottom:6px;">اعتماد مشروط</div>
<div style="display:flex;gap:4px;flex-wrap:wrap;align-items:center;margin-bottom:6px;">
<input type="date" name="expiry_date" class="form-input" style="width:140px;height:30px;font-size:12px;" required title="تاريخ الانتهاء">
</div>
<div style="margin-bottom:6px;">
<textarea name="conditional_notes" class="form-input" style="width:100%;height:50px;font-size:12px;resize:vertical;" placeholder="ملاحظات الاعتماد المشروط..." required></textarea>
</div>
<button type="submit" class="btn btn-sm" style="background:#F59E0B;color:#fff;padding:4px 10px;font-size:11px;">اعتماد مشروط</button>
</form>
<!-- Hidden Reject Form -->
<form method="POST" action="/sa/players/<?= (int) $player['id'] ?>/documents/<?= (int) $doc['id'] ?>/reject" id="reject_form_<?= (int) $doc['id'] ?>" style="display:none;margin-top:8px;">
<?= csrf_field() ?>
......
......@@ -120,6 +120,111 @@ $relationLabels = ['father' => 'أب', 'mother' => 'أم', 'brother' => 'أخ',
</div>
<?php endif; ?>
<!-- Medical History Timeline + Inline Edit -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;font-size:16px;font-weight:600;">
<i data-lucide="heart-pulse" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;color:#DC2626;"></i>
السجل الطبي
</h3>
<?php if (!empty($canApproveMedical)): ?>
<button type="button" class="btn btn-sm btn-outline" onclick="document.getElementById('medicalEditForm').style.display = document.getElementById('medicalEditForm').style.display === 'none' ? 'block' : 'none';">
<i data-lucide="edit-3" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> تعديل الحالة
</button>
<?php endif; ?>
</div>
<!-- Inline Medical Edit Form -->
<?php if (!empty($canApproveMedical)): ?>
<div id="medicalEditForm" style="display:none;padding:20px;background:#F9FAFB;border-bottom:1px solid #E5E7EB;">
<form method="POST" action="/sa/players/<?= (int) $player['id'] ?>/medical/update">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));gap:12px;margin-bottom:12px;">
<div>
<label class="form-label" style="font-size:11px;">الحالة الطبية</label>
<select name="medical_status" class="form-input" style="font-size:13px;">
<?php foreach ($medLabels as $key => $label): ?>
<option value="<?= $key ?>" <?= ($player['medical_status'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="font-size:11px;">تاريخ انتهاء الصلاحية</label>
<input type="date" name="medical_expiry_date" class="form-input" value="<?= e($player['medical_expiry_date'] ?? '') ?>" style="font-size:13px;">
</div>
<div>
<label class="form-label" style="font-size:11px;">تاريخ الكشف</label>
<input type="date" name="exam_date" class="form-input" style="font-size:13px;">
</div>
<div>
<label class="form-label" style="font-size:11px;">اسم الطبيب</label>
<input type="text" name="doctor_name" class="form-input" placeholder="د. ..." style="font-size:13px;">
</div>
<div>
<label class="form-label" style="font-size:11px;">العيادة / المستشفى</label>
<input type="text" name="clinic_name" class="form-input" placeholder="اسم العيادة" style="font-size:13px;">
</div>
</div>
<button type="submit" class="btn btn-sm btn-primary">
<i data-lucide="save" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> حفظ التعديلات
</button>
</form>
</div>
<?php endif; ?>
<!-- Timeline -->
<?php if (!empty($medicalHistory)): ?>
<div style="padding:20px;">
<?php
$approvalLabels = ['pending' => 'في الانتظار', 'approved' => 'معتمد', 'rejected' => 'مرفوض', 'expired' => 'منتهي'];
$approvalColors = ['pending' => '#D97706', 'approved' => '#059669', 'rejected' => '#DC2626', 'expired' => '#6B7280'];
?>
<?php foreach ($medicalHistory as $idx => $doc): ?>
<?php $aStatus = $doc['approval_status'] ?? 'pending'; $aColor = $approvalColors[$aStatus] ?? '#6B7280'; ?>
<div style="display:flex;gap:16px;padding-bottom:16px;<?= $idx < count($medicalHistory) - 1 ? 'border-bottom:1px dashed #E5E7EB;margin-bottom:16px;' : '' ?>">
<div style="width:10px;height:10px;border-radius:50%;background:<?= $aColor ?>;margin-top:6px;flex-shrink:0;"></div>
<div style="flex:1;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
<span style="padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600;background:<?= $aColor ?>15;color:<?= $aColor ?>;">
<?= e($approvalLabels[$aStatus] ?? $aStatus) ?>
</span>
<span style="font-size:12px;color:#9CA3AF;"><?= e($doc['created_at'] ?? '') ?></span>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(150px, 1fr));gap:8px;font-size:13px;color:#374151;">
<?php if (!empty($doc['exam_date'])): ?>
<div><span style="color:#9CA3AF;">تاريخ الكشف:</span> <?= e($doc['exam_date']) ?></div>
<?php endif; ?>
<?php if (!empty($doc['expiry_date'])): ?>
<div><span style="color:#9CA3AF;">ينتهي:</span> <?= e($doc['expiry_date']) ?></div>
<?php endif; ?>
<?php if (!empty($doc['doctor_name'])): ?>
<div><span style="color:#9CA3AF;">الطبيب:</span> <?= e($doc['doctor_name']) ?></div>
<?php endif; ?>
<?php if (!empty($doc['clinic_name'])): ?>
<div><span style="color:#9CA3AF;">العيادة:</span> <?= e($doc['clinic_name']) ?></div>
<?php endif; ?>
</div>
<?php if (!empty($doc['conditional_notes'])): ?>
<div style="margin-top:6px;padding:8px 12px;background:#FEF3C7;border-radius:6px;font-size:12px;color:#92400E;">
<strong>ملاحظات مشروطة:</strong> <?= e($doc['conditional_notes']) ?>
</div>
<?php endif; ?>
<?php if (!empty($doc['rejection_reason'])): ?>
<div style="margin-top:6px;padding:8px 12px;background:#FEE2E2;border-radius:6px;font-size:12px;color:#991B1B;">
<strong>سبب الرفض:</strong> <?= e($doc['rejection_reason']) ?>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#9CA3AF;">
لا يوجد سجل طبي بعد.
</div>
<?php endif; ?>
</div>
<!-- Documents Section -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
......@@ -151,13 +256,13 @@ $relationLabels = ['father' => 'أب', 'mother' => 'أم', 'brother' => 'أخ',
</td>
<td>
<?php
$approvalLabels = ['pending' => 'في الانتظار', 'approved' => 'معتمد', 'rejected' => 'مرفوض'];
$approvalColors = ['pending' => '#D97706', 'approved' => '#059669', 'rejected' => '#DC2626'];
$aStatus = $doc['approval_status'] ?? 'pending';
$aColor = $approvalColors[$aStatus] ?? '#6B7280';
$approvalLabels2 = ['pending' => 'في الانتظار', 'approved' => 'معتمد', 'rejected' => 'مرفوض', 'expired' => 'منتهي'];
$approvalColors2 = ['pending' => '#D97706', 'approved' => '#059669', 'rejected' => '#DC2626', 'expired' => '#6B7280'];
$aStatus2 = $doc['approval_status'] ?? 'pending';
$aColor2 = $approvalColors2[$aStatus2] ?? '#6B7280';
?>
<span style="padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600;background:<?= $aColor ?>15;color:<?= $aColor ?>;">
<?= e($approvalLabels[$aStatus] ?? $aStatus) ?>
<span style="padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600;background:<?= $aColor2 ?>15;color:<?= $aColor2 ?>;">
<?= e($approvalLabels2[$aStatus2] ?? $aStatus2) ?>
</span>
</td>
<td style="font-size:13px;"><?= e($doc['created_at'] ?? '') ?></td>
......
......@@ -77,19 +77,110 @@ $statusStyle = $statusColors[$sub['payment_status'] ?? ''] ?? 'background:#F3F4F
</div>
</div>
<!-- Receipt Info (if paid) -->
<?php if ($sub['payment_status'] === 'paid' && !empty($sub['receipt_number'])): ?>
<div class="card" style="margin-bottom:20px;padding:20px;background:#ECFDF5;border:1px solid #A7F3D0;">
<div style="display:flex;align-items:center;justify-content:space-between;">
<div>
<div style="font-size:13px;color:#065F46;font-weight:600;margin-bottom:4px;">
<i data-lucide="check-circle" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i>
تم الدفع — إيصال رقم: <?= e($sub['receipt_number']) ?>
</div>
<div style="font-size:12px;color:#047857;">
بتاريخ <?= e($sub['paid_at'] ?? '') ?>
</div>
</div>
<div style="display:flex;gap:8px;">
<?php if (!empty($sub['receipt_id'])): ?>
<a href="/receipts/<?= (int) $sub['receipt_id'] ?>/print" target="_blank" class="btn btn-sm btn-primary">
<i data-lucide="printer" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> طباعة الإيصال
</a>
<?php endif; ?>
<form method="POST" action="/sa/subscriptions/<?= (int) $sub['id'] ?>/void" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-sm btn-outline" style="color:#DC2626;border-color:#DC2626;" onclick="return confirm('هل أنت متأكد من إلغاء هذه الدفعة؟');">
<i data-lucide="x-circle" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> إلغاء الدفع
</button>
</form>
</div>
</div>
</div>
<?php endif; ?>
<!-- Actions -->
<?php if (in_array($sub['payment_status'] ?? '', ['unpaid', 'overdue', 'partial'], true)): ?>
<div style="display:flex;gap:15px;flex-wrap:wrap;">
<!-- Pay Button -->
<div class="card" style="flex:1;min-width:280px;padding:20px;">
<h4 style="margin:0 0 15px;font-size:15px;">تسجيل الدفع</h4>
<form method="POST" action="/sa/subscriptions/<?= (int) $sub['id'] ?>/pay">
<!-- Payment Form -->
<div class="card" style="flex:2;min-width:360px;padding:20px;">
<h4 style="margin:0 0 15px;font-size:15px;">
<i data-lucide="credit-card" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i>
تسجيل الدفع
</h4>
<form method="POST" action="/sa/subscriptions/<?= (int) $sub['id'] ?>/pay" id="payForm">
<?= csrf_field() ?>
<p style="color:#6B7280;font-size:13px;margin-bottom:15px;">
سيتم تسجيل دفع المبلغ الكامل: <strong><?= money((float) ($sub['final_amount'] ?? 0)) ?></strong>
المبلغ المطلوب: <strong style="font-size:15px;color:#059669;"><?= money((float) ($sub['final_amount'] ?? 0)) ?></strong>
</p>
<button type="submit" class="btn btn-primary" onclick="return confirm('تأكيد تسجيل الدفع؟');">
<i data-lucide="check-circle" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> تأكيد الدفع
<!-- Payment Method -->
<div style="margin-bottom:15px;">
<label class="form-label" style="font-size:12px;font-weight:600;">طريقة الدفع</label>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:6px;">
<label style="display:flex;align-items:center;gap:6px;padding:8px 14px;border:2px solid #E5E7EB;border-radius:8px;cursor:pointer;font-size:13px;transition:all .15s;" class="method-option">
<input type="radio" name="payment_method" value="cash" checked style="accent-color:#059669;"> نقدي
</label>
<label style="display:flex;align-items:center;gap:6px;padding:8px 14px;border:2px solid #E5E7EB;border-radius:8px;cursor:pointer;font-size:13px;transition:all .15s;" class="method-option">
<input type="radio" name="payment_method" value="visa" style="accent-color:#059669;"> فيزا
</label>
<label style="display:flex;align-items:center;gap:6px;padding:8px 14px;border:2px solid #E5E7EB;border-radius:8px;cursor:pointer;font-size:13px;transition:all .15s;" class="method-option">
<input type="radio" name="payment_method" value="check" style="accent-color:#059669;"> شيك
</label>
<label style="display:flex;align-items:center;gap:6px;padding:8px 14px;border:2px solid #E5E7EB;border-radius:8px;cursor:pointer;font-size:13px;transition:all .15s;" class="method-option">
<input type="radio" name="payment_method" value="bank_transfer" style="accent-color:#059669;"> تحويل بنكي
</label>
</div>
</div>
<!-- Check Fields -->
<div id="checkFields" style="display:none;margin-bottom:15px;padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div>
<label class="form-label" style="font-size:11px;">رقم الشيك</label>
<input type="text" name="check_number" class="form-input" placeholder="رقم الشيك">
</div>
<div>
<label class="form-label" style="font-size:11px;">البنك</label>
<input type="text" name="check_bank" class="form-input" placeholder="اسم البنك">
</div>
<div>
<label class="form-label" style="font-size:11px;">تاريخ الاستحقاق</label>
<input type="date" name="check_date" class="form-input">
</div>
</div>
</div>
<!-- Visa Field -->
<div id="visaFields" style="display:none;margin-bottom:15px;padding:12px;background:#F9FAFB;border-radius:8px;">
<label class="form-label" style="font-size:11px;">رقم مرجع الفيزا</label>
<input type="text" name="visa_reference" class="form-input" placeholder="Visa Reference">
</div>
<!-- Transfer Fields -->
<div id="transferFields" style="display:none;margin-bottom:15px;padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div>
<label class="form-label" style="font-size:11px;">رقم مرجع التحويل</label>
<input type="text" name="transfer_reference" class="form-input" placeholder="Transfer Reference">
</div>
<div>
<label class="form-label" style="font-size:11px;">البنك</label>
<input type="text" name="transfer_bank" class="form-input" placeholder="اسم البنك">
</div>
</div>
</div>
<button type="submit" class="btn btn-primary" style="width:100%;padding:10px;" onclick="return confirm('تأكيد الدفع؟');">
<i data-lucide="banknote" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> ادفع الآن
</button>
</form>
</div>
......@@ -114,6 +205,27 @@ $statusStyle = $statusColors[$sub['payment_status'] ?? ''] ?? 'background:#F3F4F
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') { lucide.createIcons(); }
const radios = document.querySelectorAll('input[name="payment_method"]');
const checkFields = document.getElementById('checkFields');
const visaFields = document.getElementById('visaFields');
const transferFields = document.getElementById('transferFields');
function toggleFields() {
const selected = document.querySelector('input[name="payment_method"]:checked')?.value || 'cash';
checkFields.style.display = selected === 'check' ? 'block' : 'none';
visaFields.style.display = selected === 'visa' ? 'block' : 'none';
transferFields.style.display = selected === 'bank_transfer' ? 'block' : 'none';
document.querySelectorAll('.method-option').forEach(function(el) {
const radio = el.querySelector('input[type="radio"]');
el.style.borderColor = radio.checked ? '#059669' : '#E5E7EB';
el.style.background = radio.checked ? '#ECFDF5' : '#fff';
});
}
radios.forEach(function(r) { r.addEventListener('change', toggleFields); });
toggleFields();
});
</script>
<?php $__template->endSection(); ?>
......@@ -3,6 +3,9 @@ declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
use App\Core\EventBus;
use App\Core\App;
use App\Core\Logger;
MenuRegistry::register('sports_activity', [
'label_ar' => 'الأنشطة الرياضية',
......@@ -71,3 +74,67 @@ PermissionRegistry::register('sports_activity', [
'sa.waitlist.manage' => ['ar' => 'إدارة قائمة الانتظار', 'en' => 'Manage Waitlist'],
'sa.pool-grid.manage' => ['ar' => 'إدارة شبكة حمام السباحة', 'en' => 'Manage Pool Grid'],
]);
// ─── Event Listeners ────────────────────────────────────────────────────────
EventBus::listen('payment_request.completed', function (array $data): void {
$entityType = $data['related_entity_type'] ?? '';
try {
$db = App::getInstance()->db();
if ($entityType === 'sa_subscriptions') {
$db->update('sa_subscriptions', [
'payment_status' => 'paid',
'paid_at' => date('Y-m-d H:i:s'),
'paid_amount' => $data['amount'] ?? 0,
'payment_id' => $data['payment_id'] ?? null,
'receipt_id' => $data['receipt_id'] ?? null,
'receipt_number' => $data['receipt_number'] ?? null,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) ($data['related_entity_id'] ?? 0)]);
} elseif ($entityType === 'sa_bookings') {
$db->update('sa_bookings', [
'payment_status' => 'paid',
'payment_id' => $data['payment_id'] ?? null,
'receipt_id' => $data['receipt_id'] ?? null,
'receipt_number' => $data['receipt_number'] ?? null,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) ($data['related_entity_id'] ?? 0)]);
}
} catch (\Throwable $e) {
Logger::error('SA payment_request.completed listener failed: ' . $e->getMessage());
}
}, 60);
EventBus::listen('payment.voided', function (array $data): void {
try {
$db = App::getInstance()->db();
$paymentId = (int) ($data['payment_id'] ?? 0);
if ($paymentId < 1) return;
$sub = $db->selectOne("SELECT id FROM sa_subscriptions WHERE payment_id = ?", [$paymentId]);
if ($sub) {
$db->update('sa_subscriptions', [
'payment_status' => 'unpaid',
'paid_at' => null,
'paid_amount' => null,
'payment_id' => null,
'receipt_id' => null,
'receipt_number' => null,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $sub['id']]);
}
$bk = $db->selectOne("SELECT id FROM sa_bookings WHERE payment_id = ?", [$paymentId]);
if ($bk) {
$db->update('sa_bookings', [
'payment_status' => 'unpaid',
'payment_id' => null,
'receipt_id' => null,
'receipt_number' => null,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $bk['id']]);
}
} catch (\Throwable $e) {
Logger::error('SA payment.voided listener failed: ' . $e->getMessage());
}
}, 60);
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
use App\Core\EventBus;
use App\Core\Logger;
class SaMedicalExpiryJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
return true;
}
public function run(): array
{
$today = date('Y-m-d');
$thirtyDaysLater = date('Y-m-d', strtotime('+30 days'));
$notified = 0;
$expired = 0;
$docsExpired = 0;
$expiring = $this->db->select("
SELECT id, full_name_ar, phone, medical_expiry_date
FROM sa_players
WHERE is_archived = 0
AND medical_expiry_date IS NOT NULL
AND medical_expiry_date BETWEEN ? AND ?
AND medical_status IN ('fit', 'conditional')
", [$today, $thirtyDaysLater]);
foreach ($expiring as $player) {
EventBus::dispatch('sa.player.medical_expiry_reminder', [
'player_id' => (int) $player['id'],
'player_name' => $player['full_name_ar'],
'phone' => $player['phone'],
'expiry_date' => $player['medical_expiry_date'],
]);
$notified++;
}
$expiredPlayers = $this->db->select("
SELECT id FROM sa_players
WHERE is_archived = 0
AND medical_expiry_date IS NOT NULL
AND medical_expiry_date < ?
AND medical_status IN ('fit', 'conditional')
", [$today]);
foreach ($expiredPlayers as $ep) {
$this->db->update('sa_players', [
'medical_status' => 'expired',
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $ep['id']]);
$expired++;
}
$stmt = $this->db->query("
UPDATE sa_player_documents
SET approval_status = 'expired', updated_at = ?
WHERE document_type = 'medical_cert'
AND approval_status = 'approved'
AND expiry_date IS NOT NULL
AND expiry_date < ?
", [date('Y-m-d H:i:s'), $today]);
$docsExpired = $stmt->rowCount();
Logger::info("SA Medical expiry: {$notified} reminders, {$expired} players expired, {$docsExpired} docs expired");
return ['reminders_sent' => $notified, 'players_expired' => $expired, 'docs_expired' => $docsExpired];
}
}
<?php
declare(strict_types=1);
return [
'up' => "ALTER TABLE `sa_player_documents`
ADD COLUMN `conditional_notes` TEXT NULL AFTER `rejection_reason`;",
'down' => "ALTER TABLE `sa_player_documents`
DROP COLUMN `conditional_notes`;",
];
<?php
declare(strict_types=1);
return [
'up' => "ALTER TABLE `sa_subscriptions`
ADD COLUMN `payment_id` BIGINT UNSIGNED NULL AFTER `paid_amount`,
ADD COLUMN `receipt_id` BIGINT UNSIGNED NULL AFTER `payment_id`,
ADD COLUMN `receipt_number` VARCHAR(50) NULL AFTER `receipt_id`,
ADD INDEX `idx_sa_sub_payment` (`payment_id`);
ALTER TABLE `sa_bookings`
ADD COLUMN `payment_id` BIGINT UNSIGNED NULL AFTER `payment_status`,
ADD COLUMN `receipt_id` BIGINT UNSIGNED NULL AFTER `payment_id`,
ADD COLUMN `receipt_number` VARCHAR(50) NULL AFTER `receipt_id`,
ADD INDEX `idx_sa_bk_payment` (`payment_id`);",
'down' => "ALTER TABLE `sa_subscriptions`
DROP INDEX `idx_sa_sub_payment`,
DROP COLUMN `receipt_number`,
DROP COLUMN `receipt_id`,
DROP COLUMN `payment_id`;
ALTER TABLE `sa_bookings`
DROP INDEX `idx_sa_bk_payment`,
DROP COLUMN `receipt_number`,
DROP COLUMN `receipt_id`,
DROP COLUMN `payment_id`;",
];
<?php
declare(strict_types=1);
return [
'up' => "INSERT INTO `accounts` (`code`, `name_ar`, `name_en`, `parent_code`, `level`, `account_type`, `is_header`, `is_active`, `created_at`)
VALUES ('410503', 'إيراد حجز ساعي', 'Hourly Booking Revenue', '4105', 5, 'revenue', 0, 1, NOW())
ON DUPLICATE KEY UPDATE `name_ar` = VALUES(`name_ar`);",
'down' => "DELETE FROM `accounts` WHERE `code` = '410503';",
];
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