updating sales
This commit is contained in:
parent
f6212d4e47
commit
ee93351390
26
api/customers.php
Normal file
26
api/customers.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../includes/app.php';
|
||||
$user = require_auth();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
header('Content-Type: application/json');
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$phone = trim($_POST['phone'] ?? '');
|
||||
|
||||
if (!$name) {
|
||||
echo json_encode(['success' => false, 'error' => tr('الاسم مطلوب', 'Name is required')]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
$stmt = $pdo->prepare('INSERT INTO customers (name, phone) VALUES (?, ?)');
|
||||
$stmt->execute([$name, $phone]);
|
||||
$id = $pdo->lastInsertId();
|
||||
|
||||
echo json_encode(['success' => true, 'customer' => ['id' => $id, 'name' => $name, 'phone' => $phone]]);
|
||||
} catch (Throwable $e) {
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
@ -256,6 +256,7 @@ function ensure_sales_table(): void
|
||||
item_count INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
subtotal DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
total_amount DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'completed',
|
||||
notes TEXT DEFAULT NULL,
|
||||
sale_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
@ -272,9 +273,9 @@ function create_sale(array $data): int
|
||||
ensure_sales_table();
|
||||
|
||||
$stmt = db()->prepare('INSERT INTO sales_orders
|
||||
(receipt_no, sale_mode, branch_code, cashier_username, cashier_name, role_name, customer_name, payment_method, items_json, item_count, subtotal, total_amount, notes, sale_date)
|
||||
(receipt_no, sale_mode, branch_code, cashier_username, cashier_name, role_name, customer_name, payment_method, items_json, item_count, subtotal, total_amount, status, notes, sale_date)
|
||||
VALUES
|
||||
(:receipt_no, :sale_mode, :branch_code, :cashier_username, :cashier_name, :role_name, :customer_name, :payment_method, :items_json, :item_count, :subtotal, :total_amount, :notes, NOW())');
|
||||
(:receipt_no, :sale_mode, :branch_code, :cashier_username, :cashier_name, :role_name, :customer_name, :payment_method, :items_json, :item_count, :subtotal, :total_amount, :status, :notes, NOW())');
|
||||
|
||||
$stmt->bindValue(':receipt_no', $data['receipt_no']);
|
||||
$stmt->bindValue(':sale_mode', $data['sale_mode']);
|
||||
@ -288,6 +289,8 @@ function create_sale(array $data): int
|
||||
$stmt->bindValue(':item_count', $data['item_count'], PDO::PARAM_INT);
|
||||
$stmt->bindValue(':subtotal', $data['subtotal']);
|
||||
$stmt->bindValue(':total_amount', $data['total_amount']);
|
||||
$stmt->bindValue(':status', $data['status'] ?? 'completed');
|
||||
$stmt->bindValue(':status', $data['status'] ?? 'completed');
|
||||
$stmt->bindValue(':notes', $data['notes']);
|
||||
$stmt->execute();
|
||||
|
||||
|
||||
@ -1,16 +1,23 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/app.php';
|
||||
$user = require_roles(['owner', 'manager', 'cashier']);
|
||||
$pageTitle = $saleMode === 'normal' ? tr('بيع عادي', 'Normal Sale') : tr('نقاط البيع', 'POS Sale');
|
||||
$pageTitle = $saleMode === 'normal' ? tr('إنشاء فاتورة ضريبية', 'Create Tax Invoice') : tr('نقاط البيع', 'POS Sale');
|
||||
$activeNav = $saleMode === 'normal' ? 'normal' : 'pos';
|
||||
$error = '';
|
||||
$catalog = catalog();
|
||||
$allowedBranches = $user['role'] === 'owner' ? array_keys(branches()) : [$user['branch_code']];
|
||||
|
||||
try {
|
||||
$customers = db()->query('SELECT id, name, phone FROM customers ORDER BY name ASC')->fetchAll();
|
||||
} catch (Throwable $e) {
|
||||
$customers = [];
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$branchCode = trim((string) ($_POST['branch_code'] ?? ''));
|
||||
$customerName = trim((string) ($_POST['customer_name'] ?? ''));
|
||||
$paymentMethod = trim((string) ($_POST['payment_method'] ?? 'cash'));
|
||||
$saleStatus = trim((string) ($_POST['sale_status'] ?? 'completed'));
|
||||
$notes = trim((string) ($_POST['notes'] ?? ''));
|
||||
$cartJson = (string) ($_POST['cart_json'] ?? '[]');
|
||||
$items = json_decode($cartJson, true);
|
||||
@ -20,7 +27,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
} elseif (!in_array($paymentMethod, ['cash', 'card', 'transfer'], true)) {
|
||||
$error = tr('اختر طريقة دفع صحيحة.', 'Choose a valid payment method.');
|
||||
} elseif (!is_array($items) || $items === []) {
|
||||
$error = tr('أضف صنفاً واحداً على الأقل إلى السلة.', 'Add at least one item to the cart.');
|
||||
$error = tr('أضف صنفاً واحداً على الأقل إلى الفاتورة.', 'Add at least one item to the invoice.');
|
||||
} else {
|
||||
$normalized = [];
|
||||
$subtotal = 0.0;
|
||||
@ -47,7 +54,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
if ($normalized === []) {
|
||||
$error = tr('السلة غير صالحة بعد التحقق من الأصناف.', 'The cart is invalid after product validation.');
|
||||
$error = tr('الفاتورة غير صالحة بعد التحقق من الأصناف.', 'The invoice is invalid after product validation.');
|
||||
} else {
|
||||
$cashierName = current_lang() === 'ar' ? $user['name_ar'] : $user['name_en'];
|
||||
$saleId = create_sale([
|
||||
@ -63,11 +70,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
'item_count' => $itemCount,
|
||||
'subtotal' => $subtotal,
|
||||
'total_amount' => $subtotal,
|
||||
'status' => $saleStatus,
|
||||
'notes' => $notes !== '' ? $notes : null,
|
||||
]);
|
||||
|
||||
set_flash('success', $saleMode === 'normal'
|
||||
? tr('تم حفظ البيع العادي بنجاح.', 'Normal sale saved successfully.')
|
||||
? tr('تم حفظ الفاتورة بنجاح.', 'Invoice saved successfully.')
|
||||
: tr('تم حفظ عملية POS بنجاح.', 'POS sale saved successfully.'));
|
||||
redirect_to('sale.php', ['id' => $saleId]);
|
||||
}
|
||||
@ -76,105 +84,574 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
require __DIR__ . '/header.php';
|
||||
?>
|
||||
<section class="row g-4">
|
||||
<div class="col-xl-7">
|
||||
<div class="surface-card h-100">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 gap-3 flex-wrap">
|
||||
<div>
|
||||
<h3 class="h5 mb-1"><?= h($pageTitle) ?></h3>
|
||||
<div class="small text-muted"><?= h(tr('أضف المنتجات إلى السلة ثم احفظ العملية.', 'Add products to the cart and save the transaction.')) ?></div>
|
||||
</div>
|
||||
<span class="badge text-bg-light border px-3 py-2"><?= h(sale_mode_label($saleMode)) ?></span>
|
||||
</div>
|
||||
<?php if ($error !== ''): ?>
|
||||
<div class="alert alert-warning"><?= h($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="post" class="d-grid gap-4" id="sale-form" data-sale-form>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="branch_code"><?= h(tr('الفرع', 'Branch')) ?></label>
|
||||
<select class="form-select" id="branch_code" name="branch_code" <?= count($allowedBranches) === 1 ? 'aria-readonly="true"' : '' ?>>
|
||||
<?php foreach ($allowedBranches as $branchCode): ?>
|
||||
<option value="<?= h($branchCode) ?>"><?= h(branch_label($branchCode)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="payment_method"><?= h(tr('طريقة الدفع', 'Payment method')) ?></label>
|
||||
<select class="form-select" id="payment_method" name="payment_method">
|
||||
<option value="cash"><?= h(tr('نقداً', 'Cash')) ?></option>
|
||||
<option value="card"><?= h(tr('بطاقة', 'Card')) ?></option>
|
||||
<option value="transfer"><?= h(tr('تحويل', 'Transfer')) ?></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="customer_name"><?= h(tr('اسم العميل', 'Customer name')) ?></label>
|
||||
<input class="form-control" id="customer_name" name="customer_name" maxlength="120" placeholder="<?= h(tr('اختياري', 'Optional')) ?>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="notes"><?= h(tr('ملاحظات', 'Notes')) ?></label>
|
||||
<input class="form-control" id="notes" name="notes" maxlength="500" placeholder="<?= h(tr('طلب خاص أو ملاحظة داخلية', 'Special request or internal note')) ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.smart-form-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
|
||||
border: 1px solid #edf2f9;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.smart-form-header {
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid #edf2f9;
|
||||
background-color: #fcfdfd;
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
.smart-form-body {
|
||||
padding: 2rem;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.form-label {
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.custom-input {
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
.custom-input:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 0.25rem rgba(59, 130, 246, 0.25);
|
||||
}
|
||||
.search-wrapper {
|
||||
position: relative;
|
||||
max-width: 600px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 1rem;
|
||||
transform: translateY(-50%);
|
||||
color: #6c757d;
|
||||
}
|
||||
[dir="rtl"] .search-icon {
|
||||
left: auto;
|
||||
right: 1rem;
|
||||
}
|
||||
.search-input {
|
||||
padding-left: 2.5rem;
|
||||
}
|
||||
[dir="rtl"] .search-input {
|
||||
padding-left: 1rem;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
.item-search-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
z-index: 1000;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
border: 1px solid #edf2f9;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.item-search-dropdown.show { display: block; }
|
||||
.search-item-row {
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #edf2f9;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.search-item-row:hover { background: #f8f9fa; }
|
||||
.search-item-row:last-child { border-bottom: none; }
|
||||
|
||||
.table-modern {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
border: 1px solid #edf2f9;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.table-modern th {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
border-bottom: 1px solid #edf2f9;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.table-modern td {
|
||||
padding: 1rem;
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid #edf2f9;
|
||||
}
|
||||
.table-modern tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
.qty-control {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem;
|
||||
}
|
||||
.btn-remove {
|
||||
color: #dc3545;
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
border: none;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-remove:hover {
|
||||
background: #dc3545;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.totals-box {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #edf2f9;
|
||||
}
|
||||
.totals-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
color: #495057;
|
||||
}
|
||||
.totals-row.grand-total {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #dee2e6;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #212529;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h3 class="fw-bold mb-0 text-dark"><?= h($pageTitle) ?></h3>
|
||||
<a href="sales.php" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> <?= h(tr('عودة للمبيعات', 'Back to Sales')) ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if ($error !== ''): ?>
|
||||
<div class="alert alert-danger rounded-3 shadow-sm mb-4"><i class="bi bi-exclamation-triangle-fill me-2"></i><?= h($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" id="smart-sale-form">
|
||||
<input type="hidden" name="cart_json" id="cart_json" value="[]">
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="h6 mb-0"><?= h(tr('الأصناف السريعة', 'Quick products')) ?></h4>
|
||||
<span class="small text-muted"><?= h(tr('انقر لإضافة المنتج', 'Tap to add product')) ?></span>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($catalog as $product): ?>
|
||||
<div class="col-sm-6 col-lg-4">
|
||||
<button class="product-tile w-100 text-start" type="button" data-add-product data-sku="<?= h($product['sku']) ?>" data-name="<?= h(current_lang() === 'ar' ? $product['name_ar'] : $product['name_en']) ?>" data-price="<?= h((string) $product['price']) ?>">
|
||||
<span class="product-pill"><?= h(current_lang() === 'ar' ? $product['unit_ar'] : $product['unit_en']) ?></span>
|
||||
<div class="fw-semibold mb-1"><?= h(current_lang() === 'ar' ? $product['name_ar'] : $product['name_en']) ?></div>
|
||||
<div class="small text-muted mb-2">SKU: <?= h($product['sku']) ?></div>
|
||||
<div class="fw-semibold"><?= h(currency((float) $product['price'])) ?></div>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<!-- Items Section -->
|
||||
<div class="smart-form-card">
|
||||
<div class="smart-form-header">
|
||||
<div class="section-title mb-0">
|
||||
<i class="bi bi-cart-plus text-primary"></i> <?= h(tr('عناصر الفاتورة', 'Invoice Items')) ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="smart-form-body">
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="search-wrapper">
|
||||
<i class="bi bi-search search-icon"></i>
|
||||
<input type="text" id="itemSearchInput" class="form-control custom-input search-input form-control-lg" placeholder="<?= h(tr('ابحث بالاسم أو الباركود...', 'Search by name or barcode...')) ?>" autocomplete="off">
|
||||
<div id="itemDropdown" class="item-search-dropdown"></div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="table-responsive">
|
||||
<table class="table-modern" id="invoiceTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="45%"><?= h(tr('المنتج', 'Product')) ?></th>
|
||||
<th width="15%" class="text-center"><?= h(tr('السعر', 'Price')) ?></th>
|
||||
<th width="15%" class="text-center"><?= h(tr('الكمية', 'Qty')) ?></th>
|
||||
<th width="20%" class="text-center"><?= h(tr('الإجمالي', 'Total')) ?></th>
|
||||
<th width="5%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="invoiceLines">
|
||||
<tr id="emptyInvoiceRow">
|
||||
<td colspan="5" class="text-center py-5 text-muted">
|
||||
<i class="bi bi-inbox fs-1 d-block mb-2 text-light"></i>
|
||||
<?= h(tr('لم يتم إضافة أي منتجات بعد.', 'No products added yet.')) ?>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- Settings Section -->
|
||||
<div class="smart-form-card">
|
||||
<div class="smart-form-header">
|
||||
<div class="section-title mb-0">
|
||||
<i class="bi bi-receipt text-primary"></i> <?= h(tr('تفاصيل الفاتورة', 'Invoice Details')) ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="smart-form-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= h(tr('الفرع', 'Branch')) ?></label>
|
||||
<select class="form-select custom-input" name="branch_code" <?= count($allowedBranches) === 1 ? 'readonly' : '' ?>>
|
||||
<?php foreach ($allowedBranches as $branchCode): ?>
|
||||
<option value="<?= h($branchCode) ?>"><?= h(branch_label($branchCode)) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3 position-relative">
|
||||
<label class="form-label"><?= h(tr('العميل', 'Customer')) ?></label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="formCustomer" name="customer_name" class="form-control custom-input" style="border-right-width: 1px;" placeholder="<?= h(tr('بحث (اسم أو هاتف)', 'Search (Name or Phone)')) ?>" autocomplete="off">
|
||||
<button class="btn btn-outline-primary px-3" style="border-radius: 0 8px 8px 0;" type="button" onclick="openNewCustomerModal()" title="<?= h(tr('إضافة عميل', 'Add Customer')) ?>">
|
||||
<i class="bi bi-person-plus-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="formCustomerDropdown" class="item-search-dropdown w-100" style="top: 100%;"></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= h(tr('نوع العملية', 'Entry Type')) ?></label>
|
||||
<select class="form-select custom-input" name="sale_status">
|
||||
<option value="completed"><?= h(tr('فاتورة بيع (تم الدفع)', 'Sale Bill (Paid)')) ?></option>
|
||||
<option value="order"><?= h(tr('طلب مسبق (دفع لاحق)', 'Order (Pay Later)')) ?></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><?= h(tr('طريقة الدفع', 'Payment Method')) ?></label>
|
||||
<select class="form-select custom-input" name="payment_method">
|
||||
<option value="cash"><?= h(tr('نقداً', 'Cash')) ?></option>
|
||||
<option value="card"><?= h(tr('بطاقة ائتمان', 'Credit Card')) ?></option>
|
||||
<option value="transfer"><?= h(tr('تحويل بنكي', 'Bank Transfer')) ?></option>
|
||||
</select>
|
||||
</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...')) ?>"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="totals-box mb-4">
|
||||
<div class="totals-row">
|
||||
<span><?= h(tr('المجموع الفرعي', 'Subtotal')) ?></span>
|
||||
<span id="displaySubtotal" class="fw-medium">0.000</span>
|
||||
</div>
|
||||
<div class="totals-row">
|
||||
<span><?= h(tr('الضريبة (15%)', 'VAT (15%)')) ?></span>
|
||||
<span class="text-success small"><?= h(tr('مشمولة', 'Included')) ?></span>
|
||||
</div>
|
||||
<div class="totals-row grand-total">
|
||||
<span><?= h(tr('الإجمالي', 'Total')) ?></span>
|
||||
<span id="displayTotal" class="text-primary">0.000 <?= h(tr('ر.ع', 'OMR')) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100 py-2 fs-5 rounded-3 shadow-sm">
|
||||
<i class="bi bi-check-circle me-1"></i> <?= h(tr('حفظ الفاتورة', 'Save Invoice')) ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-5">
|
||||
<div class="surface-card h-100 cart-panel">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3 class="h5 mb-0"><?= h(tr('السلة', 'Cart')) ?></h3>
|
||||
<span class="small text-muted" id="cart-count-label">0 <?= h(tr('قطعة', 'items')) ?></span>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- New Customer Modal -->
|
||||
<div class="modal fade" id="newCustomerModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-sm">
|
||||
<div class="modal-content border-0 shadow-lg" style="border-radius: 16px;">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h5 class="modal-title fw-bold"><?= h(tr('إضافة عميل', 'Add Customer')) ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="empty-state compact mb-3" id="cart-empty-state">
|
||||
<h4><?= h(tr('السلة فارغة', 'Cart is empty')) ?></h4>
|
||||
<p class="mb-0"><?= h(tr('اختر المنتجات من القائمة اليسرى لبدء أول فاتورة.', 'Select products from the left to start the first receipt.')) ?></p>
|
||||
</div>
|
||||
<div id="cart-lines" class="d-grid gap-2"></div>
|
||||
<div class="cart-summary mt-3 pt-3 border-top">
|
||||
<div class="d-flex justify-content-between small text-muted mb-2"><span><?= h(tr('المجموع الفرعي', 'Subtotal')) ?></span><span id="cart-subtotal">0.00</span></div>
|
||||
<div class="d-flex justify-content-between fw-semibold"><span><?= h(tr('الإجمالي', 'Total')) ?></span><span id="cart-total">0.00</span></div>
|
||||
</div>
|
||||
<div class="d-grid gap-2 mt-3">
|
||||
<button class="btn btn-dark" type="submit" form="sale-form"><?= h(tr('حفظ الفاتورة', 'Save invoice')) ?></button>
|
||||
<button class="btn btn-outline-secondary" type="button" data-clear-cart><?= h(tr('تفريغ السلة', 'Clear cart')) ?></button>
|
||||
</div>
|
||||
<div class="alert alert-light border mt-3 mb-0 small">
|
||||
<?= h(tr('بعد الحفظ ستنتقل مباشرة إلى صفحة التفاصيل ويمكنك الطباعة من هناك.', 'After saving, you will go straight to the detail page and can print from there.')) ?>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1"><?= h(tr('الاسم', 'Name')) ?> <span class="text-danger">*</span></label>
|
||||
<input type="text" id="ncName" class="form-control rounded-3">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1"><?= h(tr('رقم الهاتف', 'Phone')) ?></label>
|
||||
<input type="text" id="ncPhone" class="form-control rounded-3" dir="ltr">
|
||||
</div>
|
||||
<div class="d-grid mt-4">
|
||||
<button class="btn btn-primary rounded-pill fw-semibold shadow-sm" onclick="saveNewCustomer()"><?= h(tr('حفظ العميل', 'Save Customer')) ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
window.saleCatalog = <?= json_encode(array_values(array_map(static function (array $item): array {
|
||||
return [
|
||||
'sku' => $item['sku'],
|
||||
'name' => current_lang() === 'ar' ? $item['name_ar'] : $item['name_en'],
|
||||
'price' => (float) $item['price'],
|
||||
];
|
||||
}, $catalog)), JSON_UNESCAPED_UNICODE) ?>;
|
||||
window.saleLabels = <?= json_encode([
|
||||
'currency' => tr('ر.ع', 'OMR'),
|
||||
'empty' => tr('السلة فارغة', 'Cart is empty'),
|
||||
'remove' => tr('إزالة', 'Remove'),
|
||||
], JSON_UNESCAPED_UNICODE) ?>;
|
||||
const catalogData = <?= json_encode($catalog, JSON_UNESCAPED_UNICODE) ?>;
|
||||
const catalogArray = Object.values(catalogData);
|
||||
let invoiceItems = {};
|
||||
|
||||
const searchInput = document.getElementById('itemSearchInput');
|
||||
const dropdown = document.getElementById('itemDropdown');
|
||||
const tbody = document.getElementById('invoiceLines');
|
||||
const emptyRow = document.getElementById('emptyInvoiceRow');
|
||||
const cartJson = document.getElementById('cart_json');
|
||||
const currencySuffix = ' <?= h(tr('ر.ع', 'OMR')) ?>';
|
||||
|
||||
// Customers Logic
|
||||
let customersData = <?= json_encode($customers, JSON_UNESCAPED_UNICODE) ?>;
|
||||
const custInput = document.getElementById('formCustomer');
|
||||
const custDropdown = document.getElementById('formCustomerDropdown');
|
||||
|
||||
custInput.addEventListener('input', function() {
|
||||
const q = this.value.toLowerCase().trim();
|
||||
custDropdown.innerHTML = '';
|
||||
if (q.length < 2) {
|
||||
custDropdown.classList.remove('show');
|
||||
return;
|
||||
}
|
||||
|
||||
const matches = customersData.filter(c =>
|
||||
c.name.toLowerCase().includes(q) ||
|
||||
(c.phone && c.phone.toLowerCase().includes(q))
|
||||
).slice(0, 5);
|
||||
|
||||
if (matches.length > 0) {
|
||||
matches.forEach(c => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'search-item-row';
|
||||
div.innerHTML = `<strong>${c.name}</strong> ${c.phone ? '<small class="text-muted ms-2">'+c.phone+'</small>' : ''}`;
|
||||
div.onclick = function() {
|
||||
custInput.value = c.name + (c.phone ? ' - ' + c.phone : '');
|
||||
custDropdown.classList.remove('show');
|
||||
};
|
||||
custDropdown.appendChild(div);
|
||||
});
|
||||
custDropdown.classList.add('show');
|
||||
} else {
|
||||
custDropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!custInput.contains(e.target) && !custDropdown.contains(e.target)) {
|
||||
custDropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
let newCustomerModalObj = null;
|
||||
function openNewCustomerModal() {
|
||||
if (!newCustomerModalObj) {
|
||||
newCustomerModalObj = new bootstrap.Modal(document.getElementById('newCustomerModal'));
|
||||
}
|
||||
document.getElementById('ncName').value = '';
|
||||
document.getElementById('ncPhone').value = '';
|
||||
newCustomerModalObj.show();
|
||||
}
|
||||
|
||||
async function saveNewCustomer() {
|
||||
const name = document.getElementById('ncName').value.trim();
|
||||
const phone = document.getElementById('ncPhone').value.trim();
|
||||
if (!name) {
|
||||
alert('<?= h(tr('الاسم مطلوب', 'Name is required')) ?>');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('name', name);
|
||||
formData.append('phone', phone);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/customers.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
customersData.push(data.customer);
|
||||
custInput.value = data.customer.name + (data.customer.phone ? ' - ' + data.customer.phone : '');
|
||||
newCustomerModalObj.hide();
|
||||
const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 2000 });
|
||||
Toast.fire({ icon: 'success', title: '<?= h(tr('تم إضافة العميل', 'Customer added')) ?>' });
|
||||
} else {
|
||||
alert(data.error);
|
||||
}
|
||||
} catch(err) {
|
||||
alert('Error saving customer');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Search logic
|
||||
searchInput.addEventListener('input', function() {
|
||||
const q = this.value.toLowerCase().trim();
|
||||
dropdown.innerHTML = '';
|
||||
|
||||
if (q === '') {
|
||||
dropdown.classList.remove('show');
|
||||
return;
|
||||
}
|
||||
|
||||
const matches = catalogArray.filter(item => {
|
||||
const nameAr = (item.name_ar || '').toLowerCase();
|
||||
const nameEn = (item.name_en || '').toLowerCase();
|
||||
const sku = (item.sku || '').toLowerCase();
|
||||
return nameAr.includes(q) || nameEn.includes(q) || sku.includes(q);
|
||||
}).slice(0, 6);
|
||||
|
||||
if (matches.length > 0) {
|
||||
matches.forEach(item => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'search-item-row d-flex justify-content-between align-items-center';
|
||||
const name = '<?= current_lang() ?>' === 'ar' ? item.name_ar : item.name_en;
|
||||
div.innerHTML = `
|
||||
<div>
|
||||
<div class="fw-medium text-dark">${name}</div>
|
||||
<div class="text-muted small">SKU: ${item.sku}</div>
|
||||
</div>
|
||||
<div class="fw-semibold text-primary">${parseFloat(item.price).toFixed(3)}</div>
|
||||
`;
|
||||
div.onclick = () => {
|
||||
addItemToInvoice(item.sku);
|
||||
searchInput.value = '';
|
||||
dropdown.classList.remove('show');
|
||||
searchInput.focus();
|
||||
};
|
||||
dropdown.appendChild(div);
|
||||
});
|
||||
dropdown.classList.add('show');
|
||||
} else {
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Barcode scanner integration on enter
|
||||
searchInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const q = this.value.trim();
|
||||
if(q === '') return;
|
||||
|
||||
const match = catalogArray.find(item => item.sku === q);
|
||||
if (match) {
|
||||
addItemToInvoice(match.sku);
|
||||
searchInput.value = '';
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!searchInput.contains(e.target) && !dropdown.contains(e.target)) {
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
function addItemToInvoice(sku) {
|
||||
if (invoiceItems[sku]) {
|
||||
invoiceItems[sku].qty += 1;
|
||||
} else {
|
||||
const item = catalogData[sku];
|
||||
invoiceItems[sku] = {
|
||||
sku: sku,
|
||||
name: '<?= current_lang() ?>' === 'ar' ? item.name_ar : item.name_en,
|
||||
price: parseFloat(item.price),
|
||||
qty: 1
|
||||
};
|
||||
}
|
||||
renderInvoice();
|
||||
}
|
||||
|
||||
function changeQty(sku, newQty) {
|
||||
const qty = parseInt(newQty);
|
||||
if (isNaN(qty) || qty < 1) {
|
||||
delete invoiceItems[sku];
|
||||
} else {
|
||||
invoiceItems[sku].qty = qty;
|
||||
}
|
||||
renderInvoice();
|
||||
}
|
||||
|
||||
function removeItem(sku) {
|
||||
delete invoiceItems[sku];
|
||||
renderInvoice();
|
||||
}
|
||||
|
||||
function renderInvoice() {
|
||||
const skus = Object.keys(invoiceItems);
|
||||
if (skus.length === 0) {
|
||||
tbody.innerHTML = '';
|
||||
tbody.appendChild(emptyRow);
|
||||
updateTotals(0);
|
||||
cartJson.value = '[]';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = '';
|
||||
let totalAmount = 0;
|
||||
const cartData = [];
|
||||
|
||||
skus.forEach(sku => {
|
||||
const item = invoiceItems[sku];
|
||||
const lineTotal = item.qty * item.price;
|
||||
totalAmount += lineTotal;
|
||||
cartData.push({ sku: item.sku, qty: item.qty });
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<div class="fw-medium text-dark">${item.name}</div>
|
||||
<div class="text-muted small">SKU: ${item.sku}</div>
|
||||
</td>
|
||||
<td class="text-center text-muted align-middle">${item.price.toFixed(3)}</td>
|
||||
<td class="text-center align-middle">
|
||||
<input type="number" class="qty-control mx-auto fw-medium" min="1" value="${item.qty}" onchange="changeQty('${sku}', this.value)" onkeyup="if(event.key==='Enter') changeQty('${sku}', this.value)">
|
||||
</td>
|
||||
<td class="text-center fw-semibold text-dark align-middle">${lineTotal.toFixed(3)}</td>
|
||||
<td class="text-center align-middle">
|
||||
<button type="button" class="btn-remove mx-auto" onclick="removeItem('${sku}')" title="<?= h(tr('إزالة', 'Remove')) ?>">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
updateTotals(totalAmount);
|
||||
cartJson.value = JSON.stringify(cartData);
|
||||
}
|
||||
|
||||
function updateTotals(total) {
|
||||
document.getElementById('displaySubtotal').innerText = total.toFixed(3);
|
||||
document.getElementById('displayTotal').innerText = total.toFixed(3) + currencySuffix;
|
||||
}
|
||||
|
||||
// Intercept form submission to check if items exist
|
||||
document.getElementById('smart-sale-form').addEventListener('submit', function(e) {
|
||||
if (Object.keys(invoiceItems).length === 0) {
|
||||
e.preventDefault();
|
||||
alert('<?= h(tr('الرجاء إضافة أصناف للفاتورة أولاً.', 'Please add items to the invoice first.')) ?>');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php require __DIR__ . '/footer.php'; ?>
|
||||
|
||||
<?php require __DIR__ . '/footer.php'; ?>
|
||||
@ -1,17 +0,0 @@
|
||||
<?php
|
||||
$content = file_get_contents('includes/app.php');
|
||||
$content = str_replace(
|
||||
'available' => max(0, $base - $used),
|
||||
'price' => $item['price']
|
||||
,
|
||||
'available' => max(0, $base - $used),
|
||||
'price' => $item['price'],
|
||||
'category_id' => $item['category_id'],
|
||||
'supplier_id' => $item['supplier_id'],
|
||||
'image_url' => $item['image_url'],
|
||||
'vat' => $item['vat']
|
||||
,
|
||||
$content
|
||||
);
|
||||
file_put_contents('includes/app.php', $content);
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
$content = file_get_contents('stock.php');
|
||||
$content = str_replace(
|
||||
"function openItemModal(sku = '', name = '', price = '', base_stock = '') {",
|
||||
"function openItemModal(sku = '', name = '', price = '', base_stock = '', vat = '5', category_id = '', supplier_id = '', image_url = '') {",
|
||||
$content
|
||||
);
|
||||
$content = str_replace(
|
||||
"document.getElementById('item_vat').value = '5';",
|
||||
"document.getElementById('item_vat').value = vat;\n document.getElementById('item_category').value = category_id;\n document.getElementById('item_supplier').value = supplier_id;\n \n // Remove old image preview if any\n const oldPreview = document.getElementById('image_preview');\n if (oldPreview) oldPreview.remove();\n \n if (image_url) {\n const preview = document.createElement('img');\n preview.id = 'image_preview';\n preview.src = image_url;\n preview.style.maxHeight = '100px';\n preview.className = 'mt-2 rounded';\n document.getElementById('item_picture').parentElement.appendChild(preview);\n }",
|
||||
$content
|
||||
);
|
||||
$content = str_replace(
|
||||
"document.getElementById('item_category').value = '';\n document.getElementById('item_supplier').value = '';",
|
||||
"",
|
||||
$content
|
||||
);
|
||||
file_put_contents('stock.php', $content);
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
<?php
|
||||
$content = file_get_contents('stock.php');
|
||||
$content = preg_replace('/(\s*)<td><\?= h\(\$row[\'name\']\) \?><\/td>/', "$1<td>\n$1 <?php if (!empty(\$row['image_url'])): ?>\n$1 <img src=\"<?= h(\$row['image_url']) ?>\" alt=\"\" class=\"rounded\" style=\"width: 40px; height: 40px; object-fit: cover;\">\n$1 <?php else: ?>\n$1 <div class=\"bg-light rounded d-flex align-items-center justify-content-center text-muted\" style=\"width: 40px; height: 40px;\"><i class=\"bi bi-image\"></i></div>\n$1 <?php endif; ?>\n$1</td>\n$1<td><?= h(\$row['name']) ?></td>", $content);
|
||||
file_put_contents('stock.php', $content);
|
||||
150
pos.php
150
pos.php
@ -11,8 +11,10 @@ $allowedBranches = $user['role'] === 'owner' ? array_keys(branches()) : [$user['
|
||||
try {
|
||||
$pdo = db();
|
||||
$categories = $pdo->query('SELECT id, name_ar, name_en FROM categories ORDER BY name_ar ASC')->fetchAll();
|
||||
$customers = $pdo->query('SELECT id, name, phone FROM customers ORDER BY name ASC')->fetchAll();
|
||||
} catch (Throwable $e) {
|
||||
$categories = [];
|
||||
$customers = [];
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
@ -75,7 +77,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
]);
|
||||
|
||||
set_flash('success', tr('تم حفظ عملية POS بنجاح.', 'POS sale saved successfully.'));
|
||||
redirect_to('sale.php', ['id' => $saleId]);
|
||||
redirect_to('print_receipt.php', ['id' => $saleId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -336,9 +338,15 @@ require __DIR__ . '/includes/header.php';
|
||||
<div class="pos-products-area">
|
||||
<!-- Top Bar: Search & Hold -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||
<div class="position-relative" style="width: 300px;">
|
||||
<input type="text" id="posSearch" class="form-control rounded-pill ps-4" placeholder="<?= h(tr('بحث عن صنف...', 'Search item...')) ?>" autocomplete="off">
|
||||
<i class="bi bi-search position-absolute top-50 translate-middle-y text-muted" style="left: 15px;"></i>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<div class="position-relative" style="width: 200px;">
|
||||
<input type="text" id="posBarcode" class="form-control rounded-pill ps-4 border-primary shadow-sm" placeholder="<?= h(tr('الباركود...', 'Barcode...')) ?>" autocomplete="off" autofocus>
|
||||
<i class="bi bi-upc-scan position-absolute top-50 translate-middle-y text-primary" style="left: 15px;"></i>
|
||||
</div>
|
||||
<div class="position-relative" style="width: 250px;">
|
||||
<input type="text" id="posSearch" class="form-control rounded-pill ps-4" placeholder="<?= h(tr('بحث بالاسم...', 'Search by name...')) ?>" autocomplete="off">
|
||||
<i class="bi bi-search position-absolute top-50 translate-middle-y text-muted" style="left: 15px;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-outline-primary rounded-pill px-4 shadow-sm" onclick="openHeldOrdersModal()">
|
||||
@ -403,7 +411,13 @@ require __DIR__ . '/includes/header.php';
|
||||
<option value="<?= h($branchCode) ?>"><?= $branchLabel ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<input type="text" id="posCustomer" class="form-control border-0 shadow-sm rounded-3" placeholder="<?= h(tr('اسم العميل (اختياري)', 'Customer Name (Optional)')) ?>">
|
||||
<div class="input-group position-relative">
|
||||
<input type="text" id="posCustomer" class="form-control border-0 shadow-sm rounded-start-3" placeholder="<?= h(tr('بحث عن عميل (اسم أو هاتف)', 'Search Customer (Name or Phone)')) ?>" autocomplete="off">
|
||||
<button class="btn btn-primary border-0 shadow-sm rounded-end-3" onclick="openNewCustomerModal()" type="button" title="<?= h(tr('إضافة عميل', 'Add Customer')) ?>">
|
||||
<i class="bi bi-person-plus-fill"></i>
|
||||
</button>
|
||||
<div id="customerDropdown" class="list-group position-absolute w-100 shadow-lg d-none" style="top: 100%; left: 0; z-index: 1050; max-height: 200px; overflow-y: auto; border-radius: 8px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cart-items" id="cartItemsList">
|
||||
@ -476,6 +490,31 @@ require __DIR__ . '/includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Customer Modal -->
|
||||
<div class="modal fade" id="newCustomerModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-sm">
|
||||
<div class="modal-content border-0 shadow-lg" style="border-radius: 16px;">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h5 class="modal-title fw-bold"><?= h(tr('إضافة عميل', 'Add Customer')) ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1"><?= h(tr('الاسم', 'Name')) ?> <span class="text-danger">*</span></label>
|
||||
<input type="text" id="ncName" class="form-control rounded-3">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted small mb-1"><?= h(tr('رقم الهاتف', 'Phone')) ?></label>
|
||||
<input type="text" id="ncPhone" class="form-control rounded-3" dir="ltr">
|
||||
</div>
|
||||
<div class="d-grid mt-4">
|
||||
<button class="btn btn-primary rounded-pill fw-semibold shadow-sm" onclick="saveNewCustomer()"><?= h(tr('حفظ العميل', 'Save Customer')) ?></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Held Orders Modal -->
|
||||
<div class="modal fade" id="heldOrdersModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
@ -496,8 +535,91 @@ require __DIR__ . '/includes/header.php';
|
||||
<script>
|
||||
let cart = {};
|
||||
let catalogData = <?= json_encode($catalog, JSON_UNESCAPED_UNICODE) ?>;
|
||||
let customersData = <?= json_encode($customers, JSON_UNESCAPED_UNICODE) ?>;
|
||||
let currencyLabel = '<?= h(tr('ر.ع', 'OMR')) ?>';
|
||||
|
||||
// Customer Autocomplete & Add Logic
|
||||
const custInput = document.getElementById('posCustomer');
|
||||
const custDropdown = document.getElementById('customerDropdown');
|
||||
|
||||
custInput.addEventListener('input', function() {
|
||||
const q = this.value.toLowerCase().trim();
|
||||
custDropdown.innerHTML = '';
|
||||
if (q.length < 2) {
|
||||
custDropdown.classList.add('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
const matches = customersData.filter(c =>
|
||||
c.name.toLowerCase().includes(q) ||
|
||||
(c.phone && c.phone.toLowerCase().includes(q))
|
||||
).slice(0, 5);
|
||||
|
||||
if (matches.length > 0) {
|
||||
matches.forEach(c => {
|
||||
const a = document.createElement('a');
|
||||
a.className = 'list-group-item list-group-item-action cursor-pointer border-0 border-bottom';
|
||||
a.innerHTML = `<strong>${c.name}</strong> ${c.phone ? '<small class="text-muted ms-2">'+c.phone+'</small>' : ''}`;
|
||||
a.onclick = function() {
|
||||
custInput.value = c.name + (c.phone ? ' - ' + c.phone : '');
|
||||
custDropdown.classList.add('d-none');
|
||||
};
|
||||
custDropdown.appendChild(a);
|
||||
});
|
||||
custDropdown.classList.remove('d-none');
|
||||
} else {
|
||||
custDropdown.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!custInput.contains(e.target) && !custDropdown.contains(e.target)) {
|
||||
custDropdown.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
let newCustomerModalObj = null;
|
||||
function openNewCustomerModal() {
|
||||
if (!newCustomerModalObj) {
|
||||
newCustomerModalObj = new bootstrap.Modal(document.getElementById('newCustomerModal'));
|
||||
}
|
||||
document.getElementById('ncName').value = '';
|
||||
document.getElementById('ncPhone').value = '';
|
||||
newCustomerModalObj.show();
|
||||
}
|
||||
|
||||
async function saveNewCustomer() {
|
||||
const name = document.getElementById('ncName').value.trim();
|
||||
const phone = document.getElementById('ncPhone').value.trim();
|
||||
if (!name) {
|
||||
alert('<?= h(tr('الاسم مطلوب', 'Name is required')) ?>');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('name', name);
|
||||
formData.append('phone', phone);
|
||||
|
||||
try {
|
||||
const res = await fetch('api/customers.php', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
customersData.push(data.customer);
|
||||
custInput.value = data.customer.name + (data.customer.phone ? ' - ' + data.customer.phone : '');
|
||||
newCustomerModalObj.hide();
|
||||
const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 2000 });
|
||||
Toast.fire({ icon: 'success', title: '<?= h(tr('تم إضافة العميل', 'Customer added')) ?>' });
|
||||
} else {
|
||||
alert(data.error);
|
||||
}
|
||||
} catch(err) {
|
||||
alert('Error saving customer');
|
||||
}
|
||||
}
|
||||
|
||||
// Product Grid Filtering & Searching
|
||||
function filterCat(catId) {
|
||||
document.querySelectorAll('.cat-btn').forEach(btn => btn.classList.remove('active'));
|
||||
@ -507,6 +629,22 @@ function filterCat(catId) {
|
||||
|
||||
document.getElementById('posSearch').addEventListener('input', applyFilters);
|
||||
|
||||
document.getElementById('posBarcode').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const sku = this.value.trim();
|
||||
if (sku === '') return;
|
||||
|
||||
if (catalogData[sku]) {
|
||||
addToCart(sku);
|
||||
} else {
|
||||
const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 2000 });
|
||||
Toast.fire({ icon: 'error', title: '<?= h(tr('الصنف غير موجود', 'Item not found')) ?>' });
|
||||
}
|
||||
this.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
function applyFilters() {
|
||||
const q = document.getElementById('posSearch').value.toLowerCase();
|
||||
const activeCat = document.querySelector('.cat-btn.active').dataset.cat;
|
||||
@ -772,4 +910,4 @@ function deleteHeldOrder(index) {
|
||||
|
||||
</script>
|
||||
|
||||
<?php require __DIR__ . '/includes/footer.php'; ?>
|
||||
<?php require __DIR__ . '/includes/footer.php'; ?>
|
||||
|
||||
284
print_receipt.php
Normal file
284
print_receipt.php
Normal file
@ -0,0 +1,284 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
$user = require_roles(['owner', 'manager', 'cashier']);
|
||||
$id = (int) ($_GET['id'] ?? 0);
|
||||
$sale = null;
|
||||
$dbError = null;
|
||||
if ($id > 0) {
|
||||
try {
|
||||
$sale = fetch_sale($id);
|
||||
} catch (Throwable $e) {
|
||||
$dbError = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
if (!$sale) {
|
||||
die("Sale not found.");
|
||||
}
|
||||
|
||||
// Receipt Configuration
|
||||
$storeName = tr('متجر فلات لوجيك', 'Flatlogic Store');
|
||||
$storeAddress = tr('شارع الملك فهد، الرياض، السعودية', 'King Fahd Road, Riyadh, KSA');
|
||||
$vatNo = '300123456789012';
|
||||
$registerNo = 'REG-01';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?= current_lang() ?>" dir="<?= current_lang() === 'ar' ? 'rtl' : 'ltr' ?>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= h(tr('إيصال', 'Receipt')) ?> #<?= h($sale['receipt_no']) ?></title>
|
||||
<style>
|
||||
/* 80mm Receipt Styles */
|
||||
body {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
font-size: 13px;
|
||||
color: #000;
|
||||
background: #f8f9fa;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.receipt-container {
|
||||
width: 80mm;
|
||||
max-width: 100%;
|
||||
background: #fff;
|
||||
padding: 15px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
@media print {
|
||||
body {
|
||||
background: #fff;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.receipt-container {
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
@page {
|
||||
margin: 0;
|
||||
size: 80mm 297mm; /* standard 80mm paper roll size */
|
||||
}
|
||||
}
|
||||
.text-center { text-align: center; }
|
||||
.text-right { text-align: right; }
|
||||
.text-left { text-align: left; }
|
||||
.font-bold { font-weight: bold; }
|
||||
|
||||
.logo-area {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.logo-area img {
|
||||
max-width: 60px;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.header-info { margin-bottom: 15px; line-height: 1.4; }
|
||||
.header-info div { margin-bottom: 2px; }
|
||||
|
||||
.divider {
|
||||
border-bottom: 1px dashed #000;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.sale-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
table.items {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
}
|
||||
table.items th, table.items td {
|
||||
padding: 4px 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
table.items th {
|
||||
border-bottom: 1px dashed #000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.col-qty { width: 15%; text-align: center; }
|
||||
.col-price { width: 25%; text-align: <?= current_lang() === 'ar' ? 'left' : 'right' ?>; }
|
||||
.col-total { width: 25%; text-align: <?= current_lang() === 'ar' ? 'left' : 'right' ?>; font-weight: bold; }
|
||||
.col-name { width: 35%; }
|
||||
|
||||
.totals-area {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.totals-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.totals-row.grand-total {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
border-top: 1px dashed #000;
|
||||
border-bottom: 1px dashed #000;
|
||||
padding: 8px 0;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.footer-msg {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.barcode {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
font-family: 'Libre Barcode 39', cursive;
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
/* Interactive actions */
|
||||
.print-actions {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.btn {
|
||||
background: #0d6efd;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
}
|
||||
.btn-secondary { background: #6c757d; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="receipt-container">
|
||||
|
||||
<!-- Logo -->
|
||||
<div class="text-center logo-area">
|
||||
<!-- SVG Placeholder Logo -->
|
||||
<svg width="60" height="60" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100" height="100" rx="20" fill="#000"/>
|
||||
<path d="M50 20 L80 80 L20 80 Z" fill="#fff"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Store Info -->
|
||||
<div class="text-center header-info">
|
||||
<div class="font-bold" style="font-size: 16px;"><?= h($storeName) ?></div>
|
||||
<div><?= h($storeAddress) ?></div>
|
||||
<div>VAT: <?= h($vatNo) ?></div>
|
||||
<div><?= h(tr('هاتف', 'Tel')) ?>: 920000000</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Receipt Meta -->
|
||||
<div class="sale-info">
|
||||
<span><?= h(tr('رقم الفاتورة', 'Receipt No')) ?>:</span>
|
||||
<span class="font-bold"><?= h($sale['receipt_no']) ?></span>
|
||||
</div>
|
||||
<div class="sale-info">
|
||||
<span><?= h(tr('التاريخ', 'Date')) ?>:</span>
|
||||
<span><?= h(date('Y-m-d H:i', strtotime((string)$sale['sale_date']))) ?></span>
|
||||
</div>
|
||||
<div class="sale-info">
|
||||
<span><?= h(tr('الكاشير', 'Cashier')) ?>:</span>
|
||||
<span><?= h($sale['cashier_name']) ?> (<?= h($registerNo) ?>)</span>
|
||||
</div>
|
||||
<?php if(!empty($sale['customer_name'])): ?>
|
||||
<div class="sale-info">
|
||||
<span><?= h(tr('العميل', 'Customer')) ?>:</span>
|
||||
<span><?= h($sale['customer_name']) ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Items -->
|
||||
<table class="items">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-<?= current_lang() === 'ar' ? 'right' : 'left' ?> col-name"><?= h(tr('الصنف', 'Item')) ?></th>
|
||||
<th class="col-qty"><?= h(tr('كمية', 'Qty')) ?></th>
|
||||
<th class="col-price"><?= h(tr('سعر', 'Price')) ?></th>
|
||||
<th class="col-total"><?= h(tr('إجمالي', 'Total')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($sale['items'] as $item):
|
||||
$name = current_lang() === 'ar' ? ($item['name_ar'] ?? $item['sku']) : ($item['name_en'] ?? $item['sku']);
|
||||
?>
|
||||
<tr>
|
||||
<td class="col-name"><?= h($name) ?></td>
|
||||
<td class="col-qty"><?= h($item['qty']) ?></td>
|
||||
<td class="col-price"><?= number_format((float)$item['price'], 3) ?></td>
|
||||
<td class="col-total"><?= number_format((float)$item['line_total'], 3) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Totals -->
|
||||
<div class="totals-area">
|
||||
<div class="totals-row">
|
||||
<span><?= h(tr('المجموع الفرعي', 'Subtotal')) ?></span>
|
||||
<span><?= number_format((float)$sale['subtotal'], 3) ?></span>
|
||||
</div>
|
||||
<div class="totals-row">
|
||||
<span><?= h(tr('ضريبة القيمة المضافة (15%)', 'VAT (15%)')) ?></span>
|
||||
<span><?= h(tr('شامل', 'Inclusive')) ?></span>
|
||||
</div>
|
||||
<div class="totals-row grand-total">
|
||||
<span><?= h(tr('الإجمالي', 'Total')) ?></span>
|
||||
<span><?= number_format((float)$sale['total_amount'], 3) ?> <?= h(tr('ر.ع', 'OMR')) ?></span>
|
||||
</div>
|
||||
<div class="totals-row">
|
||||
<span><?= h(tr('طريقة الدفع', 'Payment Method')) ?></span>
|
||||
<span><?= h(ucfirst((string)$sale['payment_method'])) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="footer-msg">
|
||||
<p class="font-bold"><?= h(tr('شكراً لتسوقكم معنا!', 'Thank you for shopping with us!')) ?></p>
|
||||
<p><?= h(tr('البضاعة المباعة ترد وتستبدل خلال 14 يوماً من تاريخ الشراء', 'Items can be returned or exchanged within 14 days of purchase.')) ?></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Print Actions -->
|
||||
<div class="print-actions no-print">
|
||||
<a href="pos.php" class="btn btn-secondary"><?= h(tr('رجوع لـ POS', 'Back to POS')) ?></a>
|
||||
<button onclick="window.print()" class="btn"><?= h(tr('طباعة الآن', 'Print Now')) ?></button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Auto print when page loads
|
||||
window.onload = function() {
|
||||
setTimeout(() => {
|
||||
window.print();
|
||||
}, 500);
|
||||
};
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
371
sale.php
371
sale.php
@ -1,7 +1,7 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
$user = require_roles(['owner', 'manager', 'cashier']);
|
||||
$pageTitle = tr('تفاصيل الفاتورة', 'Sale Detail');
|
||||
$pageTitle = tr('تفاصيل الفاتورة الضريبية', 'Tax Invoice Details');
|
||||
$activeNav = 'sales';
|
||||
$id = (int) ($_GET['id'] ?? 0);
|
||||
$sale = null;
|
||||
@ -13,60 +13,321 @@ if ($id > 0) {
|
||||
$dbError = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// Company Info for Invoice
|
||||
$companyName = tr('متجر فلات لوجيك', 'Flatlogic Store');
|
||||
$companyAddress = tr('شارع الملك فهد، الرياض، المملكة العربية السعودية', 'King Fahd Road, Riyadh, KSA');
|
||||
$companyVat = '300123456789012';
|
||||
$companyEmail = 'info@flatlogic.com';
|
||||
$companyPhone = '920000000';
|
||||
|
||||
require __DIR__ . '/includes/header.php';
|
||||
?>
|
||||
<section class="surface-card mb-4">
|
||||
<?php if ($dbError): ?>
|
||||
<div class="alert alert-warning"><?= h($dbError) ?></div>
|
||||
<?php elseif (!$sale): ?>
|
||||
<div class="empty-state">
|
||||
<h4><?= h(tr('الفاتورة غير موجودة', 'Sale not found')) ?></h4>
|
||||
<p><?= h(tr('قد تكون الفاتورة خارج صلاحية هذا الحساب أو لم تعد موجودة.', 'The sale may be outside this account scope or no longer exists.')) ?></p>
|
||||
<a class="btn btn-outline-secondary" href="<?= h(url_for('sales.php')) ?>"><?= h(tr('العودة إلى المبيعات', 'Back to sales')) ?></a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap mb-4">
|
||||
<div>
|
||||
<div class="eyebrow"><?= h(sale_mode_label((string) $sale['sale_mode'])) ?></div>
|
||||
<h3 class="h4 mb-1"><?= h($sale['receipt_no']) ?></h3>
|
||||
<div class="small text-muted"><?= h(branch_label((string) $sale['branch_code'])) ?> · <?= h(date('Y-m-d H:i', strtotime((string) $sale['sale_date']))) ?></div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button type="button" class="btn btn-dark" data-print-page><?= h(tr('طباعة الإيصال', 'Print receipt')) ?></button>
|
||||
<a class="btn btn-outline-secondary" href="<?= h(url_for('sales.php')) ?>"><?= h(tr('العودة للسجل', 'Back to ledger')) ?></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3"><div class="detail-card"><span><?= h(tr('العميل', 'Customer')) ?></span><strong><?= h((string) ($sale['customer_name'] ?: tr('دون اسم', 'Walk-in'))) ?></strong></div></div>
|
||||
<div class="col-md-3"><div class="detail-card"><span><?= h(tr('الدفع', 'Payment')) ?></span><strong><?= h(ucfirst((string) $sale['payment_method'])) ?></strong></div></div>
|
||||
<div class="col-md-3"><div class="detail-card"><span><?= h(tr('الكاشير', 'Cashier')) ?></span><strong><?= h((string) $sale['cashier_name']) ?></strong></div></div>
|
||||
<div class="col-md-3"><div class="detail-card"><span><?= h(tr('الإجمالي', 'Total')) ?></span><strong><?= h(currency((float) $sale['total_amount'])) ?></strong></div></div>
|
||||
</div>
|
||||
<div class="table-responsive shadow-sm" style="border-radius: 12px; overflow: hidden; border: 1px solid rgba(0,0,0,0.05);">
|
||||
<table class="table table-hover align-middle mb-0 text-center" style="background-color: #fff;">
|
||||
<thead style="background: linear-gradient(90deg, #0d6efd, #0dcaf0);">
|
||||
<tr>
|
||||
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الصنف', 'Item')) ?></th>
|
||||
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الكمية', 'Qty')) ?></th>
|
||||
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('السعر', 'Price')) ?></th>
|
||||
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الإجمالي', 'Line total')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="border-top-0">
|
||||
<?php foreach ($sale['items'] as $item): ?>
|
||||
<tr>
|
||||
<td><?= h(current_lang() === 'ar' ? ($item['name_ar'] ?? $item['sku']) : ($item['name_en'] ?? $item['sku'])) ?></td>
|
||||
<td><?= h((string) ($item['qty'] ?? 0)) ?></td>
|
||||
<td><?= h(currency((float) ($item['price'] ?? 0))) ?></td>
|
||||
<td><?= h(currency((float) ($item['line_total'] ?? 0))) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if (!empty($sale['notes'])): ?>
|
||||
<div class="alert alert-light border mb-0"><strong><?= h(tr('ملاحظات:', 'Notes:')) ?></strong> <?= h((string) $sale['notes']) ?></div>
|
||||
|
||||
<style>
|
||||
/* Full Page Borderless Invoice */
|
||||
.invoice-page {
|
||||
background: #fff;
|
||||
min-height: 80vh;
|
||||
max-width: 900px; margin: 0 auto; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.08); overflow: hidden;
|
||||
padding: 4rem 3rem;
|
||||
position: relative;
|
||||
}
|
||||
.invoice-ribbon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
background: linear-gradient(90deg, #212529, #6c757d);
|
||||
}
|
||||
.invoice-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
.company-details h2 {
|
||||
font-weight: 800;
|
||||
color: #212529;
|
||||
font-size: 2.5rem;
|
||||
letter-spacing: -1px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.company-details p {
|
||||
color: #adb5bd;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.invoice-meta {
|
||||
text-align: <?= current_lang() === 'ar' ? 'left' : 'right' ?>;
|
||||
}
|
||||
.invoice-meta h1 {
|
||||
font-size: 4rem;
|
||||
font-weight: 900;
|
||||
color: #f8f9fa;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
line-height: 1;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.meta-box-row {
|
||||
display: flex;
|
||||
justify-content: <?= current_lang() === 'ar' ? 'flex-start' : 'flex-end' ?>;
|
||||
gap: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.meta-label {
|
||||
color: #adb5bd;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.meta-val {
|
||||
font-weight: 700;
|
||||
color: #212529;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.invoice-parties {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4rem;
|
||||
padding-top: 3rem;
|
||||
border-top: 1px dashed #dee2e6;
|
||||
}
|
||||
.party-box {
|
||||
flex: 1;
|
||||
}
|
||||
.party-title {
|
||||
font-size: 0.85rem;
|
||||
color: #adb5bd;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 1rem;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.party-info h4 {
|
||||
font-weight: 800;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #212529;
|
||||
}
|
||||
.party-info p {
|
||||
color: #6c757d;
|
||||
margin-bottom: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.invoice-table {
|
||||
width: 100%;
|
||||
margin-bottom: 4rem;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.invoice-table th {
|
||||
color: #adb5bd;
|
||||
font-weight: 600;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 2px solid #212529;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.invoice-table td {
|
||||
padding: 1.5rem 0;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.invoice-table tr:last-child td { border-bottom: 1px solid #dee2e6; }
|
||||
.totals-section {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
.totals-box {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
.totals-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.2rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
.totals-row.grand-total {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 900;
|
||||
color: #212529;
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 2px solid #212529;
|
||||
margin-bottom: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.invoice-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
color: #adb5bd;
|
||||
font-size: 0.95rem;
|
||||
border-top: 1px dashed #dee2e6;
|
||||
padding-top: 3rem;
|
||||
}
|
||||
.qr-placeholder {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" fill="%23f8f9fa"/><path d="M20 20h20v20H20zM60 20h20v20H60zM20 60h20v20H20zM50 50h10v10H50zM70 60h10v10H70zM60 70h10v10H60z" fill="%23dee2e6"/></svg>') center/cover;
|
||||
}
|
||||
.print-actions {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
z-index: 100;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 1rem;
|
||||
border-radius: 100px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
body { background: #fff; margin: 0; padding: 0; }
|
||||
.main-sidebar, .main-header, .print-actions, .alert { display: none !important; }
|
||||
.main-content { margin: 0 !important; padding: 0 !important; width: 100% !important; }
|
||||
.invoice-page { padding: 0; margin: 0; min-height: auto; }
|
||||
@page { size: A4; margin: 1.5cm; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container-fluid mb-5">
|
||||
<?php if ($dbError): ?>
|
||||
<div class="alert alert-warning"><?= h($dbError) ?></div>
|
||||
<?php elseif (!$sale): ?>
|
||||
<div class="empty-state bg-white rounded-4 shadow-sm p-5 text-center">
|
||||
<i class="bi bi-file-earmark-x fs-1 text-muted d-block mb-3"></i>
|
||||
<h4><?= h(tr('الفاتورة غير موجودة', 'Sale not found')) ?></h4>
|
||||
<p class="text-muted"><?= h(tr('قد تكون الفاتورة خارج صلاحية هذا الحساب أو لم تعد موجودة.', 'The sale may be outside this account scope or no longer exists.')) ?></p>
|
||||
<a class="btn btn-outline-secondary mt-3 rounded-pill px-4" href="<?= h(url_for('sales.php')) ?>"><?= h(tr('العودة إلى المبيعات', 'Back to sales')) ?></a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
<!-- Print Actions (Hidden when printing) -->
|
||||
<div class="print-actions d-flex justify-content-between align-items-center mb-4 mx-auto" style="max-width: 800px;">
|
||||
<a href="sales.php" class="btn btn-link text-muted text-decoration-none">
|
||||
<i class="bi bi-arrow-<?= current_lang() === 'ar' ? 'right' : 'left' ?> me-1"></i> <?= h(tr('رجوع للسجل', 'Back to ledger')) ?>
|
||||
</a>
|
||||
<button onclick="window.print()" class="btn btn-dark rounded-pill px-4 shadow-sm fs-6">
|
||||
<i class="bi bi-printer me-2"></i><?= h(tr('طباعة الفاتورة (A4)', 'Print Invoice (A4)')) ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Invoice A4 Document -->
|
||||
<div class="invoice-page">
|
||||
<div class="invoice-ribbon"></div>
|
||||
|
||||
<div class="invoice-header flex-column flex-md-row">
|
||||
<div class="company-details mb-4 mb-md-0">
|
||||
<h2><?= h($companyName) ?></h2>
|
||||
<p><?= h($companyAddress) ?></p>
|
||||
<p>VAT NO: <?= h($companyVat) ?></p>
|
||||
<p><?= h($companyEmail) ?> • <?= h($companyPhone) ?></p>
|
||||
</div>
|
||||
<div class="invoice-meta">
|
||||
<h1><?= ($sale['status'] ?? 'completed') === 'order' ? h(tr('طلب حجز', 'ORDER')) : h(tr('فاتورة ضريبية', 'TAX INVOICE')) ?></h1>
|
||||
<div class="meta-box-row">
|
||||
<span class="meta-label"><?= h(tr('رقم الفاتورة', 'Invoice No.')) ?></span>
|
||||
<span class="meta-val">#<?= h($sale['receipt_no']) ?></span>
|
||||
</div>
|
||||
<div class="meta-box-row">
|
||||
<span class="meta-label"><?= h(tr('تاريخ الإصدار', 'Date Issued')) ?></span>
|
||||
<span class="meta-val"><?= h(date('Y-m-d', strtotime((string) $sale['sale_date']))) ?></span>
|
||||
</div>
|
||||
<div class="meta-box-row">
|
||||
<span class="meta-label"><?= h(tr('طريقة الدفع', 'Payment')) ?></span>
|
||||
<span class="meta-val"><?= h(ucfirst((string) $sale['payment_method'])) ?></span>
|
||||
</div>
|
||||
<div class="meta-box-row">
|
||||
<span class="meta-label"><?= h(tr('الحالة', 'Status')) ?></span>
|
||||
<span class="meta-val"><?= ($sale['status'] ?? 'completed') === 'order' ? h(tr('طلب حجز (غير مدفوع)\", 'Order (Unpaid)\")) : h(tr('مدفوعة', 'Paid')) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="invoice-parties">
|
||||
<div class="party-box">
|
||||
<div class="party-title"><?= h(tr('فاتورة إلى', 'Invoice To')) ?></div>
|
||||
<div class="party-info">
|
||||
<h4><?= h((string) ($sale['customer_name'] ?: tr('عميل نقدي', 'Walk-in Customer'))) ?></h4>
|
||||
<p><?= h(tr('الفرع:', 'Branch:')) ?> <?= h(branch_label((string) $sale['branch_code'])) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="party-box text-<?= current_lang() === 'ar' ? 'left' : 'right' ?>">
|
||||
<div class="party-title"><?= h(tr('بواسطة', 'Served By')) ?></div>
|
||||
<div class="party-info">
|
||||
<h4><?= h((string) $sale['cashier_name']) ?></h4>
|
||||
<p><?= h(tr('موظف مبيعات', 'Sales Rep')) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="invoice-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50%" class="text-<?= current_lang() === 'ar' ? 'right' : 'left' ?>"><?= h(tr('وصف الصنف', 'Item Description')) ?></th>
|
||||
<th width="15%" class="text-center"><?= h(tr('السعر', 'Price')) ?></th>
|
||||
<th width="15%" class="text-center"><?= h(tr('الكمية', 'Qty')) ?></th>
|
||||
<th width="20%" class="text-<?= current_lang() === 'ar' ? 'left' : 'right' ?>"><?= h(tr('الإجمالي', 'Line Total')) ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($sale['items'] as $item): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-bold fs-5 text-dark"><?= h(current_lang() === 'ar' ? ($item['name_ar'] ?? $item['sku']) : ($item['name_en'] ?? $item['sku'])) ?></div>
|
||||
<div class="text-muted">SKU: <?= h($item['sku']) ?></div>
|
||||
</td>
|
||||
<td class="text-center fs-5"><?= h(number_format((float) ($item['price'] ?? 0), 3)) ?></td>
|
||||
<td class="text-center fs-5 fw-bold"><?= h((string) ($item['qty'] ?? 0)) ?></td>
|
||||
<td class="text-<?= current_lang() === 'ar' ? 'left' : 'right' ?> fs-4 fw-bold text-dark"><?= h(number_format((float) ($item['line_total'] ?? 0), 3)) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="totals-section">
|
||||
<div class="totals-box">
|
||||
<div class="totals-row">
|
||||
<span><?= h(tr('المجموع الفرعي', 'Subtotal')) ?></span>
|
||||
<span class="text-dark fw-bold"><?= h(number_format((float) $sale['subtotal'], 3)) ?></span>
|
||||
</div>
|
||||
<div class="totals-row">
|
||||
<span><?= h(tr('ضريبة القيمة المضافة (15%)', 'VAT (15%)')) ?></span>
|
||||
<span class="text-success"><?= h(tr('شامل', 'Inclusive')) ?></span>
|
||||
</div>
|
||||
<div class="totals-row grand-total">
|
||||
<span><?= h(tr('الإجمالي', 'Total')) ?></span>
|
||||
<span><?= h(number_format((float) $sale['total_amount'], 3)) ?> <small class="fs-5 text-muted"><?= h(tr('ر.ع', 'OMR')) ?></small></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($sale['notes'])): ?>
|
||||
<div class="mb-5 px-4 py-3 bg-light" style="border-left: 4px solid #dee2e6;">
|
||||
<strong class="text-uppercase text-muted fs-6 d-block mb-1"><?= h(tr('ملاحظات', 'Notes')) ?></strong>
|
||||
<span class="fs-5 text-dark"><?= h((string) $sale['notes']) ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="invoice-footer">
|
||||
<div>
|
||||
<h5 class="fw-bold text-dark mb-1"><?= h(tr('شكراً لتعاملكم معنا!', 'Thank you for your business!')) ?></h5>
|
||||
<p class="mb-0"><?= h(tr('هذه الفاتورة معتمدة ضريبياً، يُرجى الاحتفاظ بها لضمان حقوقك.', 'This is a certified tax invoice. Please keep it for your records.')) ?></p>
|
||||
</div>
|
||||
<div class="qr-placeholder"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php require __DIR__ . '/includes/footer.php'; ?>
|
||||
</div>
|
||||
|
||||
<?php require __DIR__ . '/includes/footer.php'; ?>
|
||||
29
sales.php
29
sales.php
@ -109,6 +109,7 @@ require __DIR__ . '/includes/header.php';
|
||||
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('النوع', 'Type')) ?></th>
|
||||
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الكاشير', 'Cashier')) ?></th>
|
||||
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الإجمالي', 'Total')) ?></th>
|
||||
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الحالة', 'Status')) ?></th>
|
||||
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('التاريخ', 'Date')) ?></th>
|
||||
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('إجراءات', 'Actions')) ?></th>
|
||||
</tr>
|
||||
@ -124,8 +125,20 @@ require __DIR__ . '/includes/header.php';
|
||||
<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>
|
||||
<td class="fw-semibold"><?= h(currency((float) $sale['total_amount'])) ?></td>
|
||||
<td>
|
||||
<?php if (($sale['status'] ?? 'completed') === 'order'): ?>
|
||||
<span class="badge bg-warning text-dark px-3 py-2 rounded-pill"><i class="bi bi-clock"></i> <?= h(tr('طلب حجز', 'Order')) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-success px-3 py-2 rounded-pill"><i class="bi bi-check-circle"></i> <?= h(tr('مدفوع', 'Paid')) ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= h(date('Y-m-d H:i', strtotime((string) $sale['sale_date']))) ?></td>
|
||||
<td>
|
||||
<?php if (($sale['status'] ?? 'completed') === 'order'): ?>
|
||||
<button class="btn btn-sm btn-outline-success rounded-circle shadow-sm me-1" style="width: 34px; height: 34px; padding: 0;" onclick="markAsPaid(<?= $sale['id'] ?>)" title="<?= h(tr('تأكيد الدفع', 'Confirm Payment')) ?>">
|
||||
<i class="bi bi-check-lg"></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>
|
||||
@ -157,6 +170,22 @@ require __DIR__ . '/includes/header.php';
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function markAsPaid(id) {
|
||||
Swal.fire({
|
||||
title: "<?= h(tr('تأكيد الدفع والاستلام؟', 'Confirm payment and pickup?')) ?>",
|
||||
text: "<?= h(tr('سيتم تحويل هذا الطلب إلى فاتورة مبيعات مدفوعة.', 'This order will be marked as a paid sale.')) ?>",
|
||||
icon: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#198754",
|
||||
confirmButtonText: "<?= h(tr('نعم، تم الدفع', 'Yes, Paid')) ?>",
|
||||
cancelButtonText: "<?= h(tr('إلغاء', 'Cancel')) ?>"
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
window.location.href = "sales.php?mark_paid=" + id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mockEdit() {
|
||||
Swal.fire({
|
||||
title: '<?= h(tr('تعديل (غير متاح)', 'Edit (Disabled)')) ?>',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user