Commit 1445d411 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Retroactive wizard: strict validation, no placeholders, clear error messages

Client-side: validates ALL mandatory DB fields before allowing next/submit
- Step 1: name, phone, gender, date_of_birth (or NID)
- Step 2: form_date, join_date
- Step 3: membership_value
- Step 5: each spouse needs name, DOB/NID, marriage_date, join_date
         each child needs name, DOB/NID, join_date
         each temp member needs name, DOB/NID, category
- Red border + Arabic error on each missing field, scrolls to first error

Server-side: full validation before any DB write
- Rejects with clear Arabic messages listing every missing field
- No more placeholder dates or fake phone numbers
- If anything bypasses JS, server still blocks with human-readable errors
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent b98382dd
......@@ -17,6 +17,47 @@ final class RetroactiveMembershipService
$empId = $employee ? (int) $employee->id : null;
$ts = date('Y-m-d H:i:s');
// Server-side mandatory field validation
$errors = [];
if (empty(trim($data['full_name_ar'] ?? ''))) $errors[] = 'الاسم بالعربية مطلوب';
if (empty(trim($data['phone_mobile'] ?? ''))) $errors[] = 'رقم الهاتف المحمول مطلوب';
if (empty(trim($data['join_date'] ?? ''))) $errors[] = 'تاريخ الانضمام مطلوب';
if (empty(trim($data['form_date'] ?? ''))) $errors[] = 'تاريخ الاستمارة مطلوب';
$hasNid = !empty(trim($data['national_id'] ?? '')) && strlen(trim($data['national_id'])) === 14;
$hasDob = !empty(trim($data['date_of_birth'] ?? ''));
if (!$hasNid && !$hasDob) $errors[] = 'تاريخ الميلاد مطلوب (أو أدخل الرقم القومي 14 رقم)';
if (!empty($data['spouses'])) {
foreach ($data['spouses'] as $si => $sp) {
$n = $si + 1;
if (empty(trim($sp['full_name_ar'] ?? ''))) $errors[] = "اسم الزوجة #{$n} مطلوب";
if (empty(trim($sp['marriage_date'] ?? ''))) $errors[] = "تاريخ زواج الزوجة #{$n} مطلوب";
$spNid = !empty(trim($sp['national_id'] ?? '')) && strlen(trim($sp['national_id'])) === 14;
$spDob = !empty(trim($sp['date_of_birth'] ?? ''));
if (!$spNid && !$spDob) $errors[] = "تاريخ ميلاد الزوجة #{$n} مطلوب (أو الرقم القومي)";
}
}
if (!empty($data['children'])) {
foreach ($data['children'] as $ci => $ch) {
$n = $ci + 1;
if (empty(trim($ch['full_name_ar'] ?? ''))) $errors[] = "اسم الابن/الابنة #{$n} مطلوب";
$chNid = !empty(trim($ch['national_id'] ?? '')) && strlen(trim($ch['national_id'])) === 14;
$chDob = !empty(trim($ch['date_of_birth'] ?? ''));
if (!$chNid && !$chDob) $errors[] = "تاريخ ميلاد الابن/الابنة #{$n} مطلوب (أو الرقم القومي)";
}
}
if (!empty($data['temporary_members'])) {
foreach ($data['temporary_members'] as $ti => $tm) {
$n = $ti + 1;
if (empty(trim($tm['full_name_ar'] ?? ''))) $errors[] = "اسم العضو المؤقت #{$n} مطلوب";
$tmNid = !empty(trim($tm['national_id'] ?? '')) && strlen(trim($tm['national_id'])) === 14;
$tmDob = !empty(trim($tm['date_of_birth'] ?? ''));
if (!$tmNid && !$tmDob) $errors[] = "تاريخ ميلاد العضو المؤقت #{$n} مطلوب (أو الرقم القومي)";
}
}
if (!empty($errors)) {
return ['success' => false, 'error' => implode("\n", $errors)];
}
$db->beginTransaction();
try {
$formNumber = (string) (MemberNumberGenerator::next() ?? 1);
......@@ -40,7 +81,7 @@ final class RetroactiveMembershipService
}
if (!$dateOfBirth) {
$dateOfBirth = '1980-01-01';
return ['success' => false, 'error' => 'تاريخ الميلاد مطلوب — أدخل الرقم القومي (14 رقم) أو تاريخ الميلاد يدوياً'];
}
$memberData = [
......@@ -51,7 +92,7 @@ final class RetroactiveMembershipService
'id_type' => $nationalId ? 'national_id' : 'passport',
'date_of_birth' => $dateOfBirth,
'gender' => $gender ?: 'male',
'phone_mobile' => trim($data['phone_mobile'] ?? '') ?: '00000000000',
'phone_mobile' => trim($data['phone_mobile'] ?? ''),
'phone_home' => self::emptyToNull($data['phone_home'] ?? null),
'email' => self::emptyToNull($data['email'] ?? null),
'branch_id' => (int) ($data['branch_id'] ?? 1),
......@@ -179,7 +220,7 @@ final class RetroactiveMembershipService
if ($parsed['gender']) $spouseGender = $parsed['gender'];
}
}
if (!$spouseDob) $spouseDob = '1985-01-01';
if (!$spouseDob) return ['success' => false, 'error' => 'تاريخ ميلاد الزوجة مطلوب'];
$spouseId = $db->insert('spouses', [
'member_id' => $memberId,
......@@ -233,7 +274,7 @@ final class RetroactiveMembershipService
if ($parsed['gender']) $childGender = $parsed['gender'];
}
}
if (!$childDob) $childDob = '2005-01-01';
if (!$childDob) return ['success' => false, 'error' => 'تاريخ ميلاد الابن/الابنة مطلوب'];
$childId = $db->insert('children', [
'member_id' => $memberId,
......@@ -284,7 +325,7 @@ final class RetroactiveMembershipService
if ($parsed['gender']) $tempGender = $parsed['gender'];
}
}
if (!$tempDob) $tempDob = '1990-01-01';
if (!$tempDob) return ['success' => false, 'error' => 'تاريخ ميلاد العضو المؤقت مطلوب'];
$tempId = $db->insert('temporary_members', [
'member_id' => $memberId,
......
......@@ -545,36 +545,95 @@ function validateStep(step) {
if (!panel) return true;
let valid = true;
panel.querySelectorAll('.val-hint').forEach(h => h.remove());
panel.querySelectorAll('[style*="border: 2px solid"],.form-input,.form-select').forEach(el => { el.style.border = ''; el.style.background = ''; });
const rules = {
1: [
{name: 'full_name_ar', msg: 'الاسم بالعربية مطلوب'},
{name: 'branch_id', msg: 'الفرع مطلوب'},
],
2: [
{name: 'form_date', msg: 'تاريخ الاستمارة مطلوب'},
{name: 'join_date', msg: 'تاريخ الانضمام مطلوب'},
],
3: [
{name: 'membership_value', msg: 'قيمة العضوية مطلوبة', min: 1},
],
};
const stepRules = rules[step] || [];
stepRules.forEach(function(rule) {
const el = panel.querySelector('[name="' + rule.name + '"]');
panel.querySelectorAll('.form-input,.form-select,select,input,textarea').forEach(el => { el.style.border = ''; el.style.background = ''; });
function check(el, msg) {
if (!el) return;
const val = el.value ? el.value.trim() : '';
if (!val) { markInvalid(el, msg); valid = false; }
}
function checkMin(el, min, msg) {
if (!el) return;
const val = el.value.trim();
if (!val || (rule.min && parseFloat(val) < rule.min)) {
markInvalid(el, rule.msg);
if (parseFloat(el.value) < min || !el.value.trim()) { markInvalid(el, msg); valid = false; }
}
if (step === 1) {
check(panel.querySelector('[name=full_name_ar]'), 'الاسم بالعربية مطلوب');
check(panel.querySelector('[name=branch_id]'), 'الفرع مطلوب');
check(panel.querySelector('[name=gender]'), 'النوع مطلوب');
check(panel.querySelector('[name=phone_mobile]'), 'رقم الهاتف المحمول مطلوب');
// date_of_birth OR national_id required
const dob = panel.querySelector('[name=date_of_birth]');
const nid = panel.querySelector('[name=national_id]');
if ((!dob || !dob.value.trim()) && (!nid || nid.value.trim().length !== 14)) {
if (dob) markInvalid(dob, 'تاريخ الميلاد مطلوب (أو أدخل الرقم القومي 14 رقم)');
valid = false;
}
});
}
if (step === 2) {
check(panel.querySelector('[name=form_date]'), 'تاريخ الاستمارة مطلوب');
check(panel.querySelector('[name=join_date]'), 'تاريخ الانضمام مطلوب');
}
if (step === 3) {
checkMin(panel.querySelector('[name=membership_value]'), 1, 'قيمة العضوية مطلوبة');
}
// Step 5: validate dependents — each one needs mandatory fields matching DB NOT NULL
if (step === 5) {
const sc = parseInt(document.getElementById('spouseCount').value) || 0;
for (let i = 1; i <= sc; i++) {
const row = document.getElementById('spouseRow' + i);
if (!row) continue;
const nameEl = row.querySelector('[name=spouse_name_' + i + ']');
const dobEl = row.querySelector('[name=spouse_dob_' + i + ']');
const nidEl = row.querySelector('[name=spouse_nid_' + i + ']');
const marriageEl = row.querySelector('[name=spouse_marriage_date_' + i + ']');
const joinEl = row.querySelector('[name=spouse_join_date_' + i + ']');
check(nameEl, 'اسم الزوجة مطلوب');
check(marriageEl, 'تاريخ الزواج مطلوب');
check(joinEl, 'تاريخ الانضمام مطلوب');
if ((!dobEl || !dobEl.value.trim()) && (!nidEl || nidEl.value.trim().length !== 14)) {
if (dobEl) markInvalid(dobEl, 'تاريخ الميلاد مطلوب (أو الرقم القومي)');
valid = false;
}
}
const cc = parseInt(document.getElementById('childCount').value) || 0;
for (let i = 1; i <= cc; i++) {
const row = document.getElementById('childRow' + i);
if (!row) continue;
const nameEl = row.querySelector('[name=child_name_' + i + ']');
const dobEl = row.querySelector('[name=child_dob_' + i + ']');
const nidEl = row.querySelector('[name=child_nid_' + i + ']');
const joinEl = row.querySelector('[name=child_join_date_' + i + ']');
check(nameEl, 'اسم الابن/الابنة مطلوب');
check(joinEl, 'تاريخ الانضمام مطلوب');
if ((!dobEl || !dobEl.value.trim()) && (!nidEl || nidEl.value.trim().length !== 14)) {
if (dobEl) markInvalid(dobEl, 'تاريخ الميلاد مطلوب (أو الرقم القومي)');
valid = false;
}
}
const tc = parseInt(document.getElementById('tempMemberCount').value) || 0;
for (let i = 1; i <= tc; i++) {
const row = document.getElementById('tempRow' + i);
if (!row) continue;
const nameEl = row.querySelector('[name=temp_name_' + i + ']');
const dobEl = row.querySelector('[name=temp_dob_' + i + ']');
const nidEl = row.querySelector('[name=temp_nid_' + i + ']');
const catEl = row.querySelector('[name=temp_category_' + i + ']');
check(nameEl, 'اسم العضو المؤقت مطلوب');
check(catEl, 'الفئة مطلوبة');
if ((!dobEl || !dobEl.value.trim()) && (!nidEl || nidEl.value.trim().length !== 14)) {
if (dobEl) markInvalid(dobEl, 'تاريخ الميلاد مطلوب (أو الرقم القومي)');
valid = false;
}
}
}
if (!valid) {
const firstBad = panel.querySelector('[style*="border: 2px solid"]');
if (firstBad) firstBad.focus();
if (firstBad) { firstBad.focus(); firstBad.scrollIntoView({behavior:'smooth', block:'center'}); }
}
return valid;
}
......@@ -1078,7 +1137,9 @@ function updateFinalSummary() {
// Form submit validation
document.getElementById('retroForm').addEventListener('submit', function(e) {
for (let s = 1; s <= 3; s++) {
var stepsToValidate = [1, 2, 3, 5];
for (var i = 0; i < stepsToValidate.length; i++) {
var s = stepsToValidate[i];
if (!validateStep(s)) {
e.preventDefault();
currentStep = s;
......
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