Commit 619c24d2 authored by Fares's avatar Fares

feat(waiver): transfer all data to target member, preserve full archive

WaiverProcessor now transfers ALL active data (dependents, payments,
subscriptions, installments, fines, documents) from source to target
member during waiver execution. Snapshot is taken first so archive
retains the complete historical state.

Changes:
- WaiverProcessor::transferActiveData() moves spouses, children, temps,
  payments, subscriptions, installment_plans, fines, documents, and
  payment_requests from source to target member
- ArchiveService snapshot now includes installment_cheques and
  installment_schedule for complete financial history
- Archive show page displays cheques per installment plan
- Member show page adds "عرض العضوية المؤرشفة" button for waiver transfers
- Archive page adds "الانتقال إلى العضوية الحالية" link to current holder
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 19806b6c
...@@ -137,6 +137,28 @@ final class ArchiveService ...@@ -137,6 +137,28 @@ final class ArchiveService
[$entityId] [$entityId]
); );
} }
// Load installment cheques (linked via installment_plans)
if ($db->tableExists('installment_cheques') && !empty($related['installment_plans'])) {
$planIds = array_column($related['installment_plans'], 'id');
if (!empty($planIds)) {
$placeholders = implode(',', array_fill(0, count($planIds), '?'));
$related['installment_cheques'] = $db->select(
"SELECT * FROM installment_cheques WHERE installment_plan_id IN ({$placeholders}) ORDER BY due_date ASC",
$planIds
);
}
}
// Load installment schedule (linked via installment_plans)
if ($db->tableExists('installment_schedule') && !empty($related['installment_plans'])) {
$planIds = array_column($related['installment_plans'], 'id');
if (!empty($planIds)) {
$placeholders = implode(',', array_fill(0, count($planIds), '?'));
$related['installment_schedule'] = $db->select(
"SELECT * FROM installment_schedule WHERE installment_plan_id IN ({$placeholders}) ORDER BY due_date ASC",
$planIds
);
}
}
// Load seasonal if table exists // Load seasonal if table exists
if ($db->tableExists('seasonal_memberships')) { if ($db->tableExists('seasonal_memberships')) {
$related['seasonal_memberships'] = $db->select( $related['seasonal_memberships'] = $db->select(
......
...@@ -178,17 +178,31 @@ class MemberArchiveController extends Controller ...@@ -178,17 +178,31 @@ class MemberArchiveController extends Controller
"SELECT * FROM documents WHERE member_id = ? AND is_archived = 0 ORDER BY created_at DESC", [(int) $id] "SELECT * FROM documents WHERE member_id = ? AND is_archived = 0 ORDER BY created_at DESC", [(int) $id]
); );
// For installment plans from live DB, also get their schedules // For installment plans, get their schedules and cheques
$installmentSchedules = []; $installmentSchedules = [];
$installmentCheques = [];
if (!empty($installmentPlans)) { if (!empty($installmentPlans)) {
// Prefer snapshot data for cheques/schedules
$installmentSchedules = !empty($related['installment_schedule']) ? $related['installment_schedule'] : [];
$installmentCheques = !empty($related['installment_cheques']) ? $related['installment_cheques'] : [];
// Fall back to live DB if snapshot didn't have them
$planIds = array_column($installmentPlans, 'id'); $planIds = array_column($installmentPlans, 'id');
if (!empty($planIds)) { if (!empty($planIds)) {
$placeholders = implode(',', array_fill(0, count($planIds), '?')); $placeholders = implode(',', array_fill(0, count($planIds), '?'));
if (empty($installmentSchedules)) {
$installmentSchedules = $db->select( $installmentSchedules = $db->select(
"SELECT * FROM installment_schedule WHERE installment_plan_id IN ({$placeholders}) ORDER BY due_date ASC", "SELECT * FROM installment_schedule WHERE installment_plan_id IN ({$placeholders}) ORDER BY due_date ASC",
$planIds $planIds
); );
} }
if (empty($installmentCheques)) {
$installmentCheques = $db->select(
"SELECT * FROM installment_cheques WHERE installment_plan_id IN ({$placeholders}) ORDER BY due_date ASC",
$planIds
);
}
}
} }
// Operator info // Operator info
...@@ -209,6 +223,7 @@ class MemberArchiveController extends Controller ...@@ -209,6 +223,7 @@ class MemberArchiveController extends Controller
'subscriptions' => $subscriptions, 'subscriptions' => $subscriptions,
'installmentPlans' => $installmentPlans, 'installmentPlans' => $installmentPlans,
'installmentSchedules' => $installmentSchedules, 'installmentSchedules' => $installmentSchedules,
'installmentCheques' => $installmentCheques,
'fines' => $fines, 'fines' => $fines,
'documents' => $documents, 'documents' => $documents,
'operator' => $operator, 'operator' => $operator,
......
...@@ -18,6 +18,9 @@ $related = $primarySnapshot['related_data'] ?? []; ...@@ -18,6 +18,9 @@ $related = $primarySnapshot['related_data'] ?? [];
<div style="display:flex;align-items:center;gap:12px;"> <div style="display:flex;align-items:center;gap:12px;">
<span style="width:12px;height:12px;border-radius:50%;background:<?= $color ?>;"></span> <span style="width:12px;height:12px;border-radius:50%;background:<?= $color ?>;"></span>
<span style="font-weight:700;font-size:16px;color:<?= $color ?>;">مؤرشف — <?= e($statusLabels[$member['status']] ?? $member['status']) ?></span> <span style="font-weight:700;font-size:16px;color:<?= $color ?>;">مؤرشف — <?= e($statusLabels[$member['status']] ?? $member['status']) ?></span>
<?php if ($linkedMember): ?>
<a href="/members/<?= (int) $linkedMember['id'] ?>" style="background:#059669;color:#fff;padding:4px 12px;border-radius:6px;font-size:12px;font-weight:600;text-decoration:none;">الانتقال إلى العضوية الحالية &#x2192;</a>
<?php endif; ?>
</div> </div>
<div style="text-align:left;font-size:13px;color:#6B7280;"> <div style="text-align:left;font-size:13px;color:#6B7280;">
<div>تاريخ الأرشفة: <strong><?= $member['archived_at'] ? arabic_date($member['archived_at']) : '—' ?></strong></div> <div>تاريخ الأرشفة: <strong><?= $member['archived_at'] ? arabic_date($member['archived_at']) : '—' ?></strong></div>
...@@ -249,6 +252,42 @@ $related = $primarySnapshot['related_data'] ?? []; ...@@ -249,6 +252,42 @@ $related = $primarySnapshot['related_data'] ?? [];
</table> </table>
</details> </details>
<?php endif; ?> <?php endif; ?>
<?php
$planCheques = array_filter($installmentCheques ?? [], fn($c) => (int) $c['installment_plan_id'] === (int) $plan['id']);
if (!empty($planCheques)):
?>
<details style="margin-top:10px;">
<summary style="cursor:pointer;font-size:12px;color:#7C3AED;font-weight:600;">عرض الشيكات (<?= count($planCheques) ?>)</summary>
<table class="data-table" style="font-size:11px;margin-top:8px;width:100%;">
<thead><tr><th>رقم الشيك</th><th>تاريخ الاستحقاق</th><th>المبلغ</th><th>البنك</th><th>الحالة</th></tr></thead>
<tbody>
<?php foreach ($planCheques as $cheque): ?>
<tr>
<td style="font-weight:600;"><?= e($cheque['cheque_number'] ?? '—') ?></td>
<td><?= e($cheque['due_date'] ?? '—') ?></td>
<td><?= money($cheque['amount'] ?? '0') ?></td>
<td><?= e($cheque['bank_name'] ?? '—') ?></td>
<td>
<?php
$chequeStatus = $cheque['status'] ?? 'pending';
$chequeStatusLabel = match($chequeStatus) {
'collected' => 'تم التحصيل', 'deposited' => 'مودع', 'bounced' => 'مرتجع',
'replaced' => 'مستبدل', 'cancelled' => 'ملغى', default => 'معلق',
};
$chequeStatusColor = match($chequeStatus) {
'collected' => '#059669', 'deposited' => '#0284C7', 'bounced' => '#DC2626',
'replaced' => '#F59E0B', 'cancelled' => '#6B7280', default => '#D97706',
};
?>
<span style="color:<?= $chequeStatusColor ?>;font-weight:600;"><?= $chequeStatusLabel ?></span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</details>
<?php endif; ?>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
......
...@@ -119,6 +119,9 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false)); ...@@ -119,6 +119,9 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false));
<?php endif; ?> <?php endif; ?>
<?php if (!empty($waiverTransfer)): ?> <?php if (!empty($waiverTransfer)): ?>
<span style="background:#F3E8FF;padding:2px 8px;border-radius:10px;font-size:11px;">تنازل من: <a href="/waivers/<?= (int) $waiverTransfer['id'] ?>" style="color:#7C3AED;font-weight:700;"><?= e($waiverTransfer['source_member_name'] ?? $member->waived_from_member_name ?? '—') ?></a></span> <span style="background:#F3E8FF;padding:2px 8px;border-radius:10px;font-size:11px;">تنازل من: <a href="/waivers/<?= (int) $waiverTransfer['id'] ?>" style="color:#7C3AED;font-weight:700;"><?= e($waiverTransfer['source_member_name'] ?? $member->waived_from_member_name ?? '—') ?></a></span>
<?php if (!empty($waiverTransfer['source_member_id_ref'])): ?>
<a href="/members/archive/<?= (int) $waiverTransfer['source_member_id_ref'] ?>" style="background:#FEF3C7;padding:2px 8px;border-radius:10px;font-size:11px;color:#92400E;font-weight:600;text-decoration:none;">&#x1f4e6; عرض العضوية المؤرشفة</a>
<?php endif; ?>
<?php endif; ?> <?php endif; ?>
<span>📋 استمارة: <strong style="color:#D97706;"><?= e($member->form_number ?? '—') ?></strong></span> <span>📋 استمارة: <strong style="color:#D97706;"><?= e($member->form_number ?? '—') ?></strong></span>
<span>🏢 <?= e($branchName) ?></span> <span>🏢 <?= e($branchName) ?></span>
......
...@@ -35,6 +35,9 @@ final class WaiverProcessor ...@@ -35,6 +35,9 @@ final class WaiverProcessor
try { try {
$snapshotId = ArchiveService::takeSnapshot('members', (int) $sourceMember['id'], 'waiver', 'تنازل — طلب #' . $waiverId); $snapshotId = ArchiveService::takeSnapshot('members', (int) $sourceMember['id'], 'waiver', 'تنازل — طلب #' . $waiverId);
// Transfer ALL active data from source to target before archiving
self::transferActiveData($db, (int) $sourceMember['id'], (int) $targetMember['id']);
// Find the waiver_fee payment made for this transfer (to set as activated_by_payment_id) // Find the waiver_fee payment made for this transfer (to set as activated_by_payment_id)
$waiverPayment = $db->selectOne( $waiverPayment = $db->selectOne(
"SELECT id FROM payments WHERE member_id = ? AND payment_type = 'waiver_fee' AND related_entity_type = 'waiver_requests' AND related_entity_id = ? AND is_voided = 0 ORDER BY id DESC LIMIT 1", "SELECT id FROM payments WHERE member_id = ? AND payment_type = 'waiver_fee' AND related_entity_type = 'waiver_requests' AND related_entity_id = ? AND is_voided = 0 ORDER BY id DESC LIMIT 1",
...@@ -899,4 +902,57 @@ final class WaiverProcessor ...@@ -899,4 +902,57 @@ final class WaiverProcessor
default => $personName, default => $personName,
}; };
} }
/**
* Transfer all active data from source member to target member.
* Called AFTER snapshot is taken so the archive preserves original state.
*/
private static function transferActiveData($db, int $sourceId, int $targetId): void
{
$ts = date('Y-m-d H:i:s');
// Dependents
$db->query(
"UPDATE spouses SET member_id = ?, updated_at = ? WHERE member_id = ? AND is_archived = 0",
[$targetId, $ts, $sourceId]
);
$db->query(
"UPDATE children SET member_id = ?, updated_at = ? WHERE member_id = ? AND is_archived = 0",
[$targetId, $ts, $sourceId]
);
$db->query(
"UPDATE temporary_members SET member_id = ?, updated_at = ? WHERE member_id = ? AND is_archived = 0",
[$targetId, $ts, $sourceId]
);
// Financial records
$db->query(
"UPDATE payments SET member_id = ? WHERE member_id = ?",
[$targetId, $sourceId]
);
$db->query(
"UPDATE subscriptions SET member_id = ? WHERE member_id = ?",
[$targetId, $sourceId]
);
$db->query(
"UPDATE installment_plans SET member_id = ? WHERE member_id = ?",
[$targetId, $sourceId]
);
$db->query(
"UPDATE fines SET member_id = ? WHERE member_id = ?",
[$targetId, $sourceId]
);
// Documents
$db->query(
"UPDATE documents SET member_id = ? WHERE member_id = ? AND is_archived = 0",
[$targetId, $sourceId]
);
// Payment requests (pending ones move to new owner)
$db->query(
"UPDATE payment_requests SET member_id = ? WHERE member_id = ? AND is_voided = 0",
[$targetId, $sourceId]
);
}
} }
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