Autosave: 20260504-045808

This commit is contained in:
Flatlogic Bot 2026-05-04 04:58:07 +00:00
parent 07894c8f77
commit 8df558e09d
2 changed files with 225 additions and 44 deletions

221
debts.php
View File

@ -8,6 +8,11 @@ $pdo = db();
$debtsLoadError = ''; $debtsLoadError = '';
$unpaidSales = []; $unpaidSales = [];
$debtsByCustomer = []; $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 // Handle legacy mark-as-paid shortcut
if (isset($_GET['mark_paid'])) { if (isset($_GET['mark_paid'])) {
@ -53,6 +58,7 @@ try {
'COALESCE(s.due_amount, s.total_amount) AS due_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_id']) ? 's.customer_id' : 'NULL AS customer_id',
isset($salesColumns['customer_name']) ? 's.customer_name' : 'NULL AS customer_name', 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_name',
'NULL AS c_phone', 'NULL AS c_phone',
]; ];
@ -61,28 +67,95 @@ try {
if ($hasCustomersTable && isset($salesColumns['customer_id']) && isset($customerColumns['id'])) { if ($hasCustomersTable && isset($salesColumns['customer_id']) && isset($customerColumns['id'])) {
$joinSql = ' LEFT JOIN customers c ON s.customer_id = c.id '; $joinSql = ' LEFT JOIN customers c ON s.customer_id = c.id ';
if (isset($customerColumns['name'])) { if (isset($customerColumns['name'])) {
$selectParts[9] = 'c.name AS c_name'; $selectParts[10] = 'c.name AS c_name';
} }
if (isset($customerColumns['phone'])) { 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'])) { 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'])) { } elseif (isset($salesColumns['payment_method'])) {
$whereSql = " WHERE s.payment_method = 'pay_later'"; $whereParts[] = "s.payment_method = 'pay_later'";
} else { } 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) $sqlUnpaid = 'SELECT ' . implode(', ', $selectParts)
. ' FROM sales_orders s' . ' FROM sales_orders s'
. $joinSql . $joinSql
. $whereSql . $whereSql
. ' ORDER BY s.sale_date DESC'; . ' ORDER BY s.sale_date DESC';
$stmtUnpaid = $pdo->query($sqlUnpaid); $stmtUnpaid = $pdo->prepare($sqlUnpaid);
$unpaidSales = $stmtUnpaid ? $stmtUnpaid->fetchAll(PDO::FETCH_ASSOC) : []; foreach ($params as $key => $value) {
$stmtUnpaid->bindValue($key, $value);
}
$stmtUnpaid->execute();
$unpaidSales = $stmtUnpaid->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) { } catch (Throwable $e) {
$debtsLoadError = tr( $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 // Aggregate by customer
foreach ($unpaidSales as $sale) { foreach ($unpaidSales as $sale) {
$customerContact = $extractCustomerContact($sale);
$cId = $sale['customer_id'] ?? 'unknown'; $cId = $sale['customer_id'] ?? 'unknown';
if (!isset($debtsByCustomer[$cId])) { if (!isset($debtsByCustomer[$cId])) {
$debtsByCustomer[$cId] = [ $debtsByCustomer[$cId] = [
'name' => $sale['c_name'] ?: $sale['customer_name'] ?: tr('عميل غير معروف', 'Unknown Customer'), 'name' => $customerContact['name'],
'phone' => $sale['c_phone'] ?: '', 'phone' => $customerContact['phone'],
'total' => 0.0, 'total' => 0.0,
'open_invoices' => 0, 'open_invoices' => 0,
'partial_invoices' => 0 'partial_invoices' => 0,
]; ];
} }
$saleSummary = sale_payment_summary($sale); $saleSummary = sale_payment_summary($sale);
@ -126,6 +231,60 @@ require_once 'includes/header.php';
</div> </div>
<?php endif; ?> <?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"> <div class="row">
<!-- Debts by Customer --> <!-- Debts by Customer -->
<div class="col-lg-3 mb-4"> <div class="col-lg-3 mb-4">
@ -135,7 +294,7 @@ require_once 'includes/header.php';
</div> </div>
<div class="card-body"> <div class="card-body">
<?php if (empty($debtsByCustomer)): ?> <?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: ?> <?php else: ?>
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<?php foreach ($debtsByCustomer as $debt): ?> <?php foreach ($debtsByCustomer as $debt): ?>
@ -143,7 +302,7 @@ require_once 'includes/header.php';
<div> <div>
<strong><?= h($debt['name']) ?></strong> <strong><?= h($debt['name']) ?></strong>
<?php if ($debt['phone']): ?> <?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; ?> <?php endif; ?>
<div class="small text-muted"><?= h($debt['open_invoices']) ?> <?= h(tr('فواتير مفتوحة', 'open invoices')) ?></div> <div class="small text-muted"><?= h($debt['open_invoices']) ?> <?= h(tr('فواتير مفتوحة', 'open invoices')) ?></div>
<?php if ($debt['partial_invoices'] > 0): ?> <?php if ($debt['partial_invoices'] > 0): ?>
@ -184,42 +343,26 @@ require_once 'includes/header.php';
<tbody> <tbody>
<?php if (empty($unpaidSales)): ?> <?php if (empty($unpaidSales)): ?>
<tr> <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> </tr>
<?php else: ?> <?php else: ?>
<?php foreach ($unpaidSales as $sale): ?> <?php foreach ($unpaidSales as $sale): ?>
<?php $paymentSummary = sale_payment_summary($sale); ?> <?php
$paymentSummary = sale_payment_summary($sale);
$customerContact = $extractCustomerContact($sale);
?>
<tr> <tr>
<td> <td>
<a href="<?= h(url_for('sale.php', ['id' => $sale['id']])) ?>" class="fw-bold text-decoration-none"> <a href="<?= h(url_for('sale.php', ['id' => $sale['id']])) ?>" class="fw-bold text-decoration-none">
<?= h($sale['receipt_no']) ?> <?= h($sale['receipt_no']) ?>
</a> </a>
</td> </td>
<?php <td><?= h($customerContact['name']) ?></td>
$rawCustomerName = (string) ($sale['c_name'] ?: $sale['customer_name'] ?: '-'); <td dir="ltr"><?= h($customerContact['phone'] !== '' ? $customerContact['phone'] : '-') ?></td>
$displayPhone = ''; <td><?= h(date('Y-m-d', strtotime((string) $sale['sale_date']))) ?></td>
if (str_contains($rawCustomerName, ' - ')) { <td class="fw-semibold"><?= h(currency((float) $sale['total_amount'])) ?></td>
$parts = explode(' - ', $rawCustomerName); <td class="text-primary"><?= h(currency((float) $paymentSummary['paid_amount'])) ?></td>
$lastPart = trim(end($parts)); <td class="fw-bold text-danger"><?= h(currency((float) $paymentSummary['due_amount'])) ?></td>
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> <td>
<span class="badge <?= h(payment_status_badge_class($paymentSummary['payment_status'])) ?>"><?= h(payment_status_label($paymentSummary['payment_status'])) ?></span> <span class="badge <?= h(payment_status_badge_class($paymentSummary['payment_status'])) ?>"><?= h(payment_status_label($paymentSummary['payment_status'])) ?></span>
</td> </td>

View File

@ -21,6 +21,15 @@ $selectedDeliveryStatus = trim((string) ($_POST['delivery_status'] ?? ($isEidOrd
$deliveryDateInput = trim((string) ($_POST['delivery_date'] ?? '')); $deliveryDateInput = trim((string) ($_POST['delivery_date'] ?? ''));
$saleStatusInput = trim((string) ($_POST['sale_status'] ?? ($isEidOrder ? 'order' : 'completed'))); $saleStatusInput = trim((string) ($_POST['sale_status'] ?? ($isEidOrder ? 'order' : 'completed')));
$notesInput = trim((string) ($_POST['notes'] ?? '')); $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 { try {
$customers = db()->query('SELECT id, name, phone FROM customers ORDER BY name ASC')->fetchAll(); $customers = db()->query('SELECT id, name, phone FROM customers ORDER BY name ASC')->fetchAll();
@ -340,6 +349,32 @@ require __DIR__ . '/header.php';
color: #212529; color: #212529;
margin-bottom: 0; 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 { .line-input {
min-width: 0; min-width: 0;
} }
@ -496,11 +531,6 @@ require __DIR__ . '/header.php';
</div> </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 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>
<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 --> <!-- Summary -->
<div class="totals-box mb-4"> <div class="totals-box mb-4">
<div class="totals-row"> <div class="totals-row">
@ -515,6 +545,14 @@ require __DIR__ . '/header.php';
<span><?= h(tr('الإجمالي', 'Total')) ?></span> <span><?= h(tr('الإجمالي', 'Total')) ?></span>
<span id="displayTotal" class="text-primary">0.000 <?= h(tr('ر.ع', 'OMR')) ?></span> <span id="displayTotal" class="text-primary">0.000 <?= h(tr('ر.ع', 'OMR')) ?></span>
</div> </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>
<div class="d-grid gap-2"> <div class="d-grid gap-2">