Commit 69293591 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Revamp membership forms to match paper format with NID-first approach

- Restructured create.php: NID is now primary input with auto-extracted
  DOB/age/gender/governorate displayed in highlighted results box
- Rebuilt fill-form.php to match paper form sections: personal data,
  contact, residence, work details, referral source, special discount
- All fill-form fields non-mandatory except qualification_id (pricing)
- Spouse form: NID locks DOB field when parsed, shows source indicators
- Children form: NID locks both DOB and gender when parsed
- Added discount document upload handling to saveFillForm controller
- Bilingual labels (Arabic/English) matching physical paper forms
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 69da9aa2
...@@ -35,26 +35,26 @@ ...@@ -35,26 +35,26 @@
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">الرقم القومي <small style="color:#6B7280;">(مطلوب فوق 16 سنة)</small></label> <label class="form-label">الرقم القومي <small style="color:#0284C7;">(مطلوب فوق 16 سنة — يستخرج تاريخ الميلاد والنوع تلقائياً)</small></label>
<input type="text" name="national_id" id="child_nid" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;"> <input type="text" name="national_id" id="child_nid" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;font-size:16px;letter-spacing:2px;" placeholder="14 رقم">
<div id="child-nid-feedback" style="margin-top:5px;font-size:12px;"></div> <div id="child-nid-feedback" style="margin-top:5px;font-size:12px;"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">رقم شهادة الميلاد <small style="color:#6B7280;">(تحت 16 سنة)</small></label> <label class="form-label">رقم شهادة الميلاد <small style="color:#6B7280;">(للأطفال تحت 16 سنة)</small></label>
<input type="text" name="birth_certificate_number" value="<?= e(old('birth_certificate_number')) ?>" class="form-input" style="direction:ltr;text-align:left;"> <input type="text" name="birth_certificate_number" value="<?= e(old('birth_certificate_number')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label> <label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span> <small id="child_dob_source" style="color:#059669;display:none;">(من الرقم القومي)</small></label>
<input type="date" name="date_of_birth" id="child_dob" value="<?= e(old('date_of_birth')) ?>" class="form-input" required> <input type="date" name="date_of_birth" id="child_dob" value="<?= e(old('date_of_birth')) ?>" class="form-input" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">السن</label> <label class="form-label">السن <small id="child_age_source" style="color:#059669;display:none;">(محسوب تلقائياً)</small></label>
<input type="text" id="child_age_display" class="form-input" style="background:#F3F4F6;" readonly> <input type="text" id="child_age_display" class="form-input" style="background:#F3F4F6;" readonly>
<input type="hidden" name="age_years" id="child_age_years"> <input type="hidden" name="age_years" id="child_age_years">
<input type="hidden" name="age_months" id="child_age_months"> <input type="hidden" name="age_months" id="child_age_months">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label> <label class="form-label">النوع <span style="color:#DC2626;">*</span> <small id="child_gender_source" style="color:#059669;display:none;">(من الرقم القومي)</small></label>
<select name="gender" id="child_gender" class="form-select" required> <select name="gender" id="child_gender" class="form-select" required>
<option value="">-- اختر --</option> <option value="">-- اختر --</option>
<option value="male" <?= old('gender') === 'male' ? 'selected' : '' ?>>ذكر</option> <option value="male" <?= old('gender') === 'male' ? 'selected' : '' ?>>ذكر</option>
...@@ -102,6 +102,11 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -102,6 +102,11 @@ document.addEventListener('DOMContentLoaded', function() {
var genderSelect = document.getElementById('child_gender'); var genderSelect = document.getElementById('child_gender');
var feedback = document.getElementById('child-nid-feedback'); var feedback = document.getElementById('child-nid-feedback');
var dobSourceEl = document.getElementById('child_dob_source');
var ageSourceEl = document.getElementById('child_age_source');
var genderSourceEl = document.getElementById('child_gender_source');
var nidLocked = false;
nidInput.addEventListener('input', function() { nidInput.addEventListener('input', function() {
var val = this.value.replace(/\D/g, ''); var val = this.value.replace(/\D/g, '');
this.value = val; this.value = val;
...@@ -117,20 +122,40 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -117,20 +122,40 @@ document.addEventListener('DOMContentLoaded', function() {
var p = data.parsed; var p = data.parsed;
if (p && p.is_valid) { if (p && p.is_valid) {
dobInput.value = p.dob; dobInput.value = p.dob;
dobInput.readOnly = true;
dobInput.style.background = '#F0FDF4';
dobSourceEl.style.display = 'inline';
ageSourceEl.style.display = 'inline';
genderSourceEl.style.display = 'inline';
ageDisplay.value = p.age_years + ' سنة و ' + p.age_months + ' شهر'; ageDisplay.value = p.age_years + ' سنة و ' + p.age_months + ' شهر';
ageYears.value = p.age_years; ageYears.value = p.age_years;
ageMonths.value = p.age_months; ageMonths.value = p.age_months;
genderSelect.value = p.gender; genderSelect.value = p.gender;
feedback.innerHTML = '<span style="color:#059669;">✓ صالح</span>'; genderSelect.style.background = '#F0FDF4';
genderSelect.setAttribute('data-nid-locked', '1');
nidLocked = true;
feedback.innerHTML = '<span style="color:#059669;">صالح — تم استخراج تاريخ الميلاد والنوع</span>';
if (data.duplicate) { if (data.duplicate) {
feedback.innerHTML += '<br><span style="color:#DC2626;">مسجل: ' + data.duplicate.full_name_ar + '</span>'; feedback.innerHTML += '<br><span style="color:#DC2626;">مسجل: ' + data.duplicate.full_name_ar + '</span>';
} }
} else { } else {
feedback.innerHTML = '<span style="color:#DC2626;">' + (p.errors ? p.errors.join(' | ') : 'غير صالح') + '</span>'; feedback.innerHTML = '<span style="color:#DC2626;">' + (p.errors ? p.errors.join(' | ') : 'غير صالح') + '</span>';
} }
}) })
.catch(function() { feedback.innerHTML = '<span style="color:#DC2626;">خطأ</span>'; }); .catch(function() { feedback.innerHTML = '<span style="color:#DC2626;">خطأ</span>'; });
} else { feedback.innerHTML = ''; } } else {
feedback.innerHTML = '';
if (nidLocked) {
dobInput.readOnly = false;
dobInput.style.background = '';
genderSelect.style.background = '';
genderSelect.removeAttribute('data-nid-locked');
dobSourceEl.style.display = 'none';
ageSourceEl.style.display = 'none';
genderSourceEl.style.display = 'none';
nidLocked = false;
}
}
}); });
// Manual DOB age calculation // Manual DOB age calculation
......
...@@ -373,6 +373,22 @@ class MemberController extends Controller ...@@ -373,6 +373,22 @@ class MemberController extends Controller
$update['special_discount_id'] = (int) $discountId; $update['special_discount_id'] = (int) $discountId;
$mValue = $update['membership_value'] ?? ($member->membership_value ?? '0.00'); $mValue = $update['membership_value'] ?? ($member->membership_value ?? '0.00');
$update['discount_amount'] = bcdiv(bcmul($mValue, $discountRow['discount_percentage'], 4), '100', 2); $update['discount_amount'] = bcdiv(bcmul($mValue, $discountRow['discount_percentage'], 4), '100', 2);
if ($discountRow['requires_document'] && !empty($_FILES['discount_document']['tmp_name'])) {
$file = $_FILES['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 = '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['special_discount_document'] = 'storage/uploads/discounts/' . $storedName;
}
}
}
} }
} }
......
This diff is collapsed.
This diff is collapsed.
...@@ -16,7 +16,11 @@ class SeasonalMembership extends Model ...@@ -16,7 +16,11 @@ class SeasonalMembership extends Model
protected static array $fillable = [ protected static array $fillable = [
'member_id', 'branch_id', 'start_date', 'end_date', 'member_id', 'branch_id', 'start_date', 'end_date',
'fee_amount', 'fee_receipt_number', 'status', 'carnet_marking', 'notes', 'nationality_type', 'duration_months', 'person_type', 'person_name',
'person_age', 'age_category', 'parent_seasonal_id',
'fee_amount', 'currency', 'vat_amount', 'discount_amount',
'discount_type', 'total_amount', 'includes_pool',
'fee_receipt_number', 'status', 'carnet_marking', 'notes',
]; ];
public static function getForMember(int $memberId): array public static function getForMember(int $memberId): array
......
...@@ -5,4 +5,5 @@ return [ ...@@ -5,4 +5,5 @@ return [
['GET', '/seasonal', 'Seasonal\Controllers\SeasonalController@index', ['auth'], 'temp.view'], ['GET', '/seasonal', 'Seasonal\Controllers\SeasonalController@index', ['auth'], 'temp.view'],
['GET', '/members/{memberId}/seasonal/create', 'Seasonal\Controllers\SeasonalController@create', ['auth'], 'temp.add'], ['GET', '/members/{memberId}/seasonal/create', 'Seasonal\Controllers\SeasonalController@create', ['auth'], 'temp.add'],
['POST', '/members/{memberId}/seasonal', 'Seasonal\Controllers\SeasonalController@store', ['auth', 'csrf'], 'temp.add'], ['POST', '/members/{memberId}/seasonal', 'Seasonal\Controllers\SeasonalController@store', ['auth', 'csrf'], 'temp.add'],
['GET', '/api/seasonal/price-preview', 'Seasonal\Controllers\SeasonalController@pricePreview', ['auth'], 'temp.add'],
]; ];
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
...@@ -50,18 +50,18 @@ $genderValue = $requiredGender; ...@@ -50,18 +50,18 @@ $genderValue = $requiredGender;
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">الرقم القومي</label> <label class="form-label">الرقم القومي <small style="color:#0284C7;">(يستخرج تاريخ الميلاد والسن تلقائياً)</small></label>
<input type="text" name="national_id" id="spouse_nid" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;"> <input type="text" name="national_id" id="spouse_nid" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;font-size:16px;letter-spacing:2px;" placeholder="14 رقم">
<div id="spouse-nid-feedback" style="margin-top:5px;font-size:12px;"></div> <div id="spouse-nid-feedback" style="margin-top:5px;font-size:12px;"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label> <label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span> <small id="dob_source" style="color:#059669;display:none;">(من الرقم القومي)</small></label>
<input type="date" name="date_of_birth" id="spouse_dob" value="<?= e(old('date_of_birth')) ?>" class="form-input" required> <input type="date" name="date_of_birth" id="spouse_dob" value="<?= e(old('date_of_birth')) ?>" class="form-input" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">السن</label> <label class="form-label">السن <small id="age_source" style="color:#059669;display:none;">(محسوب تلقائياً)</small></label>
<input type="text" id="spouse_age_display" class="form-input" style="background:#F3F4F6;" readonly> <input type="text" id="spouse_age_display" class="form-input" style="background:#F3F4F6;" readonly>
<input type="hidden" name="age_years" id="spouse_age_years"> <input type="hidden" name="age_years" id="spouse_age_years">
<input type="hidden" name="age_months" id="spouse_age_months"> <input type="hidden" name="age_months" id="spouse_age_months">
...@@ -136,6 +136,10 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -136,6 +136,10 @@ document.addEventListener('DOMContentLoaded', function() {
var feedback = document.getElementById('spouse-nid-feedback'); var feedback = document.getElementById('spouse-nid-feedback');
var requiredGender = '<?= e($genderValue) ?>'; var requiredGender = '<?= e($genderValue) ?>';
var dobSource = document.getElementById('dob_source');
var ageSource = document.getElementById('age_source');
var nidLocked = false;
nidInput.addEventListener('input', function() { nidInput.addEventListener('input', function() {
var val = this.value.replace(/\D/g, ''); var val = this.value.replace(/\D/g, '');
this.value = val; this.value = val;
...@@ -151,26 +155,39 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -151,26 +155,39 @@ document.addEventListener('DOMContentLoaded', function() {
var p = data.parsed; var p = data.parsed;
if (p && p.is_valid) { if (p && p.is_valid) {
dobInput.value = p.dob; dobInput.value = p.dob;
dobInput.readOnly = true;
dobInput.style.background = '#F0FDF4';
dobSource.style.display = 'inline';
ageSource.style.display = 'inline';
ageDisplay.value = p.age_years + ' سنة و ' + p.age_months + ' شهر'; ageDisplay.value = p.age_years + ' سنة و ' + p.age_months + ' شهر';
ageYears.value = p.age_years; ageYears.value = p.age_years;
ageMonths.value = p.age_months; ageMonths.value = p.age_months;
feedback.innerHTML = '<span style="color:#059669;">✓ صالح</span>'; nidLocked = true;
feedback.innerHTML = '<span style="color:#059669;">صالح — تم استخراج تاريخ الميلاد والسن</span>';
// Check gender mismatch
if (p.gender !== requiredGender) { if (p.gender !== requiredGender) {
var needed = requiredGender === 'male' ? 'ذكر' : 'أنثى'; var needed = requiredGender === 'male' ? 'ذكر' : 'أنثى';
feedback.innerHTML += '<br><span style="color:#DC2626;">الرقم القومي يشير إلى نوع مختلف — المطلوب: ' + needed + '</span>'; feedback.innerHTML += '<br><span style="color:#DC2626;">الرقم القومي يشير إلى نوع مختلف — المطلوب: ' + needed + '</span>';
} }
if (data.duplicate) { if (data.duplicate) {
feedback.innerHTML += '<br><span style="color:#DC2626;">مسجل: ' + data.duplicate.full_name_ar + '</span>'; feedback.innerHTML += '<br><span style="color:#DC2626;">مسجل: ' + data.duplicate.full_name_ar + '</span>';
} }
} else { } else {
feedback.innerHTML = '<span style="color:#DC2626;">' + (p.errors ? p.errors.join(' | ') : 'غير صالح') + '</span>'; feedback.innerHTML = '<span style="color:#DC2626;">' + (p.errors ? p.errors.join(' | ') : 'غير صالح') + '</span>';
} }
}) })
.catch(function() { feedback.innerHTML = '<span style="color:#DC2626;">خطأ</span>'; }); .catch(function() { feedback.innerHTML = '<span style="color:#DC2626;">خطأ</span>'; });
} else { feedback.innerHTML = ''; } } else {
feedback.innerHTML = '';
if (nidLocked) {
dobInput.readOnly = false;
dobInput.style.background = '';
dobSource.style.display = 'none';
ageSource.style.display = 'none';
nidLocked = false;
}
}
}); });
dobInput.addEventListener('change', function() { dobInput.addEventListener('change', function() {
......
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE `seasonal_memberships` ADD COLUMN `nationality_type` ENUM('egyptian','foreign') NOT NULL DEFAULT 'egyptian' AFTER `branch_id`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `duration_months` TINYINT UNSIGNED NOT NULL DEFAULT 6 AFTER `nationality_type`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `person_type` ENUM('member','spouse','child') NOT NULL DEFAULT 'member' AFTER `duration_months`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `person_name` VARCHAR(200) NULL AFTER `person_type`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `person_age` TINYINT UNSIGNED NULL AFTER `person_name`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `age_category` ENUM('under_15','15_to_21','over_21','over_60') NULL AFTER `person_age`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `currency` ENUM('EGP','USD') NOT NULL DEFAULT 'EGP' AFTER `fee_amount`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `vat_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 AFTER `currency`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `discount_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 AFTER `vat_amount`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `discount_type` VARCHAR(100) NULL AFTER `discount_amount`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `total_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 AFTER `discount_type`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `parent_seasonal_id` BIGINT UNSIGNED NULL AFTER `member_id`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `includes_pool` TINYINT(1) NOT NULL DEFAULT 0 AFTER `carnet_marking`;
ALTER TABLE `seasonal_memberships` ADD INDEX `idx_seasonal_nationality` (`nationality_type`);
ALTER TABLE `seasonal_memberships` ADD INDEX `idx_seasonal_parent` (`parent_seasonal_id`)",
'down' => "
ALTER TABLE `seasonal_memberships` DROP INDEX IF EXISTS `idx_seasonal_parent`;
ALTER TABLE `seasonal_memberships` DROP INDEX IF EXISTS `idx_seasonal_nationality`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `includes_pool`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `parent_seasonal_id`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `total_amount`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `discount_type`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `discount_amount`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `vat_amount`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `currency`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `age_category`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `person_age`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `person_name`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `person_type`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `duration_months`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `nationality_type`",
];
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$rules = [
['key' => 'SEASONAL_VAT_RATE', 'value' => json_encode(['percentage' => '14.00']), 'description' => 'نسبة ضريبة القيمة المضافة على الاشتراكات الموسمية'],
['key' => 'SEASONAL_FAMILY_DISCOUNT_PARENT_CHILD', 'value' => json_encode(['amount' => '500.00']), 'description' => 'خصم والد مع ابن أقل من 15 سنة'],
['key' => 'SEASONAL_FAMILY_DISCOUNT_COUPLE', 'value' => json_encode(['amount' => '1000.00']), 'description' => 'خصم زوج وزوجة في نفس الفترة'],
['key' => 'SEASONAL_FAMILY_DISCOUNT_FAMILY_5', 'value' => json_encode(['amount' => '1000.00']), 'description' => 'خصم أسرة 5 أفراد'],
['key' => 'SEASONAL_NO_QUALIFICATION_DISCOUNT', 'value' => json_encode(['percentage' => '10.00']), 'description' => 'خصم عدم وجود مؤهل عالي/متوسط'],
];
foreach ($rules as $rule) {
$exists = $db->selectOne("SELECT id FROM system_config WHERE config_key = ?", [$rule['key']]);
if (!$exists) {
$db->insert('system_config', [
'config_key' => $rule['key'],
'config_value' => $rule['value'],
'description' => $rule['description'],
'is_active' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i: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