Commit 615c9041 authored by Mahmoud Aglan's avatar Mahmoud Aglan

lockers Update

parent 54e15d8a
...@@ -394,6 +394,8 @@ class MemberController extends Controller ...@@ -394,6 +394,8 @@ class MemberController extends Controller
$formFeePaid = $db->selectOne("SELECT id FROM payments WHERE member_id = ? AND payment_type = 'form_fee' AND is_voided = 0 LIMIT 1", [(int) $id]); $formFeePaid = $db->selectOne("SELECT id FROM payments WHERE member_id = ? AND payment_type = 'form_fee' AND is_voided = 0 LIMIT 1", [(int) $id]);
if (!$formFeePaid) return $this->redirect('/members/' . $id)->withError('⚠ يجب دفع رسوم الاستمارة أولاً'); if (!$formFeePaid) return $this->redirect('/members/' . $id)->withError('⚠ يجب دفع رسوم الاستمارة أولاً');
$salesReps = $db->select("SELECT id, code, name_ar FROM sales_representatives WHERE is_active = 1 ORDER BY name_ar", []);
return $this->view('Members.Views.fill-form', [ return $this->view('Members.Views.fill-form', [
'member' => $member, 'member' => $member,
'branches' => $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar"), 'branches' => $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar"),
...@@ -401,6 +403,7 @@ class MemberController extends Controller ...@@ -401,6 +403,7 @@ class MemberController extends Controller
'governorates' => $db->select("SELECT code, name_ar FROM governorates WHERE is_active = 1 ORDER BY name_ar"), 'governorates' => $db->select("SELECT code, name_ar FROM governorates WHERE is_active = 1 ORDER BY name_ar"),
'countries' => $db->select("SELECT nationality_ar FROM countries WHERE is_active = 1 ORDER BY name_ar"), 'countries' => $db->select("SELECT nationality_ar FROM countries WHERE is_active = 1 ORDER BY name_ar"),
'specialDiscounts' => SpecialDiscount::allActive(), 'specialDiscounts' => SpecialDiscount::allActive(),
'salesReps' => $salesReps,
'schemaHtml' => '', 'schemaHtml' => '',
]); ]);
} }
...@@ -417,10 +420,13 @@ class MemberController extends Controller ...@@ -417,10 +420,13 @@ class MemberController extends Controller
if (!$formFeePaid) return $this->redirect('/members/' . $id)->withError('⚠ يجب دفع رسوم الاستمارة أولاً'); if (!$formFeePaid) return $this->redirect('/members/' . $id)->withError('⚠ يجب دفع رسوم الاستمارة أولاً');
$data = $request->all(); unset($data['_csrf_token']); $data = $request->all(); unset($data['_csrf_token']);
$fields = ['full_name_en','place_of_birth','nationality','religion','qualification_id','marital_status','passport_number','id_issue_date','id_expiry_date','phone_home','phone_international','email','emergency_name','emergency_phone','residence_type','residence_address','landmark','floor','apartment','area','governorate','correspondence_address','employment_type','occupation','job_title','employment_date','business_address','office_phone','office_fax','business_activity','referral_source']; $fields = ['full_name_en','place_of_birth','nationality','religion','qualification_id','marital_status','passport_number','id_issue_date','id_expiry_date','phone_home','phone_international','email','emergency_name','emergency_phone','residence_type','residence_address','landmark','floor','apartment','area','governorate','correspondence_address','employment_type','occupation','job_title','employment_date','business_address','office_phone','office_fax','business_activity','referral_source','referred_by_representative_id'];
$update = []; $update = [];
foreach ($fields as $f) { if (array_key_exists($f, $data)) { $v = trim((string) ($data[$f] ?? '')); $update[$f] = $v === '' ? null : $v; } } foreach ($fields as $f) { if (array_key_exists($f, $data)) { $v = trim((string) ($data[$f] ?? '')); $update[$f] = $v === '' ? null : $v; } }
if (isset($update['qualification_id'])) $update['qualification_id'] = $update['qualification_id'] ? (int) $update['qualification_id'] : null; if (isset($update['qualification_id'])) $update['qualification_id'] = $update['qualification_id'] ? (int) $update['qualification_id'] : null;
if (isset($update['referred_by_representative_id'])) {
$update['referred_by_representative_id'] = $update['referred_by_representative_id'] !== null && $update['referred_by_representative_id'] !== '' ? (int) $update['referred_by_representative_id'] : null;
}
if (!empty($update['qualification_id'])) { if (!empty($update['qualification_id'])) {
$pricing = $db->selectOne("SELECT price FROM pricing_configs WHERE branch_id = ? AND qualification_id = ? AND membership_type = 'working' AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE()) ORDER BY effective_from DESC LIMIT 1", [(int) $member->branch_id, (int) $update['qualification_id']]); $pricing = $db->selectOne("SELECT price FROM pricing_configs WHERE branch_id = ? AND qualification_id = ? AND membership_type = 'working' AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE()) ORDER BY effective_from DESC LIMIT 1", [(int) $member->branch_id, (int) $update['qualification_id']]);
...@@ -474,6 +480,8 @@ class MemberController extends Controller ...@@ -474,6 +480,8 @@ class MemberController extends Controller
return $this->redirect('/members/' . $id)->withError('لا يمكن تعديل بيانات العضو بعد تفعيل العضوية — تواصل مع المشرف العام'); return $this->redirect('/members/' . $id)->withError('لا يمكن تعديل بيانات العضو بعد تفعيل العضوية — تواصل مع المشرف العام');
} }
$salesReps = $db->select("SELECT id, code, name_ar FROM sales_representatives WHERE is_active = 1 ORDER BY name_ar", []);
return $this->view('Members.Views.edit', [ return $this->view('Members.Views.edit', [
'member' => $member, 'member' => $member,
'branches' => $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1"), 'branches' => $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1"),
...@@ -482,6 +490,7 @@ class MemberController extends Controller ...@@ -482,6 +490,7 @@ class MemberController extends Controller
'countries' => $db->select("SELECT nationality_ar FROM countries WHERE is_active = 1 ORDER BY name_ar"), 'countries' => $db->select("SELECT nationality_ar FROM countries WHERE is_active = 1 ORDER BY name_ar"),
'isSuperAdmin' => self::isSuperAdmin(), 'isSuperAdmin' => self::isSuperAdmin(),
'specialDiscounts' => SpecialDiscount::allActive(), 'specialDiscounts' => SpecialDiscount::allActive(),
'salesReps' => $salesReps,
]); ]);
} }
...@@ -500,7 +509,7 @@ class MemberController extends Controller ...@@ -500,7 +509,7 @@ class MemberController extends Controller
$data = $request->all(); $data = $request->all();
unset($data['_csrf_token']); unset($data['_csrf_token']);
$allowed = ['full_name_en','phone_home','phone_mobile','phone_international','email','emergency_name','emergency_phone','residence_type','residence_address','landmark','floor','apartment','area','governorate','correspondence_address','employment_type','occupation','job_title','employment_date','business_address','office_phone','office_fax','business_activity','referral_source','religion','marital_status']; $allowed = ['full_name_en','phone_home','phone_mobile','phone_international','email','emergency_name','emergency_phone','residence_type','residence_address','landmark','floor','apartment','area','governorate','correspondence_address','employment_type','occupation','job_title','employment_date','business_address','office_phone','office_fax','business_activity','referral_source','referred_by_representative_id','religion','marital_status'];
$update = []; $update = [];
foreach ($allowed as $f) { foreach ($allowed as $f) {
...@@ -510,6 +519,10 @@ class MemberController extends Controller ...@@ -510,6 +519,10 @@ class MemberController extends Controller
} }
} }
if (isset($update['referred_by_representative_id'])) {
$update['referred_by_representative_id'] = $update['referred_by_representative_id'] !== null && $update['referred_by_representative_id'] !== '' ? (int) $update['referred_by_representative_id'] : null;
}
// ── SuperAdmin-only fields: full_name_ar, national_id ── // ── SuperAdmin-only fields: full_name_ar, national_id ──
if ($isSuperAdmin) { if ($isSuperAdmin) {
$errors = []; $errors = [];
......
...@@ -29,7 +29,7 @@ class Member extends Model ...@@ -29,7 +29,7 @@ class Member extends Model
'employment_type', 'occupation', 'job_title', 'employment_date', 'employment_type', 'occupation', 'job_title', 'employment_date',
'business_address', 'office_phone', 'office_fax', 'business_activity', 'business_address', 'office_phone', 'office_fax', 'business_activity',
'membership_value', 'special_discount_id', 'special_discount_document', 'discount_amount', 'membership_value', 'special_discount_id', 'special_discount_document', 'discount_amount',
'payment_method', 'referral_source', 'photo_path', 'payment_method', 'referral_source', 'referred_by_representative_id', 'photo_path',
'workflow_instance_id', 'workflow_instance_id',
]; ];
......
...@@ -63,6 +63,36 @@ ...@@ -63,6 +63,36 @@
</div> </div>
</div> </div>
<!-- Referral Source & Sales Rep -->
<div class="card" style="margin-bottom:20px;padding:20px;">
<h3 style="color:#0D7377;margin-bottom:15px;">كيف تعرفت على النادي</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">مصدر المعرفة</label>
<select name="referral_source" class="form-select">
<option value="">-- اختر --</option>
<option value="social_media" <?= ($member->referral_source ?? '') === 'social_media' ? 'selected' : '' ?>>مواقع التواصل الاجتماعي</option>
<option value="tv" <?= ($member->referral_source ?? '') === 'tv' ? 'selected' : '' ?>>إعلان التليفزيون</option>
<option value="friend" <?= ($member->referral_source ?? '') === 'friend' ? 'selected' : '' ?>>من خلال صديق</option>
<option value="radio" <?= ($member->referral_source ?? '') === 'radio' ? 'selected' : '' ?>>إعلان الراديو</option>
<option value="outdoor" <?= ($member->referral_source ?? '') === 'outdoor' ? 'selected' : '' ?>>إعلانات الطريق</option>
<option value="member" <?= ($member->referral_source ?? '') === 'member' ? 'selected' : '' ?>>عضو بالنادي</option>
<option value="other" <?= ($member->referral_source ?? '') === 'other' ? 'selected' : '' ?>>أخرى</option>
</select>
</div>
<div class="form-group">
<label class="form-label">موظف المبيعات المُحيل</label>
<select name="referred_by_representative_id" class="form-select">
<option value="">-- لا يوجد --</option>
<?php foreach ($salesReps as $rep): ?>
<option value="<?= (int) $rep['id'] ?>" <?= (int) ($member->referred_by_representative_id ?? 0) === (int) $rep['id'] ? 'selected' : '' ?>><?= e($rep['name_ar']) ?> (<?= e($rep['code']) ?>)</option>
<?php endforeach; ?>
</select>
<small style="color:#6B7280;display:block;margin-top:4px;">اختر موظف المبيعات إذا كان العضو جاي عن طريقه</small>
</div>
</div>
</div>
<!-- Special Discount --> <!-- Special Discount -->
<?php if (!empty($specialDiscounts)): ?> <?php if (!empty($specialDiscounts)): ?>
<div class="card" style="margin-bottom:20px;padding:20px;border-right:4px solid #D97706;"> <div class="card" style="margin-bottom:20px;padding:20px;border-right:4px solid #D97706;">
......
...@@ -281,6 +281,16 @@ ...@@ -281,6 +281,16 @@
<option value="other" <?= ($member->referral_source ?? '') === 'other' ? 'selected' : '' ?>>أخرى / Other</option> <option value="other" <?= ($member->referral_source ?? '') === 'other' ? 'selected' : '' ?>>أخرى / Other</option>
</select> </select>
</div> </div>
<div class="form-group">
<label class="form-label">موظف المبيعات المُحيل</label>
<select name="referred_by_representative_id" class="form-select">
<option value="">-- لا يوجد --</option>
<?php foreach ($salesReps as $rep): ?>
<option value="<?= (int) $rep['id'] ?>" <?= (int) ($member->referred_by_representative_id ?? 0) === (int) $rep['id'] ? 'selected' : '' ?>><?= e($rep['name_ar']) ?> (<?= e($rep['code']) ?>)</option>
<?php endforeach; ?>
</select>
<small style="color:#6B7280;display:block;margin-top:4px;">اختر موظف المبيعات إذا كان العضو جاي عن طريقه</small>
</div>
</div> </div>
</div> </div>
......
<?php
declare(strict_types=1);
namespace App\Modules\Sales\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\Pagination;
class ReferralController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('sales.referral.view');
$db = App::getInstance()->db();
$repFilter = trim((string) $request->get('rep_id', ''));
$dateFrom = trim((string) $request->get('date_from', ''));
$dateTo = trim((string) $request->get('date_to', ''));
$statusFilter = trim((string) $request->get('status', ''));
$page = max(1, (int) $request->get('page', 1));
$perPage = 25;
$where = '1=1';
$params = [];
if ($repFilter !== '') {
$where .= ' AND mrc.representative_id = ?';
$params[] = (int) $repFilter;
}
if ($dateFrom !== '') {
$where .= ' AND mrc.commission_date >= ?';
$params[] = $dateFrom;
}
if ($dateTo !== '') {
$where .= ' AND mrc.commission_date <= ?';
$params[] = $dateTo;
}
if ($statusFilter !== '') {
$where .= ' AND mrc.status = ?';
$params[] = $statusFilter;
}
$total = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM member_referral_commissions mrc WHERE {$where}",
$params
)['cnt'] ?? 0);
$pagination = Pagination::paginate($total, $perPage, $page);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT mrc.*, sr.name_ar as rep_name, sr.code as rep_code, m.full_name_ar as member_name, m.membership_number
FROM member_referral_commissions mrc
LEFT JOIN sales_representatives sr ON sr.id = mrc.representative_id
LEFT JOIN members m ON m.id = mrc.member_id
WHERE {$where}
ORDER BY mrc.commission_date DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
$reps = $db->select("SELECT id, code, name_ar FROM sales_representatives WHERE is_active = 1 ORDER BY name_ar", []);
$totals = $db->selectOne(
"SELECT COUNT(*) as total_count, COALESCE(SUM(commission_amount), 0) as total_amount FROM member_referral_commissions mrc WHERE {$where}",
$params
);
return $this->view('Sales.Views.referrals.index', [
'rows' => $rows,
'pagination' => $pagination,
'reps' => $reps,
'filters' => ['rep_id' => $repFilter, 'date_from' => $dateFrom, 'date_to' => $dateTo, 'status' => $statusFilter],
'totals' => $totals,
]);
}
}
...@@ -34,4 +34,7 @@ return [ ...@@ -34,4 +34,7 @@ return [
['GET', '/sales/reports/daily', 'Sales\Controllers\SaleReportController@daily', ['auth'], 'report.sales'], ['GET', '/sales/reports/daily', 'Sales\Controllers\SaleReportController@daily', ['auth'], 'report.sales'],
['GET', '/sales/reports/monthly', 'Sales\Controllers\SaleReportController@monthly', ['auth'], 'report.sales'], ['GET', '/sales/reports/monthly', 'Sales\Controllers\SaleReportController@monthly', ['auth'], 'report.sales'],
['GET', '/sales/reports/by-item', 'Sales\Controllers\SaleReportController@byItem', ['auth'], 'report.sales'], ['GET', '/sales/reports/by-item', 'Sales\Controllers\SaleReportController@byItem', ['auth'], 'report.sales'],
// Member Referrals
['GET', '/sales/referrals', 'Sales\Controllers\ReferralController@index', ['auth'], 'sales.referral.view'],
]; ];
<?php
declare(strict_types=1);
namespace App\Modules\Sales\Services;
use App\Core\App;
final class ReferralCommissionService
{
public static function onMemberActivated(int $memberId): void
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT id, referred_by_representative_id, membership_value FROM members WHERE id = ?", [$memberId]);
if (!$member || empty($member['referred_by_representative_id'])) {
return;
}
$repId = (int) $member['referred_by_representative_id'];
$membershipValue = (float) ($member['membership_value'] ?? 0);
if ($membershipValue <= 0) {
return;
}
// Don't double-create
$existing = $db->selectOne(
"SELECT 1 FROM member_referral_commissions WHERE member_id = ?",
[$memberId]
);
if ($existing) {
return;
}
$rep = $db->selectOne("SELECT id, commission_rate FROM sales_representatives WHERE id = ? AND is_active = 1", [$repId]);
if (!$rep) {
return;
}
$rate = (float) $rep['commission_rate'];
$commissionAmount = round($membershipValue * $rate / 100, 2);
$db->insert('member_referral_commissions', [
'representative_id' => $repId,
'member_id' => $memberId,
'membership_value' => $membershipValue,
'commission_rate' => $rate,
'commission_amount' => $commissionAmount,
'commission_date' => date('Y-m-d'),
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تقرير إحالات الأعضاء<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/sales/referrals" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div style="min-width:200px;">
<label class="form-label" style="font-size:12px;">المندوب</label>
<select name="rep_id" class="form-select">
<option value="">جميع المندوبين</option>
<?php foreach ($reps as $rep): ?>
<option value="<?= (int) $rep['id'] ?>" <?= ($filters['rep_id'] ?? '') == $rep['id'] ? 'selected' : '' ?>><?= e($rep['name_ar']) ?> (<?= e($rep['code']) ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">من تاريخ</label>
<input type="date" name="date_from" class="form-input" value="<?= e($filters['date_from'] ?? '') ?>">
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">إلى تاريخ</label>
<input type="date" name="date_to" class="form-input" value="<?= e($filters['date_to'] ?? '') ?>">
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select">
<option value="">الكل</option>
<option value="pending" <?= ($filters['status'] ?? '') === 'pending' ? 'selected' : '' ?>>معلقة</option>
<option value="approved" <?= ($filters['status'] ?? '') === 'approved' ? 'selected' : '' ?>>معتمدة</option>
<option value="paid" <?= ($filters['status'] ?? '') === 'paid' ? 'selected' : '' ?>>مدفوعة</option>
</select>
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> عرض</button>
</form>
</div>
<!-- Summary Cards -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:13px;color:#6B7280;margin-bottom:5px;">عدد الإحالات</div>
<div style="font-size:28px;font-weight:700;color:#0D7377;"><?= (int) ($totals['total_count'] ?? 0) ?></div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:13px;color:#6B7280;margin-bottom:5px;">إجمالي العمولات</div>
<div style="font-size:28px;font-weight:700;color:#059669;"><?= money($totals['total_amount'] ?? 0) ?></div>
</div>
</div>
<!-- Data Table -->
<?php if (!empty($rows)): ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="users" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">تفاصيل الإحالات</h3>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>المندوب</th>
<th>العضو</th>
<th>رقم العضوية</th>
<th>قيمة العضوية</th>
<th>نسبة العمولة</th>
<th>مبلغ العمولة</th>
<th>التاريخ</th>
<th>الحالة</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row): ?>
<tr>
<td style="font-weight:600;"><?= e($row['rep_name'] ?? '—') ?> <small style="color:#6B7280;">(<?= e($row['rep_code'] ?? '') ?>)</small></td>
<td><?= e($row['member_name'] ?? '—') ?></td>
<td><code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($row['membership_number'] ?? '—') ?></code></td>
<td style="direction:ltr;text-align:left;"><?= money($row['membership_value'] ?? 0) ?></td>
<td><?= e($row['commission_rate'] ?? '0') ?>%</td>
<td style="direction:ltr;text-align:left;font-weight:600;color:#059669;"><?= money($row['commission_amount'] ?? 0) ?></td>
<td><?= e($row['commission_date'] ?? '—') ?></td>
<td>
<?php
$statusColor = match ($row['status'] ?? '') {
'paid' => '#059669',
'approved' => '#2563EB',
'pending' => '#D97706',
default => '#6B7280',
};
$statusLabel = match ($row['status'] ?? '') {
'paid' => 'مدفوعة',
'approved' => 'معتمدة',
'pending' => 'معلقة',
default => $row['status'] ?? '—',
};
?>
<span style="background:<?= $statusColor ?>15;color:<?= $statusColor ?>;padding:3px 10px;border-radius:4px;font-size:12px;font-weight:600;"><?= $statusLabel ?></span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="margin-top:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="file-search" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد إحالات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0;">لا توجد إحالات أعضاء مسجلة بعد.</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
...@@ -27,6 +27,7 @@ PermissionRegistry::register('sales', [ ...@@ -27,6 +27,7 @@ PermissionRegistry::register('sales', [
'sales.commission.manage' => ['ar' => 'إدارة المندوبين والعمولات','en' => 'Manage Reps & Commissions'], 'sales.commission.manage' => ['ar' => 'إدارة المندوبين والعمولات','en' => 'Manage Reps & Commissions'],
'sales.pricing.view' => ['ar' => 'عرض تسعير العملاء', 'en' => 'View Customer Pricing'], 'sales.pricing.view' => ['ar' => 'عرض تسعير العملاء', 'en' => 'View Customer Pricing'],
'sales.pricing.manage' => ['ar' => 'إدارة تسعير العملاء', 'en' => 'Manage Customer Pricing'], 'sales.pricing.manage' => ['ar' => 'إدارة تسعير العملاء', 'en' => 'Manage Customer Pricing'],
'sales.referral.view' => ['ar' => 'عرض تقرير إحالات الأعضاء', 'en' => 'View Member Referral Report'],
]); ]);
// ──────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────
...@@ -50,5 +51,17 @@ MenuRegistry::register('sales', [ ...@@ -50,5 +51,17 @@ MenuRegistry::register('sales', [
['label_ar' => 'المندوبون', 'label_en' => 'Sales Reps', 'route' => '/sales/commissions/representatives', 'permission' => 'sales.commission.view', 'order' => 6], ['label_ar' => 'المندوبون', 'label_en' => 'Sales Reps', 'route' => '/sales/commissions/representatives', 'permission' => 'sales.commission.view', 'order' => 6],
['label_ar' => 'العمولات', 'label_en' => 'Commissions', 'route' => '/sales/commissions/report', 'permission' => 'sales.commission.view', 'order' => 7], ['label_ar' => 'العمولات', 'label_en' => 'Commissions', 'route' => '/sales/commissions/report', 'permission' => 'sales.commission.view', 'order' => 7],
['label_ar' => 'تسعير العملاء','label_en' => 'Customer Pricing', 'route' => '/sales/pricing/customer', 'permission' => 'sales.pricing.view', 'order' => 8], ['label_ar' => 'تسعير العملاء','label_en' => 'Customer Pricing', 'route' => '/sales/pricing/customer', 'permission' => 'sales.pricing.view', 'order' => 8],
['label_ar' => 'إحالات الأعضاء', 'label_en' => 'Member Referrals', 'route' => '/sales/referrals', 'permission' => 'sales.referral.view', 'order' => 9],
], ],
]); ]);
// ────────────────────────────────────────────────────────────
// Sales — Member Referral Commission (on activation)
// ────────────────────────────────────────────────────────────
\App\Core\EventBus::listen('member.activated', function (array $data): void {
$memberId = (int) ($data['member_id'] ?? $data['id'] ?? 0);
if ($memberId > 0) {
\App\Modules\Sales\Services\ReferralCommissionService::onMemberActivated($memberId);
}
}, 60);
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Models;
use App\Core\Model;
use App\Core\App;
class Locker extends Model
{
protected static string $table = 'sa_lockers';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static array $fillable = [
'code', 'name_ar', 'name_en', 'facility_id', 'discipline_id',
'locker_type', 'location_note', 'is_active',
];
public static function getTypeOptions(): array
{
return [
'standard' => 'عادي',
'large' => 'كبير',
'vip' => 'VIP',
];
}
public static function findByCode(string $code): ?self
{
$db = App::getInstance()->db();
$row = $db->selectOne("SELECT * FROM sa_lockers WHERE code = ? AND is_archived = 0", [$code]);
return $row ? static::hydrate($row) : null;
}
public function getCurrentRental(): ?array
{
$db = App::getInstance()->db();
return $db->selectOne(
"SELECT lr.*, p.full_name_ar as player_name
FROM sa_locker_rentals lr
LEFT JOIN sa_players p ON p.id = lr.player_id
WHERE lr.locker_id = ? AND lr.status IN ('active','grace_period','pending_eviction') AND lr.is_archived = 0
ORDER BY lr.start_date DESC LIMIT 1",
[(int) $this->id]
);
}
public function isOccupied(): bool
{
return $this->getCurrentRental() !== null;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Models;
use App\Core\Model;
use App\Core\App;
class LockerRental extends Model
{
protected static string $table = 'sa_locker_rentals';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static array $fillable = [
'rental_number', 'locker_id', 'player_id', 'rental_type',
'start_date', 'end_date', 'amount', 'payment_status',
'payment_id', 'receipt_id', 'receipt_number', 'status',
'grace_started_at', 'evicted_at', 'eviction_reason', 'eviction_by',
'renewed_from_id', 'notes',
];
public static function getRentalTypeOptions(): array
{
return [
'monthly' => 'شهري',
'6_months' => '6 شهور',
'yearly' => 'سنوي',
];
}
public static function getStatusOptions(): array
{
return [
'active' => 'فعال',
'grace_period' => 'فترة سماح',
'pending_eviction' => 'بانتظار الإخلاء',
'evicted' => 'تم الإخلاء',
'completed' => 'مكتمل',
'cancelled' => 'ملغي',
];
}
public static function generateNumber(): string
{
$db = App::getInstance()->db();
$prefix = 'LR-' . date('Ym') . '-';
$last = $db->selectOne(
"SELECT rental_number FROM sa_locker_rentals WHERE rental_number LIKE ? ORDER BY id DESC LIMIT 1",
[$prefix . '%']
);
if ($last) {
$seq = (int) substr($last['rental_number'], -4) + 1;
} else {
$seq = 1;
}
return $prefix . str_pad((string) $seq, 4, '0', STR_PAD_LEFT);
}
}
...@@ -144,6 +144,25 @@ return [ ...@@ -144,6 +144,25 @@ return [
['POST', '/sa/waitlist/{id:\d+}/offer', 'SportsActivity\Controllers\WaitlistController@offer', ['auth', 'csrf'], 'sa.waitlist.manage'], ['POST', '/sa/waitlist/{id:\d+}/offer', 'SportsActivity\Controllers\WaitlistController@offer', ['auth', 'csrf'], 'sa.waitlist.manage'],
['POST', '/sa/waitlist/{id:\d+}/cancel', 'SportsActivity\Controllers\WaitlistController@cancel', ['auth', 'csrf'], 'sa.waitlist.manage'], ['POST', '/sa/waitlist/{id:\d+}/cancel', 'SportsActivity\Controllers\WaitlistController@cancel', ['auth', 'csrf'], 'sa.waitlist.manage'],
// Lockers
['GET', '/sa/lockers', 'SportsActivity\Controllers\LockerController@index', ['auth'], 'sa.locker.view'],
['GET', '/sa/lockers/create', 'SportsActivity\Controllers\LockerController@create', ['auth'], 'sa.locker.manage'],
['POST', '/sa/lockers', 'SportsActivity\Controllers\LockerController@store', ['auth', 'csrf'], 'sa.locker.manage'],
['GET', '/sa/lockers/{id:\d+}', 'SportsActivity\Controllers\LockerController@show', ['auth'], 'sa.locker.view'],
['GET', '/sa/lockers/{id:\d+}/edit', 'SportsActivity\Controllers\LockerController@edit', ['auth'], 'sa.locker.manage'],
['POST', '/sa/lockers/{id:\d+}', 'SportsActivity\Controllers\LockerController@update', ['auth', 'csrf'], 'sa.locker.manage'],
['POST', '/sa/lockers/{id:\d+}/toggle', 'SportsActivity\Controllers\LockerController@toggle', ['auth', 'csrf'], 'sa.locker.manage'],
// Locker Rentals
['GET', '/sa/locker-rentals', 'SportsActivity\Controllers\LockerRentalController@index', ['auth'], 'sa.locker_rental.view'],
['GET', '/sa/locker-rentals/create', 'SportsActivity\Controllers\LockerRentalController@create', ['auth'], 'sa.locker_rental.create'],
['POST', '/sa/locker-rentals', 'SportsActivity\Controllers\LockerRentalController@store', ['auth', 'csrf'], 'sa.locker_rental.create'],
['GET', '/sa/locker-rentals/{id:\d+}', 'SportsActivity\Controllers\LockerRentalController@show', ['auth'], 'sa.locker_rental.view'],
['GET', '/sa/locker-rentals/{id:\d+}/renew', 'SportsActivity\Controllers\LockerRentalController@renew', ['auth'], 'sa.locker_rental.create'],
['POST', '/sa/locker-rentals/{id:\d+}/renew', 'SportsActivity\Controllers\LockerRentalController@storeRenew', ['auth', 'csrf'], 'sa.locker_rental.create'],
['GET', '/sa/locker-rentals/{id:\d+}/evict', 'SportsActivity\Controllers\LockerRentalController@evict', ['auth'], 'sa.locker_rental.evict'],
['POST', '/sa/locker-rentals/{id:\d+}/evict', 'SportsActivity\Controllers\LockerRentalController@storeEvict', ['auth', 'csrf'], 'sa.locker_rental.evict'],
// Pool Grid Management // Pool Grid Management
['GET', '/sa/pool-grid', 'SportsActivity\Controllers\PoolGridController@index', ['auth'], 'sa.pool-grid.manage'], ['GET', '/sa/pool-grid', 'SportsActivity\Controllers\PoolGridController@index', ['auth'], 'sa.pool-grid.manage'],
['GET', '/sa/pool-grid/{id:\d+}', 'SportsActivity\Controllers\PoolGridController@manage', ['auth'], 'sa.pool-grid.manage'], ['GET', '/sa/pool-grid/{id:\d+}', 'SportsActivity\Controllers\PoolGridController@manage', ['auth'], 'sa.pool-grid.manage'],
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إيجار لوكر جديد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/locker-rentals" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/sa/locker-rentals">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="key" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات الإيجار</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">اللوكر <span style="color:#DC2626;">*</span></label>
<select name="locker_id" class="form-select" required>
<option value="">-- اختر لوكر متاح --</option>
<?php foreach ($lockers as $locker): ?>
<option value="<?= (int) $locker['id'] ?>" <?= old('locker_id') == $locker['id'] ? 'selected' : '' ?>>
<?= e($locker['code']) ?> - <?= e($locker['name_ar']) ?> (<?= e(\App\Modules\SportsActivity\Models\Locker::getTypeOptions()[$locker['locker_type'] ?? ''] ?? '') ?>)
</option>
<?php endforeach; ?>
</select>
<?php if (empty($lockers)): ?>
<small style="color:#D97706;font-size:11px;margin-top:4px;display:block;">لا توجد لوكرات متاحة حاليا.</small>
<?php endif; ?>
</div>
<div class="form-group">
<label class="form-label">اللاعب <span style="color:#DC2626;">*</span></label>
<select name="player_id" class="form-select" required id="playerSelect">
<option value="">-- اختر اللاعب --</option>
<?php foreach ($players as $player): ?>
<option value="<?= (int) $player['id'] ?>" <?= old('player_id') == $player['id'] ? 'selected' : '' ?>>
<?= e($player['full_name_ar']) ?> (<?= e($player['code'] ?? '') ?>)
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">نوع الإيجار <span style="color:#DC2626;">*</span></label>
<select name="rental_type" class="form-select" required id="rentalTypeSelect">
<option value="">-- اختر --</option>
<?php foreach ($rentalTypeOptions as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('rental_type') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">تاريخ البداية <span style="color:#DC2626;">*</span></label>
<input type="date" name="start_date" id="startDate" value="<?= e(old('start_date') ?: date('Y-m-d')) ?>" class="form-input" required style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">تاريخ النهاية (محسوب)</label>
<input type="text" id="endDateDisplay" class="form-input" readonly style="direction:ltr;text-align:left;background:#F9FAFB;" placeholder="يتم حسابه تلقائيا">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 2fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">المبلغ (ج.م) <span style="color:#DC2626;">*</span></label>
<input type="number" name="amount" value="<?= e(old('amount')) ?>" class="form-input" required min="0.01" step="0.01" placeholder="0.00" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">ملاحظات</label>
<input type="text" name="notes" value="<?= e(old('notes')) ?>" class="form-input" placeholder="ملاحظات إضافية...">
</div>
</div>
</div>
</div>
<!-- Submit -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary"><i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إنشاء الإيجار</button>
<a href="/sa/locker-rentals" class="btn btn-outline">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
var rentalType = document.getElementById('rentalTypeSelect');
var startDate = document.getElementById('startDate');
var endDisplay = document.getElementById('endDateDisplay');
function calculateEndDate() {
var type = rentalType.value;
var start = startDate.value;
if (!type || !start) { endDisplay.value = ''; return; }
var d = new Date(start);
switch (type) {
case 'monthly': d.setMonth(d.getMonth() + 1); break;
case '6_months': d.setMonth(d.getMonth() + 6); break;
case 'yearly': d.setFullYear(d.getFullYear() + 1); break;
}
endDisplay.value = d.toISOString().split('T')[0];
}
rentalType.addEventListener('change', calculateEndDate);
startDate.addEventListener('change', calculateEndDate);
calculateEndDate();
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>محضر فض - إخلاء لوكر<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/locker-rentals/<?= (int) $rental['id'] ?>" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للتفاصيل</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Warning Card -->
<div class="card" style="margin-bottom:20px;border:2px solid #DC2626;">
<div style="padding:15px 20px;background:#FEF2F2;display:flex;align-items:center;gap:8px;">
<i data-lucide="alert-triangle" style="width:20px;height:20px;color:#DC2626;"></i>
<h3 style="margin:0;color:#DC2626;font-size:15px;">تحذير - محضر فض لوكر</h3>
</div>
<div style="padding:20px;background:#FEF2F2;">
<p style="margin:0 0 10px;color:#991B1B;font-size:14px;">أنت على وشك إجراء محضر فض وإخلاء اللوكر. هذا الإجراء نهائي ولا يمكن التراجع عنه.</p>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;margin-top:15px;">
<div>
<div style="font-size:12px;color:#991B1B;margin-bottom:4px;">اللوكر</div>
<div style="font-weight:600;color:#111;"><?= e($rental['locker_code'] ?? '') ?> - <?= e($rental['locker_name'] ?? '') ?></div>
</div>
<div>
<div style="font-size:12px;color:#991B1B;margin-bottom:4px;">اللاعب</div>
<div style="font-weight:600;color:#111;"><?= e($rental['player_name'] ?? '') ?></div>
</div>
<div>
<div style="font-size:12px;color:#991B1B;margin-bottom:4px;">رقم الإيجار</div>
<div style="font-weight:600;color:#111;"><?= e($rental['rental_number'] ?? '') ?></div>
</div>
</div>
</div>
</div>
<!-- Eviction Form -->
<form method="POST" action="/sa/locker-rentals/<?= (int) $rental['id'] ?>/evict">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="file-text" style="width:18px;height:18px;color:#DC2626;"></i>
<h3 style="margin:0;color:#DC2626;font-size:15px;">محضر الفض</h3>
</div>
<div style="padding:20px;">
<div class="form-group">
<label class="form-label">سبب الإخلاء (محضر الفض) <span style="color:#DC2626;">*</span></label>
<textarea name="eviction_reason" class="form-input" rows="6" required placeholder="اكتب تفاصيل محضر الفض هنا... (يشمل سبب الإخلاء، حالة اللوكر، أي ملاحظات)"><?= e(old('eviction_reason')) ?></textarea>
<small style="color:#6B7280;font-size:11px;margin-top:4px;display:block;">هذا الحقل مطلوب. يرجى كتابة تفاصيل كافية لمحضر الفض.</small>
</div>
</div>
</div>
<!-- Submit -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="background:#DC2626;border-color:#DC2626;" onclick="return confirm('هل أنت متأكد من إجراء محضر الفض؟ هذا الإجراء نهائي.');">
<i data-lucide="user-x" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> تأكيد محضر الفض
</button>
<a href="/sa/locker-rentals/<?= (int) $rental['id'] ?>" class="btn btn-outline">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إيجارات اللوكرات<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if (can('sa.locker_rental.create')): ?>
<a href="/sa/locker-rentals/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إيجار جديد</a>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Stats Cards -->
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:24px;font-weight:700;color:#059669;"><?= (int) ($stats['active'] ?? 0) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">فعال</div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:24px;font-weight:700;color:#D97706;"><?= (int) ($stats['grace_period'] ?? 0) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">فترة سماح</div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:24px;font-weight:700;color:#DC2626;"><?= (int) ($stats['pending_eviction'] ?? 0) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">بانتظار الإخلاء</div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:24px;font-weight:700;color:#6B7280;"><?= (int) ($stats['available'] ?? 0) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">متاح</div>
</div>
</div>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/sa/locker-rentals" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($search ?? '') ?>" placeholder="رقم الإيجار، اسم اللاعب، كود اللوكر..." class="form-input">
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select">
<option value="">-- الكل --</option>
<?php foreach ($statusOptions as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($status ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<?php if (($search ?? '') !== '' || ($status ?? '') !== ''): ?>
<a href="/sa/locker-rentals" class="btn btn-outline" style="color:#6B7280;">مسح</a>
<?php endif; ?>
</form>
</div>
<!-- Results -->
<?php if (!empty($rentals)): ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>رقم الإيجار</th>
<th>اللوكر</th>
<th>اللاعب</th>
<th>النوع</th>
<th>من</th>
<th>إلى</th>
<th>المبلغ</th>
<th>الحالة</th>
<th>إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($rentals as $rental): ?>
<?php
$rst = $rental['status'] ?? '';
$rstColors = ['active' => '#059669', 'grace_period' => '#D97706', 'pending_eviction' => '#DC2626', 'evicted' => '#6B7280', 'completed' => '#2563EB', 'cancelled' => '#6B7280'];
$rstBg = ['active' => '#D1FAE5', 'grace_period' => '#FEF3C7', 'pending_eviction' => '#FEE2E2', 'evicted' => '#F3F4F6', 'completed' => '#DBEAFE', 'cancelled' => '#F3F4F6'];
?>
<tr>
<td>
<a href="/sa/locker-rentals/<?= (int) $rental['id'] ?>" style="color:#0D7377;text-decoration:none;font-weight:600;">
<?= e($rental['rental_number'] ?? '-') ?>
</a>
</td>
<td><code style="font-size:12px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($rental['locker_code'] ?? '-') ?></code></td>
<td><?= e($rental['player_name'] ?? '-') ?></td>
<td><?= e($rentalTypeOptions[$rental['rental_type'] ?? ''] ?? '-') ?></td>
<td style="font-size:12px;"><?= e($rental['start_date'] ?? '-') ?></td>
<td style="font-size:12px;"><?= e($rental['end_date'] ?? '-') ?></td>
<td><?= number_format((float) ($rental['amount'] ?? 0), 2) ?> ج.م</td>
<td>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $rstBg[$rst] ?? '#F3F4F6' ?>;color:<?= $rstColors[$rst] ?? '#6B7280' ?>;"><?= e($statusOptions[$rst] ?? '-') ?></span>
</td>
<td>
<div style="display:flex;gap:4px;">
<a href="/sa/locker-rentals/<?= (int) $rental['id'] ?>" class="btn btn-sm btn-outline" title="عرض">
<i data-lucide="eye" style="width:14px;height:14px;"></i>
</a>
<?php if (in_array($rst, ['active', 'grace_period']) && can('sa.locker_rental.create')): ?>
<a href="/sa/locker-rentals/<?= (int) $rental['id'] ?>/renew" class="btn btn-sm btn-outline" title="تجديد" style="color:#059669;border-color:#059669;">
<i data-lucide="refresh-cw" style="width:14px;height:14px;"></i>
</a>
<?php endif; ?>
<?php if (in_array($rst, ['grace_period', 'pending_eviction']) && can('sa.locker_rental.evict')): ?>
<a href="/sa/locker-rentals/<?= (int) $rental['id'] ?>/evict" class="btn btn-sm btn-outline" title="محضر فض" style="color:#DC2626;border-color:#DC2626;">
<i data-lucide="user-x" style="width:14px;height:14px;"></i>
</a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if (($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php
$queryParams = '';
if (($search ?? '') !== '') $queryParams .= 'q=' . urlencode($search) . '&';
if (($status ?? '') !== '') $queryParams .= 'status=' . urlencode($status) . '&';
?>
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination, 'baseUrl' => '/sa/locker-rentals?' . $queryParams]); ?>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="key" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد إيجارات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (($search ?? '') !== '' || ($status ?? '') !== ''): ?>
لا توجد نتائج مطابقة. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإنشاء إيجار جديد.
<?php endif; ?>
</p>
<?php if (can('sa.locker_rental.create')): ?>
<a href="/sa/locker-rentals/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إيجار جديد</a>
<?php endif; ?>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تجديد إيجار لوكر<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/locker-rentals/<?= (int) $rental['id'] ?>" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للتفاصيل</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Current Rental Info -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="info" style="width:18px;height:18px;color:#2563EB;"></i>
<h3 style="margin:0;color:#2563EB;font-size:15px;">الإيجار الحالي</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:15px;">
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">اللوكر</div>
<div style="font-weight:600;"><?= e($rental['locker_code'] ?? '') ?> - <?= e($rental['locker_name'] ?? '') ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">اللاعب</div>
<div style="font-weight:600;"><?= e($rental['player_name'] ?? '') ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">ينتهي في</div>
<div style="font-weight:600;"><?= e($rental['end_date'] ?? '-') ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">المبلغ السابق</div>
<div style="font-weight:600;"><?= number_format((float) ($rental['amount'] ?? 0), 2) ?> ج.م</div>
</div>
</div>
</div>
</div>
<!-- Renewal Form -->
<form method="POST" action="/sa/locker-rentals/<?= (int) $rental['id'] ?>/renew">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="refresh-cw" style="width:18px;height:18px;color:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">بيانات التجديد</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">نوع الإيجار <span style="color:#DC2626;">*</span></label>
<select name="rental_type" class="form-select" required id="rentalTypeSelect">
<?php foreach ($rentalTypeOptions as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('rental_type') ?: $rental['rental_type'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">تاريخ البداية <span style="color:#DC2626;">*</span></label>
<input type="date" name="start_date" id="startDate" value="<?= e(old('start_date') ?: $rental['end_date'] ?? date('Y-m-d')) ?>" class="form-input" required style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">تاريخ النهاية (محسوب)</label>
<input type="text" id="endDateDisplay" class="form-input" readonly style="direction:ltr;text-align:left;background:#F9FAFB;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 2fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">المبلغ (ج.م) <span style="color:#DC2626;">*</span></label>
<input type="number" name="amount" value="<?= e(old('amount') ?: $rental['amount'] ?? '') ?>" class="form-input" required min="0.01" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">ملاحظات</label>
<input type="text" name="notes" value="<?= e(old('notes')) ?>" class="form-input" placeholder="ملاحظات إضافية...">
</div>
</div>
</div>
</div>
<!-- Submit -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="background:#059669;border-color:#059669;"><i data-lucide="refresh-cw" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> تأكيد التجديد</button>
<a href="/sa/locker-rentals/<?= (int) $rental['id'] ?>" class="btn btn-outline">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
var rentalType = document.getElementById('rentalTypeSelect');
var startDate = document.getElementById('startDate');
var endDisplay = document.getElementById('endDateDisplay');
function calculateEndDate() {
var type = rentalType.value;
var start = startDate.value;
if (!type || !start) { endDisplay.value = ''; return; }
var d = new Date(start);
switch (type) {
case 'monthly': d.setMonth(d.getMonth() + 1); break;
case '6_months': d.setMonth(d.getMonth() + 6); break;
case 'yearly': d.setFullYear(d.getFullYear() + 1); break;
}
endDisplay.value = d.toISOString().split('T')[0];
}
rentalType.addEventListener('change', calculateEndDate);
startDate.addEventListener('change', calculateEndDate);
calculateEndDate();
});
</script>
<?php $__template->endSection(); ?>
This diff is collapsed.
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إضافة لوكر جديد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/lockers" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/sa/lockers">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="lock" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات اللوكر</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">كود اللوكر <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code')) ?>" class="form-input" required maxlength="30" placeholder="مثال: LK-001" style="direction:ltr;text-align:left;text-transform:uppercase;">
</div>
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e(old('name_ar')) ?>" class="form-input" required maxlength="200" placeholder="اسم اللوكر بالعربي">
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e(old('name_en')) ?>" class="form-input" maxlength="200" placeholder="Locker name in English" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">المرفق</label>
<select name="facility_id" class="form-select">
<option value="">-- اختر المرفق --</option>
<?php foreach ($facilities as $fac): ?>
<option value="<?= (int) $fac['id'] ?>" <?= old('facility_id') == $fac['id'] ? 'selected' : '' ?>><?= e($fac['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">اللعبة</label>
<select name="discipline_id" class="form-select">
<option value="">-- اختر اللعبة --</option>
<?php foreach ($disciplines as $disc): ?>
<option value="<?= (int) $disc['id'] ?>" <?= old('discipline_id') == $disc['id'] ? 'selected' : '' ?>><?= e($disc['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">نوع اللوكر <span style="color:#DC2626;">*</span></label>
<select name="locker_type" class="form-select" required>
<option value="">-- اختر النوع --</option>
<?php foreach ($typeOptions as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('locker_type') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div style="margin-top:15px;">
<div class="form-group">
<label class="form-label">ملاحظات الموقع</label>
<textarea name="location_note" class="form-input" rows="3" placeholder="مثال: الدور الثاني - الجهة اليمنى..."><?= e(old('location_note')) ?></textarea>
</div>
</div>
</div>
</div>
<!-- Submit -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary"><i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حفظ اللوكر</button>
<a href="/sa/lockers" class="btn btn-outline">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل اللوكر: <?= e($locker['name_ar'] ?? '') ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/lockers/<?= (int) $locker['id'] ?>" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للتفاصيل</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/sa/lockers/<?= (int) $locker['id'] ?>">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="lock" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات اللوكر</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">كود اللوكر <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code') ?: $locker['code'] ?? '') ?>" class="form-input" required maxlength="30" style="direction:ltr;text-align:left;text-transform:uppercase;">
</div>
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e(old('name_ar') ?: $locker['name_ar'] ?? '') ?>" class="form-input" required maxlength="200">
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e(old('name_en') ?: $locker['name_en'] ?? '') ?>" class="form-input" maxlength="200" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">المرفق</label>
<select name="facility_id" class="form-select">
<option value="">-- اختر المرفق --</option>
<?php foreach ($facilities as $fac): ?>
<option value="<?= (int) $fac['id'] ?>" <?= (old('facility_id') ?: $locker['facility_id'] ?? '') == $fac['id'] ? 'selected' : '' ?>><?= e($fac['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">اللعبة</label>
<select name="discipline_id" class="form-select">
<option value="">-- اختر اللعبة --</option>
<?php foreach ($disciplines as $disc): ?>
<option value="<?= (int) $disc['id'] ?>" <?= (old('discipline_id') ?: $locker['discipline_id'] ?? '') == $disc['id'] ? 'selected' : '' ?>><?= e($disc['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">نوع اللوكر <span style="color:#DC2626;">*</span></label>
<select name="locker_type" class="form-select" required>
<option value="">-- اختر النوع --</option>
<?php foreach ($typeOptions as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('locker_type') ?: $locker['locker_type'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div style="margin-top:15px;">
<div class="form-group">
<label class="form-label">ملاحظات الموقع</label>
<textarea name="location_note" class="form-input" rows="3"><?= e(old('location_note') ?: $locker['location_note'] ?? '') ?></textarea>
</div>
</div>
</div>
</div>
<!-- Submit -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary"><i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> تحديث اللوكر</button>
<a href="/sa/lockers/<?= (int) $locker['id'] ?>" class="btn btn-outline">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>اللوكرات<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if (can('sa.locker.manage')): ?>
<a href="/sa/lockers/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة لوكر</a>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/sa/lockers" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($search ?? '') ?>" placeholder="ابحث بالكود أو الاسم..." class="form-input">
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">المرفق</label>
<select name="facility_id" class="form-select">
<option value="">-- الكل --</option>
<?php foreach ($facilities as $fac): ?>
<option value="<?= (int) $fac['id'] ?>" <?= ($facilityId ?? 0) == $fac['id'] ? 'selected' : '' ?>><?= e($fac['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">اللعبة</label>
<select name="discipline_id" class="form-select">
<option value="">-- الكل --</option>
<?php foreach ($disciplines as $disc): ?>
<option value="<?= (int) $disc['id'] ?>" <?= ($disciplineId ?? 0) == $disc['id'] ? 'selected' : '' ?>><?= e($disc['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:120px;">
<label class="form-label" style="font-size:12px;">النوع</label>
<select name="locker_type" class="form-select">
<option value="">-- الكل --</option>
<?php foreach ($typeOptions as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($lockerType ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<?php if (($search ?? '') !== '' || ($facilityId ?? 0) > 0 || ($disciplineId ?? 0) > 0 || ($lockerType ?? '') !== ''): ?>
<a href="/sa/lockers" class="btn btn-outline" style="color:#6B7280;">مسح</a>
<?php endif; ?>
</form>
</div>
<!-- Results -->
<?php if (!empty($lockers)): ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الكود</th>
<th>الاسم</th>
<th>المرفق</th>
<th>اللعبة</th>
<th>النوع</th>
<th>الحالة</th>
<th>إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($lockers as $locker): ?>
<tr>
<td><code style="font-size:12px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($locker['code'] ?? '') ?></code></td>
<td>
<a href="/sa/lockers/<?= (int) $locker['id'] ?>" style="color:#0D7377;text-decoration:none;font-weight:600;">
<?= e($locker['name_ar'] ?? '') ?>
</a>
<?php if (!empty($locker['name_en'])): ?>
<div style="font-size:11px;color:#9CA3AF;"><?= e($locker['name_en']) ?></div>
<?php endif; ?>
</td>
<td><?= e($locker['facility_name'] ?? '-') ?></td>
<td><?= e($locker['discipline_name'] ?? '-') ?></td>
<td><?= e($typeOptions[$locker['locker_type'] ?? ''] ?? '-') ?></td>
<td>
<?php if (!empty($locker['is_occupied'])): ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;background:#FEE2E2;color:#DC2626;">مشغول</span>
<?php elseif ((int) ($locker['is_active'] ?? 0)): ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;background:#D1FAE5;color:#059669;">متاح</span>
<?php else: ?>
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;background:#F3F4F6;color:#6B7280;">معطّل</span>
<?php endif; ?>
</td>
<td>
<div style="display:flex;gap:4px;">
<a href="/sa/lockers/<?= (int) $locker['id'] ?>" class="btn btn-sm btn-outline" title="عرض">
<i data-lucide="eye" style="width:14px;height:14px;"></i>
</a>
<?php if (can('sa.locker.manage')): ?>
<a href="/sa/lockers/<?= (int) $locker['id'] ?>/edit" class="btn btn-sm btn-outline" title="تعديل">
<i data-lucide="edit-3" style="width:14px;height:14px;"></i>
</a>
<form method="POST" action="/sa/lockers/<?= (int) $locker['id'] ?>/toggle" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-sm btn-outline" title="<?= (int) $locker['is_active'] ? 'تعطيل' : 'تفعيل' ?>" onclick="return confirm('<?= (int) $locker['is_active'] ? 'هل أنت متأكد من تعطيل هذا اللوكر؟' : 'هل أنت متأكد من تفعيل هذا اللوكر؟' ?>');">
<i data-lucide="<?= (int) $locker['is_active'] ? 'toggle-right' : 'toggle-left' ?>" style="width:14px;height:14px;"></i>
</button>
</form>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if (($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php
$queryParams = '';
if (($search ?? '') !== '') $queryParams .= 'q=' . urlencode($search) . '&';
if (($facilityId ?? 0) > 0) $queryParams .= 'facility_id=' . $facilityId . '&';
if (($disciplineId ?? 0) > 0) $queryParams .= 'discipline_id=' . $disciplineId . '&';
if (($lockerType ?? '') !== '') $queryParams .= 'locker_type=' . urlencode($lockerType) . '&';
?>
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination, 'baseUrl' => '/sa/lockers?' . $queryParams]); ?>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="lock" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد لوكرات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (($search ?? '') !== '' || ($facilityId ?? 0) > 0 || ($disciplineId ?? 0) > 0 || ($lockerType ?? '') !== ''): ?>
لا توجد نتائج مطابقة. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإضافة لوكر جديد.
<?php endif; ?>
</p>
<?php if (can('sa.locker.manage')): ?>
<a href="/sa/lockers/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة لوكر</a>
<?php endif; ?>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
This diff is collapsed.
...@@ -32,6 +32,8 @@ MenuRegistry::register('sports_activity', [ ...@@ -32,6 +32,8 @@ MenuRegistry::register('sports_activity', [
['label_ar' => 'الاشتراكات', 'label_en' => 'Subscriptions', 'route' => '/sa/subscriptions', 'permission' => 'sa.subscription.view', 'order' => 13], ['label_ar' => 'الاشتراكات', 'label_en' => 'Subscriptions', 'route' => '/sa/subscriptions', 'permission' => 'sa.subscription.view', 'order' => 13],
['label_ar' => 'الحضور', 'label_en' => 'Attendance', 'route' => '/sa/attendance', 'permission' => 'sa.attendance.view', 'order' => 14], ['label_ar' => 'الحضور', 'label_en' => 'Attendance', 'route' => '/sa/attendance', 'permission' => 'sa.attendance.view', 'order' => 14],
['label_ar' => 'قائمة الانتظار', 'label_en' => 'Waitlist', 'route' => '/sa/waitlist', 'permission' => 'sa.waitlist.view', 'order' => 15], ['label_ar' => 'قائمة الانتظار', 'label_en' => 'Waitlist', 'route' => '/sa/waitlist', 'permission' => 'sa.waitlist.view', 'order' => 15],
['label_ar' => 'اللوكرات', 'label_en' => 'Lockers', 'route' => '/sa/lockers', 'permission' => 'sa.locker.view', 'order' => 16],
['label_ar' => 'إيجارات اللوكرات', 'label_en' => 'Locker Rentals', 'route' => '/sa/locker-rentals', 'permission' => 'sa.locker_rental.view','order' => 17],
], ],
]); ]);
...@@ -73,6 +75,12 @@ PermissionRegistry::register('sports_activity', [ ...@@ -73,6 +75,12 @@ PermissionRegistry::register('sports_activity', [
'sa.waitlist.view' => ['ar' => 'عرض قائمة الانتظار', 'en' => 'View Waitlist'], 'sa.waitlist.view' => ['ar' => 'عرض قائمة الانتظار', 'en' => 'View Waitlist'],
'sa.waitlist.manage' => ['ar' => 'إدارة قائمة الانتظار', 'en' => 'Manage Waitlist'], 'sa.waitlist.manage' => ['ar' => 'إدارة قائمة الانتظار', 'en' => 'Manage Waitlist'],
'sa.pool-grid.manage' => ['ar' => 'إدارة شبكة حمام السباحة', 'en' => 'Manage Pool Grid'], 'sa.pool-grid.manage' => ['ar' => 'إدارة شبكة حمام السباحة', 'en' => 'Manage Pool Grid'],
'sa.locker.view' => ['ar' => 'عرض اللوكرات', 'en' => 'View Lockers'],
'sa.locker.manage' => ['ar' => 'إدارة اللوكرات', 'en' => 'Manage Lockers'],
'sa.locker_rental.view' => ['ar' => 'عرض إيجارات اللوكرات', 'en' => 'View Locker Rentals'],
'sa.locker_rental.create' => ['ar' => 'إنشاء إيجار لوكر', 'en' => 'Create Locker Rental'],
'sa.locker_rental.manage' => ['ar' => 'إدارة إيجارات اللوكرات', 'en' => 'Manage Locker Rentals'],
'sa.locker_rental.evict' => ['ar' => 'محضر فض لوكر', 'en' => 'Evict Locker Tenant'],
]); ]);
// ─── Event Listeners ──────────────────────────────────────────────────────── // ─── Event Listeners ────────────────────────────────────────────────────────
......
...@@ -411,6 +411,12 @@ final class TutorialRegistry ...@@ -411,6 +411,12 @@ final class TutorialRegistry
['title' => 'الفلترة', 'body' => 'يمكنك التصفية حسب: <span class="field">التاريخ</span>، <span class="field">المندوب</span>، <span class="field">طريقة الدفع</span>.'], ['title' => 'الفلترة', 'body' => 'يمكنك التصفية حسب: <span class="field">التاريخ</span>، <span class="field">المندوب</span>، <span class="field">طريقة الدفع</span>.'],
['title' => 'التصدير', 'body' => 'اضغط <span class="field">تصدير Excel</span> أو <span class="field">طباعة</span>.'], ['title' => 'التصدير', 'body' => 'اضغط <span class="field">تصدير Excel</span> أو <span class="field">طباعة</span>.'],
], ],
'sales.referral-tracking' => [
['title' => 'ربط العضو بموظف المبيعات', 'body' => 'عند ملء استمارة عضو جديد: في قسم <span class="field">كيف تعرفت على النادي</span>، اختر <span class="field">موظف المبيعات المُحيل</span> من القائمة المنسدلة.<span class="info">هذا الحقل اختياري — يُستخدم فقط إذا جاء العضو عن طريق مندوب مبيعات.</span>'],
['title' => 'تفعيل العضوية', 'body' => 'عند تفعيل العضوية (بعد الدفع)، النظام يحسب العمولة تلقائياً بناءً على نسبة المندوب المسجلة.<span class="success">العمولة تُسجل في جدول عمولات الإحالات بحالة «معلقة» تلقائياً.</span>'],
['title' => 'استعراض تقرير الإحالات', 'body' => 'من <span class="field">المبيعات</span> > <span class="field">تقرير الإحالات</span>. يعرض: المندوب، العضو، قيمة العضوية، نسبة العمولة، المبلغ المستحق، الحالة.'],
['title' => 'الفلترة والتصدير', 'body' => 'يمكنك التصفية حسب: <span class="field">المندوب</span>، <span class="field">الفترة</span>، <span class="field">الحالة</span> (معلقة/معتمدة/مدفوعة).'],
],
// ── PAYMENTS ── // ── PAYMENTS ──
'payments.payment-types-overview' => [ 'payments.payment-types-overview' => [
['title' => 'فتح إعدادات الدفع', 'body' => 'من <span class="field">الإعدادات</span> > <span class="field">طرق الدفع</span>.'], ['title' => 'فتح إعدادات الدفع', 'body' => 'من <span class="field">الإعدادات</span> > <span class="field">طرق الدفع</span>.'],
...@@ -494,6 +500,24 @@ final class TutorialRegistry ...@@ -494,6 +500,24 @@ final class TutorialRegistry
['title' => 'الأولوية', 'body' => 'حدد أولوية الحجز: الأعضاء أولاً، ثم الفرق، ثم الزوار.<span class="info">يمكن تخصيص أسعار مختلفة لكل فئة.</span>'], ['title' => 'الأولوية', 'body' => 'حدد أولوية الحجز: الأعضاء أولاً، ثم الفرق، ثم الزوار.<span class="info">يمكن تخصيص أسعار مختلفة لكل فئة.</span>'],
['title' => 'الحفظ', 'body' => 'اضغط <span class="field">حفظ</span>. القواعد تُطبق تلقائياً على جميع الحجوزات الجديدة.'], ['title' => 'الحفظ', 'body' => 'اضغط <span class="field">حفظ</span>. القواعد تُطبق تلقائياً على جميع الحجوزات الجديدة.'],
], ],
'facilities.locker-setup' => [
['title' => 'فتح إدارة اللوكرات', 'body' => 'من <span class="field">النشاط الرياضي</span> > <span class="field">اللوكرات</span> > <span class="field">إضافة لوكر</span>.'],
['title' => 'بيانات اللوكر', 'body' => 'أدخل <span class="field">الكود</span> (رقم/اسم فريد)، <span class="field">الاسم</span>، <span class="field">النوع</span> (عادي/كبير/VIP).<span class="info">يمكنك ربط اللوكر بمنشأة أو رياضة معينة اختيارياً.</span>'],
['title' => 'ملاحظة الموقع', 'body' => 'أضف <span class="field">ملاحظة الموقع</span> لتسهيل تحديد مكان اللوكر (مثال: الدور الثاني - يمين المدخل).'],
['title' => 'الحفظ والتفعيل', 'body' => 'اضغط <span class="field">حفظ</span>. اللوكر يصبح متاحاً للتأجير فوراً.<span class="success">يمكنك تعطيل اللوكر مؤقتاً من صفحة التفاصيل بدون حذفه.</span>'],
],
'facilities.locker-rental' => [
['title' => 'إنشاء إيجار جديد', 'body' => 'من <span class="field">النشاط الرياضي</span> > <span class="field">إيجارات اللوكرات</span> > <span class="field">إيجار جديد</span>.'],
['title' => 'اختيار اللوكر واللاعب', 'body' => 'اختر <span class="field">اللوكر</span> (تظهر فقط اللوكرات المتاحة). ثم اختر <span class="field">اللاعب</span> من البحث.'],
['title' => 'تحديد مدة الإيجار', 'body' => 'اختر <span class="field">نوع الإيجار</span>: شهري (شهر واحد)، 6 شهور، أو سنوي. النظام يحسب تاريخ الانتهاء تلقائياً.<span class="info">المبلغ يُحدد يدوياً — يمكنك تطبيق تسعيرة مختلفة حسب نوع اللوكر.</span>'],
['title' => 'الحفظ', 'body' => 'اضغط <span class="field">حفظ</span>. يُنشئ النظام رقم إيجار فريد (LR-YYYYMM-XXXX) ويُسجل الإيجار بحالة «فعال».<span class="success">عند انتهاء الإيجار بدون تجديد، يدخل اللوكر فترة سماح 3 شهور تلقائياً.</span>'],
],
'facilities.locker-eviction' => [
['title' => 'متى يتم الإخلاء؟', 'body' => 'بعد انتهاء فترة السماح (3 شهور من تاريخ انتهاء الإيجار) بدون تجديد أو دفع، يتحول الإيجار لحالة <span class="field">بانتظار الإخلاء</span> تلقائياً.'],
['title' => 'فتح صفحة الإخلاء', 'body' => 'من <span class="field">إيجارات اللوكرات</span> > اضغط <span class="field">محضر فض</span> بجانب الإيجار المطلوب.'],
['title' => 'كتابة محضر الفض', 'body' => 'أدخل <span class="field">سبب الإخلاء</span> بالتفصيل. هذا يُسجل كمحضر رسمي.<span class="warn">عملية الإخلاء نهائية ولا يمكن التراجع عنها — يتم تحرير اللوكر للتأجير مرة أخرى.</span>'],
['title' => 'تأكيد الإخلاء', 'body' => 'اضغط <span class="field">تأكيد الإخلاء</span>. يتحول الإيجار لحالة «تم الإخلاء» واللوكر يصبح متاحاً للتأجير.<span class="success">يتم حفظ تاريخ الإخلاء والموظف المسؤول تلقائياً.</span>'],
],
// ── TOURNAMENTS ── // ── TOURNAMENTS ──
'tournaments.create-tournament' => [ 'tournaments.create-tournament' => [
['title' => 'إنشاء بطولة', 'body' => 'من <span class="field">البطولات</span> > <span class="field">بطولة جديدة</span>.'], ['title' => 'إنشاء بطولة', 'body' => 'من <span class="field">البطولات</span> > <span class="field">بطولة جديدة</span>.'],
...@@ -1226,6 +1250,14 @@ final class TutorialRegistry ...@@ -1226,6 +1250,14 @@ final class TutorialRegistry
'category' => 'reports', 'category' => 'reports',
'order' => 4, 'order' => 4,
], ],
'referral-tracking' => [
'title' => 'تتبع الإحالات',
'subtitle' => 'ربط الأعضاء الجدد بموظف المبيعات المُحيل وحساب العمولة',
'icon' => 'user-plus',
'color' => '#0891B2',
'category' => 'operations',
'order' => 5,
],
]; ];
} }
...@@ -1420,6 +1452,30 @@ final class TutorialRegistry ...@@ -1420,6 +1452,30 @@ final class TutorialRegistry
'category' => 'setup', 'category' => 'setup',
'order' => 4, 'order' => 4,
], ],
'locker-setup' => [
'title' => 'إعداد اللوكرات',
'subtitle' => 'تعريف اللوكرات وربطها بالرياضة أو المنشأة',
'icon' => 'lock',
'color' => '#7C3AED',
'category' => 'setup',
'order' => 5,
],
'locker-rental' => [
'title' => 'تأجير لوكر',
'subtitle' => 'إنشاء عقد إيجار لوكر شهري أو سنوي',
'icon' => 'key',
'color' => '#059669',
'category' => 'operations',
'order' => 6,
],
'locker-eviction' => [
'title' => 'محضر فض لوكر',
'subtitle' => 'إخلاء لوكر بعد انتهاء فترة السماح بدون تجديد',
'icon' => 'alert-triangle',
'color' => '#DC2626',
'category' => 'operations',
'order' => 7,
],
]; ];
} }
......
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
use App\Core\Logger;
/**
* Checks locker rental expiry and manages grace period transitions.
* - Active rentals past end_date → grace_period
* - Grace period rentals past 3 months → pending_eviction
*/
class LockerGraceCheckJob
{
private Database $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function shouldRun(): bool
{
// Run daily
return true;
}
public function run(): array
{
$ts = date('Y-m-d H:i:s');
$today = date('Y-m-d');
$processed = 0;
// 1. Active rentals past end_date → grace_period
$expired = $this->db->select(
"SELECT id FROM sa_locker_rentals
WHERE status = 'active'
AND end_date < ?
AND is_archived = 0",
[$today]
);
foreach ($expired as $row) {
$this->db->update('sa_locker_rentals', [
'status' => 'grace_period',
'grace_started_at' => $today,
'updated_at' => $ts,
], 'id = ?', [(int) $row['id']]);
$processed++;
}
// 2. Grace period rentals past 3 months → pending_eviction
$overGrace = $this->db->select(
"SELECT id FROM sa_locker_rentals
WHERE status = 'grace_period'
AND grace_started_at <= DATE_SUB(?, INTERVAL 3 MONTH)
AND is_archived = 0",
[$today]
);
foreach ($overGrace as $row) {
$this->db->update('sa_locker_rentals', [
'status' => 'pending_eviction',
'updated_at' => $ts,
], 'id = ?', [(int) $row['id']]);
$processed++;
}
Logger::info("LockerGraceCheck: {$processed} rentals updated (expired=" . count($expired) . ", over_grace=" . count($overGrace) . ")");
return ['processed' => $processed];
}
}
<?php
declare(strict_types=1);
return [
'up' => "ALTER TABLE `members`
ADD COLUMN `referred_by_representative_id` INT UNSIGNED NULL AFTER `referral_source`,
ADD INDEX `idx_members_referred_by` (`referred_by_representative_id`);",
'down' => "ALTER TABLE `members` DROP INDEX `idx_members_referred_by`, DROP COLUMN `referred_by_representative_id`;",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE `member_referral_commissions` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`representative_id` INT UNSIGNED NOT NULL,
`member_id` BIGINT UNSIGNED NOT NULL,
`membership_value` DECIMAL(15,2) NOT NULL,
`commission_rate` DECIMAL(5,2) NOT NULL,
`commission_amount` DECIMAL(15,2) NOT NULL,
`commission_date` DATE NOT NULL,
`status` ENUM('pending','approved','paid') NOT NULL DEFAULT 'pending',
`paid_at` DATETIME NULL,
`notes` VARCHAR(500) NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_mrc_rep` (`representative_id`),
INDEX `idx_mrc_member` (`member_id`),
INDEX `idx_mrc_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;",
'down' => "DROP TABLE IF EXISTS `member_referral_commissions`;",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE `sa_lockers` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(30) NOT NULL,
`name_ar` VARCHAR(200) NOT NULL,
`name_en` VARCHAR(200) NULL,
`facility_id` BIGINT UNSIGNED NULL,
`discipline_id` BIGINT UNSIGNED NULL,
`locker_type` VARCHAR(30) NOT NULL DEFAULT 'standard',
`location_note` VARCHAR(300) NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_sa_locker_code` (`code`),
INDEX `idx_sa_locker_facility` (`facility_id`),
INDEX `idx_sa_locker_discipline` (`discipline_id`),
INDEX `idx_sa_locker_active` (`is_active`, `is_archived`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;",
'down' => "DROP TABLE IF EXISTS `sa_lockers`;",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE `sa_locker_rentals` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`rental_number` VARCHAR(30) NOT NULL,
`locker_id` BIGINT UNSIGNED NOT NULL,
`player_id` BIGINT UNSIGNED NOT NULL,
`rental_type` ENUM('monthly','6_months','yearly') NOT NULL,
`start_date` DATE NOT NULL,
`end_date` DATE NOT NULL,
`amount` DECIMAL(15,2) NOT NULL,
`payment_status` ENUM('unpaid','paid','partial') NOT NULL DEFAULT 'unpaid',
`payment_id` BIGINT UNSIGNED NULL,
`receipt_id` BIGINT UNSIGNED NULL,
`receipt_number` VARCHAR(50) NULL,
`status` VARCHAR(30) NOT NULL DEFAULT 'active',
`grace_started_at` DATE NULL,
`evicted_at` DATE NULL,
`eviction_reason` TEXT NULL,
`eviction_by` BIGINT UNSIGNED NULL,
`renewed_from_id` BIGINT UNSIGNED NULL,
`notes` TEXT NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_sa_lr_number` (`rental_number`),
INDEX `idx_sa_lr_locker` (`locker_id`),
INDEX `idx_sa_lr_player` (`player_id`),
INDEX `idx_sa_lr_status` (`status`),
INDEX `idx_sa_lr_dates` (`start_date`, `end_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;",
'down' => "DROP TABLE IF EXISTS `sa_locker_rentals`;",
];
<?php
declare(strict_types=1);
return function (\App\Core\Database $db): void {
$now = date('Y-m-d H:i:s');
$permissionKeys = [
'sa.locker.view',
'sa.locker.manage',
'sa.locker_rental.view',
'sa.locker_rental.create',
'sa.locker_rental.manage',
'sa.locker_rental.evict',
'sales.referral.view',
];
$superAdmin = $db->selectOne("SELECT id FROM roles WHERE role_code = 'super_admin'");
if (!$superAdmin) {
return;
}
$roleId = (int) $superAdmin['id'];
foreach ($permissionKeys as $key) {
$exists = $db->selectOne(
"SELECT 1 FROM role_permissions WHERE role_id = ? AND permission_key = ?",
[$roleId, $key]
);
if (!$exists) {
$db->insert('role_permissions', [
'role_id' => $roleId,
'permission_key' => $key,
'granted_at' => $now,
]);
}
}
};
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