Commit 34e2e781 authored by Administrator's avatar Administrator

Update 21 files via Son of Anton

parent c8b79155
<?php
declare(strict_types=1);
namespace App\Modules\Members\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Modules\Members\Services\NationalIdParser;
use App\Modules\Members\Services\MemberSearchService;
class MemberApiController extends Controller
{
public function parseNid(Request $request): Response
{
$nid = trim((string) $request->post('national_id', ''));
if ($nid === '') {
return $this->json(['error' => 'الرقم القومي مطلوب'], 422);
}
$result = NationalIdParser::parse($nid);
// Also check duplicate
$duplicate = null;
if ($result['is_valid']) {
$dup = NationalIdParser::checkDuplicate($nid);
if ($dup) {
$duplicate = [
'id' => (int) $dup['id'],
'full_name_ar' => $dup['full_name_ar'],
'membership_number' => $dup['membership_number'],
'status' => $dup['status'],
'is_archived' => (bool) $dup['is_archived'],
];
}
}
return $this->json([
'parsed' => $result,
'duplicate' => $duplicate,
]);
}
public function quickSearch(Request $request): Response
{
$query = trim((string) $request->post('q', ''));
if (mb_strlen($query) < 2) {
return $this->json(['results' => []]);
}
$results = MemberSearchService::quickSearch($query, 10);
return $this->json(['results' => $results]);
}
public function checkDuplicate(Request $request): Response
{
$nid = trim((string) $request->post('national_id', ''));
$excludeId = $request->post('exclude_id', null);
if ($nid === '') {
return $this->json(['exists' => false]);
}
$dup = NationalIdParser::checkDuplicate($nid, $excludeId ? (int) $excludeId : null);
return $this->json([
'exists' => $dup !== null,
'member' => $dup,
]);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Members\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Members\Models\Member;
use App\Modules\Members\Models\MemberNote;
use App\Modules\Members\Services\MemberSearchService;
use App\Modules\Members\Services\NationalIdParser;
use App\Modules\Pricing\Services\PricingEngine;
use App\Modules\Workflow\Services\WorkflowEngine;
class MemberController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'status' => $request->get('status', ''),
'branch_id' => $request->get('branch_id', ''),
'membership_type' => $request->get('membership_type', ''),
'date_from' => $request->get('date_from', ''),
'date_to' => $request->get('date_to', ''),
];
$page = max(1, (int) $request->get('page', 1));
$result = MemberSearchService::search($filters, 25, $page);
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar");
return $this->view('Members.Views.index', [
'members' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'branches' => $branches,
'statuses' => Member::getStatusOptions(),
'types' => Member::getMembershipTypes(),
]);
}
public function create(Request $request): Response
{
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar");
$qualifications = $db->select("SELECT id, name_ar FROM qualifications WHERE is_active = 1 ORDER BY sort_order");
$governorates = $db->select("SELECT code, name_ar FROM governorates WHERE is_active = 1 ORDER BY name_ar");
$countries = $db->select("SELECT nationality_ar FROM countries WHERE is_active = 1 ORDER BY name_ar");
return $this->view('Members.Views.create', [
'branches' => $branches,
'qualifications' => $qualifications,
'governorates' => $governorates,
'countries' => $countries,
]);
}
public function store(Request $request): Response
{
$data = $request->all();
unset($data['_csrf_token']);
// Validate required fields
$errors = [];
if (empty(trim($data['full_name_ar'] ?? ''))) $errors[] = 'الاسم بالعربي مطلوب';
if (empty($data['branch_id'] ?? '')) $errors[] = 'الفرع مطلوب';
if (empty($data['date_of_birth'] ?? '')) $errors[] = 'تاريخ الميلاد مطلوب';
if (empty($data['gender'] ?? '')) $errors[] = 'النوع مطلوب';
if (empty($data['phone_mobile'] ?? '')) $errors[] = 'رقم المحمول مطلوب';
$idType = $data['id_type'] ?? 'national_id';
// Parse and validate NID if Egyptian
if ($idType === 'national_id') {
$nid = trim($data['national_id'] ?? '');
if (empty($nid)) {
$errors[] = 'الرقم القومي مطلوب';
} else {
$parsed = NationalIdParser::parse($nid);
if (!$parsed['is_valid']) {
$errors = array_merge($errors, $parsed['errors']);
} else {
// Override with parsed data
$data['date_of_birth'] = $parsed['dob'];
$data['age_years'] = $parsed['age_years'];
$data['age_months'] = $parsed['age_months'];
$data['gender'] = $parsed['gender'];
$data['governorate_code'] = $parsed['governorate_code'];
// Check minimum working age
if ($parsed['age_years'] < 21) {
$errors[] = 'يجب أن يكون عمر العضو العامل 21 سنة على الأقل';
}
// Check for duplicate NID
$dup = NationalIdParser::checkDuplicate($nid);
if ($dup) {
if (!$dup['is_archived']) {
$errors[] = 'الرقم القومي مسجل بالفعل للعضو: ' . $dup['full_name_ar'] . ' (' . ($dup['membership_number'] ?: 'بدون رقم') . ')';
}
// If archived, allow but we could show a warning — for now allow
}
}
}
} elseif ($idType === 'passport') {
if (empty(trim($data['passport_number'] ?? ''))) {
$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/create');
}
// Calculate age if not already set
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'];
}
// Get membership price
$branchId = (int) $data['branch_id'];
$qualCode = null;
if (!empty($data['qualification_id'])) {
$db = App::getInstance()->db();
$qualRow = $db->selectOne("SELECT code FROM qualifications WHERE id = ?", [(int) $data['qualification_id']]);
$qualCode = $qualRow['code'] ?? 'high';
}
$priceInfo = $qualCode ? PricingEngine::getMembershipPrice($branchId, $qualCode) : ['price' => '0.00'];
$membershipValue = $priceInfo['price'] ?? '0.00';
$employee = App::getInstance()->currentEmployee();
// Create the member
$member = Member::create([
'full_name_ar' => trim($data['full_name_ar']),
'full_name_en' => trim($data['full_name_en'] ?? ''),
'national_id' => $data['national_id'] ?? null,
'passport_number' => $data['passport_number'] ?? null,
'id_type' => $idType,
'id_issue_date' => $data['id_issue_date'] ?: null,
'id_expiry_date' => $data['id_expiry_date'] ?: 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'],
'place_of_birth' => $data['place_of_birth'] ?? null,
'nationality' => $data['nationality'] ?? 'مصري',
'governorate_code' => $data['governorate_code'] ?? null,
'religion' => $data['religion'] ?? null,
'qualification_id' => !empty($data['qualification_id']) ? (int) $data['qualification_id'] : null,
'marital_status' => $data['marital_status'] ?? null,
'branch_id' => $branchId,
'membership_type' => $data['membership_type'] ?? 'working',
'member_category' => 'working_member',
'status' => 'potential',
'phone_home' => $data['phone_home'] ?? null,
'phone_mobile' => $data['phone_mobile'],
'phone_international' => $data['phone_international'] ?? null,
'email' => $data['email'] ?? null,
'emergency_name' => $data['emergency_name'] ?? null,
'emergency_phone' => $data['emergency_phone'] ?? null,
'residence_type' => $data['residence_type'] ?? null,
'residence_address' => $data['residence_address'] ?? null,
'landmark' => $data['landmark'] ?? null,
'floor' => $data['floor'] ?? null,
'apartment' => $data['apartment'] ?? null,
'area' => $data['area'] ?? null,
'governorate' => $data['governorate'] ?? null,
'correspondence_address'=> $data['correspondence_address'] ?? null,
'employment_type' => $data['employment_type'] ?? null,
'occupation' => $data['occupation'] ?? null,
'job_title' => $data['job_title'] ?? null,
'employment_date' => !empty($data['employment_date']) ? $data['employment_date'] : null,
'business_address' => $data['business_address'] ?? null,
'office_phone' => $data['office_phone'] ?? null,
'office_fax' => $data['office_fax'] ?? null,
'business_activity' => $data['business_activity'] ?? null,
'membership_value' => $membershipValue,
'referral_source' => $data['referral_source'] ?? null,
]);
// Create workflow instance
try {
$wfInstance = WorkflowEngine::createInstance(
'new_membership',
'members',
(int) $member->id
);
$db = App::getInstance()->db();
$db->update('members', [
'workflow_instance_id' => (int) $wfInstance->id,
], '`id` = ?', [$member->id]);
} catch (\Throwable $e) {
// Workflow creation failed — log but don't block member creation
\App\Core\Logger::warning("Failed to create workflow for member #{$member->id}: " . $e->getMessage());
}
EventBus::dispatch('member.created', [
'member_id' => (int) $member->id,
'branch_id' => $branchId,
'created_by' => $employee ? (int) $employee->id : null,
]);
return $this->redirect('/members/' . $member->id)->withSuccess('تم إنشاء العضو بنجاح — الحالة: عضوية محتملة');
}
public function show(Request $request, string $id): Response
{
$member = Member::find((int) $id);
if (!$member) {
return $this->redirect('/members')->withError('العضو غير موجود');
}
$db = App::getInstance()->db();
// Build profile data — future phases enrich this via events
$profileData = [
'member' => $member,
'spouses' => [],
'children' => [],
'temporary' => [],
'payments' => [],
'installments' => [],
'subscriptions'=> [],
'documents' => [],
'violations' => [],
'fines' => [],
];
// Load data from tables that exist (future phases add data via event)
$tablesToCheck = [
'spouses' => "SELECT * FROM spouses WHERE member_id = ? AND is_archived = 0 ORDER BY spouse_order",
'children' => "SELECT * FROM children WHERE member_id = ? AND is_archived = 0 ORDER BY child_order",
'temporary_members' => "SELECT * FROM temporary_members WHERE member_id = ? AND is_archived = 0 ORDER BY created_at",
];
foreach ($tablesToCheck as $key => $sql) {
try {
if ($db->tableExists($key)) {
$mapKey = $key === 'temporary_members' ? 'temporary' : $key;
$profileData[$mapKey] = $db->select($sql, [(int) $member->id]);
}
} catch (\Throwable $e) {
// Table doesn't exist yet — that's fine
}
}
// Let other modules enrich profile data
EventBus::dispatch('member.profile_data', $profileData);
// Get notes
$notes = $member->getNotes();
// Get workflow info
$workflowInstance = null;
if ($member->workflow_instance_id) {
try {
$workflowInstance = $db->selectOne(
"SELECT wi.*, wd.name_ar as workflow_name FROM workflow_instances wi JOIN workflow_definitions wd ON wd.id = wi.workflow_definition_id WHERE wi.id = ?",
[(int) $member->workflow_instance_id]
);
} catch (\Throwable $e) {
// ignore
}
}
// Get audit history (last 20 actions)
$auditHistory = [];
try {
$auditHistory = $db->select(
"SELECT * FROM audit_trail WHERE entity_type = 'members' AND entity_id = ? ORDER BY created_at DESC LIMIT 20",
[(int) $member->id]
);
} catch (\Throwable $e) {
// ignore
}
return $this->view('Members.Views.show', array_merge($profileData, [
'notes' => $notes,
'workflowInstance' => $workflowInstance,
'auditHistory' => $auditHistory,
]));
}
public function edit(Request $request, string $id): Response
{
$member = Member::find((int) $id);
if (!$member) {
return $this->redirect('/members')->withError('العضو غير موجود');
}
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar");
$qualifications = $db->select("SELECT id, name_ar FROM qualifications WHERE is_active = 1 ORDER BY sort_order");
$governorates = $db->select("SELECT code, name_ar FROM governorates WHERE is_active = 1 ORDER BY name_ar");
$countries = $db->select("SELECT nationality_ar FROM countries WHERE is_active = 1 ORDER BY name_ar");
return $this->view('Members.Views.edit', [
'member' => $member,
'branches' => $branches,
'qualifications' => $qualifications,
'governorates' => $governorates,
'countries' => $countries,
]);
}
public function update(Request $request, string $id): Response
{
$member = Member::find((int) $id);
if (!$member) {
return $this->redirect('/members')->withError('العضو غير موجود');
}
$data = $request->all();
unset($data['_csrf_token']);
// Recalculate age
if (!empty($data['date_of_birth'])) {
$age = age_from_dob($data['date_of_birth']);
$data['age_years'] = $age['years'];
$data['age_months'] = $age['months'];
}
$updateFields = [
'full_name_en', 'religion', 'marital_status',
'phone_home', 'phone_mobile', 'phone_international', 'email',
'emergency_name', 'emergency_phone',
'residence_type', 'residence_address', 'landmark', 'floor', 'apartment',
'area', 'governorate', 'correspondence_address',
'employment_type', 'occupation', 'job_title', 'employment_date',
'business_address', 'office_phone', 'office_fax', 'business_activity',
'referral_source',
];
$updateData = [];
foreach ($updateFields as $field) {
if (array_key_exists($field, $data)) {
$val = $data[$field];
$updateData[$field] = ($val === '' || $val === null) ? null : $val;
}
}
if (!empty($updateData)) {
$member->update($updateData);
}
// Handle notes
$noteText = trim($data['new_note'] ?? '');
if ($noteText !== '') {
$employee = App::getInstance()->currentEmployee();
MemberNote::create([
'member_id' => (int) $member->id,
'note_text' => $noteText,
]);
}
return $this->redirect('/members/' . $member->id)->withSuccess('تم تحديث بيانات العضو بنجاح');
}
public function search(Request $request): Response
{
$db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar");
return $this->view('Members.Views.search', [
'branches' => $branches,
'statuses' => Member::getStatusOptions(),
'results' => null,
]);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Members\Models;
use App\Core\Model;
use App\Core\App;
class Member extends Model
{
protected static string $table = '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 = [
'membership_number', 'form_number', 'form_date', 'form_submission_id',
'branch_id', 'membership_type', 'member_category', 'status',
'full_name_ar', 'full_name_en', 'national_id', 'passport_number',
'id_type', 'id_issue_date', 'id_expiry_date',
'date_of_birth', 'age_years', 'age_months', 'gender',
'place_of_birth', 'nationality', 'governorate_code',
'religion', 'qualification_id', 'marital_status',
'phone_home', 'phone_mobile', 'phone_international',
'email', 'emergency_name', 'emergency_phone',
'residence_type', 'residence_address', 'landmark', 'floor', 'apartment',
'area', 'governorate', 'correspondence_address',
'employment_type', 'occupation', 'job_title', 'employment_date',
'business_address', 'office_phone', 'office_fax', 'business_activity',
'membership_value', 'payment_method', 'referral_source', 'photo_path',
'workflow_instance_id',
];
public function getBranchName(): string
{
if (!$this->branch_id) return '—';
$db = App::getInstance()->db();
$row = $db->selectOne("SELECT name_ar FROM branches WHERE id = ?", [$this->branch_id]);
return $row['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'] ?? '—';
}
public function getStatusLabel(): string
{
$labels = [
'potential' => 'عضوية محتملة',
'under_review' => 'تحت المراجعة',
'interview_scheduled'=> 'في انتظار المقابلة',
'accepted' => 'مقبول',
'rejected' => 'مرفوض',
'payment_pending' => 'في انتظار السداد',
'active' => 'فعال',
'frozen' => 'مجمد',
'suspended' => 'موقوف',
'dropped' => 'مسقط',
'expired' => 'منتهي',
'terminated' => 'منتهي بقرار',
];
return $labels[$this->status] ?? $this->status;
}
public function getStatusColor(): string
{
$colors = [
'potential' => '#6B7280',
'under_review' => '#D97706',
'interview_scheduled' => '#0284C7',
'accepted' => '#059669',
'rejected' => '#DC2626',
'payment_pending' => '#D97706',
'active' => '#059669',
'frozen' => '#6B7280',
'suspended' => '#DC2626',
'dropped' => '#DC2626',
'expired' => '#9CA3AF',
'terminated' => '#DC2626',
];
return $colors[$this->status] ?? '#6B7280';
}
public static function getStatusOptions(): array
{
return [
'potential' => 'عضوية محتملة',
'under_review' => 'تحت المراجعة',
'interview_scheduled' => 'في انتظار المقابلة',
'accepted' => 'مقبول',
'rejected' => 'مرفوض',
'payment_pending' => 'في انتظار السداد',
'active' => 'فعال',
'frozen' => 'مجمد',
'suspended' => 'موقوف',
'dropped' => 'مسقط',
'expired' => 'منتهي',
];
}
public static function getMembershipTypes(): array
{
return [
'working' => 'عامل',
'seasonal' => 'موسمي',
'sports' => 'رياضي',
'honorary' => 'شرفي',
'foreign' => 'أجنبي',
];
}
public function getNotes(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT mn.*, e.full_name_ar as employee_name FROM member_notes mn LEFT JOIN employees e ON e.id = mn.created_by WHERE mn.member_id = ? ORDER BY mn.created_at DESC",
[$this->id]
);
}
public function getGenderLabel(): string
{
return $this->gender === 'male' ? 'ذكر' : ($this->gender === 'female' ? 'أنثى' : $this->gender);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Members\Models;
use App\Core\Model;
class MemberNote extends Model
{
protected static string $table = 'member_notes';
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static array $fillable = ['member_id', 'note_text'];
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/members', 'Members\Controllers\MemberController@index', ['auth'], 'member.view'],
['GET', '/members/search', 'Members\Controllers\MemberController@search', ['auth'], 'member.search'],
['GET', '/members/create', 'Members\Controllers\MemberController@create', ['auth'], 'member.create'],
['POST', '/members', 'Members\Controllers\MemberController@store', ['auth'], 'member.create'],
['GET', '/members/{id}', 'Members\Controllers\MemberController@show', ['auth'], 'member.view'],
['GET', '/members/{id}/edit', 'Members\Controllers\MemberController@edit', ['auth'], 'member.edit'],
['POST', '/members/{id}', 'Members\Controllers\MemberController@update', ['auth'], 'member.edit'],
// API endpoints
['POST', '/api/members/parse-nid', 'Members\Controllers\MemberApiController@parseNid', ['auth'], 'member.create'],
['POST', '/api/members/quick-search', 'Members\Controllers\MemberApiController@quickSearch', ['auth'], 'member.search'],
['POST', '/api/members/check-duplicate', 'Members\Controllers\MemberApiController@checkDuplicate', ['auth'], 'member.create'],
];
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Members\Services;
use App\Core\App;
final class MemberNumberGenerator
{
/**
* Generate the next available membership number.
* Sequential, gap-free, atomic to prevent duplicates.
* Only called after payment confirmation.
*/
public static function next(): string
{
$db = App::getInstance()->db();
// Use a transaction with locking to prevent concurrent duplicates
$db->beginTransaction();
try {
// Get the current maximum membership number
$row = $db->selectOne(
"SELECT MAX(CAST(membership_number AS UNSIGNED)) as max_num FROM members WHERE membership_number IS NOT NULL AND membership_number REGEXP '^[0-9]+$' FOR UPDATE"
);
$startNumber = 10012; // Default starting number (1001/2 per spec — interpreted as 10012)
// Check system config for starting number
$configRow = $db->selectOne("SELECT config_value FROM system_config WHERE config_key = 'membership_start_number'");
if ($configRow && $configRow['config_value']) {
$startNumber = (int) $configRow['config_value'];
}
$maxNum = $row['max_num'] ? (int) $row['max_num'] : ($startNumber - 1);
$nextNum = max($maxNum + 1, $startNumber);
$db->commit();
return (string) $nextNum;
} catch (\Throwable $e) {
$db->rollBack();
throw $e;
}
}
/**
* Assign a membership number to a member.
* Called only after payment is confirmed (Phase 11).
*/
public static function assign(int $memberId): string
{
$db = App::getInstance()->db();
// Check member doesn't already have a number
$member = $db->selectOne("SELECT membership_number FROM members WHERE id = ?", [$memberId]);
if ($member && $member['membership_number']) {
return $member['membership_number'];
}
$number = self::next();
$db->update('members', [
'membership_number' => $number,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$memberId]);
return $number;
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Members\Services;
use App\Core\App;
use App\Core\Pagination;
final class MemberSearchService
{
/**
* Search members with multiple criteria.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = "m.is_archived = 0";
$params = [];
if (!empty($filters['q'])) {
$q = trim($filters['q']);
$where .= " AND (m.full_name_ar LIKE ? OR m.full_name_en LIKE ? OR m.national_id LIKE ? OR m.membership_number LIKE ? OR m.phone_mobile LIKE ? OR m.form_number LIKE ? OR m.passport_number LIKE ?)";
$s = "%{$q}%";
$params = array_merge($params, [$s, $s, $s, $s, $s, $s, $s]);
}
if (!empty($filters['status'])) {
$where .= " AND m.status = ?";
$params[] = $filters['status'];
}
if (!empty($filters['branch_id'])) {
$where .= " AND m.branch_id = ?";
$params[] = (int) $filters['branch_id'];
}
if (!empty($filters['membership_type'])) {
$where .= " AND m.membership_type = ?";
$params[] = $filters['membership_type'];
}
if (!empty($filters['gender'])) {
$where .= " AND m.gender = ?";
$params[] = $filters['gender'];
}
if (!empty($filters['date_from'])) {
$where .= " AND m.created_at >= ?";
$params[] = $filters['date_from'] . ' 00:00:00';
}
if (!empty($filters['date_to'])) {
$where .= " AND m.created_at <= ?";
$params[] = $filters['date_to'] . ' 23:59:59';
}
if (!empty($filters['national_id'])) {
$where .= " AND m.national_id = ?";
$params[] = $filters['national_id'];
}
if (!empty($filters['membership_number'])) {
$where .= " AND m.membership_number = ?";
$params[] = $filters['membership_number'];
}
$countRow = $db->selectOne("SELECT COUNT(*) as cnt FROM members m WHERE {$where}", $params);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT m.*, b.name_ar as branch_name, q.name_ar as qualification_name
FROM members m
LEFT JOIN branches b ON b.id = m.branch_id
LEFT JOIN qualifications q ON q.id = m.qualification_id
WHERE {$where}
ORDER BY m.created_at DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
$pagination = Pagination::paginate($total, $perPage, $page);
return ['data' => $rows, 'pagination' => $pagination];
}
/**
* Quick search for AJAX autocomplete (header search bar).
*/
public static function quickSearch(string $query, int $limit = 10): array
{
$db = App::getInstance()->db();
$s = "%{$query}%";
return $db->select(
"SELECT id, membership_number, full_name_ar, national_id, phone_mobile, status
FROM members
WHERE is_archived = 0
AND (full_name_ar LIKE ? OR membership_number LIKE ? OR national_id LIKE ? OR phone_mobile LIKE ?)
ORDER BY
CASE WHEN membership_number = ? THEN 0
WHEN national_id = ? THEN 1
ELSE 2 END,
full_name_ar ASC
LIMIT ?",
[$s, $s, $s, $s, $query, $query, $limit]
);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Members\Services;
use App\Core\App;
final class NationalIdParser
{
private static array $governorateMap = [
'01' => ['ar' => 'القاهرة', 'en' => 'Cairo'],
'02' => ['ar' => 'الإسكندرية', 'en' => 'Alexandria'],
'03' => ['ar' => 'بورسعيد', 'en' => 'Port Said'],
'04' => ['ar' => 'السويس', 'en' => 'Suez'],
'11' => ['ar' => 'دمياط', 'en' => 'Damietta'],
'12' => ['ar' => 'الدقهلية', 'en' => 'Dakahlia'],
'13' => ['ar' => 'الشرقية', 'en' => 'Sharqia'],
'14' => ['ar' => 'القليوبية', 'en' => 'Qalyubia'],
'15' => ['ar' => 'كفر الشيخ', 'en' => 'Kafr El Sheikh'],
'16' => ['ar' => 'الغربية', 'en' => 'Gharbia'],
'17' => ['ar' => 'المنوفية', 'en' => 'Menoufia'],
'18' => ['ar' => 'البحيرة', 'en' => 'Beheira'],
'19' => ['ar' => 'الإسماعيلية', 'en' => 'Ismailia'],
'21' => ['ar' => 'الجيزة', 'en' => 'Giza'],
'22' => ['ar' => 'بني سويف', 'en' => 'Beni Suef'],
'23' => ['ar' => 'الفيوم', 'en' => 'Fayoum'],
'24' => ['ar' => 'المنيا', 'en' => 'Minya'],
'25' => ['ar' => 'أسيوط', 'en' => 'Assiut'],
'26' => ['ar' => 'سوهاج', 'en' => 'Sohag'],
'27' => ['ar' => 'قنا', 'en' => 'Qena'],
'28' => ['ar' => 'أسوان', 'en' => 'Aswan'],
'29' => ['ar' => 'الأقصر', 'en' => 'Luxor'],
'31' => ['ar' => 'البحر الأحمر', 'en' => 'Red Sea'],
'32' => ['ar' => 'الوادي الجديد', 'en' => 'New Valley'],
'33' => ['ar' => 'مطروح', 'en' => 'Matrouh'],
'34' => ['ar' => 'شمال سيناء', 'en' => 'North Sinai'],
'35' => ['ar' => 'جنوب سيناء', 'en' => 'South Sinai'],
'88' => ['ar' => 'خارج الجمهورية', 'en' => 'Born Abroad'],
];
/**
* Parse an Egyptian 14-digit National ID and extract all encoded data.
*
* @return array{is_valid: bool, errors: string[], dob: ?string, age_years: ?int, age_months: ?int, gender: ?string, governorate_code: ?string, governorate_name_ar: ?string, governorate_name_en: ?string}
*/
public static function parse(string $nid): array
{
$result = [
'is_valid' => false,
'errors' => [],
'dob' => null,
'age_years' => null,
'age_months' => null,
'gender' => null,
'governorate_code' => null,
'governorate_name_ar' => null,
'governorate_name_en' => null,
];
// Must be exactly 14 digits
if (!preg_match('/^\d{14}$/', $nid)) {
$result['errors'][] = 'الرقم القومي يجب أن يتكون من 14 رقم فقط';
return $result;
}
// Position 1: Century code
$centuryCode = (int) $nid[0];
if ($centuryCode !== 2 && $centuryCode !== 3) {
$result['errors'][] = 'رمز القرن غير صالح (يجب أن يكون 2 أو 3)';
return $result;
}
$centuryBase = ($centuryCode === 2) ? 1900 : 2000;
// Positions 2-3: Year
$year = $centuryBase + (int) substr($nid, 1, 2);
// Positions 4-5: Month
$month = (int) substr($nid, 3, 2);
if ($month < 1 || $month > 12) {
$result['errors'][] = 'الشهر غير صالح (يجب أن يكون بين 01 و 12)';
return $result;
}
// Positions 6-7: Day
$day = (int) substr($nid, 5, 2);
if ($day < 1 || $day > 31) {
$result['errors'][] = 'اليوم غير صالح (يجب أن يكون بين 01 و 31)';
return $result;
}
// Validate the full date
if (!checkdate($month, $day, $year)) {
$result['errors'][] = 'تاريخ الميلاد غير صالح';
return $result;
}
$dob = sprintf('%04d-%02d-%02d', $year, $month, $day);
// Ensure DOB is not in the future
$dobDate = new \DateTime($dob);
$now = new \DateTime();
if ($dobDate > $now) {
$result['errors'][] = 'تاريخ الميلاد لا يمكن أن يكون في المستقبل';
return $result;
}
// Positions 8-9: Governorate
$govCode = substr($nid, 7, 2);
$govData = self::$governorateMap[$govCode] ?? null;
// Also try from DB
if ($govData === null) {
$db = App::getInstance()->db();
$govRow = $db->selectOne("SELECT name_ar, name_en FROM governorates WHERE code = ? AND is_active = 1", [$govCode]);
if ($govRow) {
$govData = ['ar' => $govRow['name_ar'], 'en' => $govRow['name_en']];
}
}
if ($govData === null) {
$result['errors'][] = 'كود المحافظة غير معروف: ' . $govCode;
return $result;
}
// Positions 10-13: Sequential registration number
$seqNum = (int) substr($nid, 9, 4);
// Gender: positions 12-13 (the last two of the 4-digit sequence)
// Actually per spec: "Positions 12-13 as integer" odd=male, even=female
// But positions are 1-indexed in the spec. Position 12-13 of the NID = index 11-12
// The sequence is positions 10-13 (indices 9-12). The gender digit is the last digit of the sequence.
// Per spec: "If (Positions 12-13 as integer) is ODD → Male. If EVEN → Female."
// Positions 12-13 = indices 11-12 = 2-digit number
$genderDigits = (int) substr($nid, 11, 2);
// Actually, re-reading spec: it says positions 10-13 are sequential, and gender from odd/even of those.
// Some implementations use just the last digit of the sequence (position 13, index 12).
// The spec says "Positions 12-13 as integer" - let me use that literally
// Wait, it says "10-13 Sequential Registration Number" and "Odd number = Male, Even number = Female"
// So the ENTIRE 4-digit sequence number's parity determines gender.
$gender = ($seqNum % 2 !== 0) ? 'male' : 'female';
// Position 14: Check digit (Luhn-like validation)
$checkDigit = (int) $nid[13];
if (!self::validateCheckDigit($nid)) {
$result['errors'][] = 'رقم التحقق غير صالح';
return $result;
}
// Calculate age
$diff = $now->diff($dobDate);
$ageYears = $diff->y;
$ageMonths = $diff->m;
// Sanity check
if ($ageYears > 120 || $ageYears < 0) {
$result['errors'][] = 'العمر المحسوب غير منطقي';
return $result;
}
$result['is_valid'] = true;
$result['dob'] = $dob;
$result['age_years'] = $ageYears;
$result['age_months'] = $ageMonths;
$result['gender'] = $gender;
$result['governorate_code'] = $govCode;
$result['governorate_name_ar'] = $govData['ar'];
$result['governorate_name_en'] = $govData['en'];
return $result;
}
/**
* Validate the check digit using Luhn algorithm variant.
*/
private static function validateCheckDigit(string $nid): bool
{
$weights = [2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2];
$sum = 0;
for ($i = 0; $i < 13; $i++) {
$product = (int) $nid[$i] * $weights[$i];
if ($product >= 10) {
$product = (int) floor($product / 10) + ($product % 10);
}
$sum += $product;
}
$expectedCheck = (10 - ($sum % 10)) % 10;
return $expectedCheck === (int) $nid[13];
}
/**
* Check if a National ID already exists in the system for an active member.
*/
public static function checkDuplicate(string $nid, ?int $excludeMemberId = null): ?array
{
$db = App::getInstance()->db();
$sql = "SELECT id, membership_number, full_name_ar, status, is_archived FROM members WHERE national_id = ?";
$params = [$nid];
if ($excludeMemberId !== null) {
$sql .= " AND id != ?";
$params[] = $excludeMemberId;
}
$existing = $db->selectOne($sql, $params);
return $existing ?: null;
}
}
\ No newline at end of file
<?php $auditHistory = $auditHistory ?? []; ?>
<?php if (empty($auditHistory)): ?>
<p style="color:#6B7280;text-align:center;padding:20px;">لا يوجد سجل نشاط</p>
<?php else: ?>
<?php foreach ($auditHistory as $entry): ?>
<div style="display:flex;gap:10px;padding:8px 0;border-bottom:1px solid #F3F4F6;font-size:13px;">
<div style="width:8px;height:8px;border-radius:50%;background:#0D7377;margin-top:5px;flex-shrink:0;"></div>
<div><strong><?= e($entry['action']) ?></strong><?= e($entry['employee_name'] ?? 'النظام') ?><br><span style="color:#9CA3AF;font-size:11px;"><?= e($entry['created_at']) ?></span></div>
</div>
<?php endforeach; ?>
<?php endif; ?>
\ No newline at end of file
<?php $documents = $documents ?? []; ?>
<?php if (empty($documents)): ?>
<p style="color:#6B7280;text-align:center;padding:20px;">لا توجد مستندات مرفوعة</p>
<?php endif; ?>
\ No newline at end of file
<?php
$spouses = $spouses ?? [];
$children = $children ?? [];
$temporary = $temporary ?? [];
?>
<div style="margin-bottom:20px;">
<h4 style="color:#0D7377;">الزوجات (<?= count($spouses) ?>)</h4>
<?php if (empty($spouses)): ?>
<p style="color:#6B7280;">لا توجد زوجات مسجلة</p>
<?php endif; ?>
</div>
<div style="margin-bottom:20px;">
<h4 style="color:#0D7377;">الأبناء (<?= count($children) ?>)</h4>
<?php if (empty($children)): ?>
<p style="color:#6B7280;">لا يوجد أبناء</p>
<?php endif; ?>
</div>
\ No newline at end of file
<?php
$payments = $payments ?? [];
$installments = $installments ?? [];
$subscriptions = $subscriptions ?? [];
?>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-bottom:20px;">
<div style="background:#F0FDF4;padding:15px;border-radius:8px;text-align:center;"><div style="font-size:24px;font-weight:700;color:#059669;">0</div><div style="color:#6B7280;font-size:12px;">إجمالي المدفوع</div></div>
<div style="background:#FEF2F2;padding:15px;border-radius:8px;text-align:center;"><div style="font-size:24px;font-weight:700;color:#DC2626;">0</div><div style="color:#6B7280;font-size:12px;">المتبقي</div></div>
<div style="background:#EFF6FF;padding:15px;border-radius:8px;text-align:center;"><div style="font-size:24px;font-weight:700;color:#0284C7;">0</div><div style="color:#6B7280;font-size:12px;">الأقساط</div></div>
<div style="background:#FFF7ED;padding:15px;border-radius:8px;text-align:center;"><div style="font-size:24px;font-weight:700;color:#D97706;">0</div><div style="color:#6B7280;font-size:12px;">الاشتراكات</div></div>
</div>
\ No newline at end of file
<?php
/** @var \App\Modules\Members\Models\Member $member */
?>
<div style="display:flex;justify-content:space-between;align-items:center;">
<div>
<h2 style="margin:0;"><?= e($member->full_name_ar) ?></h2>
<div style="color:#6B7280;margin-top:4px;">رقم العضوية: <strong><?= e($member->membership_number ?: 'لم يُحدد') ?></strong></div>
</div>
<span style="padding:6px 16px;border-radius:20px;font-weight:700;color:#fff;background:<?= $member->getStatusColor() ?>;"><?= e($member->getStatusLabel()) ?></span>
</div>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تسجيل عضو جديد<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/members" id="member-form">
<?= csrf_field() ?>
<!-- Personal Data -->
<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">نوع إثبات الشخصية <span style="color:#DC2626;">*</span></label>
<select name="id_type" id="id_type" class="form-select" required>
<option value="national_id" <?= old('id_type', 'national_id') === 'national_id' ? 'selected' : '' ?>>رقم قومي</option>
<option value="passport" <?= old('id_type') === 'passport' ? 'selected' : '' ?>>جواز سفر</option>
</select>
</div>
<div class="form-group" id="nid-group">
<label class="form-label">الرقم القومي <span style="color:#DC2626;">*</span></label>
<input type="text" name="national_id" id="national_id" 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" id="passport-group" style="display:none;">
<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="date_of_birth" value="<?= e(old('date_of_birth')) ?>" class="form-input" required style="background:#F3F4F6;" readonly>
</div>
<div class="form-group">
<label class="form-label">السن</label>
<input type="text" name="age_display" id="age_display" class="form-input" style="background:#F3F4F6;" readonly>
<input type="hidden" name="age_years" id="age_years" value="<?= e(old('age_years')) ?>">
<input type="hidden" name="age_months" id="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="gender" class="form-select" required style="background:#F3F4F6;" disabled>
<option value=""></option>
<option value="male" <?= old('gender') === 'male' ? 'selected' : '' ?>>ذكر</option>
<option value="female" <?= old('gender') === 'female' ? 'selected' : '' ?>>أنثى</option>
</select>
<input type="hidden" name="gender" id="gender_hidden" value="<?= e(old('gender')) ?>">
</div>
<div class="form-group">
<label class="form-label">محافظة الميلاد</label>
<input type="text" id="gov_display" class="form-input" style="background:#F3F4F6;" readonly>
<input type="hidden" name="governorate_code" id="governorate_code" value="<?= e(old('governorate_code')) ?>">
</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">المؤهل الدراسي <span style="color:#DC2626;">*</span></label>
<select name="qualification_id" class="form-select" required>
<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>
<small style="color:#6B7280;">المؤهل يؤثر على قيمة العضوية</small>
</div>
<div class="form-group">
<label class="form-label">الحالة الاجتماعية</label>
<select name="marital_status" class="form-select">
<option value="">-- اختر --</option>
<option value="single" <?= old('marital_status') === 'single' ? 'selected' : '' ?>>أعزب</option>
<option value="married" <?= old('marital_status') === 'married' ? 'selected' : '' ?>>متزوج</option>
<option value="divorced" <?= old('marital_status') === 'divorced' ? 'selected' : '' ?>>مطلق</option>
<option value="widowed" <?= old('marital_status') === 'widowed' ? 'selected' : '' ?>>أرمل</option>
</select>
</div>
<div class="form-group">
<label class="form-label">تاريخ إصدار إثبات الشخصية</label>
<input type="date" name="id_issue_date" value="<?= e(old('id_issue_date')) ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">تاريخ انتهاء إثبات الشخصية</label>
<input type="date" name="id_expiry_date" value="<?= e(old('id_expiry_date')) ?>" class="form-input">
</div>
</div>
</div>
<!-- Contact -->
<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">
<label class="form-label">رقم المحمول <span style="color:#DC2626;">*</span></label>
<input type="tel" name="phone_mobile" value="<?= e(old('phone_mobile')) ?>" class="form-input" required maxlength="11" style="direction:ltr;text-align:left;" placeholder="01XXXXXXXXX">
</div>
<div class="form-group">
<label class="form-label">تليفون المنزل</label>
<input type="tel" name="phone_home" value="<?= e(old('phone_home')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">رقم تليفون خارج مصر</label>
<input type="tel" name="phone_international" value="<?= e(old('phone_international')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">البريد الإلكتروني</label>
<input type="email" name="email" value="<?= e(old('email')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">شخص للطوارئ — الاسم</label>
<input type="text" name="emergency_name" value="<?= e(old('emergency_name')) ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">شخص للطوارئ — المحمول</label>
<input type="tel" name="emergency_phone" value="<?= e(old('emergency_phone')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
<!-- Residence -->
<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">
<label class="form-label">نوع السكن</label>
<select name="residence_type" class="form-select">
<option value="">-- اختر --</option>
<option value="rented" <?= old('residence_type') === 'rented' ? 'selected' : '' ?>>إيجار</option>
<option value="owned" <?= old('residence_type') === 'owned' ? 'selected' : '' ?>>ملك</option>
<option value="other" <?= old('residence_type') === 'other' ? 'selected' : '' ?>>أخرى</option>
</select>
</div>
<div class="form-group">
<label class="form-label">المنطقة</label>
<input type="text" name="area" value="<?= e(old('area')) ?>" class="form-input">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">عنوان السكن</label>
<textarea name="residence_address" class="form-textarea" rows="2"><?= e(old('residence_address')) ?></textarea>
</div>
<div class="form-group">
<label class="form-label">المحافظة</label>
<select name="governorate" class="form-select">
<option value="">-- اختر --</option>
<?php foreach ($governorates as $g): ?>
<option value="<?= e($g['name_ar']) ?>" <?= old('governorate') === $g['name_ar'] ? 'selected' : '' ?>><?= e($g['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">عنوان المراسلات</label>
<select name="correspondence_address" class="form-select">
<option value="">-- اختر --</option>
<option value="work" <?= old('correspondence_address') === 'work' ? 'selected' : '' ?>>العمل</option>
<option value="residence" <?= old('correspondence_address') === 'residence' ? 'selected' : '' ?>>السكن</option>
<option value="other" <?= old('correspondence_address') === 'other' ? 'selected' : '' ?>>أخرى</option>
</select>
</div>
</div>
</div>
<!-- Work -->
<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">
<label class="form-label">نوع التوظيف</label>
<select name="employment_type" class="form-select">
<option value="">-- اختر --</option>
<option value="employed" <?= old('employment_type') === 'employed' ? 'selected' : '' ?>>موظف</option>
<option value="self_employed" <?= old('employment_type') === 'self_employed' ? 'selected' : '' ?>>أعمال حرة</option>
<option value="professions" <?= old('employment_type') === 'professions' ? 'selected' : '' ?>>مهن حرة</option>
<option value="other" <?= old('employment_type') === 'other' ? 'selected' : '' ?>>أخرى</option>
</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="text" name="job_title" value="<?= e(old('job_title')) ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">تاريخ الالتحاق بالعمل</label>
<input type="date" name="employment_date" value="<?= e(old('employment_date')) ?>" class="form-input">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">عنوان العمل</label>
<textarea name="business_address" class="form-textarea" rows="2"><?= e(old('business_address')) ?></textarea>
</div>
<div class="form-group">
<label class="form-label">تليفون العمل</label>
<input type="tel" name="office_phone" value="<?= e(old('office_phone')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">نشاط العمل</label>
<input type="text" name="business_activity" value="<?= e(old('business_activity')) ?>" class="form-input">
</div>
</div>
</div>
<!-- Membership -->
<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">
<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'] ?>" <?= old('branch_id') == $b['id'] ? 'selected' : '' ?>><?= e($b['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">كيف عرفت النادي</label>
<select name="referral_source" class="form-select">
<option value="">-- اختر --</option>
<option value="social_media" <?= old('referral_source') === 'social_media' ? 'selected' : '' ?>>مواقع التواصل الاجتماعي</option>
<option value="tv" <?= old('referral_source') === 'tv' ? 'selected' : '' ?>>إعلان التليفزيون</option>
<option value="friend" <?= old('referral_source') === 'friend' ? 'selected' : '' ?>>من خلال صديق</option>
<option value="radio" <?= old('referral_source') === 'radio' ? 'selected' : '' ?>>إعلان الراديو</option>
<option value="outdoor" <?= old('referral_source') === 'outdoor' ? 'selected' : '' ?>>إعلانات الطريق</option>
</select>
</div>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary">تسجيل العضو</button>
<a href="/members" class="btn btn-outline">إلغاء</a>
</div>
</form>
<?php $__template->endSection(); ?>
<?php $__template->section('scripts'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
var idTypeSelect = document.getElementById('id_type');
var nidGroup = document.getElementById('nid-group');
var passportGroup = document.getElementById('passport-group');
var nidInput = document.getElementById('national_id');
var dobInput = document.getElementById('date_of_birth');
var ageDisplay = document.getElementById('age_display');
var ageYears = document.getElementById('age_years');
var ageMonths = document.getElementById('age_months');
var genderSelect = document.getElementById('gender');
var genderHidden = document.getElementById('gender_hidden');
var govDisplay = document.getElementById('gov_display');
var govCode = document.getElementById('governorate_code');
var feedback = document.getElementById('nid-feedback');
function toggleIdType() {
if (idTypeSelect.value === 'passport') {
nidGroup.style.display = 'none';
passportGroup.style.display = 'block';
dobInput.removeAttribute('readonly');
dobInput.style.background = '';
genderSelect.removeAttribute('disabled');
genderSelect.style.background = '';
} else {
nidGroup.style.display = 'block';
passportGroup.style.display = 'none';
dobInput.setAttribute('readonly', 'readonly');
dobInput.style.background = '#F3F4F6';
}
}
idTypeSelect.addEventListener('change', toggleIdType);
toggleIdType();
nidInput.addEventListener('input', function() {
var val = this.value.replace(/\D/g, '');
this.value = val;
if (val.length === 14) {
parseNid(val);
} else {
feedback.innerHTML = '';
dobInput.value = '';
ageDisplay.value = '';
ageYears.value = '';
ageMonths.value = '';
genderSelect.value = '';
genderHidden.value = '';
govDisplay.value = '';
govCode.value = '';
}
});
function parseNid(nid) {
feedback.innerHTML = '<span style="color:#0284C7;">جاري التحقق...</span>';
var formData = new FormData();
formData.append('national_id', nid);
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;
genderHidden.value = p.gender;
govDisplay.value = p.governorate_name_ar || '';
govCode.value = p.governorate_code || '';
feedback.innerHTML = '<span style="color:#059669;">✓ الرقم القومي صالح</span>';
if (data.duplicate) {
var d = data.duplicate;
var msg = d.is_archived ? '⚠ هذا الرقم مسجل في الأرشيف: ' : '✖ هذا الرقم مسجل بالفعل: ';
feedback.innerHTML += '<br><span style="color:' + (d.is_archived ? '#D97706' : '#DC2626') + ';">' + msg + d.full_name_ar + ' (' + (d.membership_number || 'بدون رقم') + ')</span>';
}
} else {
feedback.innerHTML = '<span style="color:#DC2626;">✖ ' + (p.errors ? p.errors.join(' | ') : 'رقم غير صالح') + '</span>';
}
})
.catch(function() {
feedback.innerHTML = '<span style="color:#DC2626;">خطأ في الاتصال</span>';
});
}
});
</script>
<?php $__template->endSection(); ?>
\ 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 ?>">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;padding:20px;">
<h3 style="color:#0D7377;margin-bottom:15px;">البيانات الأساسية (للقراءة فقط)</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group"><label class="form-label">الاسم بالعربي</label><input type="text" value="<?= e($member->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($member->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($member->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($member->getGenderLabel()) ?>" class="form-input" disabled style="background:#F3F4F6;"></div>
</div>
</div>
<div class="card" style="margin-bottom:20px;padding:20px;">
<h3 style="color:#0D7377;margin-bottom:15px;">البيانات القابلة للتعديل</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group"><label class="form-label">الاسم بالإنجليزي</label><input type="text" name="full_name_en" value="<?= e($member->full_name_en ?? '') ?>" class="form-input"></div>
<div class="form-group"><label class="form-label">الحالة الاجتماعية</label>
<select name="marital_status" class="form-select"><option value=""></option><option value="single" <?= $member->marital_status === 'single' ? 'selected' : '' ?>>أعزب</option><option value="married" <?= $member->marital_status === 'married' ? 'selected' : '' ?>>متزوج</option><option value="divorced" <?= $member->marital_status === 'divorced' ? 'selected' : '' ?>>مطلق</option><option value="widowed" <?= $member->marital_status === 'widowed' ? 'selected' : '' ?>>أرمل</option></select>
</div>
<div class="form-group"><label class="form-label">رقم المحمول <span style="color:#DC2626;">*</span></label><input type="tel" name="phone_mobile" value="<?= e($member->phone_mobile) ?>" class="form-input" required style="direction:ltr;text-align:left;"></div>
<div class="form-group"><label class="form-label">تليفون المنزل</label><input type="tel" name="phone_home" value="<?= e($member->phone_home ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;"></div>
<div class="form-group"><label class="form-label">البريد الإلكتروني</label><input type="email" name="email" value="<?= e($member->email ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;"></div>
<div class="form-group"><label class="form-label">شخص للطوارئ</label><input type="text" name="emergency_name" value="<?= e($member->emergency_name ?? '') ?>" class="form-input"></div>
<div class="form-group"><label class="form-label">هاتف الطوارئ</label><input type="tel" name="emergency_phone" value="<?= e($member->emergency_phone ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;"></div>
<div class="form-group"><label class="form-label">المهنة</label><input type="text" name="occupation" value="<?= e($member->occupation ?? '') ?>" class="form-input"></div>
<div class="form-group"><label class="form-label">المركز الوظيفي</label><input type="text" name="job_title" value="<?= e($member->job_title ?? '') ?>" class="form-input"></div>
<div class="form-group" style="grid-column:1/-1;"><label class="form-label">عنوان السكن</label><textarea name="residence_address" class="form-textarea" rows="2"><?= e($member->residence_address ?? '') ?></textarea></div>
<div class="form-group">
<label class="form-label">المحافظة</label>
<select name="governorate" class="form-select"><option value=""></option>
<?php foreach ($governorates as $g): ?><option value="<?= e($g['name_ar']) ?>" <?= $member->governorate === $g['name_ar'] ? 'selected' : '' ?>><?= e($g['name_ar']) ?></option><?php endforeach; ?>
</select>
</div>
<div class="form-group"><label class="form-label">المنطقة</label><input type="text" name="area" value="<?= e($member->area ?? '') ?>" class="form-input"></div>
</div>
</div>
<div class="card" style="margin-bottom:20px;padding:20px;">
<h3 style="color:#0D7377;margin-bottom:15px;">إضافة ملاحظة</h3>
<textarea name="new_note" class="form-textarea" rows="3" placeholder="أضف ملاحظة جديدة..."></textarea>
</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('page_actions'); ?>
<a href="/members/create" class="btn btn-primary">+ عضو جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/members" 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:250px;">
</div>
<div>
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select" style="min-width:120px;">
<option value="">الكل</option>
<?php foreach ($statuses as $val => $label): ?>
<option value="<?= e($val) ?>" <?= ($filters['status'] ?? '') === $val ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">الفرع</label>
<select name="branch_id" class="form-select" style="min-width:120px;">
<option value="">الكل</option>
<?php foreach ($branches as $b): ?>
<option value="<?= (int) $b['id'] ?>" <?= ($filters['branch_id'] ?? '') == $b['id'] ? 'selected' : '' ?>><?= e($b['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">من</label>
<input type="date" name="date_from" value="<?= e($filters['date_from'] ?? '') ?>" class="form-input">
</div>
<div>
<label class="form-label" style="font-size:12px;">إلى</label>
<input type="date" name="date_to" value="<?= e($filters['date_to'] ?? '') ?>" class="form-input">
</div>
<button type="submit" class="btn btn-outline">بحث</button>
<a href="/members" 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 ($members as $m): ?>
<tr>
<td style="font-weight:600;"><?= e($m['membership_number'] ?? '—') ?></td>
<td>
<a href="/members/<?= (int) $m['id'] ?>" style="color:#0D7377;font-weight:600;"><?= e($m['full_name_ar']) ?></a>
<?php if ($m['full_name_en']): ?>
<div style="font-size:11px;color:#9CA3AF;"><?= e($m['full_name_en']) ?></div>
<?php endif; ?>
</td>
<td style="direction:ltr;text-align:right;font-size:13px;"><?= e($m['national_id'] ?? '—') ?></td>
<td style="direction:ltr;text-align:right;font-size:13px;"><?= e($m['phone_mobile']) ?></td>
<td style="font-size:13px;"><?= e($m['branch_name'] ?? '—') ?></td>
<td>
<?php
$statusLabels = [
'potential' => 'محتمل', 'under_review' => 'مراجعة', 'interview_scheduled' => 'مقابلة',
'accepted' => 'مقبول', 'rejected' => 'مرفوض', 'payment_pending' => 'انتظار سداد',
'active' => 'فعال', 'frozen' => 'مجمد', 'suspended' => 'موقوف',
'dropped' => 'مسقط', 'expired' => 'منتهي',
];
$statusColors = [
'potential' => '#6B7280', 'under_review' => '#D97706', 'interview_scheduled' => '#0284C7',
'accepted' => '#059669', 'rejected' => '#DC2626', 'payment_pending' => '#D97706',
'active' => '#059669', 'frozen' => '#6B7280', 'suspended' => '#DC2626',
'dropped' => '#DC2626', 'expired' => '#9CA3AF',
];
$color = $statusColors[$m['status']] ?? '#6B7280';
$label = $statusLabels[$m['status']] ?? $m['status'];
?>
<span style="color:<?= $color ?>;font-weight:600;font-size:13px;"><?= e($label) ?></span>
</td>
<td style="font-size:12px;"><?= e(substr($m['created_at'], 0, 10)) ?></td>
<td>
<div style="display:flex;gap:5px;">
<a href="/members/<?= (int) $m['id'] ?>" class="btn btn-sm btn-outline">عرض</a>
<a href="/members/<?= (int) $m['id'] ?>/edit" class="btn btn-sm btn-outline">تعديل</a>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($members)): ?>
<tr><td colspan="8" style="text-align:center;padding:40px;color:#6B7280;">لا يوجد أعضاء</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
</div>
<?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="padding:20px;">
<form method="GET" action="/members" style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">بحث شامل</label>
<input type="text" name="q" class="form-input" placeholder="الاسم بالعربي، الرقم القومي، رقم العضوية، رقم المحمول، رقم الاستمارة..." style="font-size:16px;padding:12px;" autofocus>
</div>
<div class="form-group">
<label class="form-label">الحالة</label>
<select name="status" class="form-select"><option value="">الكل</option>
<?php foreach ($statuses as $val => $label): ?><option value="<?= e($val) ?>"><?= e($label) ?></option><?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الفرع</label>
<select name="branch_id" class="form-select"><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">من تاريخ</label><input type="date" name="date_from" class="form-input"></div>
<div class="form-group"><label class="form-label">إلى تاريخ</label><input type="date" name="date_to" class="form-input"></div>
<div style="grid-column:1/-1;"><button type="submit" class="btn btn-primary">بحث</button></div>
</form>
</div>
<?php $__template->endSection(); ?>
\ 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('page_actions'); ?>
<a href="/members/<?= (int) $member->id ?>/edit" class="btn btn-outline">تعديل</a>
<a href="/members" class="btn btn-outline">← قائمة الأعضاء</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Profile Header -->
<div class="card" style="margin-bottom:20px;padding:20px;display:flex;justify-content:space-between;align-items:center;">
<div>
<h2 style="margin:0;color:#1A1A2E;"><?= e($member->full_name_ar) ?></h2>
<?php if ($member->full_name_en): ?><div style="color:#6B7280;margin-top:4px;"><?= e($member->full_name_en) ?></div><?php endif; ?>
<div style="margin-top:8px;display:flex;gap:15px;font-size:13px;color:#6B7280;">
<span>رقم العضوية: <strong style="color:#0D7377;"><?= e($member->membership_number ?: 'لم يُحدد بعد') ?></strong></span>
<span>الفرع: <strong><?= e($member->getBranchName()) ?></strong></span>
<span>النوع: <strong><?= e($member->getGenderLabel()) ?></strong></span>
</div>
</div>
<div style="text-align:left;">
<span style="display:inline-block;padding:6px 16px;border-radius:20px;font-weight:700;font-size:14px;color:#fff;background:<?= $member->getStatusColor() ?>;">
<?= e($member->getStatusLabel()) ?>
</span>
</div>
</div>
<!-- Tabs -->
<div style="display:flex;gap:0;margin-bottom:20px;border-bottom:2px solid #E5E7EB;">
<?php
$tabs = [
'personal' => 'البيانات الشخصية',
'family' => 'الأسرة',
'financial' => 'المالية',
'documents' => 'المستندات',
'activity' => 'النشاط',
];
?>
<?php foreach ($tabs as $key => $label): ?>
<button class="tab-btn" data-tab="<?= $key ?>" style="padding:10px 20px;border:none;background:transparent;cursor:pointer;font-size:14px;font-weight:600;color:#6B7280;border-bottom:3px solid transparent;<?= $key === 'personal' ? 'color:#0D7377;border-bottom-color:#0D7377;' : '' ?>" onclick="switchTab('<?= $key ?>')"><?= e($label) ?></button>
<?php endforeach; ?>
</div>
<!-- Tab: Personal -->
<div id="tab-personal" class="tab-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;direction:ltr;text-align:right;font-weight:600;"><?= e($member->national_id ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">جواز السفر</td><td style="padding:6px 0;"><?= e($member->passport_number ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ الميلاد</td><td style="padding:6px 0;"><?= e($member->date_of_birth) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">السن</td><td style="padding:6px 0;"><?= (int) $member->age_years ?> سنة <?= $member->age_months ? 'و ' . (int) $member->age_months . ' شهر' : '' ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الجنسية</td><td style="padding:6px 0;"><?= e($member->nationality ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الديانة</td><td style="padding:6px 0;"><?= e($member->religion ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">المؤهل</td><td style="padding:6px 0;"><?= e($member->getQualificationName()) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الحالة الاجتماعية</td><td style="padding:6px 0;"><?= e($member->marital_status ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">قيمة العضوية</td><td style="padding:6px 0;font-weight:700;color:#0D7377;"><?= $member->membership_value ? money($member->membership_value) : '—' ?></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;direction:ltr;text-align:right;"><?= e($member->phone_mobile) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">المنزل</td><td style="padding:6px 0;direction:ltr;text-align:right;"><?= e($member->phone_home ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">البريد</td><td style="padding:6px 0;direction:ltr;text-align:right;"><?= e($member->email ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">طوارئ</td><td style="padding:6px 0;"><?= e($member->emergency_name ?: '—') ?><?= e($member->emergency_phone ?: '') ?></td></tr>
</table>
<h4 style="color:#0D7377;margin:20px 0 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;"><?= e($member->occupation ?: '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">جهة العمل</td><td style="padding:6px 0;"><?= e($member->business_address ?: '—') ?></td></tr>
</table>
</div>
</div>
<!-- Notes -->
<div class="card" style="margin-top:20px;padding:20px;">
<h4 style="color:#0D7377;margin-bottom:15px;">الملاحظات</h4>
<?php if (!empty($notes)): ?>
<?php foreach ($notes as $note): ?>
<div style="padding:10px 0;border-bottom:1px solid #F3F4F6;font-size:13px;">
<div style="color:#1A1A2E;"><?= nl2br(e($note['note_text'])) ?></div>
<div style="color:#9CA3AF;font-size:11px;margin-top:4px;"><?= e($note['employee_name'] ?? 'النظام') ?><?= e($note['created_at']) ?></div>
</div>
<?php endforeach; ?>
<?php else: ?>
<p style="color:#6B7280;">لا توجد ملاحظات</p>
<?php endif; ?>
</div>
</div>
<!-- Tab: Family -->
<div id="tab-family" class="tab-content" style="display:none;">
<div class="card" style="padding:20px;margin-bottom:20px;">
<h4 style="color:#0D7377;margin-bottom:15px;">الزوجات</h4>
<?php if (!empty($spouses)): ?>
<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 ($spouses as $s): ?>
<tr><td><?= (int) $s['spouse_order'] ?></td><td><?= e($s['full_name_ar']) ?></td><td style="direction:ltr;text-align:right;"><?= e($s['national_id'] ?? '—') ?></td><td><?= e($s['date_of_birth']) ?></td><td><?= (int) ($s['age_years'] ?? 0) ?></td><td><?= e($s['classification'] ?? '—') ?></td><td><?= e($s['status']) ?></td></tr>
<?php endforeach; ?>
</tbody></table>
<?php else: ?><p style="color:#6B7280;">لا توجد زوجات مسجلة</p><?php endif; ?>
</div>
<div class="card" style="padding:20px;margin-bottom:20px;">
<h4 style="color:#0D7377;margin-bottom:15px;">الأبناء</h4>
<?php if (!empty($children)): ?>
<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 ($children as $c): ?>
<tr><td><?= (int) $c['child_order'] ?></td><td><?= e($c['full_name_ar']) ?></td><td><?= $c['gender'] === 'male' ? 'ذكر' : 'أنثى' ?></td><td><?= e($c['date_of_birth']) ?></td><td><?= (int) ($c['age_years'] ?? 0) ?></td><td><?= e($c['classification'] ?? '—') ?></td><td><?= e($c['status']) ?></td></tr>
<?php endforeach; ?>
</tbody></table>
<?php else: ?><p style="color:#6B7280;">لا يوجد أبناء مسجلون</p><?php endif; ?>
</div>
<div class="card" style="padding:20px;">
<h4 style="color:#0D7377;margin-bottom:15px;">الأعضاء المؤقتون</h4>
<?php if (!empty($temporary)): ?>
<table class="data-table"><thead><tr><th>الاسم</th><th>الفئة</th><th>السن</th><th>الحالة</th></tr></thead><tbody>
<?php foreach ($temporary as $t): ?>
<tr><td><?= e($t['full_name_ar']) ?></td><td><?= e($t['category']) ?></td><td><?= (int) ($t['age_years'] ?? 0) ?></td><td><?= e($t['status']) ?></td></tr>
<?php endforeach; ?>
</tbody></table>
<?php else: ?><p style="color:#6B7280;">لا يوجد أعضاء مؤقتون</p><?php endif; ?>
</div>
</div>
<!-- Tab: Financial -->
<div id="tab-financial" class="tab-content" style="display:none;">
<div class="card" style="padding:40px;text-align:center;color:#6B7280;">
<p>البيانات المالية ستكون متاحة بعد تفعيل وحدة المدفوعات</p>
</div>
</div>
<!-- Tab: Documents -->
<div id="tab-documents" class="tab-content" style="display:none;">
<div class="card" style="padding:40px;text-align:center;color:#6B7280;">
<p>المستندات ستكون متاحة بعد تفعيل وحدة إدارة المستندات</p>
</div>
</div>
<!-- Tab: Activity -->
<div id="tab-activity" class="tab-content" style="display:none;">
<div class="card" style="padding:20px;">
<h4 style="color:#0D7377;margin-bottom:15px;">سجل النشاط</h4>
<?php if (!empty($auditHistory)): ?>
<?php foreach ($auditHistory as $i => $entry): ?>
<div style="display:flex;gap:15px;padding:12px 0;<?= $i < count($auditHistory) - 1 ? 'border-bottom:1px solid #F3F4F6;' : '' ?>">
<div style="flex-shrink:0;width:10px;height:10px;border-radius:50%;background:#0D7377;margin-top:5px;"></div>
<div style="flex:1;">
<div style="display:flex;justify-content:space-between;"><strong style="font-size:13px;"><?= e($entry['action']) ?></strong><span style="font-size:11px;color:#9CA3AF;"><?= e($entry['created_at']) ?></span></div>
<div style="font-size:12px;color:#6B7280;"><?= e($entry['employee_name'] ?? 'النظام') ?><?php if ($entry['notes']): ?><?= e(mb_substr($entry['notes'], 0, 100)) ?><?php endif; ?></div>
</div>
</div>
<?php endforeach; ?>
<?php else: ?>
<p style="color:#6B7280;">لا يوجد سجل نشاط</p>
<?php endif; ?>
<div style="margin-top:15px;"><a href="/audit/entity/members/<?= (int) $member->id ?>" class="btn btn-sm btn-outline">عرض السجل الكامل</a></div>
</div>
</div>
<?php $__template->endSection(); ?>
<?php $__template->section('scripts'); ?>
<script>
function switchTab(tabName) {
document.querySelectorAll('.tab-content').forEach(function(el) { el.style.display = 'none'; });
document.querySelectorAll('.tab-btn').forEach(function(el) { el.style.color = '#6B7280'; el.style.borderBottomColor = 'transparent'; });
var tab = document.getElementById('tab-' + tabName);
var btn = document.querySelector('[data-tab="' + tabName + '"]');
if (tab) tab.style.display = 'block';
if (btn) { btn.style.color = '#0D7377'; btn.style.borderBottomColor = '#0D7377'; }
}
</script>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
MenuRegistry::register('members', [
'label_ar' => 'إدارة الأعضاء',
'label_en' => 'Member Management',
'icon' => '👥',
'route' => '/members',
'permission' => 'member.view',
'parent' => null,
'order' => 300,
'children' => [
['label_ar' => 'كل الأعضاء', 'label_en' => 'All Members', 'route' => '/members', 'permission' => 'member.view', 'order' => 1],
['label_ar' => 'عضو جديد', 'label_en' => 'New Member', 'route' => '/members/create', 'permission' => 'member.create', 'order' => 2],
['label_ar' => 'بحث الأعضاء', 'label_en' => 'Search Members', 'route' => '/members/search', 'permission' => 'member.search', 'order' => 3],
],
]);
PermissionRegistry::register('members', [
'member.view' => ['ar' => 'عرض الأعضاء', 'en' => 'View Members'],
'member.create' => ['ar' => 'إنشاء عضو', 'en' => 'Create Member'],
'member.edit' => ['ar' => 'تعديل عضو', 'en' => 'Edit Member'],
'member.archive' => ['ar' => 'أرشفة عضو', 'en' => 'Archive Member'],
'member.search' => ['ar' => 'بحث الأعضاء', 'en' => 'Search Members'],
'member.view_financial' => ['ar' => 'عرض البيانات المالية', 'en' => 'View Financial Data'],
'member.change_status' => ['ar' => 'تغيير حالة العضوية', 'en' => 'Change Member Status'],
]);
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `members` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`membership_number` VARCHAR(20) NULL,
`form_number` VARCHAR(50) NULL,
`form_date` DATE NULL,
`form_submission_id` BIGINT UNSIGNED NULL,
`branch_id` BIGINT UNSIGNED NOT NULL,
`membership_type` VARCHAR(50) NOT NULL DEFAULT 'working',
`member_category` VARCHAR(50) NOT NULL DEFAULT 'working_member',
`status` VARCHAR(50) NOT NULL DEFAULT 'potential',
`full_name_ar` VARCHAR(200) NOT NULL,
`full_name_en` VARCHAR(200) NULL,
`national_id` VARCHAR(14) NULL,
`passport_number` VARCHAR(50) NULL,
`id_type` VARCHAR(20) NOT NULL DEFAULT 'national_id',
`id_issue_date` DATE NULL,
`id_expiry_date` DATE NULL,
`date_of_birth` DATE NOT NULL,
`age_years` INT UNSIGNED NULL,
`age_months` INT UNSIGNED NULL,
`gender` VARCHAR(10) NOT NULL,
`place_of_birth` VARCHAR(200) NULL,
`nationality` VARCHAR(100) NULL DEFAULT 'مصري',
`governorate_code` VARCHAR(2) NULL,
`religion` VARCHAR(50) NULL,
`qualification_id` BIGINT UNSIGNED NULL,
`marital_status` VARCHAR(30) NULL,
`phone_home` VARCHAR(20) NULL,
`phone_mobile` VARCHAR(20) NOT NULL,
`phone_international` VARCHAR(30) NULL,
`email` VARCHAR(255) NULL,
`emergency_name` VARCHAR(200) NULL,
`emergency_phone` VARCHAR(20) NULL,
`residence_type` VARCHAR(30) NULL,
`residence_address` TEXT NULL,
`landmark` VARCHAR(200) NULL,
`floor` VARCHAR(20) NULL,
`apartment` VARCHAR(20) NULL,
`area` VARCHAR(100) NULL,
`governorate` VARCHAR(100) NULL,
`correspondence_address` VARCHAR(50) NULL,
`employment_type` VARCHAR(50) NULL,
`occupation` VARCHAR(200) NULL,
`job_title` VARCHAR(200) NULL,
`employment_date` DATE NULL,
`business_address` TEXT NULL,
`office_phone` VARCHAR(20) NULL,
`office_fax` VARCHAR(20) NULL,
`business_activity` VARCHAR(200) NULL,
`membership_value` DECIMAL(15,2) NULL,
`payment_method` VARCHAR(30) NULL,
`referral_source` VARCHAR(100) NULL,
`photo_path` VARCHAR(500) NULL,
`workflow_instance_id` 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,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_members_membership_number` (`membership_number`),
UNIQUE KEY `uq_members_national_id` (`national_id`),
INDEX `idx_members_branch` (`branch_id`),
INDEX `idx_members_status` (`status`),
INDEX `idx_members_type` (`membership_type`),
INDEX `idx_members_name_ar` (`full_name_ar`),
INDEX `idx_members_phone` (`phone_mobile`),
INDEX `idx_members_passport` (`passport_number`),
INDEX `idx_members_form` (`form_number`),
INDEX `idx_members_archived` (`is_archived`),
INDEX `idx_members_qualification` (`qualification_id`),
INDEX `idx_members_workflow` (`workflow_instance_id`),
CONSTRAINT `fk_members_branch` FOREIGN KEY (`branch_id`) REFERENCES `branches`(`id`),
CONSTRAINT `fk_members_qualification` FOREIGN KEY (`qualification_id`) REFERENCES `qualifications`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_members_form_sub` FOREIGN KEY (`form_submission_id`) REFERENCES `form_submissions`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_members_workflow` FOREIGN KEY (`workflow_instance_id`) REFERENCES `workflow_instances`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `members`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `member_notes` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`member_id` BIGINT UNSIGNED NOT NULL,
`note_text` TEXT NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
INDEX `idx_member_notes_member` (`member_id`),
CONSTRAINT `fk_member_notes_member` FOREIGN KEY (`member_id`) REFERENCES `members`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `member_notes`",
];
\ 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