Autosave: 20260420-023828

This commit is contained in:
Flatlogic Bot 2026-04-20 02:38:20 +00:00
parent b1a016903b
commit 106c5dab28
13 changed files with 1746 additions and 64 deletions

View File

@ -23,7 +23,8 @@ if ($name === '' || $phone === '' || $address === '') {
}
$items = $input['items'];
$total = 0;
$subtotal = 0;
$totalVat = 0;
// Recalculate total for security
$db = db();
@ -32,19 +33,28 @@ foreach ($items as $id => $item) {
$qty = (int)$item['qty'];
if ($qty <= 0) continue;
// get price from DB
$stmt = $db->prepare("SELECT sku, name, price FROM items WHERE id = ?");
// get price and vat from DB
$stmt = $db->prepare("SELECT sku, name, price, vat FROM items WHERE id = ?");
$stmt->execute([$id]);
$dbItem = $stmt->fetch();
if ($dbItem) {
$price = (float)$dbItem['price'];
$total += ($price * $qty);
$vatPercent = (float)($dbItem['vat'] ?? 0);
$lineTotal = $price * $qty;
$itemVat = $lineTotal * ($vatPercent / 100);
$subtotal += $lineTotal;
$totalVat += $itemVat;
$processedItems[] = [
'id' => $id,
'sku' => $dbItem['sku'],
'name' => $dbItem['name'],
'price' => $price,
'qty' => $qty
'vat' => $vatPercent,
'vat_amount' => $itemVat,
'qty' => $qty,
'line_total' => $lineTotal
];
}
}
@ -54,24 +64,29 @@ if (empty($processedItems)) {
exit;
}
$totalAmount = $subtotal + $totalVat;
try {
$stmt = $db->prepare("INSERT INTO online_orders (customer_name, customer_phone, customer_address, items_json, total_amount) VALUES (?, ?, ?, ?, ?)");
$stmt = $db->prepare("INSERT INTO online_orders (customer_name, customer_phone, customer_address, items_json, subtotal, vat_amount, total_amount) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$name,
$phone,
$address,
json_encode($processedItems, JSON_UNESCAPED_UNICODE),
$total
$subtotal,
$totalVat,
$totalAmount
]);
// Optional: send telegram notification if configured
try {
// require_once __DIR__ . '/telegram_webhook.php'; // wait, it might not be a function but a script. Let's just do simple notification
$orderId = $db->lastInsertId();
$msg = "🛒 *New Online Order #{$orderId}*\n\n";
$msg .= "👤 {$name}\n📞 {$phone}\n📍 {$address}\n\n";
$msg .= "💰 Total: " . currency($total) . "\n";
// To send, we'd need to call telegram api directly if token is set.
$msg .= "💰 Subtotal: " . currency($subtotal) . "\n";
$msg .= "🧾 VAT: " . currency($totalVat) . "\n";
$msg .= "💵 Total: " . currency($totalAmount) . "\n";
$botToken = getenv('TELEGRAM_BOT_TOKEN') ?: get_setting('telegram_bot_token');
$chatId = getenv('TELEGRAM_CHAT_ID') ?: get_setting('telegram_chat_id');
if ($botToken && $chatId) {
@ -93,5 +108,5 @@ try {
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => 'Database error']);
}
echo json_encode(['success' => false, 'error' => 'Database error: ' . $e->getMessage()]);
}

View File

@ -13,7 +13,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$pdo = db();
$keys = [
'company_name_ar', 'company_name_en', 'vat_percentage',
'company_vat_number', 'company_phone', 'company_email', 'company_address'
'company_vat_number', 'company_phone', 'company_email', 'company_address',
'smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass', 'smtp_secure', 'mail_from', 'mail_from_name'
];
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");

571
edit_online_order.php Normal file
View File

@ -0,0 +1,571 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('sales', 'show'); // Same permission as online_orders.php
$editOrderId = (int)($_GET['id'] ?? 0);
$editOrder = null;
if ($editOrderId > 0) {
$stmt = db()->prepare('SELECT * FROM online_orders WHERE id = :id');
$stmt->execute([':id' => $editOrderId]);
$editOrder = $stmt->fetch();
}
if (!$editOrder) {
die(tr('الطلب غير موجود.', 'Order not found.'));
}
$pageTitle = tr('تعديل طلب', 'Edit Order') . ' #' . $editOrderId;
$activeNav = 'online_orders';
$error = '';
$catalog = catalog();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$customerName = trim((string) ($_POST['customer_name'] ?? ''));
$customerPhone = trim((string) ($_POST['customer_phone'] ?? ''));
$customerAddress = trim((string) ($_POST['customer_address'] ?? ''));
$saleStatus = trim((string) ($_POST['sale_status'] ?? 'pending'));
$cartJson = (string) ($_POST['cart_json'] ?? '[]');
$items = json_decode($cartJson, true);
if ($customerName === '' || $customerPhone === '' || $customerAddress === '') {
$error = tr('الرجاء تعبئة بيانات العميل الأساسية.', 'Please fill the main customer details.');
} elseif (!is_array($items) || $items === []) {
$error = tr('أضف صنفاً واحداً على الأقل إلى الطلب.', 'Add at least one item to the order.');
} else {
$normalized = [];
$subtotal = 0.0;
$totalVat = 0.0;
foreach ($items as $item) {
$sku = (string) ($item['sku'] ?? '');
$qty = (int) ($item['qty'] ?? 0);
if (!isset($catalog[$sku]) || $qty < 1) {
continue; // if sku doesn't exist in catalog or qty invalid
}
$product = $catalog[$sku];
$price = (float) $product['price'];
$lineTotal = $price * $qty;
$vatPercent = (float) ($product['vat'] ?? 0);
$itemVat = $lineTotal * ($vatPercent / 100);
$totalVat += $itemVat;
$normalized[] = [
'id' => $product['id'] ?? 0,
'sku' => $sku,
'name' => current_lang() === 'ar' ? $product['name_ar'] : $product['name_en'],
'name_ar' => $product['name_ar'],
'name_en' => $product['name_en'],
'qty' => $qty,
'price' => $price,
'line_total' => $lineTotal,
'vat_percent' => $vatPercent,
'vat_amount' => $itemVat
];
$subtotal += $lineTotal;
}
if ($normalized === []) {
$error = tr('الطلب غير صالح بعد التحقق من الأصناف.', 'The order is invalid after product validation.');
} else {
$stmt = db()->prepare('UPDATE online_orders SET
customer_name = :customer_name,
customer_phone = :customer_phone,
customer_address = :customer_address,
items_json = :items_json,
subtotal = :subtotal,
vat_amount = :vat_amount,
total_amount = :total_amount,
status = :status
WHERE id = :id');
$stmt->execute([
':customer_name' => $customerName,
':customer_phone' => $customerPhone,
':customer_address' => $customerAddress,
':items_json' => json_encode($normalized, JSON_UNESCAPED_UNICODE),
':subtotal' => $subtotal,
':vat_amount' => $totalVat,
':total_amount' => $subtotal + $totalVat,
':status' => $saleStatus,
':id' => $editOrderId,
]);
set_flash('success', tr('تم تحديث الطلب بنجاح.', 'Order updated successfully.'));
redirect_to('online_orders.php');
}
}
}
require __DIR__ . '/includes/header.php';
?>
<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="online_orders.php" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> <?= h(tr('عودة للطلبات', 'Back to Orders')) ?>
</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 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('عناصر الطلب', 'Order 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-person text-primary"></i> <?= h(tr('بيانات العميل', 'Customer Details')) ?>
</div>
</div>
<div class="smart-form-body">
<div class="mb-3">
<label class="form-label"><?= h(tr('اسم العميل', 'Customer Name')) ?></label>
<input type="text" name="customer_name" class="form-control custom-input" required value="<?= h($editOrder['customer_name'] ?? '' ) ?>">
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('رقم الهاتف', 'Phone Number')) ?></label>
<input type="text" name="customer_phone" class="form-control custom-input" required value="<?= h($editOrder['customer_phone'] ?? '' ) ?>">
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('العنوان', 'Address')) ?></label>
<textarea name="customer_address" class="form-control custom-input" rows="3" required><?= h($editOrder['customer_address'] ?? '' ) ?></textarea>
</div>
<div class="mb-4">
<label class="form-label"><?= h(tr('حالة الطلب', 'Order Status')) ?></label>
<select class="form-select custom-input" name="sale_status">
<option value="pending" <?= $editOrder['status'] === 'pending' ? 'selected' : '' ?>><?= h(tr('قيد الانتظار', 'Pending')) ?></option>
<option value="accepted" <?= $editOrder['status'] === 'accepted' ? 'selected' : '' ?>><?= h(tr('مقبول', 'Accepted')) ?></option>
<option value="completed" <?= $editOrder['status'] === 'completed' ? 'selected' : '' ?>><?= h(tr('مكتمل', 'Completed')) ?></option>
<option value="rejected" <?= $editOrder['status'] === 'rejected' ? 'selected' : '' ?>><?= h(tr('مرفوض', 'Rejected')) ?></option>
</select>
</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('الضريبة (مضافة)', 'VAT (Added)')) ?></span>
<span id="displayVat" class="text-muted">0.000</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 Changes')) ?>
</button>
</div>
</div>
</div>
</div>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
const catalogData = <?= json_encode($catalog, JSON_UNESCAPED_UNICODE) ?>;
const catalogArray = Object.values(catalogData);
let invoiceItems = {};
// Prepopulate from editOrder
const initialItemsJson = <?= empty($editOrder['items_json']) ? '[]' : $editOrder['items_json'] ?>;
initialItemsJson.forEach(item => {
// Merge existing item with catalog data to keep pricing accurate based on current catalog if needed
// or respect saved price depending on logic. Here we respect the saved price, but fall back to catalog.
const product = catalogData[item.sku];
invoiceItems[item.sku] = {
sku: item.sku,
name: item.name || ('<?= current_lang() ?>' === 'ar' ? (product ? product.name_ar : '') : (product ? product.name_en : '')),
price: parseFloat(item.price || (product ? product.price : 0)),
qty: parseInt(item.qty),
vat: parseFloat(item.vat_percent || (product ? product.vat : 0))
};
});
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')) ?>';
// 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,
vat: parseFloat(item.vat || 0)
};
}
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, 0);
cartJson.value = '[]';
return;
}
tbody.innerHTML = '';
let totalAmount = 0;
let totalVat = 0;
const cartData = [];
skus.forEach(sku => {
const item = invoiceItems[sku];
const lineTotal = item.qty * item.price;
const vatPercent = item.vat || 0;
const itemVat = lineTotal * (vatPercent / 100);
totalVat += itemVat;
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, totalVat);
cartJson.value = JSON.stringify(cartData);
}
function updateTotals(total, vat) {
const subtotal = total;
const finalTotal = subtotal + vat;
document.getElementById('displaySubtotal').innerText = subtotal.toFixed(3);
document.getElementById('displayVat').innerText = vat.toFixed(3);
document.getElementById('displayTotal').innerText = finalTotal.toFixed(3) + currencySuffix;
}
renderInvoice();
// 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__ . '/includes/footer.php'; ?>

