Commit e93afcbe authored by Mahmoud Aglan's avatar Mahmoud Aglan

test

parent c0df407f
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Modules\SportsActivity\Services\AcademyPricingService;
class AcademyPricingController extends Controller
{
public function index(Request $request)
{
$this->authorize('sa.pricing.view');
$academies = AcademyPricingService::listAcademies();
$sessionCards = AcademyPricingService::getSessionCardPricing();
return $this->view('SportsActivity.Views.pricing.academies', [
'academies' => $academies,
'sessionCards' => $sessionCards,
]);
}
public function academies(Request $request)
{
$this->authorize('sa.pricing.view');
$academies = AcademyPricingService::listAcademies();
return $this->json(['academies' => $academies]);
}
public function academyDetail(Request $request, string $code)
{
$this->authorize('sa.pricing.view');
$db = \App\Core\App::getInstance()->db();
$rows = $db->select(
"SELECT * FROM sa_academy_pricing
WHERE academy_code = ? AND is_active = 1
AND effective_from <= CURDATE()
AND (effective_to IS NULL OR effective_to >= CURDATE())
ORDER BY category, sort_order",
[$code]
);
if (empty($rows)) {
return $this->json(['error' => 'لا توجد بيانات لهذه الأكاديمية'], 404);
}
$grouped = [];
foreach ($rows as $row) {
$grouped[$row['category']][] = $row;
}
return $this->json([
'academy_code' => $code,
'academy_name_ar' => $rows[0]['academy_name_ar'],
'categories' => $grouped,
]);
}
public function sessionCards(Request $request)
{
$this->authorize('sa.pricing.view');
return $this->json(['cards' => AcademyPricingService::getSessionCardPricing()]);
}
public function laneRentals(Request $request)
{
$this->authorize('sa.pricing.view');
$laneType = $request->get('lane_type');
return $this->json(['rentals' => AcademyPricingService::getLaneRentalPricing($laneType)]);
}
public function facilityRentals(Request $request)
{
$this->authorize('sa.pricing.view');
$facilityType = $request->get('facility_type', '');
$entityType = $request->get('entity_type', '');
$db = \App\Core\App::getInstance()->db();
$sql = "SELECT * FROM sa_rental_entity_pricing WHERE is_active = 1
AND effective_from <= CURDATE()
AND (effective_to IS NULL OR effective_to >= CURDATE())";
$params = [];
if ($facilityType) {
$sql .= " AND facility_type = ?";
$params[] = $facilityType;
}
if ($entityType) {
$sql .= " AND entity_type = ?";
$params[] = $entityType;
}
$sql .= " ORDER BY facility_type, entity_type, time_period";
return $this->json(['rentals' => $db->select($sql, $params)]);
}
public function calculate(Request $request)
{
$this->authorize('sa.pricing.view');
$academyCode = (string) ($request->post('academy_code') ?? '');
$levelNameAr = (string) ($request->post('level_name_ar') ?? '');
$isMember = (bool) ($request->post('is_member') ?? false);
$months = max(1, (int) ($request->post('months') ?? 1));
$hasSibling = (bool) ($request->post('has_sibling') ?? false);
if (!$academyCode || !$levelNameAr) {
return $this->json(['error' => 'يجب تحديد الأكاديمية والمستوى'], 422);
}
$result = AcademyPricingService::getSubscriptionPrice(
$academyCode,
$levelNameAr,
$isMember,
$months,
$hasSibling
);
return $this->json($result);
}
}
......@@ -215,4 +215,13 @@ return [
['POST', '/sa/gate/scan', 'SportsActivity\Controllers\GateController@scan', ['auth', 'csrf'], 'sa.gate.scan'],
['GET', '/sa/gate/log', 'SportsActivity\Controllers\GateController@log', ['auth'], 'sa.gate.view'],
['GET', '/sa/gate/report', 'SportsActivity\Controllers\GateController@report', ['auth'], 'sa.gate.view'],
// ─── Academy Pricing ────────────────────────────────────────────────────────
['GET', '/sa/academy-pricing', 'SportsActivity\Controllers\AcademyPricingController@index', ['auth'], 'sa.pricing.view'],
['GET', '/sa/academy-pricing/academies', 'SportsActivity\Controllers\AcademyPricingController@academies', ['auth'], 'sa.pricing.view'],
['GET', '/sa/academy-pricing/academy/{code}', 'SportsActivity\Controllers\AcademyPricingController@academyDetail', ['auth'], 'sa.pricing.view'],
['GET', '/sa/academy-pricing/session-cards', 'SportsActivity\Controllers\AcademyPricingController@sessionCards', ['auth'], 'sa.pricing.view'],
['GET', '/sa/academy-pricing/lane-rentals', 'SportsActivity\Controllers\AcademyPricingController@laneRentals', ['auth'], 'sa.pricing.view'],
['GET', '/sa/academy-pricing/facility-rentals', 'SportsActivity\Controllers\AcademyPricingController@facilityRentals', ['auth'], 'sa.pricing.view'],
['POST', '/sa/academy-pricing/calculate', 'SportsActivity\Controllers\AcademyPricingController@calculate', ['auth', 'csrf'], 'sa.pricing.view'],
];
This diff is collapsed.
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تسعير الأكاديميات<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/pricing" class="btn btn-outline"><i data-lucide="calculator" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تسعير المرافق</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));gap:12px;margin-bottom:20px;">
<div class="card" style="padding:14px;text-align:center;">
<div style="font-size:11px;color:#6B7280;">أكاديميات</div>
<div style="font-size:22px;font-weight:800;"><?= count($academies) ?></div>
</div>
<div class="card" style="padding:14px;text-align:center;">
<div style="font-size:11px;color:#6B7280;">كروت الحصص</div>
<div style="font-size:22px;font-weight:800;"><?= count($sessionCards) ?></div>
</div>
</div>
<!-- Academies List -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:16px;font-weight:600;"><i data-lucide="building-2" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> قائمة أسعار الأكاديميات — موسم 2025/2026</h3>
</div>
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="background:#F9FAFB;">
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">الأكاديمية</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">التخصص</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">عدد المستويات</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">أقل سعر عضو</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">أعلى سعر عضو</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">تفاصيل</th>
</tr>
</thead>
<tbody>
<?php if (empty($academies)): ?>
<tr><td colspan="6" style="padding:30px;text-align:center;color:#9CA3AF;">لا توجد بيانات — يرجى تشغيل php cli.php seed</td></tr>
<?php else: ?>
<?php foreach ($academies as $a): ?>
<tr style="border-top:1px solid #F3F4F6;">
<td style="padding:10px 15px;font-weight:600;"><?= e($a['academy_name_ar']) ?></td>
<td style="padding:10px 15px;font-size:12px;color:#6B7280;"><?= e($a['discipline_code']) ?></td>
<td style="padding:10px 15px;"><?= (int) $a['pricing_count'] ?></td>
<td style="padding:10px 15px;direction:ltr;text-align:right;"><?= number_format((float) $a['min_price_member'], 0) ?> ج.م</td>
<td style="padding:10px 15px;direction:ltr;text-align:right;"><?= number_format((float) $a['max_price_member'], 0) ?> ج.م</td>
<td style="padding:10px 15px;">
<button type="button" class="btn btn-outline" style="padding:4px 10px;font-size:11px;" onclick="loadAcademy('<?= e($a['academy_code']) ?>')">
<i data-lucide="eye" style="width:12px;height:12px;vertical-align:middle;"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Session Cards -->
<?php if (!empty($sessionCards)): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:15px;font-weight:600;"><i data-lucide="credit-card" style="width:16px;height:16px;vertical-align:middle;margin-left:6px;"></i> كروت حصص السباحة</h3>
</div>
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="background:#F9FAFB;">
<th style="padding:8px 15px;text-align:right;font-weight:600;color:#6B7280;">الكارت</th>
<th style="padding:8px 15px;text-align:right;font-weight:600;color:#6B7280;">عدد الحصص</th>
<th style="padding:8px 15px;text-align:right;font-weight:600;color:#6B7280;">السعر</th>
<th style="padding:8px 15px;text-align:right;font-weight:600;color:#6B7280;">سعر الحصة</th>
</tr>
</thead>
<tbody>
<?php foreach ($sessionCards as $card): ?>
<tr style="border-top:1px solid #F3F4F6;">
<td style="padding:8px 15px;"><?= e($card['level_name_ar']) ?></td>
<td style="padding:8px 15px;"><?= $card['total_sessions'] ?></td>
<td style="padding:8px 15px;direction:ltr;text-align:right;"><?= number_format($card['price'], 0) ?> ج.م</td>
<td style="padding:8px 15px;direction:ltr;text-align:right;color:#059669;"><?= number_format($card['price_per_session'], 0) ?> ج.م</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<!-- Academy Detail Modal -->
<div id="academyDetail" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1000;display:none;align-items:center;justify-content:center;">
<div style="background:white;border-radius:12px;padding:25px;max-width:800px;width:90%;max-height:80vh;overflow-y:auto;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h3 id="academyDetailTitle" style="margin:0;font-size:18px;font-weight:700;"></h3>
<button type="button" onclick="closeDetail()" style="background:none;border:none;cursor:pointer;font-size:20px;"></button>
</div>
<div id="academyDetailBody"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
function loadAcademy(code) {
fetch('/sa/academy-pricing/academy/' + code, {headers:{'X-Requested-With':'XMLHttpRequest'}})
.then(function(r){return r.json();})
.then(function(data) {
if (data.error) { alert(data.error); return; }
document.getElementById('academyDetailTitle').textContent = data.academy_name_ar;
var html = '';
for (var cat in data.categories) {
var catLabel = {'subscription':'اشتراكات','team':'فرق','private_training':'تدريب خاص','session_card':'كروت حصص','lane_rental':'ايجار حارات'}[cat] || cat;
html += '<h4 style="margin:15px 0 8px;font-size:14px;color:#4B5563;border-bottom:1px solid #E5E7EB;padding-bottom:5px;">' + catLabel + '</h4>';
html += '<table style="width:100%;border-collapse:collapse;font-size:12px;margin-bottom:10px;"><thead><tr style="background:#F9FAFB;"><th style="padding:6px 10px;text-align:right;">المستوى</th><th style="padding:6px 10px;text-align:right;">عضو</th><th style="padding:6px 10px;text-align:right;">غير عضو</th><th style="padding:6px 10px;text-align:right;">تدريبات/اسبوع</th></tr></thead><tbody>';
data.categories[cat].forEach(function(row) {
html += '<tr style="border-top:1px solid #F3F4F6;"><td style="padding:6px 10px;">' + (row.level_name_ar||'—') + '</td><td style="padding:6px 10px;direction:ltr;text-align:right;">' + Number(row.price_member).toLocaleString() + '</td><td style="padding:6px 10px;direction:ltr;text-align:right;">' + Number(row.price_nonmember).toLocaleString() + '</td><td style="padding:6px 10px;">' + (row.sessions_per_week||'—') + '</td></tr>';
});
html += '</tbody></table>';
}
document.getElementById('academyDetailBody').innerHTML = html;
document.getElementById('academyDetail').style.display = 'flex';
});
}
function closeDetail() {
document.getElementById('academyDetail').style.display = 'none';
}
</script>
<?php $__template->endSection(); ?>
......@@ -32,6 +32,7 @@ MenuRegistry::register('sports_activity', [
['label_ar' => 'الجدول', 'label_en' => 'Schedule', 'route' => '/sa/schedule', 'permission' => 'sa.schedule.view', 'order' => 14],
['label_ar' => 'الحجوزات', 'label_en' => 'Bookings', 'route' => '/sa/bookings', 'permission' => 'sa.booking.view', 'order' => 15],
['label_ar' => 'التسعير', 'label_en' => 'Pricing', 'route' => '/sa/pricing', 'permission' => 'sa.pricing.view', 'order' => 16],
['label_ar' => 'أسعار الأكاديميات', 'label_en' => 'Academy Pricing', 'route' => '/sa/academy-pricing','permission' => 'sa.pricing.view', 'order' => 16.5],
['label_ar' => 'الاشتراكات', 'label_en' => 'Subscriptions', 'route' => '/sa/subscriptions', 'permission' => 'sa.subscription.view', 'order' => 17],
['label_ar' => 'الحضور', 'label_en' => 'Attendance', 'route' => '/sa/attendance', 'permission' => 'sa.attendance.view', 'order' => 18],
['label_ar' => 'قائمة الانتظار', 'label_en' => 'Waitlist', 'route' => '/sa/waitlist', 'permission' => 'sa.waitlist.view', 'order' => 19],
......
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE `sa_academy_pricing` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`academy_code` VARCHAR(50) NOT NULL,
`academy_name_ar` VARCHAR(300) NOT NULL,
`discipline_code` VARCHAR(30) NOT NULL,
`category` VARCHAR(50) NOT NULL COMMENT 'subscription, team, session_card, private_training, lane_rental',
`level_name_ar` VARCHAR(200) NULL COMMENT 'Group name/level/age group',
`level_name_en` VARCHAR(200) NULL,
`age_group` VARCHAR(50) NULL COMMENT 'under_13, adult, under_6, under_7, etc.',
`birth_year` INT NULL COMMENT 'Specific birth year for age-based pricing',
`age_from` INT UNSIGNED NULL,
`age_to` INT UNSIGNED NULL,
`gender` VARCHAR(10) NULL COMMENT 'male, female, NULL=mixed',
`group_size_max` INT UNSIGNED NULL COMMENT 'Max swimmers in group (for swimming)',
`sessions_per_week` INT UNSIGNED NULL,
`sessions_per_month` INT UNSIGNED NULL,
`session_duration_minutes` INT UNSIGNED NULL,
`price_member` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
`price_nonmember` DECIMAL(10,2) NOT NULL DEFAULT 0.00,
`price_subscription` DECIMAL(10,2) NULL COMMENT 'Base subscription price before member/nonmember',
`billing_period` VARCHAR(20) NOT NULL DEFAULT 'monthly' COMMENT 'monthly, quarterly, per_session, per_card, yearly',
`includes_fitness` TINYINT(1) NOT NULL DEFAULT 0,
`num_players_max` INT UNSIGNED NULL COMMENT 'For private training - max players per session',
`coach_level` VARCHAR(50) NULL COMMENT 'For private training - director/coach/fitness',
`lane_type` VARCHAR(30) NULL COMMENT '50m, 25m, mix',
`total_sessions` INT UNSIGNED NULL COMMENT 'For session cards - total sessions in card',
`season` VARCHAR(20) NOT NULL DEFAULT '2025_2026',
`notes_ar` TEXT NULL,
`sort_order` INT UNSIGNED NOT NULL DEFAULT 0,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`effective_from` DATE NOT NULL DEFAULT '2025-09-01',
`effective_to` DATE NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_sap_academy` (`academy_code`),
INDEX `idx_sap_discipline` (`discipline_code`),
INDEX `idx_sap_category` (`category`),
INDEX `idx_sap_age` (`age_from`, `age_to`),
INDEX `idx_sap_active` (`is_active`, `effective_from`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `sa_rental_entity_pricing` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`facility_type` VARCHAR(50) NOT NULL COMMENT 'football_full, football_five, tennis, padel, squash, multi_court, bowling, table_tennis, playstation, billiards, track, combat_hall',
`facility_name_ar` VARCHAR(200) NOT NULL,
`entity_type` VARCHAR(50) NOT NULL COMMENT 'member, non_member, non_member_private_coach, schools_gov, clubs_federations, other_entities, foreign_entities',
`usage_type` VARCHAR(50) NOT NULL DEFAULT 'practice' COMMENT 'practice, match, activity',
`time_period` VARCHAR(20) NOT NULL COMMENT 'morning, evening',
`duration_minutes` INT UNSIGNED NOT NULL DEFAULT 60,
`price_amount` DECIMAL(10,2) NOT NULL,
`max_players` INT UNSIGNED NULL,
`notes_ar` VARCHAR(500) NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`effective_from` DATE NOT NULL DEFAULT '2025-09-01',
`effective_to` DATE NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_srep_facility` (`facility_type`),
INDEX `idx_srep_entity` (`entity_type`),
INDEX `idx_srep_usage` (`usage_type`),
INDEX `idx_srep_active` (`is_active`, `effective_from`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `sa_discount_rules` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`rule_code` VARCHAR(50) NOT NULL UNIQUE,
`name_ar` VARCHAR(200) NOT NULL,
`discount_type` VARCHAR(20) NOT NULL COMMENT 'percentage, fixed',
`discount_value` DECIMAL(10,2) NOT NULL,
`applies_to` VARCHAR(50) NOT NULL COMMENT 'academy, facility, all',
`academy_code` VARCHAR(50) NULL COMMENT 'NULL = all academies',
`condition_type` VARCHAR(50) NOT NULL COMMENT 'advance_payment, sibling, nonmember_surcharge, free_period',
`condition_value` VARCHAR(200) NULL COMMENT 'JSON or simple value for condition params',
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_sdr_applies` (`applies_to`, `academy_code`),
INDEX `idx_sdr_condition` (`condition_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
",
'down' => "
DROP TABLE IF EXISTS `sa_discount_rules`;
DROP TABLE IF EXISTS `sa_rental_entity_pricing`;
DROP TABLE IF EXISTS `sa_academy_pricing`;
"
];
This diff is collapsed.
<?php
declare(strict_types=1);
use App\Core\Database;
/**
* إضافة التخصصات الرياضية الجديدة من قائمة الأسعار 2025/2026
* هذه التخصصات مطلوبة لربط الأسعار بالأكاديميات
*/
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
$disciplines = [
['code' => 'BASKETBALL', 'name_ar' => 'كرة السلة', 'name_en' => 'Basketball', 'category' => 'team', 'icon' => 'circle-dot', 'desc' => 'كرة السلة — أكاديمي وفرق', 'sort' => 11],
['code' => 'VOLLEYBALL', 'name_ar' => 'الكرة الطائرة', 'name_en' => 'Volleyball', 'category' => 'team', 'icon' => 'circle', 'desc' => 'الكرة الطائرة — أكاديمي وفرق', 'sort' => 12],
['code' => 'HANDBALL', 'name_ar' => 'كرة اليد', 'name_en' => 'Handball', 'category' => 'team', 'icon' => 'hand', 'desc' => 'كرة اليد — أكاديمي وفرق', 'sort' => 13],
['code' => 'CHESS', 'name_ar' => 'الشطرنج', 'name_en' => 'Chess', 'category' => 'individual', 'icon' => 'crown', 'desc' => 'الشطرنج — عدلى أكاديمى', 'sort' => 14],
['code' => 'MUSIC', 'name_ar' => 'الموسيقى', 'name_en' => 'Music', 'category' => 'individual', 'icon' => 'music', 'desc' => 'أكاديمية الآلات الموسيقية', 'sort' => 15],
['code' => 'SKATING', 'name_ar' => 'الباتيناج', 'name_en' => 'Skating', 'category' => 'individual', 'icon' => 'zap', 'desc' => 'الباتيناج والتزحلق', 'sort' => 16],
['code' => 'DRAWING', 'name_ar' => 'الرسم', 'name_en' => 'Drawing', 'category' => 'individual', 'icon' => 'pen-tool', 'desc' => 'أكاديمية الرسم والفنون', 'sort' => 17],
['code' => 'KICKBOXING', 'name_ar' => 'الكيك بوكس', 'name_en' => 'Kickboxing', 'category' => 'combat', 'icon' => 'shield-alert', 'desc' => 'الكيك بوكس — فنون قتالية', 'sort' => 18],
];
foreach ($disciplines as $d) {
$existing = $db->selectOne("SELECT id FROM sa_disciplines WHERE code = ?", [$d['code']]);
if (!$existing) {
$db->insert('sa_disciplines', [
'code' => $d['code'],
'name_ar' => $d['name_ar'],
'name_en' => $d['name_en'],
'category' => $d['category'],
'icon' => $d['icon'],
'description_ar' => $d['desc'],
'config_json' => json_encode([
'skill_levels' => [
['code' => 'beginner', 'label_ar' => 'مبتدئ'],
['code' => 'intermediate', 'label_ar' => 'متوسط'],
['code' => 'advanced', 'label_ar' => 'متقدم'],
],
], JSON_UNESCAPED_UNICODE),
'sort_order' => $d['sort'],
'is_active' => 1,
'is_archived' => 0,
'created_at' => $ts,
'updated_at' => $ts,
]);
}
}
};
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