Commit b86c24f6 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(members): integrate regulatory discounts into membership form

- Add regulatory_discount_id/amount/document columns to members table
- Add eligibility type selector with conditional sections per type
- Auto-verification for cross-branch (checks members DB) and club employee (HR)
- Document upload for types requiring manual proof (gov, ministry, board, group)
- AJAX eligibility check button calls /pricing/regulatory-discounts/check-eligibility
- Creates audit trail application record on form submit (status: pending)
- Dynamic UI: shows/hides relevant fields based on selected type
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 3f42fa01
......@@ -758,6 +758,61 @@ class MemberController extends Controller
}
}
// Regulatory discount
$regDiscountId = trim((string) ($data['regulatory_discount_id'] ?? ''));
if ($regDiscountId !== '' && $regDiscountId !== '0') {
$regRule = $db->selectOne("SELECT * FROM regulatory_discounts WHERE id = ? AND is_active = 1", [(int) $regDiscountId]);
if ($regRule) {
$update['regulatory_discount_id'] = (int) $regDiscountId;
$mValue = $update['membership_value'] ?? ($member->membership_value ?? '0.00');
$pct = $regRule['discount_percentage'];
if ($regRule['max_discount_percentage'] !== null) {
$pct = min((float) $pct, (float) $regRule['max_discount_percentage']);
$pct = number_format($pct, 2, '.', '');
}
$update['regulatory_discount_amount'] = bcmul($mValue, bcdiv($pct, '100', 4), 2);
// Upload proof document
if (!empty($_FILES['regulatory_discount_document']['tmp_name'])) {
$file = $_FILES['regulatory_discount_document'];
$allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (in_array($mimeType, $allowedTypes, true) && $file['size'] <= 10485760) {
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$storedName = 'reg_discount_' . (int) $id . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$uploadDir = App::getInstance()->basePath() . '/storage/uploads/discounts/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
if (move_uploaded_file($file['tmp_name'], $uploadDir . $storedName)) {
$update['regulatory_discount_document'] = 'storage/uploads/discounts/' . $storedName;
}
}
}
// Create application record for audit trail
\App\Modules\Pricing\Services\RegulatoryDiscountService::createApplication([
'regulatory_discount_id' => (int) $regDiscountId,
'member_id' => (int) $id,
'applicant_name' => $member->full_name_ar,
'discount_percentage' => $pct,
'original_amount' => $mValue,
'discount_amount' => $update['regulatory_discount_amount'],
'final_amount' => bcsub($mValue, $update['regulatory_discount_amount'], 2),
'status' => 'pending',
'verification_data' => [
'eligibility_type' => $data['regulatory_eligibility_type'] ?? $regRule['eligibility_type'],
'source_branch_id' => $data['reg_source_branch_id'] ?? null,
'employee_id' => $data['reg_employee_id'] ?? null,
'group_quantity' => $data['reg_group_quantity'] ?? null,
],
'created_by' => session()->get('user_id'),
]);
}
} elseif ($regDiscountId === '') {
$update['regulatory_discount_id'] = null;
$update['regulatory_discount_amount'] = null;
}
if ($member->status === 'potential') $update['status'] = 'under_review';
if (!empty($update)) $member->update($update);
......
......@@ -323,6 +323,95 @@
</div>
<?php endif; ?>
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- القسم 7: خصم لائحي (المواد 97-102، 110) -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<div class="card" style="margin-bottom:20px;border-right:4px solid #0D7377;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">
<i data-lucide="scale" style="width:16px;height:16px;vertical-align:middle;margin-left:6px;"></i>
خصم لائحي (المواد 97-102، 110)
</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:15px;">
<div class="form-group">
<label class="form-label">نوع الاستحقاق</label>
<select name="regulatory_eligibility_type" id="reg_eligibility_type" class="form-select">
<option value="">— بدون خصم لائحي —</option>
<option value="cross_branch_member" <?= ($member->regulatory_eligibility_type ?? '') === 'cross_branch_member' ? 'selected' : '' ?>>عضو فرع آخر (مادة 97)</option>
<option value="government_employee" <?= ($member->regulatory_eligibility_type ?? '') === 'government_employee' ? 'selected' : '' ?>>موظف حكومي (مادة 98)</option>
<option value="ministry_youth_employee" <?= ($member->regulatory_eligibility_type ?? '') === 'ministry_youth_employee' ? 'selected' : '' ?>>موظف وزارة الشباب (مادة 98/99)</option>
<option value="board_of_trustees" <?= ($member->regulatory_eligibility_type ?? '') === 'board_of_trustees' ? 'selected' : '' ?>>عضو مجلس أمناء (مادة 100)</option>
<option value="club_employee" <?= ($member->regulatory_eligibility_type ?? '') === 'club_employee' ? 'selected' : '' ?>>موظف النادي (مادة 102)</option>
<option value="group_membership" <?= ($member->regulatory_eligibility_type ?? '') === 'group_membership' ? 'selected' : '' ?>>عضوية مجمعة (مادة 110)</option>
</select>
</div>
<div class="form-group" id="reg_result_box" style="display:none;">
<label class="form-label">الخصم المستحق</label>
<div id="reg_discount_display" style="padding:10px;background:#ECFDF5;border-radius:6px;border:1px solid #A7F3D0;font-weight:700;color:#059669;font-size:15px;text-align:center;"></div>
<input type="hidden" name="regulatory_discount_id" id="reg_discount_id" value="<?= e($member->regulatory_discount_id ?? '') ?>">
</div>
</div>
<!-- Cross-branch: source branch -->
<div id="reg_section_cross_branch" style="display:none;margin-bottom:15px;">
<div class="form-group">
<label class="form-label">فرع العضوية الحالية (المصدر)</label>
<select name="reg_source_branch_id" id="reg_source_branch" class="form-select">
<option value="">— اختر الفرع —</option>
<?php foreach ($branches as $b): ?>
<option value="<?= (int) $b['id'] ?>"><?= e($b['name_ar']) ?></option>
<?php endforeach; ?>
</select>
<small style="color:#6B7280;display:block;margin-top:4px;">النظام سيتحقق تلقائياً من وجود عضوية نشطة في هذا الفرع</small>
</div>
</div>
<!-- Club employee: employee ID -->
<div id="reg_section_club_employee" style="display:none;margin-bottom:15px;">
<div class="form-group">
<label class="form-label">الرقم الوظيفي بالنادي</label>
<input type="text" name="reg_employee_id" id="reg_employee_id" class="form-input" placeholder="أدخل الرقم الوظيفي للتحقق من HR">
<small style="color:#6B7280;display:block;margin-top:4px;">النظام سيتحقق تلقائياً من سنوات الخدمة (5 سنوات كحد أدنى)</small>
</div>
</div>
<!-- Group membership: quantity -->
<div id="reg_section_group" style="display:none;margin-bottom:15px;">
<div class="form-group">
<label class="form-label">عدد أعضاء المجموعة</label>
<input type="number" name="reg_group_quantity" id="reg_group_quantity" class="form-input" min="5" placeholder="الحد الأدنى 5 أعضاء">
</div>
</div>
<!-- Document upload (for types that need manual proof) -->
<div id="reg_section_document" style="display:none;margin-bottom:15px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label" id="reg_doc_label">مستند الإثبات</label>
<input type="file" name="regulatory_discount_document" class="form-input" accept=".pdf,.jpg,.jpeg,.png">
<?php if ($member->regulatory_discount_document ?? ''): ?>
<small style="color:#059669;">مرفق سابق: <?= e(basename($member->regulatory_discount_document)) ?></small>
<?php endif; ?>
</div>
<div class="form-group" style="display:flex;align-items:end;">
<small id="reg_doc_hint" style="color:#6B7280;padding-bottom:8px;">PDF أو صورة — الحد الأقصى 10 ميجا</small>
</div>
</div>
</div>
<!-- Check eligibility button -->
<div id="reg_check_section" style="display:none;">
<button type="button" id="reg_check_btn" class="btn" style="background:#0D7377;color:#fff;padding:8px 20px;" onclick="checkRegulatoryEligibility()">
<i data-lucide="search-check" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i>
تحقق من الأهلية
</button>
<span id="reg_check_status" style="margin-right:10px;font-size:13px;color:#6B7280;"></span>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- ملاحظات هامة (من أسفل الاستمارة الورقية) -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
......@@ -367,6 +456,141 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
}
// Regulatory discount UX
var regType = document.getElementById('reg_eligibility_type');
if (regType) {
regType.addEventListener('change', updateRegSections);
updateRegSections();
}
});
function updateRegSections() {
var type = document.getElementById('reg_eligibility_type').value;
var crossBranch = document.getElementById('reg_section_cross_branch');
var clubEmployee = document.getElementById('reg_section_club_employee');
var groupSection = document.getElementById('reg_section_group');
var docSection = document.getElementById('reg_section_document');
var checkSection = document.getElementById('reg_check_section');
var resultBox = document.getElementById('reg_result_box');
var docLabel = document.getElementById('reg_doc_label');
var docHint = document.getElementById('reg_doc_hint');
crossBranch.style.display = 'none';
clubEmployee.style.display = 'none';
groupSection.style.display = 'none';
docSection.style.display = 'none';
checkSection.style.display = 'none';
if (!type) {
resultBox.style.display = 'none';
document.getElementById('reg_discount_id').value = '';
return;
}
checkSection.style.display = 'block';
var needsDoc = false;
switch (type) {
case 'cross_branch_member':
crossBranch.style.display = 'block';
break;
case 'government_employee':
needsDoc = true;
docLabel.textContent = 'خطاب جهة العمل الحكومية';
docHint.textContent = 'خطاب رسمي يثبت أن المتقدم موظف حكومي حالي';
break;
case 'ministry_youth_employee':
needsDoc = true;
docLabel.textContent = 'شهادة وظيفية — وزارة الشباب والرياضة';
docHint.textContent = 'خطاب رسمي من وزارة الشباب والرياضة أو إدارة مركزية';
break;
case 'board_of_trustees':
needsDoc = true;
docLabel.textContent = 'قرار تعيين بمجلس الأمناء';
docHint.textContent = 'صورة من قرار التعيين أو ما يثبت العضوية بمجلس الأمناء';
break;
case 'club_employee':
clubEmployee.style.display = 'block';
break;
case 'group_membership':
groupSection.style.display = 'block';
needsDoc = true;
docLabel.textContent = 'كشف بأسماء أعضاء المجموعة';
docHint.textContent = 'كشف يتضمن أسماء وأرقام قومية لجميع أعضاء المجموعة';
break;
}
if (needsDoc) docSection.style.display = 'block';
}
function checkRegulatoryEligibility() {
var type = document.getElementById('reg_eligibility_type').value;
if (!type) return;
var btn = document.getElementById('reg_check_btn');
var status = document.getElementById('reg_check_status');
var resultBox = document.getElementById('reg_result_box');
var display = document.getElementById('reg_discount_display');
var hiddenId = document.getElementById('reg_discount_id');
btn.disabled = true;
status.textContent = 'جارٍ التحقق...';
status.style.color = '#6B7280';
var branchSelect = document.querySelector('select[name="branch_id"]');
var targetBranch = branchSelect ? parseInt(branchSelect.value) || 0 : 0;
var body = new URLSearchParams();
body.append('_csrf_token', document.querySelector('input[name="_csrf_token"]').value);
body.append('target_branch_id', targetBranch);
body.append('source_branch_id', document.getElementById('reg_source_branch') ? document.getElementById('reg_source_branch').value : '0');
body.append('is_government_employee', type === 'government_employee' ? '1' : '0');
body.append('is_ministry_youth_employee', type === 'ministry_youth_employee' ? '1' : '0');
body.append('is_board_of_trustees', type === 'board_of_trustees' ? '1' : '0');
body.append('is_club_employee', type === 'club_employee' ? '1' : '0');
body.append('employee_years', '5');
body.append('group_quantity', document.getElementById('reg_group_quantity') ? document.getElementById('reg_group_quantity').value || '0' : '0');
var membershipValue = '0';
var mvField = document.querySelector('input[name="membership_value"]');
if (mvField) membershipValue = mvField.value || '0';
if (membershipValue === '0') {
var qualSelect = document.querySelector('select[name="qualification_id"]');
membershipValue = qualSelect ? (qualSelect.selectedOptions[0]?.dataset?.price || '150000') : '150000';
}
body.append('membership_fee', membershipValue);
fetch('/pricing/regulatory-discounts/check-eligibility', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest'},
body: body.toString()
})
.then(function(r) { return r.json(); })
.then(function(data) {
btn.disabled = false;
if (data.success && data.best) {
resultBox.style.display = 'block';
display.innerHTML = data.best.name_ar + '<br><span style="font-size:20px;color:#059669;">خصم ' + data.best.discount_percentage + '% = ' + Number(data.best.discount_amount).toLocaleString('ar-EG') + ' ج.م</span>';
if (data.best.allows_installment) {
display.innerHTML += '<br><small style="font-size:12px;color:#0D7377;">+ تقسيط ' + data.best.installment_max_years + ' سنوات بدون فائدة</small>';
}
hiddenId.value = data.best.rule_id;
status.textContent = '✓ مستحق';
status.style.color = '#059669';
} else {
resultBox.style.display = 'block';
display.innerHTML = '<span style="color:#DC2626;">غير مستحق لخصم لائحي بهذه البيانات</span>';
hiddenId.value = '';
status.textContent = 'غير مستحق';
status.style.color = '#DC2626';
}
})
.catch(function(err) {
btn.disabled = false;
status.textContent = 'خطأ في الاتصال';
status.style.color = '#DC2626';
});
}
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
return function (\App\Core\Database $db): void {
$db->raw("
ALTER TABLE `members`
ADD COLUMN `regulatory_discount_id` BIGINT UNSIGNED NULL AFTER `discount_amount`,
ADD COLUMN `regulatory_discount_amount` DECIMAL(15,2) NULL AFTER `regulatory_discount_id`,
ADD COLUMN `regulatory_discount_document` VARCHAR(500) NULL AFTER `regulatory_discount_amount`,
ADD INDEX `idx_members_regulatory_discount` (`regulatory_discount_id`)
");
};
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