222
expense_categories.php Normal file
View File

@ -0,0 +1,222 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('expense_categories', 'show');
$pageTitle = tr('تصنيفات المصروفات', 'Expense Categories');
$activeNav = 'expense_categories';
$pdo = db();
// Handle Form Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'create') {
$stmt = $pdo->prepare('INSERT INTO expense_categories (name_ar, name_en) VALUES (?, ?)');
$stmt->execute([$_POST['name_ar'], $_POST['name_en']]);
set_flash('success', tr('تمت إضافة التصنيف بنجاح', 'Category added successfully'));
redirect_to('expense_categories.php');
} elseif ($action === 'edit') {
$stmt = $pdo->prepare('UPDATE expense_categories SET name_ar = ?, name_en = ? WHERE id = ?');
$stmt->execute([$_POST['name_ar'], $_POST['name_en'], $_POST['id']]);
set_flash('success', tr('تم التحديث بنجاح', 'Updated successfully'));
redirect_to('expense_categories.php');
} elseif ($action === 'delete') {
// Check if there are expenses linked
$checkStmt = $pdo->prepare('SELECT COUNT(*) FROM expenses WHERE category_id = ?');
$checkStmt->execute([$_POST['id']]);
if ($checkStmt->fetchColumn() > 0) {
set_flash('danger', tr('لا يمكن حذف التصنيف لأنه مرتبط بمصروفات.', 'Cannot delete category because it is linked to expenses.'));
} else {
$stmt = $pdo->prepare('DELETE FROM expense_categories WHERE id = ?');
$stmt->execute([$_POST['id']]);
set_flash('success', tr('تم الحذف بنجاح', 'Deleted successfully'));
}
redirect_to('expense_categories.php');
}
}
// Pagination & Search
$page = max(1, (int)($_GET['p'] ?? 1));
$limit = 10;
$offset = ($page - 1) * $limit;
$search = $_GET['q'] ?? '';
$where = '1=1';
$params = [];
if ($search) {
$where .= ' AND (name_ar LIKE ? OR name_en LIKE ?)';
$params[] = "%$search%";
$params[] = "%$search%";
}
$totalStmt = $pdo->prepare("SELECT COUNT(*) FROM expense_categories WHERE $where");
$totalStmt->execute($params);
$total = $totalStmt->fetchColumn();
$totalPages = ceil($total / $limit);
$queryStmt = $pdo->prepare("SELECT * FROM expense_categories WHERE $where ORDER BY id DESC LIMIT $limit OFFSET $offset");
$queryStmt->execute($params);
$items = $queryStmt->fetchAll();
require __DIR__ . '/includes/header.php';
?>
<section class="surface-card mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h3 class="h5 mb-2"><i class="bi bi-tags me-2"></i><?= h($pageTitle) ?></h3>
<p class="text-muted mb-0"><?= h(tr('إدارة تصنيفات المصروفات', 'Manage expense categories')) ?></p>
</div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
<i class="bi bi-plus-lg"></i> <?= h(tr('إضافة تصنيف', 'Add Category')) ?>
</button>
</div>
<form class="d-flex mb-3" method="GET" action="expense_categories.php">
<div class="input-group" style="max-width: 400px;">
<input type="text" name="q" class="form-control" placeholder="<?= h(tr('بحث...', 'Search...')) ?>" value="<?= h($search) ?>">
<button class="btn btn-outline-secondary" type="submit"><i class="bi bi-search"></i></button>
</div>
</form>
</section>
<section class="surface-card">
<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">ID</th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الاسم (عربي)', 'Name (AR)')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الاسم (إنجليزي)', 'Name (EN)')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody class="border-top-0">
<?php if(empty($items)): ?>
<tr><td colspan="4" class="text-center text-muted py-4"><?= h(tr('لا توجد بيانات', 'No data found')) ?></td></tr>
<?php endif; ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?= h($item['id']) ?></td>
<td><?= h($item['name_ar']) ?></td>
<td><?= h($item['name_en']) ?></td>
<td>
<button class="btn btn-sm btn-outline-primary rounded-circle shadow-sm" style="width: 34px; height: 34px; padding: 0;" onclick="editItem(<?= htmlspecialchars(json_encode($item)) ?>)" title="<?= h(tr('تعديل', 'Edit')) ?>">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger rounded-circle shadow-sm ms-1" style="width: 34px; height: 34px; padding: 0;" onclick="deleteItem(<?= $item['id'] ?>)" title="<?= h(tr('حذف', 'Delete')) ?>">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($totalPages > 1): ?>
<nav class="mt-4">
<ul class="pagination justify-content-center mb-0">
<?php for($i=1; $i<=$totalPages; $i++): ?>
<li class="page-item <?= $i === $page ? 'active' : '' ?>">
<a class="page-link" href="<?= h(url_for('expense_categories.php', ['p' => $i, 'q' => $search])) ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
</ul>
</nav>
<?php endif; ?>
</section>
<!-- Add Modal -->
<div class="modal fade" id="addModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" action="expense_categories.php">
<input type="hidden" name="action" value="create">
<div class="modal-header">
<h5 class="modal-title"><?= h(tr('إضافة تصنيف', 'Add Category')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label"><?= h(tr('الاسم (عربي)', 'Name (AR)')) ?></label>
<input type="text" name="name_ar" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('الاسم (إنجليزي)', 'Name (EN)')) ?></label>
<input type="text" name="name_en" class="form-control" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= h(tr('إلغاء', 'Cancel')) ?></button>
<button type="submit" class="btn btn-primary"><?= h(tr('حفظ', 'Save')) ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" action="expense_categories.php">
<input type="hidden" name="action" value="edit">
<input type="hidden" name="id" id="edit_id">
<div class="modal-header">
<h5 class="modal-title"><?= h(tr('تعديل', 'Edit')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label"><?= h(tr('الاسم (عربي)', 'Name (AR)')) ?></label>
<input type="text" name="name_ar" id="edit_name_ar" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('الاسم (إنجليزي)', 'Name (EN)')) ?></label>
<input type="text" name="name_en" id="edit_name_en" class="form-control" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= h(tr('إلغاء', 'Cancel')) ?></button>
<button type="submit" class="btn btn-primary"><?= h(tr('حفظ', 'Save')) ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Form -->
<form id="deleteForm" method="POST" action="expense_categories.php" style="display:none;">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" id="delete_id">
</form>
<script>
function editItem(item) {
document.getElementById('edit_id').value = item.id;
document.getElementById('edit_name_ar').value = item.name_ar;
document.getElementById('edit_name_en').value = item.name_en;
new bootstrap.Modal(document.getElementById('editModal')).show();
}
function deleteItem(id) {
Swal.fire({
title: '<?= h(tr('هل أنت متأكد؟', 'Are you sure?')) ?>',
text: '<?= h(tr('لن تتمكن من التراجع عن هذا!', "You won't be able to revert this!")) ?>',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#dc3545',
cancelButtonColor: '#6c757d',
confirmButtonText: '<?= h(tr('نعم، احذف', 'Yes, delete it!')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('delete_id').value = id;
document.getElementById('deleteForm').submit();
}
});
}
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>

321
expenses.php Normal file
View File

