338 lines
17 KiB
PHP
338 lines
17 KiB
PHP
<?php
|
|
require_once __DIR__ . '/includes/app.php';
|
|
$user = require_permission('sales', 'show');
|
|
|
|
$mode = isset($_GET['mode']) && in_array($_GET['mode'], ['pos', 'normal'], true) ? $_GET['mode'] : null;
|
|
$branch = isset($_GET['branch']) && array_key_exists($_GET['branch'], branches()) ? $_GET['branch'] : null;
|
|
$search = $_GET['q'] ?? '';
|
|
$statusFilter = $_GET['status'] ?? '';
|
|
$activeNav = $statusFilter === 'order' ? 'sales_orders' : 'sales';
|
|
$pageTitle = $statusFilter === 'order' ? tr('الطلبات', 'Orders') : tr('المبيعات', 'Sales Ledger');
|
|
|
|
if (isset($_GET['mark_paid']) && is_numeric($_GET['mark_paid'])) {
|
|
try {
|
|
$id = (int) $_GET['mark_paid'];
|
|
$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);
|
|
}
|
|
}
|
|
} catch (Throwable $e) {}
|
|
$redirect = $_GET['redirect'] ?? 'sales.php';
|
|
header('Location: ' . $redirect);
|
|
exit;
|
|
}
|
|
|
|
$dbError = null;
|
|
$sales = [];
|
|
$totalPages = 1;
|
|
$page = max(1, (int)($_GET['p'] ?? 1));
|
|
$limit = 10;
|
|
$offset = ($page - 1) * $limit;
|
|
|
|
try {
|
|
ensure_sales_table();
|
|
$params = [];
|
|
$where = ' WHERE 1=1 ';
|
|
|
|
if ($mode) {
|
|
$where .= ' AND sale_mode = :sale_mode ';
|
|
$params[':sale_mode'] = $mode;
|
|
}
|
|
if ($branch) {
|
|
$where .= ' AND branch_code = :branch_code ';
|
|
$params[':branch_code'] = $branch;
|
|
}
|
|
if ($user && $user['role'] !== 'owner') {
|
|
$ubranches = get_user_branches($user);
|
|
if (empty($ubranches)) {
|
|
$where .= ' AND 1=0 ';
|
|
} else {
|
|
$namedParams = [];
|
|
foreach ($ubranches as $i => $ub) {
|
|
$key = ':v_branch_' . $i;
|
|
$namedParams[] = $key;
|
|
$params[$key] = $ub;
|
|
}
|
|
$where .= ' AND branch_code IN (' . implode(', ', $namedParams) . ') ';
|
|
}
|
|
}
|
|
|
|
if ($search) {
|
|
$where .= ' AND (receipt_no LIKE :search OR cashier_name LIKE :search OR customer_name LIKE :search)';
|
|
$params[':search'] = "%$search%";
|
|
}
|
|
|
|
if (in_array($statusFilter, ['paid', 'partial', 'unpaid'], true)) {
|
|
$where .= ' AND payment_status = :payment_status ';
|
|
$params[':payment_status'] = $statusFilter;
|
|
$where .= " AND COALESCE(status, 'completed') <> 'order' ";
|
|
} elseif ($statusFilter === 'order') {
|
|
$where .= " AND status = 'order' ";
|
|
} elseif ($statusFilter === 'completed') {
|
|
$where .= " AND COALESCE(status, 'completed') = 'completed' ";
|
|
} else {
|
|
$where .= " AND COALESCE(status, 'completed') <> 'order' ";
|
|
}
|
|
|
|
// Pagination counts
|
|
$countSql = 'SELECT COUNT(*) FROM sales_orders' . $where;
|
|
$countStmt = db()->prepare($countSql);
|
|
foreach ($params as $key => $value) {
|
|
$countStmt->bindValue($key, $value);
|
|
}
|
|
$countStmt->execute();
|
|
$total = $countStmt->fetchColumn();
|
|
$totalPages = max(1, ceil($total / $limit));
|
|
|
|
// Fetch Data
|
|
$sql = 'SELECT * FROM sales_orders' . $where . ' ORDER BY sale_date DESC LIMIT :limit OFFSET :offset';
|
|
$stmt = db()->prepare($sql);
|
|
foreach ($params as $key => $value) {
|
|
$stmt->bindValue($key, $value);
|
|
}
|
|
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
|
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
|
$stmt->execute();
|
|
$sales = $stmt->fetchAll();
|
|
|
|
} catch (Throwable $e) {
|
|
$dbError = $e->getMessage();
|
|
}
|
|
|
|
require __DIR__ . '/includes/header.php';
|
|
?>
|
|
<section class="mb-4">
|
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-3">
|
|
<div>
|
|
<h3 class="h5 mb-1"><i class="bi bi-journal-text me-2"></i><?= h($statusFilter === 'order' ? tr('قائمة الطلبات', 'Orders list') : tr('سجل الفواتير', 'Invoice ledger')) ?></h3>
|
|
<div class="small text-muted"><?= h($statusFilter === 'order' ? tr('هذه القائمة تعرض طلبات البيع العادي المحفوظة بحالة طلب.', 'This list shows normal sales saved with order status.') : tr('ابحث بصرياً في أحدث المبيعات مع صلاحيات حسب الدور والفرع.', 'Scan the latest sales with role and branch scoping.')) ?></div>
|
|
</div>
|
|
<div class="d-flex gap-2 flex-wrap">
|
|
<a class="btn btn-sm <?= $mode === null ? 'btn-dark' : 'btn-outline-secondary' ?>" href="<?= h(url_for('sales.php', ['q' => $search, 'status' => $statusFilter])) ?>"><?= h(tr('الكل', 'All')) ?></a>
|
|
<a class="btn btn-sm <?= $mode === 'pos' ? 'btn-dark' : 'btn-outline-secondary' ?>" href="<?= h(url_for('sales.php', ['mode' => 'pos', 'q' => $search, 'status' => $statusFilter])) ?>">POS</a>
|
|
<a class="btn btn-sm <?= $mode === 'normal' ? 'btn-dark' : 'btn-outline-secondary' ?>" href="<?= h(url_for('sales.php', ['mode' => 'normal', 'q' => $search, 'status' => $statusFilter])) ?>"><?= h(tr('فاتورة', 'Invoice')) ?></a>
|
|
</div>
|
|
</div>
|
|
|
|
<form class="d-flex mb-3" method="GET" action="sales.php">
|
|
<?php if($mode): ?>
|
|
<input type="hidden" name="mode" value="<?= h($mode) ?>">
|
|
<?php endif; ?>
|
|
<div class="input-group" style="max-width: 600px;">
|
|
<select name="status" class="form-select" style="max-width: 150px;" onchange="this.form.submit()">
|
|
<option value=""><?= h(tr('كل الحالات', 'All Statuses')) ?></option>
|
|
<option value="paid" <?= $statusFilter === 'paid' ? 'selected' : '' ?>><?= h(tr('مدفوعة بالكامل', 'Paid')) ?></option>
|
|
<option value="partial" <?= $statusFilter === 'partial' ? 'selected' : '' ?>><?= h(tr('مدفوعة جزئياً', 'Partially Paid')) ?></option>
|
|
<option value="unpaid" <?= $statusFilter === 'unpaid' ? 'selected' : '' ?>><?= h(tr('غير مدفوعة', 'Unpaid')) ?></option>
|
|
<option value="order" <?= $statusFilter === 'order' ? 'selected' : '' ?>><?= h(tr('طلب حجز', 'Order')) ?></option>
|
|
<option value="completed" <?= $statusFilter === 'completed' ? 'selected' : '' ?>><?= h(tr('كل الفواتير المكتملة', 'Completed Invoices')) ?></option>
|
|
</select>
|
|
<input type="text" name="q" class="form-control" placeholder="<?= h(tr('بحث بالإيصال، الكاشير، العميل أو الهاتف...', 'Search receipt, cashier, customer or phone...')) ?>" value="<?= h($search) ?>">
|
|
<button class="btn btn-outline-secondary" type="submit"><i class="bi bi-search"></i></button>
|
|
</div>
|
|
</form>
|
|
|
|
<?php if ($dbError): ?>
|
|
<div class="alert alert-warning"><?= h($dbError) ?></div>
|
|
<?php elseif (!$sales): ?>
|
|
<div class="empty-state">
|
|
<h4><?= h(tr('لا توجد نتائج', 'No sales found')) ?></h4>
|
|
<p><?= h(tr('جرّب إنشاء فاتورة جديدة من صفحة البيع.', 'Try creating a new invoice from the sale page.')) ?></p>
|
|
<div class="d-flex gap-2 justify-content-center flex-wrap">
|
|
<a class="btn btn-dark" href="<?= h(url_for('pos.php')) ?>">POS</a>
|
|
<a class="btn btn-outline-secondary" href="<?= h(url_for('normal_sale.php')) ?>"><?= h(tr('فاتورة', 'Invoice')) ?></a>
|
|
</div>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0 text-center">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th><?= h(tr('الإيصال', 'Receipt')) ?></th>
|
|
<th><?= h(tr('الفرع', 'Branch')) ?></th>
|
|
<th><?= h(tr('النوع', 'Type')) ?></th>
|
|
<th><?= h(tr('الكاشير', 'Cashier')) ?></th>
|
|
<th><?= h(tr('العميل', 'Customer')) ?></th>
|
|
<th><?= h(tr('الهاتف', 'Phone')) ?></th>
|
|
<th><?= h(tr('المجموع', 'Subtotal')) ?></th>
|
|
<th><?= h(tr('الضريبة', 'VAT')) ?></th>
|
|
<th><?= h(tr('الإجمالي', 'Total')) ?></th>
|
|
<th><?= h(tr('المدفوع', 'Paid')) ?></th>
|
|
<th><?= h(tr('المتبقي', 'Due')) ?></th>
|
|
<th><?= h(tr('الحالة', 'Status')) ?></th>
|
|
<th><?= h(tr('التاريخ', 'Date')) ?></th>
|
|
<th><?= h(tr('إجراءات', 'Actions')) ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="border-top-0">
|
|
<?php foreach ($sales as $sale): ?>
|
|
<?php $paymentSummary = sale_payment_summary($sale); ?>
|
|
<tr>
|
|
<td>
|
|
<div class="fw-semibold"><?= h($sale['receipt_no']) ?></div>
|
|
<div class="small text-muted"><?= h((string) $sale['item_count']) ?> <?= h(tr('قطعة', 'items')) ?></div>
|
|
</td>
|
|
<td><?= h(branch_label((string) $sale['branch_code'])) ?></td>
|
|
<td><span class="badge text-bg-light border"><?= h(sale_mode_label((string) $sale['sale_mode'])) ?></span></td>
|
|
<td><?= h((string) $sale['cashier_name']) ?></td>
|
|
<?php
|
|
$rawCustomerName = (string) ($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 class="text-muted"><?= h(currency((float) $sale['total_amount'] - (float) ($sale['vat_amount'] ?? 0))) ?></td>
|
|
<td class="text-muted text-danger"><?= h(currency((float) $sale['vat_amount'])) ?></td>
|
|
<td class="fw-bold text-success"><?= h(currency((float) $sale['total_amount'])) ?></td>
|
|
<td class="fw-semibold text-primary"><?= h(currency((float) $paymentSummary['paid_amount'])) ?></td>
|
|
<td class="fw-semibold <?= $paymentSummary['due_amount'] > 0 ? 'text-danger' : 'text-muted' ?>"><?= h(currency((float) $paymentSummary['due_amount'])) ?></td>
|
|
<td>
|
|
<span class="badge <?= h(payment_status_badge_class($paymentSummary['payment_status'])) ?> px-3 py-2 rounded-pill">
|
|
<?php if ($paymentSummary['payment_status'] === 'partial'): ?>
|
|
<i class="bi bi-pie-chart"></i>
|
|
<?php elseif ($paymentSummary['payment_status'] === 'unpaid'): ?>
|
|
<i class="bi bi-clock-history"></i>
|
|
<?php else: ?>
|
|
<i class="bi bi-check-circle"></i>
|
|
<?php endif; ?>
|
|
<?= h(payment_status_label($paymentSummary['payment_status'])) ?>
|
|
</span>
|
|
<?php if (($sale['status'] ?? 'completed') === 'order'): ?>
|
|
<small class="d-block text-muted mt-1"><?= h(tr('طلب حجز', 'Order')) ?></small>
|
|
<?php elseif (($sale['payment_method'] ?? '') === 'pay_later'): ?>
|
|
<small class="d-block text-muted mt-1"><?= h(tr('دفع آجل', 'Pay Later')) ?></small>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?= h(date('Y-m-d H:i', strtotime((string) $sale['sale_date']))) ?></td>
|
|
<td>
|
|
<?php if ($paymentSummary['due_amount'] > 0.0005): ?>
|
|
<button class="btn btn-sm btn-outline-success rounded-circle shadow-sm me-1" style="width: 34px; height: 34px; padding: 0;" onclick="receivePayment(<?= (int) $sale['id'] ?>, <?= json_encode((float) $paymentSummary['due_amount']) ?>, <?= ($sale['status'] ?? 'completed') === 'order' ? 'true' : 'false' ?>)" title="<?= h(tr('استلام دفعة', 'Receive Payment')) ?>">
|
|
<i class="bi bi-cash-coin"></i>
|
|
</button>
|
|
<?php endif; ?>
|
|
<a class="btn btn-sm btn-light text-primary border me-1" href="<?= h(url_for('sale.php', ['id' => $sale['id']])) ?>" title="<?= h(tr('تفاصيل', 'Detail')) ?>">
|
|
<i class="bi bi-eye"></i>
|
|
</a>
|
|
<a class="btn btn-sm btn-outline-secondary rounded-circle shadow-sm ms-1" style="width: 34px; height: 34px; padding: 0; line-height: 32px; text-align: center;" href="<?= h(url_for('edit_sale.php', ['id' => $sale['id']])) ?>" title="<?= h(tr('تعديل', 'Edit')) ?>">
|
|
<i class="bi bi-pencil"></i>
|
|
</a>
|
|
<button class="btn btn-sm btn-outline-danger rounded-circle shadow-sm ms-1" style="width: 34px; height: 34px; padding: 0;" onclick="mockDelete()" title="<?= h(tr('حذف', 'Delete')) ?>">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<?php if ($totalPages > 1): ?>
|
|
<nav class="mt-4">
|
|
<ul class="pagination justify-content-center mb-0">
|
|
<?php for($i=1; $i<=$totalPages; $i++): ?>
|
|
<li class="page-item <?= $i === $page ? 'active' : '' ?>">
|
|
<a class="page-link" href="<?= h(url_for('sales.php', ['p' => $i, 'q' => $search, 'mode' => $mode, 'status' => $statusFilter])) ?>"><?= $i ?></a>
|
|
</li>
|
|
<?php endfor; ?>
|
|
</ul>
|
|
</nav>
|
|
<?php endif; ?>
|
|
<?php endif; ?>
|
|
</section>
|
|
|
|
<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.')) ?>' });
|
|
}
|
|
}
|
|
|
|
function mockEdit() {
|
|
Swal.fire({
|
|
title: '<?= h(tr('تعديل (غير متاح)', 'Edit (Disabled)')) ?>',
|
|
text: '<?= h(tr('تعديل الفواتير غير متاح لأسباب محاسبية.', 'Invoice editing is disabled for accounting reasons.')) ?>',
|
|
icon: 'info',
|
|
confirmButtonText: '<?= h(tr('حسناً', 'OK')) ?>'
|
|
});
|
|
}
|
|
|
|
function mockDelete() {
|
|
Swal.fire({
|
|
title: '<?= h(tr('هل أنت متأكد؟', 'Are you sure?')) ?>',
|
|
text: '<?= h(tr('حذف الفاتورة قد يؤثر على حسابات المخزون. (هذه الميزة تجريبية حالياً)', "Deleting an invoice might affect stock. (This is currently mock data)")) ?>',
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#dc3545',
|
|
cancelButtonColor: '#6c757d',
|
|
confirmButtonText: '<?= h(tr('نعم، احذف', 'Yes, delete it!')) ?>',
|
|
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
Swal.fire(
|
|
'<?= h(tr('محذوف!', 'Deleted!')) ?>',
|
|
'<?= h(tr('لم يتم الحذف فعلياً.', 'Not actually deleted.')) ?>',
|
|
'success'
|
|
);
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<?php require __DIR__ . '/includes/footer.php'; ?>
|