updating sales

This commit is contained in:
Flatlogic Bot 2026-04-19 04:15:39 +00:00
parent f6212d4e47
commit ee93351390
10 changed files with 1379 additions and 201 deletions

26
api/customers.php Normal file
View 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;
}

View File

@ -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();

View File

@ -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'; ?>

View File

@ -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);

View File

@ -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);

View File

@ -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
View File

@ -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
View 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
View File

@ -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) ?> &nbsp;&bull;&nbsp; <?= 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'; ?>

View File

@ -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)')) ?>',