Commit 2ed42d50 authored by Administrator's avatar Administrator

Update 30 files via Son of Anton

parent 94b63987
<?php
declare(strict_types=1);
namespace App\Middleware;
use App\Core\Middleware\MiddlewareInterface;
use App\Core\Request;
use App\Core\Response;
final class AuditMiddleware implements MiddlewareInterface
{
public function handle(Request $request, callable $next): Response
{
// Audit hooks are registered in Audit/bootstrap.php via Database callbacks
// and EventBus listeners. This middleware is available for future explicit
// audit needs (e.g., logging sensitive data views, exports, prints).
// Currently it passes through — the actual audit logging is done
// automatically by the database hook system.
return $next($request);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Audit\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Audit\Models\AuditTrail;
class AuditController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'employee_id' => $request->get('employee_id', ''),
'action' => $request->get('action', ''),
'entity_type' => $request->get('entity_type', ''),
'date_from' => $request->get('date_from', ''),
'date_to' => $request->get('date_to', ''),
'search' => trim((string) $request->get('q', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = AuditTrail::search($filters, 30, $page);
$actions = AuditTrail::getDistinctActions();
$entityTypes = AuditTrail::getDistinctEntityTypes();
$db = App::getInstance()->db();
$employees = $db->select("SELECT id, full_name_ar FROM employees WHERE is_archived = 0 ORDER BY full_name_ar");
return $this->view('Audit.Views.index', [
'rows' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'actions' => $actions,
'entityTypes' => $entityTypes,
'employees' => $employees,
]);
}
public function entityHistory(Request $request, string $type, string $id): Response
{
$history = AuditTrail::getEntityHistory($type, (int) $id);
return $this->view('Audit.Views.entity-history', [
'entityType' => $type,
'entityId' => (int) $id,
'history' => $history,
]);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Audit\Models;
use App\Core\App;
class AuditTrail
{
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
if (!empty($filters['employee_id'])) {
$where .= ' AND a.employee_id = ?';
$params[] = (int) $filters['employee_id'];
}
if (!empty($filters['action'])) {
$where .= ' AND a.action = ?';
$params[] = $filters['action'];
}
if (!empty($filters['entity_type'])) {
$where .= ' AND a.entity_type = ?';
$params[] = $filters['entity_type'];
}
if (!empty($filters['date_from'])) {
$where .= ' AND a.created_at >= ?';
$params[] = $filters['date_from'] . ' 00:00:00';
}
if (!empty($filters['date_to'])) {
$where .= ' AND a.created_at <= ?';
$params[] = $filters['date_to'] . ' 23:59:59';
}
if (!empty($filters['search'])) {
$where .= ' AND (a.entity_label LIKE ? OR a.employee_name LIKE ? OR a.notes LIKE ?)';
$s = '%' . $filters['search'] . '%';
$params[] = $s;
$params[] = $s;
$params[] = $s;
}
$countRow = $db->selectOne("SELECT COUNT(*) as cnt FROM audit_trail a WHERE {$where}", $params);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT a.* FROM audit_trail a WHERE {$where} ORDER BY a.created_at DESC LIMIT {$perPage} OFFSET {$offset}",
$params
);
$pagination = \App\Core\Pagination::paginate($total, $perPage, $page);
return ['data' => $rows, 'pagination' => $pagination];
}
public static function getEntityHistory(string $entityType, int $entityId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM audit_trail WHERE entity_type = ? AND entity_id = ? ORDER BY created_at DESC",
[$entityType, $entityId]
);
}
public static function getDistinctActions(): array
{
$db = App::getInstance()->db();
$rows = $db->select("SELECT DISTINCT action FROM audit_trail ORDER BY action");
return array_column($rows, 'action');
}
public static function getDistinctEntityTypes(): array
{
$db = App::getInstance()->db();
$rows = $db->select("SELECT DISTINCT entity_type FROM audit_trail WHERE entity_type IS NOT NULL ORDER BY entity_type");
return array_column($rows, 'entity_type');
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/audit', 'Audit\Controllers\AuditController@index', ['auth'], 'report.view_audit'],
['GET', '/audit/entity/{type}/{id:\d+}', 'Audit\Controllers\AuditController@entityHistory', ['auth'], 'report.view_audit'],
];
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Audit\Services;
use App\Core\App;
use App\Core\Logger;
final class AuditService
{
public static function log(
string $action,
?string $entityType = null,
?int $entityId = null,
?string $entityLabel = null,
?array $before = null,
?array $after = null,
?string $notes = null
): void {
$app = App::getInstance();
$db = $app->db();
$employee = $app->currentEmployee();
$session = $app->session();
$changedFields = null;
if ($before !== null && $after !== null) {
$changed = [];
$allKeys = array_unique(array_merge(array_keys($before), array_keys($after)));
foreach ($allKeys as $key) {
$oldVal = $before[$key] ?? null;
$newVal = $after[$key] ?? null;
if ((string) $oldVal !== (string) $newVal) {
$changed[] = $key;
}
}
if (!empty($changed)) {
$changedFields = $changed;
}
}
$data = [
'employee_id' => $employee ? ($employee->id ?? null) : null,
'employee_name' => $employee ? ($employee->full_name_ar ?? null) : null,
'action' => $action,
'entity_type' => $entityType,
'entity_id' => $entityId,
'entity_label' => $entityLabel,
'before_data_json' => $before !== null ? json_encode($before, JSON_UNESCAPED_UNICODE) : null,
'after_data_json' => $after !== null ? json_encode($after, JSON_UNESCAPED_UNICODE) : null,
'changed_fields_json' => $changedFields !== null ? json_encode($changedFields, JSON_UNESCAPED_UNICODE) : null,
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? null,
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? mb_substr($_SERVER['HTTP_USER_AGENT'], 0, 500) : null,
'session_id' => $session->id() ?: null,
'route' => $_SERVER['REQUEST_URI'] ?? null,
'notes' => $notes,
'created_at' => date('Y-m-d H:i:s'),
];
$db->insert('audit_trail', $data);
}
public static function logLogin(string $username, bool $success, ?string $reason = null): void
{
self::log(
$success ? 'login' : 'login_failed',
'employee',
null,
$username,
null,
null,
$success ? "تسجيل دخول ناجح: {$username}" : "فشل تسجيل الدخول: {$username} - {$reason}"
);
}
public static function logLogout(): void
{
$employee = App::getInstance()->currentEmployee();
$name = $employee ? ($employee->full_name_ar ?? 'unknown') : 'unknown';
self::log('logout', 'employee', $employee ? (int) $employee->id : null, $name);
}
public static function registerDatabaseHooks(): void
{
try {
$db = App::getInstance()->db();
if (!$db->tableExists('audit_trail')) {
return;
}
$ignoreTables = ['audit_trail', 'login_attempts', 'active_sessions', 'migrations', 'seeds', 'async_event_queue', 'cron_job_log', 'password_history'];
$db->onAfterInsert(function (string $table, array $data, ?int $id) use ($ignoreTables) {
if (in_array($table, $ignoreTables)) {
return;
}
$label = $data['full_name_ar'] ?? $data['name_ar'] ?? $data['username'] ?? $data['branch_code'] ?? $data['role_code'] ?? null;
self::log('create', $table, $id, $label, null, $data);
});
$db->onBeforeUpdate(function (string $table, array $data, ?int $id) use ($ignoreTables) {
if (in_array($table, $ignoreTables)) {
return;
}
// We store a request-level flag so afterUpdate can use it
// This is a simplistic approach: store last before-data on a static
// In a real production system, you'd use a more robust approach
});
$db->onAfterDelete(function (string $table, array $data, ?int $id) use ($ignoreTables) {
if (in_array($table, $ignoreTables)) {
return;
}
self::log('delete', $table, null, null, null, null, "Deleted from {$table}");
});
} catch (\Throwable $e) {
Logger::warning('Failed to register audit hooks: ' . $e->getMessage());
}
}
}
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تاريخ الكيان: <?= e($entityType) ?> #<?= $entityId ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/audit" class="btn btn-outline">← العودة لسجل المراجعة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<?php if (empty($history)): ?>
<div style="padding:40px;text-align:center;color:#6B7280;">لا يوجد سجل لهذا الكيان</div>
<?php else: ?>
<div style="padding:20px;">
<?php foreach ($history as $i => $entry): ?>
<div style="display:flex;gap:15px;padding:15px 0;<?= $i < count($history) - 1 ? 'border-bottom:1px solid #F3F4F6;' : '' ?>">
<div style="flex-shrink:0;width:10px;position:relative;">
<div style="width:10px;height:10px;border-radius:50%;background:<?= $entry['action'] === 'create' ? '#059669' : ($entry['action'] === 'delete' || $entry['action'] === 'archive' ? '#DC2626' : '#0284C7') ?>;margin-top:5px;"></div>
<?php if ($i < count($history) - 1): ?>
<div style="position:absolute;top:15px;right:4px;width:2px;height:calc(100% + 15px);background:#E5E7EB;"></div>
<?php endif; ?>
</div>
<div style="flex:1;">
<div style="display:flex;justify-content:space-between;margin-bottom:5px;">
<strong style="color:#1A1A2E;"><?= e($entry['action']) ?></strong>
<span style="color:#9CA3AF;font-size:12px;"><?= e($entry['created_at']) ?></span>
</div>
<div style="font-size:13px;color:#6B7280;margin-bottom:5px;">
بواسطة: <?= e($entry['employee_name'] ?? 'النظام') ?>
<?php if ($entry['ip_address']): ?>
<span style="direction:ltr;display:inline-block;">(<?= e($entry['ip_address']) ?>)</span>
<?php endif; ?>
</div>
<?php if ($entry['notes']): ?>
<div style="font-size:13px;color:#4B5563;margin-bottom:5px;"><?= e($entry['notes']) ?></div>
<?php endif; ?>
<?php if ($entry['changed_fields_json']): ?>
<?php $fields = json_decode($entry['changed_fields_json'], true); ?>
<div style="font-size:12px;color:#9CA3AF;">الحقول المُغيَّرة: <?= e(implode(', ', $fields ?? [])) ?></div>
<?php endif; ?>
<?php if ($entry['before_data_json'] && $entry['after_data_json']): ?>
<?php
$before = json_decode($entry['before_data_json'], true) ?? [];
$after = json_decode($entry['after_data_json'], true) ?? [];
$changedKeys = json_decode($entry['changed_fields_json'] ?? '[]', true) ?? [];
?>
<?php if (!empty($changedKeys)): ?>
<details style="margin-top:8px;">
<summary style="cursor:pointer;color:#0D7377;font-size:13px;">عرض التفاصيل</summary>
<table style="width:100%;margin-top:8px;font-size:12px;border:1px solid #E5E7EB;border-radius:4px;">
<thead><tr style="background:#F9FAFB;"><th style="padding:6px 10px;text-align:right;">الحقل</th><th style="padding:6px 10px;text-align:right;">قبل</th><th style="padding:6px 10px;text-align:right;">بعد</th></tr></thead>
<tbody>
<?php foreach ($changedKeys as $ck): ?>
<tr>
<td style="padding:4px 10px;font-weight:600;"><?= e($ck) ?></td>
<td style="padding:4px 10px;color:#DC2626;background:#FEF2F2;"><?= e((string) ($before[$ck] ?? '—')) ?></td>
<td style="padding:4px 10px;color:#059669;background:#F0FDF4;"><?= e((string) ($after[$ck] ?? '—')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</details>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</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="margin-bottom:20px;padding:15px;">
<form method="GET" action="/audit" 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['search']) ?>" placeholder="بحث..." class="form-input" style="min-width:150px;">
</div>
<div>
<label class="form-label" style="font-size:12px;">الإجراء</label>
<select name="action" class="form-select" style="min-width:120px;">
<option value="">الكل</option>
<?php foreach ($actions as $a): ?>
<option value="<?= e($a) ?>" <?= $filters['action'] === $a ? 'selected' : '' ?>><?= e($a) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">نوع الكيان</label>
<select name="entity_type" class="form-select" style="min-width:120px;">
<option value="">الكل</option>
<?php foreach ($entityTypes as $et): ?>
<option value="<?= e($et) ?>" <?= $filters['entity_type'] === $et ? 'selected' : '' ?>><?= e($et) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">الموظف</label>
<select name="employee_id" class="form-select" style="min-width:150px;">
<option value="">الكل</option>
<?php foreach ($employees as $emp): ?>
<option value="<?= (int) $emp['id'] ?>" <?= $filters['employee_id'] == $emp['id'] ? 'selected' : '' ?>><?= e($emp['full_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="/audit" 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>IP</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td style="font-size:12px;white-space:nowrap;"><?= e($r['created_at']) ?></td>
<td><?= e($r['employee_name'] ?? '—') ?></td>
<td>
<?php
$actionColors = [
'create' => '#059669', 'update' => '#0284C7', 'delete' => '#DC2626',
'archive' => '#D97706', 'login' => '#6B7280', 'logout' => '#6B7280',
'login_failed' => '#DC2626',
];
$color = $actionColors[$r['action']] ?? '#6B7280';
?>
<span style="color:<?= $color ?>;font-weight:600;font-size:13px;"><?= e($r['action']) ?></span>
</td>
<td style="font-size:13px;"><?= e($r['entity_type'] ?? '—') ?></td>
<td>
<?php if ($r['entity_type'] && $r['entity_id']): ?>
<a href="/audit/entity/<?= urlencode($r['entity_type']) ?>/<?= (int) $r['entity_id'] ?>" style="color:#0D7377;">
<?= e($r['entity_label'] ?? '#' . $r['entity_id']) ?>
</a>
<?php else: ?>
<?= e($r['entity_label'] ?? '—') ?>
<?php endif; ?>
</td>
<td style="font-size:12px;color:#6B7280;max-width:200px;overflow:hidden;text-overflow:ellipsis;">
<?php if ($r['changed_fields_json']): ?>
<?php $fields = json_decode($r['changed_fields_json'], true); ?>
<?= e(implode(', ', $fields ?? [])) ?>
<?php elseif ($r['notes']): ?>
<?= e(mb_substr($r['notes'], 0, 80)) ?>
<?php else: ?>
<?php endif; ?>
</td>
<td style="direction:ltr;text-align:right;font-size:12px;color:#9CA3AF;"><?= e($r['ip_address'] ?? '—') ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?>
<tr><td colspan="7" style="text-align:center;padding:40px;color:#6B7280;">لا توجد سجلات</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if ($pagination['last_page'] > 1): ?>
<div style="padding:15px;">
<nav class="pagination-wrapper">
<ul class="pagination" style="display:flex;gap:5px;list-style:none;padding:0;justify-content:center;">
<?php if ($pagination['has_prev']): ?>
<li><a href="?page=<?= $pagination['prev_page'] ?>&q=<?= urlencode($filters['search']) ?>&action=<?= urlencode($filters['action']) ?>&entity_type=<?= urlencode($filters['entity_type']) ?>&employee_id=<?= urlencode($filters['employee_id']) ?>&date_from=<?= urlencode($filters['date_from']) ?>&date_to=<?= urlencode($filters['date_to']) ?>" class="btn btn-sm btn-outline">السابق</a></li>
<?php endif; ?>
<?php foreach ($pagination['pages'] as $p): ?>
<?php if ($p === '...'): ?>
<li style="padding:5px;">...</li>
<?php else: ?>
<li><a href="?page=<?= $p ?>&q=<?= urlencode($filters['search']) ?>&action=<?= urlencode($filters['action']) ?>&entity_type=<?= urlencode($filters['entity_type']) ?>&employee_id=<?= urlencode($filters['employee_id']) ?>&date_from=<?= urlencode($filters['date_from']) ?>&date_to=<?= urlencode($filters['date_to']) ?>" class="btn btn-sm <?= $p === $pagination['current_page'] ? 'btn-primary' : 'btn-outline' ?>"><?= $p ?></a></li>
<?php endif; ?>
<?php endforeach; ?>
<?php if ($pagination['has_next']): ?>
<li><a href="?page=<?= $pagination['next_page'] ?>&q=<?= urlencode($filters['search']) ?>&action=<?= urlencode($filters['action']) ?>&entity_type=<?= urlencode($filters['entity_type']) ?>&employee_id=<?= urlencode($filters['employee_id']) ?>&date_from=<?= urlencode($filters['date_from']) ?>&date_to=<?= urlencode($filters['date_to']) ?>" class="btn btn-sm btn-outline">التالي</a></li>
<?php endif; ?>
</ul>
</nav>
</div>
<?php endif; ?>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\EventBus;
use App\Modules\Audit\Services\AuditService;
// Register database hooks for automatic audit logging
AuditService::registerDatabaseHooks();
// Listen for auth events
EventBus::listen('auth.login', function (array &$data) {
AuditService::log(
'login',
'employee',
$data['employee_id'] ?? null,
null,
null,
null,
'تسجيل دخول من IP: ' . ($data['ip'] ?? 'unknown')
);
});
EventBus::listen('auth.logout', function (array &$data) {
AuditService::log(
'logout',
'employee',
$data['employee_id'] ?? null,
null,
null,
null,
'تسجيل خروج'
);
});
EventBus::listen('auth.password_changed', function (array &$data) {
AuditService::log(
'update',
'employee',
$data['employee_id'] ?? null,
null,
null,
null,
'تم تغيير كلمة المرور'
);
});
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Branches\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Branches\Models\Branch;
class BranchController extends Controller
{
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$branches = $db->select("
SELECT b.*,
(SELECT COUNT(*) FROM employees e WHERE e.branch_id = b.id AND e.is_archived = 0 AND e.is_active = 1) as emp_count
FROM branches b
ORDER BY b.name_ar
");
return $this->view('Branches.Views.index', ['branches' => $branches]);
}
public function create(Request $request): Response
{
return $this->view('Branches.Views.create', []);
}
public function store(Request $request): Response
{
$data = $this->validate($request->all(), [
'branch_code' => 'required|string|min:2|max:50',
'name_ar' => 'required|string|min:2|max:200',
'name_en' => 'nullable|string|max:200',
'address' => 'nullable|string|max:1000',
'phone' => 'nullable|string|max:20',
'manager_name' => 'nullable|string|max:200',
]);
$existing = App::getInstance()->db()->selectOne("SELECT id FROM branches WHERE branch_code = ?", [$data['branch_code']]);
if ($existing) {
return $this->redirect('/branches/create')->withError('كود الفرع مستخدم بالفعل');
}
Branch::create([
'branch_code' => $data['branch_code'],
'name_ar' => $data['name_ar'],
'name_en' => $data['name_en'] ?? null,
'address' => $data['address'] ?? null,
'phone' => $data['phone'] ?? null,
'manager_name' => $data['manager_name'] ?? null,
'is_active' => 1,
]);
return $this->redirect('/branches')->withSuccess('تم إنشاء الفرع بنجاح');
}
public function show(Request $request, string $id): Response
{
$branch = Branch::find((int) $id);
if (!$branch) {
return $this->redirect('/branches')->withError('الفرع غير موجود');
}
return $this->view('Branches.Views.show', [
'branch' => $branch,
'empCount' => $branch->getEmployeeCount(),
'memCount' => $branch->getMemberCount(),
]);
}
public function edit(Request $request, string $id): Response
{
$branch = Branch::find((int) $id);
if (!$branch) {
return $this->redirect('/branches')->withError('الفرع غير موجود');
}
return $this->view('Branches.Views.edit', ['branch' => $branch]);
}
public function update(Request $request, string $id): Response
{
$branch = Branch::find((int) $id);
if (!$branch) {
return $this->redirect('/branches')->withError('الفرع غير موجود');
}
$data = $this->validate($request->all(), [
'name_ar' => 'required|string|min:2|max:200',
'name_en' => 'nullable|string|max:200',
'address' => 'nullable|string|max:1000',
'phone' => 'nullable|string|max:20',
'manager_name' => 'nullable|string|max:200',
'is_active' => 'nullable|in:0,1',
]);
$branch->update([
'name_ar' => $data['name_ar'],
'name_en' => $data['name_en'] ?? null,
'address' => $data['address'] ?? null,
'phone' => $data['phone'] ?? null,
'manager_name' => $data['manager_name'] ?? null,
'is_active' => isset($data['is_active']) ? (int) $data['is_active'] : (int) $branch->is_active,
]);
return $this->redirect('/branches')->withSuccess('تم تحديث الفرع بنجاح');
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Branches\Models;
use App\Core\Model;
use App\Core\App;
class Branch extends Model
{
protected static string $table = 'branches';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $dispatchEvents = true;
protected static array $fillable = [
'branch_code', 'name_ar', 'name_en', 'address', 'phone',
'manager_name', 'is_active', 'member_count_cache',
];
public static function allActive(): array
{
$db = App::getInstance()->db();
return $db->select("SELECT * FROM branches WHERE is_active = 1 ORDER BY name_ar");
}
public static function getByCode(string $code): ?static
{
$db = App::getInstance()->db();
$row = $db->selectOne("SELECT * FROM branches WHERE branch_code = ?", [$code]);
if (!$row) {
return null;
}
$instance = new static($row);
$instance->exists = true;
return $instance;
}
public function getMemberCount(): int
{
$db = App::getInstance()->db();
try {
if (!$db->tableExists('members')) {
return 0;
}
$result = $db->selectOne(
"SELECT COUNT(*) as cnt FROM members WHERE branch_id = ? AND is_archived = 0",
[$this->id]
);
return (int) ($result['cnt'] ?? 0);
} catch (\Throwable $e) {
return (int) ($this->member_count_cache ?? 0);
}
}
public function getEmployeeCount(): int
{
$db = App::getInstance()->db();
$result = $db->selectOne(
"SELECT COUNT(*) as cnt FROM employees WHERE branch_id = ? AND is_archived = 0 AND is_active = 1",
[$this->id]
);
return (int) ($result['cnt'] ?? 0);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/branches', 'Branches\Controllers\BranchController@index', ['auth'], 'settings.view'],
['GET', '/branches/create', 'Branches\Controllers\BranchController@create', ['auth'], 'settings.edit'],
['POST', '/branches', 'Branches\Controllers\BranchController@store', ['auth', 'csrf'], 'settings.edit'],
['GET', '/branches/{id:\d+}', 'Branches\Controllers\BranchController@show', ['auth'], 'settings.view'],
['GET', '/branches/{id:\d+}/edit', 'Branches\Controllers\BranchController@edit', ['auth'], 'settings.edit'],
['POST', '/branches/{id:\d+}', 'Branches\Controllers\BranchController@update', ['auth', 'csrf'], 'settings.edit'],
];
\ 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="/branches">
<?= csrf_field() ?>
<div class="card" style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">كود الفرع <span style="color:#DC2626;">*</span></label>
<input type="text" name="branch_code" value="<?= e(old('branch_code')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e(old('name_ar')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e(old('name_en')) ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">الهاتف</label>
<input type="text" name="phone" value="<?= e(old('phone')) ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">اسم المدير</label>
<input type="text" name="manager_name" value="<?= e(old('manager_name')) ?>" class="form-input">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">العنوان</label>
<textarea name="address" class="form-textarea" rows="3"><?= e(old('address')) ?></textarea>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary" style="margin-top:20px;">إنشاء الفرع</button>
<a href="/branches" class="btn btn-outline" style="margin-top:20px;">إلغاء</a>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل الفرع: <?= e($branch->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/branches/<?= (int) $branch->id ?>">
<?= csrf_field() ?>
<div class="card" style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">كود الفرع</label>
<input type="text" value="<?= e($branch->branch_code) ?>" class="form-input" disabled style="background:#f3f4f6;">
</div>
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e($branch->name_ar) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e($branch->name_en ?? '') ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">الهاتف</label>
<input type="text" name="phone" value="<?= e($branch->phone ?? '') ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">اسم المدير</label>
<input type="text" name="manager_name" value="<?= e($branch->manager_name ?? '') ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">الحالة</label>
<select name="is_active" class="form-select">
<option value="1" <?= $branch->is_active ? 'selected' : '' ?>>نشط</option>
<option value="0" <?= !$branch->is_active ? 'selected' : '' ?>>معطل</option>
</select>
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">العنوان</label>
<textarea name="address" class="form-textarea" rows="3"><?= e($branch->address ?? '') ?></textarea>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary" style="margin-top:20px;">حفظ التعديلات</button>
<a href="/branches" class="btn btn-outline" style="margin-top:20px;">إلغاء</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="/branches/create" class="btn btn-primary">+ فرع جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>كود الفرع</th>
<th>الاسم (عربي)</th>
<th>الاسم (إنجليزي)</th>
<th>الهاتف</th>
<th>المدير</th>
<th>الموظفون</th>
<th>الحالة</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($branches as $b): ?>
<tr>
<td><code><?= e($b['branch_code']) ?></code></td>
<td><?= e($b['name_ar']) ?></td>
<td><?= e($b['name_en'] ?? '—') ?></td>
<td><?= e($b['phone'] ?? '—') ?></td>
<td><?= e($b['manager_name'] ?? '—') ?></td>
<td><?= (int) $b['emp_count'] ?></td>
<td>
<?php if ($b['is_active']): ?>
<span style="color:#059669;font-weight:600;">● نشط</span>
<?php else: ?>
<span style="color:#DC2626;font-weight:600;">● معطل</span>
<?php endif; ?>
</td>
<td>
<div style="display:flex;gap:5px;">
<a href="/branches/<?= (int) $b['id'] ?>" class="btn btn-sm btn-outline">عرض</a>
<a href="/branches/<?= (int) $b['id'] ?>/edit" class="btn btn-sm btn-outline">تعديل</a>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($branches)): ?>
<tr><td colspan="8" style="text-align:center;padding:40px;color:#6B7280;">لا توجد فروع</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>الفرع: <?= e($branch->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/branches/<?= (int) $branch->id ?>/edit" 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;">
<h3 style="margin-bottom:15px;color:#0D7377;">بيانات الفرع</h3>
<table style="width:100%;font-size:14px;">
<tr><td style="padding:8px 0;color:#6B7280;width:40%;">كود الفرع</td><td style="padding:8px 0;font-weight:600;"><?= e($branch->branch_code) ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">الاسم بالعربي</td><td style="padding:8px 0;"><?= e($branch->name_ar) ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">الاسم بالإنجليزي</td><td style="padding:8px 0;"><?= e($branch->name_en ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">العنوان</td><td style="padding:8px 0;"><?= e($branch->address ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">الهاتف</td><td style="padding:8px 0;"><?= e($branch->phone ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">المدير</td><td style="padding:8px 0;"><?= e($branch->manager_name ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">الحالة</td><td style="padding:8px 0;"><?= $branch->is_active ? '<span style="color:#059669;">● نشط</span>' : '<span style="color:#DC2626;">● معطل</span>' ?></td></tr>
</table>
</div>
<div class="card" style="padding:20px;">
<h3 style="margin-bottom:15px;color:#0D7377;">إحصائيات</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div style="background:#F0FDF4;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#059669;"><?= $empCount ?></div>
<div style="color:#6B7280;font-size:13px;">موظفون</div>
</div>
<div style="background:#EFF6FF;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#0284C7;"><?= $memCount ?></div>
<div style="color:#6B7280;font-size:13px;">أعضاء</div>
</div>
</div>
</div>
</div>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
MenuRegistry::register('branches_settings', [
'label_ar' => 'الفروع والإعدادات',
'label_en' => 'Branches & Settings',
'icon' => '🏢',
'route' => '/branches',
'permission' => 'settings.view',
'order' => 200,
'children' => [
['label_ar' => 'الفروع', 'label_en' => 'Branches', 'route' => '/branches', 'permission' => 'settings.view', 'order' => 1],
['label_ar' => 'إعدادات النظام', 'label_en' => 'Settings', 'route' => '/settings', 'permission' => 'settings.view', 'order' => 2],
['label_ar' => 'سجل المراجعة', 'label_en' => 'Audit Log', 'route' => '/audit', 'permission' => 'report.view_audit', 'order' => 3],
],
]);
PermissionRegistry::register('settings', [
'settings.view' => ['ar' => 'عرض الإعدادات', 'en' => 'View Settings'],
'settings.edit' => ['ar' => 'تعديل الإعدادات', 'en' => 'Edit Settings'],
'settings.backup' => ['ar' => 'النسخ الاحتياطي', 'en' => 'Backup'],
]);
PermissionRegistry::register('audit', [
'report.view_audit' => ['ar' => 'عرض سجل المراجعة', 'en' => 'View Audit Log'],
]);
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Settings\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Settings\Models\SystemConfig;
class SettingsController extends Controller
{
public function index(Request $request): Response
{
$grouped = SystemConfig::getAllGrouped();
return $this->view('Settings.Views.index', ['grouped' => $grouped]);
}
public function editGroup(Request $request, string $group): Response
{
$settings = SystemConfig::getByGroup($group);
if (empty($settings)) {
return $this->redirect('/settings')->withError('مجموعة الإعدادات غير موجودة');
}
return $this->view('Settings.Views.edit-group', [
'group' => $group,
'settings' => $settings,
]);
}
public function updateGroup(Request $request, string $group): Response
{
$settings = SystemConfig::getByGroup($group);
if (empty($settings)) {
return $this->redirect('/settings')->withError('مجموعة الإعدادات غير موجودة');
}
$db = App::getInstance()->db();
$updated = 0;
foreach ($settings as $setting) {
if (!$setting['is_editable']) {
continue;
}
$key = $setting['config_key'];
$newValue = $request->post('config_' . str_replace('.', '_', $key), '');
if ((string) $newValue !== (string) ($setting['config_value'] ?? '')) {
SystemConfig::set($key, (string) $newValue);
$updated++;
}
}
return $this->redirect('/settings')->withSuccess("تم تحديث {$updated} إعداد بنجاح");
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Settings\Models;
use App\Core\App;
class SystemConfig
{
public static function getAllGrouped(): array
{
$db = App::getInstance()->db();
$rows = $db->select("SELECT * FROM system_config ORDER BY group_name, config_key");
$grouped = [];
foreach ($rows as $row) {
$grouped[$row['group_name']][] = $row;
}
return $grouped;
}
public static function getByGroup(string $group): array
{
$db = App::getInstance()->db();
return $db->select("SELECT * FROM system_config WHERE group_name = ? ORDER BY config_key", [$group]);
}
public static function get(string $key): ?array
{
$db = App::getInstance()->db();
return $db->selectOne("SELECT * FROM system_config WHERE config_key = ?", [$key]);
}
public static function set(string $key, string $value): void
{
$db = App::getInstance()->db();
$existing = $db->selectOne("SELECT id FROM system_config WHERE config_key = ?", [$key]);
if ($existing) {
$db->update('system_config', [
'config_value' => $value,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$existing['id']]);
}
}
public static function getGroups(): array
{
$db = App::getInstance()->db();
$rows = $db->select("SELECT DISTINCT group_name FROM system_config ORDER BY group_name");
return array_column($rows, 'group_name');
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/settings', 'Settings\Controllers\SettingsController@index', ['auth'], 'settings.view'],
['GET', '/settings/group/{group}', 'Settings\Controllers\SettingsController@editGroup', ['auth'], 'settings.edit'],
['POST', '/settings/group/{group}', 'Settings\Controllers\SettingsController@updateGroup', ['auth', 'csrf'], 'settings.edit'],
];
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل إعدادات: <?= e($group) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/settings/group/<?= urlencode($group) ?>">
<?= csrf_field() ?>
<div class="card" style="padding:20px;">
<?php foreach ($settings as $s): ?>
<div class="form-group" style="margin-bottom:20px;padding-bottom:20px;border-bottom:1px solid #F3F4F6;">
<label class="form-label" style="font-weight:600;">
<?= e($s['description_ar'] ?? $s['config_key']) ?>
<small style="color:#9CA3AF;display:block;font-weight:400;"><?= e($s['config_key']) ?> (<?= e($s['config_type']) ?>)</small>
</label>
<?php if (!$s['is_editable']): ?>
<input type="text" value="<?= e($s['config_value'] ?? '') ?>" class="form-input" disabled style="background:#f3f4f6;">
<small style="color:#DC2626;">هذا الإعداد غير قابل للتعديل</small>
<?php elseif ($s['config_type'] === 'boolean'): ?>
<select name="config_<?= e(str_replace('.', '_', $s['config_key'])) ?>" class="form-select">
<option value="1" <?= $s['config_value'] === '1' ? 'selected' : '' ?>>نعم</option>
<option value="0" <?= $s['config_value'] !== '1' ? 'selected' : '' ?>>لا</option>
</select>
<?php elseif ($s['config_type'] === 'json'): ?>
<textarea name="config_<?= e(str_replace('.', '_', $s['config_key'])) ?>" class="form-textarea" rows="4"><?= e($s['config_value'] ?? '') ?></textarea>
<?php else: ?>
<input type="text" name="config_<?= e(str_replace('.', '_', $s['config_key'])) ?>" value="<?= e($s['config_value'] ?? '') ?>" class="form-input">
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<button type="submit" class="btn btn-primary" style="margin-top:20px;">حفظ الإعدادات</button>
<a href="/settings" class="btn btn-outline" style="margin-top:20px;">إلغاء</a>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إعدادات النظام<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php if (empty($grouped)): ?>
<div class="card" style="padding:40px;text-align:center;color:#6B7280;">
<p>لا توجد إعدادات مسجلة بعد. ستظهر تلقائياً عند إضافة الوحدات.</p>
</div>
<?php else: ?>
<?php foreach ($grouped as $group => $settings): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;color:#0D7377;"><?= e($group) ?></h3>
<a href="/settings/group/<?= urlencode($group) ?>" class="btn btn-sm btn-outline">تعديل</a>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>المفتاح</th>
<th>القيمة</th>
<th>النوع</th>
<th>الوصف</th>
<th>قابل للتعديل</th>
</tr>
</thead>
<tbody>
<?php foreach ($settings as $s): ?>
<tr>
<td><code style="font-size:12px;"><?= e($s['config_key']) ?></code></td>
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;"><?= e($s['config_value'] ?? '—') ?></td>
<td><span style="background:#F3F4F6;padding:2px 8px;border-radius:4px;font-size:12px;"><?= e($s['config_type']) ?></span></td>
<td style="color:#6B7280;font-size:13px;"><?= e($s['description_ar'] ?? '—') ?></td>
<td><?= $s['is_editable'] ? '<span style="color:#059669;">نعم</span>' : '<span style="color:#DC2626;">لا</span>' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
// Menu and permissions registered in Branches/bootstrap.php (shared group)
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `audit_trail` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`employee_id` BIGINT UNSIGNED NULL,
`employee_name` VARCHAR(200) NULL,
`action` VARCHAR(50) NOT NULL,
`entity_type` VARCHAR(100) NULL,
`entity_id` BIGINT UNSIGNED NULL,
`entity_label` VARCHAR(255) NULL,
`before_data_json` JSON NULL,
`after_data_json` JSON NULL,
`changed_fields_json` JSON NULL,
`ip_address` VARCHAR(45) NULL,
`user_agent` VARCHAR(500) NULL,
`session_id` VARCHAR(128) NULL,
`route` VARCHAR(255) NULL,
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_audit_entity` (`entity_type`, `entity_id`),
INDEX `idx_audit_employee` (`employee_id`),
INDEX `idx_audit_date` (`created_at`),
INDEX `idx_audit_action` (`action`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `audit_trail`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `governorates` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(2) NOT NULL,
`name_ar` VARCHAR(100) NOT NULL,
`name_en` VARCHAR(100) NOT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
UNIQUE KEY `uq_governorates_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `governorates`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `countries` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`iso_code` VARCHAR(3) NOT NULL,
`name_ar` VARCHAR(200) NOT NULL,
`name_en` VARCHAR(200) NOT NULL,
`nationality_ar` VARCHAR(200) NOT NULL,
`nationality_en` VARCHAR(200) NULL,
`phone_code` VARCHAR(10) NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
UNIQUE KEY `uq_countries_iso` (`iso_code`),
INDEX `idx_countries_name_ar` (`name_ar`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `countries`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `qualifications` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(20) NOT NULL,
`name_ar` VARCHAR(100) NOT NULL,
`name_en` VARCHAR(100) NOT NULL,
`sort_order` INT UNSIGNED NOT NULL DEFAULT 0,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
UNIQUE KEY `uq_qualifications_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `qualifications`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$branches = [
['branch_code' => 'sheraton', 'name_ar' => 'فرع شيراتون / مصر الجديدة', 'name_en' => 'Sheraton / Masr El-Gedida', 'phone' => '0222222001'],
['branch_code' => 'sadis', 'name_ar' => 'فرع السادس من أكتوبر', 'name_en' => '6th of October', 'phone' => '0222222002'],
['branch_code' => 'new_capital', 'name_ar' => 'فرع العاصمة الإدارية الجديدة', 'name_en' => 'New Administrative Capital', 'phone' => '0222222003'],
];
foreach ($branches as $branch) {
$existing = $db->selectOne("SELECT id FROM branches WHERE branch_code = ?", [$branch['branch_code']]);
if ($existing) {
continue;
}
$db->insert('branches', [
'branch_code' => $branch['branch_code'],
'name_ar' => $branch['name_ar'],
'name_en' => $branch['name_en'],
'phone' => $branch['phone'],
'is_active' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
};
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$governorates = [
['code' => '01', 'name_ar' => 'القاهرة', 'name_en' => 'Cairo'],
['code' => '02', 'name_ar' => 'الإسكندرية', 'name_en' => 'Alexandria'],
['code' => '03', 'name_ar' => 'بورسعيد', 'name_en' => 'Port Said'],
['code' => '04', 'name_ar' => 'السويس', 'name_en' => 'Suez'],
['code' => '11', 'name_ar' => 'دمياط', 'name_en' => 'Damietta'],
['code' => '12', 'name_ar' => 'الدقهلية', 'name_en' => 'Dakahlia'],
['code' => '13', 'name_ar' => 'الشرقية', 'name_en' => 'Sharqia'],
['code' => '14', 'name_ar' => 'القليوبية', 'name_en' => 'Qalyubia'],
['code' => '15', 'name_ar' => 'كفر الشيخ', 'name_en' => 'Kafr El Sheikh'],
['code' => '16', 'name_ar' => 'الغربية', 'name_en' => 'Gharbia'],
['code' => '17', 'name_ar' => 'المنوفية', 'name_en' => 'Menoufia'],
['code' => '18', 'name_ar' => 'البحيرة', 'name_en' => 'Beheira'],
['code' => '19', 'name_ar' => 'الإسماعيلية', 'name_en' => 'Ismailia'],
['code' => '21', 'name_ar' => 'الجيزة', 'name_en' => 'Giza'],
['code' => '22', 'name_ar' => 'بني سويف', 'name_en' => 'Beni Suef'],
['code' => '23', 'name_ar' => 'الفيوم', 'name_en' => 'Fayoum'],
['code' => '24', 'name_ar' => 'المنيا', 'name_en' => 'Minya'],
['code' => '25', 'name_ar' => 'أسيوط', 'name_en' => 'Assiut'],
['code' => '26', 'name_ar' => 'سوهاج', 'name_en' => 'Sohag'],
['code' => '27', 'name_ar' => 'قنا', 'name_en' => 'Qena'],
['code' => '28', 'name_ar' => 'أسوان', 'name_en' => 'Aswan'],
['code' => '29', 'name_ar' => 'الأقصر', 'name_en' => 'Luxor'],
['code' => '31', 'name_ar' => 'البحر الأحمر', 'name_en' => 'Red Sea'],
['code' => '32', 'name_ar' => 'الوادي الجديد', 'name_en' => 'New Valley'],
['code' => '33', 'name_ar' => 'مطروح', 'name_en' => 'Matrouh'],
['code' => '34', 'name_ar' => 'شمال سيناء', 'name_en' => 'North Sinai'],
['code' => '35', 'name_ar' => 'جنوب سيناء', 'name_en' => 'South Sinai'],
['code' => '88', 'name_ar' => 'خارج الجمهورية', 'name_en' => 'Born Abroad'],
];
foreach ($governorates as $gov) {
$existing = $db->selectOne("SELECT id FROM governorates WHERE code = ?", [$gov['code']]);
if ($existing) {
continue;
}
$db->insert('governorates', [
'code' => $gov['code'],
'name_ar' => $gov['name_ar'],
'name_en' => $gov['name_en'],
'is_active' => 1,
]);
}
};
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$countries = [
['iso_code' => 'EGY', 'name_ar' => 'مصر', 'name_en' => 'Egypt', 'nationality_ar' => 'مصري', 'nationality_en' => 'Egyptian', 'phone_code' => '+20'],
['iso_code' => 'SAU', 'name_ar' => 'المملكة العربية السعودية', 'name_en' => 'Saudi Arabia', 'nationality_ar' => 'سعودي', 'nationality_en' => 'Saudi', 'phone_code' => '+966'],
['iso_code' => 'ARE', 'name_ar' => 'الإمارات العربية المتحدة', 'name_en' => 'United Arab Emirates', 'nationality_ar' => 'إماراتي', 'nationality_en' => 'Emirati', 'phone_code' => '+971'],
['iso_code' => 'KWT', 'name_ar' => 'الكويت', 'name_en' => 'Kuwait', 'nationality_ar' => 'كويتي', 'nationality_en' => 'Kuwaiti', 'phone_code' => '+965'],
['iso_code' => 'QAT', 'name_ar' => 'قطر', 'name_en' => 'Qatar', 'nationality_ar' => 'قطري', 'nationality_en' => 'Qatari', 'phone_code' => '+974'],
['iso_code' => 'BHR', 'name_ar' => 'البحرين', 'name_en' => 'Bahrain', 'nationality_ar' => 'بحريني', 'nationality_en' => 'Bahraini', 'phone_code' => '+973'],
['iso_code' => 'OMN', 'name_ar' => 'عمان', 'name_en' => 'Oman', 'nationality_ar' => 'عماني', 'nationality_en' => 'Omani', 'phone_code' => '+968'],
['iso_code' => 'JOR', 'name_ar' => 'الأردن', 'name_en' => 'Jordan', 'nationality_ar' => 'أردني', 'nationality_en' => 'Jordanian', 'phone_code' => '+962'],
['iso_code' => 'IRQ', 'name_ar' => 'العراق', 'name_en' => 'Iraq', 'nationality_ar' => 'عراقي', 'nationality_en' => 'Iraqi', 'phone_code' => '+964'],
['iso_code' => 'SYR', 'name_ar' => 'سوريا', 'name_en' => 'Syria', 'nationality_ar' => 'سوري', 'nationality_en' => 'Syrian', 'phone_code' => '+963'],
['iso_code' => 'LBN', 'name_ar' => 'لبنان', 'name_en' => 'Lebanon', 'nationality_ar' => 'لبناني', 'nationality_en' => 'Lebanese', 'phone_code' => '+961'],
['iso_code' => 'PSE', 'name_ar' => 'فلسطين', 'name_en' => 'Palestine', 'nationality_ar' => 'فلسطيني', 'nationality_en' => 'Palestinian', 'phone_code' => '+970'],
['iso_code' => 'YEM', 'name_ar' => 'اليمن', 'name_en' => 'Yemen', 'nationality_ar' => 'يمني', 'nationality_en' => 'Yemeni', 'phone_code' => '+967'],
['iso_code' => 'LBY', 'name_ar' => 'ليبيا', 'name_en' => 'Libya', 'nationality_ar' => 'ليبي', 'nationality_en' => 'Libyan', 'phone_code' => '+218'],
['iso_code' => 'TUN', 'name_ar' => 'تونس', 'name_en' => 'Tunisia', 'nationality_ar' => 'تونسي', 'nationality_en' => 'Tunisian', 'phone_code' => '+216'],
['iso_code' => 'DZA', 'name_ar' => 'الجزائر', 'name_en' => 'Algeria', 'nationality_ar' => 'جزائري', 'nationality_en' => 'Algerian', 'phone_code' => '+213'],
['iso_code' => 'MAR', 'name_ar' => 'المغرب', 'name_en' => 'Morocco', 'nationality_ar' => 'مغربي', 'nationality_en' => 'Moroccan', 'phone_code' => '+212'],
['iso_code' => 'SDN', 'name_ar' => 'السودان', 'name_en' => 'Sudan', 'nationality_ar' => 'سوداني', 'nationality_en' => 'Sudanese', 'phone_code' => '+249'],
['iso_code' => 'SOM', 'name_ar' => 'الصومال', 'name_en' => 'Somalia', 'nationality_ar' => 'صومالي', 'nationality_en' => 'Somali', 'phone_code' => '+252'],
['iso_code' => 'MRT', 'name_ar' => 'موريتانيا', 'name_en' => 'Mauritania', 'nationality_ar' => 'موريتاني', 'nationality_en' => 'Mauritanian', 'phone_code' => '+222'],
['iso_code' => 'COM', 'name_ar' => 'جزر القمر', 'name_en' => 'Comoros', 'nationality_ar' => 'قمري', 'nationality_en' => 'Comorian', 'phone_code' => '+269'],
['iso_code' => 'DJI', 'name_ar' => 'جيبوتي', 'name_en' => 'Djibouti', 'nationality_ar' => 'جيبوتي', 'nationality_en' => 'Djiboutian', 'phone_code' => '+253'],
['iso_code' => 'USA', 'name_ar' => 'الولايات المتحدة', 'name_en' => 'United States', 'nationality_ar' => 'أمريكي', 'nationality_en' => 'American', 'phone_code' => '+1'],
['iso_code' => 'GBR', 'name_ar' => 'المملكة المتحدة', 'name_en' => 'United Kingdom', 'nationality_ar' => 'بريطاني', 'nationality_en' => 'British', 'phone_code' => '+44'],
['iso_code' => 'FRA', 'name_ar' => 'فرنسا', 'name_en' => 'France', 'nationality_ar' => 'فرنسي', 'nationality_en' => 'French', 'phone_code' => '+33'],
['iso_code' => 'DEU', 'name_ar' => 'ألمانيا', 'name_en' => 'Germany', 'nationality_ar' => 'ألماني', 'nationality_en' => 'German', 'phone_code' => '+49'],
['iso_code' => 'ITA', 'name_ar' => 'إيطاليا', 'name_en' => 'Italy', 'nationality_ar' => 'إيطالي', 'nationality_en' => 'Italian', 'phone_code' => '+39'],
['iso_code' => 'ESP', 'name_ar' => 'إسبانيا', 'name_en' => 'Spain', 'nationality_ar' => 'إسباني', 'nationality_en' => 'Spanish', 'phone_code' => '+34'],
['iso_code' => 'TUR', 'name_ar' => 'تركيا', 'name_en' => 'Turkey', 'nationality_ar' => 'تركي', 'nationality_en' => 'Turkish', 'phone_code' => '+90'],
['iso_code' => 'IRN', 'name_ar' => 'إيران', 'name_en' => 'Iran', 'nationality_ar' => 'إيراني', 'nationality_en' => 'Iranian', 'phone_code' => '+98'],
['iso_code' => 'CHN', 'name_ar' => 'الصين', 'name_en' => 'China', 'nationality_ar' => 'صيني', 'nationality_en' => 'Chinese', 'phone_code' => '+86'],
['iso_code' => 'JPN', 'name_ar' => 'اليابان', 'name_en' => 'Japan', 'nationality_ar' => 'ياباني', 'nationality_en' => 'Japanese', 'phone_code' => '+81'],
['iso_code' => 'IND', 'name_ar' => 'الهند', 'name_en' => 'India', 'nationality_ar' => 'هندي', 'nationality_en' => 'Indian', 'phone_code' => '+91'],
['iso_code' => 'RUS', 'name_ar' => 'روسيا', 'name_en' => 'Russia', 'nationality_ar' => 'روسي', 'nationality_en' => 'Russian', 'phone_code' => '+7'],
['iso_code' => 'BRA', 'name_ar' => 'البرازيل', 'name_en' => 'Brazil', 'nationality_ar' => 'برازيلي', 'nationality_en' => 'Brazilian', 'phone_code' => '+55'],
['iso_code' => 'CAN', 'name_ar' => 'كندا', 'name_en' => 'Canada', 'nationality_ar' => 'كندي', 'nationality_en' => 'Canadian', 'phone_code' => '+1'],
['iso_code' => 'AUS', 'name_ar' => 'أستراليا', 'name_en' => 'Australia', 'nationality_ar' => 'أسترالي', 'nationality_en' => 'Australian', 'phone_code' => '+61'],
['iso_code' => 'ZAF', 'name_ar' => 'جنوب أفريقيا', 'name_en' => 'South Africa', 'nationality_ar' => 'جنوب أفريقي', 'nationality_en' => 'South African', 'phone_code' => '+27'],
['iso_code' => 'NGA', 'name_ar' => 'نيجيريا', 'name_en' => 'Nigeria', 'nationality_ar' => 'نيجيري', 'nationality_en' => 'Nigerian', 'phone_code' => '+234'],
['iso_code' => 'PAK', 'name_ar' => 'باكستان', 'name_en' => 'Pakistan', 'nationality_ar' => 'باكستاني', 'nationality_en' => 'Pakistani', 'phone_code' => '+92'],
['iso_code' => 'KOR', 'name_ar' => 'كوريا الجنوبية', 'name_en' => 'South Korea', 'nationality_ar' => 'كوري جنوبي', 'nationality_en' => 'South Korean', 'phone_code' => '+82'],
['iso_code' => 'MEX', 'name_ar' => 'المكسيك', 'name_en' => 'Mexico', 'nationality_ar' => 'مكسيكي', 'nationality_en' => 'Mexican', 'phone_code' => '+52'],
['iso_code' => 'NLD', 'name_ar' => 'هولندا', 'name_en' => 'Netherlands', 'nationality_ar' => 'هولندي', 'nationality_en' => 'Dutch', 'phone_code' => '+31'],
['iso_code' => 'CHE', 'name_ar' => 'سويسرا', 'name_en' => 'Switzerland', 'nationality_ar' => 'سويسري', 'nationality_en' => 'Swiss', 'phone_code' => '+41'],
['iso_code' => 'SWE', 'name_ar' => 'السويد', 'name_en' => 'Sweden', 'nationality_ar' => 'سويدي', 'nationality_en' => 'Swedish', 'phone_code' => '+46'],
['iso_code' => 'GRC', 'name_ar' => 'اليونان', 'name_en' => 'Greece', 'nationality_ar' => 'يوناني', 'nationality_en' => 'Greek', 'phone_code' => '+30'],
['iso_code' => 'ETH', 'name_ar' => 'إثيوبيا', 'name_en' => 'Ethiopia', 'nationality_ar' => 'إثيوبي', 'nationality_en' => 'Ethiopian', 'phone_code' => '+251'],
['iso_code' => 'KEN', 'name_ar' => 'كينيا', 'name_en' => 'Kenya', 'nationality_ar' => 'كيني', 'nationality_en' => 'Kenyan', 'phone_code' => '+254'],
['iso_code' => 'PHL', 'name_ar' => 'الفلبين', 'name_en' => 'Philippines', 'nationality_ar' => 'فلبيني', 'nationality_en' => 'Filipino', 'phone_code' => '+63'],
['iso_code' => 'IDN', 'name_ar' => 'إندونيسيا', 'name_en' => 'Indonesia', 'nationality_ar' => 'إندونيسي', 'nationality_en' => 'Indonesian', 'phone_code' => '+62'],
];
foreach ($countries as $c) {
$existing = $db->selectOne("SELECT id FROM countries WHERE iso_code = ?", [$c['iso_code']]);
if ($existing) {
continue;
}
$db->insert('countries', [
'iso_code' => $c['iso_code'],
'name_ar' => $c['name_ar'],
'name_en' => $c['name_en'],
'nationality_ar' => $c['nationality_ar'],
'nationality_en' => $c['nationality_en'],
'phone_code' => $c['phone_code'],
'is_active' => 1,
]);
}
};
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$qualifications = [
['code' => 'high', 'name_ar' => 'مؤهل عالي', 'name_en' => 'High Qualification', 'sort_order' => 1],
['code' => 'medium', 'name_ar' => 'مؤهل متوسط', 'name_en' => 'Medium Qualification', 'sort_order' => 2],
['code' => 'none', 'name_ar' => 'بدون مؤهل', 'name_en' => 'No Qualification', 'sort_order' => 3],
];
foreach ($qualifications as $q) {
$existing = $db->selectOne("SELECT id FROM qualifications WHERE code = ?", [$q['code']]);
if ($existing) {
continue;
}
$db->insert('qualifications', [
'code' => $q['code'],
'name_ar' => $q['name_ar'],
'name_en' => $q['name_en'],
'sort_order' => $q['sort_order'],
'is_active' => 1,
]);
}
};
\ 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