Commit d41f51be authored by Administrator's avatar Administrator

Update 20 files via Son of Anton

parent cb11b4e5
<?php
declare(strict_types=1);
namespace App\Modules\Children\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Children\Models\Child;
use App\Modules\Children\Services\ChildFeeCalculator;
use App\Modules\Members\Services\NationalIdParser;
class ChildController extends Controller
{
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('Children.Views.create', [
'member' => $member,
'childOrder' => Child::getNextOrder((int) $memberId),
]);
}
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['date_of_birth'] ?? '')) $errors[] = 'تاريخ الميلاد مطلوب';
if (empty($data['gender'] ?? '')) $errors[] = 'النوع مطلوب';
if (empty($data['relationship'] ?? '')) $errors[] = 'نوع القرابة مطلوب';
// Parse NID if provided
$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'];
}
// Cannot be the member themselves
if ($nid === ($member['national_id'] ?? '')) {
$errors[] = 'لا يمكن إضافة العضو نفسه كابن';
}
$dup = Child::nidExistsElsewhere($nid);
if ($dup) {
$errors[] = 'الرقم القومي مسجل بالفعل: ' . ($dup['data']['full_name_ar'] ?? '');
}
}
// Calculate age
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'];
}
$childAge = (int) ($data['age_years'] ?? 0);
// Children 16+ require NID
if ($childAge >= 16 && $nid === '') {
$errors[] = 'الأبناء فوق 16 سنة يجب أن يكون لديهم رقم قومي';
}
// Age gap validation
if (!empty($data['date_of_birth']) && !empty($member['date_of_birth'])) {
$memberAge = age_from_dob($member['date_of_birth']);
$ageGap = $memberAge['years'] - $childAge;
if ($ageGap < 15) {
$errors[] = 'الحد الأدنى لفرق السن بين العضو والابن 15 سنة';
}
if ($ageGap > 60) {
$errors[] = 'الحد الأقصى لفرق السن بين العضو والابن 60 سنة';
}
}
// Stepchild age check
if (($data['relationship'] ?? '') === 'stepchild' && $childAge >= 25) {
$errors[] = 'أبناء الزوج/الزوجة لا يمكن أن يتجاوز عمرهم 25 سنة';
}
// Check age 25+ not accepted
if ($childAge >= 25 && ($data['relationship'] ?? '') !== 'stepchild') {
$errors[] = 'لا يمكن إضافة أبناء فوق 25 سنة';
}
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}/children/create");
}
// Calculate fee
$feeCalc = ChildFeeCalculator::calculate((int) $memberId, $data);
if (($feeCalc['classification'] ?? '') === 'not_accepted') {
return $this->redirect("/members/{$memberId}/children/create")
->withError($feeCalc['error'] ?? 'لا يمكن إضافة هذا الابن');
}
$childOrder = $feeCalc['child_order'] ?? Child::getNextOrder((int) $memberId);
$classification = $feeCalc['classification'] ?? 'included';
$child = Child::create([
'member_id' => (int) $memberId,
'child_order' => $childOrder,
'full_name_ar' => trim($data['full_name_ar']),
'full_name_en' => trim($data['full_name_en'] ?? ''),
'national_id' => $nid ?: null,
'birth_certificate_number' => $data['birth_certificate_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'],
'relationship' => $data['relationship'],
'school_faculty' => $data['school_faculty'] ?? null,
'nationality' => $data['nationality'] ?? 'مصري',
'classification' => $classification,
'addition_fee' => $feeCalc['fee'] ?? '0.00',
'status' => 'active',
'remarks' => $data['remarks'] ?? null,
]);
EventBus::dispatch('child.added', [
'member_id' => (int) $memberId,
'child_id' => (int) $child->id,
'classification' => $classification,
'fee' => $feeCalc['fee'] ?? '0.00',
]);
return $this->redirect("/members/{$memberId}")
->withSuccess('تم إضافة الابن/الابنة — التصنيف: ' . ($child->getClassificationLabel()) . ' — الرسوم: ' . money($feeCalc['fee'] ?? '0.00'));
}
public function show(Request $request, string $memberId, string $id): Response
{
$child = Child::find((int) $id);
if (!$child || (int) $child->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('Children.Views.show', ['member' => $member, 'child' => $child]);
}
public function edit(Request $request, string $memberId, string $id): Response
{
$child = Child::find((int) $id);
if (!$child || (int) $child->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('Children.Views.edit', ['member' => $member, 'child' => $child]);
}
public function update(Request $request, string $memberId, string $id): Response
{
$child = Child::find((int) $id);
if (!$child || (int) $child->member_id !== (int) $memberId) {
return $this->redirect("/members/{$memberId}")->withError('بيانات الابن غير موجودة');
}
$data = $request->all();
unset($data['_csrf_token']);
$updateData = [];
foreach (['full_name_en', 'school_faculty', 'remarks'] as $field) {
if (array_key_exists($field, $data)) {
$val = $data[$field];
$updateData[$field] = ($val === '' || $val === null) ? null : $val;
}
}
if (!empty($updateData)) {
$child->update($updateData);
}
return $this->redirect("/members/{$memberId}")->withSuccess('تم تحديث بيانات الابن/الابنة');
}
public function archive(Request $request, string $memberId, string $id): Response
{
$child = Child::find((int) $id);
if (!$child || (int) $child->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('children', [
'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('child.removed', [
'member_id' => (int) $memberId,
'child_id' => (int) $id,
'reason' => $reason,
]);
return $this->redirect("/members/{$memberId}")->withSuccess('تم إزالة الابن/الابنة');
}
public function freeze(Request $request, string $memberId, string $id): Response
{
$child = Child::find((int) $id);
if (!$child || (int) $child->member_id !== (int) $memberId) {
return $this->redirect("/members/{$memberId}")->withError('بيانات الابن غير موجودة');
}
$reason = trim((string) $request->post('freeze_reason', 'بلوغ سن 25'));
$db = App::getInstance()->db();
$db->update('children', [
'is_frozen' => 1,
'frozen_at' => date('Y-m-d H:i:s'),
'frozen_reason' => $reason,
'classification' => 'frozen',
'status' => 'frozen',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
EventBus::dispatch('child.frozen', [
'member_id' => (int) $memberId,
'child_id' => (int) $id,
'reason' => $reason,
]);
return $this->redirect("/members/{$memberId}")->withSuccess('تم تجميد عضوية الابن/الابنة');
}
public function calculateFee(Request $request): Response
{
$memberId = (int) $request->post('member_id', 0);
if ($memberId <= 0) {
return $this->json(['error' => 'بيانات غير صالحة'], 422);
}
$result = ChildFeeCalculator::calculate($memberId, $request->all());
return $this->json($result);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Children\Models;
use App\Core\Model;
use App\Core\App;
class Child extends Model
{
protected static string $table = 'children';
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', 'child_order', 'full_name_ar', 'full_name_en',
'national_id', 'birth_certificate_number', 'date_of_birth',
'age_years', 'age_months', 'gender', 'relationship',
'school_faculty', 'nationality', 'classification',
'addition_fee', 'fee_receipt_number', 'status',
'is_frozen', 'frozen_at', 'frozen_reason', 'photo_path', 'remarks',
];
public static function getForMember(int $memberId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM children WHERE member_id = ? AND is_archived = 0 ORDER BY child_order ASC",
[$memberId]
);
}
public static function countActiveForMember(int $memberId): int
{
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT COUNT(*) as cnt FROM children WHERE member_id = ? AND is_archived = 0 AND status = 'active'",
[$memberId]
);
return (int) ($row['cnt'] ?? 0);
}
public static function countActiveUnder18ForMember(int $memberId): int
{
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT COUNT(*) as cnt FROM children WHERE member_id = ? AND is_archived = 0 AND status = 'active' AND age_years < 18",
[$memberId]
);
return (int) ($row['cnt'] ?? 0);
}
public static function getNextOrder(int $memberId): int
{
return self::countActiveForMember($memberId) + 1;
}
public static function nidExistsElsewhere(string $nid, ?int $excludeChildId = null): ?array
{
$db = App::getInstance()->db();
$memberDup = $db->selectOne(
"SELECT id, full_name_ar, membership_number 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, m.membership_number FROM spouses s JOIN members m ON m.id = s.member_id WHERE s.national_id = ? AND s.is_archived = 0",
[$nid]
);
if ($spouseDup) return ['type' => 'spouse', 'data' => $spouseDup];
$sql = "SELECT c.id, c.full_name_ar, m.membership_number FROM children c JOIN members m ON m.id = c.member_id WHERE c.national_id = ? AND c.is_archived = 0";
$params = [$nid];
if ($excludeChildId) {
$sql .= " AND c.id != ?";
$params[] = $excludeChildId;
}
$childDup = $db->selectOne($sql, $params);
if ($childDup) return ['type' => 'child', 'data' => $childDup];
return null;
}
public function getClassificationLabel(): string
{
return match ($this->classification) {
'included' => 'مشمول (بدون رسوم)',
'dependent_with_fee' => 'تابع (برسوم)',
'temporary' => 'مؤقت',
'frozen' => 'مجمد',
'separated' => 'منفصل',
default => $this->classification,
};
}
public function getRelationshipLabel(): string
{
return match ($this->relationship) {
'son' => 'ابن',
'daughter' => 'ابنة',
'stepchild' => 'ابن/ابنة الزوج',
default => $this->relationship,
};
}
public function getStatusLabel(): string
{
return match ($this->status) {
'active' => 'نشط',
'inactive' => 'غير نشط',
'frozen' => 'مجمد',
'separated' => 'منفصل',
default => $this->status,
};
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/members/{memberId}/children/create', 'Children\Controllers\ChildController@create', ['auth'], 'child.add'],
['POST', '/members/{memberId}/children', 'Children\Controllers\ChildController@store', ['auth'], 'child.add'],
['GET', '/members/{memberId}/children/{id}', 'Children\Controllers\ChildController@show', ['auth'], 'child.view'],
['GET', '/members/{memberId}/children/{id}/edit', 'Children\Controllers\ChildController@edit', ['auth'], 'child.edit'],
['POST', '/members/{memberId}/children/{id}', 'Children\Controllers\ChildController@update', ['auth'], 'child.edit'],
['POST', '/members/{memberId}/children/{id}/archive', 'Children\Controllers\ChildController@archive', ['auth'], 'child.remove'],
['POST', '/members/{memberId}/children/{id}/freeze', 'Children\Controllers\ChildController@freeze', ['auth'], 'child.freeze'],
['POST', '/api/children/calculate-fee', 'Children\Controllers\ChildController@calculateFee', ['auth'], 'child.add'],
];
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Children\Services;
use App\Core\App;
use App\Modules\Children\Models\Child;
use App\Modules\Pricing\Services\PricingEngine;
use App\Modules\Rules\Services\RuleEngine;
final class ChildFeeCalculator
{
/**
* Calculate the fee and classification for adding a child.
*/
public static function calculate(int $memberId, array $childData): array
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$member) {
return ['error' => 'العضو غير موجود', 'fee' => '0.00', 'classification' => 'included'];
}
$membershipValue = $member['membership_value'] ?? '0.00';
if (bccomp($membershipValue, '0.00', 2) <= 0) {
return ['error' => 'قيمة العضوية غير محددة', 'fee' => '0.00', 'classification' => 'included'];
}
// Get child age
$childDob = $childData['date_of_birth'] ?? null;
$childAge = 0;
if ($childDob) {
$age = age_from_dob($childDob);
$childAge = $age['years'];
}
// Count existing children under 18 to determine order
$childrenUnder18 = Child::countActiveUnder18ForMember($memberId);
$totalChildren = Child::countActiveForMember($memberId);
$childOrder = $totalChildren + 1;
// Use PricingEngine
$feeResult = PricingEngine::calculateChildFee($membershipValue, $childAge, $childOrder);
// Add form fee if post-creation
$formFee = '0.00';
if ($member['status'] !== 'potential') {
$formFeeData = RuleEngine::get('FORM_ADDITION_FEE');
$formFee = $formFeeData['amount'] ?? '570.00';
}
// Determine classification
$classification = $feeResult['classification'] ?? 'included';
if ($classification === 'not_accepted') {
return [
'child_order' => $childOrder,
'child_age' => $childAge,
'membership_value' => $membershipValue,
'classification' => 'not_accepted',
'fee' => '0.00',
'percentage' => '0.00',
'form_fee' => '0.00',
'total_fee' => '0.00',
'rule_applied' => $feeResult['rule_applied'] ?? '',
'error' => $feeResult['error'] ?? 'سن الابن/الابنة يتجاوز الحد المسموح',
];
}
$childFee = $feeResult['fee'] ?? '0.00';
return [
'child_order' => $childOrder,
'child_age' => $childAge,
'children_under_18' => $childrenUnder18,
'membership_value' => $membershipValue,
'classification' => $classification,
'fee' => $childFee,
'percentage' => $feeResult['percentage'] ?? '0.00',
'form_fee' => $formFee,
'total_fee' => bcadd($childFee, $formFee, 2),
'rule_applied' => $feeResult['rule_applied'] ?? '',
'error' => null,
];
}
}
\ No newline at end of file
<?php
$children = $children ?? [];
$memberId = $memberId ?? 0;
?>
<?php if (!empty($children)): ?>
<table class="data-table">
<thead>
<tr><th>#</th><th>الاسم</th><th>النوع</th><th>القرابة</th><th>تاريخ الميلاد</th><th>السن</th><th>التصنيف</th><th>رسوم</th><th>الحالة</th><th>الإجراءات</th></tr>
</thead>
<tbody>
<?php foreach ($children as $c): ?>
<tr>
<td><?= (int) $c['child_order'] ?></td>
<td><a href="/members/<?= (int) $memberId ?>/children/<?= (int) $c['id'] ?>" style="color:#0D7377;font-weight:600;"><?= e($c['full_name_ar']) ?></a></td>
<td><?= $c['gender'] === 'male' ? 'ذكر' : 'أنثى' ?></td>
<td style="font-size:13px;"><?php
echo match ($c['relationship'] ?? '') {
'son' => 'ابن', 'daughter' => 'ابنة', 'stepchild' => 'ابن/ابنة زوج', default => $c['relationship'] ?? ''
};
?></td>
<td style="font-size:13px;"><?= e($c['date_of_birth']) ?></td>
<td><?= (int) ($c['age_years'] ?? 0) ?></td>
<td style="font-size:12px;"><?php
echo match ($c['classification'] ?? '') {
'included' => '<span style="color:#059669;">مشمول</span>',
'dependent_with_fee' => '<span style="color:#D97706;">تابع برسوم</span>',
'temporary' => '<span style="color:#0284C7;">مؤقت</span>',
'frozen' => '<span style="color:#DC2626;">مجمد</span>',
'separated' => '<span style="color:#6B7280;">منفصل</span>',
default => e($c['classification'] ?? ''),
};
?></td>
<td style="font-weight:600;"><?= money($c['addition_fee'] ?? '0') ?></td>
<td>
<?php if ($c['is_frozen'] ?? false): ?>
<span style="color:#DC2626;font-weight:600;">● مجمد</span>
<?php else: ?>
<span style="color:<?= ($c['status'] ?? '') === 'active' ? '#059669' : '#DC2626' ?>;font-weight:600;"><?= ($c['status'] ?? '') === 'active' ? 'نشط' : ($c['status'] ?? '') ?></span>
<?php endif; ?>
</td>
<td>
<div style="display:flex;gap:5px;">
<a href="/members/<?= (int) $memberId ?>/children/<?= (int) $c['id'] ?>" class="btn btn-sm btn-outline">عرض</a>
<a href="/members/<?= (int) $memberId ?>/children/<?= (int) $c['id'] ?>/edit" class="btn btn-sm btn-outline">تعديل</a>
</div>
</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'] ?? 'لم يُحدد') ?>
&nbsp;|&nbsp; <strong>ترتيب الابن:</strong> #<?= (int) $childOrder ?>
</div>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">← العودة للعضو</a>
</div>
<form method="POST" action="/members/<?= (int) $member['id'] ?>/children">
<?= 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 minlength="5" maxlength="200">
</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="relationship" class="form-select" required>
<option value="">-- اختر --</option>
<option value="son" <?= old('relationship') === 'son' ? 'selected' : '' ?>>ابن</option>
<option value="daughter" <?= old('relationship') === 'daughter' ? 'selected' : '' ?>>ابنة</option>
<option value="stepchild" <?= old('relationship') === 'stepchild' ? 'selected' : '' ?>>ابن/ابنة الزوج</option>
</select>
</div>
<div class="form-group">
<label class="form-label">الرقم القومي <small style="color:#6B7280;">(مطلوب فوق 16 سنة)</small></label>
<input type="text" name="national_id" id="child_nid" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;">
<div id="child-nid-feedback" style="margin-top:5px;font-size:12px;"></div>
</div>
<div class="form-group">
<label class="form-label">رقم شهادة الميلاد <small style="color:#6B7280;">(تحت 16 سنة)</small></label>
<input type="text" name="birth_certificate_number" value="<?= e(old('birth_certificate_number')) ?>" class="form-input" 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" id="child_dob" value="<?= e(old('date_of_birth')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">السن</label>
<input type="text" id="child_age_display" class="form-input" style="background:#F3F4F6;" readonly>
<input type="hidden" name="age_years" id="child_age_years">
<input type="hidden" name="age_months" id="child_age_months">
</div>
<div class="form-group">
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<select name="gender" id="child_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="school_faculty" value="<?= e(old('school_faculty')) ?>" class="form-input">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="remarks" class="form-textarea" rows="2"><?= e(old('remarks')) ?></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(); ?>
<?php $__template->section('scripts'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
var nidInput = document.getElementById('child_nid');
var dobInput = document.getElementById('child_dob');
var ageDisplay = document.getElementById('child_age_display');
var ageYears = document.getElementById('child_age_years');
var ageMonths = document.getElementById('child_age_months');
var genderSelect = document.getElementById('child_gender');
var feedback = document.getElementById('child-nid-feedback');
nidInput.addEventListener('input', function() {
var val = this.value.replace(/\D/g, '');
this.value = val;
if (val.length === 14) {
var formData = new FormData();
formData.append('national_id', val);
var csrfToken = document.querySelector('input[name="_csrf_token"]');
if (csrfToken) formData.append('_csrf_token', csrfToken.value);
fetch('/api/members/parse-nid', {method: 'POST', body: formData})
.then(function(r) { return r.json(); })
.then(function(data) {
var p = data.parsed;
if (p && p.is_valid) {
dobInput.value = p.dob;
ageDisplay.value = p.age_years + ' سنة و ' + p.age_months + ' شهر';
ageYears.value = p.age_years;
ageMonths.value = p.age_months;
genderSelect.value = p.gender;
feedback.innerHTML = '<span style="color:#059669;">✓ صالح</span>';
if (data.duplicate) {
feedback.innerHTML += '<br><span style="color:#DC2626;">✖ مسجل: ' + data.duplicate.full_name_ar + '</span>';
}
} else {
feedback.innerHTML = '<span style="color:#DC2626;">✖ ' + (p.errors ? p.errors.join(' | ') : 'غير صالح') + '</span>';
}
})
.catch(function() { feedback.innerHTML = '<span style="color:#DC2626;">خطأ</span>'; });
} else { feedback.innerHTML = ''; }
});
// Manual DOB age calculation
dobInput.addEventListener('change', function() {
if (this.value && !nidInput.value) {
var dob = new Date(this.value);
var now = new Date();
var years = now.getFullYear() - dob.getFullYear();
var months = now.getMonth() - dob.getMonth();
if (months < 0 || (months === 0 && now.getDate() < dob.getDate())) { years--; months += 12; }
if (now.getDate() < dob.getDate()) months--;
if (months < 0) months = 0;
ageDisplay.value = years + ' سنة و ' + months + ' شهر';
ageYears.value = years;
ageMonths.value = months;
}
});
});
</script>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل بيانات الابن: <?= e($child->full_name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/members/<?= (int) $member['id'] ?>/children/<?= (int) $child->id ?>">
<?= csrf_field() ?>
<div class="card" style="padding:20px;margin-bottom: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><input type="text" value="<?= e($child->full_name_ar) ?>" class="form-input" disabled style="background:#F3F4F6;"></div>
<div class="form-group"><label class="form-label">الرقم القومي</label><input type="text" value="<?= e($child->national_id ?: '—') ?>" class="form-input" disabled style="background:#F3F4F6;direction:ltr;text-align:left;"></div>
<div class="form-group"><label class="form-label">تاريخ الميلاد</label><input type="text" value="<?= e($child->date_of_birth) ?>" class="form-input" disabled style="background:#F3F4F6;"></div>
<div class="form-group"><label class="form-label">السن</label><input type="text" value="<?= (int) $child->age_years ?> سنة" class="form-input" disabled style="background:#F3F4F6;"></div>
<div class="form-group"><label class="form-label">النوع</label><input type="text" value="<?= $child->gender === 'male' ? 'ذكر' : 'أنثى' ?>" class="form-input" disabled style="background:#F3F4F6;"></div>
<div class="form-group"><label class="form-label">نوع القرابة</label><input type="text" value="<?= e($child->getRelationshipLabel()) ?>" class="form-input" disabled style="background:#F3F4F6;"></div>
<div class="form-group"><label class="form-label">التصنيف</label><input type="text" value="<?= e($child->getClassificationLabel()) ?>" class="form-input" disabled style="background:#F3F4F6;"></div>
</div>
</div>
<div class="card" style="padding:20px;margin-bottom: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><input type="text" name="full_name_en" value="<?= e($child->full_name_en ?? '') ?>" class="form-input"></div>
<div class="form-group"><label class="form-label">الكلية / المدرسة</label><input type="text" name="school_faculty" value="<?= e($child->school_faculty ?? '') ?>" class="form-input"></div>
<div class="form-group" style="grid-column:1/-1;"><label class="form-label">ملاحظات</label><textarea name="remarks" class="form-textarea" rows="2"><?= e($child->remarks ?? '') ?></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'); ?>بيانات الابن: <?= e($child->full_name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/members/<?= (int) $member['id'] ?>/children/<?= (int) $child->id ?>/edit" class="btn btn-outline">تعديل</a>
<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="padding:6px 0;font-weight:600;"><?= e($child->full_name_ar) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الرقم القومي</td><td style="padding:6px 0;direction:ltr;text-align:right;"><?= e($child->national_id ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">شهادة الميلاد</td><td style="padding:6px 0;"><?= e($child->birth_certificate_number ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ الميلاد</td><td style="padding:6px 0;"><?= e($child->date_of_birth) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">السن</td><td style="padding:6px 0;"><?= (int) $child->age_years ?> سنة <?= $child->age_months ? 'و ' . (int) $child->age_months . ' شهر' : '' ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">النوع</td><td style="padding:6px 0;"><?= $child->gender === 'male' ? 'ذكر' : 'أنثى' ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">نوع القرابة</td><td style="padding:6px 0;"><?= e($child->getRelationshipLabel()) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الكلية / المدرسة</td><td style="padding:6px 0;"><?= e($child->school_faculty ?: '—') ?></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="padding:6px 0;font-weight:600;">#<?= (int) $child->child_order ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">التصنيف</td><td style="padding:6px 0;"><?= e($child->getClassificationLabel()) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الحالة</td><td style="padding:6px 0;color:<?= $child->status === 'active' ? '#059669' : '#DC2626' ?>;font-weight:600;"><?= e($child->getStatusLabel()) ?></td></tr>
<?php if ($child->is_frozen): ?>
<tr><td style="padding:6px 0;color:#6B7280;">مجمد منذ</td><td style="padding:6px 0;color:#DC2626;"><?= e($child->frozen_at ?? '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">سبب التجميد</td><td style="padding:6px 0;"><?= e($child->frozen_reason ?? '—') ?></td></tr>
<?php endif; ?>
<tr><td style="padding:6px 0;color:#6B7280;">رسوم الإضافة</td><td style="padding:6px 0;font-weight:700;color:#0D7377;"><?= money($child->addition_fee) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">رقم الإيصال</td><td style="padding:6px 0;"><?= e($child->fee_receipt_number ?: '—') ?></td></tr>
</table>
<?php if ($child->status === 'active' && !$child->is_frozen && $child->gender === 'male' && (int) $child->age_years >= 25): ?>
<div style="margin-top:20px;padding:15px;background:#FEF2F2;border:1px solid #FECACA;border-radius:8px;">
<strong style="color:#DC2626;">⚠ هذا الابن بلغ سن 25 ويجب تجميد عضويته</strong>
<form method="POST" action="/members/<?= (int) $member['id'] ?>/children/<?= (int) $child->id ?>/freeze" style="margin-top:10px;">
<?= csrf_field() ?>
<input type="hidden" name="freeze_reason" value="بلوغ سن 25">
<button type="submit" class="btn btn-sm" style="background:#DC2626;color:#fff;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;">تجميد العضوية</button>
</form>
</div>
<?php endif; ?>
<?php if ($child->remarks): ?>
<div style="margin-top:15px;"><h4 style="color:#6B7280;font-size:13px;">ملاحظات</h4><p style="font-size:14px;"><?= nl2br(e($child->remarks)) ?></p></div>
<?php endif; ?>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Registries\PermissionRegistry;
use App\Core\EventBus;
PermissionRegistry::register('children', [
'child.add' => ['ar' => 'إضافة أبناء', 'en' => 'Add Children'],
'child.edit' => ['ar' => 'تعديل بيانات أبناء', 'en' => 'Edit Children'],
'child.remove' => ['ar' => 'إزالة أبناء', 'en' => 'Remove Children'],
'child.view' => ['ar' => 'عرض بيانات الأبناء', 'en' => 'View Children'],
'child.freeze' => ['ar' => 'تجميد عضوية ابن', 'en' => 'Freeze Child'],
'child.separate' => ['ar' => 'فصل أبناء', 'en' => 'Separate Child'],
]);
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Spouses\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Spouses\Models\Spouse;
use App\Modules\Spouses\Services\SpouseFeeCalculator;
use App\Modules\Members\Services\NationalIdParser;
class SpouseController extends Controller
{
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('العضو غير موجود');
}
$qualifications = $db->select("SELECT id, name_ar FROM qualifications WHERE is_active = 1 ORDER BY sort_order");
$countries = $db->select("SELECT nationality_ar FROM countries WHERE is_active = 1 ORDER BY name_ar");
$currentSpouseCount = Spouse::countActiveForMember((int) $memberId);
// Check max spouses
$maxSpouses = ($member['gender'] === 'male') ? 4 : 1;
if ($currentSpouseCount >= $maxSpouses) {
return $this->redirect('/members/' . $memberId)->withError('تم الوصول للحد الأقصى لعدد الأزواج');
}
return $this->view('Spouses.Views.create', [
'member' => $member,
'qualifications' => $qualifications,
'countries' => $countries,
'spouseOrder' => $currentSpouseCount + 1,
]);
}
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 = [];
// Required fields
if (empty(trim($data['full_name_ar'] ?? ''))) $errors[] = 'اسم الزوج/الزوجة مطلوب';
if (empty($data['date_of_birth'] ?? '')) $errors[] = 'تاريخ الميلاد مطلوب';
if (empty($data['gender'] ?? '')) $errors[] = 'النوع مطلوب';
if (empty($data['marriage_date'] ?? '')) $errors[] = 'تاريخ الزواج مطلوب';
// Check max spouses
$currentCount = Spouse::countActiveForMember((int) $memberId);
$maxSpouses = ($member['gender'] === 'male') ? 4 : 1;
if ($currentCount >= $maxSpouses) {
$errors[] = 'تم الوصول للحد الأقصى لعدد الأزواج (' . $maxSpouses . ')';
}
// Parse NID if provided
$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'];
}
// Cannot be own spouse
if ($nid === ($member['national_id'] ?? '')) {
$errors[] = 'لا يمكن إضافة العضو نفسه كزوج';
}
// Duplicate NID check
$dup = Spouse::nidExistsForOtherMember($nid, (int) $memberId);
if ($dup) {
$dupName = $dup['data']['full_name_ar'] ?? '';
$dupNum = $dup['data']['membership_number'] ?? '';
$errors[] = "الرقم القومي مسجل بالفعل: {$dupName} ({$dupNum})";
}
}
// Marriage date validation
if (!empty($data['marriage_date'])) {
$marriageTs = strtotime($data['marriage_date']);
if ($marriageTs > time()) {
$errors[] = 'تاريخ الزواج لا يمكن أن يكون في المستقبل';
}
// Both must be >= 18 at marriage
if (!empty($data['date_of_birth']) && !empty($member['date_of_birth'])) {
$spouseAgeAtMarriage = (int) (((new \DateTime($data['date_of_birth']))->diff(new \DateTime($data['marriage_date'])))->y);
$memberAgeAtMarriage = (int) (((new \DateTime($member['date_of_birth']))->diff(new \DateTime($data['marriage_date'])))->y);
if ($spouseAgeAtMarriage < 18) {
$errors[] = 'عمر الزوج/الزوجة عند الزواج يجب أن يكون 18 سنة على الأقل';
}
if ($memberAgeAtMarriage < 18) {
$errors[] = 'عمر العضو عند الزواج يجب أن يكون 18 سنة على الأقل';
}
}
}
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}/spouses/create");
}
// Calculate age
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'];
}
// Determine classification
$spouseAge = (int) ($data['age_years'] ?? 0);
$classification = $spouseAge >= 21 ? 'working' : 'dependent';
// Calculate fee
$feeCalc = SpouseFeeCalculator::calculate((int) $memberId, $data);
$spouseOrder = $feeCalc['spouse_order'] ?? ($currentCount + 1);
$spouse = Spouse::create([
'member_id' => (int) $memberId,
'spouse_order' => $spouseOrder,
'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'] ?? 'مصري',
'religion' => $data['religion'] ?? null,
'qualification_id' => !empty($data['qualification_id']) ? (int) $data['qualification_id'] : null,
'occupation' => $data['occupation'] ?? null,
'work_address' => $data['work_address'] ?? null,
'work_phone' => $data['work_phone'] ?? null,
'mobile' => $data['mobile'] ?? null,
'marriage_date' => $data['marriage_date'],
'join_date' => date('Y-m-d'),
'classification' => $classification,
'addition_fee' => $feeCalc['spouse_fee'] ?? '0.00',
'status' => 'active',
]);
EventBus::dispatch('spouse.added', [
'member_id' => (int) $memberId,
'spouse_id' => (int) $spouse->id,
'fee' => $feeCalc['spouse_fee'] ?? '0.00',
]);
return $this->redirect("/members/{$memberId}")
->withSuccess('تم إضافة الزوج/الزوجة بنجاح — الرسوم: ' . money($feeCalc['spouse_fee'] ?? '0.00'));
}
public function show(Request $request, string $memberId, string $id): Response
{
$spouse = Spouse::find((int) $id);
if (!$spouse || (int) $spouse->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('Spouses.Views.show', [
'member' => $member,
'spouse' => $spouse,
]);
}
public function edit(Request $request, string $memberId, string $id): Response
{
$spouse = Spouse::find((int) $id);
if (!$spouse || (int) $spouse->member_id !== (int) $memberId) {
return $this->redirect("/members/{$memberId}")->withError('بيانات الزوج غير موجودة');
}
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $memberId]);
$qualifications = $db->select("SELECT id, name_ar FROM qualifications WHERE is_active = 1 ORDER BY sort_order");
$countries = $db->select("SELECT nationality_ar FROM countries WHERE is_active = 1 ORDER BY name_ar");
return $this->view('Spouses.Views.edit', [
'member' => $member,
'spouse' => $spouse,
'qualifications' => $qualifications,
'countries' => $countries,
]);
}
public function update(Request $request, string $memberId, string $id): Response
{
$spouse = Spouse::find((int) $id);
if (!$spouse || (int) $spouse->member_id !== (int) $memberId) {
return $this->redirect("/members/{$memberId}")->withError('بيانات الزوج غير موجودة');
}
$data = $request->all();
unset($data['_csrf_token']);
$updateFields = [
'full_name_en', 'religion', 'occupation', 'work_address',
'work_phone', 'mobile', 'qualification_id',
];
$updateData = [];
foreach ($updateFields as $field) {
if (array_key_exists($field, $data)) {
$val = $data[$field];
$updateData[$field] = ($val === '' || $val === null) ? null : $val;
}
}
if (!empty($updateData)) {
$spouse->update($updateData);
}
EventBus::dispatch('spouse.updated', [
'member_id' => (int) $memberId,
'spouse_id' => (int) $id,
]);
return $this->redirect("/members/{$memberId}")->withSuccess('تم تحديث بيانات الزوج/الزوجة');
}
public function archive(Request $request, string $memberId, string $id): Response
{
$spouse = Spouse::find((int) $id);
if (!$spouse || (int) $spouse->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('spouses', [
'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('spouse.removed', [
'member_id' => (int) $memberId,
'spouse_id' => (int) $id,
'reason' => $reason,
]);
return $this->redirect("/members/{$memberId}")->withSuccess('تم إزالة الزوج/الزوجة');
}
public function calculateFee(Request $request): Response
{
$memberId = (int) $request->post('member_id', 0);
$data = $request->all();
if ($memberId <= 0) {
return $this->json(['error' => 'بيانات غير صالحة'], 422);
}
$result = SpouseFeeCalculator::calculate($memberId, $data);
return $this->json($result);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Spouses\Models;
use App\Core\Model;
use App\Core\App;
class Spouse extends Model
{
protected static string $table = 'spouses';
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', 'spouse_order', 'full_name_ar', 'full_name_en',
'national_id', 'passport_number', 'date_of_birth', 'age_years', 'age_months',
'gender', 'nationality', 'religion', 'qualification_id',
'occupation', 'work_address', 'work_phone', 'mobile',
'marriage_date', 'join_date', 'classification',
'addition_fee', 'fee_receipt_number', 'status', 'photo_path',
];
public static function getForMember(int $memberId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT s.*, q.name_ar as qualification_name
FROM spouses s
LEFT JOIN qualifications q ON q.id = s.qualification_id
WHERE s.member_id = ? AND s.is_archived = 0
ORDER BY s.spouse_order ASC",
[$memberId]
);
}
public static function countActiveForMember(int $memberId): int
{
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT COUNT(*) as cnt FROM spouses WHERE member_id = ? AND is_archived = 0 AND status = 'active'",
[$memberId]
);
return (int) ($row['cnt'] ?? 0);
}
public static function getNextOrder(int $memberId): int
{
return self::countActiveForMember($memberId) + 1;
}
public static function nidExistsForOtherMember(string $nid, int $excludeMemberId, ?int $excludeSpouseId = null): ?array
{
$db = App::getInstance()->db();
// Check members table
$memberDup = $db->selectOne(
"SELECT id, full_name_ar, membership_number FROM members WHERE national_id = ? AND id != ? AND is_archived = 0",
[$nid, $excludeMemberId]
);
if ($memberDup) {
return ['type' => 'member', 'data' => $memberDup];
}
// Check spouses table
$sql = "SELECT s.id, s.full_name_ar, s.member_id, m.membership_number
FROM spouses s
JOIN members m ON m.id = s.member_id
WHERE s.national_id = ? AND s.is_archived = 0";
$params = [$nid];
if ($excludeSpouseId) {
$sql .= " AND s.id != ?";
$params[] = $excludeSpouseId;
}
$spouseDup = $db->selectOne($sql, $params);
if ($spouseDup) {
return ['type' => 'spouse', 'data' => $spouseDup];
}
// Check children table
$childDup = $db->selectOne(
"SELECT c.id, c.full_name_ar, c.member_id, m.membership_number
FROM children c
JOIN members m ON m.id = c.member_id
WHERE c.national_id = ? AND c.is_archived = 0",
[$nid]
);
if ($childDup) {
return ['type' => 'child', 'data' => $childDup];
}
return null;
}
public function getClassificationLabel(): string
{
return match ($this->classification) {
'working' => 'عضو عامل',
'dependent' => 'عضو تابع',
default => $this->classification,
};
}
public function getStatusLabel(): string
{
return match ($this->status) {
'active' => 'نشط',
'inactive' => 'غير نشط',
'separated' => 'منفصل',
'deceased' => 'متوفى',
default => $this->status,
};
}
public function getMemberName(): string
{
$db = App::getInstance()->db();
$row = $db->selectOne("SELECT full_name_ar FROM members WHERE id = ?", [$this->member_id]);
return $row['full_name_ar'] ?? '—';
}
public function getQualificationName(): string
{
if (!$this->qualification_id) return '—';
$db = App::getInstance()->db();
$row = $db->selectOne("SELECT name_ar FROM qualifications WHERE id = ?", [$this->qualification_id]);
return $row['name_ar'] ?? '—';
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/members/{memberId}/spouses/create', 'Spouses\Controllers\SpouseController@create', ['auth'], 'spouse.add'],
['POST', '/members/{memberId}/spouses', 'Spouses\Controllers\SpouseController@store', ['auth'], 'spouse.add'],
['GET', '/members/{memberId}/spouses/{id}', 'Spouses\Controllers\SpouseController@show', ['auth'], 'spouse.view'],
['GET', '/members/{memberId}/spouses/{id}/edit', 'Spouses\Controllers\SpouseController@edit', ['auth'], 'spouse.edit'],
['POST', '/members/{memberId}/spouses/{id}', 'Spouses\Controllers\SpouseController@update', ['auth'], 'spouse.edit'],
['POST', '/members/{memberId}/spouses/{id}/archive', 'Spouses\Controllers\SpouseController@archive', ['auth'], 'spouse.remove'],
['POST', '/api/spouses/calculate-fee', 'Spouses\Controllers\SpouseController@calculateFee', ['auth'], 'spouse.add'],
];
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Spouses\Services;
use App\Core\App;
use App\Modules\Spouses\Models\Spouse;
use App\Modules\Pricing\Services\PricingEngine;
use App\Modules\Rules\Services\RuleEngine;
final class SpouseFeeCalculator
{
/**
* Calculate the full fee breakdown for adding a spouse to a member.
*/
public static function calculate(int $memberId, array $spouseData): array
{
$db = App::getInstance()->db();
// Load member
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$member) {
return ['error' => 'العضو غير موجود', 'fee' => '0.00'];
}
$membershipValue = $member['membership_value'] ?? '0.00';
if (bccomp($membershipValue, '0.00', 2) <= 0) {
return ['error' => 'قيمة العضوية غير محددة', 'fee' => '0.00'];
}
// Determine spouse order
$currentCount = Spouse::countActiveForMember($memberId);
$spouseOrder = $currentCount + 1;
// Get nationality
$nationality = $spouseData['nationality'] ?? 'مصري';
// Get marriage date
$marriageDate = $spouseData['marriage_date'] ?? date('Y-m-d');
// Get membership acquisition date (member created_at or a specific field)
$acquisitionDate = $member['created_at'] ? substr($member['created_at'], 0, 10) : date('Y-m-d');
// Determine member type (base or acquired) - for now default to 'base'
// In future phases, this will come from transfer/separation history
$memberType = 'base';
// Determine spouse age for classification
$spouseDob = $spouseData['date_of_birth'] ?? null;
$spouseAge = 0;
if ($spouseDob) {
$age = age_from_dob($spouseDob);
$spouseAge = $age['years'];
}
// Classification: working if >= 21, dependent if < 21
$workingAgeThreshold = RuleEngine::getValue('SPOUSE_WORKING_AGE_THRESHOLD', 'threshold') ?? 21;
$classification = $spouseAge >= $workingAgeThreshold ? 'working' : 'dependent';
// Calculate fee using PricingEngine
$feeResult = PricingEngine::calculateSpouseFee(
$membershipValue,
$spouseOrder,
$nationality,
$marriageDate,
$acquisitionDate,
$memberType
);
// Add form fee if this is a post-creation addition
$formFee = '0.00';
if ($member['status'] !== 'potential') {
$formFeeData = RuleEngine::get('FORM_ADDITION_FEE');
$formFee = $formFeeData['amount'] ?? '570.00';
}
return [
'spouse_order' => $spouseOrder,
'classification' => $classification,
'membership_value' => $membershipValue,
'nationality' => $nationality,
'marriage_date' => $marriageDate,
'acquisition_date' => $acquisitionDate,
'member_type' => $memberType,
'percentage_fee' => $feeResult['percentage_fee'] ?? '0.00',
'annual_fee' => $feeResult['annual_fee'] ?? '0.00',
'years_count' => $feeResult['years_count'] ?? 0,
'spouse_fee' => $feeResult['total'] ?? '0.00',
'form_fee' => $formFee,
'total_fee' => bcadd($feeResult['total'] ?? '0.00', $formFee, 2),
'rule_applied' => $feeResult['rule_applied'] ?? 'unknown',
'error' => $feeResult['error'] ?? null,
];
}
}
\ No newline at end of file
<?php
$spouses = $spouses ?? [];
$memberId = $memberId ?? 0;
?>
<?php if (!empty($spouses)): ?>
<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 ($spouses as $s): ?>
<tr>
<td><?= (int) $s['spouse_order'] ?></td>
<td><a href="/members/<?= (int) $memberId ?>/spouses/<?= (int) $s['id'] ?>" style="color:#0D7377;font-weight:600;"><?= e($s['full_name_ar']) ?></a></td>
<td style="direction:ltr;text-align:right;font-size:13px;"><?= e($s['national_id'] ?? '—') ?></td>
<td style="font-size:13px;"><?= e($s['date_of_birth']) ?></td>
<td><?= (int) ($s['age_years'] ?? 0) ?></td>
<td><?= ($s['classification'] ?? '') === 'working' ? 'عامل' : 'تابع' ?></td>
<td style="font-weight:600;"><?= money($s['addition_fee'] ?? '0') ?></td>
<td><span style="color:<?= ($s['status'] ?? '') === 'active' ? '#059669' : '#DC2626' ?>;font-weight:600;"><?= ($s['status'] ?? '') === 'active' ? 'نشط' : ($s['status'] ?? '') ?></span></td>
<td>
<div style="display:flex;gap:5px;">
<a href="/members/<?= (int) $memberId ?>/spouses/<?= (int) $s['id'] ?>" class="btn btn-sm btn-outline">عرض</a>
<a href="/members/<?= (int) $memberId ?>/spouses/<?= (int) $s['id'] ?>/edit" class="btn btn-sm btn-outline">تعديل</a>
</div>
</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'] ?? 'لم يُحدد') ?>
&nbsp;|&nbsp; <strong>ترتيب الزوج/الزوجة:</strong> #<?= (int) $spouseOrder ?>
</div>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">← العودة للعضو</a>
</div>
<form method="POST" action="/members/<?= (int) $member['id'] ?>/spouses" id="spouse-form">
<?= 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 minlength="10" maxlength="200">
</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">الرقم القومي</label>
<input type="text" name="national_id" id="spouse_nid" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" pattern="\d{14}" style="direction:ltr;text-align:left;" placeholder="14 رقم">
<div id="nid-feedback" style="margin-top:5px;font-size:12px;"></div>
</div>
<div class="form-group">
<label class="form-label">رقم جواز السفر</label>
<input type="text" name="passport_number" value="<?= e(old('passport_number')) ?>" class="form-input" 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" id="spouse_dob" value="<?= e(old('date_of_birth')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">السن</label>
<input type="text" id="spouse_age" class="form-input" style="background:#F3F4F6;" readonly>
<input type="hidden" name="age_years" id="spouse_age_years" value="<?= e(old('age_years')) ?>">
<input type="hidden" name="age_months" id="spouse_age_months" value="<?= e(old('age_months')) ?>">
</div>
<div class="form-group">
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<select name="gender" id="spouse_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">الجنسية <span style="color:#DC2626;">*</span></label>
<select name="nationality" class="form-select">
<option value="مصري" selected>مصري</option>
<?php foreach ($countries as $c): ?>
<?php if ($c['nationality_ar'] !== 'مصري'): ?>
<option value="<?= e($c['nationality_ar']) ?>" <?= old('nationality') === $c['nationality_ar'] ? 'selected' : '' ?>><?= e($c['nationality_ar']) ?></option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الديانة</label>
<select name="religion" class="form-select">
<option value="">-- اختر --</option>
<option value="muslim" <?= old('religion') === 'muslim' ? 'selected' : '' ?>>مسلم</option>
<option value="christian" <?= old('religion') === 'christian' ? 'selected' : '' ?>>مسيحي</option>
<option value="other" <?= old('religion') === 'other' ? 'selected' : '' ?>>أخرى</option>
</select>
</div>
<div class="form-group">
<label class="form-label">المؤهل الدراسي</label>
<select name="qualification_id" class="form-select">
<option value="">-- اختر --</option>
<?php foreach ($qualifications as $q): ?>
<option value="<?= (int) $q['id'] ?>" <?= old('qualification_id') == $q['id'] ? 'selected' : '' ?>><?= e($q['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الوظيفة</label>
<input type="text" name="occupation" value="<?= e(old('occupation')) ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">رقم المحمول</label>
<input type="tel" name="mobile" value="<?= e(old('mobile')) ?>" class="form-input" style="direction:ltr;text-align:left;" maxlength="11">
</div>
<div class="form-group">
<label class="form-label">تليفون العمل</label>
<input type="tel" name="work_phone" value="<?= e(old('work_phone')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">عنوان العمل</label>
<input type="text" name="work_address" value="<?= e(old('work_address')) ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">تاريخ الزواج <span style="color:#DC2626;">*</span></label>
<input type="date" name="marriage_date" value="<?= e(old('marriage_date')) ?>" class="form-input" required>
</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(); ?>
<?php $__template->section('scripts'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
var nidInput = document.getElementById('spouse_nid');
var dobInput = document.getElementById('spouse_dob');
var ageDisplay = document.getElementById('spouse_age');
var ageYears = document.getElementById('spouse_age_years');
var ageMonths = document.getElementById('spouse_age_months');
var genderSelect = document.getElementById('spouse_gender');
var feedback = document.getElementById('nid-feedback');
nidInput.addEventListener('input', function() {
var val = this.value.replace(/\D/g, '');
this.value = val;
if (val.length === 14) {
var formData = new FormData();
formData.append('national_id', val);
var csrfToken = document.querySelector('input[name="_csrf_token"]');
if (csrfToken) formData.append('_csrf_token', csrfToken.value);
fetch('/api/members/parse-nid', {method: 'POST', body: formData})
.then(function(r) { return r.json(); })
.then(function(data) {
var p = data.parsed;
if (p && p.is_valid) {
dobInput.value = p.dob;
ageDisplay.value = p.age_years + ' سنة و ' + p.age_months + ' شهر';
ageYears.value = p.age_years;
ageMonths.value = p.age_months;
genderSelect.value = p.gender;
feedback.innerHTML = '<span style="color:#059669;">✓ الرقم القومي صالح</span>';
if (data.duplicate) {
feedback.innerHTML += '<br><span style="color:#DC2626;">✖ الرقم مسجل بالفعل: ' + data.duplicate.full_name_ar + '</span>';
}
} else {
feedback.innerHTML = '<span style="color:#DC2626;">✖ ' + (p.errors ? p.errors.join(' | ') : 'رقم غير صالح') + '</span>';
}
})
.catch(function() { feedback.innerHTML = '<span style="color:#DC2626;">خطأ في الاتصال</span>'; });
} else {
feedback.innerHTML = '';
}
});
});
</script>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل بيانات الزوج: <?= e($spouse->full_name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/members/<?= (int) $member['id'] ?>/spouses/<?= (int) $spouse->id ?>">
<?= csrf_field() ?>
<div class="card" style="padding:20px;margin-bottom: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><input type="text" value="<?= e($spouse->full_name_ar) ?>" class="form-input" disabled style="background:#F3F4F6;"></div>
<div class="form-group"><label class="form-label">الرقم القومي</label><input type="text" value="<?= e($spouse->national_id ?: '—') ?>" class="form-input" disabled style="background:#F3F4F6;direction:ltr;text-align:left;"></div>
<div class="form-group"><label class="form-label">تاريخ الميلاد</label><input type="text" value="<?= e($spouse->date_of_birth) ?>" class="form-input" disabled style="background:#F3F4F6;"></div>
<div class="form-group"><label class="form-label">تاريخ الزواج</label><input type="text" value="<?= e($spouse->marriage_date) ?>" class="form-input" disabled style="background:#F3F4F6;"></div>
<div class="form-group"><label class="form-label">الترتيب</label><input type="text" value="<?= (int) $spouse->spouse_order ?>" class="form-input" disabled style="background:#F3F4F6;"></div>
<div class="form-group"><label class="form-label">التصنيف</label><input type="text" value="<?= e($spouse->getClassificationLabel()) ?>" class="form-input" disabled style="background:#F3F4F6;"></div>
</div>
</div>
<div class="card" style="padding:20px;margin-bottom: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><input type="text" name="full_name_en" value="<?= e($spouse->full_name_en ?? '') ?>" class="form-input"></div>
<div class="form-group"><label class="form-label">الديانة</label>
<select name="religion" class="form-select"><option value=""></option><option value="muslim" <?= $spouse->religion === 'muslim' ? 'selected' : '' ?>>مسلم</option><option value="christian" <?= $spouse->religion === 'christian' ? 'selected' : '' ?>>مسيحي</option><option value="other" <?= $spouse->religion === 'other' ? 'selected' : '' ?>>أخرى</option></select>
</div>
<div class="form-group"><label class="form-label">المؤهل الدراسي</label>
<select name="qualification_id" class="form-select"><option value=""></option>
<?php foreach ($qualifications as $q): ?><option value="<?= (int) $q['id'] ?>" <?= (int) ($spouse->qualification_id ?? 0) === (int) $q['id'] ? 'selected' : '' ?>><?= e($q['name_ar']) ?></option><?php endforeach; ?>
</select>
</div>
<div class="form-group"><label class="form-label">الوظيفة</label><input type="text" name="occupation" value="<?= e($spouse->occupation ?? '') ?>" class="form-input"></div>
<div class="form-group"><label class="form-label">رقم المحمول</label><input type="tel" name="mobile" value="<?= e($spouse->mobile ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;"></div>
<div class="form-group"><label class="form-label">تليفون العمل</label><input type="tel" name="work_phone" value="<?= e($spouse->work_phone ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;"></div>
<div class="form-group" style="grid-column:1/-1;"><label class="form-label">عنوان العمل</label><input type="text" name="work_address" value="<?= e($spouse->work_address ?? '') ?>" class="form-input"></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'); ?>بيانات الزوج: <?= e($spouse->full_name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/members/<?= (int) $member['id'] ?>/spouses/<?= (int) $spouse->id ?>/edit" class="btn btn-outline">تعديل</a>
<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="padding:6px 0;font-weight:600;"><?= e($spouse->full_name_ar) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الرقم القومي</td><td style="padding:6px 0;direction:ltr;text-align:right;"><?= e($spouse->national_id ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ الميلاد</td><td style="padding:6px 0;"><?= e($spouse->date_of_birth) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">السن</td><td style="padding:6px 0;"><?= (int) $spouse->age_years ?> سنة <?= $spouse->age_months ? 'و ' . (int) $spouse->age_months . ' شهر' : '' ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">النوع</td><td style="padding:6px 0;"><?= $spouse->gender === 'male' ? 'ذكر' : 'أنثى' ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الجنسية</td><td style="padding:6px 0;"><?= e($spouse->nationality ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">المؤهل</td><td style="padding:6px 0;"><?= e($spouse->getQualificationName()) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ الزواج</td><td style="padding:6px 0;"><?= e($spouse->marriage_date) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ الالتحاق</td><td style="padding:6px 0;"><?= e($spouse->join_date) ?></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="padding:6px 0;font-weight:600;">#<?= (int) $spouse->spouse_order ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">التصنيف</td><td style="padding:6px 0;"><?= e($spouse->getClassificationLabel()) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الحالة</td><td style="padding:6px 0;color:<?= $spouse->status === 'active' ? '#059669' : '#DC2626' ?>;font-weight:600;"><?= e($spouse->getStatusLabel()) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">رسوم الإضافة</td><td style="padding:6px 0;font-weight:700;color:#0D7377;"><?= money($spouse->addition_fee) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">رقم الإيصال</td><td style="padding:6px 0;"><?= e($spouse->fee_receipt_number ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الوظيفة</td><td style="padding:6px 0;"><?= e($spouse->occupation ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">المحمول</td><td style="padding:6px 0;direction:ltr;text-align:right;"><?= e($spouse->mobile ?: '—') ?></td></tr>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Registries\PermissionRegistry;
use App\Core\EventBus;
PermissionRegistry::register('spouses', [
'spouse.add' => ['ar' => 'إضافة زوج/زوجة', 'en' => 'Add Spouse'],
'spouse.edit' => ['ar' => 'تعديل بيانات زوج', 'en' => 'Edit Spouse'],
'spouse.remove' => ['ar' => 'إزالة زوج/زوجة', 'en' => 'Remove Spouse'],
'spouse.view' => ['ar' => 'عرض بيانات الأزواج', 'en' => 'View Spouses'],
]);
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `spouses` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`member_id` BIGINT UNSIGNED NOT NULL,
`spouse_order` INT UNSIGNED NOT NULL DEFAULT 1,
`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 'مصري',
`religion` VARCHAR(50) NULL,
`qualification_id` BIGINT UNSIGNED NULL,
`occupation` VARCHAR(200) NULL,
`work_address` TEXT NULL,
`work_phone` VARCHAR(20) NULL,
`mobile` VARCHAR(20) NULL,
`marriage_date` DATE NOT NULL,
`join_date` DATE NOT NULL,
`classification` VARCHAR(30) NOT NULL DEFAULT 'working',
`addition_fee` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`fee_receipt_number` VARCHAR(50) NULL,
`status` VARCHAR(50) NOT NULL DEFAULT 'active',
`photo_path` VARCHAR(500) 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_spouses_member` (`member_id`),
INDEX `idx_spouses_nid` (`national_id`),
INDEX `idx_spouses_status` (`status`),
INDEX `idx_spouses_archived` (`is_archived`),
CONSTRAINT `fk_spouses_member` FOREIGN KEY (`member_id`) REFERENCES `members`(`id`),
CONSTRAINT `fk_spouses_qualification` FOREIGN KEY (`qualification_id`) REFERENCES `qualifications`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `spouses`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `children` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`member_id` BIGINT UNSIGNED NOT NULL,
`child_order` INT UNSIGNED NOT NULL DEFAULT 1,
`full_name_ar` VARCHAR(200) NOT NULL,
`full_name_en` VARCHAR(200) NULL,
`national_id` VARCHAR(14) NULL,
`birth_certificate_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,
`relationship` VARCHAR(30) NOT NULL DEFAULT 'son',
`school_faculty` VARCHAR(200) NULL,
`nationality` VARCHAR(100) NULL DEFAULT 'مصري',
`classification` VARCHAR(50) NOT NULL DEFAULT 'included',
`addition_fee` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`fee_receipt_number` VARCHAR(50) NULL,
`status` VARCHAR(50) NOT NULL DEFAULT 'active',
`is_frozen` TINYINT(1) NOT NULL DEFAULT 0,
`frozen_at` TIMESTAMP NULL DEFAULT NULL,
`frozen_reason` VARCHAR(200) NULL,
`photo_path` VARCHAR(500) NULL,
`remarks` 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_children_member` (`member_id`),
INDEX `idx_children_nid` (`national_id`),
INDEX `idx_children_status` (`status`),
INDEX `idx_children_classification` (`classification`),
INDEX `idx_children_frozen` (`is_frozen`),
INDEX `idx_children_dob` (`date_of_birth`),
INDEX `idx_children_archived` (`is_archived`),
CONSTRAINT `fk_children_member` FOREIGN KEY (`member_id`) REFERENCES `members`(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `children`",
];
\ 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