Commit 91ab2d74 authored by Mahmoud Aglan's avatar Mahmoud Aglan

hgdghkd

parent f458cd55
<?php
declare(strict_types=1);
namespace App\Modules\ActivitySubscriptions\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Disciplines\Models\SportDiscipline;
class EnrollWizardController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('activity_sub.view');
$db = App::getInstance()->db();
$disciplines = SportDiscipline::allActive();
$levels = $db->select(
"SELECT al.*, a.name_ar AS academy_name, a.discipline_id
FROM academy_levels al
JOIN academies a ON a.id = al.academy_id
WHERE a.is_active = 1
ORDER BY a.discipline_id, al.sort_order ASC"
);
$groupTypes = [
'private' => ['label' => 'خاص (1 لاعب)', 'max' => 1],
'small_group' => ['label' => 'مجموعة صغيرة (2-4)', 'max' => 4],
'group' => ['label' => 'مجموعة (5-12)', 'max' => 12],
'team' => ['label' => 'فريق (13+)', 'max' => 25],
];
$pricing = $db->select(
"SELECT ap.*, sd.name_ar AS discipline_name
FROM activity_pricing ap
LEFT JOIN sport_disciplines sd ON sd.id = ap.reference_id AND ap.pricing_type = 'discipline'
WHERE ap.is_active = 1 AND ap.pricing_type = 'discipline'
AND (ap.effective_to IS NULL OR ap.effective_to >= CURDATE())
AND ap.effective_from <= CURDATE()
ORDER BY ap.reference_id, ap.group_type"
);
$pricingMatrix = [];
foreach ($pricing as $p) {
$key = $p['reference_id'] . '_' . ($p['group_type'] ?? 'group');
$pricingMatrix[$key] = $p;
}
return $this->view('ActivitySubscriptions.Views.enroll_wizard', [
'disciplines' => $disciplines,
'levels' => $levels,
'groupTypes' => $groupTypes,
'pricingMatrix' => $pricingMatrix,
]);
}
public function getAvailability(Request $request): Response
{
$db = App::getInstance()->db();
$disciplineId = (int) ($_GET['discipline_id'] ?? 0);
$groupType = $_GET['group_type'] ?? 'group';
if (!$disciplineId) {
return $this->json(['success' => false]);
}
$groups = $db->select(
"SELECT tg.*, c.full_name_ar AS coach_name, f.name_ar AS facility_name
FROM training_groups tg
LEFT JOIN coaches c ON c.id = tg.coach_id
LEFT JOIN facilities f ON f.id = tg.facility_id
JOIN academies a ON a.id = tg.academy_id
WHERE a.discipline_id = ? AND tg.group_type = ?
AND tg.is_active = 1 AND tg.is_full = 0
ORDER BY (tg.max_capacity - tg.current_count) DESC",
[$disciplineId, $groupType]
);
return $this->json(['success' => true, 'groups' => $groups]);
}
public function getLevels(Request $request): Response
{
$db = App::getInstance()->db();
$disciplineId = (int) ($_GET['discipline_id'] ?? 0);
if (!$disciplineId) {
return $this->json(['success' => false]);
}
$levels = $db->select(
"SELECT al.id, al.name_ar, al.sort_order
FROM academy_levels al
JOIN academies a ON a.id = al.academy_id
WHERE a.discipline_id = ? AND a.is_active = 1
GROUP BY al.name_ar
ORDER BY al.sort_order ASC",
[$disciplineId]
);
return $this->json(['success' => true, 'levels' => $levels]);
}
public function getPrice(Request $request): Response
{
$disciplineId = (int) ($_GET['discipline_id'] ?? 0);
$groupType = $_GET['group_type'] ?? 'group';
$isMember = ($_GET['is_member'] ?? '1') === '1';
$db = App::getInstance()->db();
$pricing = $db->selectOne(
"SELECT * FROM activity_pricing
WHERE pricing_type = 'discipline' AND reference_id = ?
AND (group_type = ? OR group_type IS NULL)
AND is_active = 1
AND effective_from <= CURDATE()
AND (effective_to IS NULL OR effective_to >= CURDATE())
ORDER BY group_type DESC
LIMIT 1",
[$disciplineId, $groupType]
);
if (!$pricing) {
return $this->json(['success' => true, 'price' => 0, 'note' => 'لم يتم تحديد سعر بعد']);
}
$price = $isMember ? (float) $pricing['member_rate'] : (float) $pricing['nonmember_rate'];
return $this->json(['success' => true, 'price' => $price]);
}
public function enroll(Request $request): Response
{
$this->authorize('academy.enroll');
$db = App::getInstance()->db();
$playerId = (int) ($_POST['player_id'] ?? 0);
$disciplineId = (int) ($_POST['discipline_id'] ?? 0);
$levelId = (int) ($_POST['level_id'] ?? 0);
$groupType = $_POST['group_type'] ?? 'group';
if (!$playerId || !$disciplineId) {
return $this->redirect('/activity-subscriptions/enroll')->withError('يجب تحديد اللاعب والنشاط');
}
$player = $db->selectOne("SELECT * FROM players WHERE id = ?", [$playerId]);
if (!$player) {
return $this->redirect('/activity-subscriptions/enroll')->withError('اللاعب غير موجود');
}
$group = $db->selectOne(
"SELECT tg.*
FROM training_groups tg
JOIN academies a ON a.id = tg.academy_id
WHERE a.discipline_id = ? AND tg.group_type = ?
AND tg.is_active = 1 AND tg.is_full = 0
" . ($levelId ? "AND tg.level_id = ?" : "") . "
ORDER BY (tg.max_capacity - tg.current_count) DESC
LIMIT 1",
$levelId ? [$disciplineId, $groupType, $levelId] : [$disciplineId, $groupType]
);
if (!$group) {
return $this->redirect('/activity-subscriptions/enroll')->withError(
'لا توجد مجموعات متاحة لهذا النشاط والمستوى. يتم إضافة اللاعب لقائمة الانتظار.'
);
}
$academyId = (int) $group['academy_id'];
$groupLevelId = (int) $group['level_id'];
$employee = App::getInstance()->currentEmployee();
$enrollment = \App\Modules\PlayerAffairs\Models\AcademyEnrollment::create([
'player_id' => $playerId,
'academy_id' => $academyId,
'level_id' => $groupLevelId,
'schedule_id' => null,
'season' => date('Y') . '-' . (date('Y') + 1),
'enrolled_at' => date('Y-m-d H:i:s'),
'enrollment_day' => (int) date('j'),
'status' => 'active',
'created_by' => $employee ? (int) $employee->id : null,
]);
$db->insert('group_memberships', [
'group_id' => (int) $group['id'],
'enrollment_id' => (int) $enrollment->id,
'player_id' => $playerId,
'status' => 'active',
'joined_at' => date('Y-m-d H:i:s'),
]);
$db->update('training_groups', [
'current_count' => (int) $group['current_count'] + 1,
'is_full' => ((int) $group['current_count'] + 1) >= (int) $group['max_capacity'] ? 1 : 0,
], 'id = ?', [(int) $group['id']]);
return $this->redirect('/players/' . $playerId)
->withSuccess('تم تسجيل اللاعب بنجاح في ' . ($group['name_ar'] ?? 'المجموعة'));
}
}
<?php
declare(strict_types=1);
namespace App\Modules\ActivitySubscriptions\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
class PlayerSearchController extends Controller
{
public function search(Request $request): Response
{
$q = trim((string) ($_GET['q'] ?? ''));
if (mb_strlen($q) < 2) {
return $this->json(['success' => true, 'players' => []]);
}
$db = App::getInstance()->db();
$players = $db->select(
"SELECT p.id, p.full_name_ar, p.national_id, p.player_type, p.phone
FROM players p
WHERE (p.full_name_ar LIKE ? OR p.national_id LIKE ? OR p.phone LIKE ?)
AND p.is_archived = 0
ORDER BY p.full_name_ar ASC
LIMIT 15",
["%{$q}%", "%{$q}%", "%{$q}%"]
);
$results = [];
foreach ($players as $p) {
$results[] = [
'id' => (int) $p['id'],
'name' => $p['full_name_ar'],
'national_id' => $p['national_id'] ?? '',
'phone' => $p['phone'] ?? '',
'player_type' => $p['player_type'] ?? 'non_member',
];
}
return $this->json(['success' => true, 'players' => $results]);
}
}
......@@ -9,4 +9,14 @@ return [
['GET', '/activity-subscriptions/{id:\d+}', 'ActivitySubscriptions\Controllers\ActivitySubscriptionController@show', ['auth'], 'activity_sub.view'],
['POST', '/activity-subscriptions/{id:\d+}/pay', 'ActivitySubscriptions\Controllers\ActivitySubscriptionController@pay', ['auth', 'csrf'], 'activity_sub.collect'],
['POST', '/activity-subscriptions/{id:\d+}/exempt', 'ActivitySubscriptions\Controllers\ActivitySubscriptionController@exempt', ['auth', 'csrf'], 'activity_sub.exempt'],
// Enrollment Wizard
['GET', '/activity-subscriptions/enroll', 'ActivitySubscriptions\Controllers\EnrollWizardController@index', ['auth'], 'activity_sub.view'],
['GET', '/activity-subscriptions/enroll/levels', 'ActivitySubscriptions\Controllers\EnrollWizardController@getLevels', ['auth'], 'activity_sub.view'],
['GET', '/activity-subscriptions/enroll/price', 'ActivitySubscriptions\Controllers\EnrollWizardController@getPrice', ['auth'], 'activity_sub.view'],
['GET', '/activity-subscriptions/enroll/availability', 'ActivitySubscriptions\Controllers\EnrollWizardController@getAvailability', ['auth'], 'activity_sub.view'],
['POST', '/activity-subscriptions/enroll', 'ActivitySubscriptions\Controllers\EnrollWizardController@enroll', ['auth', 'csrf'], 'academy.enroll'],
// Player Search API (used by wizard)
['GET', '/api/players/search', 'ActivitySubscriptions\Controllers\PlayerSearchController@search', ['auth'], 'player.view'],
];
This diff is collapsed.
......@@ -5,6 +5,9 @@ $__template->layout('Layout.main');
<?php $__template->section('title'); ?>اشتراكات الأنشطة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/activity-subscriptions/enroll" class="btn btn-primary" style="gap:6px;">
<i data-lucide="user-plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> تسجيل لاعب جديد
</a>
<form method="POST" action="/activity-subscriptions/generate" style="display:inline-flex;gap:6px;align-items:center;">
<?= csrf_field() ?>
<input type="month" name="month" value="<?= e(date('Y-m')) ?>" class="form-input" style="width:160px;font-size:13px;">
......@@ -17,6 +20,31 @@ $__template->layout('Layout.main');
<?php $__template->section('content'); ?>
<!-- Workflow Guide -->
<div class="card" style="margin-bottom:20px;padding:16px 20px;border-right:4px solid #0D7377;background:#F0FDFA;">
<div style="display:flex;align-items:start;gap:12px;">
<i data-lucide="info" style="width:20px;height:20px;color:#0D7377;flex-shrink:0;margin-top:2px;"></i>
<div style="font-size:13px;color:#374151;line-height:1.8;">
<strong style="color:#0D7377;">كيف تعمل الاشتراكات:</strong>
اللاعب يختار الرياضة والمستوى ونوع الخدمة (فردي، مجموعة صغيرة، مجموعة، فريق) - والنظام يقوم بتعيينه تلقائيا في المجموعة المناسبة.
الاشتراك الشهري يتولد تلقائيا بناء على التسجيل الفعال. استخدم زر "تسجيل لاعب جديد" لبدء العملية.
<br>
<span style="color:#6B7280;">
<i data-lucide="arrow-left" style="width:12px;height:12px;vertical-align:middle;"></i>
تسجيل لاعب
<i data-lucide="arrow-left" style="width:12px;height:12px;vertical-align:middle;"></i>
اختيار رياضة ومستوى
<i data-lucide="arrow-left" style="width:12px;height:12px;vertical-align:middle;"></i>
تحديد نوع الخدمة
<i data-lucide="arrow-left" style="width:12px;height:12px;vertical-align:middle;"></i>
التعيين التلقائي في مجموعة
<i data-lucide="arrow-left" style="width:12px;height:12px;vertical-align:middle;"></i>
توليد اشتراك شهري
</span>
</div>
</div>
</div>
<?php
$currentStatus = $filters['status'] ?? '';
$allStatuses = ActivitySubscription::getStatuses();
......
......@@ -34,9 +34,19 @@ foreach (($pricings ?? []) as $p) {
<div style="display:flex;align-items:start;gap:12px;">
<i data-lucide="info" style="width:20px;height:20px;color:#0D7377;flex-shrink:0;margin-top:2px;"></i>
<div style="font-size:13px;color:#374151;line-height:1.8;">
<strong>ما هذه الصفحة؟</strong> هنا تحدد سعر الاشتراك الشهري لكل أكاديمية أو نشاط رياضي.<br>
عند توليد الاشتراكات الشهرية، يستخدم النظام هذه الأسعار لحساب المبلغ المطلوب من كل لاعب.<br>
<strong>عضو</strong> = لاعب عضو بالنادي &nbsp;&middot;&nbsp; <strong>غير عضو</strong> = لاعب خارجي &nbsp;&middot;&nbsp; <strong>مسائي</strong> = فترة مسائية (بعد <?= e(date('g', mktime(17, 0))) ?>:00 م)
<strong>كيف يعمل التسعير:</strong>
السعر الشهري يتحدد بناء على ثلاث عوامل: النشاط الرياضي، نوع الخدمة (حجم المجموعة)، ونوع العضوية (عضو / غير عضو).<br>
كل ما قل حجم المجموعة زاد السعر (فردي اغلى من مجموعة). السعر ثابت لكل combination ولا يتغير حسب الاكاديمية او المدرب.<br>
<span style="color:#6B7280;">
<i data-lucide="arrow-left" style="width:12px;height:12px;vertical-align:middle;"></i>
فردي (1 لاعب)
<i data-lucide="arrow-left" style="width:12px;height:12px;vertical-align:middle;"></i>
مجموعة صغيرة (2-4)
<i data-lucide="arrow-left" style="width:12px;height:12px;vertical-align:middle;"></i>
مجموعة (5-12)
<i data-lucide="arrow-left" style="width:12px;height:12px;vertical-align:middle;"></i>
فريق (13+)
</span>
</div>
</div>
</div>
......
......@@ -116,16 +116,38 @@ final class GridStateService
switch ($type) {
case 'row':
$row = (int) ($json['row'] ?? 0);
for ($c = 0; $c < $totalCols; $c++) {
$positions[] = ['row' => $row, 'col' => $c];
// New format: array of {row, col} cells — extract unique rows
if (isset($json[0]['row'])) {
$rows = array_unique(array_column($json, 'row'));
foreach ($rows as $row) {
for ($c = 0; $c < $totalCols; $c++) {
$positions[] = ['row' => (int) $row, 'col' => $c];
}
}
} else {
// Legacy format: {"row": N}
$row = (int) ($json['row'] ?? 0);
for ($c = 0; $c < $totalCols; $c++) {
$positions[] = ['row' => $row, 'col' => $c];
}
}
break;
case 'column':
$col = (int) ($json['col'] ?? 0);
for ($r = 0; $r < $totalRows; $r++) {
$positions[] = ['row' => $r, 'col' => $col];
// New format: array of {row, col} cells — extract unique cols
if (isset($json[0]['row'])) {
$cols = array_unique(array_column($json, 'col'));
foreach ($cols as $col) {
for ($r = 0; $r < $totalRows; $r++) {
$positions[] = ['row' => $r, 'col' => (int) $col];
}
}
} else {
// Legacy format: {"col": N}
$col = (int) ($json['col'] ?? 0);
for ($r = 0; $r < $totalRows; $r++) {
$positions[] = ['row' => $r, 'col' => $col];
}
}
break;
......
This diff is collapsed.
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