268 lines
12 KiB
PHP
268 lines
12 KiB
PHP
<?php
|
|
require_once 'includes/app.php';
|
|
$user = require_auth();
|
|
|
|
$activeNav = 'debts';
|
|
$pageTitle = tr('الديون والفواتير الآجلة', 'Debts & Unpaid Bills');
|
|
$pdo = db();
|
|
$debtsLoadError = '';
|
|
$unpaidSales = [];
|
|
$debtsByCustomer = [];
|
|
|
|
// Handle legacy mark-as-paid shortcut
|
|
if (isset($_GET['mark_paid'])) {
|
|
$id = (int) $_GET['mark_paid'];
|
|
try {
|
|
$sale = fetch_sale($id);
|
|
if ($sale) {
|
|
$summary = sale_payment_summary($sale);
|
|
if ($summary['due_amount'] > 0.0005) {
|
|
apply_sale_payment($id, $summary['due_amount'], true);
|
|
}
|
|
}
|
|
set_flash('success', tr('تم استلام المبلغ بنجاح.', 'Payment received successfully.'));
|
|
} catch (Throwable $e) {
|
|
set_flash('danger', tr('خطأ أثناء التحديث.', 'Error updating.'));
|
|
}
|
|
redirect_to('debts.php');
|
|
}
|
|
|
|
try {
|
|
$salesColumns = [];
|
|
foreach ($pdo->query('SHOW COLUMNS FROM sales_orders')->fetchAll(PDO::FETCH_ASSOC) as $column) {
|
|
$salesColumns[$column['Field']] = true;
|
|
}
|
|
|
|
$hasCustomersTable = false;
|
|
$customerColumns = [];
|
|
$customersTable = $pdo->query("SHOW TABLES LIKE 'customers'");
|
|
if ($customersTable && $customersTable->fetchColumn()) {
|
|
$hasCustomersTable = true;
|
|
foreach ($pdo->query('SHOW COLUMNS FROM customers')->fetchAll(PDO::FETCH_ASSOC) as $column) {
|
|
$customerColumns[$column['Field']] = true;
|
|
}
|
|
}
|
|
|
|
$selectParts = [
|
|
's.id',
|
|
's.receipt_no',
|
|
's.sale_date',
|
|
's.total_amount',
|
|
's.payment_status',
|
|
'COALESCE(s.paid_amount, 0) AS paid_amount',
|
|
'COALESCE(s.due_amount, s.total_amount) AS due_amount',
|
|
isset($salesColumns['customer_id']) ? 's.customer_id' : 'NULL AS customer_id',
|
|
isset($salesColumns['customer_name']) ? 's.customer_name' : 'NULL AS customer_name',
|
|
'NULL AS c_name',
|
|
'NULL AS c_phone',
|
|
];
|
|
|
|
$joinSql = '';
|
|
if ($hasCustomersTable && isset($salesColumns['customer_id']) && isset($customerColumns['id'])) {
|
|
$joinSql = ' LEFT JOIN customers c ON s.customer_id = c.id ';
|
|
if (isset($customerColumns['name'])) {
|
|
$selectParts[9] = 'c.name AS c_name';
|
|
}
|
|
if (isset($customerColumns['phone'])) {
|
|
$selectParts[10] = 'c.phone AS c_phone';
|
|
}
|
|
}
|
|
|
|
if (isset($salesColumns['payment_status'])) {
|
|
$whereSql = " WHERE s.payment_status IN ('unpaid', 'partial')";
|
|
} elseif (isset($salesColumns['payment_method'])) {
|
|
$whereSql = " WHERE s.payment_method = 'pay_later'";
|
|
} else {
|
|
$whereSql = ' WHERE 1 = 0';
|
|
}
|
|
|
|
$sqlUnpaid = 'SELECT ' . implode(', ', $selectParts)
|
|
. ' FROM sales_orders s'
|
|
. $joinSql
|
|
. $whereSql
|
|
. ' ORDER BY s.sale_date DESC';
|
|
$stmtUnpaid = $pdo->query($sqlUnpaid);
|
|
$unpaidSales = $stmtUnpaid ? $stmtUnpaid->fetchAll(PDO::FETCH_ASSOC) : [];
|
|
} catch (Throwable $e) {
|
|
$debtsLoadError = tr(
|
|
'تعذر تحميل صفحة الديون بسبب اختلاف في هيكل قاعدة البيانات. احفظ التعديلات ثم أنشئ نسخة جديدة وأعد التحديث.',
|
|
'The debts page could not load because the deployed database schema is older than the current code. Save these changes, create a new version, then refresh the page.'
|
|
);
|
|
}
|
|
|
|
// Aggregate by customer
|
|
foreach ($unpaidSales as $sale) {
|
|
$cId = $sale['customer_id'] ?? 'unknown';
|
|
if (!isset($debtsByCustomer[$cId])) {
|
|
$debtsByCustomer[$cId] = [
|
|
'name' => $sale['c_name'] ?: $sale['customer_name'] ?: tr('عميل غير معروف', 'Unknown Customer'),
|
|
'phone' => $sale['c_phone'] ?: '',
|
|
'total' => 0.0,
|
|
'open_invoices' => 0,
|
|
'partial_invoices' => 0
|
|
];
|
|
}
|
|
$saleSummary = sale_payment_summary($sale);
|
|
$debtsByCustomer[$cId]['total'] += (float) $saleSummary['due_amount'];
|
|
$debtsByCustomer[$cId]['open_invoices'] += 1;
|
|
if ($saleSummary['payment_status'] === 'partial') {
|
|
$debtsByCustomer[$cId]['partial_invoices'] += 1;
|
|
}
|
|
}
|
|
|
|
// Sort by highest debt
|
|
uasort($debtsByCustomer, fn($a, $b) => $b['total'] <=> $a['total']);
|
|
|
|
require_once 'includes/header.php';
|
|
?>
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h1 class="h3 mb-0 text-gray-800"><?= h($pageTitle) ?></h1>
|
|
</div>
|
|
|
|
<?php if ($debtsLoadError !== ''): ?>
|
|
<div class="alert alert-warning" role="alert">
|
|
<?= h($debtsLoadError) ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="row">
|
|
<!-- Debts by Customer -->
|
|
<div class="col-lg-4 mb-4">
|
|
<div class="card shadow-sm border-0 h-100">
|
|
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
|
|
<h6 class="m-0 font-weight-bold text-primary"><i class="bi bi-people"></i> <?= h(tr('الديون حسب العميل', 'Debts by Customer')) ?></h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<?php if (empty($debtsByCustomer)): ?>
|
|
<div class="text-center text-muted py-4"><?= h(tr('لا توجد ديون مسجلة.', 'No debts recorded.')) ?></div>
|
|
<?php else: ?>
|
|
<ul class="list-group list-group-flush">
|
|
<?php foreach ($debtsByCustomer as $debt): ?>
|
|
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
|
|
<div>
|
|
<strong><?= h($debt['name']) ?></strong>
|
|
<?php if ($debt['phone']): ?>
|
|
<div class="small text-muted" dir="ltr"><?= h(phone_display($debt['phone'])) ?></div>
|
|
<?php endif; ?>
|
|
<div class="small text-muted"><?= h($debt['open_invoices']) ?> <?= h(tr('فواتير مفتوحة', 'open invoices')) ?></div>
|
|
<?php if ($debt['partial_invoices'] > 0): ?>
|
|
<div class="small text-warning"><?= h($debt['partial_invoices']) ?> <?= h(tr('منها دفعات جزئية', 'partially paid')) ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<span class="badge bg-danger rounded-pill fs-6"><?= h(currency($debt['total'])) ?></span>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Unpaid Invoices -->
|
|
<div class="col-lg-8 mb-4">
|
|
<div class="card shadow-sm border-0 h-100">
|
|
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
|
|
<h6 class="m-0 font-weight-bold text-primary"><i class="bi bi-receipt"></i> <?= h(tr('الفواتير غير المدفوعة والجزئية', 'Unpaid & Partial Invoices')) ?></h6>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th><?= h(tr('رقم الفاتورة', 'Receipt No')) ?></th>
|
|
<th><?= h(tr('العميل', 'Customer')) ?></th>
|
|
<th><?= h(tr('التاريخ', 'Date')) ?></th>
|
|
<th><?= h(tr('الإجمالي', 'Total')) ?></th>
|
|
<th><?= h(tr('المدفوع', 'Paid')) ?></th>
|
|
<th><?= h(tr('المتبقي', 'Due')) ?></th>
|
|
<th><?= h(tr('الحالة', 'Status')) ?></th>
|
|
<th><?= h(tr('الإجراء', 'Action')) ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($unpaidSales)): ?>
|
|
<tr>
|
|
<td colspan="8" class="text-center py-4 text-muted"><?= h(tr('لا توجد فواتير غير مدفوعة أو جزئية.', 'No unpaid or partial invoices.')) ?></td>
|
|
</tr>
|
|
<?php else: ?>
|
|
<?php foreach ($unpaidSales as $sale): ?>
|
|
<?php $paymentSummary = sale_payment_summary($sale); ?>
|
|
<tr>
|
|
<td>
|
|
<a href="<?= h(url_for('sale.php', ['id' => $sale['id']])) ?>" class="fw-bold text-decoration-none">
|
|
<?= h($sale['receipt_no']) ?>
|
|
</a>
|
|
</td>
|
|
<td><?= h($sale['c_name'] ?: $sale['customer_name'] ?: '-') ?></td>
|
|
<td><?= h(date('Y-m-d', strtotime((string)$sale['sale_date']))) ?></td>
|
|
<td class="fw-semibold"><?= h(currency((float)$sale['total_amount'])) ?></td>
|
|
<td class="text-primary"><?= h(currency((float)$paymentSummary['paid_amount'])) ?></td>
|
|
<td class="fw-bold text-danger"><?= h(currency((float)$paymentSummary['due_amount'])) ?></td>
|
|
<td>
|
|
<span class="badge <?= h(payment_status_badge_class($paymentSummary['payment_status'])) ?>"><?= h(payment_status_label($paymentSummary['payment_status'])) ?></span>
|
|
</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="receivePayment(<?= (int) $sale['id'] ?>, <?= json_encode((float) $paymentSummary['due_amount']) ?>, <?= ($sale['status'] ?? 'completed') === 'order' ? 'true' : 'false' ?>)">
|
|
<i class="bi bi-cash-coin"></i> <?= h(tr('استلام دفعة', 'Receive Payment')) ?>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
async function receivePayment(id, dueAmount, completeOrder = false) {
|
|
const { value: paymentAmount } = await Swal.fire({
|
|
title: '<?= h(tr('استلام دفعة', 'Receive Payment')) ?>',
|
|
text: '<?= h(tr('أدخل المبلغ المستلم لهذه الفاتورة.', 'Enter the amount received for this invoice.')) ?>',
|
|
input: 'number',
|
|
inputAttributes: { min: '0.001', step: '0.001', max: String(dueAmount) },
|
|
inputValue: Number(dueAmount).toFixed(3),
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#198754',
|
|
confirmButtonText: '<?= h(tr('حفظ الدفعة', 'Save Payment')) ?>',
|
|
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>',
|
|
inputValidator: (value) => {
|
|
const amount = parseFloat(value || '0');
|
|
if (!amount || amount <= 0) {
|
|
return '<?= h(tr('أدخل مبلغاً صحيحاً.', 'Enter a valid amount.')) ?>';
|
|
}
|
|
if (amount - dueAmount > 0.0005) {
|
|
return '<?= h(tr('المبلغ لا يمكن أن يتجاوز المتبقي.', 'Amount cannot exceed the due balance.')) ?>';
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
|
|
if (!paymentAmount) {
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('sale_id', String(id));
|
|
formData.append('payment_amount', String(paymentAmount));
|
|
if (completeOrder) {
|
|
formData.append('complete_order', '1');
|
|
}
|
|
|
|
const response = await fetch('api/sales_payment.php', { method: 'POST', body: formData });
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
await Swal.fire({ icon: 'success', text: data.message, confirmButtonText: 'OK' });
|
|
window.location.reload();
|
|
} else {
|
|
Swal.fire({ icon: 'error', text: data.error || '<?= h(tr('تعذر تسجيل الدفعة.', 'Could not record the payment.')) ?>' });
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<?php require_once 'includes/footer.php'; ?>
|