Commit 3dc95862 authored by Mahmoud Aglan's avatar Mahmoud Aglan

test

parent e93afcbe
...@@ -180,12 +180,14 @@ class RegistrationWizardController extends Controller ...@@ -180,12 +180,14 @@ class RegistrationWizardController extends Controller
{ {
$registrationId = (int) $id; $registrationId = (int) $id;
$groupId = (int) $request->post('group_id', 0); $groupId = (int) $request->post('group_id', 0);
$months = max(1, (int) $request->post('months', 1));
$hasSibling = (bool) $request->post('has_sibling', false);
if ($groupId <= 0) { if ($groupId <= 0) {
return $this->json(['success' => false, 'error' => 'اختر مجموعة']); return $this->json(['success' => false, 'error' => 'اختر مجموعة']);
} }
$result = RegistrationWizardService::selectGroup($registrationId, $groupId); $result = RegistrationWizardService::selectGroup($registrationId, $groupId, $months, $hasSibling);
return $this->json($result); return $this->json($result);
} }
......
...@@ -167,7 +167,7 @@ final class RegistrationWizardService ...@@ -167,7 +167,7 @@ final class RegistrationWizardService
]; ];
} }
public static function selectGroup(int $registrationId, int $groupId): array public static function selectGroup(int $registrationId, int $groupId, int $months = 1, bool $hasSibling = false): array
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
...@@ -192,18 +192,64 @@ final class RegistrationWizardService ...@@ -192,18 +192,64 @@ final class RegistrationWizardService
} }
$playerType = $registration['player_type']; $playerType = $registration['player_type'];
$subscriptionAmount = $playerType === 'member' $monthlyFee = $playerType === 'member'
? (float) $group['monthly_fee_member'] ? (float) $group['monthly_fee_member']
: (float) $group['monthly_fee_nonmember']; : (float) $group['monthly_fee_nonmember'];
$subscriptionAmount = $monthlyFee * max(1, $months);
$discounts = [];
$totalDiscount = 0.0;
// 15% discount for 3-month advance payment
if ($months >= 3) {
$advanceRule = $db->selectOne(
"SELECT * FROM sa_discount_rules WHERE rule_code = 'ADVANCE_3MONTHS_15PCT' AND is_active = 1",
[]
);
if ($advanceRule) {
$discountAmt = $subscriptionAmount * ((float) $advanceRule['discount_value'] / 100);
$totalDiscount += $discountAmt;
$discounts[] = ['code' => 'ADVANCE_3MONTHS_15PCT', 'name_ar' => $advanceRule['name_ar'], 'amount' => round($discountAmt, 2)];
}
}
// Sibling discount (if applicable to this academy)
if ($hasSibling) {
$siblingRule = $db->selectOne(
"SELECT * FROM sa_discount_rules WHERE condition_type = 'sibling' AND is_active = 1",
[]
);
if ($siblingRule) {
$acadCode = $siblingRule['academy_code'];
$applyDiscount = true;
if ($acadCode) {
$program = $db->selectOne("SELECT p.academy_id FROM sa_programs p WHERE p.id = ?", [(int) $group['program_id']]);
if ($program) {
$academy = $db->selectOne("SELECT code FROM sa_academies WHERE id = ?", [(int) $program['academy_id']]);
if (!$academy || $academy['code'] !== $acadCode) {
$applyDiscount = false;
}
}
}
if ($applyDiscount) {
$remaining = $subscriptionAmount - $totalDiscount;
$discountAmt = $remaining * ((float) $siblingRule['discount_value'] / 100);
$totalDiscount += $discountAmt;
$discounts[] = ['code' => $siblingRule['rule_code'], 'name_ar' => $siblingRule['name_ar'], 'amount' => round($discountAmt, 2)];
}
}
}
$subscriptionAfterDiscount = round($subscriptionAmount - $totalDiscount, 2);
$totalFees = (float) $registration['registration_fee'] $totalFees = (float) $registration['registration_fee']
+ (float) $registration['card_fee'] + (float) $registration['card_fee']
+ (float) $registration['form_fee'] + (float) $registration['form_fee']
+ $subscriptionAmount; + $subscriptionAfterDiscount;
$db->update('sa_registrations', [ $db->update('sa_registrations', [
'group_id' => $groupId, 'group_id' => $groupId,
'subscription_amount' => $subscriptionAmount, 'subscription_amount' => $subscriptionAfterDiscount,
'total_fees' => $totalFees, 'total_fees' => $totalFees,
'updated_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$registrationId]); ], 'id = ?', [$registrationId]);
...@@ -211,7 +257,12 @@ final class RegistrationWizardService ...@@ -211,7 +257,12 @@ final class RegistrationWizardService
return [ return [
'success' => true, 'success' => true,
'group_name' => $group['name_ar'], 'group_name' => $group['name_ar'],
'subscription_amount' => $subscriptionAmount, 'monthly_fee' => $monthlyFee,
'months' => $months,
'subscription_before_discount' => $subscriptionAmount,
'discounts' => $discounts,
'total_discount' => round($totalDiscount, 2),
'subscription_amount' => $subscriptionAfterDiscount,
'total_fees' => $totalFees, 'total_fees' => $totalFees,
]; ];
} }
......
...@@ -150,9 +150,9 @@ ...@@ -150,9 +150,9 @@
</table> </table>
</div> </div>
<?php if (isset($pagination) && $pagination && $pagination['total_pages'] > 1): ?> <?php if (isset($pagination) && $pagination && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="margin-top:15px;display:flex;justify-content:center;gap:5px;"> <div style="margin-top:15px;display:flex;justify-content:center;gap:5px;">
<?php for ($p = 1; $p <= $pagination['total_pages']; $p++): ?> <?php for ($p = 1; $p <= $pagination['last_page']; $p++): ?>
<a href="?<?= http_build_query(array_merge($filters, ['page' => $p])) ?>" class="btn <?= $p === $pagination['current_page'] ? 'btn-primary' : 'btn-outline' ?>" style="padding:6px 12px;font-size:12px;"><?= $p ?></a> <a href="?<?= http_build_query(array_merge($filters, ['page' => $p])) ?>" class="btn <?= $p === $pagination['current_page'] ? 'btn-primary' : 'btn-outline' ?>" style="padding:6px 12px;font-size:12px;"><?= $p ?></a>
<?php endfor; ?> <?php endfor; ?>
</div> </div>
......
...@@ -115,6 +115,32 @@ ...@@ -115,6 +115,32 @@
</div> </div>
</div> </div>
<!-- Subscription Options -->
<div class="card" style="margin-bottom:20px;" id="subscriptionOptions">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:16px;font-weight:600;"><i data-lucide="settings-2" style="width:16px;height:16px;vertical-align:middle;margin-left:6px;"></i> خيارات الاشتراك</h3>
</div>
<div style="padding:20px;">
<div style="display:flex;gap:15px;flex-wrap:wrap;margin-bottom:15px;">
<label style="display:flex;align-items:center;gap:8px;padding:12px 18px;border:2px solid #2563EB;border-radius:8px;cursor:pointer;background:#EFF6FF;">
<input type="radio" name="subscription_months" value="1" checked style="accent-color:#2563EB;">
<span style="font-weight:600;">شهر واحد</span>
</label>
<label style="display:flex;align-items:center;gap:8px;padding:12px 18px;border:2px solid #E5E7EB;border-radius:8px;cursor:pointer;position:relative;">
<input type="radio" name="subscription_months" value="3" style="accent-color:#059669;">
<span style="font-weight:600;">3 أشهر</span>
<span style="position:absolute;top:-8px;left:10px;background:#059669;color:#fff;font-size:10px;padding:2px 6px;border-radius:4px;font-weight:700;">خصم 15%</span>
</label>
</div>
<div style="margin-bottom:0;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="checkbox" id="hasSibling" style="accent-color:#7C3AED;width:18px;height:18px;">
<span style="font-size:14px;">يوجد أخ/أخت مسجل بنفس النشاط <span style="font-size:11px;color:#7C3AED;font-weight:600;">(خصم أشقاء)</span></span>
</label>
</div>
</div>
</div>
<!-- Fee Breakdown --> <!-- Fee Breakdown -->
<div class="card" style="margin-bottom:20px;" id="feeBreakdown" style="<?= $selectedGroup ? '' : 'display:none;' ?>"> <div class="card" style="margin-bottom:20px;" id="feeBreakdown" style="<?= $selectedGroup ? '' : 'display:none;' ?>">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;"> <div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
...@@ -126,8 +152,10 @@ ...@@ -126,8 +152,10 @@
<tr><td style="padding:6px 0;">رسوم الكارت</td><td style="text-align:left;font-weight:600;" id="feeCard"><?= money((float) $registration['card_fee']) ?></td></tr> <tr><td style="padding:6px 0;">رسوم الكارت</td><td style="text-align:left;font-weight:600;" id="feeCard"><?= money((float) $registration['card_fee']) ?></td></tr>
<tr><td style="padding:6px 0;">رسوم الاستمارة</td><td style="text-align:left;font-weight:600;" id="feeForm"><?= money((float) $registration['form_fee']) ?></td></tr> <tr><td style="padding:6px 0;">رسوم الاستمارة</td><td style="text-align:left;font-weight:600;" id="feeForm"><?= money((float) $registration['form_fee']) ?></td></tr>
<tr><td style="padding:6px 0;">اشتراك النشاط</td><td style="text-align:left;font-weight:600;" id="feeSub"><?= money((float) ($registration['subscription_amount'] ?? 0)) ?></td></tr> <tr><td style="padding:6px 0;">اشتراك النشاط</td><td style="text-align:left;font-weight:600;" id="feeSub"><?= money((float) ($registration['subscription_amount'] ?? 0)) ?></td></tr>
<tr id="feeDiscountRow" style="display:none;color:#059669;"><td style="padding:6px 0;">خصم</td><td style="text-align:left;font-weight:600;" id="feeDiscount"></td></tr>
<tr style="border-top:2px solid #E5E7EB;"><td style="padding:10px 0;font-weight:700;font-size:15px;">الإجمالي</td><td style="text-align:left;font-weight:800;font-size:15px;color:#2563EB;" id="feeTotal"><?= money((float) $registration['total_fees']) ?></td></tr> <tr style="border-top:2px solid #E5E7EB;"><td style="padding:10px 0;font-weight:700;font-size:15px;">الإجمالي</td><td style="text-align:left;font-weight:800;font-size:15px;color:#2563EB;" id="feeTotal"><?= money((float) $registration['total_fees']) ?></td></tr>
</table> </table>
<div id="discountNotes" style="display:none;margin-top:10px;padding:8px 12px;background:#ECFDF5;border-radius:6px;font-size:12px;color:#059669;"></div>
</div> </div>
</div> </div>
...@@ -429,26 +457,70 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -429,26 +457,70 @@ document.addEventListener('DOMContentLoaded', function() {
} }
// Activity selection // Activity selection
var selectedGroupId = null;
function getSubscriptionMonths() {
var checked = document.querySelector('input[name="subscription_months"]:checked');
return checked ? parseInt(checked.value) : 1;
}
function submitActivitySelection(gid) {
var months = getSubscriptionMonths();
var hasSibling = document.getElementById('hasSibling').checked ? 1 : 0;
fetch('/sa/registration/' + regId + '/activity', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest','X-CSRF-TOKEN': csrfToken},
body: JSON.stringify({group_id: gid, months: months, has_sibling: hasSibling, _csrf_token: csrfToken})
}).then(function(r){return r.json();}).then(function(data) {
if (data.success) {
document.getElementById('feeSub').textContent = data.subscription_amount.toLocaleString() + ' ج.م';
document.getElementById('feeTotal').textContent = data.total_fees.toLocaleString() + ' ج.م';
document.getElementById('feeBreakdown').style.display = '';
document.getElementById('btnActivityNext').disabled = false;
var discountRow = document.getElementById('feeDiscountRow');
var discountNotes = document.getElementById('discountNotes');
if (data.total_discount && data.total_discount > 0) {
discountRow.style.display = '';
document.getElementById('feeDiscount').textContent = '- ' + data.total_discount.toLocaleString() + ' ج.م';
var notes = '';
if (data.discounts && data.discounts.length > 0) {
data.discounts.forEach(function(d) { notes += '✓ ' + (d.name_ar || d.code) + ' (' + d.amount.toLocaleString() + ' ج.م)<br>'; });
}
if (notes) { discountNotes.innerHTML = notes; discountNotes.style.display = ''; }
} else {
discountRow.style.display = 'none';
discountNotes.style.display = 'none';
}
}
});
}
document.querySelectorAll('.group-option').forEach(function(el) { document.querySelectorAll('.group-option').forEach(function(el) {
el.addEventListener('click', function() { el.addEventListener('click', function() {
document.querySelectorAll('.group-option').forEach(function(g) { g.style.borderColor = '#E5E7EB'; }); document.querySelectorAll('.group-option').forEach(function(g) { g.style.borderColor = '#E5E7EB'; });
this.style.borderColor = '#2563EB'; this.style.borderColor = '#2563EB';
var gid = this.dataset.groupId; selectedGroupId = this.dataset.groupId;
submitActivitySelection(selectedGroupId);
});
});
fetch('/sa/registration/' + regId + '/activity', { // Re-calculate on months/sibling change
method: 'POST', document.querySelectorAll('input[name="subscription_months"]').forEach(function(radio) {
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest','X-CSRF-TOKEN': csrfToken}, radio.addEventListener('change', function() {
body: JSON.stringify({group_id: gid, _csrf_token: csrfToken}) document.querySelectorAll('input[name="subscription_months"]').forEach(function(r) {
}).then(function(r){return r.json();}).then(function(data) { r.closest('label').style.borderColor = '#E5E7EB';
if (data.success) { r.closest('label').style.background = '';
document.getElementById('feeSub').textContent = data.subscription_amount.toLocaleString() + ' ج.م';
document.getElementById('feeTotal').textContent = data.total_fees.toLocaleString() + ' ج.م';
document.getElementById('feeBreakdown').style.display = '';
document.getElementById('btnActivityNext').disabled = false;
}
}); });
this.closest('label').style.borderColor = this.value === '3' ? '#059669' : '#2563EB';
this.closest('label').style.background = this.value === '3' ? '#F0FDF4' : '#EFF6FF';
if (selectedGroupId) submitActivitySelection(selectedGroupId);
}); });
}); });
document.getElementById('hasSibling').addEventListener('change', function() {
if (selectedGroupId) submitActivitySelection(selectedGroupId);
});
// Group search // Group search
var groupSearch = document.getElementById('groupSearch'); var groupSearch = document.getElementById('groupSearch');
......
...@@ -1258,7 +1258,7 @@ final class TutorialRegistry ...@@ -1258,7 +1258,7 @@ final class TutorialRegistry
['title' => 'فتح مكتب التسجيل', 'body' => 'من القائمة: <span class="field">النشاط الرياضي</span> > <span class="field">مكتب التسجيل</span>.'], ['title' => 'فتح مكتب التسجيل', 'body' => 'من القائمة: <span class="field">النشاط الرياضي</span> > <span class="field">مكتب التسجيل</span>.'],
['title' => 'إدخال الرقم القومي', 'body' => 'أدخل الرقم القومي (14 رقم) واضغط <span class="field">استعلام</span>. النظام يستخرج تلقائياً: تاريخ الميلاد، العمر، النوع، المحافظة.<span class="info">إذا كان اللاعب مسجلاً مسبقاً — يتم استرجاع بياناته مباشرة.</span>'], ['title' => 'إدخال الرقم القومي', 'body' => 'أدخل الرقم القومي (14 رقم) واضغط <span class="field">استعلام</span>. النظام يستخرج تلقائياً: تاريخ الميلاد، العمر، النوع، المحافظة.<span class="info">إذا كان اللاعب مسجلاً مسبقاً — يتم استرجاع بياناته مباشرة.</span>'],
['title' => 'التقاط الصورة', 'body' => 'التقط صورة عبر <span class="field">كاميرا الويب</span> أو ارفع ملف صورة. الصورة إلزامية لإتمام التسجيل.<span class="warn">الصورة تظهر على الاستمارة والكارت — تأكد من جودتها.</span>'], ['title' => 'التقاط الصورة', 'body' => 'التقط صورة عبر <span class="field">كاميرا الويب</span> أو ارفع ملف صورة. الصورة إلزامية لإتمام التسجيل.<span class="warn">الصورة تظهر على الاستمارة والكارت — تأكد من جودتها.</span>'],
['title' => 'اختيار النشاط والمجموعة', 'body' => 'اختر المجموعة المناسبة (حسب السن والنشاط). يظهر تلقائياً:<ul><li>رسوم التسجيل (100 ج عضو / 50 ج غير عضو)</li><li>رسوم الكارت (25 ج)</li><li>رسوم الاستمارة (10 ج)</li><li>اشتراك المجموعة الشهري</li></ul>'], ['title' => 'اختيار النشاط والمجموعة', 'body' => 'اختر المجموعة المناسبة (حسب السن والنشاط). يمكنك أيضاً:<ul><li>اختيار <span class="field">3 أشهر</span> للحصول على خصم 15%</li><li>تفعيل <span class="field">خصم الأشقاء</span> إن وُجد أخ/أخت بنفس النشاط</li></ul>يظهر تلقائياً: رسوم التسجيل + الكارت + الاستمارة + الاشتراك مع أي خصومات مطبقة.'],
['title' => 'إرسال للخزينة', 'body' => 'اضغط <span class="field">إرسال للخزينة</span>. يتم إنشاء طلب دفع شامل يظهر في طابور التحصيل.<span class="success">بعد السداد يتم تفعيل التسجيل وإصدار الكارت تلقائياً.</span>'], ['title' => 'إرسال للخزينة', 'body' => 'اضغط <span class="field">إرسال للخزينة</span>. يتم إنشاء طلب دفع شامل يظهر في طابور التحصيل.<span class="success">بعد السداد يتم تفعيل التسجيل وإصدار الكارت تلقائياً.</span>'],
['title' => 'الطباعة وإصدار الكارت', 'body' => 'بعد السداد يمكنك: <span class="field">طباعة الاستمارة</span> (A4) و<span class="field">طباعة الكارت</span> (PVC). الكارت يحتوي على QR للدخول من البوابة.'], ['title' => 'الطباعة وإصدار الكارت', 'body' => 'بعد السداد يمكنك: <span class="field">طباعة الاستمارة</span> (A4) و<span class="field">طباعة الكارت</span> (PVC). الكارت يحتوي على QR للدخول من البوابة.'],
], ],
...@@ -1286,6 +1286,13 @@ final class TutorialRegistry ...@@ -1286,6 +1286,13 @@ final class TutorialRegistry
['title' => 'الدخول خلال المدة', 'body' => 'اللاعب يستخدم الكارت المؤقت للدخول من البوابة بشكل طبيعي خلال فترة الصلاحية.'], ['title' => 'الدخول خلال المدة', 'body' => 'اللاعب يستخدم الكارت المؤقت للدخول من البوابة بشكل طبيعي خلال فترة الصلاحية.'],
['title' => 'انتهاء الصلاحية', 'body' => 'بعد 7 أيام يتحول الكارت تلقائياً إلى حالة <span style="color:#6B7280;font-weight:bold;">منتهي</span> ويُرفض عند البوابة.<span class="warn">يجب إصدار كارت دائم قبل انتهاء المؤقت.</span>'], ['title' => 'انتهاء الصلاحية', 'body' => 'بعد 7 أيام يتحول الكارت تلقائياً إلى حالة <span style="color:#6B7280;font-weight:bold;">منتهي</span> ويُرفض عند البوابة.<span class="warn">يجب إصدار كارت دائم قبل انتهاء المؤقت.</span>'],
], ],
'sa-registration.academy-pricing' => [
['title' => 'فتح أسعار الأكاديميات', 'body' => 'من القائمة: <span class="field">النشاط الرياضي</span> > <span class="field">أسعار الأكاديميات</span>. تعرض قائمة كل الأكاديميات مع نطاق الأسعار.'],
['title' => 'عرض تفاصيل أكاديمية', 'body' => 'اضغط <span class="field">عرض</span> بجانب أي أكاديمية. يظهر:<ul><li>أسعار الاشتراكات (عضو/غير عضو)</li><li>أسعار الفرق</li><li>التدريب الخاص</li><li>كروت الحصص</li></ul>'],
['title' => 'كروت حصص السباحة', 'body' => 'في نفس الصفحة يظهر جدول كروت الحصص: الكارت (1-24 حصة) مع السعر الإجمالي وسعر الحصة الواحدة.'],
['title' => 'الخصومات التلقائية', 'body' => 'عند التسجيل من المعالج:<ul><li><span style="color:#059669;font-weight:bold;">15%</span> عند اختيار 3 أشهر مقدماً</li><li><span style="color:#7C3AED;font-weight:bold;">خصم أشقاء</span> عند وجود أخ/أخت بنفس النشاط (حسب اللعبة)</li></ul><span class="info">الخصومات تُحسب وتُعرض تلقائياً في خطوة اختيار النشاط.</span>'],
['title' => 'فترات مجانية للأعضاء', 'body' => 'الأعضاء يحصلون على فترات مجانية حسب الموسم:<ul><li>شتوي: من بداية الموسم حتى نهاية فبراير</li><li>صيفي: من يونيو حتى نهاية سبتمبر</li></ul><span class="info">هذه الفترات تظهر في قواعد الخصم فقط — التطبيق يكون من مكتب التسجيل.</span>'],
],
]; ];
} }
...@@ -3366,6 +3373,14 @@ final class TutorialRegistry ...@@ -3366,6 +3373,14 @@ final class TutorialRegistry
'category' => 'cards', 'category' => 'cards',
'order' => 5, 'order' => 5,
], ],
'academy-pricing' => [
'title' => 'أسعار الأكاديميات',
'subtitle' => 'عرض وحساب أسعار الاشتراكات والخصومات',
'icon' => 'calculator',
'color' => '#D97706',
'category' => 'registration',
'order' => 6,
],
]; ];
} }
......
...@@ -75,7 +75,7 @@ CREATE TABLE `sa_discount_rules` ( ...@@ -75,7 +75,7 @@ CREATE TABLE `sa_discount_rules` (
`applies_to` VARCHAR(50) NOT NULL COMMENT 'academy, facility, all', `applies_to` VARCHAR(50) NOT NULL COMMENT 'academy, facility, all',
`academy_code` VARCHAR(50) NULL COMMENT 'NULL = all academies', `academy_code` VARCHAR(50) NULL COMMENT 'NULL = all academies',
`condition_type` VARCHAR(50) NOT NULL COMMENT 'advance_payment, sibling, nonmember_surcharge, free_period', `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', `condition_value` TEXT NULL COMMENT 'JSON or simple value for condition params',
`is_active` TINYINT(1) NOT NULL DEFAULT 1, `is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
......
<?php
declare(strict_types=1);
return [
'up' => "ALTER TABLE `sa_discount_rules` MODIFY COLUMN `condition_value` TEXT NULL COMMENT 'JSON or simple value for condition params';",
'down' => "ALTER TABLE `sa_discount_rules` MODIFY COLUMN `condition_value` VARCHAR(200) NULL COMMENT 'JSON or simple value for condition params';"
];
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