Commit 1e79566a authored by Administrator's avatar Administrator

Update 11 files via Son of Anton

parent 2ed42d50
<?php
declare(strict_types=1);
namespace App\Modules\Archive\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Modules\Archive\Models\ArchiveSnapshot;
use App\Modules\Archive\Services\ArchiveService;
class ArchiveController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'entity_type' => $request->get('entity_type', ''),
'membership_number' => $request->get('membership_number', ''),
'snapshot_reason' => $request->get('snapshot_reason', ''),
'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 = ArchiveSnapshot::search($filters, 25, $page);
$entityTypes = ArchiveSnapshot::getDistinctEntityTypes();
$reasons = ArchiveSnapshot::getDistinctReasons();
return $this->view('Archive.Views.index', [
'rows' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'entityTypes' => $entityTypes,
'reasons' => $reasons,
]);
}
public function show(Request $request, string $id): Response
{
$snapshot = ArchiveService::getSnapshot((int) $id);
if (!$snapshot) {
return $this->redirect('/archive')->withError('اللقطة الأرشيفية غير موجودة');
}
// Get other snapshots for same entity for navigation
$otherSnapshots = ArchiveSnapshot::getForEntity($snapshot['entity_type'], (int) $snapshot['entity_id']);
return $this->view('Archive.Views.show', [
'snapshot' => $snapshot,
'otherSnapshots' => $otherSnapshots,
]);
}
public function compare(Request $request, string $id1, string $id2): Response
{
try {
$diff = ArchiveService::compareSnapshots((int) $id1, (int) $id2);
} catch (\RuntimeException $e) {
return $this->redirect('/archive')->withError($e->getMessage());
}
$snap1 = ArchiveService::getSnapshot((int) $id1);
$snap2 = ArchiveService::getSnapshot((int) $id2);
return $this->view('Archive.Views.compare', [
'diff' => $diff,
'snap1' => $snap1,
'snap2' => $snap2,
]);
}
public function entitySnapshots(Request $request, string $type, string $id): Response
{
$snapshots = ArchiveService::getSnapshots($type, (int) $id);
return $this->view('Archive.Views.index', [
'rows' => $snapshots,
'pagination' => ['last_page' => 1, 'current_page' => 1],
'filters' => ['entity_type' => $type, 'search' => '', 'membership_number' => '', 'snapshot_reason' => '', 'date_from' => '', 'date_to' => ''],
'entityTypes' => ArchiveSnapshot::getDistinctEntityTypes(),
'reasons' => ArchiveSnapshot::getDistinctReasons(),
'entityFilter' => ['type' => $type, 'id' => (int) $id],
]);
}
public function numberChain(Request $request, string $number): Response
{
$chain = ArchiveService::getNumberChain($number);
$snapshots = ArchiveSnapshot::getByMembershipNumber($number);
return $this->view('Archive.Views.show', [
'snapshot' => null,
'otherSnapshots' => $snapshots,
'numberChain' => $chain,
'membershipNumber' => $number,
'isChainView' => true,
]);
}
public function takeManual(Request $request): Response
{
$entityType = trim((string) $request->post('entity_type', ''));
$entityId = (int) $request->post('entity_id', 0);
$reason = trim((string) $request->post('reason', 'manual'));
$notes = trim((string) $request->post('notes', ''));
if ($entityType === '' || $entityId <= 0) {
return $this->redirect('/archive')->withError('بيانات غير مكتملة');
}
try {
$snapshotId = ArchiveService::takeSnapshot(
$entityType,
$entityId,
$reason,
$notes ?: null
);
return $this->redirect("/archive/{$snapshotId}")->withSuccess('تم أخذ اللقطة الأرشيفية بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/archive')->withError('فشل أخذ اللقطة: ' . $e->getMessage());
}
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Archive\Models;
use App\Core\App;
use App\Core\Pagination;
class ArchiveSnapshot
{
public static function create(array $data): int
{
$db = App::getInstance()->db();
return $db->insert('archive_snapshots', $data);
}
public static function find(int $id): ?array
{
$db = App::getInstance()->db();
return $db->selectOne("SELECT * FROM archive_snapshots WHERE id = ?", [$id]);
}
public static function getForEntity(string $entityType, int $entityId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM archive_snapshots WHERE entity_type = ? AND entity_id = ? ORDER BY snapshot_taken_at DESC",
[$entityType, $entityId]
);
}
public static function getByMembershipNumber(string $number): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM archive_snapshots WHERE membership_number = ? ORDER BY snapshot_taken_at DESC",
[$number]
);
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = '1=1';
$params = [];
if (!empty($filters['entity_type'])) {
$where .= ' AND a.entity_type = ?';
$params[] = $filters['entity_type'];
}
if (!empty($filters['membership_number'])) {
$where .= ' AND a.membership_number = ?';
$params[] = $filters['membership_number'];
}
if (!empty($filters['snapshot_reason'])) {
$where .= ' AND a.snapshot_reason = ?';
$params[] = $filters['snapshot_reason'];
}
if (!empty($filters['date_from'])) {
$where .= ' AND a.snapshot_taken_at >= ?';
$params[] = $filters['date_from'] . ' 00:00:00';
}
if (!empty($filters['date_to'])) {
$where .= ' AND a.snapshot_taken_at <= ?';
$params[] = $filters['date_to'] . ' 23:59:59';
}
if (!empty($filters['search'])) {
$where .= ' AND (a.membership_number LIKE ? OR a.entity_type LIKE ? OR a.notes LIKE ?)';
$s = '%' . $filters['search'] . '%';
$params[] = $s;
$params[] = $s;
$params[] = $s;
}
$countRow = $db->selectOne("SELECT COUNT(*) as cnt FROM archive_snapshots a WHERE {$where}", $params);
$total = (int) ($countRow['cnt'] ?? 0);
$offset = ($page - 1) * $perPage;
$rows = $db->select(
"SELECT a.* FROM archive_snapshots a WHERE {$where} ORDER BY a.snapshot_taken_at DESC LIMIT {$perPage} OFFSET {$offset}",
$params
);
$pagination = Pagination::paginate($total, $perPage, $page);
return ['data' => $rows, 'pagination' => $pagination];
}
public static function getDistinctEntityTypes(): array
{
$db = App::getInstance()->db();
$rows = $db->select("SELECT DISTINCT entity_type FROM archive_snapshots ORDER BY entity_type");
return array_column($rows, 'entity_type');
}
public static function getDistinctReasons(): array
{
$db = App::getInstance()->db();
$rows = $db->select("SELECT DISTINCT snapshot_reason FROM archive_snapshots ORDER BY snapshot_reason");
return array_column($rows, 'snapshot_reason');
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Archive\Models;
use App\Core\App;
class MembershipNumberChain
{
public static function create(array $data): int
{
$db = App::getInstance()->db();
return $db->insert('membership_number_chain', $data);
}
public static function find(int $id): ?array
{
$db = App::getInstance()->db();
return $db->selectOne("SELECT * FROM membership_number_chain WHERE id = ?", [$id]);
}
public static function getChainForNumber(string $membershipNumber): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM membership_number_chain WHERE membership_number = ? ORDER BY held_from ASC",
[$membershipNumber]
);
}
public static function getCurrentHolder(string $membershipNumber): ?array
{
$db = App::getInstance()->db();
return $db->selectOne(
"SELECT * FROM membership_number_chain WHERE membership_number = ? AND held_until IS NULL ORDER BY held_from DESC LIMIT 1",
[$membershipNumber]
);
}
public static function getHoldersForEntity(string $entityType, int $entityId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM membership_number_chain WHERE holder_entity_type = ? AND holder_entity_id = ? ORDER BY held_from DESC",
[$entityType, $entityId]
);
}
public static function endCurrentHolder(string $membershipNumber): void
{
$db = App::getInstance()->db();
$db->update(
'membership_number_chain',
['held_until' => date('Y-m-d H:i:s')],
'`membership_number` = ? AND `held_until` IS NULL',
[$membershipNumber]
);
}
}
\ No newline at end of file
<?php
declare(strict_types=1);
return [
['GET', '/archive', 'Archive\Controllers\ArchiveController@index', ['auth'], 'report.view_audit'],
['GET', '/archive/{id:\d+}', 'Archive\Controllers\ArchiveController@show', ['auth'], 'report.view_audit'],
['GET', '/archive/compare/{id1:\d+}/{id2:\d+}', 'Archive\Controllers\ArchiveController@compare', ['auth'], 'report.view_audit'],
['GET', '/archive/entity/{type}/{id:\d+}', 'Archive\Controllers\ArchiveController@entitySnapshots', ['auth'], 'report.view_audit'],
['GET', '/archive/number-chain/{number}', 'Archive\Controllers\ArchiveController@numberChain', ['auth'], 'report.view_audit'],
['POST', '/archive/snapshot', 'Archive\Controllers\ArchiveController@takeManual', ['auth', 'csrf'], 'settings.edit'],
];
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Archive\Services;
use App\Core\App;
use App\Core\Logger;
use App\Modules\Archive\Models\ArchiveSnapshot;
use App\Modules\Archive\Models\MembershipNumberChain;
final class ArchiveService
{
/**
* Takes a complete snapshot of an entity and all its related data.
* This is the core archive operation — called before major lifecycle events.
*
* @param string $entityType Table name or entity identifier (e.g., 'members', 'employees')
* @param int $entityId Primary key of the entity
* @param string $reason Why the snapshot is being taken
* @param string|null $notes Optional human-readable notes
* @param array|null $relatedData Optional pre-loaded related data
* @param string|null $membershipNumber Optional membership number for cross-reference
* @return int The snapshot ID
*/
public static function takeSnapshot(
string $entityType,
int $entityId,
string $reason,
?string $notes = null,
?array $relatedData = null,
?string $membershipNumber = null
): int {
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
// Load the main entity record
$entityData = $db->selectOne("SELECT * FROM `{$entityType}` WHERE id = ?", [$entityId]);
if (!$entityData) {
throw new \RuntimeException("Entity not found: {$entityType} #{$entityId}");
}
// If membership_number not provided, try to extract from entity data
if ($membershipNumber === null && isset($entityData['membership_number'])) {
$membershipNumber = $entityData['membership_number'];
}
// If related data not provided, try to auto-load based on entity type
if ($relatedData === null) {
$relatedData = self::autoLoadRelatedData($entityType, $entityId, $db);
}
$snapshotId = ArchiveSnapshot::create([
'entity_type' => $entityType,
'entity_id' => $entityId,
'membership_number' => $membershipNumber,
'snapshot_reason' => $reason,
'full_data_json' => json_encode($entityData, JSON_UNESCAPED_UNICODE),
'related_data_json' => $relatedData !== null ? json_encode($relatedData, JSON_UNESCAPED_UNICODE) : null,
'snapshot_taken_by' => $employee ? (int) ($employee->id ?? 0) : null,
'snapshot_taken_at' => date('Y-m-d H:i:s'),
'notes' => $notes,
]);
Logger::info("Archive snapshot taken", [
'snapshot_id' => $snapshotId,
'entity_type' => $entityType,
'entity_id' => $entityId,
'reason' => $reason,
]);
return $snapshotId;
}
/**
* Auto-loads related data for known entity types.
* For unknown types, returns null (caller can provide their own).
*/
private static function autoLoadRelatedData(string $entityType, int $entityId, $db): ?array
{
$related = [];
switch ($entityType) {
case 'members':
// Load spouses if table exists
if ($db->tableExists('spouses')) {
$related['spouses'] = $db->select(
"SELECT * FROM spouses WHERE member_id = ? AND is_archived = 0",
[$entityId]
);
}
// Load children if table exists
if ($db->tableExists('children')) {
$related['children'] = $db->select(
"SELECT * FROM children WHERE member_id = ? AND is_archived = 0",
[$entityId]
);
}
// Load temporary members if table exists
if ($db->tableExists('temporary_members')) {
$related['temporary_members'] = $db->select(
"SELECT * FROM temporary_members WHERE member_id = ? AND is_archived = 0",
[$entityId]
);
}
// Load payments if table exists
if ($db->tableExists('payments')) {
$related['payments'] = $db->select(
"SELECT * FROM payments WHERE member_id = ? AND is_voided = 0 ORDER BY payment_date DESC",
[$entityId]
);
}
// Load installment plans if table exists
if ($db->tableExists('installment_plans')) {
$related['installment_plans'] = $db->select(
"SELECT * FROM installment_plans WHERE member_id = ?",
[$entityId]
);
}
// Load subscriptions if table exists
if ($db->tableExists('subscriptions')) {
$related['subscriptions'] = $db->select(
"SELECT * FROM subscriptions WHERE member_id = ? ORDER BY financial_year DESC",
[$entityId]
);
}
// Load fines if table exists
if ($db->tableExists('fines')) {
$related['fines'] = $db->select(
"SELECT * FROM fines WHERE member_id = ?",
[$entityId]
);
}
// Load documents if table exists
if ($db->tableExists('documents')) {
$related['documents'] = $db->select(
"SELECT id, member_id, document_type, original_filename, file_path, uploaded_at FROM documents WHERE member_id = ? AND is_archived = 0",
[$entityId]
);
}
// Load seasonal if table exists
if ($db->tableExists('seasonal_memberships')) {
$related['seasonal_memberships'] = $db->select(
"SELECT * FROM seasonal_memberships WHERE member_id = ? AND is_archived = 0",
[$entityId]
);
}
// Load sports if table exists
if ($db->tableExists('sports_members')) {
$related['sports_members'] = $db->select(
"SELECT * FROM sports_members WHERE member_id = ? AND is_archived = 0",
[$entityId]
);
}
// Load honorary if table exists
if ($db->tableExists('honorary_members')) {
$related['honorary_members'] = $db->select(
"SELECT * FROM honorary_members WHERE member_id = ? AND is_archived = 0",
[$entityId]
);
}
// Load foreign details if table exists
if ($db->tableExists('foreign_member_details')) {
$related['foreign_member_details'] = $db->select(
"SELECT * FROM foreign_member_details WHERE member_id = ? AND is_archived = 0",
[$entityId]
);
}
break;
case 'employees':
// Load employee roles
$related['roles'] = $db->select(
"SELECT er.*, r.role_code, r.name_ar FROM employee_roles er JOIN roles r ON r.id = er.role_id WHERE er.employee_id = ? AND er.is_active = 1",
[$entityId]
);
break;
case 'roles':
// Load role permissions
$related['permissions'] = $db->select(
"SELECT * FROM role_permissions WHERE role_id = ?",
[$entityId]
);
break;
default:
// For unknown entity types, return empty array — caller can provide their own
return !empty($related) ? $related : null;
}
return !empty($related) ? $related : null;
}
/**
* Get all snapshots for an entity.
*/
public static function getSnapshots(string $entityType, int $entityId): array
{
return ArchiveSnapshot::getForEntity($entityType, $entityId);
}
/**
* Get a single snapshot with decoded JSON.
*/
public static function getSnapshot(int $snapshotId): ?array
{
$snapshot = ArchiveSnapshot::find($snapshotId);
if (!$snapshot) {
return null;
}
$snapshot['full_data'] = json_decode($snapshot['full_data_json'] ?? '{}', true) ?? [];
$snapshot['related_data'] = json_decode($snapshot['related_data_json'] ?? 'null', true);
return $snapshot;
}
/**
* Compare two snapshots and return the differences.
*/
public static function compareSnapshots(int $snapshotId1, int $snapshotId2): array
{
$snap1 = self::getSnapshot($snapshotId1);
$snap2 = self::getSnapshot($snapshotId2);
if (!$snap1 || !$snap2) {
throw new \RuntimeException('One or both snapshots not found');
}
$diff = [
'snapshot_1' => [
'id' => $snap1['id'],
'reason' => $snap1['snapshot_reason'],
'taken_at' => $snap1['snapshot_taken_at'],
],
'snapshot_2' => [
'id' => $snap2['id'],
'reason' => $snap2['snapshot_reason'],
'taken_at' => $snap2['snapshot_taken_at'],
],
'main_entity_diff' => self::diffArrays($snap1['full_data'], $snap2['full_data']),
'related_diff' => self::diffRelatedData(
$snap1['related_data'] ?? [],
$snap2['related_data'] ?? []
),
];
return $diff;
}
/**
* Recursively diff two associative arrays.
* Returns fields that changed with old/new values.
*/
public static function diffArrays(array $old, array $new): array
{
$diff = [];
$allKeys = array_unique(array_merge(array_keys($old), array_keys($new)));
foreach ($allKeys as $key) {
$oldVal = $old[$key] ?? null;
$newVal = $new[$key] ?? null;
if (is_array($oldVal) && is_array($newVal)) {
$subDiff = self::diffArrays($oldVal, $newVal);
if (!empty($subDiff)) {
$diff[$key] = $subDiff;
}
} elseif ((string) $oldVal !== (string) $newVal) {
$diff[$key] = [
'old' => $oldVal,
'new' => $newVal,
];
}
}
return $diff;
}
/**
* Diff related data sections.
*/
private static function diffRelatedData(?array $old, ?array $new): array
{
$diff = [];
$old = $old ?? [];
$new = $new ?? [];
$allSections = array_unique(array_merge(array_keys($old), array_keys($new)));
foreach ($allSections as $section) {
$oldSection = $old[$section] ?? [];
$newSection = $new[$section] ?? [];
$oldCount = count($oldSection);
$newCount = count($newSection);
if ($oldCount !== $newCount) {
$diff[$section] = [
'count_changed' => true,
'old_count' => $oldCount,
'new_count' => $newCount,
];
}
// Compare individual records by ID if available
$oldById = [];
$newById = [];
foreach ($oldSection as $item) {
$id = $item['id'] ?? null;
if ($id !== null) {
$oldById[$id] = $item;
}
}
foreach ($newSection as $item) {
$id = $item['id'] ?? null;
if ($id !== null) {
$newById[$id] = $item;
}
}
// Added records
$addedIds = array_diff(array_keys($newById), array_keys($oldById));
if (!empty($addedIds)) {
$diff[$section]['added'] = array_values(array_intersect_key($newById, array_flip($addedIds)));
}
// Removed records
$removedIds = array_diff(array_keys($oldById), array_keys($newById));
if (!empty($removedIds)) {
$diff[$section]['removed'] = array_values(array_intersect_key($oldById, array_flip($removedIds)));
}
// Changed records
$commonIds = array_intersect(array_keys($oldById), array_keys($newById));
foreach ($commonIds as $id) {
$fieldDiff = self::diffArrays($oldById[$id], $newById[$id]);
if (!empty($fieldDiff)) {
$diff[$section]['changed'][$id] = $fieldDiff;
}
}
}
return $diff;
}
/**
* Record a membership number transfer in the chain.
*/
public static function recordNumberTransfer(
string $membershipNumber,
string $holderType,
string $entityType,
int $entityId,
?int $previousHolderId = null
): int {
// End the current holder's record
MembershipNumberChain::endCurrentHolder($membershipNumber);
// Create new chain entry
$chainId = MembershipNumberChain::create([
'membership_number' => $membershipNumber,
'holder_type' => $holderType,
'holder_entity_type' => $entityType,
'holder_entity_id' => $entityId,
'previous_holder_id' => $previousHolderId,
'held_from' => date('Y-m-d H:i:s'),
'held_until' => null,
'created_at' => date('Y-m-d H:i:s'),
]);
Logger::info("Membership number chain updated", [
'membership_number' => $membershipNumber,
'holder_type' => $holderType,
'entity_type' => $entityType,
'entity_id' => $entityId,
'chain_id' => $chainId,
]);
return $chainId;
}
/**
* Get the complete ownership chain for a membership number.
*/
public static function getNumberChain(string $membershipNumber): array
{
return MembershipNumberChain::getChainForNumber($membershipNumber);
}
/**
* Get the current holder of a membership number.
*/
public static function getCurrentHolder(string $membershipNumber): ?array
{
return MembershipNumberChain::getCurrentHolder($membershipNumber);
}
}
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>مقارنة اللقطات: #<?= (int) ($snap1['id'] ?? 0) ?> ↔ #<?= (int) ($snap2['id'] ?? 0) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/archive/<?= (int) ($snap1['id'] ?? 0) ?>" class="btn btn-outline">← لقطة #<?= (int) ($snap1['id'] ?? 0) ?></a>
<a href="/archive/<?= (int) ($snap2['id'] ?? 0) ?>" class="btn btn-outline">← لقطة #<?= (int) ($snap2['id'] ?? 0) ?></a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Comparison Header -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;">
<div class="card" style="padding:15px;border-right:4px solid #DC2626;">
<div style="font-weight:700;color:#DC2626;margin-bottom:5px;">لقطة #<?= (int) ($diff['snapshot_1']['id'] ?? 0) ?> (قبل)</div>
<div style="font-size:13px;color:#6B7280;">السبب: <?= e($diff['snapshot_1']['reason'] ?? '') ?></div>
<div style="font-size:12px;color:#9CA3AF;"><?= e($diff['snapshot_1']['taken_at'] ?? '') ?></div>
</div>
<div class="card" style="padding:15px;border-right:4px solid #059669;">
<div style="font-weight:700;color:#059669;margin-bottom:5px;">لقطة #<?= (int) ($diff['snapshot_2']['id'] ?? 0) ?> (بعد)</div>
<div style="font-size:13px;color:#6B7280;">السبب: <?= e($diff['snapshot_2']['reason'] ?? '') ?></div>
<div style="font-size:12px;color:#9CA3AF;"><?= e($diff['snapshot_2']['taken_at'] ?? '') ?></div>
</div>
</div>
<!-- Main Entity Diff -->
<?php $mainDiff = $diff['main_entity_diff'] ?? []; ?>
<?php if (empty($mainDiff)): ?>
<div class="card" style="padding:30px;text-align:center;color:#059669;margin-bottom:20px;">
<strong>✓ لا توجد اختلافات في بيانات الكيان الرئيسية</strong>
</div>
<?php else: ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">اختلافات الكيان الرئيسي <span style="color:#DC2626;font-size:14px;">(<?= count($mainDiff) ?> حقل)</span></h3>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th style="width:30%;">الحقل</th>
<th style="width:35%;">القيمة القديمة</th>
<th style="width:35%;">القيمة الجديدة</th>
</tr>
</thead>
<tbody>
<?php foreach ($mainDiff as $field => $change): ?>
<tr>
<td style="font-weight:600;"><?= e($field) ?></td>
<?php if (isset($change['old']) || isset($change['new'])): ?>
<td style="background:#FEF2F2;color:#DC2626;font-size:13px;word-break:break-all;">
<?php
$oldVal = $change['old'] ?? null;
if ($oldVal === null) {
echo '<span style="color:#D1D5DB;">NULL</span>';
} elseif (is_array($oldVal)) {
echo '<code style="font-size:11px;">' . e(json_encode($oldVal, JSON_UNESCAPED_UNICODE)) . '</code>';
} else {
echo e((string) $oldVal);
}
?>
</td>
<td style="background:#F0FDF4;color:#059669;font-size:13px;word-break:break-all;">
<?php
$newVal = $change['new'] ?? null;
if ($newVal === null) {
echo '<span style="color:#D1D5DB;">NULL</span>';
} elseif (is_array($newVal)) {
echo '<code style="font-size:11px;">' . e(json_encode($newVal, JSON_UNESCAPED_UNICODE)) . '</code>';
} else {
echo e((string) $newVal);
}
?>
</td>
<?php else: ?>
<td colspan="2" style="font-size:12px;color:#6B7280;">
<details>
<summary style="cursor:pointer;color:#0D7377;">كائن متداخل — اضغط للعرض</summary>
<pre style="margin-top:5px;font-size:11px;background:#F9FAFB;padding:8px;border-radius:4px;overflow-x:auto;"><?= e(json_encode($change, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) ?></pre>
</details>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<!-- Related Data Diff -->
<?php $relatedDiff = $diff['related_diff'] ?? []; ?>
<?php if (!empty($relatedDiff)): ?>
<?php foreach ($relatedDiff as $section => $sectionDiff): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;"><?= e($section) ?></h3>
</div>
<div style="padding:20px;">
<?php if (isset($sectionDiff['count_changed']) && $sectionDiff['count_changed']): ?>
<div style="margin-bottom:15px;padding:10px;background:#FFF7ED;border:1px solid #FED7AA;border-radius:6px;font-size:13px;">
عدد السجلات: <span style="color:#DC2626;"><?= (int) $sectionDiff['old_count'] ?></span><span style="color:#059669;"><?= (int) $sectionDiff['new_count'] ?></span>
</div>
<?php endif; ?>
<?php if (!empty($sectionDiff['added'])): ?>
<div style="margin-bottom:10px;">
<strong style="color:#059669;">✚ سجلات مُضافة (<?= count($sectionDiff['added']) ?>)</strong>
<?php foreach ($sectionDiff['added'] as $added): ?>
<div style="margin-top:5px;padding:8px;background:#F0FDF4;border:1px solid #BBF7D0;border-radius:4px;font-size:12px;">
ID: <?= (int) ($added['id'] ?? 0) ?>
<?php if (isset($added['full_name_ar'])): ?><?= e($added['full_name_ar']) ?><?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($sectionDiff['removed'])): ?>
<div style="margin-bottom:10px;">
<strong style="color:#DC2626;">✖ سجلات مُزالة (<?= count($sectionDiff['removed']) ?>)</strong>
<?php foreach ($sectionDiff['removed'] as $removed): ?>
<div style="margin-top:5px;padding:8px;background:#FEF2F2;border:1px solid #FECACA;border-radius:4px;font-size:12px;">
ID: <?= (int) ($removed['id'] ?? 0) ?>
<?php if (isset($removed['full_name_ar'])): ?><?= e($removed['full_name_ar']) ?><?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($sectionDiff['changed'])): ?>
<div>
<strong style="color:#D97706;">✎ سجلات مُعدّلة (<?= count($sectionDiff['changed']) ?>)</strong>
<?php foreach ($sectionDiff['changed'] as $changedId => $fieldChanges): ?>
<details style="margin-top:5px;border:1px solid #FED7AA;border-radius:4px;">
<summary style="padding:8px 12px;cursor:pointer;background:#FFF7ED;font-size:13px;">سجل #<?= (int) $changedId ?><?= count($fieldChanges) ?> حقل متغير</summary>
<table style="width:100%;font-size:12px;margin:0;">
<thead><tr style="background:#F9FAFB;"><th style="padding:4px 10px;">الحقل</th><th style="padding:4px 10px;">قبل</th><th style="padding:4px 10px;">بعد</th></tr></thead>
<tbody>
<?php foreach ($fieldChanges as $fk => $fc): ?>
<?php if (isset($fc['old']) || isset($fc['new'])): ?>
<tr>
<td style="padding:4px 10px;font-weight:600;"><?= e($fk) ?></td>
<td style="padding:4px 10px;color:#DC2626;background:#FEF2F2;"><?= e((string) ($fc['old'] ?? 'NULL')) ?></td>
<td style="padding:4px 10px;color:#059669;background:#F0FDF4;"><?= e((string) ($fc['new'] ?? 'NULL')) ?></td>
</tr>
<?php endif; ?>
<?php endforeach; ?>
</tbody>
</table>
</details>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (empty($sectionDiff['added']) && empty($sectionDiff['removed']) && empty($sectionDiff['changed']) && empty($sectionDiff['count_changed'])): ?>
<div style="color:#059669;text-align:center;">✓ لا توجد اختلافات</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="card" style="padding:30px;text-align:center;color:#059669;">
<strong>✓ لا توجد اختلافات في البيانات المرتبطة</strong>
</div>
<?php endif; ?>
<?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="/archive" 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="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="snapshot_reason" class="form-select" style="min-width:120px;">
<option value="">الكل</option>
<?php foreach ($reasons as $r): ?>
<option value="<?= e($r) ?>" <?= ($filters['snapshot_reason'] ?? '') === $r ? 'selected' : '' ?>><?= e($r) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label" style="font-size:12px;">رقم العضوية</label>
<input type="text" name="membership_number" value="<?= e($filters['membership_number'] ?? '') ?>" placeholder="رقم العضوية" class="form-input" style="min-width:120px;">
</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="/archive" 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>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr>
<td><?= (int) $r['id'] ?></td>
<td style="font-size:12px;white-space:nowrap;"><?= e($r['snapshot_taken_at']) ?></td>
<td><code style="font-size:12px;"><?= e($r['entity_type']) ?></code></td>
<td><?= (int) $r['entity_id'] ?></td>
<td>
<?php if ($r['membership_number']): ?>
<a href="/archive/number-chain/<?= urlencode($r['membership_number']) ?>" style="color:#0D7377;font-weight:600;"><?= e($r['membership_number']) ?></a>
<?php else: ?>
<?php endif; ?>
</td>
<td>
<?php
$reasonColors = [
'transfer' => '#0284C7', 'separation' => '#D97706', 'divorce' => '#DC2626',
'death' => '#6B7280', 'waiver' => '#7C3AED', 'status_change' => '#059669',
'data_correction' => '#0D7377', 'manual' => '#9CA3AF',
];
$color = $reasonColors[$r['snapshot_reason']] ?? '#6B7280';
?>
<span style="color:<?= $color ?>;font-weight:600;font-size:13px;"><?= e($r['snapshot_reason']) ?></span>
</td>
<td style="font-size:13px;"><?= $r['snapshot_taken_by'] ? '#' . (int) $r['snapshot_taken_by'] : 'النظام' ?></td>
<td style="font-size:12px;color:#6B7280;max-width:200px;overflow:hidden;text-overflow:ellipsis;"><?= e(mb_substr($r['notes'] ?? '', 0, 80)) ?: '—' ?></td>
<td>
<div style="display:flex;gap:5px;">
<a href="/archive/<?= (int) $r['id'] ?>" class="btn btn-sm btn-outline">عرض</a>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?>
<tr><td colspan="9" style="text-align:center;padding:40px;color:#6B7280;">لا توجد لقطات أرشيفية</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if (isset($pagination['last_page']) && $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'] ?? false): ?>
<li><a href="?page=<?= $pagination['prev_page'] ?>&q=<?= urlencode($filters['search'] ?? '') ?>&entity_type=<?= urlencode($filters['entity_type'] ?? '') ?>&snapshot_reason=<?= urlencode($filters['snapshot_reason'] ?? '') ?>&membership_number=<?= urlencode($filters['membership_number'] ?? '') ?>&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'] ?? '') ?>&entity_type=<?= urlencode($filters['entity_type'] ?? '') ?>&snapshot_reason=<?= urlencode($filters['snapshot_reason'] ?? '') ?>&membership_number=<?= urlencode($filters['membership_number'] ?? '') ?>&date_from=<?= urlencode($filters['date_from'] ?? '') ?>&date_to=<?= urlencode($filters['date_to'] ?? '') ?>" class="btn btn-sm <?= $p === ($pagination['current_page'] ?? 1) ? 'btn-primary' : 'btn-outline' ?>"><?= $p ?></a></li>
<?php endif; ?>
<?php endforeach; ?>
<?php if ($pagination['has_next'] ?? false): ?>
<li><a href="?page=<?= $pagination['next_page'] ?>&q=<?= urlencode($filters['search'] ?? '') ?>&entity_type=<?= urlencode($filters['entity_type'] ?? '') ?>&snapshot_reason=<?= urlencode($filters['snapshot_reason'] ?? '') ?>&membership_number=<?= urlencode($filters['membership_number'] ?? '') ?>&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 $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>
<?php if (!empty($isChainView)): ?>
سلسلة ملكية رقم العضوية: <?= e($membershipNumber ?? '') ?>
<?php else: ?>
لقطة أرشيفية #<?= (int) ($snapshot['id'] ?? 0) ?>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/archive" class="btn btn-outline">← العودة للأرشيف</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php if (!empty($isChainView)): ?>
<!-- Number Chain View -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">سلسلة ملكية رقم العضوية: <?= e($membershipNumber ?? '') ?></h3>
</div>
<div style="padding:20px;">
<?php if (empty($numberChain)): ?>
<div style="text-align:center;padding:30px;color:#6B7280;">لا توجد سجلات في سلسلة الملكية</div>
<?php else: ?>
<?php foreach ($numberChain as $i => $link): ?>
<div style="display:flex;gap:15px;padding:15px 0;<?= $i < count($numberChain) - 1 ? 'border-bottom:1px solid #F3F4F6;' : '' ?>">
<div style="flex-shrink:0;width:40px;height:40px;border-radius:50%;background:<?= $link['held_until'] === null ? '#059669' : '#E5E7EB' ?>;display:flex;align-items:center;justify-content:center;color:<?= $link['held_until'] === null ? '#fff' : '#6B7280' ?>;font-weight:700;font-size:14px;">
<?= $i + 1 ?>
</div>
<div style="flex:1;">
<div style="display:flex;justify-content:space-between;margin-bottom:5px;">
<strong style="color:#1A1A2E;"><?= e($link['holder_type']) ?></strong>
<?php if ($link['held_until'] === null): ?>
<span style="background:#F0FDF4;color:#059669;padding:2px 8px;border-radius:4px;font-size:12px;">الحالي</span>
<?php endif; ?>
</div>
<div style="font-size:13px;color:#6B7280;">
الكيان: <code><?= e($link['holder_entity_type']) ?></code> #<?= (int) $link['holder_entity_id'] ?>
</div>
<div style="font-size:12px;color:#9CA3AF;margin-top:4px;">
من: <?= e($link['held_from']) ?>
<?php if ($link['held_until']): ?>
— إلى: <?= e($link['held_until']) ?>
<?php else: ?>
— حتى الآن
<?php endif; ?>
</div>
<?php if ($link['previous_holder_id']): ?>
<div style="font-size:12px;color:#9CA3AF;">المالك السابق: سلسلة #<?= (int) $link['previous_holder_id'] ?></div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<?php if (!empty($otherSnapshots)): ?>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">اللقطات الأرشيفية المرتبطة</h3>
</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 ($otherSnapshots as $s): ?>
<tr>
<td><?= (int) $s['id'] ?></td>
<td style="font-size:12px;"><?= e($s['snapshot_taken_at']) ?></td>
<td style="font-weight:600;"><?= e($s['snapshot_reason']) ?></td>
<td style="font-size:12px;color:#6B7280;"><?= e(mb_substr($s['notes'] ?? '', 0, 100)) ?: '—' ?></td>
<td><a href="/archive/<?= (int) $s['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<?php else: ?>
<!-- Single Snapshot View -->
<div style="display:grid;grid-template-columns:1fr 300px;gap:20px;">
<div>
<!-- Snapshot Info -->
<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;">معلومات اللقطة</h3>
<span style="background:#F3F4F6;padding:4px 12px;border-radius:4px;font-size:13px;font-weight:600;"><?= e($snapshot['snapshot_reason']) ?></span>
</div>
<div style="padding:20px;">
<table style="width:100%;font-size:14px;">
<tr><td style="padding:6px 0;color:#6B7280;width:35%;">نوع الكيان</td><td style="padding:6px 0;"><code><?= e($snapshot['entity_type']) ?></code></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">رقم الكيان</td><td style="padding:6px 0;"><?= (int) $snapshot['entity_id'] ?></td></tr>
<?php if ($snapshot['membership_number']): ?>
<tr><td style="padding:6px 0;color:#6B7280;">رقم العضوية</td><td style="padding:6px 0;"><a href="/archive/number-chain/<?= urlencode($snapshot['membership_number']) ?>" style="color:#0D7377;font-weight:600;"><?= e($snapshot['membership_number']) ?></a></td></tr>
<?php endif; ?>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ اللقطة</td><td style="padding:6px 0;"><?= e($snapshot['snapshot_taken_at']) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">بواسطة</td><td style="padding:6px 0;"><?= $snapshot['snapshot_taken_by'] ? '#' . (int) $snapshot['snapshot_taken_by'] : 'النظام' ?></td></tr>
<?php if ($snapshot['notes']): ?>
<tr><td style="padding:6px 0;color:#6B7280;">ملاحظات</td><td style="padding:6px 0;"><?= e($snapshot['notes']) ?></td></tr>
<?php endif; ?>
</table>
</div>
</div>
<!-- Main Entity 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;">
<table style="width:100%;font-size:13px;">
<?php foreach ($snapshot['full_data'] as $key => $value): ?>
<tr style="border-bottom:1px solid #F9FAFB;">
<td style="padding:6px 0;color:#6B7280;width:35%;font-weight:500;"><?= e($key) ?></td>
<td style="padding:6px 0;">
<?php if (is_array($value)): ?>
<code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:3px;"><?= e(json_encode($value, JSON_UNESCAPED_UNICODE)) ?></code>
<?php elseif ($value === null): ?>
<span style="color:#D1D5DB;">NULL</span>
<?php elseif (mb_strlen((string) $value) > 100): ?>
<details>
<summary style="cursor:pointer;color:#0D7377;font-size:12px;">عرض (<?= mb_strlen((string) $value) ?> حرف)</summary>
<div style="margin-top:5px;padding:8px;background:#F9FAFB;border-radius:4px;font-size:12px;word-break:break-all;"><?= e((string) $value) ?></div>
</details>
<?php else: ?>
<?= e((string) $value) ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
<!-- Related Data -->
<?php if (!empty($snapshot['related_data'])): ?>
<?php foreach ($snapshot['related_data'] as $section => $records): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;"><?= e($section) ?> <span style="color:#9CA3AF;font-size:14px;">(<?= count($records) ?>)</span></h3>
</div>
<div style="padding:20px;">
<?php if (empty($records)): ?>
<div style="text-align:center;color:#6B7280;padding:15px;">لا توجد سجلات</div>
<?php else: ?>
<?php foreach ($records as $idx => $record): ?>
<details style="margin-bottom:10px;border:1px solid #E5E7EB;border-radius:6px;" <?= $idx === 0 ? 'open' : '' ?>>
<summary style="padding:10px 15px;cursor:pointer;background:#F9FAFB;border-radius:6px;font-weight:600;font-size:13px;">
سجل #<?= $record['id'] ?? ($idx + 1) ?>
<?php if (isset($record['full_name_ar'])): ?>
<?= e($record['full_name_ar']) ?>
<?php endif; ?>
</summary>
<div style="padding:10px 15px;">
<table style="width:100%;font-size:12px;">
<?php foreach ($record as $rk => $rv): ?>
<tr>
<td style="padding:3px 0;color:#6B7280;width:35%;"><?= e($rk) ?></td>
<td style="padding:3px 0;">
<?php if ($rv === null): ?>
<span style="color:#D1D5DB;">NULL</span>
<?php elseif (is_array($rv)): ?>
<code style="font-size:10px;"><?= e(json_encode($rv, JSON_UNESCAPED_UNICODE)) ?></code>
<?php else: ?>
<?= e((string) $rv) ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
</details>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<!-- Sidebar: Other Snapshots + Compare -->
<div>
<div class="card" style="margin-bottom:20px;">
<div style="padding:12px 15px;border-bottom:1px solid #E5E7EB;">
<h4 style="margin:0;color:#1A1A2E;font-size:14px;">لقطات أخرى لنفس الكيان</h4>
</div>
<div style="padding:10px 15px;">
<?php if (empty($otherSnapshots) || count($otherSnapshots) <= 1): ?>
<div style="color:#6B7280;font-size:13px;text-align:center;padding:10px;">لا توجد لقطات أخرى</div>
<?php else: ?>
<?php foreach ($otherSnapshots as $os): ?>
<div style="padding:8px 0;border-bottom:1px solid #F3F4F6;font-size:13px;<?= (int) $os['id'] === (int) $snapshot['id'] ? 'background:#EFF6FF;margin:0 -15px;padding:8px 15px;' : '' ?>">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div>
<strong>#<?= (int) $os['id'] ?></strong>
<span style="color:#6B7280;font-size:11px;display:block;"><?= e($os['snapshot_reason']) ?></span>
</div>
<div style="text-align:left;">
<?php if ((int) $os['id'] !== (int) $snapshot['id']): ?>
<a href="/archive/<?= (int) $os['id'] ?>" style="color:#0D7377;font-size:12px;">عرض</a>
<br>
<a href="/archive/compare/<?= (int) $snapshot['id'] ?>/<?= (int) $os['id'] ?>" style="color:#D97706;font-size:11px;">مقارنة</a>
<?php else: ?>
<span style="color:#059669;font-size:11px;font-weight:600;">الحالي</span>
<?php endif; ?>
</div>
</div>
<div style="font-size:11px;color:#9CA3AF;margin-top:2px;"><?= e($os['snapshot_taken_at']) ?></div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<!-- Audit link -->
<div class="card">
<div style="padding:12px 15px;">
<a href="/audit/entity/<?= urlencode($snapshot['entity_type']) ?>/<?= (int) $snapshot['entity_id'] ?>" class="btn btn-outline" style="width:100%;text-align:center;">
عرض سجل المراجعة للكيان
</a>
</div>
</div>
</div>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
// Add archive as a child under the existing branches_settings menu
// We check if the menu already exists and add our child to it
$existing = MenuRegistry::get('branches_settings');
if ($existing) {
$children = $existing['children'] ?? [];
$children[] = ['label_ar' => 'الأرشيف', 'label_en' => 'Archive', 'route' => '/archive', 'permission' => 'report.view_audit', 'order' => 4];
$existing['children'] = $children;
MenuRegistry::register('branches_settings', $existing);
}
PermissionRegistry::register('archive', [
'archive.view' => ['ar' => 'عرض الأرشيف', 'en' => 'View Archive'],
'archive.take_snapshot' => ['ar' => 'أخذ لقطة أرشيفية', 'en' => 'Take Archive Snapshot'],
'archive.compare' => ['ar' => 'مقارنة اللقطات', 'en' => 'Compare Snapshots'],
'archive.number_chain' => ['ar' => 'سلسلة أرقام العضوية', 'en' => 'Number Chain'],
]);
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `archive_snapshots` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`entity_type` VARCHAR(100) NOT NULL,
`entity_id` BIGINT UNSIGNED NOT NULL,
`membership_number` VARCHAR(20) NULL,
`snapshot_reason` VARCHAR(50) NOT NULL,
`full_data_json` JSON NOT NULL,
`related_data_json` JSON NULL,
`snapshot_taken_by` BIGINT UNSIGNED NULL,
`snapshot_taken_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`notes` TEXT NULL,
INDEX `idx_archive_entity` (`entity_type`, `entity_id`),
INDEX `idx_archive_membership` (`membership_number`),
INDEX `idx_archive_date` (`snapshot_taken_at`),
INDEX `idx_archive_reason` (`snapshot_reason`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `archive_snapshots`",
];
\ No newline at end of file
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `membership_number_chain` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`membership_number` VARCHAR(20) NOT NULL,
`holder_type` VARCHAR(50) NOT NULL,
`holder_entity_type` VARCHAR(100) NOT NULL,
`holder_entity_id` BIGINT UNSIGNED NOT NULL,
`previous_holder_id` BIGINT UNSIGNED NULL,
`held_from` DATETIME NOT NULL,
`held_until` DATETIME NULL DEFAULT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_number_chain_number` (`membership_number`),
INDEX `idx_number_chain_holder` (`holder_entity_type`, `holder_entity_id`),
CONSTRAINT `fk_number_chain_prev` FOREIGN KEY (`previous_holder_id`) REFERENCES `membership_number_chain`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `membership_number_chain`",
];
\ 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