Commit 446d6aca authored by Administrator's avatar Administrator

Update 39 files via Son of Anton

parent d41f51be
<?php
declare(strict_types=1);
namespace App\Modules\Foreign\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Foreign\Models\ForeignMemberDetail;
use App\Modules\Rules\Services\RuleEngine;
class ForeignController extends Controller
{
public function index(Request $request): Response
{
$rows = ForeignMemberDetail::getAllActive();
return $this->view('Foreign.Views.index', ['rows' => $rows]);
}
public function create(Request $request, string $memberId): Response
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]);
if (!$member) return $this->redirect('/members')->withError('العضو غير موجود');
$countries = $db->select("SELECT id, name_ar, nationality_ar FROM countries WHERE is_active = 1 ORDER BY name_ar");
$branchCode = 'sheraton';
if ($member['branch_id']) {
$branch = $db->selectOne("SELECT branch_code FROM branches WHERE id = ?", [(int) $member['branch_id']]);
$branchCode = $branch['branch_code'] ?? 'sheraton';
}
$feeRuleCode = $branchCode === 'new_capital' ? 'FOREIGN_MEMBER_FEE_CAPITAL' : 'FOREIGN_MEMBER_FEE_SHERATON';
$feeData = RuleEngine::get($feeRuleCode);
$feeUsd = $feeData['amount_usd'] ?? '10000.00';
return $this->view('Foreign.Views.create', [
'member' => $member,
'countries' => $countries,
'fee_usd' => $feeUsd,
]);
}
public function store(Request $request, string $memberId): Response
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]);
if (!$member) return $this->redirect('/members')->withError('العضو غير موجود');
$data = $request->all();
unset($data['_csrf_token']);
$errors = [];
if (empty(trim($data['passport_number'] ?? ''))) $errors[] = 'رقم جواز السفر مطلوب';
if (empty($data['issuing_country_id'] ?? '')) $errors[] = 'بلد الإصدار مطلوب';
if (empty($data['nationality_country_id'] ?? '')) $errors[] = 'الجنسية مطلوبة';
if (empty($data['passport_issue_date'] ?? '')) $errors[] = 'تاريخ الإصدار مطلوب';
if (empty($data['passport_expiry_date'] ?? '')) $errors[] = 'تاريخ الانتهاء مطلوب';
if (!empty($data['passport_expiry_date'])) {
$minMonthsData = RuleEngine::get('PASSPORT_MIN_VALIDITY_MONTHS');
$minMonths = $minMonthsData['months'] ?? 6;
$minDate = date('Y-m-d', strtotime("+{$minMonths} months"));
if ($data['passport_expiry_date'] < $minDate) {
$errors[] = "جواز السفر يجب أن يكون صالحاً لمدة {$minMonths} أشهر على الأقل";
}
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $data);
return $this->redirect("/members/{$memberId}/foreign/create");
}
$branchCode = 'sheraton';
if ($member['branch_id']) {
$branch = $db->selectOne("SELECT branch_code FROM branches WHERE id = ?", [(int) $member['branch_id']]);
$branchCode = $branch['branch_code'] ?? 'sheraton';
}
$feeRuleCode = $branchCode === 'new_capital' ? 'FOREIGN_MEMBER_FEE_CAPITAL' : 'FOREIGN_MEMBER_FEE_SHERATON';
$feeData = RuleEngine::get($feeRuleCode);
$feeUsd = $feeData['amount_usd'] ?? '10000.00';
$exchangeRate = trim($data['exchange_rate'] ?? '');
$feeEgp = null;
if ($exchangeRate !== '' && is_numeric($exchangeRate)) {
$feeEgp = bcmul($feeUsd, $exchangeRate, 2);
}
$foreign = ForeignMemberDetail::create([
'member_id' => (int) $memberId,
'passport_number' => trim($data['passport_number']),
'issuing_country_id' => (int) $data['issuing_country_id'],
'nationality_country_id' => (int) $data['nationality_country_id'],
'passport_issue_date' => $data['passport_issue_date'],
'passport_expiry_date' => $data['passport_expiry_date'],
'place_of_birth' => trim($data['place_of_birth'] ?? ''),
'fee_amount_usd' => $feeUsd,
'exchange_rate' => $exchangeRate ?: null,
'fee_amount_egp' => $feeEgp,
'status' => 'active',
'notes' => $data['notes'] ?? null,
]);
$db->update('members', ['membership_type' => 'foreign', 'updated_at' => date('Y-m-d H:i:s')], '`id` = ?', [(int) $memberId]);
EventBus::dispatch('foreign.registered', ['member_id' => (int) $memberId, 'foreign_id' => (int) $foreign->id]);
return $this->redirect("/members/{$memberId}")->withSuccess('تم تسجيل العضوية الأجنبية — الرسوم: ' . number_format((float) $feeUsd, 2) . ' USD');
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Foreign\Models;
use App\Core\Model;
use App\Core\App;
class ForeignMemberDetail extends Model
{
protected static string $table = 'foreign_member_details';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $dispatchEvents = true;
protected static array $fillable = [
'member_id', 'passport_number', 'issuing_country_id', 'nationality_country_id',
'passport_issue_date', 'passport_expiry_date', 'place_of_birth',
'fee_amount_usd', 'exchange_rate', 'fee_amount_egp',
'fee_receipt_number', 'status', 'notes',
];
public static function getForMember(int $memberId): ?array
{
$db = App::getInstance()->db();
return $db->selectOne(
"SELECT fd.*, ic.name_ar as issuing_country_name, nc.nationality_ar as nationality_name
FROM foreign_member_details fd
LEFT JOIN countries ic ON ic.id = fd.issuing_country_id
LEFT JOIN countries nc ON nc.id = fd.nationality_country_id
WHERE fd.member_id = ? AND fd.is_archived = 0",
[$memberId]
);
}
public static function getAllActive(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT fd.*, m.full_name_ar as member_name, m.membership_number,
ic.name_ar as issuing_country_name, nc.nationality_ar as nationality_name
FROM foreign_member_details fd
JOIN members m ON m.id = fd.member_id
LEFT JOIN countries ic ON ic.id = fd.issuing_country_id
LEFT JOIN countries nc ON nc.id = fd.nationality_country_id
WHERE fd.is_archived = 0 AND fd.status = 'active'
ORDER BY fd.passport_expiry_date ASC"
);
}
public function isPassportExpiringSoon(int $monthsBefore = 6): bool
{
if (!$this->passport_expiry_date) return false;
$expiryTs = strtotime($this->passport_expiry_date);
$warnTs = time() + ($monthsBefore * 30 * 86400);
return $expiryTs <= $warnTs;
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/foreign', 'Foreign\Controllers\ForeignController@index', ['auth'], 'member.view'],
['GET', '/members/{memberId}/foreign/create', 'Foreign\Controllers\ForeignController@create', ['auth'], 'member.create'],
['POST', '/members/{memberId}/foreign', 'Foreign\Controllers\ForeignController@store', ['auth'], 'member.create'],
];
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>عضوية أجنبية — <?= e($member['full_name_ar']) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/members/<?= (int) $member['id'] ?>/foreign">
<?= csrf_field() ?>
<div class="card" style="padding:20px;margin-bottom:20px;">
<div style="padding:15px;background:#EFF6FF;border:1px solid #BFDBFE;border-radius:8px;margin-bottom:20px;">
<strong style="color:#0284C7;">العضوية الأجنبية:</strong> تشمل الزوج/الزوجة + 3 أبناء — نفس حقوق العضو العامل<br>
<strong>الرسوم:</strong> <span style="font-size:18px;font-weight:700;color:#0D7377;"><?= number_format((float) $fee_usd, 2) ?> USD</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group"><label class="form-label">رقم جواز السفر <span style="color:#DC2626;">*</span></label><input type="text" name="passport_number" value="<?= e(old('passport_number')) ?>" class="form-input" required style="direction:ltr;text-align:left;"></div>
<div class="form-group">
<label class="form-label">بلد الإصدار <span style="color:#DC2626;">*</span></label>
<select name="issuing_country_id" class="form-select" required>
<option value="">-- اختر --</option>
<?php foreach ($countries as $c): ?><option value="<?= (int) $c['id'] ?>"><?= e($c['name_ar']) ?></option><?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الجنسية <span style="color:#DC2626;">*</span></label>
<select name="nationality_country_id" class="form-select" required>
<option value="">-- اختر --</option>
<?php foreach ($countries as $c): ?><option value="<?= (int) $c['id'] ?>"><?= e($c['nationality_ar']) ?></option><?php endforeach; ?>
</select>
</div>
<div class="form-group"><label class="form-label">محل الميلاد</label><input type="text" name="place_of_birth" value="<?= e(old('place_of_birth')) ?>" class="form-input"></div>
<div class="form-group"><label class="form-label">تاريخ إصدار الجواز <span style="color:#DC2626;">*</span></label><input type="date" name="passport_issue_date" value="<?= e(old('passport_issue_date')) ?>" class="form-input" required></div>
<div class="form-group"><label class="form-label">تاريخ انتهاء الجواز <span style="color:#DC2626;">*</span></label><input type="date" name="passport_expiry_date" value="<?= e(old('passport_expiry_date')) ?>" class="form-input" required><small style="color:#6B7280;">يجب أن يكون صالحاً لمدة 6 أشهر على الأقل</small></div>
<div class="form-group"><label class="form-label">سعر الصرف (USD → EGP)</label><input type="number" name="exchange_rate" step="0.0001" value="<?= e(old('exchange_rate')) ?>" class="form-input" style="direction:ltr;text-align:left;" placeholder="مثال: 50.00"></div>
<div class="form-group" style="grid-column:1/-1;"><label class="form-label">ملاحظات</label><textarea name="notes" class="form-textarea" rows="2"><?= e(old('notes')) ?></textarea></div>
</div>
</div>
<button type="submit" class="btn btn-primary">تسجيل العضوية الأجنبية</button>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>الأعضاء الأجانب<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead><tr><th>العضو</th><th>جواز السفر</th><th>الجنسية</th><th>انتهاء الجواز</th><th>الرسوم (USD)</th><th>الحالة</th></tr></thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><a href="/members/<?= (int) $r['member_id'] ?>" style="color:#0D7377;font-weight:600;"><?= e($r['member_name']) ?></a></td>
<td style="direction:ltr;text-align:right;"><?= e($r['passport_number']) ?></td>
<td><?= e($r['nationality_name'] ?? '—') ?></td>
<td style="font-size:13px;<?php
$expiry = $r['passport_expiry_date'] ?? '';
$sixMonths = date('Y-m-d', strtotime('+6 months'));
echo ($expiry && $expiry <= $sixMonths) ? 'color:#DC2626;font-weight:600;' : '';
?>"><?= e($expiry) ?></td>
<td style="font-weight:600;direction:ltr;text-align:right;"><?= number_format((float) ($r['fee_amount_usd'] ?? 0), 2) ?> $</td>
<td><span style="color:#059669;font-weight:600;">● نشط</span></td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?><tr><td colspan="6" style="text-align:center;padding:40px;color:#6B7280;">لا يوجد أعضاء أجانب</td></tr><?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
MenuRegistry::register('foreign', [
'label_ar' => 'الأعضاء الأجانب',
'label_en' => 'Foreign Members',
'icon' => 'globe',
'route' => '/foreign',
'permission' => 'member.view',
'parent' => null,
'order' => 400,
'children' => [],
]);
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Honorary\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Honorary\Models\HonoraryMember;
use App\Modules\Rules\Services\RuleEngine;
class HonoraryController extends Controller
{
public function index(Request $request): Response
{
$rows = HonoraryMember::getAllActive();
return $this->view('Honorary.Views.index', ['rows' => $rows]);
}
public function create(Request $request, string $memberId): Response
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]);
if (!$member) return $this->redirect('/members')->withError('العضو غير موجود');
$durationData = RuleEngine::get('HONORARY_DURATION_YEARS');
$years = $durationData['years'] ?? 1;
return $this->view('Honorary.Views.create', ['member' => $member, 'duration_years' => $years]);
}
public function store(Request $request, string $memberId): Response
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]);
if (!$member) return $this->redirect('/members')->withError('العضو غير موجود');
$data = $request->all();
unset($data['_csrf_token']);
$reason = trim($data['reason'] ?? '');
if ($reason === '') {
return $this->redirect("/members/{$memberId}/honorary/create")->withError('سبب منح العضوية الشرفية مطلوب');
}
$durationData = RuleEngine::get('HONORARY_DURATION_YEARS');
$years = $durationData['years'] ?? 1;
$startDate = $data['start_date'] ?? date('Y-m-d');
$endDate = date('Y-m-d', strtotime($startDate . " +{$years} years"));
$renewableData = RuleEngine::get('HONORARY_RENEWABLE');
$renewable = $renewableData['renewable'] ?? true;
$honorary = HonoraryMember::create([
'member_id' => (int) $memberId,
'granted_by_decision' => trim($data['granted_by_decision'] ?? ''),
'reason' => $reason,
'start_date' => $startDate,
'end_date' => $endDate,
'is_renewable' => $renewable ? 1 : 0,
'status' => 'active',
'notes' => $data['notes'] ?? null,
]);
// Update member type
$db->update('members', ['membership_type' => 'honorary', 'updated_at' => date('Y-m-d H:i:s')], '`id` = ?', [(int) $memberId]);
EventBus::dispatch('honorary.created', ['member_id' => (int) $memberId, 'honorary_id' => (int) $honorary->id]);
return $this->redirect("/members/{$memberId}")->withSuccess("تم منح العضوية الشرفية — من {$startDate} إلى {$endDate}");
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Honorary\Models;
use App\Core\Model;
use App\Core\App;
class HonoraryMember extends Model
{
protected static string $table = 'honorary_members';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $dispatchEvents = true;
protected static array $fillable = [
'member_id', 'granted_by_decision', 'reason', 'start_date', 'end_date',
'is_renewable', 'renewal_count', 'last_renewed_at', 'status', 'notes',
];
public static function getForMember(int $memberId): ?array
{
$db = App::getInstance()->db();
return $db->selectOne(
"SELECT * FROM honorary_members WHERE member_id = ? AND is_archived = 0 ORDER BY start_date DESC LIMIT 1",
[$memberId]
);
}
public static function getAllActive(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT h.*, m.full_name_ar as member_name, m.membership_number
FROM honorary_members h
JOIN members m ON m.id = h.member_id
WHERE h.is_archived = 0 AND h.status = 'active'
ORDER BY h.end_date ASC"
);
}
public function isExpiring(int $daysBefore = 30): bool
{
if (!$this->end_date) return false;
$endTs = strtotime($this->end_date);
$warnTs = time() + ($daysBefore * 86400);
return $endTs <= $warnTs && $endTs >= time();
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/honorary', 'Honorary\Controllers\HonoraryController@index', ['auth'], 'member.view'],
['GET', '/members/{memberId}/honorary/create', 'Honorary\Controllers\HonoraryController@create', ['auth'], 'member.change_status'],
['POST', '/members/{memberId}/honorary', 'Honorary\Controllers\HonoraryController@store', ['auth'], 'member.change_status'],
];
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>عضوية شرفية — <?= e($member['full_name_ar']) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/members/<?= (int) $member['id'] ?>/honorary">
<?= csrf_field() ?>
<div class="card" style="padding:20px;margin-bottom:20px;">
<div style="padding:15px;background:#F0FDF4;border:1px solid #BBF7D0;border-radius:8px;margin-bottom:20px;">
<strong style="color:#059669;">العضوية الشرفية:</strong> بدون رسوم — بدون اشتراكات — مدة <?= (int) $duration_years ?> سنة — بقرار مجلس أمناء
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group"><label class="form-label">تاريخ البداية <span style="color:#DC2626;">*</span></label><input type="date" name="start_date" value="<?= e(date('Y-m-d')) ?>" class="form-input" required></div>
<div class="form-group"><label class="form-label">رقم/مرجع قرار المجلس</label><input type="text" name="granted_by_decision" class="form-input"></div>
<div class="form-group" style="grid-column:1/-1;"><label class="form-label">سبب المنح <span style="color:#DC2626;">*</span></label><textarea name="reason" class="form-textarea" rows="3" required placeholder="خدمات متميزة للدولة أو المنشأة..."></textarea></div>
<div class="form-group" style="grid-column:1/-1;"><label class="form-label">ملاحظات</label><textarea name="notes" class="form-textarea" rows="2"></textarea></div>
</div>
</div>
<button type="submit" class="btn btn-primary">منح العضوية الشرفية</button>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>العضوية الشرفية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<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></tr></thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><a href="/members/<?= (int) $r['member_id'] ?>" style="color:#0D7377;font-weight:600;"><?= e($r['member_name']) ?></a></td>
<td style="font-size:13px;max-width:300px;overflow:hidden;text-overflow:ellipsis;"><?= e(mb_substr($r['reason'], 0, 100)) ?></td>
<td style="font-size:13px;"><?= e($r['start_date']) ?></td>
<td style="font-size:13px;<?= $r['end_date'] <= date('Y-m-d', strtotime('+30 days')) ? 'color:#DC2626;font-weight:600;' : '' ?>"><?= e($r['end_date']) ?></td>
<td><?= $r['is_renewable'] ? 'نعم' : 'لا' ?></td>
<td><span style="color:#059669;font-weight:600;">● نشط</span></td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?><tr><td colspan="6" style="text-align:center;padding:40px;color:#6B7280;">لا توجد عضويات شرفية</td></tr><?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
MenuRegistry::register('honorary', [
'label_ar' => 'العضوية الشرفية',
'label_en' => 'Honorary Membership',
'icon' => 'star',
'route' => '/honorary',
'permission' => 'member.view',
'parent' => null,
'order' => 395,
'children' => [],
]);
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Seasonal\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Seasonal\Models\SeasonalMembership;
use App\Modules\Rules\Services\RuleEngine;
class SeasonalController extends Controller
{
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$status = $request->get('status', '');
$where = 's.is_archived = 0';
$params = [];
if ($status !== '') {
$where .= ' AND s.status = ?';
$params[] = $status;
}
$rows = $db->select(
"SELECT s.*, m.full_name_ar as member_name, m.membership_number, b.name_ar as branch_name
FROM seasonal_memberships s
JOIN members m ON m.id = s.member_id
JOIN branches b ON b.id = s.branch_id
WHERE {$where}
ORDER BY s.created_at DESC LIMIT 100",
$params
);
return $this->view('Seasonal.Views.index', ['rows' => $rows, 'status' => $status]);
}
public function create(Request $request, string $memberId): Response
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]);
if (!$member) {
return $this->redirect('/members')->withError('العضو غير موجود');
}
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar");
$membershipValue = $member['membership_value'] ?? '0.00';
$feeData = RuleEngine::get('SEASONAL_FEE');
$pct = $feeData['percentage'] ?? '5.00';
$fee = bcmul($membershipValue, bcdiv($pct, '100', 4), 2);
$maxMonthsData = RuleEngine::get('SEASONAL_MAX_MONTHS');
$maxMonths = $maxMonthsData['months'] ?? 6;
return $this->view('Seasonal.Views.create', [
'member' => $member,
'branches' => $branches,
'fee' => $fee,
'pct' => $pct,
'maxMonths' => $maxMonths,
]);
}
public function store(Request $request, string $memberId): Response
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]);
if (!$member) {
return $this->redirect('/members')->withError('العضو غير موجود');
}
$data = $request->all();
unset($data['_csrf_token']);
$errors = [];
if (empty($data['branch_id'] ?? '')) $errors[] = 'الفرع مطلوب';
if (empty($data['start_date'] ?? '')) $errors[] = 'تاريخ البداية مطلوب';
$maxMonthsData = RuleEngine::get('SEASONAL_MAX_MONTHS');
$maxMonths = $maxMonthsData['months'] ?? 6;
$startDate = $data['start_date'] ?? date('Y-m-d');
$endDate = date('Y-m-d', strtotime($startDate . " +{$maxMonths} months"));
$membershipValue = $member['membership_value'] ?? '0.00';
$feeData = RuleEngine::get('SEASONAL_FEE');
$pct = $feeData['percentage'] ?? '5.00';
$fee = bcmul($membershipValue, bcdiv($pct, '100', 4), 2);
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $data);
return $this->redirect("/members/{$memberId}/seasonal/create");
}
$seasonal = SeasonalMembership::create([
'member_id' => (int) $memberId,
'branch_id' => (int) $data['branch_id'],
'start_date' => $startDate,
'end_date' => $endDate,
'fee_amount' => $fee,
'status' => 'active',
'carnet_marking' => 'موسمي',
'notes' => $data['notes'] ?? null,
]);
EventBus::dispatch('seasonal.created', [
'member_id' => (int) $memberId,
'seasonal_id' => (int) $seasonal->id,
'fee' => $fee,
]);
return $this->redirect("/members/{$memberId}")
->withSuccess("تم إنشاء العضوية الموسمية — من {$startDate} إلى {$endDate} — الرسوم: " . money($fee));
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Seasonal\Models;
use App\Core\Model;
use App\Core\App;
class SeasonalMembership extends Model
{
protected static string $table = 'seasonal_memberships';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $dispatchEvents = true;
protected static array $fillable = [
'member_id', 'branch_id', 'start_date', 'end_date',
'fee_amount', 'fee_receipt_number', 'status', 'carnet_marking', 'notes',
];
public static function getForMember(int $memberId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT s.*, b.name_ar as branch_name FROM seasonal_memberships s JOIN branches b ON b.id = s.branch_id WHERE s.member_id = ? AND s.is_archived = 0 ORDER BY s.start_date DESC",
[$memberId]
);
}
public static function getActive(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT s.*, m.full_name_ar as member_name, m.membership_number, b.name_ar as branch_name
FROM seasonal_memberships s
JOIN members m ON m.id = s.member_id
JOIN branches b ON b.id = s.branch_id
WHERE s.is_archived = 0 AND s.status = 'active'
ORDER BY s.end_date ASC"
);
}
public function isExpired(): bool
{
return $this->end_date && $this->end_date < date('Y-m-d');
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/seasonal', 'Seasonal\Controllers\SeasonalController@index', ['auth'], 'temp.view'],
['GET', '/members/{memberId}/seasonal/create', 'Seasonal\Controllers\SeasonalController@create', ['auth'], 'temp.add'],
['POST', '/members/{memberId}/seasonal', 'Seasonal\Controllers\SeasonalController@store', ['auth'], 'temp.add'],
];
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>عضوية موسمية — <?= e($member['full_name_ar']) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="margin-bottom:15px;padding:15px;display:flex;justify-content:space-between;align-items:center;">
<div><strong>العضو:</strong> <?= e($member['full_name_ar']) ?> &nbsp;|&nbsp; <strong>قيمة العضوية:</strong> <?= money($member['membership_value'] ?? '0') ?></div>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">← العودة</a>
</div>
<form method="POST" action="/members/<?= (int) $member['id'] ?>/seasonal">
<?= csrf_field() ?>
<div class="card" style="padding:20px;margin-bottom:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">الفرع <span style="color:#DC2626;">*</span></label>
<select name="branch_id" class="form-select" required>
<option value="">-- اختر --</option>
<?php foreach ($branches as $b): ?><option value="<?= (int) $b['id'] ?>"><?= e($b['name_ar']) ?></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" value="<?= e(date('Y-m-d')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">المدة</label>
<input type="text" value="<?= (int) $maxMonths ?> أشهر" class="form-input" disabled style="background:#F3F4F6;">
</div>
<div class="form-group">
<label class="form-label">الرسوم (<?= e($pct) ?>% من قيمة العضوية)</label>
<input type="text" value="<?= money($fee) ?>" class="form-input" disabled style="background:#F3F4F6;font-weight:700;color:#0D7377;">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-textarea" rows="2"></textarea>
</div>
</div>
<div style="margin-top:15px;padding:15px;background:#FFF7ED;border:1px solid #FED7AA;border-radius:8px;font-size:13px;">
<strong style="color:#D97706;">⚠ تنبيهات:</strong><br>
• العضوية الموسمية مدتها <?= (int) $maxMonths ?> أشهر<br>
• لا يحق للعضو الموسمي المشاركة في فرق النادي<br>
• الكارنيه مميز بعلامة خاصة<br>
• يمكن للنادي إلغاء العضوية في أي وقت للمخالفات بدون استرداد
</div>
</div>
<button type="submit" class="btn btn-primary">إنشاء العضوية الموسمية</button>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>العضويات الموسمية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/seasonal" style="display:flex;gap:10px;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select"><option value="">الكل</option><option value="active" <?= $status === 'active' ? 'selected' : '' ?>>نشط</option><option value="expired" <?= $status === 'expired' ? 'selected' : '' ?>>منتهي</option></select>
</div>
<button type="submit" class="btn btn-outline">تصفية</button>
</form>
</div>
<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></tr></thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><a href="/members/<?= (int) $r['member_id'] ?>" style="color:#0D7377;font-weight:600;"><?= e($r['member_name']) ?></a> <small style="color:#9CA3AF;"><?= e($r['membership_number'] ?? '') ?></small></td>
<td><?= e($r['branch_name']) ?></td>
<td style="font-size:13px;"><?= e($r['start_date']) ?></td>
<td style="font-size:13px;"><?= e($r['end_date']) ?></td>
<td style="font-weight:600;"><?= money($r['fee_amount'] ?? '0') ?></td>
<td><span style="color:<?= $r['status'] === 'active' ? '#059669' : '#DC2626' ?>;font-weight:600;"><?= $r['status'] === 'active' ? 'نشط' : ($r['status'] === 'expired' ? 'منتهي' : $r['status']) ?></span></td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?><tr><td colspan="6" style="text-align:center;padding:40px;color:#6B7280;">لا توجد عضويات موسمية</td></tr><?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\EventBus;
MenuRegistry::register('seasonal', [
'label_ar' => 'العضوية الموسمية',
'label_en' => 'Seasonal Memberships',
'icon' => 'calendar',
'route' => '/seasonal',
'permission' => 'temp.view',
'parent' => null,
'order' => 385,
'children' => [],
]);
EventBus::listen('member.profile_data', function (array &$data) {
if (isset($data['member']) && $data['member']) {
$memberId = is_object($data['member']) ? (int) $data['member']->id : (int) ($data['member']['id'] ?? 0);
if ($memberId > 0) {
try {
$db = \App\Core\App::getInstance()->db();
if ($db->tableExists('seasonal_memberships')) {
$data['seasonal'] = $db->select(
"SELECT s.*, b.name_ar as branch_name FROM seasonal_memberships s JOIN branches b ON b.id = s.branch_id WHERE s.member_id = ? AND s.is_archived = 0 ORDER BY s.start_date DESC",
[$memberId]
);
}
} catch (\Throwable $e) {}
}
}
});
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Sports\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Sports\Models\SportsMember;
use App\Modules\Sports\Services\SportsConversionCalculator;
class SportsController extends Controller
{
public function index(Request $request): Response
{
$rows = SportsMember::getAllActive();
return $this->view('Sports.Views.index', ['rows' => $rows]);
}
public function create(Request $request, string $memberId): Response
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]);
if (!$member) return $this->redirect('/members')->withError('العضو غير موجود');
$existing = SportsMember::getForMember((int) $memberId);
if ($existing) return $this->redirect("/members/{$memberId}")->withError('العضو لديه سجل رياضي بالفعل');
return $this->view('Sports.Views.create', ['member' => $member]);
}
public function store(Request $request, string $memberId): Response
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]);
if (!$member) return $this->redirect('/members')->withError('العضو غير موجود');
$data = $request->all();
unset($data['_csrf_token']);
$errors = [];
if (empty(trim($data['sport_name'] ?? ''))) $errors[] = 'اسم الرياضة مطلوب';
if (empty($data['years_of_service'] ?? '')) $errors[] = 'عدد سنوات الخدمة مطلوب';
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $data);
return $this->redirect("/members/{$memberId}/sports/create");
}
$yearsOfService = (int) ($data['years_of_service'] ?? 0);
$minYearsData = \App\Modules\Rules\Services\RuleEngine::get('SPORTS_MIN_YEARS');
$minYears = $minYearsData['years'] ?? 8;
$sports = SportsMember::create([
'member_id' => (int) $memberId,
'sport_name' => trim($data['sport_name']),
'federation_name' => trim($data['federation_name'] ?? ''),
'federation_registration' => trim($data['federation_registration'] ?? ''),
'registration_date' => $data['registration_date'] ?: null,
'years_of_service' => $yearsOfService,
'highest_competitive_level' => trim($data['highest_competitive_level'] ?? ''),
'is_conversion_eligible' => $yearsOfService >= $minYears ? 1 : 0,
'status' => 'active',
'notes' => $data['notes'] ?? null,
]);
EventBus::dispatch('sports.registered', ['member_id' => (int) $memberId, 'sports_id' => (int) $sports->id]);
return $this->redirect("/members/{$memberId}")->withSuccess('تم تسجيل العضوية الرياضية' . ($yearsOfService >= $minYears ? ' — مؤهل للتحويل لعضو عامل' : ''));
}
public function checkConversion(Request $request, string $memberId): Response
{
$result = SportsConversionCalculator::checkEligibility((int) $memberId);
return $this->json($result);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Sports\Models;
use App\Core\Model;
use App\Core\App;
class SportsMember extends Model
{
protected static string $table = 'sports_members';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $dispatchEvents = true;
protected static array $fillable = [
'member_id', 'sport_name', 'federation_name', 'federation_registration',
'registration_date', 'years_of_service', 'highest_competitive_level',
'is_conversion_eligible', 'conversion_requested', 'conversion_date',
'conversion_fee', 'conversion_receipt', 'status', 'notes',
];
public static function getForMember(int $memberId): ?array
{
$db = App::getInstance()->db();
return $db->selectOne(
"SELECT * FROM sports_members WHERE member_id = ? AND is_archived = 0",
[$memberId]
);
}
public static function getAllActive(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT s.*, m.full_name_ar as member_name, m.membership_number
FROM sports_members s
JOIN members m ON m.id = s.member_id
WHERE s.is_archived = 0 AND s.status = 'active'
ORDER BY s.years_of_service DESC"
);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/sports', 'Sports\Controllers\SportsController@index', ['auth'], 'temp.view'],
['GET', '/members/{memberId}/sports/create', 'Sports\Controllers\SportsController@create', ['auth'], 'temp.add'],
['POST', '/members/{memberId}/sports', 'Sports\Controllers\SportsController@store', ['auth'], 'temp.add'],
['POST', '/members/{memberId}/sports/check-conversion', 'Sports\Controllers\SportsController@checkConversion', ['auth'], 'temp.view'],
];
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Sports\Services;
use App\Core\App;
use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Pricing\Services\PricingEngine;
final class SportsConversionCalculator
{
public static function checkEligibility(int $memberId): array
{
$db = App::getInstance()->db();
$sports = $db->selectOne("SELECT * FROM sports_members WHERE member_id = ? AND is_archived = 0", [$memberId]);
if (!$sports) {
return ['eligible' => false, 'error' => 'لا يوجد سجل رياضي'];
}
$minYearsData = RuleEngine::get('SPORTS_MIN_YEARS');
$minYears = $minYearsData['years'] ?? 8;
if ((int) $sports['years_of_service'] < $minYears) {
return [
'eligible' => false,
'years' => (int) $sports['years_of_service'],
'min_years' => $minYears,
'remaining' => $minYears - (int) $sports['years_of_service'],
'error' => "الحد الأدنى {$minYears} سنوات خدمة. لديك " . $sports['years_of_service'] . " سنوات",
];
}
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$member) {
return ['eligible' => false, 'error' => 'العضو غير موجود'];
}
$qualCode = 'high';
if ($member['qualification_id']) {
$qual = $db->selectOne("SELECT code FROM qualifications WHERE id = ?", [(int) $member['qualification_id']]);
$qualCode = $qual['code'] ?? 'high';
}
$priceInfo = PricingEngine::getMembershipPrice((int) $member['branch_id'], $qualCode);
$newValue = $priceInfo['price'] ?? '0.00';
$feeData = RuleEngine::get('SPORTS_CONVERSION_FEE');
$pct = $feeData['percentage'] ?? '50.00';
$fee = bcmul($newValue, bcdiv($pct, '100', 4), 2);
return [
'eligible' => true,
'years' => (int) $sports['years_of_service'],
'min_years' => $minYears,
'new_membership_value' => $newValue,
'fee_percentage' => $pct,
'conversion_fee' => $fee,
'error' => null,
];
}
}
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تسجيل رياضي — <?= e($member['full_name_ar']) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/members/<?= (int) $member['id'] ?>/sports">
<?= csrf_field() ?>
<div class="card" style="padding:20px;margin-bottom:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group"><label class="form-label">اسم الرياضة <span style="color:#DC2626;">*</span></label><input type="text" name="sport_name" value="<?= e(old('sport_name')) ?>" class="form-input" required></div>
<div class="form-group"><label class="form-label">اسم الاتحاد</label><input type="text" name="federation_name" value="<?= e(old('federation_name')) ?>" class="form-input"></div>
<div class="form-group"><label class="form-label">رقم تسجيل الاتحاد</label><input type="text" name="federation_registration" value="<?= e(old('federation_registration')) ?>" class="form-input"></div>
<div class="form-group"><label class="form-label">تاريخ التسجيل</label><input type="date" name="registration_date" value="<?= e(old('registration_date')) ?>" class="form-input"></div>
<div class="form-group"><label class="form-label">سنوات الخدمة <span style="color:#DC2626;">*</span></label><input type="number" name="years_of_service" value="<?= e(old('years_of_service')) ?>" class="form-input" min="0" max="50" required><small style="color:#6B7280;">الحد الأدنى للتحويل لعضو عامل: 8 سنوات</small></div>
<div class="form-group"><label class="form-label">أعلى مستوى تنافسي</label><input type="text" name="highest_competitive_level" value="<?= e(old('highest_competitive_level')) ?>" class="form-input"></div>
<div class="form-group" style="grid-column:1/-1;"><label class="form-label">ملاحظات</label><textarea name="notes" class="form-textarea" rows="2"><?= e(old('notes')) ?></textarea></div>
</div>
</div>
<button type="submit" class="btn btn-primary">تسجيل العضوية الرياضية</button>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>العضوية الرياضية<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<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></tr></thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><a href="/members/<?= (int) $r['member_id'] ?>" style="color:#0D7377;font-weight:600;"><?= e($r['member_name']) ?></a></td>
<td><?= e($r['sport_name']) ?></td>
<td style="font-size:13px;"><?= e($r['federation_name'] ?? '—') ?></td>
<td style="text-align:center;font-weight:600;"><?= (int) $r['years_of_service'] ?></td>
<td><?= $r['is_conversion_eligible'] ? '<span style="color:#059669;font-weight:600;">✓ نعم</span>' : '<span style="color:#6B7280;">لا</span>' ?></td>
<td><span style="color:#059669;font-weight:600;">● نشط</span></td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?><tr><td colspan="6" style="text-align:center;padding:40px;color:#6B7280;">لا يوجد أعضاء رياضيون</td></tr><?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
MenuRegistry::register('sports', [
'label_ar' => 'العضوية الرياضية',
'label_en' => 'Sports Membership',
'icon' => 'trophy',
'route' => '/sports',
'permission' => 'temp.view',
'parent' => null,
'order' => 390,
'children' => [],
]);
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Temporary\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Temporary\Models\TemporaryMember;
use App\Modules\Temporary\Services\TemporaryFeeCalculator;
use App\Modules\Members\Services\NationalIdParser;
class TemporaryController extends Controller
{
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$filters = [
'category' => $request->get('category', ''),
'status' => $request->get('status', ''),
'q' => trim((string) $request->get('q', '')),
];
$where = 't.is_archived = 0';
$params = [];
if (!empty($filters['category'])) {
$where .= ' AND t.category = ?';
$params[] = $filters['category'];
}
if (!empty($filters['status'])) {
$where .= ' AND t.status = ?';
$params[] = $filters['status'];
}
if (!empty($filters['q'])) {
$where .= ' AND (t.full_name_ar LIKE ? OR t.national_id LIKE ? OR m.full_name_ar LIKE ?)';
$s = '%' . $filters['q'] . '%';
$params = array_merge($params, [$s, $s, $s]);
}
$rows = $db->select(
"SELECT t.*, m.full_name_ar as member_name, m.membership_number
FROM temporary_members t
JOIN members m ON m.id = t.member_id
WHERE {$where}
ORDER BY t.created_at DESC
LIMIT 100",
$params
);
return $this->view('Temporary.Views.index', [
'rows' => $rows,
'filters' => $filters,
'categories' => TemporaryMember::getCategories(),
]);
}
public function create(Request $request, string $memberId): Response
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]);
if (!$member) {
return $this->redirect('/members')->withError('العضو غير موجود');
}
return $this->view('Temporary.Views.create', [
'member' => $member,
'categories' => TemporaryMember::getCategories(),
]);
}
public function store(Request $request, string $memberId): Response
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]);
if (!$member) {
return $this->redirect('/members')->withError('العضو غير موجود');
}
$data = $request->all();
unset($data['_csrf_token']);
$errors = [];
if (empty(trim($data['full_name_ar'] ?? ''))) $errors[] = 'الاسم مطلوب';
if (empty($data['category'] ?? '')) $errors[] = 'الفئة مطلوبة';
if (empty($data['date_of_birth'] ?? '')) $errors[] = 'تاريخ الميلاد مطلوب';
if (empty($data['gender'] ?? '')) $errors[] = 'النوع مطلوب';
$nid = trim($data['national_id'] ?? '');
if ($nid !== '') {
$parsed = NationalIdParser::parse($nid);
if (!$parsed['is_valid']) {
$errors = array_merge($errors, $parsed['errors']);
} else {
$data['date_of_birth'] = $parsed['dob'];
$data['age_years'] = $parsed['age_years'];
$data['age_months'] = $parsed['age_months'];
$data['gender'] = $parsed['gender'];
}
if ($nid === ($member['national_id'] ?? '')) {
$errors[] = 'لا يمكن إضافة العضو نفسه';
}
$dup = TemporaryMember::nidExistsElsewhere($nid);
if ($dup) {
$errors[] = 'الرقم القومي مسجل بالفعل: ' . ($dup['data']['full_name_ar'] ?? '');
}
}
if (!empty($data['date_of_birth']) && empty($data['age_years'])) {
$age = age_from_dob($data['date_of_birth']);
$data['age_years'] = $age['years'];
$data['age_months'] = $age['months'];
}
$category = $data['category'] ?? '';
$catErrors = TemporaryFeeCalculator::validateCategory($category, $data, $member);
$errors = array_merge($errors, $catErrors);
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $data);
return $this->redirect("/members/{$memberId}/temporary/create");
}
$feeCalc = TemporaryFeeCalculator::calculate((int) $memberId, $data);
$temp = TemporaryMember::create([
'member_id' => (int) $memberId,
'category' => $category,
'full_name_ar' => trim($data['full_name_ar']),
'full_name_en' => trim($data['full_name_en'] ?? ''),
'national_id' => $nid ?: null,
'passport_number' => $data['passport_number'] ?? null,
'date_of_birth' => $data['date_of_birth'],
'age_years' => (int) ($data['age_years'] ?? 0),
'age_months' => (int) ($data['age_months'] ?? 0),
'gender' => $data['gender'],
'nationality' => $data['nationality'] ?? 'مصري',
'relationship_to_member' => $data['relationship_to_member'] ?? null,
'has_championship' => !empty($data['has_championship']) ? 1 : 0,
'disability_documentation' => !empty($data['disability_documentation']) ? 1 : 0,
'addition_fee' => $feeCalc['fee'] ?? '0.00',
'can_separate' => TemporaryFeeCalculator::canSeparate($category) ? 1 : 0,
'can_get_independent' => TemporaryFeeCalculator::canGetIndependent($category) ? 1 : 0,
'status' => 'active',
'notes' => $data['notes'] ?? null,
]);
EventBus::dispatch('temporary.added', [
'member_id' => (int) $memberId,
'temporary_id' => (int) $temp->id,
'category' => $category,
'fee' => $feeCalc['fee'] ?? '0.00',
]);
return $this->redirect("/members/{$memberId}")
->withSuccess('تم إضافة العضو المؤقت — الرسوم: ' . money($feeCalc['fee'] ?? '0.00'));
}
public function show(Request $request, string $memberId, string $id): Response
{
$temp = TemporaryMember::find((int) $id);
if (!$temp || (int) $temp->member_id !== (int) $memberId) {
return $this->redirect("/members/{$memberId}")->withError('العضو المؤقت غير موجود');
}
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $memberId]);
return $this->view('Temporary.Views.show', ['member' => $member, 'temp' => $temp]);
}
public function archive(Request $request, string $memberId, string $id): Response
{
$temp = TemporaryMember::find((int) $id);
if (!$temp || (int) $temp->member_id !== (int) $memberId) {
return $this->redirect("/members/{$memberId}")->withError('العضو المؤقت غير موجود');
}
$reason = trim((string) $request->post('reason', ''));
if ($reason === '') {
return $this->redirect("/members/{$memberId}")->withError('يجب إدخال سبب الإزالة');
}
$employee = App::getInstance()->currentEmployee();
$db = App::getInstance()->db();
$db->update('temporary_members', [
'is_archived' => 1,
'archived_at' => date('Y-m-d H:i:s'),
'archived_by' => $employee ? (int) $employee->id : null,
'status' => 'inactive',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
EventBus::dispatch('temporary.removed', [
'member_id' => (int) $memberId,
'temporary_id' => (int) $id,
'reason' => $reason,
]);
return $this->redirect("/members/{$memberId}")->withSuccess('تم إزالة العضو المؤقت');
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Temporary\Models;
use App\Core\Model;
use App\Core\App;
class TemporaryMember extends Model
{
protected static string $table = 'temporary_members';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $dispatchEvents = true;
protected static array $fillable = [
'member_id', 'category', 'full_name_ar', 'full_name_en',
'national_id', 'passport_number', 'date_of_birth',
'age_years', 'age_months', 'gender', 'nationality',
'relationship_to_member', 'has_championship', 'disability_documentation',
'addition_fee', 'fee_receipt_number', 'can_separate', 'can_get_independent',
'status', 'photo_path', 'notes',
];
public static function getForMember(int $memberId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM temporary_members WHERE member_id = ? AND is_archived = 0 ORDER BY created_at ASC",
[$memberId]
);
}
public static function countActiveForMember(int $memberId): int
{
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT COUNT(*) as cnt FROM temporary_members WHERE member_id = ? AND is_archived = 0 AND status = 'active'",
[$memberId]
);
return (int) ($row['cnt'] ?? 0);
}
public static function getCategories(): array
{
return [
'parent' => 'والدين العضو العامل',
'special_needs' => 'أبناء ذوي الاحتياجات الخاصة',
'unmarried_daughter' => 'بنات العضو غير المتزوجات',
'sister' => 'شقيقة العضو العامل',
'stepchild' => 'أبناء الزوج/الزوجة',
'orphan' => 'الطفل اليتيم',
'disabled_sibling' => 'شقيق العضو المعاق',
'nanny' => 'المربية',
];
}
public function getCategoryLabel(): string
{
$cats = self::getCategories();
return $cats[$this->category] ?? $this->category;
}
public function getStatusLabel(): string
{
return match ($this->status) {
'active' => 'نشط',
'inactive' => 'غير نشط',
'expired' => 'منتهي',
default => $this->status,
};
}
public static function nidExistsElsewhere(string $nid, ?int $excludeId = null): ?array
{
$db = App::getInstance()->db();
$memberDup = $db->selectOne("SELECT id, full_name_ar FROM members WHERE national_id = ? AND is_archived = 0", [$nid]);
if ($memberDup) return ['type' => 'member', 'data' => $memberDup];
$spouseDup = $db->selectOne("SELECT s.id, s.full_name_ar FROM spouses s WHERE s.national_id = ? AND s.is_archived = 0", [$nid]);
if ($spouseDup) return ['type' => 'spouse', 'data' => $spouseDup];
$childDup = $db->selectOne("SELECT c.id, c.full_name_ar FROM children c WHERE c.national_id = ? AND c.is_archived = 0", [$nid]);
if ($childDup) return ['type' => 'child', 'data' => $childDup];
$sql = "SELECT t.id, t.full_name_ar FROM temporary_members t WHERE t.national_id = ? AND t.is_archived = 0";
$params = [$nid];
if ($excludeId) {
$sql .= " AND t.id != ?";
$params[] = $excludeId;
}
$tempDup = $db->selectOne($sql, $params);
if ($tempDup) return ['type' => 'temporary', 'data' => $tempDup];
return null;
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/temporary', 'Temporary\Controllers\TemporaryController@index', ['auth'], 'temp.view'],
['GET', '/members/{memberId}/temporary/create', 'Temporary\Controllers\TemporaryController@create', ['auth'], 'temp.add'],
['POST', '/members/{memberId}/temporary', 'Temporary\Controllers\TemporaryController@store', ['auth'], 'temp.add'],
['GET', '/members/{memberId}/temporary/{id}', 'Temporary\Controllers\TemporaryController@show', ['auth'], 'temp.view'],
['POST', '/members/{memberId}/temporary/{id}/archive', 'Temporary\Controllers\TemporaryController@archive', ['auth'], 'temp.remove'],
];
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Temporary\Services;
use App\Core\App;
use App\Modules\Rules\Services\RuleEngine;
final class TemporaryFeeCalculator
{
public static function calculate(int $memberId, array $tempData): array
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$member) {
return ['fee' => '0.00', 'error' => 'العضو غير موجود'];
}
$membershipValue = $member['membership_value'] ?? '0.00';
if (bccomp($membershipValue, '0.00', 2) <= 0) {
return ['fee' => '0.00', 'error' => 'قيمة العضوية غير محددة'];
}
$hasChampionship = (bool) ($tempData['has_championship'] ?? false);
$category = $tempData['category'] ?? '';
// Championship exemption
$exemptData = RuleEngine::get('TEMP_CHAMPIONSHIP_EXEMPT');
$isExempt = $hasChampionship && ($exemptData['exempt'] ?? true);
if ($isExempt) {
return [
'fee' => '0.00',
'percentage' => '0.00',
'rule_applied' => 'TEMP_CHAMPIONSHIP_EXEMPT',
'exempt' => true,
'error' => null,
];
}
$feeData = RuleEngine::get('TEMP_MEMBER_FEE');
$pct = $feeData['percentage'] ?? '10.00';
$fee = bcmul($membershipValue, bcdiv($pct, '100', 4), 2);
// Form fee if post-creation
$formFee = '0.00';
if ($member['status'] !== 'potential') {
$formFeeData = RuleEngine::get('FORM_ADDITION_FEE');
$formFee = $formFeeData['amount'] ?? '570.00';
}
return [
'membership_value' => $membershipValue,
'fee' => $fee,
'percentage' => $pct,
'form_fee' => $formFee,
'total_fee' => bcadd($fee, $formFee, 2),
'rule_applied' => 'TEMP_MEMBER_FEE',
'exempt' => false,
'error' => null,
];
}
public static function validateCategory(string $category, array $tempData, array $member): array
{
$errors = [];
$age = (int) ($tempData['age_years'] ?? 0);
switch ($category) {
case 'parent':
// No specific age limit for parents
break;
case 'special_needs':
if ($age < 21) {
$errors[] = 'أبناء ذوي الاحتياجات الخاصة يجب أن يكونوا فوق 21 سنة';
}
if (empty($tempData['disability_documentation'])) {
$errors[] = 'يجب تقديم وثائق الإعاقة';
}
break;
case 'unmarried_daughter':
if (($tempData['gender'] ?? '') !== 'female') {
$errors[] = 'هذه الفئة للإناث فقط';
}
break;
case 'sister':
if ($age >= 25) {
$errors[] = 'شقيقة العضو يجب أن تكون أقل من 25 سنة';
}
if (($tempData['gender'] ?? '') !== 'female') {
$errors[] = 'هذه الفئة للإناث فقط';
}
break;
case 'stepchild':
if ($age >= 25) {
$errors[] = 'أبناء الزوج/الزوجة يجب أن تكون أعمارهم أقل من 25 سنة';
}
break;
case 'orphan':
if ($age >= 25) {
$errors[] = 'الطفل اليتيم يجب أن يكون أقل من 25 سنة';
}
break;
case 'disabled_sibling':
if (empty($tempData['disability_documentation'])) {
$errors[] = 'يجب تقديم وثائق الإعاقة';
}
break;
case 'nanny':
// Nanny — companion, no specific age rules
break;
default:
$errors[] = 'فئة غير معروفة';
}
return $errors;
}
public static function canSeparate(string $category): bool
{
$noSeparation = ['orphan', 'disabled_sibling', 'nanny', 'parent'];
return !in_array($category, $noSeparation, true);
}
public static function canGetIndependent(string $category): bool
{
$noIndependent = ['orphan', 'disabled_sibling', 'nanny'];
return !in_array($category, $noIndependent, true);
}
}
\ No newline at end of file
<?php
$temporary = $temporary ?? [];
$memberId = $memberId ?? 0;
?>
<?php if (!empty($temporary)): ?>
<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 ($temporary as $t): ?>
<tr>
<td><a href="/members/<?= (int) $memberId ?>/temporary/<?= (int) $t['id'] ?>" style="color:#0D7377;font-weight:600;"><?= e($t['full_name_ar']) ?></a></td>
<td style="font-size:13px;"><?php $cats = \App\Modules\Temporary\Models\TemporaryMember::getCategories(); echo e($cats[$t['category']] ?? $t['category']); ?></td>
<td><?= $t['gender'] === 'male' ? 'ذكر' : 'أنثى' ?></td>
<td><?= (int) ($t['age_years'] ?? 0) ?></td>
<td style="font-weight:600;"><?= money($t['addition_fee'] ?? '0') ?></td>
<td><span style="color:<?= ($t['status'] ?? '') === 'active' ? '#059669' : '#DC2626' ?>;font-weight:600;"><?= ($t['status'] ?? '') === 'active' ? 'نشط' : ($t['status'] ?? '') ?></span></td>
<td><a href="/members/<?= (int) $memberId ?>/temporary/<?= (int) $t['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p style="color:#6B7280;text-align:center;padding:20px;">لا يوجد أعضاء مؤقتون</p>
<?php endif; ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إضافة عضو مؤقت — <?= e($member['full_name_ar']) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="margin-bottom:15px;padding:15px;display:flex;justify-content:space-between;align-items:center;">
<div><strong>العضو:</strong> <?= e($member['full_name_ar']) ?> &nbsp;|&nbsp; <strong>رقم العضوية:</strong> <?= e($member['membership_number'] ?? 'لم يُحدد') ?></div>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">← العودة للعضو</a>
</div>
<form method="POST" action="/members/<?= (int) $member['id'] ?>/temporary">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;"><h3 style="margin:0;color:#0D7377;">بيانات العضو المؤقت</h3></div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">الاسم بالكامل (عربي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="full_name_ar" value="<?= e(old('full_name_ar')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="full_name_en" value="<?= e(old('full_name_en')) ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">الفئة <span style="color:#DC2626;">*</span></label>
<select name="category" class="form-select" required>
<option value="">-- اختر --</option>
<?php foreach ($categories as $code => $label): ?>
<option value="<?= e($code) ?>" <?= old('category') === $code ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الرقم القومي</label>
<input type="text" name="national_id" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label>
<input type="date" name="date_of_birth" value="<?= e(old('date_of_birth')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<select name="gender" class="form-select" required>
<option value="">-- اختر --</option>
<option value="male" <?= old('gender') === 'male' ? 'selected' : '' ?>>ذكر</option>
<option value="female" <?= old('gender') === 'female' ? 'selected' : '' ?>>أنثى</option>
</select>
</div>
<div class="form-group">
<label class="form-label">صلة القرابة</label>
<input type="text" name="relationship_to_member" value="<?= e(old('relationship_to_member')) ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">&nbsp;</label>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="checkbox" name="has_championship" value="1" <?= old('has_championship') ? 'checked' : '' ?>>
<span>حاصل على بطولات جمهورية (إعفاء من الرسوم)</span>
</label>
</div>
<div class="form-group">
<label class="form-label">&nbsp;</label>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="checkbox" name="disability_documentation" value="1" <?= old('disability_documentation') ? 'checked' : '' ?>>
<span>لديه وثائق إعاقة</span>
</label>
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-textarea" rows="2"><?= e(old('notes')) ?></textarea>
</div>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary">إضافة العضو المؤقت</button>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
</div>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>الأعضاء المؤقتون<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/temporary" style="display:flex;gap:10px;flex-wrap:wrap;align-items:end;">
<div>
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="الاسم، الرقم القومي..." class="form-input" style="min-width:200px;">
</div>
<div>
<label class="form-label" style="font-size:12px;">الفئة</label>
<select name="category" class="form-select" style="min-width:150px;">
<option value="">الكل</option>
<?php foreach ($categories as $code => $label): ?>
<option value="<?= e($code) ?>" <?= ($filters['category'] ?? '') === $code ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select">
<option value="">الكل</option>
<option value="active" <?= ($filters['status'] ?? '') === 'active' ? 'selected' : '' ?>>نشط</option>
<option value="inactive" <?= ($filters['status'] ?? '') === 'inactive' ? 'selected' : '' ?>>غير نشط</option>
</select>
</div>
<button type="submit" class="btn btn-outline">بحث</button>
<a href="/temporary" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<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></tr></thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td style="font-weight:600;"><?= e($r['full_name_ar']) ?></td>
<td><a href="/members/<?= (int) $r['member_id'] ?>" style="color:#0D7377;"><?= e($r['member_name']) ?></a> <small style="color:#9CA3AF;"><?= e($r['membership_number'] ?? '') ?></small></td>
<td style="font-size:13px;"><?php $cats = \App\Modules\Temporary\Models\TemporaryMember::getCategories(); echo e($cats[$r['category']] ?? $r['category']); ?></td>
<td><?= $r['gender'] === 'male' ? 'ذكر' : 'أنثى' ?></td>
<td><?= (int) ($r['age_years'] ?? 0) ?></td>
<td style="font-weight:600;"><?= money($r['addition_fee'] ?? '0') ?></td>
<td><span style="color:<?= $r['status'] === 'active' ? '#059669' : '#DC2626' ?>;font-weight:600;"><?= $r['status'] === 'active' ? 'نشط' : 'غير نشط' ?></span></td>
<td><a href="/members/<?= (int) $r['member_id'] ?>/temporary/<?= (int) $r['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?>
<tr><td colspan="8" style="text-align:center;padding:40px;color:#6B7280;">لا يوجد أعضاء مؤقتون</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>عضو مؤقت: <?= e($temp->full_name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">← العودة للعضو</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="card" style="padding:20px;">
<h4 style="color:#0D7377;margin-bottom:15px;">البيانات الشخصية</h4>
<table style="width:100%;font-size:14px;">
<tr><td style="padding:6px 0;color:#6B7280;width:40%;">الاسم</td><td style="font-weight:600;"><?= e($temp->full_name_ar) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الفئة</td><td><?= e($temp->getCategoryLabel()) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الرقم القومي</td><td style="direction:ltr;text-align:right;"><?= e($temp->national_id ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ الميلاد</td><td><?= e($temp->date_of_birth) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">السن</td><td><?= (int) $temp->age_years ?> سنة</td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">النوع</td><td><?= $temp->gender === 'male' ? 'ذكر' : 'أنثى' ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">صلة القرابة</td><td><?= e($temp->relationship_to_member ?: '—') ?></td></tr>
</table>
</div>
<div class="card" style="padding:20px;">
<h4 style="color:#0D7377;margin-bottom:15px;">بيانات العضوية</h4>
<table style="width:100%;font-size:14px;">
<tr><td style="padding:6px 0;color:#6B7280;width:40%;">الحالة</td><td style="color:<?= $temp->status === 'active' ? '#059669' : '#DC2626' ?>;font-weight:600;"><?= e($temp->getStatusLabel()) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">رسوم الإضافة</td><td style="font-weight:700;color:#0D7377;"><?= money($temp->addition_fee) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">رقم الإيصال</td><td><?= e($temp->fee_receipt_number ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">بطولات جمهورية</td><td><?= $temp->has_championship ? '<span style="color:#059669;">نعم (معفي)</span>' : 'لا' ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">وثائق إعاقة</td><td><?= $temp->disability_documentation ? 'نعم' : 'لا' ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">يحق له الفصل</td><td><?= $temp->can_separate ? 'نعم' : '<span style="color:#DC2626;">لا</span>' ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">يحق له الاستقلال</td><td><?= $temp->can_get_independent ? 'نعم' : '<span style="color:#DC2626;">لا</span>' ?></td></tr>
</table>
<?php if ($temp->notes): ?>
<div style="margin-top:15px;"><h4 style="color:#6B7280;font-size:13px;">ملاحظات</h4><p style="font-size:14px;"><?= nl2br(e($temp->notes)) ?></p></div>
<?php endif; ?>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
use App\Core\EventBus;
MenuRegistry::register('temporary', [
'label_ar' => 'الأعضاء المؤقتون',
'label_en' => 'Temporary Members',
'icon' => 'user-clock',
'route' => '/temporary',
'permission' => 'temp.view',
'parent' => null,
'order' => 380,
'children' => [],
]);
PermissionRegistry::register('temporary', [
'temp.add' => ['ar' => 'إضافة عضو مؤقت', 'en' => 'Add Temporary Member'],
'temp.edit' => ['ar' => 'تعديل عضو مؤقت', 'en' => 'Edit Temporary Member'],
'temp.remove' => ['ar' => 'إزالة عضو مؤقت', 'en' => 'Remove Temporary Member'],
'temp.view' => ['ar' => 'عرض الأعضاء المؤقتين', 'en' => 'View Temporary Members'],
]);
EventBus::listen('member.profile_data', function (array &$data) {
if (isset($data['member']) && $data['member']) {
$memberId = is_object($data['member']) ? (int) $data['member']->id : (int) ($data['member']['id'] ?? 0);
if ($memberId > 0) {
try {
$db = \App\Core\App::getInstance()->db();
if ($db->tableExists('temporary_members')) {
$data['temporary'] = $db->select(
"SELECT * FROM temporary_members WHERE member_id = ? AND is_archived = 0 ORDER BY created_at ASC",
[$memberId]
);
}
} catch (\Throwable $e) {
// Table may not exist yet
}
}
}
});
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `temporary_members` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`member_id` BIGINT UNSIGNED NOT NULL,
`category` VARCHAR(50) NOT NULL,
`full_name_ar` VARCHAR(200) NOT NULL,
`full_name_en` VARCHAR(200) NULL,
`national_id` VARCHAR(14) NULL,
`passport_number` VARCHAR(50) NULL,
`date_of_birth` DATE NOT NULL,
`age_years` INT UNSIGNED NULL,
`age_months` INT UNSIGNED NULL,
`gender` VARCHAR(10) NOT NULL,
`nationality` VARCHAR(100) NULL DEFAULT 'مصري',
`relationship_to_member` VARCHAR(100) NULL,
`has_championship` TINYINT(1) NOT NULL DEFAULT 0,
`disability_documentation` TINYINT(1) NOT NULL DEFAULT 0,
`addition_fee` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`fee_receipt_number` VARCHAR(50) NULL,
`can_separate` TINYINT(1) NOT NULL DEFAULT 0,
`can_get_independent` TINYINT(1) NOT NULL DEFAULT 0,
`status` VARCHAR(50) NOT NULL DEFAULT 'active',
`photo_path` VARCHAR(500) NULL,
`notes` TEXT NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT 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,
`updated_by` BIGINT UNSIGNED NULL,
INDEX `idx_temp_members_member` (`member_id`),
INDEX `idx_temp_members_category` (`category`),
INDEX `idx_temp_members_nid` (`national_id`),
INDEX `idx_temp_members_status` (`status`),
INDEX `idx_temp_members_archived` (`is_archived`),
CONSTRAINT `fk_temp_members_member` FOREIGN KEY (`member_id`) REFERENCES `members`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `temporary_members`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `seasonal_memberships` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`member_id` BIGINT UNSIGNED NOT NULL,
`branch_id` BIGINT UNSIGNED NOT NULL,
`start_date` DATE NOT NULL,
`end_date` DATE NOT NULL,
`fee_amount` DECIMAL(15,2) NOT NULL,
`fee_receipt_number` VARCHAR(50) NULL,
`status` VARCHAR(50) NOT NULL DEFAULT 'active',
`carnet_marking` VARCHAR(100) NULL,
`notes` TEXT NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT 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,
`updated_by` BIGINT UNSIGNED NULL,
INDEX `idx_seasonal_member` (`member_id`),
INDEX `idx_seasonal_status` (`status`),
INDEX `idx_seasonal_dates` (`start_date`, `end_date`),
CONSTRAINT `fk_seasonal_member` FOREIGN KEY (`member_id`) REFERENCES `members`(`id`),
CONSTRAINT `fk_seasonal_branch` FOREIGN KEY (`branch_id`) REFERENCES `branches`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `seasonal_memberships`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `sports_members` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`member_id` BIGINT UNSIGNED NOT NULL,
`sport_name` VARCHAR(200) NOT NULL,
`federation_name` VARCHAR(200) NULL,
`federation_registration` VARCHAR(100) NULL,
`registration_date` DATE NULL,
`years_of_service` INT UNSIGNED NOT NULL DEFAULT 0,
`highest_competitive_level` VARCHAR(200) NULL,
`is_conversion_eligible` TINYINT(1) NOT NULL DEFAULT 0,
`conversion_requested` TINYINT(1) NOT NULL DEFAULT 0,
`conversion_date` DATE NULL,
`conversion_fee` DECIMAL(15,2) NULL,
`conversion_receipt` VARCHAR(50) NULL,
`status` VARCHAR(50) NOT NULL DEFAULT 'active',
`notes` TEXT NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT 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,
`updated_by` BIGINT UNSIGNED NULL,
INDEX `idx_sports_member` (`member_id`),
INDEX `idx_sports_status` (`status`),
CONSTRAINT `fk_sports_member` FOREIGN KEY (`member_id`) REFERENCES `members`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `sports_members`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `honorary_members` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`member_id` BIGINT UNSIGNED NOT NULL,
`granted_by_decision` VARCHAR(200) NULL,
`reason` TEXT NOT NULL,
`start_date` DATE NOT NULL,
`end_date` DATE NOT NULL,
`is_renewable` TINYINT(1) NOT NULL DEFAULT 1,
`renewal_count` INT UNSIGNED NOT NULL DEFAULT 0,
`last_renewed_at` TIMESTAMP NULL DEFAULT NULL,
`status` VARCHAR(50) NOT NULL DEFAULT 'active',
`notes` TEXT NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT 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,
`updated_by` BIGINT UNSIGNED NULL,
INDEX `idx_honorary_member` (`member_id`),
INDEX `idx_honorary_status` (`status`),
INDEX `idx_honorary_end_date` (`end_date`),
CONSTRAINT `fk_honorary_member` FOREIGN KEY (`member_id`) REFERENCES `members`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `honorary_members`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `foreign_member_details` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`member_id` BIGINT UNSIGNED NOT NULL,
`passport_number` VARCHAR(50) NOT NULL,
`issuing_country_id` BIGINT UNSIGNED NOT NULL,
`nationality_country_id` BIGINT UNSIGNED NOT NULL,
`passport_issue_date` DATE NOT NULL,
`passport_expiry_date` DATE NOT NULL,
`place_of_birth` VARCHAR(200) NULL,
`fee_amount_usd` DECIMAL(15,2) NOT NULL,
`exchange_rate` DECIMAL(15,4) NULL,
`fee_amount_egp` DECIMAL(15,2) NULL,
`fee_receipt_number` VARCHAR(50) NULL,
`status` VARCHAR(50) NOT NULL DEFAULT 'active',
`notes` TEXT NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT 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,
`updated_by` BIGINT UNSIGNED NULL,
INDEX `idx_foreign_member` (`member_id`),
INDEX `idx_foreign_passport` (`passport_number`),
INDEX `idx_foreign_expiry` (`passport_expiry_date`),
CONSTRAINT `fk_foreign_member` FOREIGN KEY (`member_id`) REFERENCES `members`(`id`),
CONSTRAINT `fk_foreign_issuing_country` FOREIGN KEY (`issuing_country_id`) REFERENCES `countries`(`id`),
CONSTRAINT `fk_foreign_nationality_country` FOREIGN KEY (`nationality_country_id`) REFERENCES `countries`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `foreign_member_details`",
];
\ No newline at end of file
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