@ -0,0 +1,321 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('expenses', 'show');
$pageTitle = tr('المصروفات', 'Expenses');
$activeNav = 'expenses';
$pdo = db();
// Fetch Categories for dropdowns
$catStmt = $pdo->query("SELECT id, name_ar, name_en FROM expense_categories ORDER BY name_ar");
$categories = $catStmt->fetchAll();
$branchesStmt = $pdo->query("SELECT code, name_ar, name_en FROM branches ORDER BY name_ar");
$branches = $branchesStmt->fetchAll();
// Check if user is restricted to a branch
$userBranch = $user['branch_code'] ?? '';
$isOwner = $user['role'] === 'owner';
// Handle Form Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'create' && has_permission('expenses', 'add')) {
$branch_code = $isOwner ? ($_POST['branch_code'] ?? null) : $userBranch;
$stmt = $pdo->prepare('INSERT INTO expenses (branch_code, category_id, amount, expense_date, description, created_by) VALUES (?, ?, ?, ?, ?, ?)');
$stmt->execute([
$branch_code === '' ? null : $branch_code,
$_POST['category_id'],
$_POST['amount'],
$_POST['expense_date'],
$_POST['description'] ?? '',
$user['id']
]);
set_flash('success', tr('تمت إضافة المصروف بنجاح', 'Expense added successfully'));
redirect_to('expenses.php');
} elseif ($action === 'edit' && has_permission('expenses', 'edit')) {
$branch_code = $isOwner ? ($_POST['branch_code'] ?? null) : $userBranch;
$stmt = $pdo->prepare('UPDATE expenses SET branch_code = ?, category_id = ?, amount = ?, expense_date = ?, description = ? WHERE id = ?');
$stmt->execute([
$branch_code === '' ? null : $branch_code,
$_POST['category_id'],
$_POST['amount'],
$_POST['expense_date'],
$_POST['description'] ?? '',
$_POST['id']
]);
set_flash('success', tr('تم التحديث بنجاح', 'Updated successfully'));
redirect_to('expenses.php');
} elseif ($action === 'delete' && has_permission('expenses', 'del')) {
$stmt = $pdo->prepare('DELETE FROM expenses WHERE id = ?');
$stmt->execute([$_POST['id']]);
set_flash('success', tr('تم الحذف بنجاح', 'Deleted successfully'));
redirect_to('expenses.php');
}
}
// Pagination & Search
$page = max(1, (int)($_GET['p'] ?? 1));
$limit = 10;
$offset = ($page - 1) * $limit;
$search = $_GET['q'] ?? '';
$where = '1=1';
$params = [];
if ($search) {
$where .= ' AND (e.description LIKE ?)';
$params[] = "%$search%";
}
if (!$isOwner && $userBranch) {
$where .= ' AND (e.branch_code = ? OR e.branch_code IS NULL)';
$params[] = $userBranch;
}
$totalStmt = $pdo->prepare("SELECT COUNT(*) FROM expenses e WHERE $where");
$totalStmt->execute($params);
$total = $totalStmt->fetchColumn();
$totalPages = ceil($total / $limit);
$queryStmt = $pdo->prepare("
SELECT e.*,
c.name_ar as category_ar, c.name_en as category_en,
b.name_ar as branch_ar, b.name_en as branch_en,
u.name_ar as user_ar, u.name_en as user_en
FROM expenses e
LEFT JOIN expense_categories c ON e.category_id = c.id
LEFT JOIN branches b ON e.branch_code = b.code
LEFT JOIN users u ON e.created_by = u.id
WHERE $where
ORDER BY e.expense_date DESC, e.id DESC
LIMIT $limit OFFSET $offset
");
$queryStmt->execute($params);
$items = $queryStmt->fetchAll();
require __DIR__ . '/includes/header.php';
?>
<section class="surface-card mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h3 class="h5 mb-2"><i class="bi bi-wallet2 me-2"></i><?= h($pageTitle) ?></h3>
<p class="text-muted mb-0"><?= h(tr('إدارة المصروفات وتسجيلها', 'Manage and record expenses')) ?></p>
</div>
<?php if(has_permission('expenses', 'add')): ?>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addModal">
<i class="bi bi-plus-lg"></i> <?= h(tr('إضافة مصروف', 'Add Expense')) ?>
</button>
<?php endif; ?>
</div>
<form class="d-flex mb-3" method="GET" action="expenses.php">
<div class="input-group" style="max-width: 400px;">
<input type="text" name="q" class="form-control" placeholder="<?= h(tr('بحث بالوصف...', 'Search by description...')) ?>" value="<?= h($search) ?>">
<button class="btn btn-outline-secondary" type="submit"><i class="bi bi-search"></i></button>
</div>
</form>
</section>
<section class="surface-card">
<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('التاريخ', 'Date')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('التصنيف', 'Category')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('المبلغ', 'Amount')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الفرع', 'Branch')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('الوصف', 'Description')) ?></th>
<th class="text-white border-0 py-3 fw-semibold bg-transparent"><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody class="border-top-0">
<?php if(empty($items)): ?>
<tr><td colspan="6" class="text-center text-muted py-4"><?= h(tr('لا توجد بيانات', 'No data found')) ?></td></tr>
<?php endif; ?>
<?php foreach ($items as $item): ?>
<tr>
<td><?= h($item['expense_date']) ?></td>
<td><?= h(current_lang() == 'ar' ? $item['category_ar'] : $item['category_en']) ?></td>
<td><?= h(number_format($item['amount'], 2)) ?></td>
<td><?= $item['branch_code'] ? h(current_lang() == 'ar' ? $item['branch_ar'] : $item['branch_en']) : '<span class="badge bg-secondary">'.h(tr('عام', 'General')).'</span>' ?></td>
<td><?= h($item['description']) ?></td>
<td>
<?php if(has_permission('expenses', 'edit')): ?>
<button class="btn btn-sm btn-outline-primary rounded-circle shadow-sm" style="width: 34px; height: 34px; padding: 0;" onclick="editItem(<?= htmlspecialchars(json_encode($item)) ?>)" title="<?= h(tr('تعديل', 'Edit')) ?>">
<i class="bi bi-pencil"></i>
</button>
<?php endif; ?>
<?php if(has_permission('expenses', 'del')): ?>
<button class="btn btn-sm btn-outline-danger rounded-circle shadow-sm ms-1" style="width: 34px; height: 34px; padding: 0;" onclick="deleteItem(<?= $item['id'] ?>)" title="<?= h(tr('حذف', 'Delete')) ?>">
<i class="bi bi-trash"></i>
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($totalPages > 1): ?>
<nav class="mt-4">
<ul class="pagination justify-content-center mb-0">
<?php for($i=1; $i<=$totalPages; $i++): ?>
<li class="page-item <?= $i === $page ? 'active' : '' ?>">
<a class="page-link" href="<?= h(url_for('expenses.php', ['p' => $i, 'q' => $search])) ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
</ul>
</nav>
<?php endif; ?>
</section>
<!-- Add Modal -->
<div class="modal fade" id="addModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" action="expenses.php">
<input type="hidden" name="action" value="create">
<div class="modal-header">
<h5 class="modal-title"><?= h(tr('إضافة مصروف', 'Add Expense')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label"><?= h(tr('التاريخ', 'Date')) ?> <span class="text-danger">*</span></label>
<input type="date" name="expense_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('التصنيف', 'Category')) ?> <span class="text-danger">*</span></label>
<select name="category_id" class="form-select" required>
<option value=""><?= h(tr('-- اختر التصنيف --', '-- Select Category --')) ?></option>
<?php foreach($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= h(current_lang() == 'ar' ? $cat['name_ar'] : $cat['name_en']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('المبلغ', 'Amount')) ?> <span class="text-danger">*</span></label>
<input type="number" step="0.001" name="amount" class="form-control" required>
</div>
<?php if($isOwner): ?>
<div class="mb-3">
<label class="form-label"><?= h(tr('الفرع', 'Branch')) ?></label>
<select name="branch_code" class="form-select">
<option value=""><?= h(tr('مصروف عام (بدون فرع)', 'General (No branch)')) ?></option>
<?php foreach($branches as $b): ?>
<option value="<?= h($b['code']) ?>"><?= h(current_lang() == 'ar' ? $b['name_ar'] : $b['name_en']) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="mb-3">
<label class="form-label"><?= h(tr('الوصف', 'Description')) ?></label>
<textarea name="description" class="form-control"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= h(tr('إلغاء', 'Cancel')) ?></button>
<button type="submit" class="btn btn-primary"><?= h(tr('حفظ', 'Save')) ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" action="expenses.php">
<input type="hidden" name="action" value="edit">
<input type="hidden" name="id" id="edit_id">
<div class="modal-header">
<h5 class="modal-title"><?= h(tr('تعديل', 'Edit')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label"><?= h(tr('التاريخ', 'Date')) ?> <span class="text-danger">*</span></label>
<input type="date" name="expense_date" id="edit_expense_date" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('التصنيف', 'Category')) ?> <span class="text-danger">*</span></label>
<select name="category_id" id="edit_category_id" class="form-select" required>
<option value=""><?= h(tr('-- اختر التصنيف --', '-- Select Category --')) ?></option>
<?php foreach($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= h(current_lang() == 'ar' ? $cat['name_ar'] : $cat['name_en']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('المبلغ', 'Amount')) ?> <span class="text-danger">*</span></label>
<input type="number" step="0.001" name="amount" id="edit_amount" class="form-control" required>
</div>
<?php if($isOwner): ?>
<div class="mb-3">
<label class="form-label"><?= h(tr('الفرع', 'Branch')) ?></label>
<select name="branch_code" id="edit_branch_code" class="form-select">
<option value=""><?= h(tr('مصروف عام (بدون فرع)', 'General (No branch)')) ?></option>
<?php foreach($branches as $b): ?>
<option value="<?= h($b['code']) ?>"><?= h(current_lang() == 'ar' ? $b['name_ar'] : $b['name_en']) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="mb-3">
<label class="form-label"><?= h(tr('الوصف', 'Description')) ?></label>
<textarea name="description" id="edit_description" class="form-control"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= h(tr('إلغاء', 'Cancel')) ?></button>
<button type="submit" class="btn btn-primary"><?= h(tr('حفظ', 'Save')) ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Form -->
<form id="deleteForm" method="POST" action="expenses.php" style="display:none;">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" id="delete_id">
</form>
<script>
function editItem(item) {
document.getElementById('edit_id').value = item.id;
document.getElementById('edit_expense_date').value = item.expense_date;
document.getElementById('edit_category_id').value = item.category_id;
document.getElementById('edit_amount').value = parseFloat(item.amount);
if(document.getElementById('edit_branch_code')) {
document.getElementById('edit_branch_code').value = item.branch_code || '';
}
document.getElementById('edit_description').value = item.description || '';
new bootstrap.Modal(document.getElementById('editModal')).show();
}
function deleteItem(id) {
Swal.fire({
title: '<?= h(tr('هل أنت متأكد؟', 'Are you sure?')) ?>',
text: '<?= h(tr('لن تتمكن من التراجع عن هذا!', "You won't be able to revert this!")) ?>',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#dc3545',
cancelButtonColor: '#6c757d',
confirmButtonText: '<?= h(tr('نعم، احذف', 'Yes, delete it!')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>'
}).then((result) => {
if (result.isConfirmed) {
document.getElementById('delete_id').value = id;
document.getElementById('deleteForm').submit();
}
});
}
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>

View File

@ -180,7 +180,7 @@ function require_auth(): array
return $user;
}
function get_app_modules(): array { return ["pos" => ["name_ar" => "نقاط البيع", "name_en" => "POS", "actions" => ["show", "add"]], "normal_sale" => ["name_ar" => "بيع عادي", "name_en" => "Normal Sale", "actions" => ["show", "add"]], "sales" => ["name_ar" => "المبيعات", "name_en" => "Sales", "actions" => ["show", "edit", "del"]], "purchases" => ["name_ar" => "المشتريات", "name_en" => "Purchases", "actions" => ["show", "add", "edit", "del"]], "stock" => ["name_ar" => "المخزون", "name_en" => "Stock", "actions" => ["show", "add", "edit", "del"]], "reports" => ["name_ar" => "التقارير", "name_en" => "Reports", "actions" => ["show"]], "customers" => ["name_ar" => "العملاء", "name_en" => "Customers", "actions" => ["show", "add", "edit", "del"]], "suppliers" => ["name_ar" => "الموردين", "name_en" => "Suppliers", "actions" => ["show", "add", "edit", "del"]], "categories" => ["name_ar" => "التصنيفات", "name_en" => "Categories", "actions" => ["show", "add", "edit", "del"]], "units" => ["name_ar" => "الوحدات", "name_en" => "Units", "actions" => ["show", "add", "edit", "del"]], "users" => ["name_ar" => "المستخدمين", "name_en" => "Users", "actions" => ["show", "add", "edit", "del"]], "settings" => ["name_ar" => "الإعدادات", "name_en" => "Settings", "actions" => ["show", "edit"]]]; } function has_permission(string $m, string $a = "show"): bool { $u = current_user(); if (!$u) return false; if ($u["role"] === "owner") return true; $p = !empty($u["permissions"]) ? (is_array($u["permissions"]) ? $u["permissions"] : json_decode($u["permissions"], true)) : []; return !empty($p[$m][$a]); } function require_permission(string $m, string $a = "show"): array { $u = require_auth(); if (!has_permission($m, $a)) { set_flash("warning", tr("ليس لديك صلاحية.", "You do not have permission.")); redirect_to("index.php"); } return $u; }
function get_app_modules(): array { return ["pos" => ["name_ar" => "نقاط البيع", "name_en" => "POS", "actions" => ["show", "add"]], "normal_sale" => ["name_ar" => "بيع عادي", "name_en" => "Normal Sale", "actions" => ["show", "add"]], "sales" => ["name_ar" => "المبيعات", "name_en" => "Sales", "actions" => ["show", "edit", "del"]], "purchases" => ["name_ar" => "المشتريات", "name_en" => "Purchases", "actions" => ["show", "add", "edit", "del"]], "stock" => ["name_ar" => "المخزون", "name_en" => "Stock", "actions" => ["show", "add", "edit", "del"]], "reports" => ["name_ar" => "التقارير", "name_en" => "Reports", "actions" => ["show"]], "customers" => ["name_ar" => "العملاء", "name_en" => "Customers", "actions" => ["show", "add", "edit", "del"]], "suppliers" => ["name_ar" => "الموردين", "name_en" => "Suppliers", "actions" => ["show", "add", "edit", "del"]], "categories" => ["name_ar" => "التصنيفات", "name_en" => "Categories", "actions" => ["show", "add", "edit", "del"]], "units" => ["name_ar" => "الوحدات", "name_en" => "Units", "actions" => ["show", "add", "edit", "del"]], "users" => ["name_ar" => "المستخدمين", "name_en" => "Users", "actions" => ["show", "add", "edit", "del"]], "settings" => ["name_ar" => "الإعدادات", "name_en" => "Settings", "actions" => ["show", "edit"]], "expense_categories" => ["name_ar" => "تصنيفات المصروفات", "name_en" => "Expense Categories", "actions" => ["show", "add", "edit", "del"]], "expenses" => ["name_ar" => "المصروفات", "name_en" => "Expenses", "actions" => ["show", "add", "edit", "del"]]]; } function has_permission(string $m, string $a = "show"): bool { $u = current_user(); if (!$u) return false; if ($u["role"] === "owner") return true; $p = !empty($u["permissions"]) ? (is_array($u["permissions"]) ? $u["permissions"] : json_decode($u["permissions"], true)) : []; return !empty($p[$m][$a]); } function require_permission(string $m, string $a = "show"): array { $u = require_auth(); if (!has_permission($m, $a)) { set_flash("warning", tr("ليس لديك صلاحية.", "You do not have permission.")); redirect_to("index.php"); } return $u; }
function require_roles(array $roles): array
{
$user = require_auth();

View File

@ -52,6 +52,42 @@
<div class="mt-2"><img src="<?= h(get_setting('company_favicon')) ?>" height="32" style="background: #f8f9fa; padding: 2px; border-radius: 4px;"></div>
<?php endif; ?>
</div>
<hr>
<div class="col-md-12">
<h6 class="mb-0 fw-bold"><?= h(tr("إعدادات البريد الإلكتروني (SMTP)", "SMTP Email Settings")) ?></h6>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("خادم SMTP (Host)", "SMTP Host")) ?></label>
<input type="text" class="form-control" name="smtp_host" value="<?= h(get_setting("smtp_host")) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("منفذ SMTP (Port)", "SMTP Port")) ?></label>
<input type="number" class="form-control" name="smtp_port" value="<?= h(get_setting("smtp_port", 587)) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("مستخدم SMTP (User)", "SMTP User")) ?></label>
<input type="text" class="form-control" name="smtp_user" value="<?= h(get_setting("smtp_user")) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("كلمة مرور SMTP (Pass)", "SMTP Password")) ?></label>
<input type="password" class="form-control" name="smtp_pass" value="<?= h(get_setting("smtp_pass")) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("تشفير SMTP (Secure)", "SMTP Secure (tls/ssl)")) ?></label>
<select class="form-select" name="smtp_secure">
<option value="tls" <?= get_setting("smtp_secure", "tls") === "tls" ? "selected" : "" ?>>TLS</option>
<option value="ssl" <?= get_setting("smtp_secure") === "ssl" ? "selected" : "" ?>>SSL</option>
<option value="" <?= get_setting("smtp_secure") === "" ? "selected" : "" ?>>None</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr("البريد المرسل (From Email)", "From Email")) ?></label>
<input type="email" class="form-control" name="mail_from" value="<?= h(get_setting("mail_from")) ?>">
</div>
<div class="col-md-12">
<label class="form-label"><?= h(tr("اسم المرسل (From Name)", "From Name")) ?></label>
<input type="text" class="form-control" name="mail_from_name" value="<?= h(get_setting("mail_from_name")) ?>">
</div>
</div>
</div>
<div class="modal-footer">

View File

@ -135,6 +135,30 @@ $isPublic = !empty($forcePublic) || !isset($user) || !$user;
</div>
</div>
<?php endif; ?>
<?php if (has_permission('expenses', 'show') || has_permission('expense_categories', 'show')): ?>
<!-- المصروفات (Expenses) -->
<a class="list-group-item list-group-item-action <?= in_array($activeNav, ['expenses', 'expense_categories']) ? '' : 'collapsed' ?>" data-bs-toggle="collapse" href="#collapseExpenses" role="button" aria-expanded="<?= in_array($activeNav, ['expenses', 'expense_categories']) ? 'true' : 'false' ?>" aria-controls="collapseExpenses">
<div class="d-flex justify-content-between align-items-center w-100">
<span><i class="bi bi-wallet2"></i> <?= h(tr('المصروفات', 'Expenses')) ?></span>
<i class="bi bi-chevron-down toggle-icon" style="transition: transform 0.2s;"></i>
</div>
</a>
<div class="collapse <?= in_array($activeNav, ['expenses', 'expense_categories']) ? 'show' : '' ?>" id="collapseExpenses">
<div class="list-group list-group-flush" style="background-color: rgba(0,0,0,0.15);">
<?php if (has_permission('expenses', 'show')): ?>
<a class="list-group-item list-group-item-action <?= $activeNav === 'expenses' ? 'active' : '' ?>" href="<?= h(url_for('expenses.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('قائمة المصروفات', 'Expenses List')) ?>
</a>
<?php endif; ?>
<?php if (has_permission('expense_categories', 'show')): ?>
<a class="list-group-item list-group-item-action <?= $activeNav === 'expense_categories' ? 'active' : '' ?>" href="<?= h(url_for('expense_categories.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('تصنيفات المصروفات', 'Expense Categories')) ?>
</a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<a class="list-group-item list-group-item-action <?= $activeNav === 'suppliers' ? 'active' : '' ?>" href="<?= h(url_for('suppliers.php')) ?>">
<i class="bi bi-truck"></i> <?= h(tr('الموردون', 'Suppliers')) ?>
</a>

View File

@ -1,21 +1,48 @@
<?php
// Mail configuration sourced from environment variables.
// No secrets are stored here; the file just maps env -> config array for MailService.
// Mail configuration sourced from DB settings with environment variables fallback.
function env_val(string $key, $default = null) {
$v = getenv($key);
return ($v === false || $v === null || $v === '') ? $default : $v;
}
$transport = env_val('MAIL_TRANSPORT', 'smtp');
$smtp_host = env_val('SMTP_HOST');
$smtp_port = (int) env_val('SMTP_PORT', 587);
$smtp_secure = env_val('SMTP_SECURE', 'tls'); // tls | ssl | null
$smtp_user = env_val('SMTP_USER');
$smtp_pass = env_val('SMTP_PASS');
$db_settings = [];
if (file_exists(__DIR__ . '/../db/config.php')) {
try {
require_once __DIR__ . '/../db/config.php';
if (function_exists('db')) {
$pdo = db();
if ($pdo) {
$stmt = $pdo->query("SELECT setting_key, setting_value FROM settings WHERE setting_key IN ('smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass', 'smtp_secure', 'mail_from', 'mail_from_name')");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$db_settings[$row['setting_key']] = $row['setting_value'];
}
}
}
} catch (\Throwable $e) {
// ignore DB errors during config loading
}
}
$from_email = env_val('MAIL_FROM', 'no-reply@localhost');
$from_name = env_val('MAIL_FROM_NAME', 'App');
// Function to get config value with fallback
function get_cfg($db_settings, $key, $env_key, $default = null, $allow_empty = false) {
if (isset($db_settings[$key])) {
if ($allow_empty || $db_settings[$key] !== '') {
return $db_settings[$key];
}
}
return env_val($env_key, $default);
}
$transport = env_val('MAIL_TRANSPORT', 'smtp');
$smtp_host = get_cfg($db_settings, 'smtp_host', 'SMTP_HOST');
$smtp_port = (int) get_cfg($db_settings, 'smtp_port', 'SMTP_PORT', 587);
$smtp_secure = get_cfg($db_settings, 'smtp_secure', 'SMTP_SECURE', 'tls', true);
$smtp_user = get_cfg($db_settings, 'smtp_user', 'SMTP_USER');
$smtp_pass = get_cfg($db_settings, 'smtp_pass', 'SMTP_PASS', null, true);
$from_email = get_cfg($db_settings, 'mail_from', 'MAIL_FROM', 'no-reply@localhost');
$from_name = get_cfg($db_settings, 'mail_from_name', 'MAIL_FROM_NAME', 'App');
$reply_to = env_val('MAIL_REPLY_TO');
$dkim_domain = env_val('DKIM_DOMAIN');

View File

@ -15,21 +15,79 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$stmt->execute([$status, $id]);
set_flash('success', tr('تم تحديث حالة الطلب', 'Order status updated'));
redirect_to('online_orders.php');
} elseif ($_POST['action'] === 'delete') {
$id = (int)$_POST['id'];
$stmt = $db->prepare("DELETE FROM online_orders WHERE id = ?");
$stmt->execute([$id]);
set_flash('success', tr('تم حذف الطلب بنجاح', 'Order deleted successfully'));
redirect_to('online_orders.php');
}
}
$stmt = $db->query("SELECT * FROM online_orders ORDER BY created_at DESC");
$search = $_GET['search'] ?? '';
$date_from = $_GET['date_from'] ?? date('Y-m-d', strtotime('-30 days'));
$date_to = $_GET['date_to'] ?? date('Y-m-d');
$query = "SELECT * FROM online_orders WHERE DATE(created_at) >= ? AND DATE(created_at) <= ?";
$params = [$date_from, $date_to];
if ($search !== '') {
$query .= " AND (customer_name LIKE ? OR customer_phone LIKE ?)";
$params[] = "%$search%";
$params[] = "%$search%";
}
$query .= " ORDER BY created_at DESC";
$stmt = $db->prepare($query);
$stmt->execute($params);
$orders = $stmt->fetchAll(PDO::FETCH_ASSOC);
require __DIR__ . '/includes/header.php';
?>
<div class="row align-items-center mb-4">
<style>
@media print {
.d-print-none { display: none !important; }
body { background: #fff !important; }
.card { border: none !important; box-shadow: none !important; }
.table { width: 100% !important; }
.table th, .table td { border-bottom: 1px solid #dee2e6 !important; }
/* Hide specific columns on print if needed */
.print-hide { display: none !important; }
}
</style>
<div class="row align-items-center mb-4 d-print-none">
<div class="col">
<h3 class="h5 mb-0 fw-bold"><i class="bi bi-cart-check me-2"></i><?= h(tr('طلبات المتجر الإلكتروني', 'Online Store Orders')) ?></h3>
</div>
</div>
<div class="d-none d-print-block mb-4 text-center">
<h2><?= h(tr('تقرير طلبات المتجر', 'Online Orders Report')) ?></h2>
<p><?= h($date_from) ?> - <?= h($date_to) ?></p>
</div>
<form method="get" class="row g-3 mb-4 align-items-end d-print-none bg-light p-3 rounded-4 shadow-sm">
<div class="col-md-4">
<label class="form-label text-muted small"><?= h(tr('بحث', 'Search')) ?></label>
<input type="text" name="search" class="form-control" placeholder="<?= h(tr('الاسم أو الهاتف', 'Name or Phone')) ?>" value="<?= h($search) ?>">
</div>
<div class="col-md-3">
<label class="form-label text-muted small"><?= h(tr('من تاريخ', 'From Date')) ?></label>
<input type="date" name="date_from" class="form-control" value="<?= h($date_from) ?>">
</div>
<div class="col-md-3">
<label class="form-label text-muted small"><?= h(tr('إلى تاريخ', 'To Date')) ?></label>
<input type="date" name="date_to" class="form-control" value="<?= h($date_to) ?>">
</div>
<div class="col-md-2 d-flex gap-2">
<button type="submit" class="btn btn-primary flex-grow-1"><i class="bi bi-search"></i> <?= h(tr('بحث', 'Search')) ?></button>
<button type="button" class="btn btn-secondary" onclick="window.print()"><i class="bi bi-printer"></i></button>
</div>
</form>
<div class="card shadow-sm border-0 rounded-4">
<div class="card-body p-0 table-responsive">
<table class="table table-hover align-middle mb-0">
@ -42,14 +100,17 @@ require __DIR__ . '/includes/header.php';
<th><?= h(tr('العنوان', 'Address')) ?></th>
<th><?= h(tr('المبلغ', 'Amount')) ?></th>
<th><?= h(tr('الحالة', 'Status')) ?></th>
<th class="pe-4 text-end"><?= h(tr('إجراءات', 'Actions')) ?></th>
<th class="pe-4 text-end print-hide"><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody>
<?php if(empty($orders)): ?>
<tr><td colspan="8" class="text-center py-5 text-muted"><?= h(tr('لا توجد طلبات بعد', 'No orders yet')) ?></td></tr>
<tr><td colspan="8" class="text-center py-5 text-muted"><?= h(tr('لا توجد طلبات', 'No orders found')) ?></td></tr>
<?php else: ?>
<?php foreach($orders as $o):
<?php
$totalAmount = 0;
foreach($orders as $o):
$totalAmount += $o['total_amount'];
$statusClass = 'bg-secondary';
$statusText = $o['status'];
if ($o['status'] === 'pending') { $statusClass = 'bg-warning text-dark'; $statusText = tr('قيد الانتظار', 'Pending'); }
@ -67,20 +128,21 @@ require __DIR__ . '/includes/header.php';
<td style="max-width: 200px;" class="text-truncate" title="<?= h($o['customer_address']) ?>"><?= h($o['customer_address']) ?></td>
<td class="fw-bold text-primary"><?= h(currency($o['total_amount'])) ?></td>
<td><span class="badge <?= $statusClass ?> px-2 py-1"><?= h($statusText) ?></span></td>
<td class="pe-4 text-end">
<button class="btn btn-sm btn-light shadow-sm" onclick='viewOrder(<?= json_encode([
<td class="pe-4 text-end print-hide">
<button class="btn btn-sm btn-light shadow-sm" onclick='viewOrder(<?= htmlspecialchars(json_encode([
"id" => $o["id"],
"name" => $o["customer_name"],
"phone" => $o["customer_phone"],
"address" => $o["customer_address"],
"total" => $o["total_amount"],
"subtotal" => $o["subtotal"] ?? 0, "vat" => $o["vat_amount"] ?? 0, "total" => $o["total_amount"],
"items" => $items
], JSON_UNESCAPED_UNICODE) ?>)'>
<i class="bi bi-eye"></i> <?= h(tr('عرض', 'View')) ?>
], JSON_UNESCAPED_UNICODE), ENT_QUOTES, "UTF-8") ?>)'>
<i class="bi bi-eye"></i>
</button>
<a href="edit_online_order.php?id=<?= $o ['id'] ?>" class="btn btn-sm btn-info shadow-sm text-white" title="<?= h(tr('تعديل الطلب', 'Edit Order')) ?>"><i class="bi bi-pencil"></i></a>
<div class="dropdown d-inline-block">
<button class="btn btn-sm btn-primary dropdown-toggle shadow-sm" type="button" data-bs-toggle="dropdown">
<?= h(tr('تغيير الحالة', 'Change Status')) ?>
<i class="bi bi-gear"></i>
</button>
<ul class="dropdown-menu shadow">
<li><form method="post"><input type="hidden" name="action" value="update_status"><input type="hidden" name="id" value="<?= h($o['id']) ?>"><input type="hidden" name="status" value="pending"><button class="dropdown-item" type="submit"><?= h(tr('قيد الانتظار', 'Pending')) ?></button></form></li>
@ -89,17 +151,30 @@ require __DIR__ . '/includes/header.php';
<li><form method="post"><input type="hidden" name="action" value="update_status"><input type="hidden" name="id" value="<?= h($o['id']) ?>"><input type="hidden" name="status" value="rejected"><button class="dropdown-item text-danger fw-bold" type="submit"><?= h(tr('مرفوض', 'Rejected')) ?></button></form></li>
</ul>
</div>
<form method="post" class="d-inline" onsubmit="return confirm('<?= h(tr('هل أنت متأكد من الحذف؟', 'Are you sure you want to delete?')) ?>');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?= h($o['id']) ?>">
<button type="submit" class="btn btn-sm btn-danger shadow-sm"><i class="bi bi-trash"></i></button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
<?php if(!empty($orders)): ?>
<tfoot class="table-light">
<tr>
<td colspan="5" class="text-end fw-bold"><?= h(tr('إجمالي المبالغ:', 'Total Amounts:')) ?></td>
<td colspan="3" class="fw-bold text-primary"><?= h(currency($totalAmount)) ?></td>
</tr>
</tfoot>
<?php endif; ?>
</table>
</div>
</div>
<!-- Order Modal -->
<div class="modal fade" id="orderModal" tabindex="-1">
<!-- Order Modal (d-print-none prevents it from flashing in print if unhidden) -->
<div class="modal fade d-print-none" id="orderModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content rounded-4 border-0 shadow">
<div class="modal-header border-bottom-0 pb-0 pt-4 px-4">
@ -119,6 +194,14 @@ require __DIR__ . '/includes/header.php';
<ul class="list-group list-group-flush mb-4" id="vItems">
</ul>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted"><?= h(tr('المجموع الفرعي', 'Subtotal')) ?></span>
<span class="fw-bold" id="vSubtotal"></span>
</div>
<div class="d-flex justify-content-between align-items-center mb-3">
<span class="text-muted"><?= h(tr('الضريبة', 'VAT')) ?></span>
<span class="fw-bold" id="vVat"></span>
</div>
<div class="d-flex justify-content-between align-items-center bg-primary bg-opacity-10 text-primary p-3 rounded-3">
<h5 class="mb-0 fw-bold"><?= h(tr('الإجمالي', 'Total')) ?></h5>
<h4 class="mb-0 fw-bold" id="vTotal"></h4>
@ -132,27 +215,32 @@ require __DIR__ . '/includes/header.php';
</div>
<script>
const orderModal = new bootstrap.Modal(document.getElementById('orderModal'));
function viewOrder(order) {
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('orderModal'));
document.getElementById('vName').innerText = order.name;
document.getElementById('vPhone').innerText = order.phone;
document.getElementById('vAddress').innerText = order.address;
document.getElementById('vSubtotal').innerText = Number(order.subtotal).toFixed(2);
document.getElementById('vVat').innerText = Number(order.vat).toFixed(2);
document.getElementById('vTotal').innerText = Number(order.total).toFixed(2);
let html = '';
order.items.forEach(item => {
html += `<li class="list-group-item px-0 d-flex justify-content-between align-items-center">
<div>
<span class="fw-bold">${item.name}</span>
<small class="text-muted ms-2 px-2 bg-light rounded">${item.price}</small>
</div>
<span class="badge bg-secondary rounded-pill">x${item.qty}</span>
</li>`;
});
if (order.items && order.items.length) {
order.items.forEach(item => {
html += `<li class="list-group-item px-0 d-flex justify-content-between align-items-center">
<div>
<span class="fw-bold">${item.name}</span>
<small class="text-muted ms-2 px-2 bg-light rounded">${item.price}</small>
</div>
<span class="badge bg-secondary rounded-pill">x${item.qty}</span>
</li>`;
});
}
document.getElementById('vItems').innerHTML = html;
orderModal.show();
modal.show();
}
</script>

