Commit 6141de46 authored by Mahmoud Aglan's avatar Mahmoud Aglan

بووششش

parent e137cd61
......@@ -22,6 +22,17 @@ final class ChildFeeCalculator
$membershipValue = $member['membership_value'] ?? '0.00';
$isOnInitialForm = FormFeeService::isOnInitialForm($member);
if (bccomp($membershipValue, '0.01', 2) < 0 && !empty($member['qualification_id']) && !empty($member['branch_id'])) {
$pricing = $db->selectOne(
"SELECT price FROM pricing_configs WHERE branch_id = ? AND qualification_id = ? AND membership_type = 'working' AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE()) ORDER BY effective_from DESC LIMIT 1",
[(int) $member['branch_id'], (int) $member['qualification_id']]
);
if ($pricing && bccomp($pricing['price'], '0.01', 2) >= 0) {
$membershipValue = $pricing['price'];
}
}
if (bccomp($membershipValue, '0.00', 2) <= 0 && !$isOnInitialForm) {
return ['error' => 'قيمة العضوية غير محددة', 'fee' => '0.00', 'classification' => 'included'];
}
......
......@@ -92,6 +92,36 @@ class MemberApiController extends Controller
]);
}
public function searchGet(Request $request): Response
{
$query = trim((string) $request->get('q', ''));
$exclude = (int) $request->get('exclude', 0);
$limit = min(20, max(1, (int) $request->get('limit', 10)));
if ($query === '' || mb_strlen($query) < 2) {
return $this->json([]);
}
$db = App::getInstance()->db();
$like = '%' . $query . '%';
$params = [$like, $like, $like];
$excludeWhere = '';
if ($exclude > 0) {
$excludeWhere = ' AND id != ?';
$params[] = $exclude;
}
$params[] = $limit;
$rows = $db->select(
"SELECT id, full_name_ar, national_id, phone_mobile, membership_number, status
FROM members WHERE is_archived = 0 AND (full_name_ar LIKE ? OR national_id LIKE ? OR membership_number LIKE ?){$excludeWhere}
ORDER BY full_name_ar ASC LIMIT ?",
$params
);
return $this->json($rows);
}
public function search(Request $request): Response
{
$query = trim((string) $request->post('q', ''));
......
......@@ -17,6 +17,7 @@ return [
['POST', '/members/{id}/fill-form', 'Members\Controllers\MemberController@saveFillForm', ['auth', 'csrf'], 'member.fill_form'],
['GET', '/members/{id}/changelog', 'Members\Controllers\MemberController@changelog', ['auth'], 'member.view'],
['POST', '/api/members/parse-nid', 'Members\Controllers\MemberApiController@parseNid', ['auth'], 'member.create'],
['GET', '/api/members/search', 'Members\Controllers\MemberApiController@searchGet', ['auth'], 'member.view'],
['POST', '/api/members/search', 'Members\Controllers\MemberApiController@search', ['auth'], 'member.view'],
// Reports
['GET', '/reports', 'Members\Controllers\ReportController@index', ['auth'], 'member.reports'],
......
......@@ -20,6 +20,17 @@ final class BillingService
if (!$member) return [];
$membershipValue = $member['membership_value'] ?? '0.00';
if (bccomp($membershipValue, '0.01', 2) < 0 && !empty($member['qualification_id']) && !empty($member['branch_id'])) {
$pricing = $db->selectOne(
"SELECT price FROM pricing_configs WHERE branch_id = ? AND qualification_id = ? AND membership_type = 'working' AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE()) ORDER BY effective_from DESC LIMIT 1",
[(int) $member['branch_id'], (int) $member['qualification_id']]
);
if ($pricing && bccomp($pricing['price'], '0.01', 2) >= 0) {
$membershipValue = $pricing['price'];
}
}
$items = [];
// ── 1. Form Fee (505) ──
......
......@@ -35,6 +35,17 @@ final class SpouseFeeCalculator
$membershipValue = $member['membership_value'] ?? '0.00';
$isOnInitialForm = FormFeeService::isOnInitialForm($member);
// Resolve membership value from pricing_configs if not stored yet
if (bccomp($membershipValue, '0.01', 2) < 0 && !empty($member['qualification_id']) && !empty($member['branch_id'])) {
$pricing = $db->selectOne(
"SELECT price FROM pricing_configs WHERE branch_id = ? AND qualification_id = ? AND membership_type = 'working' AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE()) ORDER BY effective_from DESC LIMIT 1",
[(int) $member['branch_id'], (int) $member['qualification_id']]
);
if ($pricing && bccomp($pricing['price'], '0.01', 2) >= 0) {
$membershipValue = $pricing['price'];
}
}
if (bccomp($membershipValue, '0.01', 2) < 0 && !$isOnInitialForm) {
return self::error('يجب تحديد قيمة العضوية أولاً (ملء الاستمارة واختيار المؤهل)');
}
......@@ -45,6 +56,11 @@ final class SpouseFeeCalculator
$isInitialCreation = $isOnInitialForm;
$isFreeSlot = FormFeeService::isSpouseFreeSlot($memberId, $spouseOrder);
// If value is still 0 and this spouse needs fees, block with a clear message
if (bccomp($membershipValue, '0.01', 2) < 0 && !($isFreeSlot && $isOnInitialForm)) {
return self::error('يجب ملء الاستمارة واختيار المؤهل أولاً لتحديد قيمة العضوية');
}
$formFeeOnly = FormFeeService::getFormFeeOnly($memberId, $member);
$annualSubscription = !$isOnInitialForm
? FormFeeService::getAnnualSubscriptionForAddition()
......
......@@ -66,8 +66,11 @@ class WaiverController extends Controller
$childCount = (int) ($db->selectOne("SELECT COUNT(*) as cnt FROM children WHERE member_id = ? AND is_archived = 0", [(int) $memberId])['cnt'] ?? 0);
$tempCount = (int) ($db->selectOne("SELECT COUNT(*) as cnt FROM temporary_members WHERE member_id = ? AND is_archived = 0", [(int) $memberId])['cnt'] ?? 0);
$targetMemberId = (int) $request->post('target_member_id', 0);
$waiver = WaiverRequest::create([
'source_member_id' => (int) $memberId,
'target_member_id' => $targetMemberId > 0 ? $targetMemberId : null,
'membership_number' => $member['membership_number'],
'membership_value_at_waiver' => $membershipValue,
'waiver_fee_percentage' => $waiverPct,
......
......@@ -14,28 +14,106 @@
<tr style="border-top:2px solid #0D7377;"><td style="padding:10px 0;font-weight:700;font-size:16px;">رسوم التنازل</td><td style="padding:10px 0;font-weight:700;font-size:18px;color:#0D7377;"><?= money($waiver_fee) ?></td></tr>
</table>
</div>
<form method="POST" action="/waivers/store/<?= (int) $member['id'] ?>">
<form method="POST" action="/waivers/store/<?= (int) $member['id'] ?>" id="waiver-form">
<?= csrf_field() ?>
<!-- Target Member -->
<div class="card" style="padding:20px;margin-bottom:15px;background:#ECFDF5;border:2px solid #059669;">
<h4 style="margin:0 0 15px;color:#059669;">👤 المستفيد (المتنازل إليه)</h4>
<div style="position:relative;margin-bottom:10px;">
<input type="text" id="target-search" class="form-input" placeholder="اكتب اسم أو رقم قومي المستفيد..." autocomplete="off" style="padding-left:40px;">
<i data-lucide="search" style="position:absolute;left:12px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:#9CA3AF;"></i>
</div>
<div id="target-results" style="display:none;position:absolute;z-index:100;background:#fff;border:1px solid #E5E7EB;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.1);max-height:200px;overflow-y:auto;width:calc(100% - 40px);margin-top:-6px;"></div>
<div id="target-selected" style="display:none;padding:12px 16px;background:#F0FDF4;border:1px solid #BBF7D0;border-radius:8px;">
<div style="display:flex;align-items:center;justify-content:space-between;">
<div>
<span style="font-weight:700;color:#166534;" id="target-name"></span>
<span style="color:#6B7280;font-size:12px;margin-right:10px;" id="target-info"></span>
</div>
<button type="button" onclick="clearTarget()" style="background:none;border:none;color:#DC2626;cursor:pointer;font-size:18px;"></button>
</div>
</div>
<input type="hidden" name="target_member_id" id="target-member-id" value="">
<p style="margin:10px 0 0;font-size:12px;color:#6B7280;">💡 إذا لم يكن المستفيد مسجلاً، <a href="/members/create" target="_blank" style="color:#0D7377;">أنشئ عضو جديد</a> أولاً ثم ابحث عنه هنا. يمكنك أيضاً تحديد المستفيد لاحقاً من شاشة الطلب.</p>
</div>
<div class="card" style="padding:20px;margin-bottom:15px;">
<div class="form-group"><label class="form-label">ملاحظات</label><textarea name="notes" class="form-textarea" rows="3"></textarea></div>
</div>
<!-- Fee & Payment -->
<div class="card" style="padding:20px;margin-bottom:20px;background:#FFF7ED;border:2px solid #F59E0B;">
<h4 style="margin:0 0 15px;color:#D97706;">تحصيل رسوم التنازل</h4>
<h4 style="margin:0 0 15px;color:#D97706;">رسوم التنازل — تُرسل لطابور الخزينة</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">طريقة الدفع <span style="color:#DC2626;">*</span></label>
<select name="payment_method" class="form-select" required>
<option value="cash">نقدي</option>
<option value="visa">فيزا</option>
<option value="bank_transfer">تحويل بنكي</option>
</select>
<label class="form-label">المبلغ</label>
<input type="text" value="<?= money($waiver_fee) ?>" class="form-input" style="background:#F3F4F6;font-weight:700;font-size:16px;" readonly>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary" onclick="return confirm('سيتم تقديم طلب التنازل وتحصيل الرسوم (<?= money($waiver_fee) ?>). متأكد؟')">تقديم الطلب وتحصيل الرسوم</button>
<button type="submit" class="btn btn-primary" onclick="return confirm('سيتم تقديم طلب التنازل وإرسال رسوم <?= money($waiver_fee) ?> لطابور الخزينة. متأكد؟')">تقديم الطلب</button>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
</form>
<script>
(function() {
var searchInput = document.getElementById('target-search');
var resultsDiv = document.getElementById('target-results');
var selectedDiv = document.getElementById('target-selected');
var hiddenInput = document.getElementById('target-member-id');
var timer = null;
searchInput.addEventListener('input', function() {
clearTimeout(timer);
var q = this.value.trim();
if (q.length < 2) { resultsDiv.style.display = 'none'; return; }
timer = setTimeout(function() {
fetch('/api/members/search?q=' + encodeURIComponent(q) + '&exclude=<?= (int) $member['id'] ?>&limit=10')
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.length) {
resultsDiv.innerHTML = '<div style="padding:12px;color:#6B7280;font-size:13px;">لا توجد نتائج</div>';
} else {
resultsDiv.innerHTML = data.map(function(m) {
return '<div class="target-option" data-id="' + m.id + '" data-name="' + (m.full_name_ar || '') + '" data-info="' + (m.national_id || '') + ' — ' + (m.membership_number ? 'عضوية #' + m.membership_number : 'بدون رقم عضوية') + '" style="padding:10px 14px;cursor:pointer;border-bottom:1px solid #F3F4F6;transition:background .1s;">' +
'<div style="font-weight:600;font-size:13px;">' + (m.full_name_ar || '') + '</div>' +
'<div style="font-size:11px;color:#6B7280;">' + (m.national_id || '—') + ' — ' + (m.phone_mobile || '') + (m.membership_number ? ' — عضوية #' + m.membership_number : '') + '</div></div>';
}).join('');
}
resultsDiv.style.display = 'block';
});
}, 300);
});
resultsDiv.addEventListener('click', function(e) {
var opt = e.target.closest('.target-option');
if (!opt) return;
hiddenInput.value = opt.dataset.id;
document.getElementById('target-name').textContent = opt.dataset.name;
document.getElementById('target-info').textContent = opt.dataset.info;
selectedDiv.style.display = 'block';
resultsDiv.style.display = 'none';
searchInput.value = '';
});
document.addEventListener('click', function(e) {
if (!resultsDiv.contains(e.target) && e.target !== searchInput) resultsDiv.style.display = 'none';
});
resultsDiv.addEventListener('mouseover', function(e) {
var opt = e.target.closest('.target-option');
if (opt) opt.style.background = '#F0FDF4';
});
resultsDiv.addEventListener('mouseout', function(e) {
var opt = e.target.closest('.target-option');
if (opt) opt.style.background = '';
});
})();
function clearTarget() {
document.getElementById('target-member-id').value = '';
document.getElementById('target-selected').style.display = 'none';
}
</script>
<?php $__template->endSection(); ?>
\ No newline at end of file
......@@ -50,10 +50,111 @@
<?php endif; ?>
<?php if (in_array($waiver['status'], ['approved', 'fee_paid'])): ?>
<form method="POST" action="/waivers/<?= (int) $waiver['id'] ?>/complete">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('⚠ إتمام التنازل — سيتم نقل العضوية. متأكد؟')">✅ إتمام التنازل</button>
</form>
<div class="card" style="padding:20px;margin-bottom:20px;background:#ECFDF5;border:2px solid #059669;">
<h4 style="margin:0 0 15px;color:#059669;">👤 تحديد المستفيد (المتنازل إليه)</h4>
<form method="POST" action="/waivers/<?= (int) $waiver['id'] ?>/complete" id="complete-form">
<?= csrf_field() ?>
<div style="margin-bottom:15px;">
<label class="form-label">بحث عن المستفيد <span style="color:#DC2626;">*</span></label>
<div style="position:relative;">
<input type="text" id="target-search" class="form-input" placeholder="اكتب اسم أو رقم قومي المستفيد..." autocomplete="off" style="padding-left:40px;">
<i data-lucide="search" style="position:absolute;left:12px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:#9CA3AF;"></i>
</div>
<div id="target-results" style="display:none;position:absolute;z-index:100;background:#fff;border:1px solid #E5E7EB;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.1);max-height:200px;overflow-y:auto;width:100%;margin-top:4px;"></div>
</div>
<div id="target-selected" style="display:none;padding:12px 16px;background:#F0FDF4;border:1px solid #BBF7D0;border-radius:8px;margin-bottom:15px;">
<div style="display:flex;align-items:center;justify-content:space-between;">
<div>
<span style="font-weight:700;color:#166534;" id="target-name"></span>
<span style="color:#6B7280;font-size:12px;margin-right:10px;" id="target-info"></span>
</div>
<button type="button" onclick="clearTarget()" style="background:none;border:none;color:#DC2626;cursor:pointer;font-size:18px;"></button>
</div>
</div>
<input type="hidden" name="target_member_id" id="target-member-id" value="<?= (int) ($waiver['target_member_id'] ?? 0) ?>">
<?php if (!empty($waiver['target_member_id']) && !empty($waiver['target_name'])): ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('target-name').textContent = <?= json_encode($waiver['target_name'], JSON_UNESCAPED_UNICODE) ?>;
document.getElementById('target-info').textContent = 'عضو #<?= (int) $waiver['target_member_id'] ?>';
document.getElementById('target-selected').style.display = 'block';
});
</script>
<?php endif; ?>
<div style="display:flex;gap:10px;align-items:center;">
<button type="submit" class="btn btn-primary" onclick="return validateComplete()">✅ إتمام التنازل ونقل العضوية</button>
<span style="font-size:12px;color:#6B7280;">أو <a href="/members/create?waiver_id=<?= (int) $waiver['id'] ?>" style="color:#0D7377;">أنشئ عضو جديد</a> ثم ارجع هنا</span>
</div>
</form>
</div>
<script>
(function() {
var searchInput = document.getElementById('target-search');
var resultsDiv = document.getElementById('target-results');
var selectedDiv = document.getElementById('target-selected');
var hiddenInput = document.getElementById('target-member-id');
var timer = null;
searchInput.addEventListener('input', function() {
clearTimeout(timer);
var q = this.value.trim();
if (q.length < 2) { resultsDiv.style.display = 'none'; return; }
timer = setTimeout(function() {
fetch('/api/members/search?q=' + encodeURIComponent(q) + '&exclude=<?= (int) $waiver['source_member_id'] ?>&limit=10')
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.length) {
resultsDiv.innerHTML = '<div style="padding:12px;color:#6B7280;font-size:13px;">لا توجد نتائج</div>';
} else {
resultsDiv.innerHTML = data.map(function(m) {
return '<div class="target-option" data-id="' + m.id + '" data-name="' + (m.full_name_ar || '') + '" data-info="' + (m.national_id || '') + ' — ' + (m.membership_number ? 'عضوية #' + m.membership_number : 'بدون رقم عضوية') + '" style="padding:10px 14px;cursor:pointer;border-bottom:1px solid #F3F4F6;transition:background .1s;">' +
'<div style="font-weight:600;font-size:13px;">' + (m.full_name_ar || '') + '</div>' +
'<div style="font-size:11px;color:#6B7280;">' + (m.national_id || '—') + ' — ' + (m.phone_mobile || '') + (m.membership_number ? ' — عضوية #' + m.membership_number : '') + '</div></div>';
}).join('');
}
resultsDiv.style.display = 'block';
});
}, 300);
});
resultsDiv.addEventListener('click', function(e) {
var opt = e.target.closest('.target-option');
if (!opt) return;
hiddenInput.value = opt.dataset.id;
document.getElementById('target-name').textContent = opt.dataset.name;
document.getElementById('target-info').textContent = opt.dataset.info;
selectedDiv.style.display = 'block';
resultsDiv.style.display = 'none';
searchInput.value = '';
});
document.addEventListener('click', function(e) {
if (!resultsDiv.contains(e.target) && e.target !== searchInput) resultsDiv.style.display = 'none';
});
resultsDiv.addEventListener('mouseover', function(e) {
var opt = e.target.closest('.target-option');
if (opt) opt.style.background = '#F0FDF4';
});
resultsDiv.addEventListener('mouseout', function(e) {
var opt = e.target.closest('.target-option');
if (opt) opt.style.background = '';
});
})();
function clearTarget() {
document.getElementById('target-member-id').value = '';
document.getElementById('target-selected').style.display = 'none';
}
function validateComplete() {
if (!document.getElementById('target-member-id').value || document.getElementById('target-member-id').value === '0') {
alert('يجب تحديد المستفيد (المتنازل إليه) أولاً');
return false;
}
return confirm('⚠ إتمام التنازل — سيتم نقل العضوية رقم <?= e($waiver['membership_number'] ?? '') ?> للمستفيد. هل أنت متأكد؟');
}
</script>
<?php endif; ?>
<?php if ($waiver['status'] === 'requested'): ?>
......
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