39728-vm/debts.php
2026-04-23 04:43:40 +00:00

289 lines
14 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('الهاتف', 'Phone')) ?></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="9" 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>
<?php
$rawCustomerName = (string) ($sale['c_name'] ?: $sale['customer_name'] ?: '-');
$displayPhone = '';
if (str_contains($rawCustomerName, ' - ')) {
$parts = explode(' - ', $rawCustomerName);
$lastPart = trim(end($parts));
if (preg_match('/^[0-9+\s]+$/', $lastPart)) {
$displayPhone = $lastPart;
array_pop($parts);
$rawCustomerName = trim(implode(' - ', $parts));
}
}
$displayPhone = ltrim(preg_replace('/[^0-9]/', '', $displayPhone), '0');
if ($displayPhone !== '') {
if (str_starts_with($displayPhone, '968') && strlen($displayPhone) > 8) {
$displayPhone = substr($displayPhone, 3);
}
}
?>
<td><?= h($rawCustomerName) ?></td>
<td dir="ltr"><?= h($displayPhone ?: '-') ?></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'; ?>