90
patch.php Normal file
View File

@ -0,0 +1,90 @@
<?php
$content = file_get_contents('online_orders.php');
$phpBackend = <<<'PHP'
} elseif ($_POST['action'] === 'edit_order') {
$id = (int)$_POST['id'];
$customer_name = trim($_POST['customer_name'] ?? '');
$customer_phone = trim($_POST['customer_phone'] ?? '');
$customer_address = trim($_POST['customer_address'] ?? '');
if ($customer_name && $customer_phone && $customer_address) {
$stmt = $db->prepare("UPDATE online_orders SET customer_name = ?, customer_phone = ?, customer_address = ? WHERE id = ?");
$stmt->execute([$customer_name, $customer_phone, $customer_address, $id]);
set_flash('success', tr('تم تعديل الطلب بنجاح', 'Order updated successfully'));
} else {
set_flash('danger', tr('الرجاء تعبئة جميع الحقول', 'Please fill all fields'));
}
redirect_to('online_orders.php');
PHP;
$content = str_replace("redirect_to('online_orders.php');\n }\n", "redirect_to('online_orders.php');\n }\n" . $phpBackend . "\n }\n", $content);
$editButton = <<<'HTML'
<button class="btn btn-sm btn-info shadow-sm text-white" onclick='editOrder(<?= json_encode([
"id" => $o["id"],
"name" => $o["customer_name"],
"phone" => $o["customer_phone"],
"address" => $o["customer_address"]
], JSON_UNESCAPED_UNICODE) ?>)'>
<i class="bi bi-pencil"></i>
</button>
HTML;
$content = str_replace('<i class="bi bi-eye"></i>
</button>', "<i class=\"bi bi-eye\"></i>\n </button>\n" . $editButton, $content);
$editModal = <<<'HTML'
<!-- Edit Order Modal -->
<div class="modal fade d-print-none" id="editOrderModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content rounded-4 border-0 shadow">
<form method="post">
<input type="hidden" name="action" value="edit_order">
<input type="hidden" name="id" id="editOrderId" value="">
<div class="modal-header border-bottom-0 pb-0 pt-4 px-4">
<h5 class="modal-title fw-bold"><?= h(tr('تعديل بيانات الطلب', 'Edit Order Details')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<div class="mb-3">
<label class="form-label"><?= h(tr('اسم العميل', 'Customer Name')) ?></label>
<input type="text" name="customer_name" id="editCustomerName" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('رقم الهاتف', 'Phone Number')) ?></label>
<input type="text" name="customer_phone" id="editCustomerPhone" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('العنوان', 'Address')) ?></label>
<textarea name="customer_address" id="editCustomerAddress" class="form-control" rows="3" required></textarea>
</div>
</div>
<div class="modal-footer border-top-0 pt-0 pb-4 px-4">
<button type="button" class="btn btn-secondary rounded-pill px-4" data-bs-dismiss="modal"><?= h(tr('إلغاء', 'Cancel')) ?></button>
<button type="submit" class="btn btn-primary rounded-pill px-4"><?= h(tr('حفظ التعديلات', 'Save Changes')) ?></button>
</div>
</form>
</div>
</div>
</div>
HTML;
$content = str_replace('<!-- Order Modal', $editModal . "\n\n<!-- Order Modal", $content);
$js = <<<'JS'
const editOrderModal = new bootstrap.Modal(document.getElementById('editOrderModal'));
function editOrder(order) {
document.getElementById('editOrderId').value = order.id;
document.getElementById('editCustomerName').value = order.name;
document.getElementById('editCustomerPhone').value = order.phone;
document.getElementById('editCustomerAddress').value = order.address;
editOrderModal.show();
}
JS;
$content = str_replace('const orderModal = new bootstrap.Modal', $js . "\n\nconst orderModal = new bootstrap.Modal", $content);
file_put_contents('online_orders.php', $content);
echo "Patched\n";

