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 = '';
$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>

View File

@ -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">