Commit 37d6c445 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(sports): add monthly subscription auto-generation cron + stats summary

- SaMonthlySubscriptionJob: runs on days 1-3 of each month, auto-generates
  subscriptions for all active players using SubscriptionGeneratorService
- Subscriptions index page now shows stats bar (total, paid, unpaid, overdue,
  collected amount, pending amount) for the selected month
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 7decae70
...@@ -89,6 +89,20 @@ class SubscriptionController extends Controller ...@@ -89,6 +89,20 @@ class SubscriptionController extends Controller
$disciplines = Discipline::getActive(); $disciplines = Discipline::getActive();
$groups = Group::getActive(); $groups = Group::getActive();
$statsMonth = $filters['month'] !== '' ? $filters['month'] . '-01' : date('Y-m-01');
$stats = $db->selectOne(
"SELECT
COUNT(*) as total,
SUM(CASE WHEN payment_status = 'paid' THEN 1 ELSE 0 END) as paid,
SUM(CASE WHEN payment_status = 'unpaid' THEN 1 ELSE 0 END) as unpaid,
SUM(CASE WHEN payment_status = 'overdue' THEN 1 ELSE 0 END) as overdue,
SUM(CASE WHEN payment_status = 'exempt' THEN 1 ELSE 0 END) as exempt,
SUM(CASE WHEN payment_status = 'paid' THEN final_amount ELSE 0 END) as collected,
SUM(CASE WHEN payment_status IN ('unpaid','overdue') THEN final_amount ELSE 0 END) as pending_amount
FROM sa_subscriptions WHERE period_start = ?",
[$statsMonth]
);
return $this->view('SportsActivity.Views.subscriptions.index', [ return $this->view('SportsActivity.Views.subscriptions.index', [
'subscriptions' => $subscriptions, 'subscriptions' => $subscriptions,
'pagination' => $pagination, 'pagination' => $pagination,
...@@ -96,6 +110,8 @@ class SubscriptionController extends Controller ...@@ -96,6 +110,8 @@ class SubscriptionController extends Controller
'disciplines' => $disciplines, 'disciplines' => $disciplines,
'groups' => $groups, 'groups' => $groups,
'statusOptions' => Subscription::getPaymentStatusOptions(), 'statusOptions' => Subscription::getPaymentStatusOptions(),
'stats' => $stats,
'statsMonth' => substr($statsMonth, 0, 7),
]); ]);
} }
......
...@@ -18,6 +18,37 @@ ...@@ -18,6 +18,37 @@
<?php $__template->section('content'); ?> <?php $__template->section('content'); ?>
<!-- Monthly Stats Summary -->
<?php if (!empty($stats) && (int) ($stats['total'] ?? 0) > 0): ?>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:20px;">
<div class="card" style="padding:16px;text-align:center;">
<div style="font-size:24px;font-weight:700;color:#1A1A2E;"><?= (int) $stats['total'] ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">إجمالي الاشتراكات</div>
<div style="font-size:11px;color:#9CA3AF;margin-top:2px;"><?= e($statsMonth ?? '') ?></div>
</div>
<div class="card" style="padding:16px;text-align:center;border-right:3px solid #059669;">
<div style="font-size:24px;font-weight:700;color:#059669;"><?= (int) ($stats['paid'] ?? 0) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">مدفوع</div>
<div style="font-size:11px;color:#059669;margin-top:2px;"><?= money((float) ($stats['collected'] ?? 0)) ?></div>
</div>
<div class="card" style="padding:16px;text-align:center;border-right:3px solid #F59E0B;">
<div style="font-size:24px;font-weight:700;color:#F59E0B;"><?= (int) ($stats['unpaid'] ?? 0) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">لم يدفع</div>
<div style="font-size:11px;color:#F59E0B;margin-top:2px;"><?= money((float) ($stats['pending_amount'] ?? 0)) ?></div>
</div>
<div class="card" style="padding:16px;text-align:center;border-right:3px solid #DC2626;">
<div style="font-size:24px;font-weight:700;color:#DC2626;"><?= (int) ($stats['overdue'] ?? 0) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">متأخر</div>
</div>
<?php if ((int) ($stats['exempt'] ?? 0) > 0): ?>
<div class="card" style="padding:16px;text-align:center;border-right:3px solid #7C3AED;">
<div style="font-size:24px;font-weight:700;color:#7C3AED;"><?= (int) $stats['exempt'] ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">معفى</div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Filters --> <!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;"> <div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/sa/subscriptions" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;"> <form method="GET" action="/sa/subscriptions" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
......
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\App;
use App\Core\Database;
use App\Core\Logger;
use App\Modules\SportsActivity\Services\SubscriptionGeneratorService;
class SaMonthlySubscriptionJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
return (int) date('j') <= 3;
}
public function run(): array
{
App::getInstance()->setDb($this->db);
$yearMonth = date('Y-m');
$result = SubscriptionGeneratorService::generateForMonth($yearMonth);
Logger::info(sprintf(
"SaMonthlySubscriptionJob: generated=%d, skipped=%d, medical_blocked=%d for %s",
$result['generated'],
$result['skipped'],
$result['medical_blocked'],
$yearMonth
));
return [
'generated' => $result['generated'],
'skipped' => $result['skipped'],
'medical_blocked' => $result['medical_blocked'],
'month' => $yearMonth,
];
}
}
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