View File

@ -48,6 +48,50 @@ if ($tab === 'sales') {
$dbError = $e->getMessage();
$followUpOrders = [];
}
} elseif ($tab === 'daily') {
$reportDate = $_GET['date'] ?? date('Y-m-d');
$branchFilter = $_GET['branch'] ?? '';
$params = [];
$where = base_sales_query_filters($params, null, $branchFilter ?: null);
$where .= " AND DATE(sale_date) = :rdate AND status != 'order'";
$params[':rdate'] = $reportDate;
$dailyTotals = [
'seller' => [],
'outlet' => [],
'payment' => [],
'total' => 0
];
try {
// By Seller
$sql = "SELECT cashier_name, SUM(total_amount) as total FROM sales_orders" . $where . " GROUP BY cashier_name";
$stmt = db()->prepare($sql);
$stmt->execute($params);
$dailyTotals['seller'] = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
// By Outlet
$sql = "SELECT branch_code, SUM(total_amount) as total FROM sales_orders" . $where . " GROUP BY branch_code";
$stmt = db()->prepare($sql);
$stmt->execute($params);
$dailyTotals['outlet'] = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
// By Payment
$sql = "SELECT payment_method, SUM(total_amount) as total FROM sales_orders" . $where . " GROUP BY payment_method";
$stmt = db()->prepare($sql);
$stmt->execute($params);
$dailyTotals['payment'] = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
// Grand total
$sql = "SELECT SUM(total_amount) as total FROM sales_orders" . $where;
$stmt = db()->prepare($sql);
$stmt->execute($params);
$dailyTotals['total'] = (float) $stmt->fetchColumn();
} catch(Throwable $e) {
$dbError = $e->getMessage();
}
} else {
$report = ['gross' => 0.0, 'branch_totals' => [], 'payment_totals' => [], 'product_totals' => [], 'sales_count' => 0];
try {
@ -70,6 +114,9 @@ require __DIR__ . '/includes/header.php';
<li class="nav-item">
<a class="nav-link <?= $tab === 'summary' ? 'active' : '' ?>" href="reports.php?tab=summary"><?= h(tr('ملخص عام', 'General Summary')) ?></a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'daily' ? 'active' : '' ?>" href="reports.php?tab=daily"><?= h(tr('التقرير اليومي', 'Daily Report')) ?></a>
</li>
<li class="nav-item">
<a class="nav-link <?= $tab === 'sales' ? 'active' : '' ?>" href="reports.php?tab=sales"><?= h(tr('تقرير المبيعات', 'Sales Report')) ?></a>
</li>
@ -274,43 +321,266 @@ require __DIR__ . '/includes/header.php';
<?php endif; ?>
</div>
<?php elseif ($tab === 'daily'): ?>
<div class="card mb-4 d-print-none">
<div class="card-body">
<form method="GET" action="reports.php" class="row g-3 align-items-end">
<input type="hidden" name="tab" value="daily">
<div class="col-md-4">
<label class="form-label"><?= h(tr('التاريخ', 'Date')) ?></label>
<input type="date" name="date" class="form-control" value="<?= h($reportDate) ?>" onchange="this.form.submit()">
</div>
<div class="col-md-4">
<label class="form-label"><?= h(tr('الفرع', 'Branch')) ?></label>
<select name="branch" class="form-select" onchange="this.form.submit()">
<option value=""><?= h(tr('جميع الفروع', 'All Branches')) ?></option>
<?php
$availableBranches = $user['role'] === 'owner' ? branches() : [$user['branch_code'] => branches()[$user['branch_code']]];
foreach ($availableBranches as $code => $b):
?>
<option value="<?= h($code) ?>" <?= $branchFilter === $code ? 'selected' : '' ?>><?= h(branch_label($code)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100"><?= h(tr('بحث', 'Search')) ?></button>
</div>
<div class="col-md-2">
<button type="button" class="btn btn-outline-secondary w-100" onclick="window.print()"><i class="bi bi-printer"></i> <?= h(tr('طباعة', 'Print')) ?></button>
</div>
</form>
</div>
</div>
<div class="d-none d-print-block mb-4 text-center">
<h2><?= h(tr('التقرير اليومي', 'Daily Report')) ?></h2>
<p><?= h(tr('التاريخ:', 'Date:')) ?> <?= h($reportDate) ?></p>
<?php if ($branchFilter): ?>
<p><?= h(tr('الفرع:', 'Branch:')) ?> <?= h(branch_label($branchFilter)) ?></p>
<?php endif; ?>
</div>
<?php if ($dailyTotals['total'] == 0): ?>
<div class="alert alert-info"><?= h(tr('لا توجد مبيعات في هذا اليوم.', 'No sales on this day.')) ?></div>
<?php else: ?>
<div class="row g-4 mb-4">
<!-- Total Card -->
<div class="col-12">
<div class="surface-card text-center py-4 bg-primary text-white rounded shadow-sm">
<h3 class="h5 mb-2"><?= h(tr('إجمالي المبيعات', 'Total Sales')) ?></h3>
<div class="fs-1 fw-bold"><?= h(currency($dailyTotals['total'])) ?></div>
</div>
</div>
<!-- By Seller -->
<div class="col-md-4">
<div class="surface-card h-100 shadow-sm">
<h3 class="h5 mb-3 border-bottom pb-2"><i class="bi bi-person me-2 text-primary"></i><?= h(tr('حسب الموظف', 'By Seller')) ?></h3>
<ul class="list-group list-group-flush">
<?php foreach($dailyTotals['seller'] as $seller => $amount): ?>
<li class="list-group-item d-flex justify-content-between align-items-center px-0 bg-transparent">
<span><?= h($seller ?: tr('غير محدد', 'Unknown')) ?></span>
<span class="fw-bold"><?= h(currency((float)$amount)) ?></span>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
<!-- By Outlet -->
<div class="col-md-4">
<div class="surface-card h-100 shadow-sm">
<h3 class="h5 mb-3 border-bottom pb-2"><i class="bi bi-shop me-2 text-primary"></i><?= h(tr('حسب الفرع', 'By Outlet')) ?></h3>
<ul class="list-group list-group-flush">
<?php foreach($dailyTotals['outlet'] as $outlet => $amount): ?>
<li class="list-group-item d-flex justify-content-between align-items-center px-0 bg-transparent">
<span><?= h(branch_label((string)$outlet)) ?></span>
<span class="fw-bold"><?= h(currency((float)$amount)) ?></span>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
<!-- By Payment -->
<div class="col-md-4">
<div class="surface-card h-100 shadow-sm">
<h3 class="h5 mb-3 border-bottom pb-2"><i class="bi bi-credit-card me-2 text-primary"></i><?= h(tr('حسب طريقة الدفع', 'By Payment')) ?></h3>
<ul class="list-group list-group-flush">
<?php
$payLabels = ['cash' => tr('كاش', 'Cash'), 'card' => tr('بطاقة', 'Card'), 'bank' => tr('تحويل بنكي', 'Bank'), 'mixed' => tr('متعدد', 'Mixed')];
foreach($dailyTotals['payment'] as $payment => $amount):
?>
<li class="list-group-item d-flex justify-content-between align-items-center px-0 bg-transparent">
<span><?= h($payLabels[$payment] ?? ucfirst($payment)) ?></span>
<span class="fw-bold"><?= h(currency((float)$amount)) ?></span>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
</div>
<?php endif; ?>
<?php else: ?>
<!-- Top KPIs -->
<section class="row g-3 mb-4 d-print-none">
<div class="col-md-3"><article class="metric-card"><div class="eyebrow"><?= h(tr('إجمالي المبيعات', 'Gross sales')) ?></div><div class="metric-value"><?= h(currency((float) $report['gross'])) ?></div><div class="small text-muted"><?= h(tr('حسب نطاق صلاحية المستخدم الحالي', 'Scoped to the current viewer permissions')) ?></div></article></div>
<div class="col-md-3"><article class="metric-card"><div class="eyebrow"><?= h(tr('إجمالي الضريبة', 'Total VAT')) ?></div><div class="metric-value"><?= h(currency((float) ($report['total_vat'] ?? 0))) ?></div><div class="small text-muted"><?= h(tr('مجموع ضريبة القيمة المضافة', 'Total Value Added Tax')) ?></div></article></div>
<div class="col-md-3"><article class="metric-card"><div class="eyebrow"><?= h(tr('عدد الفواتير', 'Invoices')) ?></div><div class="metric-value"><?= h((string) $report['sales_count']) ?></div><div class="small text-muted"><?= h(tr('إجمالي الفواتير المسجلة', 'Total logged invoices')) ?></div></article></div>
<div class="col-md-3"><article class="metric-card"><div class="eyebrow"><?= h(tr('أفضل صنف', 'Top product')) ?></div><div class="metric-value small-metric"><?= h($report['product_totals'] ? product_label((string) array_key_first($report['product_totals'])) : tr('لا يوجد', 'None yet')) ?></div><div class="small text-muted"><?= h(tr('الأكثر مبيعاً حتى الآن', 'Most sold item so far')) ?></div></article></div>
<div class="col-md-3">
<article class="metric-card d-flex align-items-center h-100">
<div class="me-3 fs-1 text-primary"><i class="bi bi-wallet2"></i></div>
<div>
<div class="eyebrow text-muted mb-1"><?= h(tr('إجمالي المبيعات', 'Gross sales')) ?></div>
<div class="metric-value fs-4 fw-bold"><?= h(currency((float) $report['gross'])) ?></div>
<div class="small text-muted" style="font-size:0.8rem;"><?= h(tr('شامل جميع العمليات', 'Includes all transactions')) ?></div>
</div>
</article>
</div>
<div class="col-md-3">
<article class="metric-card d-flex align-items-center h-100">
<div class="me-3 fs-1 text-info"><i class="bi bi-receipt"></i></div>
<div>
<div class="eyebrow text-muted mb-1"><?= h(tr('إجمالي الضريبة', 'Total VAT')) ?></div>
<div class="metric-value fs-4 fw-bold"><?= h(currency((float) ($report['total_vat'] ?? 0))) ?></div>
<div class="small text-muted" style="font-size:0.8rem;"><?= h(tr('مجموع ضريبة القيمة المضافة', 'Total Value Added Tax')) ?></div>
</div>
</article>
</div>
<div class="col-md-3">
<article class="metric-card d-flex align-items-center h-100">
<div class="me-3 fs-1 text-success"><i class="bi bi-basket3"></i></div>
<div>
<div class="eyebrow text-muted mb-1"><?= h(tr('عدد الفواتير', 'Invoices')) ?></div>
<div class="metric-value fs-4 fw-bold"><?= h((string) $report['sales_count']) ?></div>
<div class="small text-muted" style="font-size:0.8rem;"><?= h(tr('إجمالي الفواتير المسجلة', 'Total logged invoices')) ?></div>
</div>
</article>
</div>
<div class="col-md-3">
<article class="metric-card d-flex align-items-center h-100">
<div class="me-3 fs-1 text-warning"><i class="bi bi-star"></i></div>
<div class="overflow-hidden">
<div class="eyebrow text-muted mb-1"><?= h(tr('أفضل صنف', 'Top product')) ?></div>
<div class="metric-value fs-5 fw-bold text-truncate" style="max-width: 150px;"><?= h($report['product_totals'] ? product_label((string) array_key_first($report['product_totals'])) : tr('لا يوجد', 'None yet')) ?></div>
<div class="small text-muted" style="font-size:0.8rem;"><?= h(tr('الأكثر مبيعاً حتى الآن', 'Most sold item so far')) ?></div>
</div>
</article>
</div>
</section>
<!-- Charts and Breakdowns -->
<section class="row g-4 d-print-none">
<!-- Trend Chart -->
<div class="col-lg-12">
<div class="surface-card h-100">
<h3 class="h5 mb-3"><i class="bi bi-graph-up-arrow me-2 text-primary"></i><?= h(tr('أداء المبيعات الشهري', 'Monthly Sales Performance')) ?></h3>
<?php if (!$report['monthly_totals']): ?>
<div class="empty-state compact"><h4><?= h(tr('لا توجد بيانات', 'No data')) ?></h4></div>
<?php else: ?>
<canvas id="monthlySalesChart" height="80"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('monthlySalesChart').getContext('2d');
const rawData = <?= json_encode($report['monthly_totals']) ?>;
const labels = Object.keys(rawData);
const data = Object.values(rawData);
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: '<?= h(tr('المبيعات', 'Sales')) ?>',
data: data,
borderColor: '#0d6efd',
backgroundColor: 'rgba(13, 110, 253, 0.1)',
borderWidth: 3,
fill: true,
tension: 0.4,
pointBackgroundColor: '#0d6efd',
pointRadius: 4,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: { y: { beginAtZero: true } },
interaction: {
intersect: false,
mode: 'index',
},
}
});
});
</script>
<?php endif; ?>
</div>
</div>
<div class="col-lg-6">
<div class="surface-card h-100">
<h3 class="h5 mb-3"><?= h(tr('المبيعات حسب الفرع', 'Sales by branch')) ?></h3>
<h3 class="h5 mb-4"><i class="bi bi-shop me-2 text-primary"></i><?= h(tr('المبيعات حسب الفرع', 'Sales by branch')) ?></h3>
<?php if (!$report['branch_totals']): ?>
<div class="empty-state compact"><h4><?= h(tr('لا توجد بيانات', 'No data')) ?></h4><p><?= h(tr('أضف عملية بيع أولاً لبدء التقارير.', 'Add a first sale to activate reports.')) ?></p></div>
<?php else: ?>
<div class="d-grid gap-2">
<?php foreach ($report['branch_totals'] as $branchCode => $amount): ?>
<div class="report-row"><span><?= h(branch_label((string) $branchCode)) ?></span><strong><?= h(currency((float) $amount)) ?></strong></div>
<div class="d-grid gap-3">
<?php
foreach ($report['branch_totals'] as $branchCode => $amount):
$percent = $report['gross'] > 0 ? ($amount / $report['gross']) * 100 : 0;
?>
<div>
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-bold fs-6"><?= h(branch_label((string) $branchCode)) ?></span>
<div class="text-end">
<strong class="d-block"><?= h(currency((float) $amount)) ?></strong>
<small class="text-muted"><?= round($percent, 1) ?>%</small>
</div>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-primary rounded-pill" role="progressbar" style="width: <?= $percent ?>%"></div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<div class="col-lg-6">
<div class="surface-card h-100">
<h3 class="h5 mb-3"><?= h(tr('المبيعات حسب الدفع', 'Sales by payment')) ?></h3>
<h3 class="h5 mb-4"><i class="bi bi-credit-card me-2 text-primary"></i><?= h(tr('المبيعات حسب الدفع', 'Sales by payment')) ?></h3>
<?php if (!$report['payment_totals']): ?>
<div class="empty-state compact"><h4><?= h(tr('بانتظار البيانات', 'Waiting for data')) ?></h4><p><?= h(tr('عند تسجيل عمليات بيع ستظهر هنا طرق الدفع.', 'Payment mix will appear here once sales are logged.')) ?></p></div>
<?php else: ?>
<div class="d-grid gap-2">
<?php foreach ($report['payment_totals'] as $payment => $amount): ?>
<div class="report-row"><span><?= h(ucfirst((string) $payment)) ?></span><strong><?= h(currency((float) $amount)) ?></strong></div>
<div class="d-grid gap-3">
<?php
$colors = ['cash' => 'success', 'card' => 'info', 'bank' => 'warning', 'mixed' => 'secondary'];
foreach ($report['payment_totals'] as $payment => $amount):
$percent = $report['gross'] > 0 ? ($amount / $report['gross']) * 100 : 0;
$bg = $colors[$payment] ?? 'primary';
?>
<div>
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-bold fs-6"><?= h(ucfirst((string) $payment)) ?></span>
<div class="text-end">
<strong class="d-block"><?= h(currency((float) $amount)) ?></strong>
<small class="text-muted"><?= round($percent, 1) ?>%</small>
</div>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-<?= $bg ?> rounded-pill" role="progressbar" style="width: <?= $percent ?>%"></div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</section>
<?php endif; ?>

