Commit 69f09d80 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(installments): add cheque auto-generator with live validation

- ChequeService: add nextChequeNumber() — sequential from last numeric cheque in DB
- ChequeController: index() passes planTotal, uploadedTotal, nextChequeNum; storeBatch() validates total coverage + dedup + activates member; store() validates total coverage on final cheque
- Routes: add POST /installments/{planId}/cheques/batch
- cheques.php: full rewrite — KPI row, auto-generator panel (JS generates N editable rows from amount+bank+start-date), live total validation bar, submit disabled until total covered, editable-row table with per-row delete, existing cheques table with coverage status, single-upload form retained
Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent e93d166b
...@@ -43,6 +43,10 @@ class ChequeController extends Controller ...@@ -43,6 +43,10 @@ class ChequeController extends Controller
$remainingCount = max(0, $requiredCount - $uploadedCount); $remainingCount = max(0, $requiredCount - $uploadedCount);
$allSubmitted = $remainingCount === 0; $allSubmitted = $remainingCount === 0;
// Total amount that cheques must cover = total_with_interest (remaining after down payment)
$uploadedTotal = array_sum(array_column($cheques, 'cheque_amount'));
$planTotal = (float) ($plan['total_with_interest'] ?? 0);
return $this->view('Installments.Views.cheques', [ return $this->view('Installments.Views.cheques', [
'plan' => $plan, 'plan' => $plan,
'cheques' => $cheques, 'cheques' => $cheques,
...@@ -50,8 +54,115 @@ class ChequeController extends Controller ...@@ -50,8 +54,115 @@ class ChequeController extends Controller
'uploadedCount' => $uploadedCount, 'uploadedCount' => $uploadedCount,
'remainingCount' => $remainingCount, 'remainingCount' => $remainingCount,
'allSubmitted' => $allSubmitted, 'allSubmitted' => $allSubmitted,
'uploadedTotal' => $uploadedTotal,
'planTotal' => $planTotal,
'nextChequeNum' => ChequeService::nextChequeNumber(),
]);
}
/**
* Batch-save multiple cheques at once (no scan files required at this stage).
* Validates that the total cheque amount >= plan's total_with_interest.
*/
public function storeBatch(Request $request, string $planId): Response
{
$this->authorize('installment.create_plan');
$db = App::getInstance()->db();
$plan = $db->selectOne(
"SELECT id, member_id, number_of_months, total_with_interest, monthly_payment
FROM installment_plans WHERE id = ? AND status = 'active'",
[(int) $planId]
);
if (!$plan) {
return $this->redirect('/installments')->withError('خطة التقسيط غير موجودة');
}
$cheques = $request->post('cheques', []);
$planTotal = (float) $plan['total_with_interest'];
if (!is_array($cheques) || empty($cheques)) {
return $this->redirect("/installments/{$planId}/cheques")->withError('لم يتم إدخال أي شيكات');
}
// Validate each row and compute grand total
$errors = [];
$totalEntered = 0.0;
foreach ($cheques as $idx => $chq) {
$n = $idx + 1;
$num = trim((string) ($chq['cheque_number'] ?? ''));
$bnk = trim((string) ($chq['bank_name'] ?? ''));
$amt = (float) ($chq['cheque_amount'] ?? 0);
$dt = trim((string) ($chq['cheque_date'] ?? ''));
if ($num === '') $errors[] = "الشيك #{$n}: رقم الشيك مطلوب";
if ($bnk === '') $errors[] = "الشيك #{$n}: اسم البنك مطلوب";
if ($amt <= 0) $errors[] = "الشيك #{$n}: المبلغ يجب أن يكون أكبر من صفر";
if ($dt === '') $errors[] = "الشيك #{$n}: التاريخ مطلوب";
$totalEntered += $amt;
}
if (empty($errors) && $totalEntered < $planTotal - 0.009) {
$errors[] = sprintf(
'إجمالي قيمة الشيكات (%.2f ج.م) أقل من إجمالي التقسيط (%.2f ج.م) — يجب أن تغطي الشيكات المبلغ كاملاً',
$totalEntered,
$planTotal
);
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($m) => ['type' => 'error', 'message' => $m], $errors));
return $this->redirect("/installments/{$planId}/cheques");
}
// Check for duplicate cheque numbers within the batch and against DB
$batchNums = array_filter(array_map(fn($c) => trim((string) ($c['cheque_number'] ?? '')), $cheques));
if (count(array_unique($batchNums)) !== count($batchNums)) {
return $this->redirect("/installments/{$planId}/cheques")
->withError('توجد أرقام شيكات مكررة في القائمة — يجب أن يكون كل رقم فريداً');
}
$employeeId = App::getInstance()->session()->get('employee_id');
$ts = date('Y-m-d H:i:s');
$db->beginTransaction();
try {
foreach ($cheques as $chq) {
$db->insert('installment_cheques', [
'installment_plan_id' => (int) $planId,
'cheque_number' => trim((string) ($chq['cheque_number'] ?? '')),
'bank_name' => trim((string) ($chq['bank_name'] ?? '')),
'cheque_date' => trim((string) ($chq['cheque_date'] ?? '')),
'cheque_amount' => (float) ($chq['cheque_amount'] ?? 0),
'scan_path' => '',
'scan_original_name' => null,
'uploaded_by' => $employeeId ? (int) $employeeId : null,
'notes' => trim((string) ($chq['notes'] ?? '')) ?: null,
'created_at' => $ts,
'updated_at' => $ts,
]); ]);
} }
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
return $this->redirect("/installments/{$planId}/cheques")->withError('خطأ أثناء الحفظ: ' . $e->getMessage());
}
// Check if count threshold reached → activate member
if (ChequeService::allChequesSubmitted((int) $planId)) {
$result = ChequeService::activateMemberAfterCheques((int) $planId);
if ($result['success'] && !($result['already_active'] ?? false)) {
return $this->redirect("/installments/{$planId}/cheques")
->withSuccess('تم حفظ الشيكات وتفعيل العضوية — رقم العضوية: ' . ($result['membership_number'] ?? ''));
}
}
return $this->redirect("/installments/{$planId}/cheques")
->withSuccess('تم حفظ ' . count($cheques) . ' شيكات بنجاح — الإجمالي: ' . number_format($totalEntered, 2) . ' ج.م');
}
public function store(Request $request, string $planId): Response public function store(Request $request, string $planId): Response
{ {
...@@ -60,7 +171,7 @@ class ChequeController extends Controller ...@@ -60,7 +171,7 @@ class ChequeController extends Controller
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$plan = $db->selectOne( $plan = $db->selectOne(
"SELECT id, member_id, number_of_months FROM installment_plans WHERE id = ? AND status = 'active'", "SELECT id, member_id, number_of_months, total_with_interest FROM installment_plans WHERE id = ? AND status = 'active'",
[(int) $planId] [(int) $planId]
); );
...@@ -118,6 +229,23 @@ class ChequeController extends Controller ...@@ -118,6 +229,23 @@ class ChequeController extends Controller
} }
} }
// Total-coverage validation: sum of existing + this cheque must cover plan total
if (empty($errors)) {
$existingTotal = (float) ($db->selectOne(
"SELECT COALESCE(SUM(cheque_amount), 0) as s FROM installment_cheques WHERE installment_plan_id = ?",
[(int) $planId]
)['s'] ?? 0);
$newTotal = $existingTotal + (float) $chequeAmount;
$isLastBatch = ($currentCount + 1) >= (int) $plan['number_of_months'];
if ($isLastBatch && $newTotal < (float) $plan['total_with_interest'] - 0.009) {
$errors[] = sprintf(
'إجمالي قيمة الشيكات بعد الإضافة (%.2f ج.م) أقل من إجمالي التقسيط (%.2f ج.م)',
$newTotal,
(float) $plan['total_with_interest']
);
}
}
if (!empty($errors)) { if (!empty($errors)) {
$session = App::getInstance()->session(); $session = App::getInstance()->session();
$alerts = array_map(fn($msg) => ['type' => 'error', 'message' => $msg], $errors); $alerts = array_map(fn($msg) => ['type' => 'error', 'message' => $msg], $errors);
......
...@@ -12,4 +12,5 @@ return [ ...@@ -12,4 +12,5 @@ return [
// Cheque uploads // Cheque uploads
['GET', '/installments/{planId}/cheques', 'Installments\Controllers\ChequeController@index', ['auth'], 'installment.view'], ['GET', '/installments/{planId}/cheques', 'Installments\Controllers\ChequeController@index', ['auth'], 'installment.view'],
['POST', '/installments/{planId}/cheques', 'Installments\Controllers\ChequeController@store', ['auth', 'csrf'], 'installment.create_plan'], ['POST', '/installments/{planId}/cheques', 'Installments\Controllers\ChequeController@store', ['auth', 'csrf'], 'installment.create_plan'],
['POST', '/installments/{planId}/cheques/batch', 'Installments\Controllers\ChequeController@storeBatch', ['auth', 'csrf'], 'installment.create_plan'],
]; ];
\ No newline at end of file
...@@ -10,6 +10,22 @@ use App\Core\EventBus; ...@@ -10,6 +10,22 @@ use App\Core\EventBus;
final class ChequeService final class ChequeService
{ {
/**
* Return the next sequential cheque number by inspecting all existing cheque_number values
* that are purely numeric. Falls back to 1 if none exist.
*/
public static function nextChequeNumber(): int
{
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT MAX(CAST(cheque_number AS UNSIGNED)) AS max_num
FROM installment_cheques
WHERE cheque_number REGEXP '^[0-9]+$'",
[]
);
return (int) ($row['max_num'] ?? 0) + 1;
}
/** /**
* Check if all required cheques have been uploaded for an installment plan. * Check if all required cheques have been uploaded for an installment plan.
*/ */
......
...@@ -8,7 +8,16 @@ ...@@ -8,7 +8,16 @@
<?php $__template->section('content'); ?> <?php $__template->section('content'); ?>
<!-- Status Banner --> <?php
$planTotal = (float) ($planTotal ?? 0);
$uploadedTotal= (float) ($uploadedTotal ?? 0);
$nextNum = (int) ($nextChequeNum ?? 1);
$monthlyPay = (float) ($plan['monthly_payment'] ?? 0);
$uncoveredAmt = max(0, round($planTotal - $uploadedTotal, 2));
$coveragePct = $planTotal > 0 ? min(100, round($uploadedTotal / $planTotal * 100)) : 100;
?>
<!-- ═══ Status Banner ══════════════════════════════════════════════════════ -->
<?php if ($allSubmitted && $plan['member_status'] === 'active'): ?> <?php if ($allSubmitted && $plan['member_status'] === 'active'): ?>
<div style="background:#DCFCE7;border:1px solid #86EFAC;border-radius:8px;padding:15px 20px;margin-bottom:20px;display:flex;align-items:center;gap:10px;"> <div style="background:#DCFCE7;border:1px solid #86EFAC;border-radius:8px;padding:15px 20px;margin-bottom:20px;display:flex;align-items:center;gap:10px;">
<i data-lucide="check-circle" style="width:24px;height:24px;color:#16A34A;"></i> <i data-lucide="check-circle" style="width:24px;height:24px;color:#16A34A;"></i>
...@@ -27,44 +36,147 @@ ...@@ -27,44 +36,147 @@
</div> </div>
<?php endif; ?> <?php endif; ?>
<!-- Progress --> <!-- ═══ KPI Row ═════════════════════════════════════════════════════════════ -->
<div class="card" style="padding:20px;margin-bottom:20px;"> <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;"> <div class="card" style="padding:14px 16px;text-align:center;border-right:4px solid #7C3AED;">
<span style="font-size:14px;font-weight:600;color:#374151;">التقدم</span> <div style="font-size:11px;color:#6B7280;margin-bottom:4px;">إجمالي التقسيط</div>
<span style="font-size:14px;color:#6B7280;"><?= $uploadedCount ?> / <?= $requiredCount ?></span> <div style="font-size:17px;font-weight:800;color:#7C3AED;"><?= money($planTotal) ?></div>
</div>
<div class="card" style="padding:14px 16px;text-align:center;border-right:4px solid #059669;">
<div style="font-size:11px;color:#6B7280;margin-bottom:4px;">إجمالي الشيكات المُسلمة</div>
<div style="font-size:17px;font-weight:800;color:#059669;"><?= money($uploadedTotal) ?></div>
</div>
<div class="card" style="padding:14px 16px;text-align:center;border-right:4px solid <?= $uncoveredAmt > 0 ? '#DC2626' : '#059669' ?>;">
<div style="font-size:11px;color:#6B7280;margin-bottom:4px;">الفرق غير المغطى</div>
<div style="font-size:17px;font-weight:800;color:<?= $uncoveredAmt > 0 ? '#DC2626' : '#059669' ?>;">
<?= $uncoveredAmt > 0 ? money($uncoveredAmt) : '✅ مغطى' ?>
</div>
</div>
<div class="card" style="padding:14px 16px;text-align:center;border-right:4px solid #0284C7;">
<div style="font-size:11px;color:#6B7280;margin-bottom:4px;">الشيكات <?= $uploadedCount ?> / <?= $requiredCount ?></div>
<div style="font-size:17px;font-weight:800;color:#0284C7;"><?= $coveragePct ?>%</div>
<div style="background:#E5E7EB;border-radius:99px;height:6px;margin-top:6px;overflow:hidden;">
<div style="height:100%;background:<?= $coveragePct>=100?'#059669':'#0284C7' ?>;width:<?= $coveragePct ?>%;border-radius:99px;"></div>
</div>
</div>
</div>
<?php if ($remainingCount > 0): ?>
<!-- ═══ Auto-Generator ══════════════════════════════════════════════════════ -->
<div class="card" style="margin-bottom:20px;border:2px solid #0D7377;">
<div style="padding:14px 20px;background:#F0FDFA;border-bottom:2px solid #0D7377;display:flex;align-items:center;gap:8px;">
<i data-lucide="zap" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">مولّد الشيكات التلقائي</h3>
<span style="margin-right:auto;font-size:12px;color:#6B7280;">اكتب قيمة الشيك والبنك → الجهاز يولّد العدد المطلوب تلقائياً</span>
</div>
<div style="padding:20px;">
<!-- Generator inputs -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr auto;gap:12px;align-items:end;margin-bottom:14px;">
<div>
<label style="font-size:12px;color:#374151;font-weight:600;display:block;margin-bottom:4px;">قيمة كل شيك <span style="color:#DC2626;">*</span></label>
<input type="number" id="gen_amount" class="form-input" step="0.01" min="0.01"
value="<?= number_format($monthlyPay, 2, '.', '') ?>"
placeholder="0.00" oninput="genPreview()" style="direction:ltr;text-align:left;">
</div>
<div>
<label style="font-size:12px;color:#374151;font-weight:600;display:block;margin-bottom:4px;">البنك الافتراضي <span style="color:#DC2626;">*</span></label>
<input type="text" id="gen_bank" class="form-input" placeholder="البنك الأهلي المصري" oninput="genPreview()">
</div>
<div>
<label style="font-size:12px;color:#374151;font-weight:600;display:block;margin-bottom:4px;">تاريخ أول شيك <span style="color:#DC2626;">*</span></label>
<input type="date" id="gen_start_date" class="form-input"
value="<?= e(date('Y-m-d')) ?>" oninput="genPreview()">
</div>
<div>
<button type="button" class="btn btn-primary" onclick="generateCheques()"
style="background:#0D7377;border-color:#0D7377;white-space:nowrap;">
⚡ توليد الشيكات
</button>
</div>
</div>
<!-- Generator preview -->
<div id="genPreviewBox" style="display:none;padding:10px 14px;background:#EFF6FF;border:1px solid #BFDBFE;border-radius:8px;font-size:13px;margin-bottom:12px;">
سيتم توليد <strong id="genCount"></strong> شيك |
قيمة كل شيك: <strong id="genAmtDisplay"></strong> |
إجمالي: <strong id="genTotalDisplay"></strong> |
أرقام الشيكات: <strong id="genNumRange"></strong>
</div>
<!-- Generated editable table -->
<div id="genTableWrap" style="display:none;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<span style="font-size:13px;font-weight:600;color:#374151;">الشيكات المولّدة — قابلة للتعديل قبل الحفظ</span>
<button type="button" onclick="clearGenerated()" style="background:none;border:none;color:#DC2626;cursor:pointer;font-size:12px;">✕ مسح الكل</button>
</div>
<!-- Live total validation bar -->
<div id="batchValidBar" style="display:none;padding:8px 12px;border-radius:6px;font-size:13px;margin-bottom:10px;"></div>
<div class="table-responsive" style="max-height:420px;overflow-y:auto;">
<table style="width:100%;border-collapse:collapse;font-size:13px;" id="genTable">
<thead style="position:sticky;top:0;background:#F9FAFB;z-index:1;">
<tr style="border-bottom:2px solid #E5E7EB;">
<th style="padding:8px 10px;text-align:right;color:#6B7280;">#</th>
<th style="padding:8px 10px;text-align:right;color:#6B7280;">رقم الشيك</th>
<th style="padding:8px 10px;text-align:right;color:#6B7280;">البنك</th>
<th style="padding:8px 10px;text-align:right;color:#6B7280;">التاريخ</th>
<th style="padding:8px 10px;text-align:right;color:#6B7280;">المبلغ</th>
<th style="padding:8px 10px;text-align:right;color:#6B7280;">ملاحظات</th>
<th style="padding:8px 10px;"></th>
</tr>
</thead>
<tbody id="genTableBody"></tbody>
<tfoot>
<tr style="border-top:2px solid #E5E7EB;background:#F9FAFB;font-weight:700;">
<td colspan="4" style="padding:8px 10px;color:#374151;">الإجمالي</td>
<td style="padding:8px 10px;" id="batchTotalCell"></td>
<td colspan="2"></td>
</tr>
</tfoot>
</table>
</div>
<!-- Batch submit form -->
<form id="batchForm" method="POST" action="/installments/<?= (int) $plan['id'] ?>/cheques/batch" novalidate>
<?= csrf_field() ?>
<div id="batchHiddenInputs"></div>
<div style="margin-top:14px;display:flex;align-items:center;gap:12px;">
<button type="submit" id="batchSubmitBtn" class="btn btn-primary"
style="background:#0D7377;border-color:#0D7377;" disabled>
💾 حفظ جميع الشيكات
</button>
<span id="batchSubmitNote" style="font-size:12px;color:#DC2626;"></span>
</div>
</form>
</div> </div>
<div style="background:#E5E7EB;border-radius:999px;height:10px;overflow:hidden;">
<?php $pct = $requiredCount > 0 ? round(($uploadedCount / $requiredCount) * 100) : 0; ?>
<div style="background:<?= $pct >= 100 ? '#16A34A' : '#0D7377' ?>;height:100%;width:<?= $pct ?>%;transition:width 0.3s;border-radius:999px;"></div>
</div> </div>
</div> </div>
<div style="display:grid;grid-template-columns:<?= $remainingCount > 0 ? '1fr 1fr' : '1fr' ?>;gap:20px;"> <!-- ═══ Single-Upload Form ═══════════════════════════════════════════════════ -->
<!-- Upload Form --> <div class="card" style="margin-bottom:20px;">
<?php if ($remainingCount > 0): ?> <div style="padding:14px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="upload" style="width:18px;height:18px;color:#0D7377;"></i> <i data-lucide="upload" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">رفع شيك جديد</h3> <h3 style="margin:0;color:#0D7377;font-size:15px;">رفع شيك مفرد (مع صورة)</h3>
</div> </div>
<form method="POST" action="/installments/<?= (int) $plan['id'] ?>/cheques" enctype="multipart/form-data" style="padding:20px;"> <form method="POST" action="/installments/<?= (int) $plan['id'] ?>/cheques" enctype="multipart/form-data" style="padding:20px;" novalidate>
<?= csrf_field() ?> <?= csrf_field() ?>
<div class="form-group" style="margin-bottom:15px;"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:15px;">
<div class="form-group">
<label class="form-label">رقم الشيك <span style="color:#DC2626;">*</span></label> <label class="form-label">رقم الشيك <span style="color:#DC2626;">*</span></label>
<input type="text" name="cheque_number" class="form-input" required maxlength="50" value="<?= e(old('cheque_number') ?? '') ?>" placeholder="مثال: 123456789"> <input type="text" name="cheque_number" class="form-input" required maxlength="50" value="<?= e(old('cheque_number') ?? $nextNum) ?>" placeholder="مثال: <?= $nextNum ?>">
</div> </div>
<div class="form-group" style="margin-bottom:15px;"> <div class="form-group">
<label class="form-label">البنك <span style="color:#DC2626;">*</span></label> <label class="form-label">البنك <span style="color:#DC2626;">*</span></label>
<input type="text" name="bank_name" class="form-input" required maxlength="200" value="<?= e(old('bank_name') ?? '') ?>" placeholder="مثال: البنك الأهلي المصري"> <input type="text" name="bank_name" class="form-input" required maxlength="200" value="<?= e(old('bank_name') ?? '') ?>" placeholder="البنك الأهلي المصري">
</div> </div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:15px;">
<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></label>
<input type="date" name="cheque_date" class="form-input" required value="<?= e(old('cheque_date') ?? '') ?>"> <input type="date" name="cheque_date" class="form-input" required value="<?= e(old('cheque_date') ?? '') ?>">
</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></label>
<input type="number" name="cheque_amount" class="form-input" required step="0.01" min="0.01" value="<?= e(old('cheque_amount') ?? $plan['monthly_payment'] ?? '') ?>" placeholder="0.00"> <input type="number" name="cheque_amount" class="form-input" required step="0.01" min="0.01" value="<?= e(old('cheque_amount') ?? number_format($monthlyPay, 2, '.', '')) ?>" style="direction:ltr;text-align:left;">
</div> </div>
</div> </div>
<div class="form-group" style="margin-bottom:15px;"> <div class="form-group" style="margin-bottom:15px;">
...@@ -78,34 +190,70 @@ ...@@ -78,34 +190,70 @@
</div> </div>
<button type="submit" class="btn btn-primary" style="width:100%;"><i data-lucide="upload" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> رفع الشيك</button> <button type="submit" class="btn btn-primary" style="width:100%;"><i data-lucide="upload" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> رفع الشيك</button>
</form> </form>
</div> </div>
<?php endif; ?> <?php endif; ?>
<!-- Uploaded Cheques --> <!-- ═══ Existing Cheques ══════════════════════════════════════════════════════ -->
<div class="card"> <div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;"> <div style="padding:14px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<i data-lucide="file-check" style="width:18px;height:18px;color:#059669;"></i> <i data-lucide="file-check" style="width:18px;height:18px;color:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">الشيكات المُسلمة (<?= $uploadedCount ?>)</h3> <h3 style="margin:0;color:#059669;font-size:15px;">الشيكات المُسلمة (<?= $uploadedCount ?>)</h3>
</div> </div>
<?php if ($uploadedTotal > 0): ?>
<div style="font-size:13px;">
الإجمالي: <strong style="color:<?= $uploadedTotal >= $planTotal - 0.009 ? '#059669' : '#DC2626' ?>;"><?= money($uploadedTotal) ?></strong>
/ <?= money($planTotal) ?>
<?php if ($uploadedTotal < $planTotal - 0.009): ?>
<span style="color:#DC2626;font-weight:700;margin-right:6px;">⚠️ غير مكتمل</span>
<?php else: ?>
<span style="color:#059669;font-weight:700;margin-right:6px;">✅ مغطى بالكامل</span>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php if (!empty($cheques)): ?> <?php if (!empty($cheques)): ?>
<div class="table-responsive"> <div class="table-responsive">
<table class="data-table"> <table class="data-table">
<thead> <thead>
<tr><th>#</th><th>رقم الشيك</th><th>البنك</th><th>التاريخ</th><th>المبلغ</th><th>الملف</th><th>رفع بواسطة</th></tr> <tr>
<th>#</th>
<th>رقم الشيك</th>
<th>البنك</th>
<th>التاريخ</th>
<th>المبلغ</th>
<th>الملف</th>
<th>رفع بواسطة</th>
</tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($cheques as $i => $chq): ?> <?php foreach ($cheques as $i => $chq): ?>
<tr> <tr>
<td><?= $i + 1 ?></td> <td style="font-weight:600;"><?= $i + 1 ?></td>
<td style="font-weight:600;"><code style="background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($chq['cheque_number']) ?></code></td> <td><code style="background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($chq['cheque_number']) ?></code></td>
<td><?= e($chq['bank_name']) ?></td> <td><?= e($chq['bank_name']) ?></td>
<td><?= e($chq['cheque_date']) ?></td> <td style="white-space:nowrap;"><?= e($chq['cheque_date']) ?></td>
<td style="font-weight:600;color:#059669;"><?= money($chq['cheque_amount']) ?></td> <td style="font-weight:700;color:#059669;"><?= money($chq['cheque_amount']) ?></td>
<td><a href="/<?= e($chq['scan_path']) ?>" target="_blank" style="color:#0D7377;"><i data-lucide="eye" style="width:14px;height:14px;vertical-align:middle;"></i> عرض</a></td> <td>
<?php if ($chq['scan_path']): ?>
<a href="/<?= e($chq['scan_path']) ?>" target="_blank" style="color:#0D7377;">
<i data-lucide="eye" style="width:14px;height:14px;vertical-align:middle;"></i> عرض
</a>
<?php else: ?>
<span style="color:#9CA3AF;font-size:12px;">بدون ملف</span>
<?php endif; ?>
</td>
<td style="font-size:12px;color:#6B7280;"><?= e($chq['uploaded_by_name'] ?? '—') ?></td> <td style="font-size:12px;color:#6B7280;"><?= e($chq['uploaded_by_name'] ?? '—') ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
<tfoot>
<tr style="background:#F9FAFB;font-weight:700;">
<td colspan="4" style="padding:8px 12px;color:#374151;">الإجمالي</td>
<td style="padding:8px 12px;color:#059669;"><?= money($uploadedTotal) ?></td>
<td colspan="2"></td>
</tr>
</tfoot>
</table> </table>
</div> </div>
<?php else: ?> <?php else: ?>
...@@ -114,8 +262,180 @@ ...@@ -114,8 +262,180 @@
<p style="color:#6B7280;margin:0;">لم يتم رفع أي شيكات بعد</p> <p style="color:#6B7280;margin:0;">لم يتم رفع أي شيكات بعد</p>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div>
</div> </div>
<script>document.addEventListener('DOMContentLoaded',function(){if(typeof lucide!=='undefined')lucide.createIcons();});</script> <script>
(function () {
// ── Constants injected from PHP ──────────────────────────────────────────
var PLAN_TOTAL = <?= json_encode(round($planTotal, 2)) ?>;
var ALREADY_UPLOADED = <?= json_encode(round($uploadedTotal, 2)) ?>;
var NEXT_NUM = <?= json_encode($nextNum) ?>;
var MONTHLY = <?= json_encode(round($monthlyPay, 2)) ?>;
var rows = []; // Array of row objects currently in the generated table
// ── Formatting helpers ──────────────────────────────────────────────────
function fmt(v) {
return parseFloat(v).toLocaleString('ar-EG', {minimumFractionDigits:2, maximumFractionDigits:2}) + ' ج.م';
}
function addMonths(dateStr, n) {
var d = new Date(dateStr);
d.setMonth(d.getMonth() + n);
return d.toISOString().slice(0,10);
}
// ── Preview how many cheques will be generated ─────────────────────────
window.genPreview = function () {
var amt = parseFloat(document.getElementById('gen_amount').value) || 0;
var bank = document.getElementById('gen_bank').value.trim();
var box = document.getElementById('genPreviewBox');
if (!amt || amt <= 0) { box.style.display='none'; return; }
var needed = Math.ceil((PLAN_TOTAL - ALREADY_UPLOADED) / amt);
if (needed <= 0) needed = 1;
var total = needed * amt;
var endNum = NEXT_NUM + needed - 1;
document.getElementById('genCount').textContent = needed;
document.getElementById('genAmtDisplay').textContent = fmt(amt);
document.getElementById('genTotalDisplay').textContent = fmt(total);
document.getElementById('genNumRange').textContent = NEXT_NUM + ' → ' + endNum;
box.style.display = '';
};
// ── Generate the editable rows ─────────────────────────────────────────
window.generateCheques = function () {
var amt = parseFloat(document.getElementById('gen_amount').value) || 0;
var bank = document.getElementById('gen_bank').value.trim();
var startDate = document.getElementById('gen_start_date').value;
if (!amt || amt <= 0) { alert('أدخل قيمة الشيك أولاً'); return; }
if (!bank) { alert('أدخل اسم البنك أولاً'); return; }
if (!startDate) { alert('أدخل تاريخ أول شيك'); return; }
var needed = Math.ceil((PLAN_TOTAL - ALREADY_UPLOADED) / amt);
if (needed <= 0) needed = 1;
rows = [];
for (var i = 0; i < needed; i++) {
rows.push({
cheque_number: String(NEXT_NUM + i),
bank_name: bank,
cheque_date: addMonths(startDate, i),
cheque_amount: (i === needed - 1)
? Math.round((PLAN_TOTAL - ALREADY_UPLOADED - amt * (needed - 1)) * 100) / 100
: amt,
notes: '',
});
}
renderTable();
document.getElementById('genTableWrap').style.display = '';
document.getElementById('genPreviewBox').style.display = 'none';
};
// ── Render the editable table ───────────────────────────────────────────
function renderTable() {
var tbody = document.getElementById('genTableBody');
tbody.innerHTML = '';
rows.forEach(function (row, idx) {
var tr = document.createElement('tr');
tr.style.borderBottom = '1px solid #F3F4F6';
tr.innerHTML =
'<td style="padding:6px 8px;color:#6B7280;width:36px;">' + (idx+1) + '</td>' +
'<td style="padding:4px 6px;"><input class="form-input" style="min-width:90px;" value="' + esc(row.cheque_number) + '" oninput="updateRow('+idx+',\'cheque_number\',this.value)"></td>' +
'<td style="padding:4px 6px;"><input class="form-input" style="min-width:140px;" value="' + esc(row.bank_name) + '" oninput="updateRow('+idx+',\'bank_name\',this.value)"></td>' +
'<td style="padding:4px 6px;"><input type="date" class="form-input" value="' + esc(row.cheque_date) + '" oninput="updateRow('+idx+',\'cheque_date\',this.value)"></td>' +
'<td style="padding:4px 6px;"><input type="number" class="form-input" step="0.01" style="min-width:100px;direction:ltr;text-align:left;" value="' + row.cheque_amount + '" oninput="updateRow('+idx+',\'cheque_amount\',parseFloat(this.value)||0)"></td>' +
'<td style="padding:4px 6px;"><input class="form-input" style="min-width:100px;" value="' + esc(row.notes) + '" oninput="updateRow('+idx+',\'notes\',this.value)" placeholder="اختياري"></td>' +
'<td style="padding:4px 6px;"><button type="button" onclick="removeRow('+idx+')" style="background:none;border:none;color:#DC2626;cursor:pointer;font-size:16px;" title="حذف">✕</button></td>';
tbody.appendChild(tr);
});
updateValidation();
}
// ── Edit a row field ────────────────────────────────────────────────────
window.updateRow = function (idx, field, val) {
rows[idx][field] = val;
updateValidation();
};
// ── Remove a row ─────────────────────────────────────────────────────────
window.removeRow = function (idx) {
rows.splice(idx, 1);
renderTable();
};
// ── Clear all ────────────────────────────────────────────────────────────
window.clearGenerated = function () {
rows = [];
document.getElementById('genTableWrap').style.display = 'none';
};
// ── Live validation + footer total + submit button state ────────────────
function updateValidation() {
var batchTotal = rows.reduce(function(s, r){ return s + (parseFloat(r.cheque_amount)||0); }, 0);
batchTotal = Math.round(batchTotal * 100) / 100;
var grandTotal = Math.round((ALREADY_UPLOADED + batchTotal) * 100) / 100;
document.getElementById('batchTotalCell').textContent = fmt(batchTotal);
var bar = document.getElementById('batchValidBar');
var btn = document.getElementById('batchSubmitBtn');
var note = document.getElementById('batchSubmitNote');
var covered = grandTotal >= PLAN_TOTAL - 0.009;
bar.style.display = '';
if (covered) {
bar.style.background = '#DCFCE7';
bar.style.border = '1px solid #86EFAC';
bar.style.color = '#166534';
bar.innerHTML = '✅ إجمالي الشيكات (<strong>' + fmt(grandTotal) + '</strong>) يغطي إجمالي التقسيط (<strong>' + fmt(PLAN_TOTAL) + '</strong>)';
btn.disabled = false;
btn.style.opacity = '1';
note.textContent = '';
} else {
bar.style.background = '#FEF2F2';
bar.style.border = '1px solid #FECACA';
bar.style.color = '#991B1B';
var short = Math.round((PLAN_TOTAL - grandTotal) * 100) / 100;
bar.innerHTML = '⚠️ الإجمالي الحالي <strong>' + fmt(grandTotal) + '</strong> — ينقص <strong>' + fmt(short) + '</strong> لتغطية إجمالي التقسيط (<strong>' + fmt(PLAN_TOTAL) + '</strong>)';
btn.disabled = true;
btn.style.opacity = '0.5';
note.textContent = 'لا يمكن الحفظ حتى تتساوى القيمة أو تزيد عن إجمالي التقسيط';
}
// Rebuild hidden inputs for the form
buildHiddenInputs();
}
// ── Build hidden inputs mirroring the editable rows ────────────────────
function buildHiddenInputs() {
var container = document.getElementById('batchHiddenInputs');
container.innerHTML = '';
rows.forEach(function (row, idx) {
['cheque_number','bank_name','cheque_date','cheque_amount','notes'].forEach(function (field) {
var inp = document.createElement('input');
inp.type = 'hidden';
inp.name = 'cheques[' + idx + '][' + field + ']';
inp.value = row[field] ?? '';
container.appendChild(inp);
});
});
}
// ── HTML escape helper ──────────────────────────────────────────────────
function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// ── Init Lucide icons ────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', function () {
if (typeof lucide !== 'undefined') lucide.createIcons();
genPreview();
});
})();
</script>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
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