Commit 71387a23 authored by Mahmoud Aglan's avatar Mahmoud Aglan

ASfsdg

parent 79079d7a
......@@ -70,6 +70,11 @@ final class CarnetPrintService
}
}
// Check profile image uploaded
if (empty($member['photo_path'])) {
$reasons[] = 'يجب رفع صورة شخصية قبل إصدار الكارنيه';
}
return $reasons;
}
......
......@@ -87,9 +87,13 @@ EventBus::listen('payment_request.completed', function (array $data) {
$memberId, $paymentId, $paymentType, $requestData
);
if ($result['success'] && empty($result['already_active'])) {
if ($result['success'] && empty($result['already_active']) && empty($result['already_pending_cheques'])) {
$membershipNumber = $result['membership_number'] ?? null;
EventBus::dispatch('member.activated', ['member_id' => $memberId, 'membership_number' => $membershipNumber]);
if ($paymentType === 'down_payment') {
EventBus::dispatch('member.pending_cheques', ['member_id' => $memberId]);
} else {
EventBus::dispatch('member.activated', ['member_id' => $memberId, 'membership_number' => $membershipNumber]);
}
}
}
......
......@@ -233,28 +233,30 @@ function addCertRow() {
function parseNid(val) {
if (val.length !== 14) { status.style.display = 'none'; dob.readOnly = false; dob.style.background = ''; genderSelect.disabled = false; return; }
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
body: JSON.stringify({national_id: val})
})
var formData = new FormData();
formData.append('national_id', val);
var csrfToken = document.querySelector('input[name="_csrf_token"]');
if (csrfToken) formData.append('_csrf_token', csrfToken.value);
fetch('/api/members/parse-nid', {method: 'POST', body: formData})
.then(function(r){ return r.json(); })
.then(function(d){
if (d.success && d.data) {
dob.value = d.data.dob || '';
var p = d.parsed;
if (p && p.is_valid) {
dob.value = p.dob || '';
dob.readOnly = true;
dob.style.background = '#F9FAFB';
var g = d.data.gender || '';
var g = p.gender || '';
genderSelect.value = g;
genderSelect.disabled = true;
genderHidden.value = g;
status.style.display = 'block';
status.style.color = '#059669';
status.textContent = (d.data.governorate || '') + ' — ' + (d.data.age || '') + ' سنة';
status.textContent = (p.governorate_name_ar || '') + ' — ' + (p.age_years || '') + ' سنة';
} else {
status.style.display = 'block';
status.style.color = '#DC2626';
status.textContent = d.message || 'رقم قومي غير صحيح';
status.textContent = (p && p.errors) ? p.errors[0] : 'رقم قومي غير صحيح';
}
}).catch(function(){});
}
......
......@@ -53,7 +53,7 @@ class FineController extends Controller
if ($penaltyType === 'fine') {
$minData = RuleEngine::get('VIOLATION_FINE_MIN');
$maxData = RuleEngine::get('VIOLATION_FINE_MAX');
$min = $minData['amount'] ?? '1000.00';
$min = $minData['amount'] ?? '200.00';
$max = $maxData['amount'] ?? '10000.00';
if (bccomp($amount, $min, 2) < 0 || bccomp($amount, $max, 2) > 0) {
$errors[] = "مبلغ الغرامة يجب أن يكون بين {$min} و {$max} ج.م";
......
......@@ -228,6 +228,7 @@ class MemberController extends Controller
'formFee' => MemberNumberGenerator::getFormFee(),
'formFilled' => $formFilled,
'specialDiscount' => $specialDiscount,
'availableDiscounts' => SpecialDiscount::allActive(),
'pendingFormFee' => $pendingFormFee,
'pendingMembership' => $pendingMembership,
'pendingAdditions' => $pendingAdditions,
......@@ -267,6 +268,51 @@ class MemberController extends Controller
return $this->redirect('/members/' . $id)->withSuccess('تم إرسال طلب الدفع للخزينة — رقم الطلب: ' . $result['request_number']);
}
public function applyDiscount(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$member = Member::find((int) $id);
if (!$member) return $this->redirect('/members')->withError('العضو غير موجود');
$discountId = (int) $request->post('special_discount_id', 0);
if ($discountId <= 0) {
$db->update('members', [
'special_discount_id' => null,
'discount_amount' => null,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
return $this->redirect("/members/{$id}")->withSuccess('تم إزالة الخصم');
}
$discount = $db->selectOne("SELECT * FROM special_discounts WHERE id = ? AND is_active = 1", [$discountId]);
if (!$discount) {
return $this->redirect("/members/{$id}")->withError('الخصم غير موجود');
}
$membershipValue = $member->membership_value ?? '0.00';
$discountAmount = bcdiv(bcmul($membershipValue, $discount['discount_percentage'], 4), '100', 2);
$updateData = [
'special_discount_id' => $discountId,
'discount_amount' => $discountAmount,
'updated_at' => date('Y-m-d H:i:s'),
];
if ($discount['requires_document'] && !empty($_FILES['discount_document']['tmp_name'])) {
$file = $_FILES['discount_document'];
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$storedName = 'disc_' . $id . '_' . date('Ymd_His') . '.' . $ext;
$destDir = __DIR__ . '/../../../../storage/uploads/discounts';
if (!is_dir($destDir)) mkdir($destDir, 0755, true);
move_uploaded_file($file['tmp_name'], $destDir . '/' . $storedName);
$updateData['special_discount_document'] = 'storage/uploads/discounts/' . $storedName;
}
$db->update('members', $updateData, '`id` = ?', [(int) $id]);
return $this->redirect("/members/{$id}")->withSuccess('تم تطبيق الخصم: ' . $discount['name_ar'] . ' (' . $discount['discount_percentage'] . '%)');
}
public function payMembership(Request $request, string $id): Response
{
$db = App::getInstance()->db();
......
......@@ -12,6 +12,7 @@ return [
['POST', '/members/{id}/status', 'Members\Controllers\MemberController@changeStatus', ['auth', 'csrf'], 'member.change_status'],
['POST', '/members/{id}/pay-form-fee', 'Members\Controllers\MemberController@payFormFee', ['auth', 'csrf'], 'member.pay_form_fee'],
['POST', '/members/{id}/pay-membership', 'Members\Controllers\MemberController@payMembership',['auth', 'csrf'], 'member.pay_membership'],
['POST', '/members/{id}/apply-discount','Members\Controllers\MemberController@applyDiscount', ['auth', 'csrf'], 'member.edit'],
['POST', '/members/{id}/pay-addition', 'Members\Controllers\MemberController@payAdditionFee', ['auth', 'csrf'], 'member.pay_membership'],
['GET', '/members/{id}/fill-form', 'Members\Controllers\MemberController@fillForm', ['auth'], 'member.fill_form'],
['POST', '/members/{id}/fill-form', 'Members\Controllers\MemberController@saveFillForm', ['auth', 'csrf'], 'member.fill_form'],
......
......@@ -236,6 +236,40 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false));
<a href="/members/<?= (int) $member->id ?>/temporary/create" class="btn btn-sm btn-outline">👤 عضو مؤقت</a>
</div>
<!-- Special Discount Section -->
<?php if (!empty($availableDiscounts) && in_array($member->status, ['accepted', 'payment_pending']) && empty($pendingMembership)): ?>
<div style="padding:15px 20px;border-top:1px solid #E5E7EB;">
<?php if (!empty($specialDiscount)): ?>
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<span style="color:#059669;font-weight:600;font-size:13px;">&#x2705; خصم مُطبق: <?= e($specialDiscount['name_ar']) ?> (<?= e($specialDiscount['discount_percentage']) ?>%) = -<?= money($member->discount_amount ?? '0') ?></span>
<form method="POST" action="/members/<?= (int) $member->id ?>/apply-discount" style="display:inline;">
<?= csrf_field() ?>
<input type="hidden" name="special_discount_id" value="0">
<button type="submit" class="btn btn-sm btn-outline" style="color:#DC2626;border-color:#DC2626;" onclick="return confirm('إزالة الخصم؟')">&#x274c; إزالة</button>
</form>
</div>
<?php else: ?>
<form method="POST" action="/members/<?= (int) $member->id ?>/apply-discount" enctype="multipart/form-data" style="display:flex;align-items:end;gap:10px;flex-wrap:wrap;">
<?= csrf_field() ?>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">&#x1f3f7;&#xfe0f; خصم خاص</label>
<select name="special_discount_id" class="form-select" style="min-width:200px;" id="discountSelect" onchange="document.getElementById('discountDoc').style.display = this.selectedOptions[0].dataset.doc === '1' ? 'block' : 'none'">
<option value="">-- اختر خصم --</option>
<?php foreach ($availableDiscounts as $disc): ?>
<option value="<?= (int) $disc['id'] ?>" data-doc="<?= $disc['requires_document'] ? '1' : '0' ?>"><?= e($disc['name_ar']) ?> (<?= e($disc['discount_percentage']) ?>%)</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin:0;display:none;" id="discountDoc">
<label class="form-label" style="font-size:12px;">مستند الإثبات</label>
<input type="file" name="discount_document" class="form-input" accept="image/*,.pdf">
</div>
<button type="submit" class="btn btn-sm btn-primary">تطبيق الخصم</button>
</form>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Payment Section -->
<?php if (bccomp($bill['total_pending'], '0', 2) > 0 && in_array($member->status, ['accepted', 'payment_pending'])): ?>
<?php if (!empty($pendingMembership)): ?>
......
......@@ -23,19 +23,54 @@ final class PaymentLifecycleService
*/
public static function onMembershipPaymentCompleted(int $memberId, int $paymentId, string $paymentType, array $requestData = []): array
{
if ($paymentType === 'down_payment') {
$result = self::transitionToPendingCheques($memberId, $paymentId);
if ($result['success']) {
self::createInstallmentPlan($memberId, $paymentId, $requestData);
}
return $result;
}
$result = MembershipPaymentGuard::activateMember($memberId, $paymentId);
// Only auto-activate dependents for full cash payment.
// Installment members must pay addition_fee separately for each dependent.
if ($result['success'] && $paymentType !== 'down_payment') {
if ($result['success']) {
MembershipPaymentGuard::activateIncludedDependents($memberId, $paymentId);
}
if ($result['success'] && $paymentType === 'down_payment') {
self::createInstallmentPlan($memberId, $paymentId, $requestData);
return $result;
}
/**
* Transition member to pending_cheques after installment down-payment.
* Member stays in this state until all cheques are submitted via ChequeService.
*/
private static function transitionToPendingCheques(int $memberId, int $paymentId): array
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$member) {
return ['success' => false, 'error' => 'العضو غير موجود'];
}
$validStatuses = ['potential', 'payment_pending', 'accepted', 'under_review'];
if (!in_array($member['status'], $validStatuses, true)) {
if ($member['status'] === 'pending_cheques') {
return ['success' => true, 'already_pending_cheques' => true];
}
if ($member['status'] === 'active') {
return ['success' => true, 'already_active' => true];
}
return ['success' => false, 'error' => 'حالة العضو لا تسمح بالانتقال: ' . $member['status']];
}
return $result;
$db->update('members', [
'status' => 'pending_cheques',
'activated_by_payment_id'=> $paymentId,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$memberId]);
Logger::info("PaymentLifecycleService: member #{$memberId} transitioned to pending_cheques after down-payment #{$paymentId}");
return ['success' => true];
}
/**
......
......@@ -219,28 +219,30 @@ document.addEventListener('DOMContentLoaded', function() {
function parseNid(val) {
if (val.length !== 14) { nidStatus.style.display = 'none'; dob.readOnly = false; dob.style.background = ''; genderSelect.disabled = false; return; }
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
body: JSON.stringify({national_id: val})
})
var formData = new FormData();
formData.append('national_id', val);
var csrfToken = document.querySelector('input[name="_csrf_token"]');
if (csrfToken) formData.append('_csrf_token', csrfToken.value);
fetch('/api/members/parse-nid', {method: 'POST', body: formData})
.then(function(r){ return r.json(); })
.then(function(d){
if (d.success && d.data) {
dob.value = d.data.dob || '';
var p = d.parsed;
if (p && p.is_valid) {
dob.value = p.dob || '';
dob.readOnly = true;
dob.style.background = '#F9FAFB';
var g = d.data.gender || '';
var g = p.gender || '';
genderSelect.value = g;
genderSelect.disabled = true;
genderHidden.value = g;
nidStatus.style.display = 'block';
nidStatus.style.color = '#059669';
nidStatus.textContent = (d.data.governorate || '') + ' — ' + (d.data.age || '') + ' سنة';
nidStatus.textContent = (p.governorate_name_ar || '') + ' — ' + (p.age_years || '') + ' سنة';
} else {
nidStatus.style.display = 'block';
nidStatus.style.color = '#DC2626';
nidStatus.textContent = d.message || 'رقم قومي غير صحيح';
nidStatus.textContent = (p && p.errors) ? p.errors[0] : 'رقم قومي غير صحيح';
}
}).catch(function(){});
}
......
......@@ -251,28 +251,30 @@ document.addEventListener('DOMContentLoaded', function() {
function parseNid(val) {
if (val.length !== 14) { nidStatus.style.display = 'none'; dob.readOnly = false; dob.style.background = ''; genderSelect.disabled = false; return; }
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
body: JSON.stringify({national_id: val})
})
var formData = new FormData();
formData.append('national_id', val);
var csrfToken = document.querySelector('input[name="_csrf_token"]');
if (csrfToken) formData.append('_csrf_token', csrfToken.value);
fetch('/api/members/parse-nid', {method: 'POST', body: formData})
.then(function(r){ return r.json(); })
.then(function(d){
if (d.success && d.data) {
dob.value = d.data.dob || '';
var p = d.parsed;
if (p && p.is_valid) {
dob.value = p.dob || '';
dob.readOnly = true;
dob.style.background = '#F9FAFB';
var g = d.data.gender || '';
var g = p.gender || '';
genderSelect.value = g;
genderSelect.disabled = true;
genderHidden.value = g;
nidStatus.style.display = 'block';
nidStatus.style.color = '#059669';
nidStatus.textContent = (d.data.governorate || '') + ' — ' + (d.data.age || '') + ' سنة';
nidStatus.textContent = (p.governorate_name_ar || '') + ' — ' + (p.age_years || '') + ' سنة';
} else {
nidStatus.style.display = 'block';
nidStatus.style.color = '#DC2626';
nidStatus.textContent = d.message || 'رقم قومي غير صحيح';
nidStatus.textContent = (p && p.errors) ? p.errors[0] : 'رقم قومي غير صحيح';
}
}).catch(function(){});
}
......
......@@ -140,28 +140,30 @@ document.addEventListener('DOMContentLoaded', function() {
function parseNid(val) {
if (val.length !== 14) { status.style.display = 'none'; dob.readOnly = false; dob.style.background = ''; genderSelect.disabled = false; return; }
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
body: JSON.stringify({national_id: val})
})
var formData = new FormData();
formData.append('national_id', val);
var csrfToken = document.querySelector('input[name="_csrf_token"]');
if (csrfToken) formData.append('_csrf_token', csrfToken.value);
fetch('/api/members/parse-nid', {method: 'POST', body: formData})
.then(function(r){ return r.json(); })
.then(function(d){
if (d.success && d.data) {
dob.value = d.data.dob || '';
var p = d.parsed;
if (p && p.is_valid) {
dob.value = p.dob || '';
dob.readOnly = true;
dob.style.background = '#F9FAFB';
var g = d.data.gender || '';
var g = p.gender || '';
genderSelect.value = g;
genderSelect.disabled = true;
genderHidden.value = g;
status.style.display = 'block';
status.style.color = '#059669';
status.textContent = (d.data.governorate || '') + ' — ' + (d.data.age || '') + ' سنة';
status.textContent = (p.governorate_name_ar || '') + ' — ' + (p.age_years || '') + ' سنة';
} else {
status.style.display = 'block';
status.style.color = '#DC2626';
status.textContent = d.message || 'رقم قومي غير صحيح';
status.textContent = (p && p.errors) ? p.errors[0] : 'رقم قومي غير صحيح';
}
}).catch(function(){});
}
......
......@@ -88,28 +88,30 @@
function parseNid(val) {
if (val.length !== 14) { status.style.display = 'none'; dob.readOnly = false; dob.style.background = ''; genderSelect.disabled = false; return; }
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
body: JSON.stringify({national_id: val})
})
var formData = new FormData();
formData.append('national_id', val);
var csrfToken = document.querySelector('input[name="_csrf_token"]');
if (csrfToken) formData.append('_csrf_token', csrfToken.value);
fetch('/api/members/parse-nid', {method: 'POST', body: formData})
.then(function(r){ return r.json(); })
.then(function(d){
if (d.success && d.data) {
dob.value = d.data.dob || '';
var p = d.parsed;
if (p && p.is_valid) {
dob.value = p.dob || '';
dob.readOnly = true;
dob.style.background = '#F9FAFB';
var g = d.data.gender || '';
var g = p.gender || '';
genderSelect.value = g;
genderSelect.disabled = true;
genderHidden.value = g;
status.style.display = 'block';
status.style.color = '#059669';
status.textContent = (d.data.governorate || '') + ' — ' + (d.data.age || '') + ' سنة';
status.textContent = (p.governorate_name_ar || '') + ' — ' + (p.age_years || '') + ' سنة';
} else {
status.style.display = 'block';
status.style.color = '#DC2626';
status.textContent = d.message || 'رقم قومي غير صحيح';
status.textContent = (p && p.errors) ? p.errors[0] : 'رقم قومي غير صحيح';
}
}).catch(function(){});
}
......
......@@ -248,8 +248,8 @@ class TransferController extends Controller
$db = App::getInstance()->db();
$transfer = $db->selectOne("SELECT * FROM transfer_requests WHERE id = ?", [(int) $id]);
if (!$transfer) return $this->redirect('/transfers')->withError('الطلب غير موجود');
if (in_array($transfer['status'], ['completed', 'fee_paid', 'rejected'])) {
return $this->redirect("/transfers/{$id}")->withError('لا يمكن الدفع لهذا الطلب');
if ($transfer['status'] !== 'approved') {
return $this->redirect("/transfers/{$id}")->withError('يجب اعتماد الطلب أولاً قبل الدفع');
}
$amount = $transfer['total_fee'] ?? '0.00';
......
......@@ -96,6 +96,15 @@ final class TransferProcessor
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $sourceMember['id']]);
// Verify number is actually released (no other member holds it)
$numberConflict = $db->selectOne(
"SELECT id FROM members WHERE membership_number = ? AND id != ?",
[$sameNumber, (int) $sourceMember['id']]
);
if ($numberConflict) {
$sameNumber = MemberNumberGenerator::generateNext();
}
// 4. Create new member record with SAME membership number
$newMemberData = [
'full_name_ar' => $recipientData['full_name_ar'] ?? $subjectData['full_name_ar'] ?? $subjectName,
......
......@@ -264,28 +264,30 @@
function parseRecipientNid(val) {
if (val.length !== 14) { rStatus.style.display = 'none'; rDob.readOnly = false; rDob.style.background = ''; rGenderSelect.disabled = false; return; }
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest'},
body: JSON.stringify({national_id: val})
})
var formData = new FormData();
formData.append('national_id', val);
var csrfToken = document.querySelector('input[name="_csrf_token"]');
if (csrfToken) formData.append('_csrf_token', csrfToken.value);
fetch('/api/members/parse-nid', {method: 'POST', body: formData})
.then(function(r){ return r.json(); })
.then(function(d){
if (d.success && d.data) {
rDob.value = d.data.dob || '';
var p = d.parsed;
if (p && p.is_valid) {
rDob.value = p.dob || '';
rDob.readOnly = true;
rDob.style.background = '#F9FAFB';
var g = d.data.gender || '';
var g = p.gender || '';
rGenderSelect.value = g;
rGenderSelect.disabled = true;
rGenderHidden.value = g;
rStatus.style.display = 'block';
rStatus.style.color = '#059669';
rStatus.textContent = (d.data.governorate || '') + ' — ' + (d.data.age || '') + ' سنة';
rStatus.textContent = (p.governorate_name_ar || '') + ' — ' + (p.age_years || '') + ' سنة';
} else {
rStatus.style.display = 'block';
rStatus.style.color = '#DC2626';
rStatus.textContent = d.message || 'رقم قومي غير صحيح';
rStatus.textContent = (p && p.errors) ? p.errors[0] : 'رقم قومي غير صحيح';
}
}).catch(function(){});
}
......
......@@ -99,7 +99,25 @@ if ($transfer['transfer_type'] === 'full_transfer' && !empty($transfer['notes'])
</div>
</div>
<?php if (in_array($transfer['status'], ['requested', 'approved']) && bccomp($transfer['total_fee'] ?? '0', '0', 2) > 0): ?>
<?php if ($transfer['status'] === 'requested'): ?>
<?php if (can('transfer.approve')): ?>
<div class="card" style="padding:20px;margin-bottom:20px;background:#EFF6FF;border:2px solid #3B82F6;">
<h4 style="margin:0 0 15px;color:#1D4ED8;">اعتماد الطلب</h4>
<div style="display:flex;gap:10px;">
<form method="POST" action="/transfers/<?= (int) $transfer['id'] ?>/approve">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('اعتماد طلب التحويل؟')">✅ اعتماد</button>
</form>
<form method="POST" action="/transfers/<?= (int) $transfer['id'] ?>/reject">
<?= csrf_field() ?>
<button type="submit" class="btn btn-danger" onclick="return confirm('رفض طلب التحويل؟')">❌ رفض</button>
</form>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if ($transfer['status'] === 'approved' && bccomp($transfer['total_fee'] ?? '0', '0', 2) > 0): ?>
<?php if (can('transfer.approve')): ?>
<div class="card" style="padding:20px;margin-bottom:20px;background:#FFF7ED;border:2px solid #F59E0B;">
<h4 style="margin:0 0 15px;color:#D97706;">💰 دفع رسوم التحويل/الفصل</h4>
......@@ -116,7 +134,7 @@ if ($transfer['transfer_type'] === 'full_transfer' && !empty($transfer['notes'])
<?php endif; ?>
<?php endif; ?>
<?php if (in_array($transfer['status'], ['requested', 'approved', 'fee_paid'])): ?>
<?php if (in_array($transfer['status'], ['approved', 'fee_paid'])): ?>
<?php if (can('transfer.approve')): ?>
<form method="POST" action="/transfers/<?= (int) $transfer['id'] ?>/complete" style="margin-top:15px;">
<?= csrf_field() ?>
......
......@@ -15,17 +15,13 @@ class SessionController extends Controller
public function index(Request $request): Response
{
$this->authorize('treasury.view_sessions');
$treasury = TreasuryService::getSubTreasury();
if (!$treasury) {
return $this->redirect('/treasury')->withError('لم يتم تعريف الخزنة الفرعية');
}
$employee = App::getInstance()->currentEmployee();
$sessions = SessionService::getSessionHistory((int) $treasury['id']);
$sessions = SessionService::getAllSessionHistory((int) $employee->id);
return $this->view('Treasury.Views.sessions.index', [
'sessions' => $sessions,
'treasury' => $treasury,
'treasury' => TreasuryService::getSubTreasury(),
]);
}
......@@ -33,18 +29,15 @@ class SessionController extends Controller
{
$this->authorize('treasury.open_session');
$employee = App::getInstance()->currentEmployee();
$treasury = TreasuryService::getSubTreasury();
if (!$treasury) {
return $this->redirect('/treasury')->withError('لم يتم تعريف الخزنة الفرعية');
}
$session = SessionService::getCurrentSession((int) $treasury['id'], (int) $employee->id);
$session = SessionService::getAnyCurrentSession((int) $employee->id);
if (!$session) {
return $this->redirect('/treasury')->withWarning('لا توجد وردية مفتوحة حالياً');
}
$treasury = TreasuryService::find((int) $session['treasury_id']);
$db = App::getInstance()->db();
$payments = $db->select(
"SELECT p.*, m.full_name_ar as member_name, r.receipt_number
......
......@@ -118,6 +118,20 @@ final class SessionService
return $db->selectOne($sql, $params);
}
public static function getAnyCurrentSession(int $cashierId): ?array
{
$db = App::getInstance()->db();
return $db->selectOne(
"SELECT ts.*, e.full_name_ar as cashier_name, t.name_ar as treasury_name
FROM treasury_sessions ts
LEFT JOIN employees e ON e.id = ts.cashier_id
LEFT JOIN treasuries t ON t.id = ts.treasury_id
WHERE ts.cashier_id = ? AND ts.status = 'open'
LIMIT 1",
[$cashierId]
);
}
public static function getSessionHistory(int $treasuryId, int $limit = 50): array
{
$db = App::getInstance()->db();
......@@ -132,6 +146,21 @@ final class SessionService
);
}
public static function getAllSessionHistory(int $cashierId, int $limit = 50): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT ts.*, e.full_name_ar as cashier_name, t.name_ar as treasury_name
FROM treasury_sessions ts
LEFT JOIN employees e ON e.id = ts.cashier_id
LEFT JOIN treasuries t ON t.id = ts.treasury_id
WHERE ts.cashier_id = ?
ORDER BY ts.id DESC
LIMIT ?",
[$cashierId, $limit]
);
}
public static function find(int $sessionId): ?array
{
$db = App::getInstance()->db();
......
<?php
declare(strict_types=1);
return [
'up' => "UPDATE business_rules SET current_value_json = '{\"amount\":\"200.00\"}', updated_at = NOW() WHERE rule_code = 'VIOLATION_FINE_MIN'",
'down' => "UPDATE business_rules SET current_value_json = '{\"amount\":\"1000.00\"}', updated_at = NOW() WHERE rule_code = 'VIOLATION_FINE_MIN'",
];
......@@ -74,7 +74,7 @@ return function (Database $db): void {
['rule_code' => 'LATE_SUB_FINE_YEAR_3', 'category' => 'penalty', 'name_ar' => 'غرامة تأخير سنة ثالثة', 'name_en' => 'Late Fine Year 3', 'data_type' => 'percentage', 'current_value_json' => '{"percentage_of_subscription":"300.00"}', 'parameters_json' => '{"percentage_of_subscription":"decimal"}'],
['rule_code' => 'LATE_SUB_FINE_MAX_YEARS', 'category' => 'penalty', 'name_ar' => 'أقصى سنوات تراكم غرامات', 'name_en' => 'Max Fine Years', 'data_type' => 'integer', 'current_value_json' => '{"years":5}', 'parameters_json' => '{"years":"integer"}'],
['rule_code' => 'LATE_SUB_DROP_YEARS', 'category' => 'penalty', 'name_ar' => 'سنوات إسقاط العضوية', 'name_en' => 'Drop Years', 'data_type' => 'integer', 'current_value_json' => '{"years":5}', 'parameters_json' => '{"years":"integer"}'],
['rule_code' => 'VIOLATION_FINE_MIN', 'category' => 'penalty', 'name_ar' => 'الحد الأدنى للغرامة', 'name_en' => 'Min Violation Fine', 'data_type' => 'amount', 'current_value_json' => '{"amount":"1000.00"}', 'parameters_json' => '{"amount":"decimal"}'],
['rule_code' => 'VIOLATION_FINE_MIN', 'category' => 'penalty', 'name_ar' => 'الحد الأدنى للغرامة', 'name_en' => 'Min Violation Fine', 'data_type' => 'amount', 'current_value_json' => '{"amount":"200.00"}', 'parameters_json' => '{"amount":"decimal"}'],
['rule_code' => 'VIOLATION_FINE_MAX', 'category' => 'penalty', 'name_ar' => 'الحد الأقصى للغرامة', 'name_en' => 'Max Violation Fine', 'data_type' => 'amount', 'current_value_json' => '{"amount":"10000.00"}', 'parameters_json' => '{"amount":"decimal"}'],
['rule_code' => 'APPEAL_WINDOW', 'category' => 'penalty', 'name_ar' => 'مهلة التظلم', 'name_en' => 'Appeal Window', 'data_type' => 'integer', 'current_value_json' => '{"days":15}', 'parameters_json' => '{"days":"integer"}'],
['rule_code' => 'REVIEW_AFTER_EXPULSION', 'category' => 'penalty', 'name_ar' => 'إعادة النظر بعد الفصل', 'name_en' => 'Review After Expulsion', 'data_type' => 'integer', 'current_value_json' => '{"months":6}', 'parameters_json' => '{"months":"integer"}'],
......
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