View File

@ -130,7 +130,7 @@ body { background-color: #f8f9fa; }
'id' => $item['id'],
'sku' => $item['sku'],
'name' => $item['name'],
'price' => $item['price']
'price' => $item['price'], 'vat' => $item['vat'] ?? 0
]), ENT_QUOTES, 'UTF-8') ?>)">
<i class="bi bi-cart-plus me-1"></i> <?= h(tr('إضافة للسلة', 'Add to Cart')) ?>
</button>
@ -168,9 +168,21 @@ body { background-color: #f8f9fa; }
<div id="cartItemsList" class="mb-4">
<!-- Items will be rendered here -->
</div>
<div class="d-flex justify-content-between align-items-center bg-light p-3 rounded-3 mb-4">
<h5 class="mb-0 fw-bold"><?= h(tr('المجموع الإجمالي', 'Total Amount')) ?></h5>
<!-- cart summary -->
<div class="bg-light p-3 rounded-3 mb-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted"><?= h(tr("المجموع الفرعي", "Subtotal")) ?></span>
<span class="fw-bold" id="cartSubtotal">0.00</span>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted"><?= h(tr("الضريبة", "VAT")) ?></span>
<span class="fw-bold" id="cartVat">0.00</span>
</div>
<hr>
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-bold"><?= h(tr("المجموع الإجمالي", "Total Amount")) ?></h5>
<h4 class="mb-0 fw-bold text-primary" id="cartTotal">0.00</h4>
</div>
</div>
<h5 class="fw-bold mb-3 border-bottom pb-2"><?= h(tr('بيانات العميل', 'Customer Details')) ?></h5>
@ -251,6 +263,7 @@ function renderCart() {
const list = document.getElementById('cartItemsList');
let html = '';
let total = 0;
let totalVat = 0;
if (Object.keys(cart).length === 0) {
html = '<div class="text-center text-muted py-4"><?= h(tr('السلة فارغة', 'Cart is empty')) ?></div>';
@ -259,6 +272,8 @@ function renderCart() {
for (let id in cart) {
const item = cart[id];
const subtotal = item.price * item.qty;
const itemVat = subtotal * ((item.vat || 0) / 100);
totalVat += itemVat;
total += subtotal;
html += `
<div class="list-group-item d-flex justify-content-between align-items-center py-3 px-0 border-bottom-dashed">
@ -278,7 +293,9 @@ function renderCart() {
}
list.innerHTML = html;
document.getElementById('cartTotal').innerText = total.toFixed(2);
document.getElementById('cartSubtotal').innerText = total.toFixed(2);
document.getElementById('cartVat').innerText = totalVat.toFixed(2);
document.getElementById('cartTotal').innerText = (total + totalVat).toFixed(2);
}
function openCart() {