Autosave: 20260504-045808
This commit is contained in:
parent
07894c8f77
commit
8df558e09d
221
debts.php
221
debts.php
@ -8,6 +8,11 @@ $pdo = db();
|
||||
$debtsLoadError = '';
|
||||
$unpaidSales = [];
|
||||
$debtsByCustomer = [];
|
||||
$customerNameFilter = trim((string) ($_GET['customer_name'] ?? ''));
|
||||
$phoneFilter = trim((string) ($_GET['phone'] ?? ''));
|
||||
$dateFrom = trim((string) ($_GET['date_from'] ?? ''));
|
||||
$dateTo = trim((string) ($_GET['date_to'] ?? ''));
|
||||
$hasFilters = $customerNameFilter !== '' || $phoneFilter !== '' || $dateFrom !== '' || $dateTo !== '';
|
||||
|
||||
// Handle legacy mark-as-paid shortcut
|
||||
if (isset($_GET['mark_paid'])) {
|
||||
@ -53,6 +58,7 @@ try {
|
||||
'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',
|
||||
isset($salesColumns['status']) ? "COALESCE(s.status, 'completed') AS status" : "'completed' AS status",
|
||||
'NULL AS c_name',
|
||||
'NULL AS c_phone',
|
||||
];
|
||||
@ -61,28 +67,95 @@ try {
|
||||
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';
|
||||
$selectParts[10] = 'c.name AS c_name';
|
||||
}
|
||||
if (isset($customerColumns['phone'])) {
|
||||
$selectParts[10] = 'c.phone AS c_phone';
|
||||
$selectParts[11] = 'c.phone AS c_phone';
|
||||
}
|
||||
}
|
||||
|
||||
$params = [];
|
||||
$whereParts = [];
|
||||
if (isset($salesColumns['payment_status'])) {
|
||||
$whereSql = " WHERE s.payment_status IN ('unpaid', 'partial')";
|
||||
$whereParts[] = "s.payment_status IN ('unpaid', 'partial')";
|
||||
} elseif (isset($salesColumns['payment_method'])) {
|
||||
$whereSql = " WHERE s.payment_method = 'pay_later'";
|
||||
$whereParts[] = "s.payment_method = 'pay_later'";
|
||||
} else {
|
||||
$whereSql = ' WHERE 1 = 0';
|
||||
$whereParts[] = '1 = 0';
|
||||
}
|
||||
|
||||
if ($customerNameFilter !== '') {
|
||||
$nameConditions = [];
|
||||
if ($hasCustomersTable && isset($customerColumns['name'])) {
|
||||
$nameConditions[] = 'c.name LIKE :customer_name_customer';
|
||||
$params[':customer_name_customer'] = '%' . $customerNameFilter . '%';
|
||||
}
|
||||
if (isset($salesColumns['customer_name'])) {
|
||||
$nameConditions[] = 's.customer_name LIKE :customer_name_sale';
|
||||
$params[':customer_name_sale'] = '%' . $customerNameFilter . '%';
|
||||
}
|
||||
if ($nameConditions !== []) {
|
||||
$whereParts[] = '(' . implode(' OR ', $nameConditions) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
if ($phoneFilter !== '') {
|
||||
$rawDigits = phone_digits($phoneFilter);
|
||||
$normalizedPhone = normalize_oman_phone($phoneFilter);
|
||||
$phoneVariants = [];
|
||||
if ($rawDigits !== '') {
|
||||
$phoneVariants[] = $rawDigits;
|
||||
}
|
||||
if ($normalizedPhone !== '') {
|
||||
$phoneVariants[] = $normalizedPhone;
|
||||
$phoneVariants[] = '0' . $normalizedPhone;
|
||||
$phoneVariants[] = '968' . $normalizedPhone;
|
||||
$phoneVariants[] = '00968' . $normalizedPhone;
|
||||
}
|
||||
$phoneVariants = array_values(array_unique(array_filter($phoneVariants, static fn($value) => $value !== '')));
|
||||
|
||||
$phoneColumns = [];
|
||||
if ($hasCustomersTable && isset($customerColumns['phone'])) {
|
||||
$phoneColumns[] = "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(c.phone, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '')";
|
||||
}
|
||||
if (isset($salesColumns['customer_name'])) {
|
||||
$phoneColumns[] = "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(s.customer_name, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '')";
|
||||
}
|
||||
|
||||
if ($phoneColumns !== [] && $phoneVariants !== []) {
|
||||
$phoneConditions = [];
|
||||
foreach ($phoneColumns as $columnIndex => $columnExpression) {
|
||||
foreach ($phoneVariants as $variantIndex => $variant) {
|
||||
$paramKey = ':phone_' . $columnIndex . '_' . $variantIndex;
|
||||
$phoneConditions[] = $columnExpression . ' LIKE ' . $paramKey;
|
||||
$params[$paramKey] = '%' . $variant . '%';
|
||||
}
|
||||
}
|
||||
$whereParts[] = '(' . implode(' OR ', $phoneConditions) . ')';
|
||||
}
|
||||
}
|
||||
|
||||
if ($dateFrom !== '') {
|
||||
$whereParts[] = 'DATE(s.sale_date) >= :date_from';
|
||||
$params[':date_from'] = $dateFrom;
|
||||
}
|
||||
if ($dateTo !== '') {
|
||||
$whereParts[] = 'DATE(s.sale_date) <= :date_to';
|
||||
$params[':date_to'] = $dateTo;
|
||||
}
|
||||
|
||||
$whereSql = ' WHERE ' . implode(' AND ', $whereParts);
|
||||
$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) : [];
|
||||
$stmtUnpaid = $pdo->prepare($sqlUnpaid);
|
||||
foreach ($params as $key => $value) {
|
||||
$stmtUnpaid->bindValue($key, $value);
|
||||
}
|
||||
$stmtUnpaid->execute();
|
||||
$unpaidSales = $stmtUnpaid->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable $e) {
|
||||
$debtsLoadError = tr(
|
||||
'تعذر تحميل صفحة الديون بسبب اختلاف في هيكل قاعدة البيانات. احفظ التعديلات ثم أنشئ نسخة جديدة وأعد التحديث.',
|
||||
@ -90,16 +163,48 @@ try {
|
||||
);
|
||||
}
|
||||
|
||||
$extractCustomerContact = static function (array $sale): array {
|
||||
$sourceName = trim((string) ($sale['customer_name'] ?? ''));
|
||||
$displayName = trim((string) ($sale['c_name'] ?? ''));
|
||||
if ($displayName === '') {
|
||||
$displayName = $sourceName;
|
||||
}
|
||||
|
||||
$displayPhone = trim((string) ($sale['c_phone'] ?? ''));
|
||||
if ($displayPhone === '' && str_contains($sourceName, ' - ')) {
|
||||
$parts = explode(' - ', $sourceName);
|
||||
$lastPart = trim((string) end($parts));
|
||||
if (preg_match('/^[0-9+\s]+$/', $lastPart)) {
|
||||
$displayPhone = $lastPart;
|
||||
array_pop($parts);
|
||||
if ($displayName === '' || $displayName === $sourceName) {
|
||||
$displayName = trim(implode(' - ', $parts));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$displayPhone = phone_display($displayPhone);
|
||||
if ($displayName === '') {
|
||||
$displayName = tr('عميل غير معروف', 'Unknown Customer');
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $displayName,
|
||||
'phone' => $displayPhone,
|
||||
];
|
||||
};
|
||||
|
||||
// Aggregate by customer
|
||||
foreach ($unpaidSales as $sale) {
|
||||
$customerContact = $extractCustomerContact($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'] ?: '',
|
||||
'name' => $customerContact['name'],
|
||||
'phone' => $customerContact['phone'],
|
||||
'total' => 0.0,
|
||||
'open_invoices' => 0,
|
||||
'partial_invoices' => 0
|
||||
'partial_invoices' => 0,
|
||||
];
|
||||
}
|
||||
$saleSummary = sale_payment_summary($sale);
|
||||
@ -126,6 +231,60 @@ require_once 'includes/header.php';
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<form method="GET" action="debts.php" class="row g-3 align-items-end">
|
||||
<input type="hidden" name="lang" value="<?= h(current_lang()) ?>">
|
||||
<div class="col-12 col-md-6 col-xl-3">
|
||||
<label class="form-label mb-1" for="debts-customer-name"><?= h(tr('اسم العميل', 'Customer name')) ?></label>
|
||||
<input
|
||||
id="debts-customer-name"
|
||||
type="text"
|
||||
name="customer_name"
|
||||
class="form-control"
|
||||
value="<?= h($customerNameFilter) ?>"
|
||||
placeholder="<?= h(tr('ابحث باسم العميل', 'Search by customer name')) ?>"
|
||||
>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-3">
|
||||
<label class="form-label mb-1" for="debts-phone"><?= h(tr('رقم الهاتف', 'Phone number')) ?></label>
|
||||
<input
|
||||
id="debts-phone"
|
||||
type="text"
|
||||
name="phone"
|
||||
class="form-control"
|
||||
value="<?= h($phoneFilter) ?>"
|
||||
placeholder="<?= h(tr('ابحث برقم الهاتف', 'Search by phone number')) ?>"
|
||||
dir="ltr"
|
||||
>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-2">
|
||||
<label class="form-label mb-1" for="debts-date-from"><?= h(tr('من تاريخ', 'From date')) ?></label>
|
||||
<input id="debts-date-from" type="date" name="date_from" class="form-control" value="<?= h($dateFrom) ?>">
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-xl-2">
|
||||
<label class="form-label mb-1" for="debts-date-to"><?= h(tr('إلى تاريخ', 'To date')) ?></label>
|
||||
<input id="debts-date-to" type="date" name="date_to" class="form-control" value="<?= h($dateTo) ?>">
|
||||
</div>
|
||||
<div class="col-12 col-xl-2 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-dark flex-fill">
|
||||
<i class="bi bi-funnel me-1"></i><?= h(tr('تطبيق', 'Apply')) ?>
|
||||
</button>
|
||||
<a class="btn btn-outline-secondary flex-fill" href="<?= h(url_for('debts.php')) ?>">
|
||||
<?= h(tr('إعادة ضبط', 'Reset')) ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php if ($hasFilters): ?>
|
||||
<div class="col-12">
|
||||
<div class="small text-muted">
|
||||
<i class="bi bi-info-circle me-1"></i><?= h(tr('يتم الآن عرض الديون المطابقة للفلاتر المحددة فقط.', 'Showing only debts that match the selected filters.')) ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Debts by Customer -->
|
||||
<div class="col-lg-3 mb-4">
|
||||
@ -135,7 +294,7 @@ require_once 'includes/header.php';
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (empty($debtsByCustomer)): ?>
|
||||
<div class="text-center text-muted py-4"><?= h(tr('لا توجد ديون مسجلة.', 'No debts recorded.')) ?></div>
|
||||
<div class="text-center text-muted py-4"><?= h($hasFilters ? tr('لا توجد ديون مطابقة للفلاتر المحددة.', 'No debts match the selected filters.') : tr('لا توجد ديون مسجلة.', 'No debts recorded.')) ?></div>
|
||||
<?php else: ?>
|
||||
<ul class="list-group list-group-flush">
|
||||
<?php foreach ($debtsByCustomer as $debt): ?>
|
||||
@ -143,7 +302,7 @@ require_once 'includes/header.php';
|
||||
<div>
|
||||
<strong><?= h($debt['name']) ?></strong>
|
||||
<?php if ($debt['phone']): ?>
|
||||
<div class="small text-muted" dir="ltr"><?= h(phone_display($debt['phone'])) ?></div>
|
||||
<div class="small text-muted" dir="ltr"><?= h($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): ?>
|
||||
@ -184,42 +343,26 @@ require_once 'includes/header.php';
|
||||
<tbody>
|
||||
<?php if (empty($unpaidSales)): ?>
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-4 text-muted"><?= h(tr('لا توجد فواتير غير مدفوعة أو جزئية.', 'No unpaid or partial invoices.')) ?></td>
|
||||
<td colspan="9" class="text-center py-4 text-muted"><?= h($hasFilters ? tr('لا توجد فواتير مطابقة للفلاتر المحددة.', 'No invoices match the selected filters.') : tr('لا توجد فواتير غير مدفوعة أو جزئية.', 'No unpaid or partial invoices.')) ?></td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($unpaidSales as $sale): ?>
|
||||
<?php $paymentSummary = sale_payment_summary($sale); ?>
|
||||
<?php
|
||||
$paymentSummary = sale_payment_summary($sale);
|
||||
$customerContact = $extractCustomerContact($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><?= h($customerContact['name']) ?></td>
|
||||
<td dir="ltr"><?= h($customerContact['phone'] !== '' ? $customerContact['phone'] : '-') ?></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>
|
||||
|
||||
@ -21,6 +21,15 @@ $selectedDeliveryStatus = trim((string) ($_POST['delivery_status'] ?? ($isEidOrd
|
||||
$deliveryDateInput = trim((string) ($_POST['delivery_date'] ?? ''));
|
||||
$saleStatusInput = trim((string) ($_POST['sale_status'] ?? ($isEidOrder ? 'order' : 'completed')));
|
||||
$notesInput = trim((string) ($_POST['notes'] ?? ''));
|
||||
$notesLabel = $isEidOrder
|
||||
? tr('ملاحظة داخلية للفاتورة', 'Internal invoice notice')
|
||||
: tr('ملاحظات (اختياري)', 'Notes (Optional)');
|
||||
$notesPlaceholder = $isEidOrder
|
||||
? tr('مثال: بدون مكسرات، تغليف خاص، اتصال قبل التسليم...', 'Example: no nuts, special packing, call before delivery...')
|
||||
: tr('أي ملاحظات إضافية...', 'Any additional notes...');
|
||||
$notesHelper = $isEidOrder
|
||||
? tr('هذه الملاحظة داخلية وتبقى في النظام فقط، ولن تظهر في الإيصال المطبوع.', 'This note is internal, stays in the system only, and will not appear on the printed receipt.')
|
||||
: tr('سيتم حفظ هذه الملاحظات مع الفاتورة داخل النظام.', 'These notes will be saved with the invoice inside the system.');
|
||||
|
||||
try {
|
||||
$customers = db()->query('SELECT id, name, phone FROM customers ORDER BY name ASC')->fetchAll();
|
||||
@ -340,6 +349,32 @@ require __DIR__ . '/header.php';
|
||||
color: #212529;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.inline-notice-box {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px dashed #d9e3f0;
|
||||
}
|
||||
.inline-notice-box .form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.inline-notice-box .notice-icon {
|
||||
width: 1.9rem;
|
||||
height: 1.9rem;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #92400e;
|
||||
background: rgba(245, 158, 11, 0.14);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.inline-notice-box textarea {
|
||||
background: #fff;
|
||||
resize: vertical;
|
||||
}
|
||||
.line-input {
|
||||
min-width: 0;
|
||||
}
|
||||
@ -496,11 +531,6 @@ require __DIR__ . '/header.php';
|
||||
</div>
|
||||
<div class="form-text" id="paymentAmountHint"><?= h(tr('يمكنك تعديل المبلغ المدفوع يدوياً وسيتم تتبع الباقي تلقائياً.', 'You can edit the paid amount manually and the remaining balance will be tracked automatically.')) ?></div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label"><?= h(tr('ملاحظات (اختياري)', 'Notes (Optional)')) ?></label>
|
||||
<textarea class="form-control custom-input" name="notes" rows="2" placeholder="<?= h(tr('أي ملاحظات إضافية...', 'Any additional notes...')) ?>"><?= h($notesInput) ?></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="totals-box mb-4">
|
||||
<div class="totals-row">
|
||||
@ -515,6 +545,14 @@ require __DIR__ . '/header.php';
|
||||
<span><?= h(tr('الإجمالي', 'Total')) ?></span>
|
||||
<span id="displayTotal" class="text-primary">0.000 <?= h(tr('ر.ع', 'OMR')) ?></span>
|
||||
</div>
|
||||
<div class="inline-notice-box">
|
||||
<label class="form-label" for="invoice_notes">
|
||||
<span class="notice-icon"><i class="bi bi-journal-text"></i></span>
|
||||
<span><?= h($notesLabel) ?></span>
|
||||
</label>
|
||||
<textarea class="form-control custom-input" id="invoice_notes" name="notes" rows="<?= $isEidOrder ? '3' : '2' ?>" placeholder="<?= h($notesPlaceholder) ?>"><?= h($notesInput) ?></textarea>
|
||||
<div class="form-text mt-2"><?= h($notesHelper) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user