Compare commits

...

71 Commits

Author SHA1 Message Date
Flatlogic Bot
3d926edafb update setting 2026-05-05 05:26:36 +00:00
Flatlogic Bot
ed77a396a7 edit reset area 2026-05-05 05:19:53 +00:00
Flatlogic Bot
3694996f67 update vat issue 2026-05-05 04:54:23 +00:00
Flatlogic Bot
46b5eae015 Autosave: 20260505-041812 2026-05-05 04:18:09 +00:00
Flatlogic Bot
4e6f8a4404 ascending order in eid list 2026-05-05 02:07:08 +00:00
Flatlogic Bot
f9e55d0ef0 Autosave: 20260505-020504 2026-05-05 02:05:01 +00:00
Flatlogic Bot
680965e7e9 another update for eid sales 2026-05-04 07:34:36 +00:00
Flatlogic Bot
ea4bf1e47f edit eid order2 2026-05-04 06:01:30 +00:00
Flatlogic Bot
aa9007e0be change eid orders 2026-05-04 05:56:13 +00:00
Flatlogic Bot
ba54d3065c new updates 2026-05-04 05:24:49 +00:00
Flatlogic Bot
8df558e09d Autosave: 20260504-045808 2026-05-04 04:58:07 +00:00
Flatlogic Bot
07894c8f77 new updates 2026-05-01 18:08:28 +00:00
Flatlogic Bot
5f5205783f Autosave: 20260501-140940 2026-05-01 14:09:40 +00:00
Flatlogic Bot
7377170520 update customers 2026-04-26 17:57:47 +00:00
Flatlogic Bot
96dbdba460 Autosave: 20260426-173202 2026-04-26 17:31:59 +00:00
Flatlogic Bot
64c01fd51a update sale list adding filter 2026-04-26 02:37:30 +00:00
Flatlogic Bot
bf43c58e3a some new updates 2026-04-26 02:28:28 +00:00
Flatlogic Bot
a2cefc7b7a Autosave: 20260426-021157 2026-04-26 02:11:55 +00:00
Flatlogic Bot
10edd2e5bc Edit includes/footer.php via Editor 2026-04-23 17:29:21 +00:00
Flatlogic Bot
c5cad9853d Autosave: 20260423-172454 2026-04-23 17:24:52 +00:00
Flatlogic Bot
992a466f9c update permissions 2026-04-23 17:10:16 +00:00
Flatlogic Bot
a4e592231b update final1 2026-04-23 16:59:54 +00:00
Flatlogic Bot
ec5e25fdd0 Autosave: 20260423-152803 2026-04-23 15:28:01 +00:00
Flatlogic Bot
9b5e175eec update lists splitting tel 2026-04-23 04:43:40 +00:00
Flatlogic Bot
0590b5d747 more updates 2026-04-23 04:19:35 +00:00
Flatlogic Bot
ac740c5a25 Autosave: 20260423-030915 2026-04-23 03:09:14 +00:00
Flatlogic Bot
c5a18a8ed1 Autosave: 20260423-020651 2026-04-23 02:06:50 +00:00
Flatlogic Bot
b2cb1b5a0b Autosave: 20260422-182218 2026-04-22 18:22:05 +00:00
Flatlogic Bot
5bfaa401f2 update22 2026-04-22 05:05:39 +00:00
Flatlogic Bot
56dfdabd5b adding policy 2026-04-22 04:55:52 +00:00
Flatlogic Bot
69f39cc9cd Autosave: 20260422-044403 2026-04-22 04:43:50 +00:00
Flatlogic Bot
8cadf8aa40 thawani payments 2026-04-22 04:37:48 +00:00
Flatlogic Bot
9b0dd7d971 sales update 2 2026-04-22 03:19:22 +00:00
Flatlogic Bot
0700bb66f7 update sales 2026-04-22 03:08:12 +00:00
Flatlogic Bot
c8919d7836 Autosave: 20260421-040526 2026-04-21 04:05:16 +00:00
Flatlogic Bot
f6d94ddca5 update the setting tabs 2026-04-21 03:10:21 +00:00
Flatlogic Bot
e914f26914 Autosave: 20260421-030404 2026-04-21 03:03:54 +00:00
Flatlogic Bot
9f8d42862c fixing setup form 2026-04-21 02:50:35 +00:00
Flatlogic Bot
be707683ba update whatsapp and payments 2026-04-21 02:42:34 +00:00
Flatlogic Bot
9d38c126a5 update sidebar 2026-04-20 17:24:01 +00:00
Flatlogic Bot
554fd02f89 Autosave: 20260420-172034 2026-04-20 17:20:25 +00:00
Flatlogic Bot
2f1d29f034 update normal sale 2026-04-20 17:07:09 +00:00
Flatlogic Bot
b6bf7b7b5e update normal sale 2026-04-20 16:38:50 +00:00
Flatlogic Bot
52c9aed760 update pos 4 2026-04-20 16:02:36 +00:00
Flatlogic Bot
4ca9818d92 update pos3 2026-04-20 15:57:03 +00:00
Flatlogic Bot
30f1ee1e49 update pos 2026-04-20 15:49:03 +00:00
Flatlogic Bot
5f4a0b383c update pos 2026-04-20 15:36:59 +00:00
Flatlogic Bot
017bae675e Autosave: 20260420-113152 2026-04-20 11:31:42 +00:00
Flatlogic Bot
5f9eadae19 update profile db 2026-04-20 07:27:06 +00:00
Flatlogic Bot
eb784514a0 update user profile 2026-04-20 06:17:24 +00:00
Flatlogic Bot
32bfb0c109 adding user profile 2026-04-20 06:12:47 +00:00
Flatlogic Bot
58eab84b92 update users 2026-04-20 06:07:14 +00:00
Flatlogic Bot
b2926230f7 update users 2026-04-20 05:55:07 +00:00
Flatlogic Bot
7c04f1dfef Autosave: 20260420-054952 2026-04-20 05:49:43 +00:00
Flatlogic Bot
21027ba93c update schema 2026-04-20 05:26:38 +00:00
Flatlogic Bot
aabdb7b681 update setup file 2026-04-20 05:05:37 +00:00
Flatlogic Bot
393595fe77 feat: Add support for users to access multiple branches/outlets 2026-04-20 03:23:35 +00:00
Flatlogic Bot
2d79f6ac5f update timezone 2026-04-20 03:11:26 +00:00
Flatlogic Bot
106c5dab28 Autosave: 20260420-023828 2026-04-20 02:38:20 +00:00
Flatlogic Bot
b1a016903b Autosave: 20260419-160352 2026-04-19 16:03:44 +00:00
Flatlogic Bot
cc0da06fbb Autosave: 20260419-143136 2026-04-19 14:31:28 +00:00
Flatlogic Bot
c4d7baf9d6 Autosave: 20260419-133057 2026-04-19 13:30:50 +00:00
Flatlogic Bot
6e3e0c556a Autosave: 20260419-100930 2026-04-19 10:09:23 +00:00
Flatlogic Bot
1ac0f55ef8 update sales id 2026-04-19 09:52:55 +00:00
Flatlogic Bot
9b7ec271a9 Autosave: 20260419-091830 2026-04-19 09:18:23 +00:00
Flatlogic Bot
4f9eddc419 Autosave: 20260419-074108 2026-04-19 07:41:01 +00:00
Flatlogic Bot
02afd76d75 Autosave: 20260419-042924 2026-04-19 04:29:17 +00:00
Flatlogic Bot
ee93351390 updating sales 2026-04-19 04:15:39 +00:00
Flatlogic Bot
f6212d4e47 Autosave: 20260419-032550 2026-04-19 03:25:43 +00:00
Flatlogic Bot
033b73cd60 feat: setup items, categories, and suppliers database tables with real demo data and images from pexels 2026-04-19 02:43:31 +00:00
Flatlogic Bot
ccaa56bcff Autosave: 20260419-023017 2026-04-19 02:30:10 +00:00
89 changed files with 29676 additions and 552 deletions

40
api/customers.php Normal file
View File

@ -0,0 +1,40 @@
<?php
require_once __DIR__ . '/../includes/app.php';
$user = require_permission('customers', 'add');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
$name = trim($_POST['name'] ?? '');
$phoneInput = trim($_POST['phone'] ?? '');
$phone = $phoneInput === '' ? '' : normalize_oman_phone($phoneInput);
$phoneForStorage = $phone === '' ? null : $phone;
if (!$name) {
echo json_encode(['success' => false, 'error' => tr('الاسم مطلوب', 'Name is required')]);
exit;
}
if ($phoneInput !== '' && $phone === '') {
echo json_encode(['success' => false, 'error' => tr('رقم الهاتف يجب أن يكون عمانياً من 8 خانات.', 'Phone must be an 8-digit Oman number.')]);
exit;
}
if ($phone !== '' && customer_phone_exists($phone)) {
echo json_encode(['success' => false, 'error' => tr('رقم الهاتف هذا مسجل لعميل آخر بالفعل.', 'This phone number is already assigned to another customer.')]);
exit;
}
try {
$pdo = db();
$stmt = $pdo->prepare('INSERT INTO customers (name, phone, phone_normalized) VALUES (?, ?, ?)');
$stmt->execute([$name, $phoneForStorage, $phoneForStorage]);
$id = $pdo->lastInsertId();
echo json_encode(['success' => true, 'customer' => ['id' => $id, 'name' => $name, 'phone' => $phone]]);
} catch (Throwable $e) {
if (is_customer_phone_unique_violation($e)) {
echo json_encode(['success' => false, 'error' => tr('رقم الهاتف هذا مسجل لعميل آخر بالفعل.', 'This phone number is already assigned to another customer.')]);
} else {
echo json_encode(['success' => false, 'error' => tr('تعذر حفظ العميل حالياً.', 'Could not save the customer right now.')]);
}
}
exit;
}

183
api/place_order.php Normal file
View File

@ -0,0 +1,183 @@
<?php
require_once __DIR__ . '/../includes/app.php';
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['success' => false, 'error' => 'Invalid method']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['items'])) {
echo json_encode(['success' => false, 'error' => 'Cart is empty']);
exit;
}
$name = trim($input['name'] ?? '');
$phoneInput = trim($input['phone'] ?? '');
$phone = normalize_oman_phone($phoneInput);
$address = trim($input['address'] ?? '');
$acceptPolicies = !empty($input['accept_policies']);
$paymentMethod = trim((string) ($input['payment_method'] ?? 'pay_later'));
if (!in_array($paymentMethod, ['pay_later', 'pay_online'], true)) {
echo json_encode(['success' => false, 'error' => 'Invalid payment method']);
exit;
}
if (!$acceptPolicies) {
echo json_encode(['success' => false, 'error' => 'You must accept the Privacy Policy and Terms & Conditions']);
exit;
}
if ($paymentMethod === 'pay_online' && !thawani_is_configured()) {
echo json_encode(['success' => false, 'error' => 'Thawani payment is not configured']);
exit;
}
if ($name === '' || $phoneInput === '' || $address === '') {
echo json_encode(['success' => false, 'error' => 'Missing customer details']);
exit;
}
if ($phone === '') {
echo json_encode(['success' => false, 'error' => 'Phone must be an 8-digit Oman number']);
exit;
}
$items = $input['items'];
$subtotal = 0;
$totalVat = 0;
// Recalculate total for security
$db = db();
$processedItems = [];
foreach ($items as $id => $item) {
$qty = (int)$item['qty'];
if ($qty <= 0) continue;
// 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'];
$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,
'vat' => $vatPercent,
'vat_amount' => $itemVat,
'qty' => $qty,
'line_total' => $lineTotal
];
}
}
if (empty($processedItems)) {
echo json_encode(['success' => false, 'error' => 'Invalid items']);
exit;
}
$totalAmount = $subtotal + $totalVat;
try {
$db->beginTransaction();
$paymentGateway = $paymentMethod === 'pay_online' ? 'thawani' : null;
$paymentStatus = $paymentMethod === 'pay_online' ? 'pending' : 'unpaid';
$stmt = $db->prepare("INSERT INTO online_orders (customer_name, customer_phone, customer_address, items_json, subtotal, vat_amount, total_amount, payment_method, payment_gateway, payment_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$name,
$phone,
$address,
json_encode($processedItems, JSON_UNESCAPED_UNICODE),
$subtotal,
$totalVat,
$totalAmount,
$paymentMethod,
$paymentGateway,
$paymentStatus
]);
$orderId = (int) $db->lastInsertId();
sync_online_order_stock_reservation([], 'rejected', $processedItems, 'pending');
$checkoutUrl = null;
if ($paymentMethod === 'pay_online') {
$thawaniResult = thawani_create_checkout_session($orderId, [
'customer_name' => $name,
'customer_phone' => $phone,
'customer_address' => $address,
'items' => $processedItems,
]);
if (empty($thawaniResult['success'])) {
throw new RuntimeException((string) ($thawaniResult['error'] ?? 'Unable to create Thawani session'));
}
$checkoutUrl = (string) ($thawaniResult['checkout_url'] ?? '');
$sessionId = (string) ($thawaniResult['session_id'] ?? '');
$updateStmt = $db->prepare("UPDATE online_orders SET gateway_session_id = ? WHERE id = ?");
$updateStmt->execute([$sessionId, $orderId]);
}
$db->commit();
// Optional: send Telegram admin notice and customer WhatsApp notification.
try {
$msg = "🛒 *New Online Order #{$orderId}*
";
$msg .= "👤 {$name}
📞 " . phone_display($phone) . "
📍 {$address}
";
$msg .= "💰 Subtotal: " . currency($subtotal) . "
";
$msg .= "🧾 VAT: " . currency($totalVat) . "
";
$msg .= "💵 Total: " . currency($totalAmount) . "
";
$botToken = getenv('TELEGRAM_BOT_TOKEN') ?: get_setting('telegram_bot_token');
$chatId = getenv('TELEGRAM_CHAT_ID') ?: get_setting('telegram_chat_id');
if ($botToken && $chatId) {
$url = "https://api.telegram.org/bot{$botToken}/sendMessage";
$data = ['chat_id' => $chatId, 'text' => $msg, 'parse_mode' => 'Markdown'];
$options = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded
",
'method' => 'POST',
'content' => http_build_query($data)
]
];
$context = stream_context_create($options);
@file_get_contents($url, false, $context);
}
} catch (Throwable $e) {
error_log('Telegram notify failed for online order #' . $orderId . ': ' . $e->getMessage());
}
if ($paymentMethod === 'pay_later' && wablas_is_configured()) {
try {
wablas_notify_online_order_by_id($orderId, 'created');
} catch (Throwable $e) {
error_log('Customer WhatsApp notify failed for online order #' . $orderId . ': ' . $e->getMessage());
}
}
echo json_encode(['success' => true, 'redirect_url' => $checkoutUrl]);
} catch (Throwable $e) {
if ($db->inTransaction()) {
$db->rollBack();
}
echo json_encode(['success' => false, 'error' => 'Database error: ' . $e->getMessage()]);
}

46
api/sales_payment.php Normal file
View File

@ -0,0 +1,46 @@
<?php
require_once __DIR__ . '/../includes/app.php';
require_auth();
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => tr('طريقة الطلب غير مدعومة.', 'Method not allowed.')]);
exit;
}
$saleId = (int) ($_POST['sale_id'] ?? 0);
$paymentAmount = (string) ($_POST['payment_amount'] ?? '');
$completeOrder = !empty($_POST['complete_order']);
if ($saleId <= 0) {
echo json_encode(['success' => false, 'error' => tr('الفاتورة غير صالحة.', 'Invalid invoice.')]);
exit;
}
if ($paymentAmount === '' || !is_numeric($paymentAmount) || (float) $paymentAmount <= 0) {
echo json_encode(['success' => false, 'error' => tr('أدخل مبلغ دفعة صحيحاً.', 'Enter a valid payment amount.')]);
exit;
}
try {
$sale = fetch_sale($saleId);
if (!$sale) {
echo json_encode(['success' => false, 'error' => tr('الفاتورة غير موجودة أو غير مصرح بها.', 'Invoice not found or not allowed.')]);
exit;
}
$result = apply_sale_payment($saleId, (float) $paymentAmount, $completeOrder);
echo json_encode([
'success' => true,
'message' => $result['due_amount'] <= 0.0005
? tr('تم سداد الفاتورة بالكامل.', 'Invoice paid in full.')
: tr('تم تسجيل الدفعة الجزئية بنجاح.', 'Partial payment recorded successfully.'),
'payment_status' => $result['payment_status'],
'paid_amount' => $result['paid_amount'],
'due_amount' => $result['due_amount'],
'applied_amount' => $result['applied_amount'],
]);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'error' => tr('تعذر تسجيل الدفعة.', 'Could not record the payment.')]);
}

120
api/settings.php Normal file
View File

@ -0,0 +1,120 @@
<?php
require_once __DIR__ . '/../includes/app.php';
require_permission('settings', 'edit');
$user = current_user();
if (!in_array($user['role'], ['owner', 'manager'])) {
set_flash('danger', tr('غير مصرح لك.', 'Unauthorized.'));
redirect_to('../index.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$isAjax = strtolower((string) ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '')) === 'xmlhttprequest';
$respond = static function (bool $success, string $type, string $message, ?string $redirect = null) use ($isAjax): void {
if ($isAjax) {
header('Content-Type: application/json; charset=UTF-8');
echo json_encode([
'success' => $success,
'type' => $type,
'message' => $message,
'redirect' => $redirect,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
set_flash($type, $message);
header('Location: ' . ($redirect ?: '../index.php'));
exit;
};
$redirectBack = static function (): string {
$referer = $_SERVER['HTTP_REFERER'] ?? '../index.php';
$returnModal = trim((string) ($_POST['return_modal'] ?? ''));
if ($returnModal === 'wablas') {
return append_query_params($referer, ['open_modal' => 'wablas']);
}
return $referer;
};
$pdo = db();
$action = trim((string) ($_POST['action'] ?? ''));
if ($action === 'reset_eid_serial') {
ensure_sales_table();
reset_eid_serial_next($pdo, 1);
$respond(true, 'success', tr('تمت إعادة تعيين الرقم التسلسلي القادم لطلبات العيد إلى 1. سيُستخدم هذا للطلبات الجديدة فقط.', 'The next Eid order serial has been reset to 1. This applies to new Eid orders only.'), $redirectBack());
}
$keys = [
'timezone', 'company_name_ar', 'company_name_en', 'vat_percentage',
'company_vat_number', 'company_phone', 'company_email', 'company_address',
'wablas_enabled', 'wablas_token', 'wablas_secret_key', 'wablas_api_url',
'wablas_invoice_recipients', 'wablas_report_recipients',
'wablas_daily_auto_send', 'wablas_daily_auto_time', 'wablas_daily_auto_last_date',
'wablas_template_invoice', 'wablas_template_daily_report',
'wablas_template_created', 'wablas_template_pending', 'wablas_template_accepted', 'wablas_template_completed', 'wablas_template_rejected',
'smtp_host', 'smtp_port', 'smtp_user', 'smtp_pass', 'smtp_secure', 'mail_from', 'mail_from_name',
'thawani_enabled', 'thawani_mode', 'thawani_publishable_key', 'thawani_secret_key', 'thawani_success_url', 'thawani_cancel_url',
'privacy_policy_content', 'terms_conditions_content'
];
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
$companyPhone = trim((string) ($_POST['company_phone'] ?? ''));
if ($companyPhone !== '') {
$companyPhone = normalize_oman_phone($companyPhone);
if ($companyPhone === '') {
$respond(false, 'danger', tr('رقم هاتف الشركة يجب أن يكون عمانياً من 8 خانات.', 'Company phone must be an 8-digit Oman number.'), $redirectBack());
}
$_POST['company_phone'] = $companyPhone;
}
foreach (['wablas_invoice_recipients', 'wablas_report_recipients'] as $phoneListKey) {
$parsed = wablas_parse_phone_list((string) ($_POST[$phoneListKey] ?? ''));
if (!empty($parsed['invalid'])) {
$respond(false, 'danger', tr('يوجد رقم واتساب غير صالح في الحقل.', 'There is an invalid WhatsApp number in the field.') . ' ' . implode(', ', $parsed['invalid']), $redirectBack());
}
$_POST[$phoneListKey] = implode(',', $parsed['phones']);
}
$_POST['wablas_daily_auto_time'] = wablas_format_time_setting((string) ($_POST['wablas_daily_auto_time'] ?? '21:00'));
if (!isset($_POST['wablas_daily_auto_send'])) {
$_POST['wablas_daily_auto_send'] = '0';
}
if (!isset($_POST['thawani_enabled'])) {
$_POST['thawani_enabled'] = '0';
}
$thawaniMode = strtolower(trim((string) ($_POST['thawani_mode'] ?? 'sandbox')));
$_POST['thawani_mode'] = in_array($thawaniMode, ['sandbox', 'live'], true) ? $thawaniMode : 'sandbox';
unset($_POST['wablas_daily_auto_last_date']);
foreach ($keys as $key) {
if (isset($_POST[$key])) {
$value = is_string($_POST[$key]) ? trim($_POST[$key]) : $_POST[$key];
$stmt->execute([$key, $value]);
}
}
// Handle logo upload
$uploadDir = __DIR__ . '/../assets/images/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
if (isset($_FILES['company_logo']) && $_FILES['company_logo']['error'] === UPLOAD_ERR_OK) {
$ext = pathinfo($_FILES['company_logo']['name'], PATHINFO_EXTENSION);
$filename = 'logo_' . time() . '.' . $ext;
if (move_uploaded_file($_FILES['company_logo']['tmp_name'], $uploadDir . $filename)) {
$stmt->execute(['company_logo', 'assets/images/' . $filename]);
}
}
// Handle favicon upload
if (isset($_FILES['company_favicon']) && $_FILES['company_favicon']['error'] === UPLOAD_ERR_OK) {
$ext = pathinfo($_FILES['company_favicon']['name'], PATHINFO_EXTENSION);
$filename = 'favicon_' . time() . '.' . $ext;
if (move_uploaded_file($_FILES['company_favicon']['tmp_name'], $uploadDir . $filename)) {
$stmt->execute(['company_favicon', 'assets/images/' . $filename]);
}
}
$respond(true, 'success', tr('تم حفظ الإعدادات بنجاح.', 'Settings saved successfully.'), $redirectBack());
}

26
api/suppliers.php Normal file
View File

@ -0,0 +1,26 @@
<?php
require_once __DIR__ . '/../includes/app.php';
$user = require_permission('suppliers', 'add');
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 suppliers (name, phone) VALUES (?, ?)');
$stmt->execute([$name, $phone]);
$id = $pdo->lastInsertId();
echo json_encode(['success' => true, 'supplier' => ['id' => $id, 'name' => $name, 'phone' => $phone]]);
} catch (Throwable $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}

View File

@ -0,0 +1,39 @@
<?php
require_once __DIR__ . '/../includes/app.php';
require_permission('reports', 'show');
$user = current_user();
if (!in_array($user['role'], ['owner', 'manager'], true)) {
set_flash('danger', tr('غير مصرح لك.', 'Unauthorized.'));
redirect_to('../reports.php', ['tab' => 'daily']);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
redirect_to('../reports.php', ['tab' => 'daily']);
}
$reportDate = trim((string) ($_POST['date'] ?? date('Y-m-d')));
$branch = trim((string) ($_POST['branch'] ?? ''));
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $reportDate)) {
set_flash('danger', tr('تاريخ التقرير غير صالح.', 'Invalid report date.'));
redirect_to('../reports.php', ['tab' => 'daily']);
}
try {
$result = wablas_send_daily_report($reportDate, $branch !== '' ? $branch : null);
if (!empty($result['success'])) {
set_flash('success', tr('تم إرسال ملخص التقرير اليومي عبر واتساب.', 'Daily summary report sent via WhatsApp.'));
} else {
$error = (string) ($result['error'] ?? tr('تعذر إرسال التقرير عبر واتساب.', 'Could not send the WhatsApp report.'));
set_flash('danger', tr('فشل إرسال التقرير عبر واتساب.', 'Failed to send the WhatsApp report.') . ' ' . $error);
}
} catch (Throwable $e) {
set_flash('danger', tr('فشل إرسال التقرير عبر واتساب.', 'Failed to send the WhatsApp report.') . ' ' . $e->getMessage());
}
redirect_to('../reports.php', [
'tab' => 'daily',
'date' => $reportDate,
'branch' => $branch,
]);

79
api/wablas_test.php Normal file
View File

@ -0,0 +1,79 @@
<?php
require_once __DIR__ . '/../includes/app.php';
require_permission('settings', 'edit');
$user = current_user();
if (!in_array($user['role'], ['owner', 'manager'], true)) {
set_flash('danger', tr('غير مصرح لك.', 'Unauthorized.'));
redirect_to('../index.php');
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
redirect_to('../index.php');
}
$isAjax = strtolower((string) ($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '')) === 'xmlhttprequest';
$respond = static function (bool $success, string $type, string $message, ?string $redirect = null) use ($isAjax): void {
if ($isAjax) {
header('Content-Type: application/json; charset=UTF-8');
echo json_encode([
'success' => $success,
'type' => $type,
'message' => $message,
'redirect' => $redirect,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
set_flash($type, $message);
header('Location: ' . ($redirect ?: '../index.php'));
exit;
};
$redirectBack = static function (): string {
$referer = $_SERVER['HTTP_REFERER'] ?? '../index.php';
$returnModal = trim((string) ($_POST['return_modal'] ?? ''));
if ($returnModal === 'wablas') {
return append_query_params($referer, ['open_modal' => 'wablas']);
}
return $referer;
};
$phoneInput = trim((string) ($_POST['wablas_test_phone'] ?? ''));
$message = trim((string) ($_POST['wablas_test_message'] ?? ''));
$token = trim((string) ($_POST['wablas_token'] ?? get_setting('wablas_token', '')));
$secretKey = trim((string) ($_POST['wablas_secret_key'] ?? get_setting('wablas_secret_key', '')));
$apiUrl = trim((string) ($_POST['wablas_api_url'] ?? get_setting('wablas_api_url', '')));
if ($phoneInput === '') {
$respond(false, 'danger', tr('أدخل رقم واتساب تجريبي صالحاً من 8 خانات.', 'Enter a valid 8-digit test WhatsApp number.'), $redirectBack());
}
$phone = normalize_oman_phone($phoneInput);
if ($phone === '') {
$respond(false, 'danger', tr('رقم الاختبار يجب أن يكون عمانياً من 8 خانات.', 'The test phone must be a valid 8-digit Oman number.'), $redirectBack());
}
if ($message === '') {
$respond(false, 'danger', tr('اكتب رسالة الاختبار أولاً.', 'Write the test message first.'), $redirectBack());
}
if (!wablas_has_credentials($token, $secretKey)) {
$respond(false, 'danger', tr('أدخل Wablas Token و Secret Key قبل إرسال الاختبار.', 'Enter the Wablas token and secret key before sending a test message.'), $redirectBack());
}
$result = wablas_send_message($phone, $message, [
'token' => $token,
'secret_key' => $secretKey,
'api_url' => $apiUrl,
'ignore_enabled' => true,
]);
if (!empty($result['success'])) {
$respond(true, 'success', tr('تم إرسال رسالة الاختبار إلى 968 ', 'Test message sent to 968 ') . $phone . '.', $redirectBack());
} else {
$status = isset($result['status']) ? (' (' . (int) $result['status'] . ')') : '';
$respond(false, 'danger', tr('فشل إرسال رسالة الاختبار.', 'Failed to send the test message.') . ' ' . (string) ($result['error'] ?? ('Wablas error' . $status)), $redirectBack());
}
$respond(false, 'danger', tr('فشل إرسال رسالة الاختبار.', 'Failed to send the test message.'), $redirectBack());

View File

@ -1,403 +1,394 @@
:root {
--sidebar-width: 260px;
--primary-color: #4f46e5;
--primary-hover: #4338ca;
--surface-color: #ffffff;
--bg-color: #f8fafc;
--text-main: #0f172a;
--text-muted: #64748b;
--border-color: #e2e8f0;
--radius-sm: 0.5rem;
--radius-md: 0.75rem;
--radius-lg: 1rem;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
body {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
color: #212529;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
margin: 0;
min-height: 100vh;
font-family: 'Inter', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--bg-color);
color: var(--text-main);
-webkit-font-smoothing: antialiased;
}
.main-wrapper {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
#wrapper {
overflow-x: hidden;
}
/* Sidebar */
#sidebar-wrapper {
min-height: 100vh;
height: 100vh;
margin-left: calc(-1 * var(--sidebar-width));
transition: margin .25s ease-out;
width: var(--sidebar-width);
position: fixed;
top: 0;
left: 0;
z-index: 1000;
overflow-y: auto;
overflow-x: hidden;
scrollbar-gutter: stable;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.35) transparent;
background: #1e293b;
}
[dir="rtl"] #sidebar-wrapper {
margin-left: 0;
margin-right: calc(-1 * var(--sidebar-width));
left: auto;
right: 0;
}
#sidebar-wrapper::-webkit-scrollbar {
width: 6px;
}
#sidebar-wrapper::-webkit-scrollbar-track {
background: transparent;
}
#sidebar-wrapper::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
}
#sidebar-wrapper::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
.sidebar-heading {
padding: 1.5rem 1.25rem;
font-size: 1.25rem;
color: #fff;
background: rgba(0,0,0,0.1);
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.list-group-item {
border: none;
padding: 0.85rem 1.25rem;
background-color: transparent;
color: #cbd5e1;
font-weight: 500;
transition: all 0.2s;
margin: 0.2rem 0.75rem;
border-radius: var(--radius-sm);
}
.list-group-item:hover, .list-group-item.active {
background-color: rgba(255, 255, 255, 0.1);
color: #fff;
}
.list-group-item i {
margin-right: 12px;
width: 20px;
text-align: center;
font-size: 1.1em;
}
[dir="rtl"] .list-group-item i {
margin-right: 0;
margin-left: 12px;
}
#wrapper.toggled #sidebar-wrapper {
margin-left: 0;
}
[dir="rtl"] #wrapper.toggled #sidebar-wrapper {
margin-right: 0;
}
#page-content-wrapper {
min-width: 100vw;
transition: margin .25s ease-out;
}
@media (min-width: 768px) {
#sidebar-wrapper {
margin-left: 0;
}
[dir="rtl"] #sidebar-wrapper {
margin-right: 0;
}
#page-content-wrapper {
min-width: 0;
width: 100%;
padding: 20px;
box-sizing: border-box;
position: relative;
z-index: 1;
margin-left: var(--sidebar-width);
}
[dir="rtl"] #page-content-wrapper {
margin-left: 0;
margin-right: var(--sidebar-width);
}
#wrapper.toggled #sidebar-wrapper {
margin-left: calc(-1 * var(--sidebar-width));
}
[dir="rtl"] #wrapper.toggled #sidebar-wrapper {
margin-right: calc(-1 * var(--sidebar-width));
}
#wrapper.toggled #page-content-wrapper {
margin-left: 0;
}
[dir="rtl"] #wrapper.toggled #page-content-wrapper {
margin-right: 0;
}
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
/* Navbar */
.top-navbar {
background-color: var(--surface-color);
box-shadow: var(--shadow-sm);
border-bottom: 1px solid var(--border-color);
}
.chat-container {
width: 100%;
max-width: 600px;
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 20px;
display: flex;
flex-direction: column;
height: 85vh;
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
overflow: hidden;
/* Cards & Surface */
.surface-card, .card {
background: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
margin-bottom: 1.5rem;
transition: box-shadow 0.2s;
}
.surface-card:hover, .card:hover {
box-shadow: var(--shadow-md);
}
.surface-card.p-0, .card.p-0 {
padding: 0;
}
.card-header {
background-color: transparent;
border-bottom: 1px solid var(--border-color);
font-weight: 600;
padding: 1.25rem 1.5rem;
}
.card-body, .surface-card-body {
}
.chat-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background: rgba(255, 255, 255, 0.5);
font-weight: 700;
font-size: 1.1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
.message {
max-width: 85%;
padding: 0.85rem 1.1rem;
border-radius: 16px;
line-height: 1.5;
font-size: 0.95rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.message.visitor {
align-self: flex-end;
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
color: #fff;
border-bottom-right-radius: 4px;
}
.message.bot {
align-self: flex-start;
background: #ffffff;
color: #212529;
border-bottom-left-radius: 4px;
}
.chat-input-area {
padding: 1.25rem;
background: rgba(255, 255, 255, 0.5);
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.chat-input-area form {
display: flex;
gap: 0.75rem;
}
.chat-input-area input {
flex: 1;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 0.75rem 1rem;
outline: none;
background: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
}
.chat-input-area input:focus {
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
}
.chat-input-area button {
background: #212529;
color: #fff;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
.chat-input-area button:hover {
background: #000;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
/* Background Animations */
.bg-animations {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
pointer-events: none;
}
.blob {
position: absolute;
width: 500px;
height: 500px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
filter: blur(80px);
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
}
.blob-1 {
top: -10%;
left: -10%;
background: rgba(238, 119, 82, 0.4);
}
.blob-2 {
bottom: -10%;
right: -10%;
background: rgba(35, 166, 213, 0.4);
animation-delay: -7s;
width: 600px;
height: 600px;
}
.blob-3 {
top: 40%;
left: 30%;
background: rgba(231, 60, 126, 0.3);
animation-delay: -14s;
width: 450px;
height: 450px;
}
@keyframes move {
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
100% { transform: translate(0, 0) rotate(360deg) scale(1); }
}
.header-link {
font-size: 14px;
color: #fff;
text-decoration: none;
background: rgba(0, 0, 0, 0.2);
padding: 0.5rem 1rem;
border-radius: 8px;
transition: all 0.3s ease;
}
.header-link:hover {
background: rgba(0, 0, 0, 0.4);
text-decoration: none;
}
/* Admin Styles */
.admin-container {
max-width: 900px;
margin: 3rem auto;
padding: 2.5rem;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 24px;
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
border: 1px solid rgba(255, 255, 255, 0.4);
position: relative;
z-index: 1;
}
.admin-container h1 {
margin-top: 0;
color: #212529;
font-weight: 800;
/* Typography & Headers */
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
color: var(--text-main);
}
h1 { font-size: 1.75rem; }
h2 { font-size: 1.5rem; }
/* Tables (Grids / Lists) */
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
margin-top: 1.5rem;
width: 100%;
margin-bottom: 0;
color: var(--text-main);
vertical-align: middle;
}
.table th {
background: transparent;
border: none;
padding: 1rem;
color: #6c757d;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
font-weight: 600;
color: var(--text-muted);
background-color: #f8fafc;
border-bottom: 1px solid var(--border-color);
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 0.05em;
padding: 1rem 1.25rem;
white-space: nowrap;
}
.table td {
background: #fff;
padding: 1rem;
border: none;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border-color);
vertical-align: middle;
}
.table-hover tbody tr:hover {
background-color: #f1f5f9;
}
.table-responsive {
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
overflow: hidden;
background: var(--surface-color);
box-shadow: var(--shadow-sm);
margin-bottom: 1.5rem;
}
.table-responsive .table {
margin-bottom: 0;
}
.table-responsive .table th {
border-top: none;
}
.table tr td:first-child { border-radius: 12px 0 0 12px; }
.table tr td:last-child { border-radius: 0 12px 12px 0; }
.form-group {
margin-bottom: 1.25rem;
/* Forms & Inputs */
.form-control, .form-select {
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 0.6rem 1rem;
font-size: 0.95rem;
transition: all 0.2s;
background-color: #f8fafc;
}
.form-control:focus, .form-select:focus {
background-color: #fff;
border-color: var(--primary-color);
box-shadow: 0 0 0 0.25rem rgba(79, 70, 229, 0.2);
}
.form-label {
font-weight: 500;
font-size: 0.9rem;
color: var(--text-muted);
margin-bottom: 0.4rem;
}
.input-group-text {
background-color: #f8fafc;
border-color: var(--border-color);
color: var(--text-muted);
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
/* Buttons */
.btn {
border-radius: var(--radius-md);
padding: 0.6rem 1.25rem;
font-weight: 500;
transition: all 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-primary:hover {
background-color: var(--primary-hover);
border-color: var(--primary-hover);
}
.btn-sm {
padding: 0.4rem 0.8rem;
font-size: 0.875rem;
}
.form-control {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
background: #fff;
transition: all 0.3s ease;
box-sizing: border-box;
/* Modals */
.modal-content {
border-radius: var(--radius-lg);
border: none;
box-shadow: var(--shadow-lg);
}
.modal-header {
background-color: var(--surface-color);
border-bottom: 1px solid var(--border-color);
padding: 1.25rem 1.5rem;
position: relative;
}
.modal-body {
}
.modal-footer {
border-top: 1px solid var(--border-color);
background-color: #f8fafc;
padding: 1.25rem 1.5rem;
border-bottom-left-radius: var(--radius-lg);
border-bottom-right-radius: var(--radius-lg);
}
.modal-header .btn-close {
margin: 0;
position: absolute;
top: 1.25rem;
}
[dir="rtl"] .modal-header .btn-close {
left: 1.5rem;
right: auto;
}
[dir="ltr"] .modal-header .btn-close {
right: 1.5rem;
left: auto;
}
.form-control:focus {
outline: none;
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
/* Helpers */
.glassmorphism {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.filter-shell {
background: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
margin-bottom: 1.5rem;
box-shadow: var(--shadow-sm);
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
body.auth-body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background-color: #f1f5f9;
}
.header-links {
display: flex;
gap: 1rem;
/* Fix form-select arrow position in RTL mode */
[dir="rtl"] .form-select {
background-position: left 0.75rem center;
padding-right: 1rem;
padding-left: 2.25rem;
}
.admin-card {
background: rgba(255, 255, 255, 0.6);
padding: 2rem;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.5);
margin-bottom: 2.5rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
/* Print specific styles */
@media print {
#sidebar-wrapper, .top-navbar, .d-print-none {
display: none !important;
}
#page-content-wrapper {
margin: 0 !important;
width: 100% !important;
min-width: 100% !important;
padding: 0 !important;
}
body {
font-size: 11pt;
background-color: #fff;
}
.surface-card, .card {
box-shadow: none !important;
border: none !important;
margin-bottom: 0 !important;
padding: 0 !important;
}
.table-responsive {
border: none !important;
box-shadow: none !important;
}
.table th, .table td {
padding: 0.3rem;
}
}
details summary { list-style: none; cursor: pointer; }
details summary::-webkit-details-marker { display: none; }
.eid-advanced-panel {
border-top: 1px dashed var(--border-color);
margin-top: 0.85rem;
padding-top: 0.85rem;
}
.admin-card h3 {
margin-top: 0;
margin-bottom: 1.5rem;
font-weight: 700;
.card .table-responsive, .surface-card .table-responsive {
box-shadow: none; border: none; border-radius: 0;
margin-bottom: 0;
}
.btn-delete {
background: #dc3545;
color: white;
border: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
.surface-card { padding: 1.5rem; }
.table-sm th, .table-sm td {
padding: 0.5rem 0.75rem !important;
}
.btn-add {
background: #212529;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
margin-top: 1rem;
}
.btn-save {
background: #0088cc;
color: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
width: 100%;
transition: all 0.3s ease;
}
.webhook-url {
font-size: 0.85em;
color: #555;
margin-top: 0.5rem;
}
.history-table-container {
overflow-x: auto;
background: rgba(255, 255, 255, 0.4);
padding: 1rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.history-table {
width: 100%;
}
.history-table-time {
width: 15%;
white-space: nowrap;
font-size: 0.85em;
color: #555;
}
.history-table-user {
width: 35%;
background: rgba(255, 255, 255, 0.3);
border-radius: 8px;
padding: 8px;
}
.history-table-ai {
width: 50%;
background: rgba(255, 255, 255, 0.5);
border-radius: 8px;
padding: 8px;
}
.no-messages {
text-align: center;
color: #777;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -1,39 +1,125 @@
// Basic logic for cart interactions
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages');
const saleForm = document.querySelector('[data-sale-form]');
if (!saleForm) return;
const appendMessage = (text, sender) => {
const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender);
msgDiv.textContent = text;
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
};
let cart = [];
const cartJsonInput = document.getElementById('cart_json');
const cartLinesContainer = document.getElementById('cart-lines');
const cartEmptyState = document.getElementById('cart-empty-state');
const cartCountLabel = document.getElementById('cart-count-label');
const cartSubtotalLabel = document.getElementById('cart-subtotal');
const cartTotalLabel = document.getElementById('cart-total');
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const message = chatInput.value.trim();
if (!message) return;
function renderCart() {
if (cart.length === 0) {
cartEmptyState.style.display = 'block';
cartLinesContainer.innerHTML = '';
cartCountLabel.textContent = '0 ' + (window.saleLabels ? window.saleLabels.empty : 'items');
cartSubtotalLabel.textContent = '0.00';
cartTotalLabel.textContent = '0.00';
cartJsonInput.value = '[]';
return;
}
appendMessage(message, 'visitor');
chatInput.value = '';
cartEmptyState.style.display = 'none';
let html = '';
let totalItems = 0;
let subtotal = 0;
try {
const response = await fetch('api/chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
const data = await response.json();
// Artificial delay for realism
setTimeout(() => {
appendMessage(data.reply, 'bot');
}, 500);
} catch (error) {
console.error('Error:', error);
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
}
cart.forEach((item, index) => {
const lineTotal = item.qty * item.price;
totalItems += item.qty;
subtotal += lineTotal;
html += `
<div class="cart-line mb-2 p-2 border rounded bg-white shadow-sm d-flex justify-content-between align-items-center">
<div>
<div class="fw-semibold small">${item.name}</div>
<div class="text-muted" style="font-size: 0.75rem;">${item.qty} x ${item.price.toFixed(3)}</div>
</div>
<div class="d-flex align-items-center gap-2">
<span class="fw-bold text-primary">${lineTotal.toFixed(3)}</span>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary" onclick="updateCartQty(${index}, -1)">-</button>
<button type="button" class="btn btn-outline-secondary" onclick="updateCartQty(${index}, 1)">+</button>
<button type="button" class="btn btn-outline-danger" onclick="removeCartItem(${index})"><i class="bi bi-trash"></i></button>
</div>
</div>
</div>
`;
});
});
cartLinesContainer.innerHTML = html;
cartCountLabel.textContent = totalItems + ' items';
cartSubtotalLabel.textContent = subtotal.toFixed(3);
cartTotalLabel.textContent = subtotal.toFixed(3);
cartJsonInput.value = JSON.stringify(cart);
}
window.updateCartQty = function(index, delta) {
if (cart[index]) {
cart[index].qty += delta;
if (cart[index].qty <= 0) {
cart.splice(index, 1);
}
renderCart();
}
};
window.removeCartItem = function(index) {
cart.splice(index, 1);
renderCart();
};
document.querySelectorAll('[data-add-product]').forEach(btn => {
btn.addEventListener('click', () => {
const sku = btn.dataset.sku;
const name = btn.dataset.name;
const price = parseFloat(btn.dataset.price);
const existing = cart.find(i => i.sku === sku);
if (existing) {
existing.qty++;
} else {
cart.push({ sku, name, price, qty: 1 });
}
// SweetAlert2 Toast for adding product
if (typeof Swal !== 'undefined') {
Swal.fire({
toast: true,
position: 'top-end',
icon: 'success',
title: name + ' added',
showConfirmButton: false,
timer: 1500
});
}
renderCart();
});
});
const clearBtn = document.querySelector('[data-clear-cart]');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
cart = [];
renderCart();
});
}
// Handle form submission warning if empty
saleForm.addEventListener('submit', (e) => {
if (cart.length === 0) {
e.preventDefault();
if (typeof Swal !== 'undefined') {
Swal.fire('Empty Cart', 'Please add items before saving.', 'warning');
} else {
Swal.fire({icon: 'warning', text: 'Cart is empty'});
}
}
});
renderCart();
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

226
categories.php Normal file
View File

@ -0,0 +1,226 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('categories', 'show');
$pageTitle = tr('التصنيفات', 'Categories');
$activeNav = 'categories';
$pdo = db();
// Handle Form Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'create') {
$stmt = $pdo->prepare('INSERT INTO categories (name_ar, name_en, description) VALUES (?, ?, ?)');
$stmt->execute([$_POST['name_ar'], $_POST['name_en'], $_POST['description'] ?? '']);
set_flash('success', tr('تمت إضافة التصنيف بنجاح', 'Category added successfully'));
redirect_to('categories.php');
} elseif ($action === 'edit') {
$stmt = $pdo->prepare('UPDATE categories SET name_ar = ?, name_en = ?, description = ? WHERE id = ?');
$stmt->execute([$_POST['name_ar'], $_POST['name_en'], $_POST['description'] ?? '', $_POST['id']]);
set_flash('success', tr('تم التحديث بنجاح', 'Updated successfully'));
redirect_to('categories.php');
} elseif ($action === 'delete') {
$stmt = $pdo->prepare('DELETE FROM categories WHERE id = ?');
$stmt->execute([$_POST['id']]);
set_flash('success', tr('تم الحذف بنجاح', 'Deleted successfully'));
redirect_to('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 categories WHERE $where");
$totalStmt->execute($params);
$total = $totalStmt->fetchColumn();
$totalPages = ceil($total / $limit);
$queryStmt = $pdo->prepare("SELECT * FROM categories WHERE $where ORDER BY id DESC LIMIT $limit OFFSET $offset");
$queryStmt->execute($params);
$items = $queryStmt->fetchAll();
require __DIR__ . '/includes/header.php';
?>
<section class="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 product 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="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>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 text-center">
<thead class="table-light">
<tr>
<th>ID</th>
<th><?= h(tr('الاسم (عربي)', 'Name (AR)')) ?></th>
<th><?= h(tr('الاسم (إنجليزي)', 'Name (EN)')) ?></th>
<th><?= h(tr('الوصف', 'Description')) ?></th>
<th><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody class="border-top-0">
<?php if(empty($items)): ?>
<tr><td colspan="5" 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><?= h($item['description']) ?></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('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="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 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="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 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="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;
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'; ?>

5
cookies.txt Normal file
View File

@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
127.0.0.1 FALSE / FALSE 0 PHPSESSID knhve7lgedl0acegda65ol7h47

5
cookies_login.txt Normal file
View File

@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
127.0.0.1 FALSE / FALSE 0 PHPSESSID 3lfjkalu291jmcfc01kbpt4umj

5
cookies_login_owner.txt Normal file
View File

@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
127.0.0.1 FALSE / FALSE 0 PHPSESSID 43km9i3a7704fr6prmt70vau78

5
cookies_wablas_test.txt Normal file
View File

@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
127.0.0.1 FALSE / FALSE 0 PHPSESSID 1snhg6de9qetq9ilb98ihn8qnv

487
current_pos_snapshot.html Normal file
View File

@ -0,0 +1,487 @@
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>نقاط البيع · حلوى الريامي | Al Riyami Sweets</title>
<meta name="description" content='حلوى الريامي: Simplify sweets sales with multilingual POS, stock management, and detailed reports for multi-branch effic' />
<meta property="og:description" content="حلوى الريامي: Simplify sweets sales with multilingual POS, stock management, and detailed reports for multi-branch effic" />
<meta property="twitter:description" content="حلوى الريامي: Simplify sweets sales with multilingual POS, stock management, and detailed reports for multi-branch effic" />
<meta property="og:image" content="https://project-screens.s3.amazonaws.com/screenshots/39728/app-hero-20260419-014257.png" />
<meta property="twitter:image" content="https://project-screens.s3.amazonaws.com/screenshots/39728/app-hero-20260419-014257.png" />
<meta name="theme-color" content="#343a40" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css?v=202604201942">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
</head>
<body class="">
<!-- Private Admin Layout -->
<div class="d-flex" id="wrapper">
<!-- Sidebar -->
<div class="border-end bg-dark text-white shadow-sm" id="sidebar-wrapper">
<div class="sidebar-heading text-center py-4 fs-5 fw-bold text-uppercase border-bottom border-secondary d-flex flex-column align-items-center">
<i class="bi bi-shop me-2 fs-2 mb-2"></i>
<span>حلوى الريامي</span>
</div>
<div class="p-3 text-center border-bottom border-secondary">
<div class="fw-semibold">مالك النظام</div>
<div class="text-white-50 small">مالك / مدير عام · الفرع الرئيسي</div>
</div>
<div class="list-group list-group-flush my-3 pb-5">
<a class="list-group-item list-group-item-action " href="index.php?lang=ar">
<i class="bi bi-speedometer2"></i> لوحة التحكم </a>
<!-- المخزون (Inventory) - Now First -->
<a class="list-group-item list-group-item-action collapsed" data-bs-toggle="collapse" href="#collapseStock" role="button" aria-expanded="false" aria-controls="collapseStock">
<div class="d-flex justify-content-between align-items-center w-100">
<span><i class="bi bi-box-seam"></i> المخزون</span>
<i class="bi bi-chevron-down toggle-icon" style="transition: transform 0.2s;"></i>
</div>
</a>
<div class="collapse " id="collapseStock">
<div class="list-group list-group-flush" style="background-color: rgba(0,0,0,0.15);">
<a class="list-group-item list-group-item-action " href="stock.php?lang=ar" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> قائمة الأصناف </a>
<a class="list-group-item list-group-item-action " href="categories.php?lang=ar" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> التصنيفات </a>
<a class="list-group-item list-group-item-action " href="units.php?lang=ar" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> الوحدات </a>
</div>
</div>
<!-- المبيعات (Sales) - Now Collapsible -->
<a class="list-group-item list-group-item-action " data-bs-toggle="collapse" href="#collapseSales" role="button" aria-expanded="true" aria-controls="collapseSales">
<div class="d-flex justify-content-between align-items-center w-100">
<span><i class="bi bi-cart"></i> المبيعات</span>
<i class="bi bi-chevron-down toggle-icon" style="transition: transform 0.2s;"></i>
</div>
</a>
<div class="collapse show" id="collapseSales">
<div class="list-group list-group-flush" style="background-color: rgba(0,0,0,0.15);">
<a class="list-group-item list-group-item-action " href="sales.php?lang=ar" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> قائمة الفواتير </a>
<a class="list-group-item list-group-item-action " href="normal_sale.php?lang=ar" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> فاتورة جديدة </a>
<a class="list-group-item list-group-item-action active" href="pos.php?lang=ar" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> نقاط البيع </a>
</div>
</div>
<a class="list-group-item list-group-item-action collapsed" data-bs-toggle="collapse" href="#collapsePurchases" role="button" aria-expanded="false" aria-controls="collapsePurchases">
<div class="d-flex justify-content-between align-items-center w-100">
<span><i class="bi bi-bag-plus"></i> المشتريات</span>
<i class="bi bi-chevron-down toggle-icon" style="transition: transform 0.2s;"></i>
</div>
</a>
<div class="collapse " id="collapsePurchases">
<div class="list-group list-group-flush" style="background-color: rgba(0,0,0,0.15);">
<a class="list-group-item list-group-item-action " href="purchases.php?lang=ar" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> قائمة المشتريات </a>
<a class="list-group-item list-group-item-action " href="new_purchase.php?lang=ar" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> فاتورة مشتريات جديدة </a>
</div>
</div>
<!-- المصروفات (Expenses) -->
<a class="list-group-item list-group-item-action collapsed" data-bs-toggle="collapse" href="#collapseExpenses" role="button" aria-expanded="false" aria-controls="collapseExpenses">
<div class="d-flex justify-content-between align-items-center w-100">
<span><i class="bi bi-wallet2"></i> المصروفات</span>
<i class="bi bi-chevron-down toggle-icon" style="transition: transform 0.2s;"></i>
</div>
</a>
<div class="collapse " id="collapseExpenses">
<div class="list-group list-group-flush" style="background-color: rgba(0,0,0,0.15);">
<a class="list-group-item list-group-item-action " href="expenses.php?lang=ar" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> قائمة المصروفات </a>
<a class="list-group-item list-group-item-action " href="expense_categories.php?lang=ar" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> تصنيفات المصروفات </a>
</div>
</div>
<a class="list-group-item list-group-item-action " href="suppliers.php?lang=ar">
<i class="bi bi-truck"></i> الموردون </a>
<a class="list-group-item list-group-item-action " href="online_orders.php?lang=ar">
<i class="bi bi-cart-check"></i> طلبات المتجر </a>
<a class="list-group-item list-group-item-action " href="customers.php?lang=ar">
<i class="bi bi-people-fill"></i> العملاء </a>
<a class="list-group-item list-group-item-action " href="debts.php?lang=ar">
<i class="bi bi-journal-text"></i> الديون والفواتير الآجلة </a>
<a class="list-group-item list-group-item-action " href="reports.php?lang=ar">
<i class="bi bi-bar-chart"></i> التقارير </a>
<a class="list-group-item list-group-item-action " href="users.php?lang=ar">
<i class="bi bi-people"></i> المستخدمون والأدوار </a>
<a class="list-group-item list-group-item-action collapsed" data-bs-toggle="collapse" href="#collapseSettings" role="button" aria-expanded="false" aria-controls="collapseSettings">
<div class="d-flex justify-content-between align-items-center w-100">
<span><i class="bi bi-gear"></i> الإعدادات</span>
<i class="bi bi-chevron-down toggle-icon" style="transition: transform 0.2s;"></i>
</div>
</a>
<div class="collapse " id="collapseSettings">
<div class="list-group list-group-flush" style="background-color: rgba(0,0,0,0.15);">
<a class="list-group-item list-group-item-action" href="#" data-bs-toggle="modal" data-bs-target="#settingsModal" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> إعدادات الشركة </a>
<a class="list-group-item list-group-item-action " href="outlets.php?lang=ar" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> الفروع (المنافذ) </a>
</div>
</div>
</div>
</div>
<!-- /#sidebar-wrapper -->
<!-- Page Content -->
<div id="page-content-wrapper" class="d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom top-navbar px-3">
<div class="d-flex align-items-center justify-content-between w-100">
<div class="d-flex align-items-center">
<button class="btn btn-outline-secondary me-2" id="menu-toggle"><i class="bi bi-list"></i></button>
<h4 class="mb-0 ms-2 fw-semibold d-none d-md-block">نقاط البيع</h4>
</div>
<div class="d-flex align-items-center gap-3">
<a href="shop.php" target="_blank" class="btn btn-outline-success btn-sm me-2" title="زيارة المتجر"><i class="bi bi-shop"></i> <span class="d-none d-md-inline">المتجر</span></a>
<div class="language-switcher btn-group" role="group">
<a class="btn btn-sm btn-primary" href="pos.php?lang=ar">AR</a>
<a class="btn btn-sm btn-outline-primary" href="pos.php?lang=en">EN</a>
</div>
<div class="dropdown">
<button class="btn btn-light dropdown-toggle border" type="button" id="userMenu" data-bs-toggle="dropdown" aria-expanded="false">
<img src="assets/images/users/9869519f5f02861e6471bb2219173462.webp" alt="Avatar" class="rounded-circle me-1" style="width: 24px; height: 24px; object-fit: cover;">مالك النظام </button>
<ul class="dropdown-menu dropdown-menu-end shadow" aria-labelledby="userMenu">
<li><a class="dropdown-item" href="profile.php?lang=ar"><i class="bi bi-person me-2 text-primary"></i> الملف الشخصي</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="logout.php?lang=ar"><i class="bi bi-box-arrow-right text-danger me-2"></i> تسجيل الخروج</a></li>
</ul>
</div>
</div>
</div>
</nav>
<div class="container-fluid p-4">
<!-- Flash messages integration using SweetAlert2 -->
<style>
/* Modern POS Styles */
.pos-wrapper {
height: calc(100vh - 140px); /* Adjust based on your header */
min-height: 600px;
display: flex;
gap: 1.5rem;
}
.pos-products-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.pos-cart-area {
width: 380px;
background: #fff;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
display: flex;
flex-direction: column;
overflow: hidden;
flex-shrink: 0;
}
/* Categories Scroll */
.cat-scroll-container {
display: flex;
gap: 0.75rem;
overflow-x: auto;
padding-bottom: 0.5rem;
scrollbar-width: none; /* Firefox */
}
.cat-scroll-container::-webkit-scrollbar {
display: none; /* Chrome */
}
.cat-btn {
white-space: nowrap;
border-radius: 20px;
padding: 0.5rem 1.25rem;
font-weight: 500;
transition: all 0.2s;
background: #fff;
border: 1px solid #e9ecef;
color: #495057;
cursor: pointer;
}
.cat-btn:hover {
background: #f8f9fa;
}
.cat-btn.active {
background: linear-gradient(90deg, #0d6efd, #0dcaf0);
color: #fff;
border-color: transparent;
box-shadow: 0 4px 10px rgba(13, 110, 253, 0.2);
}
/* Products Grid */
.products-grid {
flex: 1;
overflow-y: auto;
padding-right: 0.5rem;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 1.25rem;
align-content: start;
}
.product-card {
background: #fff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
position: relative;
user-select: none;
}
.product-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
border-color: #0dcaf0;
}
.product-card:active {
transform: translateY(0);
}
.product-img-wrapper {
height: 120px;
width: 100%;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.product-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-placeholder {
font-size: 2rem;
color: #dee2e6;
}
.product-info {
padding: 0.75rem;
text-align: center;
}
.product-badges {
display: flex;
justify-content: center;
gap: 0.35rem;
flex-wrap: wrap;
margin-bottom: 0.4rem;
}
.product-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.15rem 0.5rem;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
line-height: 1;
}
.product-badge-new {
background: rgba(13, 202, 240, 0.14);
color: #087990;
}
.product-badge-no-image {
background: rgba(108, 117, 125, 0.12);
color: #6c757d;
}
.product-title {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 0.35rem;
color: #212529;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
line-height: 1.25;
min-height: 2.5em;
word-break: break-word;
}
.product-meta {
display: flex;
flex-direction: column;
gap: 0.15rem;
margin-bottom: 0.5rem;
}
.product-sku,
.product-created {
font-size: 0.76rem;
color: #6c757d;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.product-price {
font-weight: 700;
color: #0d6efd;
font-size: 1.05rem;
}
/* Cart Styles */
.cart-header {
background: linear-gradient(90deg, #0d6efd, #0dcaf0);
color: #fff;
padding: 1rem 1.25rem;
}
.cart-items {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.cart-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px dashed #e9ecef;
}
.cart-item-info {
flex: 1;
padding-right: 1rem;
}
.cart-item-title {
font-weight: 600;
font-size: 0.9rem;
color: #343a40;
}
.cart-item-price {
color: #6c757d;
font-size: 0.85rem;
}
.cart-item-controls {
display: flex;
align-items: center;
gap: 0.5rem;
background: #f8f9fa;
border-radius: 20px;
padding: 0.25rem;
}
.cart-btn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: none;
background: #fff;
color: #495057;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
transition: all 0.2s;
}
.cart-btn:hover {
background: #e9ecef;
}
.cart-qty {
font-weight: 600;
font-size: 0.9rem;
min-width: 20px;
text-align: center;
}
.cart-footer {
padding: 1.25rem;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
}
.summary-row {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.95rem;
color: #495057;
}
.summary-total {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
font-size: 1.25rem;
font-weight: 700;
color: #212529;
padding-top: 0.5rem;
border-top: 1px solid #dee2e6;
}
/* Pay / Action Buttons */
.btn-pay {
background: linear-gradient(90deg, #198754, #20c997);
border: none;
color: white;
font-size: 1.1rem;
font-weight: 600;
padding: 0.75rem;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(25, 135, 84, 0.2);
transition: all 0.2s;
}
.btn-pay:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(25, 135, 84, 0.3);
}
.btn-pay:disabled {
background: #adb5bd;
transform: none;
box-shadow: none;
}
.action-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-bottom: 1rem;
}
[dir="ltr"] .cart-item-info {
padding-right: 0;
padding-left: 1rem;
}
</style>
<div class="pos-wrapper">
<!-- Left Area: Products -->
<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="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="الباركود..." 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="بحث بالاسم..." 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()">
<i class="bi bi-clock-history me-1"></i> <span id="heldOrdersCount">0</span> قيد الانتظار </button>
</div>
</div>
<!-- Categories -->
<div class="cat-scroll-container mb-4" id="catContainer">
<button class="cat-btn active" data-cat="all" onclick="filterCat('all')">الكل</button>
<button class="cat-btn" data-cat="2" onclick="filterCat('2')">إكسسوارات</button>
<button class="cat-btn" data-cat="1" onclick="filterCat('1')">إلكترونيات</button>
<button class="cat-btn" data-cat="3" onclick="filterCat('3')">ملابس</button>
</div>
<!-- Grid -->
<div class="products-grid" id="productsGrid">
<div class="product-card" data-sku="25427" data-name="هدايا مختلفة" data-price="5.65" data-cat="1" data-search="

291
customers.php Normal file
View File

@ -0,0 +1,291 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('customers', 'show');
$pageTitle = tr('العملاء', 'Customers');
$activeNav = 'customers';
$pdo = db();
// Handle Form Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'create') {
$phoneInput = trim((string) ($_POST['phone'] ?? ''));
$phone = $phoneInput === '' ? '' : normalize_oman_phone($phoneInput);
$phoneForStorage = $phone === '' ? null : $phone;
if ($phoneInput !== '' && $phone === '') {
set_flash('danger', tr('رقم الهاتف يجب أن يكون عمانياً من 8 خانات.', 'Phone must be an 8-digit Oman number.'));
redirect_to('customers.php');
}
if ($phone !== '' && customer_phone_exists($phone)) {
set_flash('danger', tr('رقم الهاتف هذا مسجل لعميل آخر بالفعل.', 'This phone number is already assigned to another customer.'));
redirect_to('customers.php');
}
try {
$stmt = $pdo->prepare('INSERT INTO customers (name, phone, phone_normalized, email, address) VALUES (?, ?, ?, ?, ?)');
$stmt->execute([$_POST['name'], $phoneForStorage, $phoneForStorage, $_POST['email'] ?? '', $_POST['address'] ?? '']);
set_flash('success', tr('تمت إضافة العميل بنجاح', 'Customer added successfully'));
} catch (Throwable $e) {
if (is_customer_phone_unique_violation($e)) {
set_flash('danger', tr('رقم الهاتف هذا مسجل لعميل آخر بالفعل.', 'This phone number is already assigned to another customer.'));
} else {
throw $e;
}
}
redirect_to('customers.php');
} elseif ($action === 'edit') {
$phoneInput = trim((string) ($_POST['phone'] ?? ''));
$phone = $phoneInput === '' ? '' : normalize_oman_phone($phoneInput);
$phoneForStorage = $phone === '' ? null : $phone;
if ($phoneInput !== '' && $phone === '') {
set_flash('danger', tr('رقم الهاتف يجب أن يكون عمانياً من 8 خانات.', 'Phone must be an 8-digit Oman number.'));
redirect_to('customers.php');
}
$customerId = (int) ($_POST['id'] ?? 0);
if ($phone !== '' && customer_phone_exists($phone, $customerId)) {
set_flash('danger', tr('رقم الهاتف هذا مسجل لعميل آخر بالفعل.', 'This phone number is already assigned to another customer.'));
redirect_to('customers.php');
}
try {
$stmt = $pdo->prepare('UPDATE customers SET name = ?, phone = ?, phone_normalized = ?, email = ?, address = ? WHERE id = ?');
$stmt->execute([$_POST['name'], $phoneForStorage, $phoneForStorage, $_POST['email'] ?? '', $_POST['address'] ?? '', $_POST['id']]);
set_flash('success', tr('تم التحديث بنجاح', 'Updated successfully'));
} catch (Throwable $e) {
if (is_customer_phone_unique_violation($e)) {
set_flash('danger', tr('رقم الهاتف هذا مسجل لعميل آخر بالفعل.', 'This phone number is already assigned to another customer.'));
} else {
throw $e;
}
}
redirect_to('customers.php');
} elseif ($action === 'delete') {
$stmt = $pdo->prepare('DELETE FROM customers WHERE id = ?');
$stmt->execute([$_POST['id']]);
set_flash('success', tr('تم الحذف بنجاح', 'Deleted successfully'));
redirect_to('customers.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 LIKE ? OR phone LIKE ? OR CONCAT('968', phone) LIKE ? OR email LIKE ?)";
$params[] = "%$search%";
$params[] = "%$search%";
$params[] = "%$search%";
$params[] = "%$search%";
}
$totalStmt = $pdo->prepare("SELECT COUNT(*) FROM customers WHERE $where");
$totalStmt->execute($params);
$total = $totalStmt->fetchColumn();
$totalPages = ceil($total / $limit);
$queryStmt = $pdo->prepare("SELECT * FROM customers WHERE $where ORDER BY id DESC LIMIT $limit OFFSET $offset");
$queryStmt->execute($params);
$items = $queryStmt->fetchAll();
require __DIR__ . '/includes/header.php';
?>
<section class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h3 class="h5 mb-2"><i class="bi bi-people-fill me-2"></i><?= h($pageTitle) ?></h3>
<p class="text-muted mb-0"><?= h(tr('إدارة حسابات العملاء', 'Manage customer accounts')) ?></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 Customer')) ?>
</button>
</div>
<form class="d-flex mb-3" method="GET" action="customers.php">
<div class="input-group" style="max-width: 400px;">
<input type="text" name="q" class="form-control" placeholder="<?= h(tr('بحث بالاسم أو الهاتف...', 'Search name or phone...')) ?>" value="<?= h($search) ?>">
<button class="btn btn-outline-secondary" type="submit"><i class="bi bi-search"></i></button>
</div>
</form>
</section>
<section>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 text-center">
<thead class="table-light">
<tr>
<th>ID</th>
<th><?= h(tr('الاسم', 'Name')) ?></th>
<th><?= h(tr('الهاتف', 'Phone')) ?></th>
<th><?= h(tr('البريد', 'Email')) ?></th>
<th><?= h(tr('العنوان', 'Address')) ?></th>
<th><?= 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['id']) ?></td>
<td class="fw-semibold"><?= h($item['name']) ?></td>
<td><?= h(phone_display($item['phone'])) ?></td>
<td><?= h($item['email']) ?></td>
<td><?= h($item['address']) ?></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('customers.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="customers.php">
<input type="hidden" name="action" value="create">
<div class="modal-header">
<h5 class="modal-title"><?= h(tr('إضافة عميل', 'Add Customer')) ?></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')) ?></label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label"><?= h(tr('الهاتف', 'Phone')) ?></label>
<div class="input-group" dir="ltr">
<span class="input-group-text">968</span>
<input type="text" name="phone" class="form-control" inputmode="numeric" maxlength="8" pattern="\d{8}" placeholder="91234567">
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?= h(tr('البريد الإلكتروني', 'Email')) ?></label>
<input type="email" name="email" class="form-control">
</div>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('العنوان', 'Address')) ?></label>
<textarea name="address" 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="customers.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')) ?></label>
<input type="text" name="name" id="edit_name" class="form-control" required>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label"><?= h(tr('الهاتف', 'Phone')) ?></label>
<div class="input-group" dir="ltr">
<span class="input-group-text">968</span>
<input type="text" name="phone" id="edit_phone" class="form-control" inputmode="numeric" maxlength="8" pattern="\d{8}" placeholder="91234567">
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?= h(tr('البريد الإلكتروني', 'Email')) ?></label>
<input type="email" name="email" id="edit_email" class="form-control">
</div>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('العنوان', 'Address')) ?></label>
<textarea name="address" id="edit_address" 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="customers.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').value = item.name;
document.getElementById('edit_phone').value = item.phone || '';
document.getElementById('edit_email').value = item.email || '';
document.getElementById('edit_address').value = item.address || '';
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

@ -0,0 +1,133 @@
-- Existing installation patch for partial payments + WhatsApp toggle/test settings support
-- Safe to import from phpMyAdmin on an already-installed database.
-- It only adds missing columns/settings and backfills payment amounts/status.
SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS;
SET FOREIGN_KEY_CHECKS = 0;
DROP PROCEDURE IF EXISTS apply_existing_install_patch;
DELIMITER $$
CREATE PROCEDURE apply_existing_install_patch()
BEGIN
-- users.avatar
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'users'
AND COLUMN_NAME = 'avatar'
) THEN
ALTER TABLE users
ADD COLUMN avatar VARCHAR(255) DEFAULT NULL;
END IF;
-- branches.avatar
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'branches'
AND COLUMN_NAME = 'avatar'
) THEN
ALTER TABLE branches
ADD COLUMN avatar VARCHAR(255) DEFAULT NULL;
END IF;
-- sales_orders.customer_id
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sales_orders'
AND COLUMN_NAME = 'customer_id'
) THEN
ALTER TABLE sales_orders
ADD COLUMN customer_id INT(10) UNSIGNED DEFAULT NULL;
END IF;
-- sales_orders.payment_status
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sales_orders'
AND COLUMN_NAME = 'payment_status'
) THEN
ALTER TABLE sales_orders
ADD COLUMN payment_status VARCHAR(20) NOT NULL DEFAULT 'paid';
END IF;
-- sales_orders.vat_amount
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sales_orders'
AND COLUMN_NAME = 'vat_amount'
) THEN
ALTER TABLE sales_orders
ADD COLUMN vat_amount DECIMAL(10,3) NOT NULL DEFAULT 0.000 AFTER subtotal;
END IF;
-- sales_orders.paid_amount
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sales_orders'
AND COLUMN_NAME = 'paid_amount'
) THEN
ALTER TABLE sales_orders
ADD COLUMN paid_amount DECIMAL(10,3) NOT NULL DEFAULT 0.000 AFTER total_amount;
END IF;
-- sales_orders.due_amount
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sales_orders'
AND COLUMN_NAME = 'due_amount'
) THEN
ALTER TABLE sales_orders
ADD COLUMN due_amount DECIMAL(10,3) NOT NULL DEFAULT 0.000 AFTER paid_amount;
END IF;
-- Backfill / normalize payment values for existing invoices.
UPDATE sales_orders
SET paid_amount = CASE
WHEN payment_status = 'unpaid' THEN 0
ELSE total_amount
END
WHERE paid_amount IS NULL OR paid_amount = 0;
UPDATE sales_orders
SET due_amount = GREATEST(total_amount - paid_amount, 0);
UPDATE sales_orders
SET payment_status = CASE
WHEN due_amount <= 0.0005 THEN 'paid'
WHEN paid_amount > 0 THEN 'partial'
ELSE 'unpaid'
END;
-- Settings keys used by the new WhatsApp tab and toggle.
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_enabled', '1');
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_token', '');
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_secret_key', '');
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_template_created', '');
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_template_pending', '');
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_template_accepted', '');
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_template_completed', '');
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('wablas_template_rejected', '');
END $$
DELIMITER ;
CALL apply_existing_install_patch();
DROP PROCEDURE IF EXISTS apply_existing_install_patch;
SET FOREIGN_KEY_CHECKS = @OLD_FOREIGN_KEY_CHECKS;
-- Optional verification after import:
-- SHOW COLUMNS FROM sales_orders;
-- SELECT setting_key, setting_value FROM settings WHERE setting_key LIKE 'wablas%';

View File

@ -0,0 +1,45 @@
-- Seed default WhatsApp / Wablas order templates for existing installs.
-- Safe to import in phpMyAdmin multiple times; existing non-empty values are preserved.
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES
('wablas_template_created', ''),
('wablas_template_pending', ''),
('wablas_template_accepted', ''),
('wablas_template_completed', ''),
('wablas_template_rejected', '');
UPDATE settings
SET setting_value = 'مرحباً {customer_name}، تم استلام طلبك رقم #{order_id}.
الحالة: {status_label}
الإجمالي: {total_amount}
العنوان: {customer_address}
شكراً لتسوقك معنا.'
WHERE setting_key = 'wablas_template_created'
AND (setting_value IS NULL OR TRIM(setting_value) = '');
UPDATE settings
SET setting_value = 'مرحباً {customer_name}، طلبك رقم #{order_id} ما زال {status_label}.
الإجمالي: {total_amount}
سنوافيك بأي تحديث جديد.'
WHERE setting_key = 'wablas_template_pending'
AND (setting_value IS NULL OR TRIM(setting_value) = '');
UPDATE settings
SET setting_value = 'مرحباً {customer_name}، تم قبول طلبك رقم #{order_id}.
الإجمالي: {total_amount}
سنبدأ التجهيز الآن.'
WHERE setting_key = 'wablas_template_accepted'
AND (setting_value IS NULL OR TRIM(setting_value) = '');
UPDATE settings
SET setting_value = 'مرحباً {customer_name}، طلبك رقم #{order_id} أصبح {status_label}.
الإجمالي: {total_amount}
شكراً لك.'
WHERE setting_key = 'wablas_template_completed'
AND (setting_value IS NULL OR TRIM(setting_value) = '');
UPDATE settings
SET setting_value = 'مرحباً {customer_name}، نعتذر، تم تحديث طلبك رقم #{order_id} إلى {status_label}.
إذا رغبت بالمساعدة تواصل معنا.'
WHERE setting_key = 'wablas_template_rejected'
AND (setting_value IS NULL OR TRIM(setting_value) = '');

View File

@ -0,0 +1,65 @@
-- Step 1 foundation for Eid Orders (طلبات العيد)
-- Adds Eid-specific tracking columns to the shared sales_orders table.
SET @db_name := DATABASE();
SET @sql := IF (
EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'sales_orders' AND COLUMN_NAME = 'order_type'
),
'SELECT 1',
"ALTER TABLE sales_orders ADD COLUMN order_type varchar(30) NOT NULL DEFAULT 'standard' AFTER status"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @sql := IF (
EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'sales_orders' AND COLUMN_NAME = 'delivery_status'
),
'SELECT 1',
"ALTER TABLE sales_orders ADD COLUMN delivery_status varchar(30) NOT NULL DEFAULT 'pending' AFTER order_type"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @sql := IF (
EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'sales_orders' AND COLUMN_NAME = 'delivery_date'
),
'SELECT 1',
"ALTER TABLE sales_orders ADD COLUMN delivery_date date DEFAULT NULL AFTER delivery_status"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @sql := IF (
EXISTS (
SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'sales_orders' AND INDEX_NAME = 'idx_order_type'
),
'SELECT 1',
"ALTER TABLE sales_orders ADD INDEX idx_order_type (order_type)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @sql := IF (
EXISTS (
SELECT 1 FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'sales_orders' AND INDEX_NAME = 'idx_delivery_date'
),
'SELECT 1',
"ALTER TABLE sales_orders ADD INDEX idx_delivery_date (delivery_date)"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
UPDATE sales_orders
SET order_type = 'standard'
WHERE order_type IS NULL OR order_type = '';
UPDATE sales_orders
SET delivery_status = CASE
WHEN COALESCE(status, 'completed') = 'completed' THEN 'delivered'
ELSE 'pending'
END
WHERE delivery_status IS NULL OR delivery_status = '';

View File

@ -0,0 +1,125 @@
-- Existing installation patch for online_orders payment gateway support + legal/settings keys.
-- Safe to import from phpMyAdmin on an already-installed database.
-- It only adds missing columns/settings and backfills payment defaults.
SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS;
SET FOREIGN_KEY_CHECKS = 0;
DROP PROCEDURE IF EXISTS apply_online_orders_payment_patch;
DELIMITER $$
CREATE PROCEDURE apply_online_orders_payment_patch()
BEGIN
IF EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'online_orders'
) THEN
-- online_orders.payment_method
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'online_orders'
AND COLUMN_NAME = 'payment_method'
) THEN
ALTER TABLE online_orders
ADD COLUMN payment_method VARCHAR(30) NOT NULL DEFAULT 'pay_later' AFTER total_amount;
END IF;
-- online_orders.payment_gateway
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'online_orders'
AND COLUMN_NAME = 'payment_gateway'
) THEN
ALTER TABLE online_orders
ADD COLUMN payment_gateway VARCHAR(30) DEFAULT NULL AFTER payment_method;
END IF;
-- online_orders.payment_status
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'online_orders'
AND COLUMN_NAME = 'payment_status'
) THEN
ALTER TABLE online_orders
ADD COLUMN payment_status VARCHAR(20) NOT NULL DEFAULT 'unpaid' AFTER payment_gateway;
END IF;
-- online_orders.gateway_session_id
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'online_orders'
AND COLUMN_NAME = 'gateway_session_id'
) THEN
ALTER TABLE online_orders
ADD COLUMN gateway_session_id VARCHAR(120) DEFAULT NULL AFTER payment_status;
END IF;
-- online_orders.gateway_transaction_id
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'online_orders'
AND COLUMN_NAME = 'gateway_transaction_id'
) THEN
ALTER TABLE online_orders
ADD COLUMN gateway_transaction_id VARCHAR(120) DEFAULT NULL AFTER gateway_session_id;
END IF;
-- online_orders.paid_at
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'online_orders'
AND COLUMN_NAME = 'paid_at'
) THEN
ALTER TABLE online_orders
ADD COLUMN paid_at DATETIME DEFAULT NULL AFTER gateway_transaction_id;
END IF;
-- Backfill / normalize payment values for existing online orders.
UPDATE online_orders
SET payment_method = 'pay_later'
WHERE payment_method IS NULL OR TRIM(payment_method) = '';
UPDATE online_orders
SET payment_status = CASE
WHEN payment_method = 'pay_online' THEN 'pending'
ELSE 'unpaid'
END
WHERE payment_status IS NULL OR TRIM(payment_status) = '';
END IF;
-- Settings keys used by the Thawani integration and public legal pages.
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('thawani_enabled', '0');
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('thawani_mode', 'sandbox');
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('thawani_publishable_key', '');
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('thawani_secret_key', '');
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('thawani_success_url', '');
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('thawani_cancel_url', '');
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('privacy_policy_content', '');
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('terms_conditions_content', '');
END $$
DELIMITER ;
CALL apply_online_orders_payment_patch();
DROP PROCEDURE IF EXISTS apply_online_orders_payment_patch;
SET FOREIGN_KEY_CHECKS = @OLD_FOREIGN_KEY_CHECKS;
-- Optional verification after import:
-- SHOW COLUMNS FROM online_orders;
-- SELECT setting_key, setting_value FROM settings WHERE setting_key IN (
-- 'thawani_enabled', 'thawani_mode', 'thawani_publishable_key', 'thawani_secret_key',
-- 'thawani_success_url', 'thawani_cancel_url', 'privacy_policy_content', 'terms_conditions_content'
-- );

View File

@ -0,0 +1,54 @@
-- Add normalized customer phone storage and a unique index for duplicate protection.
-- Safe to import on an existing database before/after the app code change.
DROP PROCEDURE IF EXISTS apply_customers_phone_normalized_unique;
DELIMITER $$
CREATE PROCEDURE apply_customers_phone_normalized_unique()
BEGIN
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'customers'
AND COLUMN_NAME = 'phone_normalized'
) THEN
ALTER TABLE customers
ADD COLUMN phone_normalized VARCHAR(8) DEFAULT NULL AFTER phone;
END IF;
UPDATE customers
SET phone = NULLIF(TRIM(phone), '');
UPDATE customers
SET phone_normalized = CASE
WHEN phone IS NULL OR TRIM(phone) = '' THEN NULL
WHEN REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', '') REGEXP '^[0-9]{8}$'
THEN REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', '')
WHEN REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', '') REGEXP '^0[0-9]{8}$'
THEN RIGHT(REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', ''), 8)
WHEN REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', '') REGEXP '^968[0-9]{8}$'
THEN RIGHT(REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', ''), 8)
WHEN REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', '') REGEXP '^00968[0-9]{8}$'
THEN RIGHT(REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', ''), 8)
ELSE NULL
END;
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'customers'
AND INDEX_NAME = 'uniq_customers_phone_normalized'
) THEN
ALTER TABLE customers
ADD UNIQUE KEY uniq_customers_phone_normalized (phone_normalized);
END IF;
END $$
DELIMITER ;
CALL apply_customers_phone_normalized_unique();
DROP PROCEDURE IF EXISTS apply_customers_phone_normalized_unique;
-- Optional verification after import:
-- SHOW COLUMNS FROM customers;
-- SHOW INDEX FROM customers;

View File

@ -0,0 +1,172 @@
-- Production sync for current sales/POS/Eid release.
-- Safe to import on an existing production database.
-- This consolidates the sales_orders columns/indexes now required by the app.
SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS;
SET FOREIGN_KEY_CHECKS = 0;
DROP PROCEDURE IF EXISTS apply_production_sales_orders_sync;
DELIMITER $$
CREATE PROCEDURE apply_production_sales_orders_sync()
BEGIN
-- sales_orders.customer_id
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sales_orders'
AND COLUMN_NAME = 'customer_id'
) THEN
ALTER TABLE sales_orders
ADD COLUMN customer_id INT(10) UNSIGNED DEFAULT NULL AFTER role_name;
END IF;
-- sales_orders.payment_status
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sales_orders'
AND COLUMN_NAME = 'payment_status'
) THEN
ALTER TABLE sales_orders
ADD COLUMN payment_status VARCHAR(20) NOT NULL DEFAULT 'paid' AFTER payment_method;
END IF;
-- sales_orders.vat_amount
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sales_orders'
AND COLUMN_NAME = 'vat_amount'
) THEN
ALTER TABLE sales_orders
ADD COLUMN vat_amount DECIMAL(10,3) NOT NULL DEFAULT 0.000 AFTER subtotal;
END IF;
-- sales_orders.paid_amount
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sales_orders'
AND COLUMN_NAME = 'paid_amount'
) THEN
ALTER TABLE sales_orders
ADD COLUMN paid_amount DECIMAL(10,3) NOT NULL DEFAULT 0.000 AFTER total_amount;
END IF;
-- sales_orders.due_amount
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sales_orders'
AND COLUMN_NAME = 'due_amount'
) THEN
ALTER TABLE sales_orders
ADD COLUMN due_amount DECIMAL(10,3) NOT NULL DEFAULT 0.000 AFTER paid_amount;
END IF;
-- sales_orders.order_type
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sales_orders'
AND COLUMN_NAME = 'order_type'
) THEN
ALTER TABLE sales_orders
ADD COLUMN order_type VARCHAR(30) NOT NULL DEFAULT 'standard' AFTER status;
END IF;
-- sales_orders.delivery_status
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sales_orders'
AND COLUMN_NAME = 'delivery_status'
) THEN
ALTER TABLE sales_orders
ADD COLUMN delivery_status VARCHAR(30) NOT NULL DEFAULT 'pending' AFTER order_type;
END IF;
-- sales_orders.delivery_date
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sales_orders'
AND COLUMN_NAME = 'delivery_date'
) THEN
ALTER TABLE sales_orders
ADD COLUMN delivery_date DATE DEFAULT NULL AFTER delivery_status;
END IF;
-- sales_orders indexes used by the current Eid Orders queries.
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sales_orders'
AND INDEX_NAME = 'idx_order_type'
) THEN
ALTER TABLE sales_orders
ADD INDEX idx_order_type (order_type);
END IF;
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sales_orders'
AND INDEX_NAME = 'idx_delivery_date'
) THEN
ALTER TABLE sales_orders
ADD INDEX idx_delivery_date (delivery_date);
END IF;
-- Backfill / normalize values for existing rows.
UPDATE sales_orders
SET paid_amount = CASE
WHEN COALESCE(payment_status, 'paid') = 'unpaid' THEN 0
WHEN COALESCE(paid_amount, 0) = 0 THEN total_amount
ELSE paid_amount
END
WHERE paid_amount IS NULL OR paid_amount = 0;
UPDATE sales_orders
SET due_amount = GREATEST(total_amount - COALESCE(paid_amount, 0), 0)
WHERE due_amount IS NULL OR ABS(due_amount - GREATEST(total_amount - COALESCE(paid_amount, 0), 0)) > 0.0005;
UPDATE sales_orders
SET payment_status = CASE
WHEN COALESCE(due_amount, 0) <= 0.0005 THEN 'paid'
WHEN COALESCE(paid_amount, 0) > 0 THEN 'partial'
ELSE 'unpaid'
END
WHERE payment_status IS NULL OR TRIM(payment_status) = '' OR payment_status NOT IN ('paid', 'partial', 'unpaid');
UPDATE sales_orders
SET order_type = 'standard'
WHERE order_type IS NULL OR TRIM(order_type) = '';
UPDATE sales_orders
SET delivery_status = CASE
WHEN COALESCE(status, 'completed') = 'completed' THEN 'delivered'
ELSE 'pending'
END
WHERE delivery_status IS NULL OR TRIM(delivery_status) = '';
END $$
DELIMITER ;
CALL apply_production_sales_orders_sync();
DROP PROCEDURE IF EXISTS apply_production_sales_orders_sync;
SET FOREIGN_KEY_CHECKS = @OLD_FOREIGN_KEY_CHECKS;
-- Optional verification after import:
-- SHOW COLUMNS FROM sales_orders;
-- SHOW INDEX FROM sales_orders;

View File

@ -0,0 +1,14 @@
-- Add notes field to products/items for internal product remarks.
-- Safe to run multiple times on existing databases.
SET @db_name := DATABASE();
SET @sql := IF (
EXISTS (
SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'items' AND COLUMN_NAME = 'notes'
),
'SELECT 1',
"ALTER TABLE items ADD COLUMN notes text DEFAULT NULL AFTER image_url"
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;

View File

@ -0,0 +1,34 @@
-- Production sync for 2026-05-01 changes.
-- Safe to import on an existing production database.
-- Includes all schema migrations introduced on 2026-05-01.
-- Current scope:
-- 1) items.notes for internal product remarks in stock management.
SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS;
SET FOREIGN_KEY_CHECKS = 0;
DROP PROCEDURE IF EXISTS apply_production_updates_20260501;
DELIMITER $$
CREATE PROCEDURE apply_production_updates_20260501()
BEGIN
-- items.notes
IF NOT EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'items'
AND COLUMN_NAME = 'notes'
) THEN
ALTER TABLE items
ADD COLUMN notes TEXT DEFAULT NULL AFTER image_url;
END IF;
END $$
DELIMITER ;
CALL apply_production_updates_20260501();
DROP PROCEDURE IF EXISTS apply_production_updates_20260501;
SET FOREIGN_KEY_CHECKS = @OLD_FOREIGN_KEY_CHECKS;
-- Optional verification after import:
-- SHOW COLUMNS FROM items;

266
db/schema.sql Normal file
View File

@ -0,0 +1,266 @@
SET FOREIGN_KEY_CHECKS=0;
-- Table structure for `branches`
CREATE TABLE IF NOT EXISTS `branches` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(50) NOT NULL,
`name_ar` varchar(100) NOT NULL,
`name_en` varchar(100) NOT NULL,
`avatar` varchar(255) DEFAULT NULL,
`city_ar` varchar(100) DEFAULT NULL,
`city_en` varchar(100) DEFAULT NULL,
`created_at` datetime DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table structure for `categories`
CREATE TABLE IF NOT EXISTS `categories` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name_ar` varchar(150) NOT NULL,
`name_en` varchar(150) NOT NULL,
`description` text DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table structure for `customers`
CREATE TABLE IF NOT EXISTS `customers` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(150) NOT NULL,
`phone` varchar(50) DEFAULT NULL,
`phone_normalized` varchar(8) DEFAULT NULL,
`email` varchar(150) DEFAULT NULL,
`address` text DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_customers_phone_normalized` (`phone_normalized`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table structure for `expense_categories`
CREATE TABLE IF NOT EXISTS `expense_categories` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name_ar` varchar(255) NOT NULL,
`name_en` varchar(255) NOT NULL,
`created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table structure for `expenses`
CREATE TABLE IF NOT EXISTS `expenses` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`branch_code` varchar(50) DEFAULT NULL,
`category_id` int(11) NOT NULL,
`amount` decimal(10,3) NOT NULL,
`expense_date` date NOT NULL,
`description` text DEFAULT NULL,
`created_by` int(11) DEFAULT NULL,
`created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `category_id` (`category_id`),
CONSTRAINT `expenses_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `expense_categories` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table structure for `items`
CREATE TABLE IF NOT EXISTS `items` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`sku` varchar(50) NOT NULL,
`name` varchar(200) NOT NULL,
`price` decimal(10,3) NOT NULL,
`cost_price` decimal(10,2) DEFAULT 0.00,
`base_stock` int(11) NOT NULL DEFAULT 0,
`vat` decimal(5,3) NOT NULL DEFAULT 5.000,
`category_id` int(10) unsigned DEFAULT NULL,
`supplier_id` int(10) unsigned DEFAULT NULL,
`image_url` varchar(255) DEFAULT NULL,
`notes` text DEFAULT NULL,
`created_at` datetime DEFAULT current_timestamp(),
`unit_id` int(10) unsigned DEFAULT NULL,
`in_catalog` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `sku` (`sku`),
KEY `category_id` (`category_id`),
KEY `supplier_id` (`supplier_id`),
KEY `items_unit_fk` (`unit_id`),
CONSTRAINT `items_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
CONSTRAINT `items_ibfk_2` FOREIGN KEY (`supplier_id`) REFERENCES `suppliers` (`id`) ON DELETE SET NULL,
CONSTRAINT `items_unit_fk` FOREIGN KEY (`unit_id`) REFERENCES `units` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table structure for `online_orders`
CREATE TABLE IF NOT EXISTS `online_orders` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`customer_name` varchar(255) NOT NULL,
`customer_phone` varchar(50) NOT NULL,
`customer_address` text NOT NULL,
`items_json` longtext NOT NULL,
`subtotal` decimal(10,2) DEFAULT 0.00,
`vat_amount` decimal(10,2) DEFAULT 0.00,
`total_amount` decimal(10,2) NOT NULL,
`status` varchar(20) NOT NULL DEFAULT 'pending',
`created_at` datetime DEFAULT current_timestamp(),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table structure for `purchase_orders`
CREATE TABLE IF NOT EXISTS `purchase_orders` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`reference_no` varchar(50) NOT NULL,
`branch_code` varchar(30) NOT NULL,
`user_username` varchar(60) NOT NULL,
`user_name` varchar(120) NOT NULL,
`role_name` varchar(40) NOT NULL,
`supplier_name` varchar(120) DEFAULT NULL,
`items_json` longtext NOT NULL,
`item_count` int(10) unsigned NOT NULL DEFAULT 0,
`subtotal` decimal(10,2) NOT NULL DEFAULT 0.00,
`vat_amount` decimal(10,3) NOT NULL DEFAULT 0.000,
`total_amount` decimal(10,2) NOT NULL DEFAULT 0.00,
`notes` text DEFAULT NULL,
`status` varchar(20) NOT NULL DEFAULT 'completed',
`purchase_date` datetime NOT NULL DEFAULT current_timestamp(),
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `reference_no` (`reference_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table structure for `sales_orders`
CREATE TABLE IF NOT EXISTS `sales_orders` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`receipt_no` varchar(50) NOT NULL,
`eid_serial_no` int(10) unsigned DEFAULT NULL,
`sale_mode` varchar(20) NOT NULL,
`branch_code` varchar(30) NOT NULL,
`cashier_username` varchar(60) NOT NULL,
`cashier_name` varchar(120) NOT NULL,
`role_name` varchar(40) NOT NULL,
`customer_id` int(10) unsigned DEFAULT NULL,
`customer_name` varchar(120) DEFAULT NULL,
`payment_method` varchar(30) NOT NULL,
`payment_status` varchar(20) NOT NULL DEFAULT 'paid',
`items_json` longtext NOT NULL,
`item_count` int(10) unsigned NOT NULL DEFAULT 0,
`subtotal` decimal(10,2) NOT NULL DEFAULT 0.00,
`vat_amount` decimal(10,3) NOT NULL DEFAULT 0.000,
`total_amount` decimal(10,2) NOT NULL DEFAULT 0.00,
`paid_amount` decimal(10,3) NOT NULL DEFAULT 0.000,
`due_amount` decimal(10,3) NOT NULL DEFAULT 0.000,
`notes` text DEFAULT NULL,
`sale_date` datetime NOT NULL DEFAULT current_timestamp(),
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
`status` varchar(20) NOT NULL DEFAULT 'completed',
`order_type` varchar(30) NOT NULL DEFAULT 'standard',
`delivery_status` varchar(30) NOT NULL DEFAULT 'pending',
`delivery_date` date DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `receipt_no` (`receipt_no`),
KEY `idx_sale_mode` (`sale_mode`),
KEY `idx_branch_code` (`branch_code`),
KEY `idx_sale_date` (`sale_date`),
KEY `idx_order_type` (`order_type`),
KEY `idx_delivery_date` (`delivery_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table structure for `settings`
CREATE TABLE IF NOT EXISTS `settings` (
`setting_key` varchar(50) NOT NULL,
`setting_value` text DEFAULT NULL,
PRIMARY KEY (`setting_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table structure for `suppliers`
CREATE TABLE IF NOT EXISTS `suppliers` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(150) NOT NULL,
`contact_person` varchar(150) DEFAULT NULL,
`phone` varchar(50) DEFAULT NULL,
`email` varchar(150) DEFAULT NULL,
`address` text DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table structure for `units`
CREATE TABLE IF NOT EXISTS `units` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name_ar` varchar(150) NOT NULL,
`name_en` varchar(150) NOT NULL,
`created_at` datetime DEFAULT current_timestamp(),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Table structure for `users`
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL,
`role` varchar(50) NOT NULL,
`permissions` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`permissions`)),
`branch_code` varchar(50) NOT NULL,
`allowed_branches` varchar(255) DEFAULT NULL,
`name_ar` varchar(100) NOT NULL,
`name_en` varchar(100) NOT NULL,
`avatar` varchar(255) DEFAULT NULL,
`created_at` timestamp NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Data for table `branches`
INSERT IGNORE INTO `branches` (`id`, `code`, `name_ar`, `name_en`, `city_ar`, `city_en`, `created_at`) VALUES ('1', 'muscat', 'الفرع الرئيسي', 'Main Branch', 'مسقط', 'Muscat', '2026-04-19 15:16:52');
INSERT IGNORE INTO `branches` (`id`, `code`, `name_ar`, `name_en`, `city_ar`, `city_en`, `created_at`) VALUES ('2', 'sohar', 'فرع صحار', 'Sohar Branch', 'صحار', 'Sohar', '2026-04-19 15:16:52');
INSERT IGNORE INTO `branches` (`id`, `code`, `name_ar`, `name_en`, `city_ar`, `city_en`, `created_at`) VALUES ('3', 'nizwa', 'فرع نزوى', 'Nizwa Branch', 'نزوى', 'Nizwa', '2026-04-19 15:16:52');
-- Data for table `categories`
INSERT IGNORE INTO `categories` (`id`, `name_ar`, `name_en`, `description`, `created_at`) VALUES ('1', 'إلكترونيات', 'Electronics', NULL, '2026-04-19 02:39:32');
INSERT IGNORE INTO `categories` (`id`, `name_ar`, `name_en`, `description`, `created_at`) VALUES ('2', 'إكسسوارات', 'Accessories', NULL, '2026-04-19 02:39:32');
INSERT IGNORE INTO `categories` (`id`, `name_ar`, `name_en`, `description`, `created_at`) VALUES ('3', 'ملابس', 'Clothing', NULL, '2026-04-19 02:39:32');
-- Data for table `expense_categories`
INSERT IGNORE INTO `expense_categories` (`id`, `name_ar`, `name_en`, `created_at`) VALUES ('1', 'رواتب', 'Salary', '2026-04-20 02:33:54');
INSERT IGNORE INTO `expense_categories` (`id`, `name_ar`, `name_en`, `created_at`) VALUES ('2', 'كهرباء وماء', 'Water & Electricity ', '2026-04-20 02:34:28');
INSERT IGNORE INTO `expense_categories` (`id`, `name_ar`, `name_en`, `created_at`) VALUES ('3', 'أجور عمال', 'Labour', '2026-04-20 02:34:53');
INSERT IGNORE INTO `expense_categories` (`id`, `name_ar`, `name_en`, `created_at`) VALUES ('4', 'ضرائب', 'Taxes', '2026-04-20 02:35:13');
-- Data for table `settings`
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('company_address', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('company_email', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('company_favicon', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('company_logo', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('company_name_ar', 'حلوى الريامي');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('company_name_en', 'Al Riyami Sweets');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('company_phone', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('company_vat_number', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('mail_from', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('mail_from_name', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_enabled', '1');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_token', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_secret_key', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_template_created', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_template_pending', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_template_accepted', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_template_completed', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('wablas_template_rejected', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('smtp_host', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('smtp_pass', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('smtp_port', '587');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('smtp_secure', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('smtp_user', '');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('timezone', 'Asia/Muscat');
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`) VALUES ('vat_percentage', '5');
-- Data for table `units`
INSERT IGNORE INTO `units` (`id`, `name_ar`, `name_en`, `created_at`) VALUES ('1', 'حبة', 'piece ', '2026-04-19 05:03:16');
INSERT IGNORE INTO `units` (`id`, `name_ar`, `name_en`, `created_at`) VALUES ('2', 'كيلوم جرام', 'Kilogram', '2026-04-19 05:03:44');
INSERT IGNORE INTO `units` (`id`, `name_ar`, `name_en`, `created_at`) VALUES ('3', 'جرام', 'gram', '2026-04-19 05:04:01');
INSERT IGNORE INTO `units` (`id`, `name_ar`, `name_en`, `created_at`) VALUES ('4', 'كرتون', 'Carton', '2026-04-19 05:04:21');
INSERT IGNORE INTO `units` (`id`, `name_ar`, `name_en`, `created_at`) VALUES ('5', 'كيس', 'bag', '2026-04-19 05:04:36');
-- Data for table `users`
INSERT IGNORE INTO `users` (`id`, `username`, `password`, `role`, `permissions`, `branch_code`, `allowed_branches`, `name_ar`, `name_en`, `created_at`) VALUES ('1', 'owner', '$2y$10$QpW18WHHU8wSKQrYdYTRJuaNPw1v1puJgBpvfc6m9H8gnsws5C9/q', 'owner', NULL, 'muscat', NULL, 'مالك النظام', 'System Owner', '2026-04-19 09:23:45');
INSERT IGNORE INTO `users` (`id`, `username`, `password`, `role`, `permissions`, `branch_code`, `allowed_branches`, `name_ar`, `name_en`, `created_at`) VALUES ('2', 'manager_muscat', '$2y$10$mXI290vI7qEIP.At1YFeFOt/pHf096S3h7CWHYfJYluw7QQlM7uDm', 'manager', NULL, 'muscat', NULL, 'مدير فرع مسقط', 'Muscat Branch Manager', '2026-04-19 09:23:45');
INSERT IGNORE INTO `users` (`id`, `username`, `password`, `role`, `permissions`, `branch_code`, `allowed_branches`, `name_ar`, `name_en`, `created_at`) VALUES ('3', 'cashier_sohar', '$2y$10$.iWxSZWkRuWhuNrmt/LAv./HCWlHQJSpoYqa9pJMoobHCWbMpvXZe', 'cashier', NULL, 'sohar', NULL, 'كاشير فرع صحار', 'Sohar Cashier', '2026-04-19 09:23:46');
SET FOREIGN_KEY_CHECKS=1;

40
db/setup.php Normal file
View File

@ -0,0 +1,40 @@
<?php
/**
* Database Setup & Migration Script
*
* Run this file once to set up the database structure and insert default values.
* Access via browser: http://localhost/db/setup.php
* Or via CLI: php db/setup.php
*/
require_once __DIR__ . '/config.php';
$pdo = db();
$sql_file = __DIR__ . '/schema.sql';
if (!file_exists($sql_file)) {
die("Error: schema.sql not found in db directory.");
}
$sql = file_get_contents($sql_file);
try {
// Execute the full SQL file
$pdo->exec($sql);
$message = "Database schema and default data successfully migrated!";
if (php_sapi_name() === 'cli') {
echo $message . "\n";
} else {
echo "<h2 style='color:green; font-family:sans-serif;'>$message</h2>";
echo "<p style='font-family:sans-serif;'><a href='../index.php'>Go to App</a></p>";
}
} catch (PDOException $e) {
$error = "Migration Failed: " . $e->getMessage();
if (php_sapi_name() === 'cli') {
echo $error . "\n";
} else {
echo "<h2 style='color:red; font-family:sans-serif;'>$error</h2>";
}
}

481
debts.php Normal file
View File

@ -0,0 +1,481 @@
<?php
require_once 'includes/app.php';
$user = require_permission('debts', 'show');
$activeNav = 'debts';
$pageTitle = tr('الديون والفواتير الآجلة', 'Debts & Unpaid Bills');
$pdo = db();
$debtsLoadError = '';
$unpaidSales = [];
$debtsByCustomer = [];
$customerNameFilter = trim((string) ($_GET['customer_name'] ?? ''));
$phoneFilter = trim((string) ($_GET['phone'] ?? ''));
$dateFrom = trim((string) ($_GET['date_from'] ?? ''));
$dateTo = trim((string) ($_GET['date_to'] ?? ''));
$hasFilters = $customerNameFilter !== '' || $phoneFilter !== '' || $dateFrom !== '' || $dateTo !== '';
// Handle legacy mark-as-paid shortcut
if (isset($_GET['mark_paid'])) {
$id = (int) $_GET['mark_paid'];
try {
$sale = fetch_sale($id);
if ($sale) {
$summary = sale_payment_summary($sale);
if ($summary['due_amount'] > 0.0005) {
apply_sale_payment($id, $summary['due_amount'], true);
}
}
set_flash('success', tr('تم استلام المبلغ بنجاح.', 'Payment received successfully.'));
} catch (Throwable $e) {
set_flash('danger', tr('خطأ أثناء التحديث.', 'Error updating.'));
}
redirect_to('debts.php');
}
try {
$salesColumns = [];
foreach ($pdo->query('SHOW COLUMNS FROM sales_orders')->fetchAll(PDO::FETCH_ASSOC) as $column) {
$salesColumns[$column['Field']] = true;
}
$hasCustomersTable = false;
$customerColumns = [];
$customersTable = $pdo->query("SHOW TABLES LIKE 'customers'");
if ($customersTable && $customersTable->fetchColumn()) {
$hasCustomersTable = true;
foreach ($pdo->query('SHOW COLUMNS FROM customers')->fetchAll(PDO::FETCH_ASSOC) as $column) {
$customerColumns[$column['Field']] = true;
}
}
$selectParts = [
's.id',
's.receipt_no',
's.sale_date',
's.total_amount',
's.payment_status',
'COALESCE(s.paid_amount, 0) AS paid_amount',
'COALESCE(s.due_amount, s.total_amount) AS due_amount',
isset($salesColumns['customer_id']) ? 's.customer_id' : 'NULL AS customer_id',
isset($salesColumns['customer_name']) ? 's.customer_name' : 'NULL AS customer_name',
isset($salesColumns['status']) ? "COALESCE(s.status, 'completed') AS status" : "'completed' AS status",
'NULL AS c_name',
'NULL AS c_phone',
];
$joinSql = '';
if ($hasCustomersTable && isset($salesColumns['customer_id']) && isset($customerColumns['id'])) {
$joinSql = ' LEFT JOIN customers c ON s.customer_id = c.id ';
if (isset($customerColumns['name'])) {
$selectParts[10] = 'c.name AS c_name';
}
if (isset($customerColumns['phone'])) {
$selectParts[11] = 'c.phone AS c_phone';
}
}
$params = [];
$whereParts = [];
if (isset($salesColumns['payment_status'])) {
$whereParts[] = "s.payment_status IN ('unpaid', 'partial')";
} elseif (isset($salesColumns['payment_method'])) {
$whereParts[] = "s.payment_method = 'pay_later'";
} else {
$whereParts[] = '1 = 0';
}
if ($customerNameFilter !== '') {
$nameConditions = [];
if ($hasCustomersTable && isset($customerColumns['name'])) {
$nameConditions[] = 'c.name LIKE :customer_name_customer';
$params[':customer_name_customer'] = '%' . $customerNameFilter . '%';
}
if (isset($salesColumns['customer_name'])) {
$nameConditions[] = 's.customer_name LIKE :customer_name_sale';
$params[':customer_name_sale'] = '%' . $customerNameFilter . '%';
}
if ($nameConditions !== []) {
$whereParts[] = '(' . implode(' OR ', $nameConditions) . ')';
}
}
if ($phoneFilter !== '') {
$rawDigits = phone_digits($phoneFilter);
$normalizedPhone = normalize_oman_phone($phoneFilter);
$phoneVariants = [];
if ($rawDigits !== '') {
$phoneVariants[] = $rawDigits;
}
if ($normalizedPhone !== '') {
$phoneVariants[] = $normalizedPhone;
$phoneVariants[] = '0' . $normalizedPhone;
$phoneVariants[] = '968' . $normalizedPhone;
$phoneVariants[] = '00968' . $normalizedPhone;
}
$phoneVariants = array_values(array_unique(array_filter($phoneVariants, static fn($value) => $value !== '')));
$phoneColumns = [];
if ($hasCustomersTable && isset($customerColumns['phone'])) {
$phoneColumns[] = "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(c.phone, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '')";
}
if (isset($salesColumns['customer_name'])) {
$phoneColumns[] = "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(s.customer_name, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '')";
}
if ($phoneColumns !== [] && $phoneVariants !== []) {
$phoneConditions = [];
foreach ($phoneColumns as $columnIndex => $columnExpression) {
foreach ($phoneVariants as $variantIndex => $variant) {
$paramKey = ':phone_' . $columnIndex . '_' . $variantIndex;
$phoneConditions[] = $columnExpression . ' LIKE ' . $paramKey;
$params[$paramKey] = '%' . $variant . '%';
}
}
$whereParts[] = '(' . implode(' OR ', $phoneConditions) . ')';
}
}
if ($dateFrom !== '') {
$whereParts[] = 'DATE(s.sale_date) >= :date_from';
$params[':date_from'] = $dateFrom;
}
if ($dateTo !== '') {
$whereParts[] = 'DATE(s.sale_date) <= :date_to';
$params[':date_to'] = $dateTo;
}
$whereSql = ' WHERE ' . implode(' AND ', $whereParts);
$sqlUnpaid = 'SELECT ' . implode(', ', $selectParts)
. ' FROM sales_orders s'
. $joinSql
. $whereSql
. ' ORDER BY s.sale_date DESC';
$stmtUnpaid = $pdo->prepare($sqlUnpaid);
foreach ($params as $key => $value) {
$stmtUnpaid->bindValue($key, $value);
}
$stmtUnpaid->execute();
$unpaidSales = $stmtUnpaid->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
$debtsLoadError = tr(
'تعذر تحميل صفحة الديون بسبب اختلاف في هيكل قاعدة البيانات. احفظ التعديلات ثم أنشئ نسخة جديدة وأعد التحديث.',
'The debts page could not load because the deployed database schema is older than the current code. Save these changes, create a new version, then refresh the page.'
);
}
$extractCustomerContact = static function (array $sale): array {
$sourceName = trim((string) ($sale['customer_name'] ?? ''));
$displayName = trim((string) ($sale['c_name'] ?? ''));
if ($displayName === '') {
$displayName = $sourceName;
}
$displayPhone = trim((string) ($sale['c_phone'] ?? ''));
if ($displayPhone === '' && str_contains($sourceName, ' - ')) {
$parts = explode(' - ', $sourceName);
$lastPart = trim((string) end($parts));
if (preg_match('/^[0-9+\s]+$/', $lastPart)) {
$displayPhone = $lastPart;
array_pop($parts);
if ($displayName === '' || $displayName === $sourceName) {
$displayName = trim(implode(' - ', $parts));
}
}
}
$displayPhone = phone_display($displayPhone);
if ($displayName === '') {
$displayName = tr('عميل غير معروف', 'Unknown Customer');
}
return [
'name' => $displayName,
'phone' => $displayPhone,
];
};
// Aggregate by customer
foreach ($unpaidSales as $sale) {
$customerContact = $extractCustomerContact($sale);
$cId = $sale['customer_id'] ?? 'unknown';
if (!isset($debtsByCustomer[$cId])) {
$debtsByCustomer[$cId] = [
'name' => $customerContact['name'],
'phone' => $customerContact['phone'],
'total' => 0.0,
'open_invoices' => 0,
'partial_invoices' => 0,
];
}
$saleSummary = sale_payment_summary($sale);
$debtsByCustomer[$cId]['total'] += (float) $saleSummary['due_amount'];
$debtsByCustomer[$cId]['open_invoices'] += 1;
if ($saleSummary['payment_status'] === 'partial') {
$debtsByCustomer[$cId]['partial_invoices'] += 1;
}
}
// Sort by highest debt
uasort($debtsByCustomer, fn($a, $b) => $b['total'] <=> $a['total']);
require_once 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0 text-gray-800"><?= h($pageTitle) ?></h1>
</div>
<?php if ($debtsLoadError !== ''): ?>
<div class="alert alert-warning" role="alert">
<?= h($debtsLoadError) ?>
</div>
<?php endif; ?>
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<form method="GET" action="debts.php" class="row g-3 align-items-end">
<input type="hidden" name="lang" value="<?= h(current_lang()) ?>">
<div class="col-12 col-md-6 col-xl-3">
<label class="form-label mb-1" for="debts-customer-name"><?= h(tr('اسم العميل', 'Customer name')) ?></label>
<input
id="debts-customer-name"
type="text"
name="customer_name"
class="form-control"
value="<?= h($customerNameFilter) ?>"
placeholder="<?= h(tr('ابحث باسم العميل', 'Search by customer name')) ?>"
>
</div>
<div class="col-12 col-md-6 col-xl-3">
<label class="form-label mb-1" for="debts-phone"><?= h(tr('رقم الهاتف', 'Phone number')) ?></label>
<input
id="debts-phone"
type="text"
name="phone"
class="form-control"
value="<?= h($phoneFilter) ?>"
placeholder="<?= h(tr('ابحث برقم الهاتف', 'Search by phone number')) ?>"
dir="ltr"
>
</div>
<div class="col-12 col-md-6 col-xl-2">
<label class="form-label mb-1" for="debts-date-from"><?= h(tr('من تاريخ', 'From date')) ?></label>
<input id="debts-date-from" type="date" name="date_from" class="form-control" value="<?= h($dateFrom) ?>">
</div>
<div class="col-12 col-md-6 col-xl-2">
<label class="form-label mb-1" for="debts-date-to"><?= h(tr('إلى تاريخ', 'To date')) ?></label>
<input id="debts-date-to" type="date" name="date_to" class="form-control" value="<?= h($dateTo) ?>">
</div>
<div class="col-12 col-xl-2 d-flex gap-2">
<button type="submit" class="btn btn-dark flex-fill">
<i class="bi bi-funnel me-1"></i><?= h(tr('تطبيق', 'Apply')) ?>
</button>
<a class="btn btn-outline-secondary flex-fill" href="<?= h(url_for('debts.php')) ?>">
<?= h(tr('إعادة ضبط', 'Reset')) ?>
</a>
</div>
<?php if ($hasFilters): ?>
<div class="col-12">
<div class="small text-muted">
<i class="bi bi-info-circle me-1"></i><?= h(tr('يتم الآن عرض الديون المطابقة للفلاتر المحددة فقط.', 'Showing only debts that match the selected filters.')) ?>
</div>
</div>
<?php endif; ?>
</form>
</div>
</div>
<div class="row">
<!-- Debts by Customer -->
<div class="col-lg-3 mb-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
<h6 class="m-0 font-weight-bold text-primary"><i class="bi bi-people"></i> <?= h(tr('الديون حسب العميل', 'Debts by Customer')) ?></h6>
</div>
<div class="card-body">
<?php if (empty($debtsByCustomer)): ?>
<div class="text-center text-muted py-4"><?= h($hasFilters ? tr('لا توجد ديون مطابقة للفلاتر المحددة.', 'No debts match the selected filters.') : tr('لا توجد ديون مسجلة.', 'No debts recorded.')) ?></div>
<?php else: ?>
<ul class="list-group list-group-flush">
<?php foreach ($debtsByCustomer as $debt): ?>
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
<div>
<strong><?= h($debt['name']) ?></strong>
<?php if ($debt['phone']): ?>
<div class="small text-muted" dir="ltr"><?= h($debt['phone']) ?></div>
<?php endif; ?>
<div class="small text-muted"><?= h($debt['open_invoices']) ?> <?= h(tr('فواتير مفتوحة', 'open invoices')) ?></div>
<?php if ($debt['partial_invoices'] > 0): ?>
<div class="small text-warning"><?= h($debt['partial_invoices']) ?> <?= h(tr('منها دفعات جزئية', 'partially paid')) ?></div>
<?php endif; ?>
</div>
<span class="badge bg-danger rounded-pill fs-6"><?= h(currency($debt['total'])) ?></span>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
</div>
</div>
<!-- Unpaid Invoices -->
<div class="col-lg-9 mb-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
<h6 class="m-0 font-weight-bold text-primary"><i class="bi bi-receipt"></i> <?= h(tr('الفواتير غير المدفوعة والجزئية', 'Unpaid & Partial Invoices')) ?></h6>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th><?= h(tr('رقم الفاتورة', 'Receipt No')) ?></th>
<th><?= h(tr('العميل', 'Customer')) ?></th>
<th><?= h(tr('الهاتف', 'Phone')) ?></th>
<th><?= h(tr('التاريخ', 'Date')) ?></th>
<th><?= h(tr('الإجمالي', 'Total')) ?></th>
<th><?= h(tr('المدفوع', 'Paid')) ?></th>
<th><?= h(tr('المتبقي', 'Due')) ?></th>
<th><?= h(tr('الحالة', 'Status')) ?></th>
<th><?= h(tr('الإجراء', 'Action')) ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($unpaidSales)): ?>
<tr>
<td colspan="9" class="text-center py-4 text-muted"><?= h($hasFilters ? tr('لا توجد فواتير مطابقة للفلاتر المحددة.', 'No invoices match the selected filters.') : tr('لا توجد فواتير غير مدفوعة أو جزئية.', 'No unpaid or partial invoices.')) ?></td>
</tr>
<?php else: ?>
<?php foreach ($unpaidSales as $sale): ?>
<?php
$paymentSummary = sale_payment_summary($sale);
$customerContact = $extractCustomerContact($sale);
?>
<tr>
<td>
<a href="<?= h(url_for('sale.php', ['id' => $sale['id']])) ?>" class="fw-bold text-decoration-none">
<?= h($sale['receipt_no']) ?>
</a>
</td>
<td><?= h($customerContact['name']) ?></td>
<td dir="ltr"><?= h($customerContact['phone'] !== '' ? $customerContact['phone'] : '-') ?></td>
<td><?= h(date('Y-m-d', strtotime((string) $sale['sale_date']))) ?></td>
<td class="fw-semibold"><?= h(currency((float) $sale['total_amount'])) ?></td>
<td class="text-primary"><?= h(currency((float) $paymentSummary['paid_amount'])) ?></td>
<td class="fw-bold text-danger"><?= h(currency((float) $paymentSummary['due_amount'])) ?></td>
<td>
<span class="badge <?= h(payment_status_badge_class($paymentSummary['payment_status'])) ?>"><?= h(payment_status_label($paymentSummary['payment_status'])) ?></span>
</td>
<td>
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="receivePayment(<?= (int) $sale['id'] ?>, <?= json_encode((float) $sale['total_amount']) ?>, <?= json_encode((float) $paymentSummary['paid_amount']) ?>, <?= json_encode((float) $paymentSummary['due_amount']) ?>, <?= ($sale['status'] ?? 'completed') === 'order' ? 'true' : 'false' ?>)">
<i class="bi bi-cash-coin"></i> <?= h(tr('استلام دفعة', 'Receive Payment')) ?>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
function formatPaymentPopupAmount(value) {
return Number(value || 0).toFixed(3);
}
async function receivePayment(id, totalAmount, paidAmount, dueAmount, completeOrder = false) {
const popupHtml = `
<div class="text-start">
<div class="row g-2 mb-3">
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('إجمالي الفاتورة', 'Total amount')) ?></div>
<div class="fw-bold text-dark">${formatPaymentPopupAmount(totalAmount)}</div>
</div>
</div>
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('المدفوع سابقاً', 'Already paid')) ?></div>
<div class="fw-bold text-primary">${formatPaymentPopupAmount(paidAmount)}</div>
</div>
</div>
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('المتبقي الحالي', 'Current remaining')) ?></div>
<div class="fw-bold text-danger">${formatPaymentPopupAmount(dueAmount)}</div>
</div>
</div>
</div>
<label for="swal-payment-amount" class="form-label fw-semibold mb-2"><?= h(tr('المبلغ المطلوب دفعه الآن', 'Amount to pay now')) ?></label>
<input id="swal-payment-amount" type="number" class="swal2-input mt-0" min="0.001" step="0.001" max="${formatPaymentPopupAmount(dueAmount)}" value="${formatPaymentPopupAmount(dueAmount)}">
<div class="d-flex justify-content-between align-items-center rounded-3 border px-3 py-2 bg-light mt-3">
<span class="small text-muted"><?= h(tr('المتبقي بعد الدفعة', 'Remaining after payment')) ?></span>
<strong id="swal-payment-remaining" class="text-success">0.000</strong>
</div>
</div>`;
const { isConfirmed, value: paymentAmount } = await Swal.fire({
title: '<?= h(tr('استلام دفعة', 'Receive Payment')) ?>',
html: popupHtml,
showCancelButton: true,
confirmButtonColor: '#198754',
confirmButtonText: '<?= h(tr('حفظ الدفعة', 'Save Payment')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>',
focusConfirm: false,
didOpen: () => {
const input = document.getElementById('swal-payment-amount');
const remainingEl = document.getElementById('swal-payment-remaining');
const updateRemaining = () => {
const amount = parseFloat(input.value || '0');
const safeAmount = Number.isFinite(amount) ? amount : 0;
const remaining = Math.max(dueAmount - safeAmount, 0);
remainingEl.textContent = formatPaymentPopupAmount(remaining);
remainingEl.className = remaining > 0.0005 ? 'text-danger' : 'text-success';
};
input.addEventListener('input', updateRemaining);
input.focus();
input.select();
updateRemaining();
},
preConfirm: () => {
const input = document.getElementById('swal-payment-amount');
const amount = parseFloat(input.value || '0');
if (!amount || amount <= 0) {
Swal.showValidationMessage('<?= h(tr('أدخل مبلغاً صحيحاً.', 'Enter a valid amount.')) ?>');
return false;
}
if (amount - dueAmount > 0.0005) {
Swal.showValidationMessage('<?= h(tr('المبلغ لا يمكن أن يتجاوز المتبقي.', 'Amount cannot exceed the due balance.')) ?>');
return false;
}
return formatPaymentPopupAmount(amount);
}
});
if (!isConfirmed || !paymentAmount) {
return;
}
const formData = new FormData();
formData.append('sale_id', String(id));
formData.append('payment_amount', String(paymentAmount));
if (completeOrder) {
formData.append('complete_order', '1');
}
const response = await fetch('api/sales_payment.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.success) {
await Swal.fire({ icon: 'success', text: data.message, confirmButtonText: 'OK' });
window.location.reload();
} else {
Swal.fire({ icon: 'error', text: data.error || '<?= h(tr('تعذر تسجيل الدفعة.', 'Could not record the payment.')) ?>' });
}
}
</script>
<?php require_once 'includes/footer.php'; ?>

16
dump_schema.php Normal file
View File

@ -0,0 +1,16 @@
<?php
require_once 'db/config.php';
$pdo = db();
$stmt = $pdo->query("SHOW TABLES");
$tables = $stmt->fetchAll(PDO::FETCH_COLUMN);
$sql = "SET FOREIGN_KEY_CHECKS=0;
";
foreach ($tables as $table) {
$createStmt = $pdo->query("SHOW CREATE TABLE `$table`")->fetch(PDO::FETCH_ASSOC);
$sql .= "-- Table structure for `$table`\n";
$sql .= $createStmt['Create Table'] . ";\n\n";
}
$sql .= "SET FOREIGN_KEY_CHECKS=1;\n";
echo $sql;

616
edit_online_order.php Normal file
View File

@ -0,0 +1,616 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('online_orders', 'edit');
$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'] ?? ''));
$customerPhoneInput = trim((string) ($_POST['customer_phone'] ?? ''));
$customerPhone = normalize_oman_phone($customerPhoneInput);
$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 === '' || $customerPhoneInput === '' || $customerAddress === '') {
$error = tr('الرجاء تعبئة بيانات العميل الأساسية.', 'Please fill the main customer details.');
} elseif ($customerPhone === '') {
$error = tr('رقم الهاتف يجب أن يكون عمانياً من 8 خانات.', 'Phone must be an 8-digit Oman number.');
} 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 {
$oldStatus = (string) ($editOrder['status'] ?? 'pending');
$statusChanged = $oldStatus !== $saleStatus;
$previousItems = json_decode((string) ($editOrder['items_json'] ?? '[]'), true) ?: [];
db()->beginTransaction();
try {
sync_online_order_stock_reservation($previousItems, $oldStatus, $normalized, $saleStatus);
$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,
]);
db()->commit();
} catch (Throwable $e) {
if (db()->inTransaction()) {
db()->rollBack();
}
throw $e;
}
if ($statusChanged && wablas_is_configured()) {
wablas_notify_online_order([
'id' => $editOrderId,
'customer_name' => $customerName,
'customer_phone' => $customerPhone,
'customer_address' => $customerAddress,
'items' => $normalized,
'subtotal' => $subtotal,
'vat_amount' => $totalVat,
'total_amount' => $subtotal + $totalVat,
'status' => $saleStatus,
'created_at' => $editOrder['created_at'] ?? date('Y-m-d H:i:s'),
], $saleStatus);
}
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>
<div class="input-group" dir="ltr">
<span class="input-group-text">968</span>
<input type="text" name="customer_phone" class="form-control custom-input" required inputmode="numeric" maxlength="8" pattern="\d{8}" placeholder="91234567" value="<?= h(normalize_oman_phone((string) ($editOrder['customer_phone'] ?? ''))) ?>">
</div>
</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>
<div class="border rounded-3 p-3 bg-light mb-4">
<div class="small text-muted mb-2"><?= h(tr('الدفع', 'Payment')) ?></div>
<div class="fw-semibold"><?= h(online_payment_method_label((string) ($editOrder['payment_method'] ?? 'pay_later'))) ?></div>
<div class="mt-2"><span class="badge <?= h(online_payment_status_badge_class((string) ($editOrder['payment_status'] ?? 'unpaid'))) ?>"><?= h(online_payment_status_label((string) ($editOrder['payment_status'] ?? 'unpaid'))) ?></span></div>
<?php if (!empty($editOrder['gateway_session_id'])): ?>
<div class="small text-muted mt-2"><?= h(tr('جلسة ثواني:', 'Thawani session:')) ?> <?= h($editOrder['gateway_session_id']) ?></div>
<?php endif; ?>
</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();
Swal.fire({icon: 'warning', text: '<?= h(tr('الرجاء إضافة أصناف للفاتورة أولاً.', 'Please add items to the invoice first.')) ?>'});
}
});
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>

945
edit_sale.php Normal file
View File

@ -0,0 +1,945 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('sales', 'edit');
$editSaleId = (int)($_GET['id'] ?? 0);
$editSale = null;
if ($editSaleId > 0) {
$stmt = db()->prepare('SELECT * FROM sales_orders WHERE id = :id');
$stmt->execute([':id' => $editSaleId]);
$editSale = $stmt->fetch();
}
if ($editSale) {
$editSale['items'] = json_decode((string) ($editSale['items_json'] ?? '[]'), true) ?: [];
}
if (!$editSale) {
die(tr('الفاتورة غير موجودة.', 'Invoice not found.'));
}
if ($user['role'] !== 'owner' && $editSale['branch_code'] !== $user['branch_code']) {
die(tr('غير مصرح لك.', 'Unauthorized.'));
}
$pageTitle = tr('تعديل فاتورة', 'Edit Invoice') . ' #' . h($editSale['receipt_no']);
$isEidSale = (($editSale['order_type'] ?? 'standard') === 'eid');
$activeNav = $isEidSale ? 'eid_orders' : 'sales';
$error = '';
$editPaymentSummary = sale_payment_summary($editSale);
$paymentAmountInput = (string) ($_POST['payment_amount'] ?? number_format((float) $editPaymentSummary['paid_amount'], 3, '.', ''));
$catalog = catalog();
$allowedBranches = get_user_branches($user);
$deliveryOptions = eid_delivery_status_options();
$deliveryStatusInput = trim((string) ($_POST['delivery_status'] ?? ($editSale['delivery_status'] ?? ($isEidSale ? 'pending' : ''))));
$deliveryDateInput = trim((string) ($_POST['delivery_date'] ?? ($editSale['delivery_date'] ?? '')));
$notesInput = trim((string) ($_POST['notes'] ?? ($editSale['notes'] ?? '')));
$saleStatusInput = trim((string) ($_POST['sale_status'] ?? ($editSale['status'] ?? 'completed')));
$itemNotePlaceholder = $isEidSale
? tr('مثال: بدون سكر، تغليف هدية، لون معين، كتابة اسم...', 'Example: no sugar, gift wrap, specific color, write a name...')
: tr('ملاحظة داخلية لهذا الصنف...', 'Internal note for this item...');
$itemNoteHelper = $isEidSale
? tr('ملاحظة داخلية لهذا الصنف فقط — لا تُطبع في الإيصال.', 'Internal for this item only — not printed on the receipt.')
: tr('ملاحظة محفوظة لهذا الصنف داخل النظام.', 'This note is saved for this item inside the system.');
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'] ?? ''));
$customerId = isset($_POST['customer_id']) && $_POST['customer_id'] !== '' ? (int)$_POST['customer_id'] : null;
$customerName = trim((string) ($_POST['customer_name'] ?? ''));
$paymentMethod = trim((string) ($_POST['payment_method'] ?? 'cash'));
$paymentAmountInput = trim((string) ($_POST['payment_amount'] ?? ''));
$paymentAmountForCalculation = $paymentAmountInput === '' ? '0' : $paymentAmountInput;
$saleStatus = trim((string) ($_POST['sale_status'] ?? 'completed'));
$saleStatusInput = $saleStatus;
$notes = trim((string) ($_POST['notes'] ?? ''));
$notesInput = $notes;
$deliveryStatus = trim((string) ($_POST['delivery_status'] ?? ($editSale['delivery_status'] ?? ($isEidSale ? 'pending' : ''))));
$deliveryStatusInput = $deliveryStatus;
$deliveryDate = trim((string) ($_POST['delivery_date'] ?? ($editSale['delivery_date'] ?? '')));
$deliveryDateInput = $deliveryDate;
$cartJson = (string) ($_POST['cart_json'] ?? '[]');
$items = json_decode($cartJson, true);
if (!in_array($branchCode, $allowedBranches, true)) {
$error = tr('اختر فرعاً صالحاً لهذه الصلاحية.', 'Choose a valid branch for this role.');
} elseif (!in_array($paymentMethod, ['cash', 'card', 'transfer', 'pay_later'], true)) {
$error = tr('اختر طريقة دفع صحيحة.', 'Choose a valid payment method.');
} elseif ($isEidSale && !isset($deliveryOptions[$deliveryStatus])) {
$error = tr('اختر حالة تجهيز صحيحة لطلب العيد.', 'Choose a valid prep status for the Eid order.');
} elseif ($isEidSale && ($deliveryDate === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $deliveryDate))) {
$error = tr('حدد تاريخ تسليم صحيح لطلب العيد.', 'Choose a valid delivery date for the Eid order.');
} elseif (!is_array($items) || $items === []) {
$error = tr('أضف صنفاً واحداً على الأقل إلى الفاتورة.', 'Add at least one item to the invoice.');
} else {
$normalized = [];
$subtotal = 0.0;
$totalVat = 0.0;
$itemCount = 0;
foreach ($items as $item) {
$sku = (string) ($item['sku'] ?? '');
$qty = (int) ($item['qty'] ?? 0);
if (!isset($catalog[$sku]) || $qty < 1) {
continue;
}
$product = $catalog[$sku];
$itemNote = trim((string) ($item['item_note'] ?? ''));
$price = (float) $product['price'];
$lineTotal = $price * $qty;
$vatPercent = (float) ($product['vat'] ?? 0);
$itemVat = $lineTotal * ($vatPercent / 100);
$totalVat += $itemVat;
$normalized[] = [
'sku' => $sku,
'name_ar' => $product['name_ar'],
'name_en' => $product['name_en'],
'qty' => $qty,
'price' => $price,
'item_note' => $itemNote,
'line_total' => $lineTotal,
'vat_percent' => $vatPercent,
'vat_amount' => $itemVat
];
$subtotal += $lineTotal;
$itemCount += $qty;
}
if ($normalized === []) {
$error = tr('الفاتورة غير صالحة بعد التحقق من الأصناف.', 'The invoice is invalid after product validation.');
} else {
$totalAmount = $subtotal + $totalVat;
if ($paymentAmountInput !== '' && !is_numeric($paymentAmountInput)) {
$error = tr('أدخل مبلغاً مدفوعاً صحيحاً.', 'Enter a valid paid amount.');
} else {
$paymentMeta = sale_payment_breakdown($totalAmount, $paymentMethod, $paymentAmountForCalculation);
if ($paymentMeta['due_amount'] > 0.0005 && !$customerId) {
$error = tr('يجب اختيار عميل مسجل عند وجود مبلغ متبقٍ أو دفعة جزئية.', 'Select a registered customer when there is a remaining balance or partial payment.');
}
}
}
if ($error === '') {
$cashierName = current_lang() === 'ar' ? $user['name_ar'] : $user['name_en'];
db()->beginTransaction();
try {
sync_order_stock_reservation(
$editSale['items'] ?? [],
(string) ($editSale['status'] ?? 'completed'),
$normalized,
$saleStatus
);
$stmt = db()->prepare('UPDATE sales_orders SET
branch_code = :branch_code,
customer_id = :customer_id,
customer_name = :customer_name,
payment_method = :payment_method,
payment_status = :payment_status,
paid_amount = :paid_amount,
due_amount = :due_amount,
items_json = :items_json,
item_count = :item_count,
subtotal = :subtotal,
vat_amount = :vat_amount,
total_amount = :total_amount,
status = :status,
delivery_status = :delivery_status,
delivery_date = :delivery_date,
notes = :notes
WHERE id = :id');
$stmt->execute([
':branch_code' => $branchCode,
':customer_id' => $customerId,
':customer_name' => $customerName !== '' ? $customerName : null,
':payment_method' => $paymentMethod,
':payment_status' => $paymentMeta['payment_status'],
':paid_amount' => $paymentMeta['paid_amount'],
':due_amount' => $paymentMeta['due_amount'],
':items_json' => json_encode($normalized, JSON_UNESCAPED_UNICODE),
':item_count' => $itemCount,
':subtotal' => $subtotal,
':vat_amount' => $totalVat,
':total_amount' => $totalAmount,
':status' => $saleStatus,
':delivery_status' => $isEidSale ? $deliveryStatus : ($editSale['delivery_status'] ?? 'pending'),
':delivery_date' => $isEidSale && $deliveryDate !== '' ? $deliveryDate : ($isEidSale ? null : ($editSale['delivery_date'] ?? null)),
':notes' => $notes !== '' ? $notes : null,
':id' => $editSaleId,
]);
db()->commit();
} catch (Throwable $e) {
if (db()->inTransaction()) {
db()->rollBack();
}
$error = tr('تعذر تحديث الفاتورة.', 'Could not update the invoice.');
}
if ($error === '') {
$flashType = 'success';
$flashMessage = tr('تم تحديث الفاتورة بنجاح.', 'Invoice updated successfully.');
if ($isEidSale && wablas_is_configured()) {
$wablasResult = wablas_notify_sale_invoice($editSaleId);
if (!empty($wablasResult['success'])) {
$flashMessage = tr('تم تحديث الفاتورة وإعادة إرسالها عبر واتساب بنجاح.', 'Invoice updated and resent via WhatsApp successfully.');
} else {
$flashType = 'warning';
$flashMessage = tr('تم تحديث الفاتورة، لكن تعذر إعادة إرسالها عبر واتساب. تحقق من رقم واتساب العميل أو إعدادات واتساب.', 'Invoice updated, but resending via WhatsApp failed. Check the customer WhatsApp number or WhatsApp settings.');
}
}
set_flash($flashType, $flashMessage);
redirect_to($isEidSale ? 'eid_orders.php' : 'sales.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="<?= h($isEidSale ? url_for('eid_orders.php') : url_for('sales.php')) ?>" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> <?= h($isEidSale ? tr('عودة لطلبات العيد', 'Back to Eid Orders') : 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 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) ?>" <?= $branchCode === $editSale['branch_code'] ? 'selected' : '' ?>><?= 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="hidden" id="formCustomerId" name="customer_id" value="<?= h($editSale['customer_id'] ?? '' ) ?>">
<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" value="<?= h($editSale['customer_name'] ?? '' ) ?>">
<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">
<?php if ($isEidSale): ?>
<option value="order" <?= $saleStatusInput === 'order' ? 'selected' : '' ?>><?= h(tr('طلب عيد قيد التجهيز', 'Eid order in preparation')) ?></option>
<option value="completed" <?= $saleStatusInput === 'completed' ? 'selected' : '' ?>><?= h(tr('تم التسليم / مكتمل', 'Delivered / Completed')) ?></option>
<?php else: ?>
<option value="completed" <?= $saleStatusInput === 'completed' ? 'selected' : '' ?>><?= h(tr('فاتورة بيع (تم الدفع)', 'Sale Bill (Paid)')) ?></option>
<option value="order" <?= $saleStatusInput === 'order' ? 'selected' : '' ?>><?= h(tr('طلب مسبق (دفع لاحق)', 'Order (Pay Later)')) ?></option>
<?php endif; ?>
</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" <?= ($paymentMethod ?? $editSale['payment_method']) === 'cash' ? 'selected' : '' ?>><?= h(tr('نقداً', 'Cash')) ?></option>
<option value="card" <?= ($paymentMethod ?? $editSale['payment_method']) === 'card' ? 'selected' : '' ?>><?= h(tr('بطاقة ائتمان', 'Credit Card')) ?></option>
<option value="transfer" <?= ($paymentMethod ?? $editSale['payment_method']) === 'transfer' ? 'selected' : '' ?>><?= h(tr('تحويل بنكي', 'Bank Transfer')) ?></option>
<option value="pay_later" <?= ($paymentMethod ?? $editSale['payment_method']) === 'pay_later' ? 'selected' : '' ?>><?= h(tr('آجل (Pay Later)', 'Pay Later')) ?></option>
</select>
</div>
<?php if ($isEidSale): ?>
<div class="mb-3">
<label class="form-label" for="delivery_date"><?= h(tr('تاريخ التسليم', 'Delivery date')) ?></label>
<input type="date" class="form-control custom-input" id="delivery_date" name="delivery_date" value="<?= h($deliveryDateInput) ?>" required>
</div>
<div class="mb-3">
<label class="form-label" for="delivery_status"><?= h(tr('حالة التجهيز', 'Prep status')) ?></label>
<select class="form-select custom-input" id="delivery_status" name="delivery_status">
<?php foreach ($deliveryOptions as $value => $label): ?>
<option value="<?= h($value) ?>" <?= $deliveryStatusInput === $value ? 'selected' : '' ?>><?= h($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="mb-3">
<label class="form-label" for="payment_amount"><?= h(tr('المبلغ المدفوع الآن', 'Paid Now')) ?></label>
<input type="number" class="form-control custom-input" id="payment_amount" name="payment_amount" min="0" step="0.001" value="<?= h($paymentAmountInput) ?>" placeholder="0.000">
<div class="row g-2 mt-2" aria-live="polite">
<div class="col-6">
<div class="border rounded-3 px-3 py-2 bg-light-subtle h-100">
<div class="small text-muted mb-1"><?= h(tr('المدفوع بعد الحفظ', 'Paid after save')) ?></div>
<div class="fw-semibold text-success" id="displayPaidAmountLive">0.000 <?= h(tr('ر.ع', 'OMR')) ?></div>
</div>
</div>
<div class="col-6">
<div class="border rounded-3 px-3 py-2 bg-light-subtle h-100">
<div class="small text-muted mb-1"><?= h(tr('المتبقي بعد الحفظ', 'Remaining after save')) ?></div>
<div class="fw-semibold text-primary" id="displayDueAmountLive">0.000 <?= h(tr('ر.ع', 'OMR')) ?></div>
</div>
</div>
</div>
<div class="form-text" id="paymentAmountHint"><?= h(tr('يمكنك تعديل المبلغ المدفوع يدوياً وسيتم إعادة احتساب المتبقي تلقائياً.', 'You can edit the paid amount manually and the remaining balance will be recalculated automatically.')) ?></div>
</div>
<div class="mb-4">
<label class="form-label"><?= h(tr('ملاحظات (اختياري)', 'Notes (Optional)')) ?></label>
<textarea class="form-control custom-input" name="notes" rows="2" placeholder="<?= h(tr('أي ملاحظات إضافية...', 'Any additional notes...')) ?>"><?= h($notesInput) ?></textarea>
</div>
<!-- Summary -->
<div class="totals-box mb-4">
<div class="totals-row">
<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 Invoice')) ?>
</button>
</div>
</div>
</div>
</div>
</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="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>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
const catalogData = <?= json_encode($catalog, JSON_UNESCAPED_UNICODE) ?>;
const catalogArray = Object.values(catalogData);
const partialPaymentText = '<?= h(tr('متبقٍ بعد الحفظ:', 'Due after save:')) ?>';
const showItemNotes = <?= $isEidSale ? 'true' : 'false' ?>;
const itemNotePlaceholder = <?= json_encode($itemNotePlaceholder, JSON_UNESCAPED_UNICODE) ?>;
const itemNoteHelper = <?= json_encode($itemNoteHelper, JSON_UNESCAPED_UNICODE) ?>;
let invoiceItems = {};
let currentInvoiceTotal = 0;
// Prepopulate from editSale
const initialItemsJson = <?= empty($editSale['items_json']) ? '[]' : $editSale['items_json'] ?>;
initialItemsJson.forEach(item => {
invoiceItems[item.sku] = {
sku: item.sku,
name: '<?= current_lang() ?>' === 'ar' ? item.name_ar : item.name_en,
price: parseFloat(item.price),
qty: parseInt(item.qty),
item_note: item.item_note || ''
};
});
// renderInvoice();
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) {
Swal.fire({icon: 'warning', text: '<?= 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 : '');
document.getElementById('formCustomerId').value = data.customer.id;
newCustomerModalObj.hide();
Swal.fire({
icon: 'success',
text: '<?= h(tr('تم إضافة العميل', 'Customer added')) ?>',
position: 'center',
confirmButtonText: '<?= h(tr('حسناً', 'OK')) ?>',
confirmButtonColor: '#0d6efd'
});
} else {
Swal.fire({icon: 'warning', text: data.error});
}
} catch(err) {
Swal.fire({icon: 'warning', text: '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,
item_note: ''
};
}
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 changeItemNoteValue(sku, newNote) {
if (!invoiceItems[sku]) {
return;
}
invoiceItems[sku].item_note = String(newNote ?? '');
cartJson.value = JSON.stringify(Object.keys(invoiceItems).map(currentSku => {
const currentItem = invoiceItems[currentSku];
return {
sku: currentItem.sku,
qty: currentItem.qty,
item_note: String(currentItem.item_note ?? '')
};
}));
}
function escapeHtml(value) {
return String(value ?? '').replace(/[&<>"']/g, function(char) {
return ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'})[char] || char;
});
}
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 = parseFloat(catalogData[sku].vat) || 0;
const itemVat = lineTotal * (vatPercent / 100);
totalVat += itemVat;
totalAmount += lineTotal;
cartData.push({ sku: item.sku, qty: item.qty, item_note: String(item.item_note || '') });
const safeName = escapeHtml(item.name);
const safeSku = escapeHtml(item.sku);
const safeItemNote = escapeHtml(item.item_note || '');
const itemNoteHtml = showItemNotes ? `
<input type="text" class="form-control form-control-sm line-input mt-2" value="${safeItemNote}" oninput="changeItemNoteValue('${sku}', this.value)" placeholder="${escapeHtml(itemNotePlaceholder)}">
<div class="text-muted small mt-1">${escapeHtml(itemNoteHelper)}</div>
` : '';
const tr = document.createElement('tr');
tr.innerHTML = `
<td>
<div class="fw-medium text-dark">${safeName}</div>
${itemNoteHtml}
<div class="text-muted small mt-2">SKU: ${safeSku}</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 formatMoney(value) {
return value.toFixed(3) + currencySuffix;
}
function updatePaymentAmountHint() {
const paymentAmountField = document.getElementById('payment_amount');
const paymentAmountHint = document.getElementById('paymentAmountHint');
const displayPaidAmountLive = document.getElementById('displayPaidAmountLive');
const displayDueAmountLive = document.getElementById('displayDueAmountLive');
if (!paymentAmountField || !paymentAmountHint) {
return;
}
const entered = Math.max(0, parseFloat(paymentAmountField.value || '0') || 0);
const paid = Math.min(entered, currentInvoiceTotal);
const due = Math.max(0, currentInvoiceTotal - paid);
paymentAmountHint.innerText = partialPaymentText + ' ' + formatMoney(due);
if (displayPaidAmountLive) {
displayPaidAmountLive.innerText = formatMoney(paid);
}
if (displayDueAmountLive) {
displayDueAmountLive.innerText = formatMoney(due);
}
}
function syncPaymentAmount(force = false) {
const paymentMethodField = document.querySelector('select[name="payment_method"]');
const paymentAmountField = document.getElementById('payment_amount');
if (!paymentMethodField || !paymentAmountField) {
return;
}
const isManual = paymentAmountField.dataset.manual === '1';
if (force || !isManual) {
const defaultAmount = paymentMethodField.value === 'pay_later' ? 0 : currentInvoiceTotal;
paymentAmountField.value = defaultAmount.toFixed(3);
paymentAmountField.dataset.manual = '0';
}
updatePaymentAmountHint();
}
function updateTotals(total, vat) {
const subtotal = total;
const finalTotal = subtotal + vat;
currentInvoiceTotal = finalTotal;
document.getElementById('displaySubtotal').innerText = subtotal.toFixed(3);
document.getElementById('displayVat').innerText = vat.toFixed(3);
document.getElementById('displayTotal').innerText = finalTotal.toFixed(3) + currencySuffix;
updatePaymentAmountHint();
}
const paymentMethodField = document.querySelector('select[name="payment_method"]');
const paymentAmountField = document.getElementById('payment_amount');
if (paymentMethodField && paymentAmountField) {
paymentAmountField.dataset.manual = paymentAmountField.value !== '' ? '1' : '0';
paymentMethodField.addEventListener('change', () => syncPaymentAmount());
paymentAmountField.addEventListener('input', () => {
paymentAmountField.dataset.manual = '1';
updatePaymentAmountHint();
});
}
renderInvoice();
// Intercept form submission to check if items exist
document.getElementById('smart-sale-form').addEventListener('submit', function(e) {
const customerId = document.getElementById('formCustomerId').value;
const paymentAmount = Math.max(0, parseFloat(document.getElementById('payment_amount').value || '0') || 0);
if (Object.keys(invoiceItems).length === 0) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '<?= h(tr('الرجاء إضافة أصناف للفاتورة أولاً.', 'Please add items to the invoice first.')) ?>'});
} else if (<?= $isEidSale ? 'true' : 'false' ?> && (!document.getElementById('delivery_date') || !document.getElementById('delivery_date').value)) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '<?= h(tr('حدد تاريخ التسليم لطلب العيد.', 'Please choose a delivery date for the Eid order.')) ?>'});
} else if (paymentAmount > currentInvoiceTotal + 0.0005) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '<?= h(tr('المبلغ المدفوع لا يمكن أن يتجاوز إجمالي الفاتورة.', 'Paid amount cannot exceed the invoice total.')) ?>'});
} else if (paymentAmount < currentInvoiceTotal && !customerId) {
e.preventDefault();
Swal.fire({icon: 'warning', text: '<?= h(tr('يجب اختيار عميل مسجل عند وجود مبلغ متبقٍ.', 'Select a registered customer when there is a remaining balance.')) ?>'});
}
});
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>

677
eid_orders.php Normal file
View File

@ -0,0 +1,677 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('eid_orders', 'show');
ensure_sales_table();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'mark_as_paid') {
$id = (int)$_POST['id'];
try {
$sale = fetch_sale($id);
if ($sale && $sale['order_type'] === 'eid') {
$summary = sale_payment_summary($sale);
if ($summary['due_amount'] > 0.0005) {
apply_sale_payment($id, $summary['due_amount'], false);
set_flash('success', tr('تم تحويل حالة الدفع إلى مدفوع بنجاح.', 'Payment status updated to paid successfully.'));
} else {
set_flash('info', tr('الفاتورة مدفوعة مسبقاً.', 'Invoice is already paid.'));
}
}
} catch (Exception $e) {
set_flash('danger', tr('تعذر تحديث حالة الدفع.', 'Failed to update payment status.'));
}
redirect_to('eid_orders.php', $_GET);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'update_delivery_status') {
$id = (int)$_POST['id'];
$newStatus = trim((string)($_POST['status'] ?? 'pending'));
$allowedStatuses = array_keys(eid_delivery_status_options());
if (in_array($newStatus, $allowedStatuses, true)) {
$stmt = db()->prepare("UPDATE sales_orders SET delivery_status = :status WHERE id = :id AND order_type = 'eid'");
$stmt->execute([':status' => $newStatus, ':id' => $id]);
set_flash('success', tr('تم تحديث حالة التجهيز بنجاح.', 'Prep status updated successfully.'));
}
redirect_to('eid_orders.php', $_GET);
}
$canDeleteEidOrders = $user['role'] === 'owner' || has_permission('sales', 'del');
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete_eid_order') {
if (!$canDeleteEidOrders) {
set_flash('danger', tr('ليس لديك صلاحية حذف الفواتير.', 'You do not have permission to delete invoices.'));
redirect_to('eid_orders.php', $_GET);
}
$id = (int)($_POST['id'] ?? 0);
try {
$sale = fetch_sale($id);
if (!$sale || ($sale['order_type'] ?? '') !== 'eid') {
set_flash('warning', tr('فاتورة العيد غير موجودة.', 'Eid invoice was not found.'));
} else {
$stmt = db()->prepare("DELETE FROM sales_orders WHERE id = :id AND order_type = 'eid'");
$stmt->execute([':id' => $id]);
set_flash('success', tr('تم حذف فاتورة العيد بنجاح.', 'Eid invoice deleted successfully.'));
}
} catch (Throwable $e) {
set_flash('danger', tr('تعذر حذف فاتورة العيد.', 'Failed to delete Eid invoice.'));
}
redirect_to('eid_orders.php', $_GET);
}
$activeNav = 'eid_orders';
$pageTitle = tr('طلبات العيد', 'Eid Orders');
$metaDescription = tr('متابعة طلبات العيد مع الفلاتر والمدى الزمني.', 'Track Eid orders with filters and date range.');
$mode = isset($_GET['mode']) && in_array($_GET['mode'], ['pos', 'normal'], true) ? $_GET['mode'] : null;
$branch = isset($_GET['branch']) && array_key_exists($_GET['branch'], branches()) ? $_GET['branch'] : null;
$search = trim((string) ($_GET['q'] ?? ''));
$paymentStatus = trim((string) ($_GET['payment_status'] ?? ''));
$deliveryStatus = trim((string) ($_GET['delivery_status'] ?? ''));
$dateFrom = trim((string) ($_GET['date_from'] ?? ''));
$dateTo = trim((string) ($_GET['date_to'] ?? ''));
$sort = trim((string) ($_GET['sort'] ?? 'delivery_date'));
$dir = strtolower(trim((string) ($_GET['dir'] ?? 'asc')));
$sortMap = [
'receipt_no' => 'receipt_no',
'customer' => 'customer_name',
'branch' => 'branch_code',
'delivery_date' => 'COALESCE(delivery_date, DATE(sale_date))',
'delivery_status' => 'delivery_status',
'item_count' => 'item_count',
'total_amount' => 'total_amount',
];
if (!isset($sortMap[$sort])) {
$sort = 'delivery_date';
}
if (!in_array($dir, ['asc', 'desc'], true)) {
$dir = 'asc';
}
$page = max(1, (int) ($_GET['p'] ?? 1));
$limit = 50;
$offset = ($page - 1) * $limit;
$allowedBranches = $user && $user['role'] !== 'owner' ? get_user_branches($user) : [];
$deliveryOptions = eid_delivery_status_options();
$dbError = null;
$totalPages = 1;
$orders = [];
$summary = [
'total_orders' => 0,
'total_items' => 0,
'total_amount' => 0,
'prep_orders' => 0,
];
try {
$params = [':order_type' => 'eid'];
$where = " WHERE order_type = :order_type AND NOT (delivery_status = 'delivered' AND COALESCE(due_amount, 0) <= 0.0005) ";
if ($mode) {
$where .= ' AND sale_mode = :sale_mode ';
$params[':sale_mode'] = $mode;
}
if ($branch) {
$where .= ' AND branch_code = :branch_code ';
$params[':branch_code'] = $branch;
}
if ($user && $user['role'] !== 'owner') {
if ($allowedBranches === []) {
$where .= ' AND 1=0 ';
} else {
$namedParams = [];
foreach ($allowedBranches as $i => $allowedBranch) {
$key = ':v_branch_' . $i;
$namedParams[] = $key;
$params[$key] = $allowedBranch;
}
$where .= ' AND branch_code IN (' . implode(', ', $namedParams) . ') ';
}
}
if ($search !== '') {
$where .= ' AND (receipt_no LIKE :search OR customer_name LIKE :search OR cashier_name LIKE :search OR notes LIKE :search) ';
$params[':search'] = '%' . $search . '%';
}
if (in_array($paymentStatus, ['paid', 'partial', 'unpaid'], true)) {
$where .= ' AND payment_status = :payment_status ';
$params[':payment_status'] = $paymentStatus;
}
if (isset($deliveryOptions[$deliveryStatus])) {
$where .= ' AND delivery_status = :delivery_status ';
$params[':delivery_status'] = $deliveryStatus;
}
if ($dateFrom !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom)) {
$where .= ' AND DATE(COALESCE(delivery_date, sale_date)) >= :date_from ';
$params[':date_from'] = $dateFrom;
} else {
$dateFrom = '';
}
if ($dateTo !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) {
$where .= ' AND DATE(COALESCE(delivery_date, sale_date)) <= :date_to ';
$params[':date_to'] = $dateTo;
} else {
$dateTo = '';
}
$summarySql = "SELECT COUNT(*) AS total_orders, COALESCE(SUM(item_count), 0) AS total_items, COALESCE(SUM(total_amount), 0) AS total_amount, COALESCE(SUM(CASE WHEN delivery_status IN ('pending', 'preparing') THEN 1 ELSE 0 END), 0) AS prep_orders FROM sales_orders" . $where;
$summaryStmt = db()->prepare($summarySql);
foreach ($params as $key => $value) {
$summaryStmt->bindValue($key, $value);
}
$summaryStmt->execute();
$summary = $summaryStmt->fetch() ?: $summary;
$countSql = 'SELECT COUNT(*) FROM sales_orders' . $where;
$countStmt = db()->prepare($countSql);
foreach ($params as $key => $value) {
$countStmt->bindValue($key, $value);
}
$countStmt->execute();
$total = (int) $countStmt->fetchColumn();
$totalPages = max(1, (int) ceil($total / $limit));
$sortDirection = strtoupper($dir);
$primarySort = $sortMap[$sort] . ' ' . $sortDirection;
$secondarySort = $sort === 'delivery_date'
? ', sale_date ' . $sortDirection . ', id ' . $sortDirection
: ', COALESCE(delivery_date, DATE(sale_date)) ' . $sortDirection . ', sale_date ' . $sortDirection . ', id ' . $sortDirection;
$sql = "SELECT sales_orders.*, (
SELECT COUNT(*)
FROM sales_orders AS seq
WHERE seq.order_type = sales_orders.order_type
AND seq.sale_mode = sales_orders.sale_mode
AND seq.id <= sales_orders.id
) AS mode_serial_no
FROM sales_orders" . $where . ' ORDER BY ' . $primarySort . $secondarySort . ' LIMIT :limit OFFSET :offset';
$stmt = db()->prepare($sql);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$orders = $stmt->fetchAll();
foreach ($orders as &$order) {
$order['items'] = json_decode((string) ($order['items_json'] ?? '[]'), true) ?: [];
$order['payment_summary'] = sale_payment_summary($order);
$itemPreview = [];
foreach ($order['items'] as $item) {
$name = trim((string) ($item['name'] ?? $item['name_ar'] ?? $item['name_en'] ?? $item['sku'] ?? ''));
$qty = max(0, (int) ($item['qty'] ?? 0));
if ($name === '') {
continue;
}
$itemPreview[] = $qty > 0 ? $name . ' ×' . $qty : $name;
if (count($itemPreview) >= 2) {
break;
}
}
$remainingItems = max(0, count($order['items']) - count($itemPreview));
$order['item_preview'] = $itemPreview;
$order['item_preview_more'] = $remainingItems;
}
unset($order);
} catch (Throwable $e) {
$dbError = $e->getMessage();
}
$queryState = static function (array $extra = []) use ($search, $branch, $mode, $paymentStatus, $deliveryStatus, $dateFrom, $dateTo, $sort, $dir): array {
$params = [
'q' => $search,
'branch' => $branch,
'mode' => $mode,
'payment_status' => $paymentStatus,
'delivery_status' => $deliveryStatus,
'date_from' => $dateFrom,
'date_to' => $dateTo,
'sort' => $sort,
'dir' => $dir,
];
foreach ($extra as $key => $value) {
$params[$key] = $value;
}
return array_filter($params, static fn($value) => $value !== null && $value !== '');
};
$sortUrl = static function (string $column) use ($sort, $dir, $queryState): string {
$nextDir = $sort === $column && $dir === 'asc' ? 'desc' : 'asc';
return url_for('eid_orders.php', $queryState(['sort' => $column, 'dir' => $nextDir, 'p' => 1]));
};
$sortIcon = static function (string $column) use ($sort, $dir): string {
if ($sort !== $column) {
return 'bi-arrow-down-up text-muted';
}
return $dir === 'asc' ? 'bi-sort-down-alt' : 'bi-sort-up';
};
require __DIR__ . '/includes/header.php';
?>
<section class="mb-4">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-3">
<div>
<h1 class="h4 mb-1"><i class="bi bi-stars me-2"></i><?= h(tr('طلبات العيد', 'Eid Orders')) ?></h1>
</div>
<div class="d-flex gap-2 flex-wrap">
<a class="btn btn-sm btn-dark" href="<?= h(url_for('eid_sale.php')) ?>"><i class="bi bi-plus-circle me-1"></i><?= h(tr('طلب عيد جديد', 'New Eid Order')) ?></a>
<a class="btn btn-sm btn-outline-dark" href="<?= h(url_for('eid_print.php', $queryState())) ?>"><i class="bi bi-printer me-1"></i><?= h(tr('طباعة ملخص التجهيز', 'Print prep summary')) ?></a>
</div>
</div>
<?php $hasAdvancedFilters = $mode !== null || $paymentStatus !== ''; ?>
<form class="mb-4" method="GET" action="eid_orders.php">
<div class="d-flex justify-content-end align-items-center mb-3">
<details class="eid-advanced-toggle" <?= $hasAdvancedFilters ? 'open' : '' ?>>
<summary class="btn btn-sm btn-outline-secondary">
<i class="bi bi-sliders me-1"></i><?= h(tr('فلاتر إضافية', 'More filters')) ?>
</summary>
<div class="eid-advanced-panel">
<div class="row g-2">
<div class="col-12 col-md-6">
<label class="form-label mb-1" for="eid-mode"><?= h(tr('القناة', 'Channel')) ?></label>
<select id="eid-mode" class="form-select form-select-sm" name="mode">
<option value=""><?= h(tr('الكل', 'All')) ?></option>
<option value="normal" <?= $mode === 'normal' ? 'selected' : '' ?>><?= h(tr('فاتورة', 'Invoice')) ?></option>
<option value="pos" <?= $mode === 'pos' ? 'selected' : '' ?>>POS</option>
</select>
</div>
<div class="col-12 col-md-6">
<label class="form-label mb-1" for="eid-payment-status"><?= h(tr('حالة الدفع', 'Payment')) ?></label>
<select id="eid-payment-status" class="form-select form-select-sm" name="payment_status">
<option value=""><?= h(tr('كل الحالات', 'All statuses')) ?></option>
<option value="paid" <?= $paymentStatus === 'paid' ? 'selected' : '' ?>><?= h(tr('مدفوع', 'Paid')) ?></option>
<option value="partial" <?= $paymentStatus === 'partial' ? 'selected' : '' ?>><?= h(tr('جزئي', 'Partial')) ?></option>
<option value="unpaid" <?= $paymentStatus === 'unpaid' ? 'selected' : '' ?>><?= h(tr('غير مدفوع', 'Unpaid')) ?></option>
</select>
</div>
</div>
</div>
</details>
</div>
<div class="row g-3 align-items-end">
<div class="col-12 col-md-3">
<label class="form-label mb-1" for="eid-search"><?= h(tr('بحث سريع', 'Quick search')) ?></label>
<input id="eid-search" type="text" class="form-control form-control-sm" name="q" value="<?= h($search) ?>" placeholder="<?= h(tr('بحث...', 'Search...')) ?>">
</div>
<div class="col-12 col-md-2">
<label class="form-label mb-1" for="eid-branch"><?= h(tr('الفرع', 'Branch')) ?></label>
<select id="eid-branch" class="form-select form-select-sm" name="branch">
<option value=""><?= h(tr('كل الفروع', 'All branches')) ?></option>
<?php foreach (branches() as $branchCode => $branchLabel): ?>
<?php if ($user['role'] !== 'owner' && !in_array($branchCode, $allowedBranches, true)) { continue; } ?>
<option value="<?= h($branchCode) ?>" <?= $branch === $branchCode ? 'selected' : '' ?>><?= h(branch_label($branchCode)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-2">
<label class="form-label mb-1" for="eid-delivery-status"><?= h(tr('التجهيز', 'Prep')) ?></label>
<select id="eid-delivery-status" class="form-select form-select-sm" name="delivery_status">
<option value=""><?= h(tr('كل الحالات', 'All statuses')) ?></option>
<?php foreach ($deliveryOptions as $value => $label): ?>
<option value="<?= h($value) ?>" <?= $deliveryStatus === $value ? 'selected' : '' ?>><?= h($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-3">
<label class="form-label mb-1"><?= h(tr('فترة التسليم', 'Delivery window')) ?></label>
<div class="d-flex gap-2">
<input id="eid-date-from" type="date" class="form-control form-control-sm" name="date_from" value="<?= h($dateFrom) ?>" aria-label="<?= h(tr('من تاريخ', 'From date')) ?>">
<input id="eid-date-to" type="date" class="form-control form-control-sm" name="date_to" value="<?= h($dateTo) ?>" aria-label="<?= h(tr('إلى تاريخ', 'To date')) ?>">
</div>
</div>
<div class="col-12 col-md-2 d-flex gap-2">
<button type="submit" class="btn btn-sm btn-dark w-50"><i class="bi bi-funnel me-1"></i><?= h(tr('تطبيق', 'Apply')) ?></button>
<a class="btn btn-sm btn-outline-secondary w-50" href="<?= h(url_for('eid_orders.php')) ?>"><?= h(tr('إعادة ضبط', 'Reset')) ?></a>
</div>
</div>
</form>
<div class="row g-2 mb-3">
<div class="col-6 col-md-3">
<div class="card border-0 p-3 bg-primary-subtle">
<div class="small text-muted mb-1"><?= h(tr('إجمالي الطلبات', 'Total orders')) ?></div>
<div class="fs-5 fw-bold"><?= (int) ($summary['total_orders'] ?? 0) ?></div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 p-3 bg-info-subtle">
<div class="small text-muted mb-1"><?= h(tr('عدد القطع', 'Total items')) ?></div>
<div class="fs-5 fw-bold"><?= (int) ($summary['total_items'] ?? 0) ?></div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 p-3 bg-warning-subtle">
<div class="small text-muted mb-1"><?= h(tr('قيد التجهيز', 'Pending prep')) ?></div>
<div class="fs-5 fw-bold"><?= (int) ($summary['prep_orders'] ?? 0) ?></div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 p-3 bg-success-subtle">
<div class="small text-muted mb-1"><?= h(tr('إجمالي القيمة', 'Total value')) ?></div>
<div class="fs-5 fw-bold"><?= h(number_format((float) ($summary['total_amount'] ?? 0), 3)) ?></div>
</div>
</div>
</div>
<?php if ($dbError): ?>
<div class="alert alert-danger"><?= h($dbError) ?></div>
<?php elseif ($orders === []): ?>
<div class="text-center py-5 text-muted">
<div class="mb-3"><i class="bi bi-calendar-heart" style="font-size: 2rem;"></i></div>
<h2 class="h5"><?= h(tr('لا توجد طلبات عيد حتى الآن', 'No Eid orders yet')) ?></h2>
<p class="mb-0"><?= h(tr('لا توجد طلبات عيد حتى الآن. ابدأ بإنشاء أول طلب عيد ليظهر هنا مع الفلاتر والتواريخ.', 'There are no Eid orders yet. Create your first Eid order to see it here with filters and dates.')) ?></p>
<a class="btn btn-dark mt-3" href="<?= h(url_for('eid_sale.php')) ?>"><i class="bi bi-plus-circle me-1"></i><?= h(tr('إنشاء طلب عيد', 'Create Eid Order')) ?></a>
</div>
<?php else: ?>
<div class="d-flex justify-content-end mb-2">
<span class="badge rounded-pill text-bg-light border px-2 py-1 text-muted fw-normal">
<?= h((int) ($summary['total_orders'] ?? 0)) ?> <?= h(tr('طلب', 'orders')) ?>
</span>
</div>
<div class="table-responsive" style="min-height: 400px; padding-bottom: 180px;">
<table class="table table-sm table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>
<a class="text-decoration-none text-dark fw-semibold d-inline-flex align-items-center gap-1" href="<?= h($sortUrl('receipt_no')) ?>">
<?= h(tr('الفاتورة', 'Invoice')) ?>
<i class="bi <?= h($sortIcon('receipt_no')) ?>"></i>
</a>
</th>
<th>
<a class="text-decoration-none text-dark fw-semibold d-inline-flex align-items-center gap-1" href="<?= h($sortUrl('customer')) ?>">
<?= h(tr('العميل', 'Customer')) ?>
<i class="bi <?= h($sortIcon('customer')) ?>"></i>
</a>
</th>
<th><?= h(tr('الهاتف', 'Phone')) ?></th>
<th>
<a class="text-decoration-none text-dark fw-semibold d-inline-flex align-items-center gap-1" href="<?= h($sortUrl('delivery_date')) ?>">
<?= h(tr('تسليم', 'Delivery')) ?>
<i class="bi <?= h($sortIcon('delivery_date')) ?>"></i>
</a>
</th>
<th>
<a class="text-decoration-none text-dark fw-semibold d-inline-flex align-items-center gap-1" href="<?= h($sortUrl('delivery_status')) ?>">
<?= h(tr('حالة التجهيز', 'Prep status')) ?>
<i class="bi <?= h($sortIcon('delivery_status')) ?>"></i>
</a>
</th>
<th>
<a class="text-decoration-none text-dark fw-semibold d-inline-flex align-items-center gap-1" href="<?= h($sortUrl('branch')) ?>">
<?= h(tr('الفرع', 'Branch')) ?>
<i class="bi <?= h($sortIcon('branch')) ?>"></i>
</a>
</th>
<th>
<a class="text-decoration-none text-dark fw-semibold d-inline-flex align-items-center gap-1" href="<?= h($sortUrl('item_count')) ?>">
<?= h(tr('الأصناف', 'Items')) ?>
<i class="bi <?= h($sortIcon('item_count')) ?>"></i>
</a>
</th>
<th class="text-end">
<a class="text-decoration-none text-dark fw-semibold d-inline-flex align-items-center gap-1 justify-content-end" href="<?= h($sortUrl('total_amount')) ?>">
<?= h(tr('الإجمالي', 'Total')) ?>
<i class="bi <?= h($sortIcon('total_amount')) ?>"></i>
</a>
</th>
<th class="text-end"><?= h(tr('المدفوع', 'Paid')) ?></th>
<th class="text-end"><?= h(tr('المتبقي', 'Remaining')) ?></th>
<th><?= h(tr('الدفع', 'Payment')) ?></th>
<th class="text-end"><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($orders as $order): ?>
<?php $effectiveDelivery = $order['delivery_date'] ?: date('Y-m-d', strtotime((string) $order['sale_date'])); ?>
<tr>
<td>
<?php
$receiptReference = trim((string) ($order['receipt_no'] ?? ''));
$eidSerialValue = (int) ($order['eid_serial_no'] ?? 0);
?>
<div class="fw-semibold">#<?= h($receiptReference !== '' ? $receiptReference : (string) ($order['id'] ?? '')) ?></div>
<div class="small text-muted">
<?php if ($eidSerialValue > 0): ?>
<?= h(tr('التسلسل الموسمي', 'Season serial')) ?>: <?= h(eid_serial_label($eidSerialValue)) ?>
<span aria-hidden="true">·</span>
<?php endif; ?>
<?= h(sale_mode_label((string) ($order['sale_mode'] ?? 'normal'))) ?>
</div>
</td>
<?php
$rawCustomerName = (string) ($order['customer_name'] ?: tr('عميل نقدي', 'Walk-in customer'));
$displayPhone = '';
if (str_contains($rawCustomerName, ' - ')) {
$parts = explode(' - ', $rawCustomerName);
$lastPart = trim(end($parts));
if (preg_match('/^[0-9+\s]+$/', $lastPart)) {
$displayPhone = $lastPart;
array_pop($parts);
$rawCustomerName = trim(implode(' - ', $parts));
}
}
?>
<td>
<div class="fw-semibold"><?= h($rawCustomerName) ?></div>
</td>
<td dir="ltr" class="text-start">
<?php
$phone = ltrim(preg_replace('/[^0-9]/', '', $displayPhone), '0');
if ($phone !== '') {
if (str_starts_with($phone, '968') && strlen($phone) > 8) {
$phone = substr($phone, 3);
}
echo h($phone);
} else {
echo '-';
}
?>
</td>
<td>
<div class="fw-semibold"><?= h(date('Y-m-d', strtotime((string) $effectiveDelivery))) ?></div>
</td>
<td>
<span class="badge <?= h(eid_delivery_status_badge_class((string) ($order['delivery_status'] ?? 'pending'))) ?> px-2 py-1"><?= h(eid_delivery_status_label((string) ($order['delivery_status'] ?? 'pending'))) ?></span>
</td>
<td>
<span class="badge rounded-pill text-bg-light border"><?= h(branch_label((string) $order['branch_code'])) ?></span>
</td>
<td>
<span class="badge rounded-pill text-bg-light border px-2 py-1 fw-semibold">
<?= (int) ($order['item_count'] ?? 0) ?> <?= h(tr('صنف', 'items')) ?>
</span>
</td>
<td class="text-end fw-semibold"><?= h(number_format((float) ($order['total_amount'] ?? 0), 3)) ?></td>
<td class="text-end text-success fw-semibold"><?= h(number_format((float) (($order['payment_summary']['paid_amount'] ?? 0)), 3)) ?></td>
<td class="text-end fw-semibold <?= (($order['payment_summary']['due_amount'] ?? 0) > 0.0005) ? 'text-danger' : 'text-success' ?>"><?= h(number_format((float) (($order['payment_summary']['due_amount'] ?? 0)), 3)) ?></td>
<td>
<span class="badge <?= h(payment_status_badge_class((string) (($order['payment_summary']['payment_status'] ?? ($order['payment_status'] ?? 'paid'))))) ?> px-2 py-1">
<?= h(payment_status_label((string) (($order['payment_summary']['payment_status'] ?? ($order['payment_status'] ?? 'paid'))))) ?>
</span>
</td>
<td>
<div class="d-flex justify-content-end gap-2">
<?php if ((($order['payment_summary']['payment_status'] ?? ($order['payment_status'] ?? 'unpaid'))) !== 'paid'): ?>
<button type="button" class="btn btn-sm btn-outline-success" onclick="receivePayment(<?= (int) $order['id'] ?>, <?= json_encode((float) ($order['total_amount'] ?? 0)) ?>, <?= json_encode((float) (($order['payment_summary']['paid_amount'] ?? 0))) ?>, <?= json_encode((float) (($order['payment_summary']['due_amount'] ?? 0))) ?>, <?= (($order['status'] ?? 'completed') === 'order') ? 'true' : 'false' ?>)" title="<?= h(tr('استلام دفعة', 'Receive Payment')) ?>">
<i class="bi bi-cash"></i>
</button>
<?php endif; ?>
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" data-bs-boundary="window" aria-expanded="false" title="<?= h(tr('تغيير الحالة', 'Change status')) ?>">
<i class="bi bi-arrow-repeat"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow-sm">
<?php foreach (eid_delivery_status_options() as $val => $lbl): ?>
<?php if ($val === ($order['delivery_status'] ?? 'pending')) continue; ?>
<li>
<form method="post" action="">
<input type="hidden" name="action" value="update_delivery_status">
<input type="hidden" name="id" value="<?= h($order['id']) ?>">
<input type="hidden" name="status" value="<?= h($val) ?>">
<button class="dropdown-item" type="submit"><?= h($lbl) ?></button>
</form>
</li>
<?php endforeach; ?>
</ul>
</div>
<a class="btn btn-sm btn-light border text-primary" href="<?= h(url_for('sale.php', ['id' => $order['id']])) ?>" title="<?= h(tr('تفاصيل', 'Detail')) ?>"><i class="bi bi-eye"></i></a>
<a class="btn btn-sm btn-outline-secondary" href="<?= h(url_for('edit_sale.php', ['id' => $order['id']])) ?>" title="<?= h(tr('تعديل', 'Edit')) ?>"><i class="bi bi-pencil"></i></a>
<?php if ($canDeleteEidOrders): ?>
<form method="post" action="" class="d-inline-block js-eid-delete-form">
<input type="hidden" name="action" value="delete_eid_order">
<input type="hidden" name="id" value="<?= h($order['id']) ?>">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="confirmDeleteEidOrder(this.form)" title="<?= h(tr('حذف', 'Delete')) ?>">
<i class="bi bi-trash"></i>
</button>
</form>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($totalPages > 1): ?>
<nav class="mt-4" aria-label="<?= h(tr('صفحات طلبات العيد', 'Eid order pages')) ?>">
<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('eid_orders.php', $queryState(['p' => $i]))) ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
</ul>
</nav>
<?php endif; ?>
<?php endif; ?>
</section>
<script>
function formatPaymentPopupAmount(value) {
return Number(value || 0).toFixed(3);
}
async function receivePayment(id, totalAmount, paidAmount, dueAmount, completeOrder = false) {
const popupHtml = `
<div class="text-start">
<div class="row g-2 mb-3">
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('إجمالي الفاتورة', 'Total amount')) ?></div>
<div class="fw-bold text-dark">${formatPaymentPopupAmount(totalAmount)}</div>
</div>
</div>
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('المدفوع سابقاً', 'Already paid')) ?></div>
<div class="fw-bold text-primary">${formatPaymentPopupAmount(paidAmount)}</div>
</div>
</div>
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('المتبقي الحالي', 'Current remaining')) ?></div>
<div class="fw-bold text-danger">${formatPaymentPopupAmount(dueAmount)}</div>
</div>
</div>
</div>
<label for="swal-payment-amount" class="form-label fw-semibold mb-2"><?= h(tr('المبلغ المطلوب دفعه الآن', 'Amount to pay now')) ?></label>
<input id="swal-payment-amount" type="number" class="swal2-input mt-0" min="0.001" step="0.001" max="${formatPaymentPopupAmount(dueAmount)}" value="${formatPaymentPopupAmount(dueAmount)}">
<div class="d-flex justify-content-between align-items-center rounded-3 border px-3 py-2 bg-light mt-3">
<span class="small text-muted"><?= h(tr('المتبقي بعد الدفعة', 'Remaining after payment')) ?></span>
<strong id="swal-payment-remaining" class="text-success">0.000</strong>
</div>
</div>`;
const { isConfirmed, value: paymentAmount } = await Swal.fire({
title: '<?= h(tr('استلام دفعة', 'Receive Payment')) ?>',
html: popupHtml,
showCancelButton: true,
confirmButtonColor: '#198754',
confirmButtonText: '<?= h(tr('حفظ الدفعة', 'Save Payment')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>',
focusConfirm: false,
didOpen: () => {
const input = document.getElementById('swal-payment-amount');
const remainingEl = document.getElementById('swal-payment-remaining');
const updateRemaining = () => {
const amount = parseFloat(input.value || '0');
const safeAmount = Number.isFinite(amount) ? amount : 0;
const remaining = Math.max(dueAmount - safeAmount, 0);
remainingEl.textContent = formatPaymentPopupAmount(remaining);
remainingEl.className = remaining > 0.0005 ? 'text-danger' : 'text-success';
};
input.addEventListener('input', updateRemaining);
input.focus();
input.select();
updateRemaining();
},
preConfirm: () => {
const input = document.getElementById('swal-payment-amount');
const amount = parseFloat(input.value || '0');
if (!amount || amount <= 0) {
Swal.showValidationMessage('<?= h(tr('أدخل مبلغاً صحيحاً.', 'Enter a valid amount.')) ?>');
return false;
}
if (amount - dueAmount > 0.0005) {
Swal.showValidationMessage('<?= h(tr('المبلغ لا يمكن أن يتجاوز المتبقي.', 'Amount cannot exceed the due balance.')) ?>');
return false;
}
return formatPaymentPopupAmount(amount);
}
});
if (!isConfirmed || !paymentAmount) {
return;
}
const formData = new FormData();
formData.append('sale_id', String(id));
formData.append('payment_amount', String(paymentAmount));
if (completeOrder) {
formData.append('complete_order', '1');
}
const response = await fetch('api/sales_payment.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.success) {
await Swal.fire({ icon: 'success', text: data.message, confirmButtonText: 'OK' });
window.location.reload();
} else {
Swal.fire({ icon: 'error', text: data.error || '<?= h(tr('تعذر تسجيل الدفعة.', 'Could not record the payment.')) ?>' });
}
}
function confirmDeleteEidOrder(form) {
Swal.fire({
title: '<?= h(tr('هل أنت متأكد من حذف الفاتورة؟', 'Are you sure you want to delete this invoice?')) ?>',
text: '<?= h(tr('سيتم حذف فاتورة العيد نهائياً من النظام. استخدم هذا الخيار فقط عند الضرورة.', 'This will permanently delete the Eid invoice from the system. Use this only when necessary.')) ?>',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#dc3545',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>',
confirmButtonText: '<?= h(tr('نعم، احذف', 'Yes, delete it')) ?>'
}).then((result) => {
if (result.isConfirmed) {
form.submit();
}
});
}
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>

253
eid_print.php Normal file
View File

@ -0,0 +1,253 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('eid_orders', 'show');
ensure_sales_table();
$activeNav = 'eid_orders';
$pageTitle = tr('طباعة تجهيزات العيد', 'Print Eid Prep Summary');
$metaDescription = tr('ملخص قابل للطباعة لتجهيزات طلبات العيد حسب التاريخ والفرع والحالة.', 'Printable Eid preparation summary by date, branch, and status.');
$metaRobots = 'noindex, nofollow';
$mode = isset($_GET['mode']) && in_array($_GET['mode'], ['pos', 'normal'], true) ? $_GET['mode'] : null;
$branch = isset($_GET['branch']) && array_key_exists($_GET['branch'], branches()) ? $_GET['branch'] : null;
$search = trim((string) ($_GET['q'] ?? ''));
$paymentStatus = trim((string) ($_GET['payment_status'] ?? ''));
$deliveryStatus = trim((string) ($_GET['delivery_status'] ?? ''));
$dateFrom = trim((string) ($_GET['date_from'] ?? ''));
$dateTo = trim((string) ($_GET['date_to'] ?? ''));
$allowedBranches = $user && $user['role'] !== 'owner' ? get_user_branches($user) : [];
$deliveryOptions = eid_delivery_status_options();
$dbError = null;
$orders = [];
$itemRows = [];
$summary = [
'total_orders' => 0,
'unique_items' => 0,
'total_quantity' => 0,
'total_amount' => 0,
];
try {
$params = [':order_type' => 'eid'];
$where = ' WHERE order_type = :order_type ';
if ($mode) {
$where .= ' AND sale_mode = :sale_mode ';
$params[':sale_mode'] = $mode;
}
if ($branch) {
$where .= ' AND branch_code = :branch_code ';
$params[':branch_code'] = $branch;
}
if ($user && $user['role'] !== 'owner') {
if ($allowedBranches === []) {
$where .= ' AND 1=0 ';
} else {
$namedParams = [];
foreach ($allowedBranches as $i => $allowedBranch) {
$key = ':v_branch_' . $i;
$namedParams[] = $key;
$params[$key] = $allowedBranch;
}
$where .= ' AND branch_code IN (' . implode(', ', $namedParams) . ') ';
}
}
if ($search !== '') {
$where .= ' AND (receipt_no LIKE :search OR customer_name LIKE :search OR cashier_name LIKE :search OR notes LIKE :search) ';
$params[':search'] = '%' . $search . '%';
}
if (in_array($paymentStatus, ['paid', 'partial', 'unpaid'], true)) {
$where .= ' AND payment_status = :payment_status ';
$params[':payment_status'] = $paymentStatus;
}
if (isset($deliveryOptions[$deliveryStatus])) {
$where .= ' AND delivery_status = :delivery_status ';
$params[':delivery_status'] = $deliveryStatus;
}
if ($dateFrom !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom)) {
$where .= ' AND DATE(COALESCE(delivery_date, sale_date)) >= :date_from ';
$params[':date_from'] = $dateFrom;
} else {
$dateFrom = '';
}
if ($dateTo !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) {
$where .= ' AND DATE(COALESCE(delivery_date, sale_date)) <= :date_to ';
$params[':date_to'] = $dateTo;
} else {
$dateTo = '';
}
$sql = 'SELECT * FROM sales_orders' . $where . ' ORDER BY COALESCE(delivery_date, DATE(sale_date)) ASC, sale_date ASC';
$stmt = db()->prepare($sql);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->execute();
$orders = $stmt->fetchAll();
$itemIndex = [];
foreach ($orders as &$order) {
$decodedItems = json_decode((string) ($order['items_json'] ?? '[]'), true);
$items = is_array($decodedItems) ? $decodedItems : [];
$order['items'] = $items;
if (($order['delivery_status'] ?? '') === 'cancelled') {
continue;
}
foreach ($items as $item) {
$sku = trim((string) ($item['sku'] ?? ''));
$name = trim((string) ($item['name_ar'] ?? ''));
if ($name === '') {
$name = trim((string) ($item['name_en'] ?? ''));
}
if ($name === '') {
$name = $sku !== '' ? $sku : tr('صنف بدون اسم', 'Unnamed item');
}
$key = $sku !== '' ? $sku : md5($name);
$qty = max(0, (float) ($item['qty'] ?? 0));
if (!isset($itemIndex[$key])) {
$itemIndex[$key] = [
'sku' => $sku,
'name' => $name,
'qty' => 0.0,
'order_count' => 0,
];
}
$itemIndex[$key]['qty'] += $qty;
$itemIndex[$key]['order_count']++;
}
}
unset($order);
usort($orders, static function (array $a, array $b): int {
$aDate = (string) ($a['delivery_date'] ?: substr((string) ($a['sale_date'] ?? ''), 0, 10));
$bDate = (string) ($b['delivery_date'] ?: substr((string) ($b['sale_date'] ?? ''), 0, 10));
return [$aDate, (string) ($a['receipt_no'] ?? '')] <=> [$bDate, (string) ($b['receipt_no'] ?? '')];
});
$itemRows = array_values($itemIndex);
usort($itemRows, static function (array $a, array $b): int {
if ($a['qty'] === $b['qty']) {
return strcasecmp((string) $a['name'], (string) $b['name']);
}
return $b['qty'] <=> $a['qty'];
});
$summary['total_orders'] = count($orders);
$summary['unique_items'] = count($itemRows);
$summary['total_quantity'] = array_sum(array_map(static fn(array $row): float => (float) $row['qty'], $itemRows));
$summary['total_amount'] = array_sum(array_map(static fn(array $row): float => (float) ($row['total_amount'] ?? 0), $orders));
} catch (Throwable $e) {
$dbError = $e->getMessage();
}
$filterParams = array_filter([
'q' => $search,
'branch' => $branch,
'mode' => $mode,
'payment_status' => $paymentStatus,
'delivery_status' => $deliveryStatus,
'date_from' => $dateFrom,
'date_to' => $dateTo,
], static fn($value) => $value !== null && $value !== '');
$generatedAt = date('Y-m-d H:i');
require __DIR__ . '/includes/header.php';
?>
<style>
.eid-print-shell { max-width: 1180px; margin: 0 auto; }
.eid-print-card {
background: #fff;
border: 1px solid #e8ecf4;
border-radius: 16px;
box-shadow: 0 12px 40px rgba(15, 23, 42, 0.06);
}
.eid-print-card .table th { white-space: nowrap; }
@media print {
@page {
size: portrait;
margin: 1cm;
}
body { background: #fff !important; color: #000 !important; font-size: 12pt !important; }
.main-sidebar, .main-header, .footer-section, footer, .d-print-none, .alert-dismissible .btn-close { display: none !important; }
.main-content { margin: 0 !important; padding: 0 !important; }
.eid-print-shell { max-width: 100% !important; margin: 0 !important; padding: 0 !important; }
.eid-print-card { box-shadow: none !important; border: none !important; padding: 0 !important; margin: 0 !important; border-radius: 0 !important; }
.table { width: 100% !important; border-collapse: collapse !important; }
.table th, .table td {
border: 1px solid #000 !important;
padding: 6px !important;
color: #000 !important;
}
.table th { background: transparent !important; }
}
</style>
<div class="eid-print-shell py-4">
<section class="eid-print-card p-4 p-lg-5 mb-4">
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap mb-4 d-print-none">
<div>
<h1 class="h3 mb-1"><i class="bi bi-printer me-2"></i><?= h(tr("ملخص تجهيزات طلبات العيد", "Eid Order Prep Summary")) ?></h1>
<p class="text-muted mb-0"><?= h(tr("تقرير قابل للطباعة يوضح الأصناف المطلوبة وعدد الطلبات التي تحتوي كل صنف.", "Printable report showing required items and how many orders contain each item.")) ?></p>
</div>
<div class="d-flex gap-2 flex-wrap">
<a class="btn btn-outline-secondary" href="<?= h(url_for("eid_orders.php", $filterParams)) ?>"><i class="bi bi-arrow-<?= current_lang() === "ar" ? "right" : "left" ?> me-1"></i><?= h(tr("رجوع لطلبات العيد", "Back to Eid Orders")) ?></a>
<button type="button" class="btn btn-dark" onclick="window.print()"><i class="bi bi-printer me-1"></i><?= h(tr("طباعة", "Print")) ?></button>
</div>
</div>
<div class="text-center mb-4">
<h2 class="h4 mb-1"><?= h(tr("ملخص تجهيزات طلبات العيد", "Eid Order Prep Summary")) ?></h2>
<div class="text-muted"><?= h(tr("تاريخ الطباعة", "Printed at")) ?>: <?= h($generatedAt) ?></div>
<?php if ($dateFrom !== "" || $dateTo !== ""): ?>
<div class="text-muted mt-1"><?= h(tr("الفترة", "Period")) ?>: <?= h($dateFrom !== "" ? $dateFrom : "…") ?> &rarr; <?= h($dateTo !== "" ? $dateTo : "…") ?></div>
<?php endif; ?>
</div>
<?php if ($dbError): ?>
<div class="alert alert-danger"><?= h($dbError) ?></div>
<?php else: ?>
<?php if ($itemRows === []): ?>
<div class="alert alert-secondary text-center"><?= h(tr("لا توجد بيانات للطباعة.", "No data to print.")) ?></div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-bordered align-middle text-center">
<thead >
<tr>
<th style="width: 60px;">#</th>
<th style="text-align: right !important;"><?= h(tr("الصنف", "Item")) ?></th>
<th style="width: 150px;"><?= h(tr("عدد الطلبات", "Orders count")) ?></th>
<th style="width: 150px;"><?= h(tr("الكمية المطلوبة", "Quantity wanted")) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($itemRows as $index => $row): ?>
<tr>
<td><?= $index + 1 ?></td>
<td class="fw-bold" style="text-align: right !important;"><?= h($row["name"]) ?></td>
<td><?= (int) $row["order_count"] ?></td>
<td class="fw-bold"><?= h(rtrim(rtrim(number_format((float) $row["qty"], 3, ".", ""), "0"), ".")) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php endif; ?>
</section>
</div>
<?php require __DIR__ . "/includes/footer.php"; ?>

12
eid_sale.php Normal file
View File

@ -0,0 +1,12 @@
<?php
require_once __DIR__ . '/includes/app.php';
$saleMode = 'normal';
$orderType = 'eid';
$pageTitle = tr("إنشاء طلب عيد", "Create Eid Order");
$activeNav = "eid_sale";
$backUrl = url_for('eid_orders.php');
$backLabel = tr('عودة لطلبات العيد', 'Back to Eid Orders');
$saveLabel = tr('حفظ طلب العيد', 'Save Eid Order');
$saveOnlyLabel = tr('حفظ بدون طباعة', 'Save without print');
$savePrintLabel = tr('حفظ مع الطباعة', 'Save with print');
require_once __DIR__ . '/includes/sale_form.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="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>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 text-center">
<thead class="table-light">
<tr>
<th>ID</th>
<th><?= h(tr('الاسم (عربي)', 'Name (AR)')) ?></th>
<th><?= h(tr('الاسم (إنجليزي)', 'Name (EN)')) ?></th>
<th><?= 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'; ?>

329
expenses.php Normal file
View File

@ -0,0 +1,329 @@
<?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')) {
$pb = $_POST['branch_code'] ?? ''; $branch_code = can_access_branch($pb) ? $pb : $userBranch; if ($pb === '' && $user['role'] === 'owner') { $branch_code = null; } else if ($branch_code === '') { $branch_code = null; }
$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')) {
$pb = $_POST['branch_code'] ?? ''; $branch_code = can_access_branch($pb) ? $pb : $userBranch; if ($pb === '' && $user['role'] === 'owner') { $branch_code = null; } else if ($branch_code === '') { $branch_code = null; }
$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) {
$ubranches = get_user_branches($user);
if (!empty($ubranches)) {
$inQuery = implode(',', array_fill(0, count($ubranches), '?'));
$where .= " AND (e.branch_code IN ($inQuery) OR e.branch_code IS NULL)";
foreach ($ubranches as $ub) {
$params[] = $ub;
}
} else {
$where .= " AND e.branch_code IS NULL";
}
}
$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="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>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 text-center">
<thead class="table-light">
<tr>
<th><?= h(tr('التاريخ', 'Date')) ?></th>
<th><?= h(tr('التصنيف', 'Category')) ?></th>
<th><?= h(tr('المبلغ', 'Amount')) ?></th>
<th><?= h(tr('الفرع', 'Branch')) ?></th>
<th><?= h(tr('الوصف', 'Description')) ?></th>
<th><?= 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'; ?>

8
healthz.php Normal file
View File

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
http_response_code(200);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'status' => 'ok',
'time' => date(DATE_ATOM),
], JSON_UNESCAPED_UNICODE);

1231
includes/SimpleXLSX.php Normal file

File diff suppressed because it is too large Load Diff

2473
includes/app.php Normal file

File diff suppressed because it is too large Load Diff

82
includes/footer.php Normal file
View File

@ -0,0 +1,82 @@
<?php
$user = current_user();
$isPublic = !isset($user) || !$user;
$companyDisplayName = current_lang() === 'ar' ? get_setting('company_name_ar', 'حلوى الريامي') : get_setting('company_name_en', 'Al Riyami Sweets');
$privacyUrl = url_for('privacy-policy.php');
$termsUrl = url_for('terms-conditions.php');
?>
<?php if ($isPublic): ?>
</main>
<footer class="bg-white border-top mt-4 py-4 px-3 text-muted small">
<div class="container">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
&copy; <?= date('Y') ?> <strong><?= h($companyDisplayName) ?></strong>. <?= h(tr('جميع الحقوق محفوظة.', 'All rights reserved.')) ?>
</div>
<div class="d-flex align-items-center flex-wrap gap-3">
<a href="<?= h($privacyUrl) ?>" class="text-decoration-none"><?= h(tr('سياسة الخصوصية', 'Privacy Policy')) ?></a>
<a href="<?= h($termsUrl) ?>" class="text-decoration-none"><?= h(tr('الشروط والأحكام', 'Terms & Conditions')) ?></a>
<span class="text-secondary"><?= h(tr('تم التطوير بواسطة', 'Developed via')) ?> </span><a href="https://flatlogic.com" target="_blank" class="text-decoration-none fw-semibold">Flatlogic</a>
</div>
</div>
</div>
</footer>
<?php else: ?>
</div> <!-- /.container-fluid -->
<!-- App Footer -->
<footer class="bg-white border-top mt-auto py-3 px-4 text-muted small">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
&copy; <?= date('Y') ?> <strong><?= h($companyDisplayName) ?></strong>. <?= h(tr('جميع الحقوق محفوظة.', 'All rights reserved.')) ?>
</div>
<div class="d-flex align-items-center flex-wrap gap-3 mt-2 mt-md-0">
<a href="<?= h($privacyUrl) ?>" class="text-decoration-none"><?= h(tr('سياسة الخصوصية', 'Privacy Policy')) ?></a>
<a href="<?= h($termsUrl) ?>" class="text-decoration-none"><?= h(tr('الشروط والأحكام', 'Terms & Conditions')) ?></a>
<div>
<span class="text-secondary"><?= h(tr('تم التطوير بواسطة', 'Developed via')) ?> </span><a href="https://omanapp.cloud" target="_blank" class="text-decoration-none fw-semibold">Oman App</a>
</div>
</div>
</div>
</footer>
<!-- /App Footer -->
</div> <!-- /#page-content-wrapper -->
</div> <!-- /#wrapper -->
<?php endif; ?>
<?php require_once __DIR__ . "/footer_settings.php"; ?>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script>
// Menu Toggle Script
document.addEventListener('DOMContentLoaded', function() {
var toggleBtn = document.getElementById("menu-toggle");
if (toggleBtn) {
toggleBtn.addEventListener('click', function(e) {
e.preventDefault();
document.getElementById("wrapper").classList.toggle("toggled");
});
}
});
</script>
<script>
function confirmSwal(e, msg) {
e.preventDefault();
const form = e.target;
Swal.fire({
title: msg,
icon: 'warning',
showCancelButton: true,
confirmButtonText: '<?= h(tr("نعم", "Yes")) ?>',
cancelButtonText: '<?= h(tr("إلغاء", "Cancel")) ?>',
confirmButtonColor: '#d33'
}).then((result) => {
if (result.isConfirmed) {
form.submit();
}
});
}
</script>
<script src="assets/js/main.js?v=<?= h(date('YmdHi')) ?>"></script>
</body>
</html>

View File

@ -0,0 +1,868 @@
<?php if (isset($user) && $user && in_array($user['role'], ['owner', 'manager'])): ?>
<?php
$wablasInvoiceTemplate = (string) get_setting_non_empty('wablas_template_invoice', wablas_default_invoice_template());
$wablasInvoicePreviewVars = wablas_invoice_preview_vars();
$wablasDailyTemplate = (string) get_setting_non_empty('wablas_template_daily_report', wablas_default_daily_report_template());
$wablasDailyPreviewVars = wablas_daily_report_preview_vars();
$wablasReportRecipientsValue = (string) get_setting('wablas_report_recipients');
$wablasReportRecipientCount = count(wablas_parse_phone_list($wablasReportRecipientsValue)['phones']);
$wablasDailyAutoEnabled = (string) get_setting('wablas_daily_auto_send', '0') === '1';
$wablasDailyAutoTime = wablas_format_time_setting((string) get_setting('wablas_daily_auto_time', '21:00'));
$wablasDailyAutoLastDate = (string) get_setting('wablas_daily_auto_last_date', '');
$eidSerialNext = current_eid_serial_next(db());
?>
<!-- Settings Modal -->
<div class="modal fade" id="settingsModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<form action="api/settings.php" method="POST" enctype="multipart/form-data">
<div class="modal-header">
<div class="w-100">
<div class="settings-header-toolbar">
<div class="settings-top-actions">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal" dir="auto"><?= h(tr('إلغاء', 'Cancel')) ?></button>
<button type="submit" class="btn btn-primary btn-sm" dir="auto">
<i class="bi bi-save me-1"></i><?= h(tr('حفظ التغييرات', 'Save Changes')) ?>
</button>
</div>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-outline-success btn-sm" data-open-wablas-settings="1">
<i class="bi bi-whatsapp me-1"></i><?= h(tr('واتساب', 'WhatsApp')) ?>
</button>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="mt-3">
<h5 class="modal-title mb-1"><?= h(tr('إعدادات الشركة', 'Company Settings')) ?></h5>
<div class="small text-muted"><?= h(tr('إعدادات واتساب أصبحت في نافذة مستقلة حتى تظهر جميع القوالب بوضوح.', 'WhatsApp settings now open in a separate popup so all templates stay visible.')) ?></div>
</div>
</div>
</div>
<div class="modal-body pb-2">
<ul class="nav nav-tabs mb-4" id="settingsTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="settings-company-tab" data-bs-toggle="tab" data-bs-target="#settings-company-pane" type="button" role="tab" aria-controls="settings-company-pane" aria-selected="true">
<?= h(tr('بيانات الشركة', 'Company')) ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="settings-eid-tab" data-bs-toggle="tab" data-bs-target="#settings-eid-pane" type="button" role="tab" aria-controls="settings-eid-pane" aria-selected="false">
<?= h(tr('طلبات العيد', 'Eid Orders')) ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="settings-wablas-tab" data-bs-toggle="tab" data-bs-target="#settings-wablas-pane" type="button" role="tab" aria-controls="settings-wablas-pane" aria-selected="false">
<?= h(tr('واتساب', 'WhatsApp')) ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="settings-payment-tab" data-bs-toggle="tab" data-bs-target="#settings-payment-pane" type="button" role="tab" aria-controls="settings-payment-pane" aria-selected="false">
<?= h(tr('الدفع الإلكتروني', 'Payments')) ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="settings-email-tab" data-bs-toggle="tab" data-bs-target="#settings-email-pane" type="button" role="tab" aria-controls="settings-email-pane" aria-selected="false">
<?= h(tr('البريد الإلكتروني', 'Email')) ?>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="settings-policies-tab" data-bs-toggle="tab" data-bs-target="#settings-policies-pane" type="button" role="tab" aria-controls="settings-policies-pane" aria-selected="false">
<?= h(tr('السياسات والشروط', 'Policies')) ?>
</button>
</li>
</ul>
<div class="tab-content" id="settingsTabsContent">
<div class="tab-pane fade show active" id="settings-company-pane" role="tabpanel" aria-labelledby="settings-company-tab" tabindex="0">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label"><?= h(tr('المنطقة الزمنية (Timezone)', 'Timezone')) ?></label>
<select class="form-select" name="timezone" required>
<?php
$zones = timezone_identifiers_list();
$current_tz = get_setting('timezone', 'UTC');
foreach($zones as $z): ?>
<option value="<?= h($z) ?>" <?= $z === $current_tz ? 'selected' : '' ?>><?= h($z) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('اسم الشركة (عربي)', 'Company Name (AR)')) ?></label>
<input type="text" class="form-control" name="company_name_ar" value="<?= h(get_setting('company_name_ar')) ?>" required>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('اسم الشركة (إنجليزي)', 'Company Name (EN)')) ?></label>
<input type="text" class="form-control" name="company_name_en" value="<?= h(get_setting('company_name_en')) ?>" required>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('النسبة الضريبية %', 'VAT Percentage %')) ?></label>
<input type="number" step="0.01" class="form-control" name="vat_percentage" value="<?= h(get_setting('vat_percentage', 5)) ?>" required>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('الرقم الضريبي', 'VAT Number')) ?></label>
<input type="text" class="form-control" name="company_vat_number" value="<?= h(get_setting('company_vat_number')) ?>">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('رقم الهاتف', 'Phone Number')) ?></label>
<div class="input-group" dir="ltr">
<span class="input-group-text">968</span>
<input type="text" class="form-control" name="company_phone" value="<?= h(normalize_oman_phone((string) get_setting('company_phone'))) ?>" inputmode="numeric" maxlength="8" pattern="\d{8}" placeholder="91234567">
</div>
<div class="form-text"><?= h(tr('يُحفظ الرقم محلياً من 8 خانات ويُرسل مع المقدمة 968.', 'Phone is stored locally as 8 digits and sent with the 968 prefix.')) ?></div>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('البريد الإلكتروني', 'Email')) ?></label>
<input type="email" class="form-control" name="company_email" value="<?= h(get_setting('company_email')) ?>">
</div>
<div class="col-md-12">
<label class="form-label"><?= h(tr('العنوان', 'Address')) ?></label>
<textarea class="form-control" name="company_address" rows="2"><?= h(get_setting('company_address')) ?></textarea>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('الشعار (Logo)', 'Logo')) ?></label>
<input type="file" class="form-control" name="company_logo" accept="image/*">
<?php if (get_setting('company_logo')): ?>
<div class="mt-2"><img src="<?= h(get_setting('company_logo')) ?>" height="50" style="background: #f8f9fa; padding: 5px; border-radius: 4px;"></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('الأيقونة (Favicon)', 'Favicon')) ?></label>
<input type="file" class="form-control" name="company_favicon" accept="image/x-icon,image/png,image/jpeg">
<?php if (get_setting('company_favicon')): ?>
<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>
</div>
</div>
</div>
<div class="tab-pane fade" id="settings-eid-pane" role="tabpanel" aria-labelledby="settings-eid-tab" tabindex="0">
<div class="row g-3">
<div class="col-12">
<div class="rounded-4 border bg-body-tertiary p-3 settings-eid-card">
<div class="d-flex flex-column flex-xl-row justify-content-between align-items-xl-start gap-3 settings-eid-header-row">
<div class="flex-grow-1">
<div class="small text-uppercase text-muted mb-1"><?= h(tr('طلبات العيد', 'Eid orders')) ?></div>
<h6 class="fw-bold mb-1"><?= h(tr('إعادة ضبط التسلسل الموسمي', 'Reset seasonal serial')) ?></h6>
<p class="text-muted small mb-0"><?= h(tr('يعيد الرقم القادم لطلبات العيد إلى 1 للطلبات الجديدة فقط، بدون تغيير أرقام الطلبات السابقة.', 'Resets the next Eid order serial to 1 for new orders only, without changing older saved orders.')) ?></p>
</div>
<div class="bg-white border rounded-3 px-3 py-2 settings-eid-reset-box">
<div class="small text-muted mb-1"><?= h(tr('الرقم القادم', 'Next serial')) ?></div>
<div class="d-flex align-items-center gap-2 flex-nowrap settings-eid-inline-controls" dir="auto">
<div class="fs-5 fw-bold mb-0" data-eid-serial-next-label="1"><?= h(eid_serial_label($eidSerialNext)) ?></div>
<button
type="button"
class="btn btn-outline-danger btn-sm px-3 text-nowrap"
data-reset-eid-serial="1"
data-reset-url="<?= h(url_for('api/settings.php')) ?>"
data-reset-label="<?= h(eid_serial_label(1)) ?>"
data-reset-confirm="<?= h(tr('سيتم تعيين الرقم القادم لطلبات العيد إلى 1. لن يتم تعديل الطلبات القديمة. هل تريد المتابعة؟', 'The next Eid order serial will be reset to 1. Old orders will not be changed. Do you want to continue?')) ?>"
data-reset-success="<?= h(tr('تمت إعادة الضبط بنجاح.', 'Reset completed successfully.')) ?>"
data-reset-error="<?= h(tr('تعذر تنفيذ إعادة الضبط حالياً. حاول مرة أخرى.', 'Could not reset the Eid serial right now. Please try again.')) ?>">
<i class="bi bi-arrow-counterclockwise me-1"></i><?= h(tr('إعادة الضبط إلى 1', 'Reset to 1')) ?>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="settings-wablas-pane" role="tabpanel" aria-labelledby="settings-wablas-tab" tabindex="0">
<div class="rounded-4 border bg-body-tertiary p-4">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3">
<div>
<h5 class="mb-1 fw-bold"><?= h(tr('إعدادات واتساب في نافذة مستقلة', 'WhatsApp settings in a separate popup')) ?></h5>
<p class="text-muted mb-0"><?= h(tr('تم نقل جميع قوالب واتساب إلى نافذة مستقلة قابلة للتمرير حتى تظهر كل القوالب بوضوح بدون ازدحام داخل إعدادات الشركة.', 'All WhatsApp templates were moved into a separate scrollable popup so every template is clearly visible without being cramped inside Company Settings.')) ?></p>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-success" data-open-wablas-settings="1">
<i class="bi bi-whatsapp me-1"></i><?= h(tr('فتح إعدادات واتساب', 'Open WhatsApp Settings')) ?>
</button>
</div>
</div>
<div class="row g-3 mt-1">
<div class="col-md-4">
<div class="border rounded-3 bg-white p-3 h-100">
<div class="small text-muted mb-1"><?= h(tr('حالة الإرسال', 'Sending status')) ?></div>
<div class="fw-semibold"><?= h(wablas_is_enabled() ? tr('مفعل', 'Enabled') : tr('متوقف', 'Disabled')) ?></div>
</div>
</div>
<div class="col-md-4">
<div class="border rounded-3 bg-white p-3 h-100">
<div class="small text-muted mb-1"><?= h(tr('الفواتير', 'Invoices')) ?></div>
<div class="fw-semibold"><?= h(tr('للعميل فقط', 'Customer only')) ?></div>
</div>
</div>
<div class="col-md-4">
<div class="border rounded-3 bg-white p-3 h-100">
<div class="small text-muted mb-1"><?= h(tr('الملخص اليومي', 'Daily summary')) ?></div>
<div class="fw-semibold"><?= $wablasDailyAutoEnabled ? h($wablasDailyAutoTime . ' ' . tr('يومياً', 'daily')) : h(tr('يدوي فقط', 'Manual only')) ?></div>
<div class="small text-muted mt-1"><?= h((string) $wablasReportRecipientCount) ?> <?= h(tr('رقم مستلم', 'recipient numbers')) ?></div>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="settings-payment-pane" role="tabpanel" aria-labelledby="settings-payment-tab" tabindex="0">
<div class="row g-3">
<div class="col-12">
<div class="border rounded-4 p-3 bg-body-tertiary">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" role="switch" id="thawaniEnabled" name="thawani_enabled" value="1" <?= thawani_is_enabled() ? 'checked' : '' ?>>
<label class="form-check-label fw-semibold" for="thawaniEnabled"><?= h(tr('تفعيل بوابة ثواني', 'Enable Thawani Gateway')) ?></label>
</div>
<div class="small text-muted"><?= h(tr('فعّلها ليظهر خيارا الدفع أونلاين أو الدفع لاحقاً في طلبات المتجر.', 'Enable it so the online store can offer Pay Online or Pay Later.')) ?></div>
</div>
</div>
<div class="col-md-4">
<label class="form-label"><?= h(tr('البيئة', 'Mode')) ?></label>
<select class="form-select" name="thawani_mode">
<option value="sandbox" <?= get_setting('thawani_mode', 'sandbox') === 'sandbox' ? 'selected' : '' ?>><?= h(tr('تجريبية (Sandbox)', 'Sandbox')) ?></option>
<option value="live" <?= get_setting('thawani_mode') === 'live' ? 'selected' : '' ?>><?= h(tr('حقيقية (Live)', 'Live')) ?></option>
</select>
</div>
<div class="col-md-8">
<label class="form-label"><?= h(tr('المفتاح العام (Publishable Key)', 'Publishable Key')) ?></label>
<input type="text" class="form-control" name="thawani_publishable_key" value="<?= h(get_setting('thawani_publishable_key')) ?>" placeholder="pk_...">
</div>
<div class="col-md-12">
<label class="form-label"><?= h(tr('المفتاح السري (Secret Key)', 'Secret Key')) ?></label>
<input type="password" class="form-control" name="thawani_secret_key" value="<?= h(get_setting('thawani_secret_key')) ?>" placeholder="sk_...">
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('رابط نجاح الدفع', 'Success URL')) ?></label>
<input type="url" class="form-control" name="thawani_success_url" value="<?= h(get_setting('thawani_success_url')) ?>" placeholder="<?= h(thawani_default_return_url('success')) ?>">
<div class="form-text"><?= h(tr('اختياري. إذا تركته فارغاً سيستخدم النظام رابط الرجوع الداخلي تلقائياً.', 'Optional. Leave empty to use the built-in return URL automatically.')) ?></div>
</div>
<div class="col-md-6">
<label class="form-label"><?= h(tr('رابط إلغاء الدفع', 'Cancel URL')) ?></label>
<input type="url" class="form-control" name="thawani_cancel_url" value="<?= h(get_setting('thawani_cancel_url')) ?>" placeholder="<?= h(thawani_default_return_url('cancel')) ?>">
<div class="form-text"><?= h(tr('اختياري. يستخدم عندما يُلغى الدفع أو يعود العميل بدون إتمام العملية.', 'Optional. Used when payment is cancelled or the customer returns without completing checkout.')) ?></div>
</div>
<div class="col-12">
<div class="alert alert-info rounded-4 mb-0">
<div class="fw-semibold mb-1"><?= h(tr('معلومة', 'Note')) ?></div>
<div class="small mb-0"><?= h(tr('عند اختيار الدفع أونلاين سيُنشئ النظام جلسة Thawani ويرسل العميل مباشرةً لصفحة الدفع.', 'When Pay Online is selected, the system creates a Thawani session and redirects the customer to the hosted checkout page.')) ?></div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="settings-email-pane" role="tabpanel" aria-labelledby="settings-email-tab" tabindex="0">
<div class="row g-3">
<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="tab-pane fade" id="settings-policies-pane" role="tabpanel" aria-labelledby="settings-policies-tab" tabindex="0">
<div class="row g-3">
<div class="col-12">
<div class="alert alert-light border rounded-4 mb-0">
<div class="fw-semibold mb-1"><?= h(tr('روابط التذييل', 'Footer links')) ?></div>
<div class="small text-muted mb-0"><?= h(tr('سيتم ربط صفحات سياسة الخصوصية والشروط والأحكام تلقائياً في تذييل النظام والمتجر.', 'Privacy Policy and Terms pages will be linked automatically in the admin and store footer.')) ?></div>
</div>
</div>
<div class="col-12">
<label class="form-label"><?= h(tr('سياسة الخصوصية', 'Privacy Policy')) ?></label>
<textarea class="form-control" name="privacy_policy_content" rows="12" placeholder="<?= h(tr('أدخل نص سياسة الخصوصية هنا...', 'Enter the privacy policy content here...')) ?>"><?= h(get_setting('privacy_policy_content')) ?></textarea>
<div class="form-text"><?= h(tr('يظهر هذا النص في صفحة سياسة الخصوصية العامة.', 'This content appears on the public Privacy Policy page.')) ?></div>
</div>
<div class="col-12">
<label class="form-label"><?= h(tr('الشروط والأحكام', 'Terms & Conditions')) ?></label>
<textarea class="form-control" name="terms_conditions_content" rows="12" placeholder="<?= h(tr('أدخل نص الشروط والأحكام هنا...', 'Enter the terms and conditions content here...')) ?>"><?= h(get_setting('terms_conditions_content')) ?></textarea>
<div class="form-text"><?= h(tr('يظهر هذا النص في صفحة الشروط والأحكام العامة.', 'This content appears on the public Terms & Conditions page.')) ?></div>
</div>
<div class="col-12">
<div class="settings-inline-save border rounded-4 bg-body-tertiary px-3 py-3 d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<div>
<div class="fw-semibold"><?= h(tr('حفظ السياسات', 'Save policies')) ?></div>
<div class="small text-muted"><?= h(tr('يمكنك الحفظ من هنا أيضاً إذا كان زر التذييل خارج الشاشة.', 'You can also save from here if the footer action is outside the screen.')) ?></div>
</div>
<button type="submit" class="btn btn-primary px-4 align-self-md-start">
<i class="bi bi-save me-1"></i><?= h(tr('حفظ التغييرات', 'Save Changes')) ?>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<style>
#settingsModal .modal-header {
position: sticky;
z-index: 3;
background: var(--bs-body-bg);
}
#settingsModal .modal-header {
top: 0;
border-bottom: 1px solid var(--bs-border-color);
}
#settingsModal .settings-header-toolbar {
direction: ltr;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
}
#settingsModal .settings-top-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
#settingsModal .modal-body {
padding-top: 0.85rem;
padding-bottom: 1rem;
}
#settingsModal .settings-eid-card {
margin-top: 0;
}
#settingsModal .settings-eid-header-row {
flex-wrap: nowrap;
}
#settingsModal .settings-eid-reset-box {
flex: 0 0 auto;
min-width: 290px;
align-self: flex-start;
}
#settingsModal .settings-eid-inline-controls {
justify-content: space-between;
}
#settingsModal .settings-inline-save {
position: sticky;
bottom: -0.5rem;
z-index: 2;
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
}
@media (max-width: 767.98px) {
#settingsModal .settings-top-actions {
width: 100%;
}
#settingsModal .settings-top-actions .btn {
flex: 1 1 0;
}
#settingsModal .settings-header-toolbar > .d-flex {
width: 100%;
justify-content: flex-end;
}
#settingsModal .settings-eid-header-row {
flex-wrap: wrap;
}
#settingsModal .settings-eid-reset-box {
width: 100%;
min-width: 0;
}
}
#wablasSettingsModal {
overflow-y: auto;
}
#wablasSettingsModal .modal-dialog {
margin: 0;
max-width: none;
min-height: 100%;
}
#wablasSettingsModal .modal-content {
min-height: 100vh;
border-radius: 0;
}
#wablasSettingsModal .modal-header,
#wablasSettingsModal .modal-footer {
position: sticky;
z-index: 3;
background: var(--bs-body-bg);
}
#wablasSettingsModal .modal-header {
top: 0;
border-bottom: 1px solid var(--bs-border-color);
}
#wablasSettingsModal .modal-footer {
bottom: 0;
border-top: 1px solid var(--bs-border-color);
}
#wablasSettingsModal .modal-body {
overflow: visible;
padding-bottom: 2rem;
}
#wablasSettingsModal .wablas-template-preview {
white-space: pre-wrap;
word-break: break-word;
}
</style>
<!-- WhatsApp Settings Modal -->
<div class="modal fade" id="wablasSettingsModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content border-0 rounded-0">
<form action="api/settings.php" method="POST" id="wablasSettingsForm">
<input type="hidden" name="return_modal" value="wablas">
<div class="modal-header">
<div>
<h5 class="modal-title mb-1"><?= h(tr('إعدادات واتساب', 'WhatsApp Settings')) ?></h5>
<div class="small text-muted"><?= h(tr('نافذة مستقلة قابلة للتمرير لعرض جميع القوالب والحقول بشكل واضح.', 'A dedicated scrollable popup to show all WhatsApp fields and templates clearly.')) ?></div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert d-none" id="wablasSettingsAlert" role="alert"></div>
<div class="row g-3">
<div class="col-12">
<div class="border rounded-4 px-3 py-3 bg-body-tertiary">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3">
<div>
<h6 class="mb-1 fw-bold"><?= h(tr('ربط Wablas', 'Wablas connection')) ?></h6>
<div class="small text-muted"><?= h(tr('فعّل الإرسال ثم احفظ، ويمكنك إرسال رسالة اختبار مباشرة من هنا.', 'Enable sending, save the settings, and send a test message directly from here.')) ?></div>
</div>
<div class="form-check form-switch m-0">
<input type="hidden" name="wablas_enabled" value="0">
<input class="form-check-input" type="checkbox" role="switch" id="wablasEnabledSwitchModal" name="wablas_enabled" value="1" <?= wablas_is_enabled() ? 'checked' : '' ?>>
<label class="form-check-label fw-semibold" for="wablasEnabledSwitchModal"><?= h(tr('تفعيل واتساب', 'Enable WhatsApp')) ?></label>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<label class="form-label mb-1"><?= h(tr('Wablas Token', 'Wablas Token')) ?></label>
<input type="text" class="form-control" name="wablas_token" value="<?= h(get_setting('wablas_token')) ?>">
</div>
<div class="col-md-6">
<label class="form-label mb-1"><?= h(tr('Wablas Secret Key', 'Wablas Secret Key')) ?></label>
<input type="password" class="form-control" name="wablas_secret_key" value="<?= h(get_setting('wablas_secret_key')) ?>">
</div>
<div class="col-12">
<label class="form-label mb-1"><?= h(tr('رابط بوابة Wablas', 'Wablas Gateway URL')) ?></label>
<input type="text" class="form-control" name="wablas_api_url" value="<?= h(get_setting('wablas_api_url', 'https://wablas.com/api/send-message')) ?>" placeholder="https://wablas.com/api/send-message">
<div class="small text-muted mt-1"><?= h(tr('إذا كان حسابك مربوطاً بسيرفر محدد مثل tegal.wablas.com أو texas.wablas.com فاكتب رابط ذلك السيرفر هنا. يمكنك إدخال الدومين فقط أو رابط /api/send-message الكامل.', 'If your account is tied to a specific server like tegal.wablas.com or texas.wablas.com, enter that server here. You can paste either the domain or the full /api/send-message URL.')) ?></div>
</div>
<div class="col-md-4">
<label class="form-label mb-1"><?= h(tr('رقم الاختبار', 'Test Phone')) ?></label>
<div class="input-group" dir="ltr">
<span class="input-group-text">968</span>
<input type="text" class="form-control" name="wablas_test_phone" value="<?= h(normalize_oman_phone((string) get_setting('company_phone'))) ?>" inputmode="numeric" maxlength="8" pattern="\d{8}" placeholder="91234567">
</div>
</div>
<div class="col-md-6">
<label class="form-label mb-1"><?= h(tr('رسالة الاختبار', 'Test Message')) ?></label>
<textarea class="form-control" name="wablas_test_message" rows="2"><?= h(tr('هذه رسالة واتساب تجريبية من النظام.', 'This is a WhatsApp test message from the system.')) ?></textarea>
</div>
<div class="col-md-2 d-grid align-self-end">
<button type="submit" class="btn btn-outline-success" formaction="api/wablas_test.php" formmethod="POST">
<i class="bi bi-whatsapp me-1"></i><?= h(tr('إرسال اختبار', 'Send Test')) ?>
</button>
</div>
<div class="col-12">
<div class="border rounded-4 p-3 bg-white">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-start gap-2 mb-3">
<div>
<h6 class="mb-1 fw-semibold"><?= h(tr('قالب الفاتورة', 'Invoice template')) ?></h6>
<div class="small text-muted"><?= h(tr('يرسل تلقائياً إلى رقم العميل الموجود في الفاتورة فقط.', 'Sent automatically only to the customer phone saved on the invoice.')) ?></div>
</div>
<span class="badge text-bg-success-subtle border border-success-subtle text-success-emphasis"><?= h(tr('للعميل فقط', 'Customer only')) ?></span>
</div>
<div class="alert alert-light border small mb-3">
<strong><?= h(tr('مهم:', 'Important:')) ?></strong> <?= h(tr('لن يتم إرسال الفاتورة لأي رقم إداري. إذا لم يكن للعميل رقم واتساب محفوظ فلن يتم الإرسال.', 'The invoice will not be sent to any admin number. If the customer has no saved WhatsApp phone, nothing will be sent.')) ?>
</div>
<label class="form-label mb-1"><?= h(tr('قالب الفاتورة', 'Invoice Template')) ?></label>
<textarea class="form-control" id="wablasInvoiceTemplate" name="wablas_template_invoice" rows="12" data-wablas-preview-source="invoice"><?= h($wablasInvoiceTemplate) ?></textarea>
<div class="small text-muted mt-2"><?= h(tr('المتغيرات المتاحة: {receipt_no}, {customer_name}, {customer_phone}, {branch_name}, {payment_method_label}, {payment_status_label}, {total_amount}, {due_amount}, {sale_date}, {items_summary}, {notes}', 'Available placeholders: {receipt_no}, {customer_name}, {customer_phone}, {branch_name}, {payment_method_label}, {payment_status_label}, {total_amount}, {due_amount}, {sale_date}, {items_summary}, {notes}')) ?></div>
<div class="border rounded-3 bg-light-subtle p-3 mt-3">
<div class="small text-muted mb-2"><?= h(tr('معاينة مباشرة', 'Live preview')) ?></div>
<pre class="small mb-0 text-wrap wablas-template-preview" data-wablas-preview="invoice" data-preview-vars='<?= h(json_encode($wablasInvoicePreviewVars, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?>'><?= h(wablas_render_template($wablasInvoiceTemplate, $wablasInvoicePreviewVars)) ?></pre>
</div>
</div>
</div>
<div class="col-12">
<div class="border rounded-4 p-3 bg-white">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-start gap-2 mb-3">
<div>
<h6 class="mb-1 fw-semibold"><?= h(tr('قالب الملخص اليومي', 'Daily summary template')) ?></h6>
<div class="small text-muted"><?= h(tr('يرسل إلى أرقام الإدارة المحددة فقط، ويمكن تشغيله تلقائياً كل يوم في وقت محدد.', 'Sent only to the specified management numbers, and can run automatically every day at a chosen time.')) ?></div>
</div>
<span class="badge text-bg-light border"><?= h((string) $wablasReportRecipientCount) ?> <?= h(tr('مستلمين', 'recipients')) ?></span>
</div>
<div class="row g-3">
<div class="col-lg-6">
<label class="form-label mb-1"><?= h(tr('أرقام واتساب الملخص اليومي', 'Daily summary WhatsApp numbers')) ?></label>
<textarea class="form-control" name="wablas_report_recipients" rows="5" dir="ltr"><?= h(wablas_phone_list_for_input($wablasReportRecipientsValue)) ?></textarea>
<div class="form-text"><?= h(tr('هذه الأرقام فقط ستستلم الملخص اليومي.', 'Only these numbers will receive the daily summary.')) ?></div>
</div>
<div class="col-lg-6">
<div class="border rounded-4 bg-body-tertiary p-3 h-100">
<div class="form-check form-switch mb-3">
<input type="hidden" name="wablas_daily_auto_send" value="0">
<input class="form-check-input" type="checkbox" role="switch" id="wablasDailyAutoSend" name="wablas_daily_auto_send" value="1" <?= $wablasDailyAutoEnabled ? 'checked' : '' ?>>
<label class="form-check-label fw-semibold" for="wablasDailyAutoSend"><?= h(tr('تفعيل الإرسال التلقائي للملخص اليومي', 'Enable automatic daily summary sending')) ?></label>
</div>
<label class="form-label mb-1" for="wablasDailyAutoTime"><?= h(tr('وقت الإرسال اليومي', 'Daily send time')) ?></label>
<input type="time" class="form-control" id="wablasDailyAutoTime" name="wablas_daily_auto_time" value="<?= h($wablasDailyAutoTime) ?>">
<div class="form-text mt-2"><?= h(tr('سيتم الإرسال مرة واحدة يومياً بعد هذا الوقت عند أول استخدام للنظام.', 'It will send once per day after this time on the first app request that day.')) ?></div>
<?php if ($wablasDailyAutoLastDate !== ''): ?>
<div class="small text-muted mt-3"><?= h(tr('آخر إرسال تلقائي:', 'Last automatic send:')) ?> <?= h($wablasDailyAutoLastDate) ?></div>
<?php endif; ?>
</div>
</div>
</div>
<label class="form-label mb-1 mt-3"><?= h(tr('قالب الملخص اليومي', 'Daily Summary Template')) ?></label>
<textarea class="form-control" id="wablasDailyTemplate" name="wablas_template_daily_report" rows="12" data-wablas-preview-source="daily"><?= h($wablasDailyTemplate) ?></textarea>
<div class="small text-muted mt-2"><?= h(tr('المتغيرات المتاحة: {report_date}, {branch_name}, {invoice_count}, {total_sales}, {seller_summary}, {outlet_summary}, {payment_summary}, {generated_at}', 'Available placeholders: {report_date}, {branch_name}, {invoice_count}, {total_sales}, {seller_summary}, {outlet_summary}, {payment_summary}, {generated_at}')) ?></div>
<div class="border rounded-3 bg-light-subtle p-3 mt-3">
<div class="small text-muted mb-2"><?= h(tr('معاينة مباشرة', 'Live preview')) ?></div>
<pre class="small mb-0 text-wrap wablas-template-preview" data-wablas-preview="daily" data-preview-vars='<?= h(json_encode($wablasDailyPreviewVars, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) ?>'><?= h(wablas_render_template($wablasDailyTemplate, $wablasDailyPreviewVars)) ?></pre>
</div>
</div>
</div>
<div class="col-12">
<div class="border rounded-4 p-3 bg-white">
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-2 mb-3">
<div>
<h6 class="mb-1 fw-semibold"><?= h(tr('قوالب طلبات المتجر الإلكتروني', 'Online order templates')) ?></h6>
<div class="small text-muted"><?= h(tr('هذه القوالب تُرسل لعميل الطلب حسب حالة الطلب.', 'These templates are sent to the customer depending on the order status.')) ?></div>
</div>
</div>
<div class="row g-3">
<div class="col-12">
<label class="form-label mb-1"><?= h(tr('قالب رسالة إنشاء الطلب', 'Order Created Template')) ?></label>
<textarea class="form-control" name="wablas_template_created" rows="4"><?= h(get_setting('wablas_template_created', wablas_default_order_template('created'))) ?></textarea>
</div>
<div class="col-md-6">
<label class="form-label mb-1"><?= h(tr('قالب قيد الانتظار', 'Pending Template')) ?></label>
<textarea class="form-control" name="wablas_template_pending" rows="4"><?= h(get_setting('wablas_template_pending', wablas_default_order_template('pending'))) ?></textarea>
</div>
<div class="col-md-6">
<label class="form-label mb-1"><?= h(tr('قالب مقبول', 'Accepted Template')) ?></label>
<textarea class="form-control" name="wablas_template_accepted" rows="4"><?= h(get_setting('wablas_template_accepted', wablas_default_order_template('accepted'))) ?></textarea>
</div>
<div class="col-md-6">
<label class="form-label mb-1"><?= h(tr('قالب مكتمل', 'Completed Template')) ?></label>
<textarea class="form-control" name="wablas_template_completed" rows="4"><?= h(get_setting('wablas_template_completed', wablas_default_order_template('completed'))) ?></textarea>
</div>
<div class="col-md-6">
<label class="form-label mb-1"><?= h(tr('قالب مرفوض', 'Rejected Template')) ?></label>
<textarea class="form-control" name="wablas_template_rejected" rows="4"><?= h(get_setting('wablas_template_rejected', wablas_default_order_template('rejected'))) ?></textarea>
</div>
</div>
</div>
</div>
</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-success"><?= h(tr('حفظ إعدادات واتساب', 'Save WhatsApp Settings')) ?></button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
var settingsModalEl = document.getElementById('settingsModal');
var wablasModalEl = document.getElementById('wablasSettingsModal');
var wablasForm = document.getElementById('wablasSettingsForm');
var wablasAlertEl = document.getElementById('wablasSettingsAlert');
var reopenSettingsAfterWablas = false;
var showWablasAlert = function (type, message) {
if (!wablasAlertEl) {
return;
}
var alertClass = type === 'success' ? 'alert-success' : (type === 'warning' ? 'alert-warning' : 'alert-danger');
wablasAlertEl.className = 'alert ' + alertClass;
wablasAlertEl.textContent = message || '';
wablasAlertEl.classList.remove('d-none');
};
var clearWablasAlert = function () {
if (!wablasAlertEl) {
return;
}
wablasAlertEl.className = 'alert d-none';
wablasAlertEl.textContent = '';
};
document.querySelectorAll('[data-open-wablas-settings]').forEach(function (trigger) {
trigger.addEventListener('click', function () {
if (!wablasModalEl || typeof bootstrap === 'undefined') {
return;
}
var wablasModal = bootstrap.Modal.getOrCreateInstance(wablasModalEl);
if (settingsModalEl && settingsModalEl.classList.contains('show')) {
reopenSettingsAfterWablas = true;
settingsModalEl.addEventListener('hidden.bs.modal', function handleSettingsHidden() {
settingsModalEl.removeEventListener('hidden.bs.modal', handleSettingsHidden);
wablasModal.show();
});
bootstrap.Modal.getOrCreateInstance(settingsModalEl).hide();
return;
}
wablasModal.show();
});
});
if (settingsModalEl && wablasModalEl && typeof bootstrap !== 'undefined') {
wablasModalEl.addEventListener('hidden.bs.modal', function () {
clearWablasAlert();
if (!reopenSettingsAfterWablas) {
return;
}
reopenSettingsAfterWablas = false;
bootstrap.Modal.getOrCreateInstance(settingsModalEl).show();
});
}
if (wablasModalEl && typeof bootstrap !== 'undefined') {
var wablasUrl = new URL(window.location.href);
if (wablasUrl.searchParams.get('open_modal') === 'wablas') {
bootstrap.Modal.getOrCreateInstance(wablasModalEl).show();
wablasUrl.searchParams.delete('open_modal');
if (window.history && typeof window.history.replaceState === 'function') {
window.history.replaceState({}, document.title, wablasUrl.pathname + (wablasUrl.search ? '?' + wablasUrl.searchParams.toString() : '') + wablasUrl.hash);
}
}
}
document.querySelectorAll('[data-reset-eid-serial]').forEach(function (button) {
button.addEventListener('click', function () {
var actionUrl = button.getAttribute('data-reset-url') || 'api/settings.php';
var confirmMessage = button.getAttribute('data-reset-confirm') || '';
var successMessage = button.getAttribute('data-reset-success') || 'Done.';
var errorMessage = button.getAttribute('data-reset-error') || 'Request failed.';
var nextLabel = button.getAttribute('data-reset-label') || 'E-0001';
var nextLabelEl = document.querySelector('[data-eid-serial-next-label]');
var executeReset = function () {
var formData = new FormData();
formData.append('action', 'reset_eid_serial');
formData.append('return_modal', 'settings');
var originalHtml = button.innerHTML;
button.disabled = true;
button.setAttribute('aria-busy', 'true');
button.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span><?= h(tr('جارٍ التنفيذ...', 'Processing...')) ?>';
fetch(actionUrl, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
},
credentials: 'same-origin'
}).then(function (response) {
return response.json().catch(function () {
return { success: false, type: 'danger', message: errorMessage };
});
}).then(function (data) {
var ok = !!(data && data.success);
var type = data && data.type ? data.type : (ok ? 'success' : 'danger');
var message = data && data.message ? data.message : (ok ? successMessage : errorMessage);
if (ok && nextLabelEl) {
nextLabelEl.textContent = nextLabel;
}
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: type === 'success' ? 'success' : (type === 'warning' ? 'warning' : 'error'),
title: message,
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3200,
timerProgressBar: true
});
} else if (!ok) {
window.alert(message);
}
}).catch(function () {
if (typeof Swal !== 'undefined') {
Swal.fire({ icon: 'error', title: errorMessage, toast: true, position: 'top-end', showConfirmButton: false, timer: 3200, timerProgressBar: true });
} else {
window.alert(errorMessage);
}
}).finally(function () {
button.disabled = false;
button.removeAttribute('aria-busy');
button.innerHTML = originalHtml;
});
};
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: 'warning',
title: confirmMessage,
showCancelButton: true,
confirmButtonText: '<?= h(tr('نعم، إعادة الضبط', 'Yes, reset')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>'
}).then(function (result) {
if (result.isConfirmed) {
executeReset();
}
});
return;
}
if (window.confirm(confirmMessage)) {
executeReset();
}
});
});
if (wablasForm && window.fetch) {
wablasForm.addEventListener('submit', function (event) {
var submitter = event.submitter;
if (!submitter) {
return;
}
event.preventDefault();
clearWablasAlert();
var action = submitter.getAttribute('formaction') || wablasForm.getAttribute('action') || window.location.href;
var method = submitter.getAttribute('formmethod') || wablasForm.getAttribute('method') || 'POST';
var formData = new FormData(wablasForm, submitter);
var originalHtml = submitter.innerHTML;
submitter.disabled = true;
submitter.setAttribute('aria-busy', 'true');
submitter.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span><?= h(tr('جارٍ الحفظ...', 'Saving...')) ?>';
fetch(action, {
method: method.toUpperCase(),
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
},
credentials: 'same-origin'
}).then(function (response) {
return response.json().catch(function () {
return { success: false, type: 'danger', message: 'Unexpected response.' };
});
}).then(function (data) {
var type = data && data.type ? data.type : (data && data.success ? 'success' : 'danger');
var message = data && data.message ? data.message : 'Request finished.';
showWablasAlert(type, message);
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: type === 'success' ? 'success' : (type === 'warning' ? 'warning' : 'error'),
title: message,
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000,
timerProgressBar: true
});
}
}).catch(function () {
showWablasAlert('danger', '<?= h(tr('تعذر حفظ الإعدادات حالياً. حاول مرة أخرى.', 'Could not save the settings right now. Please try again.')) ?>');
}).finally(function () {
submitter.disabled = false;
submitter.removeAttribute('aria-busy');
submitter.innerHTML = originalHtml;
});
});
}
document.querySelectorAll('[data-wablas-preview-source]').forEach(function (field) {
var key = field.getAttribute('data-wablas-preview-source');
var preview = document.querySelector('[data-wablas-preview="' + key + '"]');
if (!preview) {
return;
}
var vars = {};
try {
vars = JSON.parse(preview.getAttribute('data-preview-vars') || '{}');
} catch (error) {
vars = {};
}
var render = function (template) {
return String(template || '').replace(/\{([a-z0-9_]+)\}/gi, function (match, token) {
return Object.prototype.hasOwnProperty.call(vars, token) ? String(vars[token]) : match;
});
};
var sync = function () {
preview.textContent = render(field.value);
};
field.addEventListener('input', sync);
sync();
});
});
</script>
<?php endif; ?>

279
includes/header.php Normal file
View File

@ -0,0 +1,279 @@
<?php
require_once __DIR__ . '/app.php';
$user = current_user();
$activeNav = $activeNav ?? 'dashboard';
$pageTitle = $pageTitle ?? app_name();
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
$projectName = $_SERVER['PROJECT_NAME'] ?? app_name();
$metaDescription = $projectDescription !== '' ? $projectDescription : tr('منصة مبيعات ومخزون متعددة الفروع لحلوى الريامي.', 'Multi-branch sweets sales and stock workspace for Al Riyami Sweets.');
$flash = pull_flash();
$assetVersion = date('YmdHi');
// Determine if we are on a public page (like login)
$isPublic = !empty($forcePublic) || !isset($user) || !$user;
$bodyClasses = $isPublic ? 'auth-body' : '';
?>
<!doctype html>
<html lang="<?= h(current_lang()) ?>" dir="<?= is_rtl() ? 'rtl' : 'ltr' ?>">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= h($pageTitle) ?> · <?= h($projectName) ?></title>
<?php if ($projectDescription): ?>
<meta name="description" content='<?= h($projectDescription) ?>' />
<meta property="og:description" content="<?= h($projectDescription) ?>" />
<meta property="twitter:description" content="<?= h($projectDescription) ?>" />
<?php else: ?>
<meta name="description" content="<?= h($metaDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= h($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>" />
<?php endif; ?>
<?php if (!empty($metaRobots)): ?>
<meta name="robots" content="<?= h($metaRobots) ?>" />
<?php endif; ?>
<meta name="theme-color" content="#343a40" />
<?php if (get_setting('company_favicon')): ?>
<link rel="icon" href="<?= h(get_setting('company_favicon')) ?>">
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= h($assetVersion) ?>">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
</head>
<body class="<?= h($bodyClasses) ?>">
<?php if ($isPublic): ?>
<!-- Public Layout for Login -->
<main class="container">
<?php else: ?>
<!-- Private Admin Layout -->
<div class="d-flex" id="wrapper">
<!-- Sidebar -->
<div class="border-end bg-dark text-white shadow-sm" id="sidebar-wrapper">
<div class="sidebar-heading text-center py-4 fs-5 fw-bold text-uppercase border-bottom border-secondary d-flex flex-column align-items-center">
<?php if (get_setting('company_logo')): ?>
<img src="<?= h(get_setting('company_logo')) ?>" alt="Logo" style="max-height: 50px; margin-bottom: 10px; background: white; padding: 5px; border-radius: 5px;">
<?php else: ?>
<i class="bi bi-shop me-2 fs-2 mb-2"></i>
<?php endif; ?>
<span><?= h(current_lang() === 'ar' ? get_setting('company_name_ar', 'حلوى الريامي') : get_setting('company_name_en', 'Al Riyami Sweets')) ?></span>
</div>
<div class="p-3 text-center border-bottom border-secondary">
<div class="fw-semibold"><?= h(current_lang() === 'ar' ? $user['name_ar'] : $user['name_en']) ?></div>
<div class="text-white-50 small"><?= h(role_label($user['role'])) ?> · <?= h(branch_label($user['branch_code'])) ?></div>
</div>
<div class="list-group list-group-flush my-3 pb-5" id="sidebar-navigation">
<a class="list-group-item list-group-item-action <?= $activeNav === 'dashboard' ? 'active' : '' ?>" href="<?= h(url_for('index.php')) ?>">
<i class="bi bi-speedometer2"></i> <?= h(tr('لوحة التحكم', 'Dashboard')) ?>
</a>
<?php if (has_permission('stock', 'show') || has_permission('categories', 'show') || has_permission('units', 'show')): ?>
<!-- المخزون (Inventory) - Now First -->
<a class="list-group-item list-group-item-action <?= in_array($activeNav, ['stock', 'categories', 'units']) ? '' : 'collapsed' ?>" data-bs-toggle="collapse" href="#collapseStock" role="button" aria-expanded="<?= in_array($activeNav, ['stock', 'categories', 'units']) ? 'true' : 'false' ?>" aria-controls="collapseStock">
<div class="d-flex justify-content-between align-items-center w-100">
<span><i class="bi bi-box-seam"></i> <?= h(tr('المخزون', 'Inventory')) ?></span>
<i class="bi bi-chevron-down toggle-icon" style="transition: transform 0.2s;"></i>
</div>
</a>
<div class="collapse <?= in_array($activeNav, ['stock', 'categories', 'units']) ? 'show' : '' ?>" id="collapseStock" data-bs-parent="#sidebar-navigation">
<div class="list-group list-group-flush" style="background-color: rgba(0,0,0,0.15);">
<a class="list-group-item list-group-item-action <?= $activeNav === 'stock' ? 'active' : '' ?>" href="<?= h(url_for('stock.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('قائمة الأصناف', 'Items List')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'categories' ? 'active' : '' ?>" href="<?= h(url_for('categories.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('التصنيفات', 'Categories')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'units' ? 'active' : '' ?>" href="<?= h(url_for('units.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('الوحدات', 'Units')) ?>
</a>
</div>
</div>
<?php endif; ?>
<?php if (has_permission('sales', 'show') || has_permission('normal_sale', 'show') || has_permission('pos', 'show')): ?>
<!-- المبيعات (Sales) - Now Collapsible -->
<a class="list-group-item list-group-item-action <?= in_array($activeNav, ['sales', 'sales_orders', 'unpaid', 'normal', 'pos', 'online_orders']) ? '' : 'collapsed' ?>" data-bs-toggle="collapse" href="#collapseSales" role="button" aria-expanded="<?= in_array($activeNav, ['sales', 'sales_orders', 'unpaid', 'normal', 'pos', 'online_orders']) ? 'true' : 'false' ?>" aria-controls="collapseSales">
<div class="d-flex justify-content-between align-items-center w-100">
<span><i class="bi bi-cart"></i> <?= h(tr('المبيعات', 'Sales')) ?></span>
<i class="bi bi-chevron-down toggle-icon" style="transition: transform 0.2s;"></i>
</div>
</a>
<div class="collapse <?= in_array($activeNav, ['sales', 'sales_orders', 'unpaid', 'normal', 'pos', 'online_orders']) ? 'show' : '' ?>" id="collapseSales" data-bs-parent="#sidebar-navigation">
<div class="list-group list-group-flush" style="background-color: rgba(0,0,0,0.15);">
<a class="list-group-item list-group-item-action <?= $activeNav === 'sales' ? 'active' : '' ?>" href="<?= h(url_for('sales.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('قائمة الفواتير', 'Invoice list')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'sales_orders' ? 'active' : '' ?>" href="<?= h(url_for('sales.php', ['status' => 'order'])) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('الطلبات', 'Orders')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'online_orders' ? 'active' : '' ?>" href="<?= h(url_for('online_orders.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('طلبات المتجر', 'Store Orders')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'normal' ? 'active' : '' ?>" href="<?= h(url_for('normal_sale.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('فاتورة جديدة', 'New invoice')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'pos' ? 'active' : '' ?>" href="<?= h(url_for('pos.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('نقاط البيع', 'POS')) ?>
</a>
</div>
</div>
<?php endif; ?>
<?php if (has_permission('sales', 'show') || has_permission('normal_sale', 'show') || has_permission('pos', 'show')): ?>
<!-- طلبات العيد (Eid Orders) -->
<a class="list-group-item list-group-item-action <?= in_array($activeNav, ['eid_orders', 'eid_sale']) ? '' : 'collapsed' ?>" data-bs-toggle="collapse" href="#collapseEid" role="button" aria-expanded="<?= in_array($activeNav, ['eid_orders', 'eid_sale']) ? 'true' : 'false' ?>" aria-controls="collapseEid">
<div class="d-flex justify-content-between align-items-center w-100">
<span><i class="bi bi-gift"></i> <?= h(tr('طلبات العيد', 'Eid Orders')) ?></span>
<i class="bi bi-chevron-down toggle-icon" style="transition: transform 0.2s;"></i>
</div>
</a>
<div class="collapse <?= in_array($activeNav, ['eid_orders', 'eid_sale']) ? 'show' : '' ?>" id="collapseEid" data-bs-parent="#sidebar-navigation">
<div class="list-group list-group-flush" style="background-color: rgba(0,0,0,0.15);">
<a class="list-group-item list-group-item-action <?= $activeNav === 'eid_orders' ? 'active' : '' ?>" href="<?= h(url_for('eid_orders.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('طلبات العيد', 'Eid Orders')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'eid_sale' ? 'active' : '' ?>" href="<?= h(url_for('eid_sale.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('طلب عيد جديد', 'New Eid Order')) ?>
</a>
</div>
</div>
<?php endif; ?>
<?php if (has_permission('purchases', 'show')): ?>
<a class="list-group-item list-group-item-action <?= in_array($activeNav, ['purchases', 'new_purchase']) ? '' : 'collapsed' ?>" data-bs-toggle="collapse" href="#collapsePurchases" role="button" aria-expanded="<?= in_array($activeNav, ['purchases', 'new_purchase']) ? 'true' : 'false' ?>" aria-controls="collapsePurchases">
<div class="d-flex justify-content-between align-items-center w-100">
<span><i class="bi bi-bag-plus"></i> <?= h(tr('المشتريات', 'Purchases')) ?></span>
<i class="bi bi-chevron-down toggle-icon" style="transition: transform 0.2s;"></i>
</div>
</a>
<div class="collapse <?= in_array($activeNav, ['purchases', 'new_purchase']) ? 'show' : '' ?>" id="collapsePurchases" data-bs-parent="#sidebar-navigation">
<div class="list-group list-group-flush" style="background-color: rgba(0,0,0,0.15);">
<a class="list-group-item list-group-item-action <?= $activeNav === 'purchases' ? 'active' : '' ?>" href="<?= h(url_for('purchases.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('قائمة المشتريات', 'Purchase list')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'new_purchase' ? 'active' : '' ?>" href="<?= h(url_for('new_purchase.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('فاتورة مشتريات جديدة', 'New purchase')) ?>
</a>
</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" data-bs-parent="#sidebar-navigation">
<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>
<a class="list-group-item list-group-item-action <?= $activeNav === 'customers' ? 'active' : '' ?>" href="<?= h(url_for('customers.php')) ?>">
<i class="bi bi-people-fill"></i> <?= h(tr('العملاء', 'Customers')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'debts' ? 'active' : '' ?>" href="<?= h(url_for('debts.php')) ?>">
<i class="bi bi-journal-text"></i> <?= h(tr('الديون والفواتير الآجلة', 'Debts & Unpaid')) ?>
</a>
<?php if (has_permission('reports', 'show')): ?>
<a class="list-group-item list-group-item-action <?= $activeNav === 'reports' ? 'active' : '' ?>" href="<?= h(url_for('reports.php')) ?>">
<i class="bi bi-bar-chart"></i> <?= h(tr('التقارير', 'Reports')) ?>
</a>
<?php endif; ?>
<?php if (has_permission('users', 'show')): ?>
<a class="list-group-item list-group-item-action <?= $activeNav === 'users' ? 'active' : '' ?>" href="<?= h(url_for('users.php')) ?>">
<i class="bi bi-people"></i> <?= h(tr('المستخدمون والأدوار', 'Users & Roles')) ?>
</a>
<?php endif; ?>
<?php if (has_permission('settings', 'show')): ?>
<a class="list-group-item list-group-item-action <?= in_array($activeNav, ['outlets']) ? '' : 'collapsed' ?>" data-bs-toggle="collapse" href="#collapseSettings" role="button" aria-expanded="<?= in_array($activeNav, ['outlets']) ? 'true' : 'false' ?>" aria-controls="collapseSettings">
<div class="d-flex justify-content-between align-items-center w-100">
<span><i class="bi bi-gear"></i> <?= h(tr('الإعدادات', 'Settings')) ?></span>
<i class="bi bi-chevron-down toggle-icon" style="transition: transform 0.2s;"></i>
</div>
</a>
<div class="collapse <?= in_array($activeNav, ['outlets']) ? 'show' : '' ?>" id="collapseSettings" data-bs-parent="#sidebar-navigation">
<div class="list-group list-group-flush" style="background-color: rgba(0,0,0,0.15);">
<a class="list-group-item list-group-item-action" href="#" data-bs-toggle="modal" data-bs-target="#settingsModal" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('إعدادات الشركة', 'App Settings')) ?>
</a>
<a class="list-group-item list-group-item-action <?= $activeNav === 'outlets' ? 'active' : '' ?>" href="<?= h(url_for('outlets.php')) ?>" style="padding-left: 2.5rem; padding-right: 2.5rem;">
<i class="bi bi-dot"></i> <?= h(tr('الفروع (المنافذ)', 'Outlets')) ?>
</a>
</div>
</div>
<?php endif; ?>
</div>
</div>
<!-- /#sidebar-wrapper -->
<!-- Page Content -->
<div id="page-content-wrapper" class="d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom top-navbar px-3">
<div class="d-flex align-items-center justify-content-between w-100">
<div class="d-flex align-items-center">
<button class="btn btn-outline-secondary me-2" id="menu-toggle"><i class="bi bi-list"></i></button>
<h4 class="mb-0 ms-2 fw-semibold d-none d-md-block"><?= h($pageTitle) ?></h4>
</div>
<div class="d-flex align-items-center gap-3">
<a href="shop.php" target="_blank" class="btn btn-outline-success btn-sm me-2" title="<?= h(tr('زيارة المتجر', 'Visit Store')) ?>"><i class="bi bi-shop"></i> <span class="d-none d-md-inline"><?= h(tr('المتجر', 'Store')) ?></span></a>
<div class="language-switcher btn-group" role="group">
<a class="btn btn-sm <?= current_lang() === 'ar' ? 'btn-primary' : 'btn-outline-primary' ?>" href="<?= h(url_for(basename($_SERVER['PHP_SELF']), array_merge($_GET, ['lang' => 'ar']))) ?>">AR</a>
<a class="btn btn-sm <?= current_lang() === 'en' ? 'btn-primary' : 'btn-outline-primary' ?>" href="<?= h(url_for(basename($_SERVER['PHP_SELF']), array_merge($_GET, ['lang' => 'en']))) ?>">EN</a>
</div>
<div class="dropdown">
<button class="btn btn-light dropdown-toggle border" type="button" id="userMenu" data-bs-toggle="dropdown" aria-expanded="false">
<?php if (!empty($user['avatar'])): ?><img src="<?= h($user['avatar']) ?>" alt="Avatar" class="rounded-circle me-1" style="width: 24px; height: 24px; object-fit: cover;"><?php else: ?><i class="bi bi-person-circle"></i> <?php endif; ?><?= h(current_lang() === 'ar' ? $user['name_ar'] : $user['name_en']) ?>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow" aria-labelledby="userMenu">
<li><a class="dropdown-item" href="<?= h(url_for('profile.php')) ?>"><i class="bi bi-person me-2 text-primary"></i> <?= h(tr('الملف الشخصي', 'Profile')) ?></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="<?= h(url_for('logout.php')) ?>"><i class="bi bi-box-arrow-right text-danger me-2"></i> <?= h(tr('تسجيل الخروج', 'Sign out')) ?></a></li>
</ul>
</div>
</div>
</div>
</nav>
<div class="container-fluid p-4">
<!-- Flash messages integration using SweetAlert2 -->
<?php if ($flash): ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
Swal.fire({
icon: '<?= h($flash['type']) === "danger" ? "error" : (h($flash['type']) === "warning" ? "warning" : "success") ?>',
text: '<?= h($flash['message']) ?>',
position: 'center',
confirmButtonText: '<?= h(tr("حسناً", "OK")) ?>',
confirmButtonColor: '#0d6efd'
});
});
</script>
<?php endif; ?>
<?php endif; ?>

25
includes/pexels.php Normal file
View File

@ -0,0 +1,25 @@
<?php
function pexels_key() {
$k = getenv('PEXELS_KEY');
return $k && strlen($k) > 0 ? $k : 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18';
}
function pexels_get($url) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [ 'Authorization: '. pexels_key() ],
CURLOPT_TIMEOUT => 15,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 200 && $code < 300 && $resp) return json_decode($resp, true);
return null;
}
function download_to($srcUrl, $destPath) {
$data = file_get_contents($srcUrl);
if ($data === false) return false;
if (!is_dir(dirname($destPath))) mkdir(dirname($destPath), 0775, true);
return file_put_contents($destPath, $data) !== false;
}

675
includes/purchase_form.php Normal file
View File

@ -0,0 +1,675 @@
<?php
require_once __DIR__ . '/app.php';
$user = require_permission('purchases', 'add');
$pageTitle = tr('فاتورة مشتريات جديدة', 'New Purchase');
$activeNav = 'new_purchase';
$error = '';
$catalog = catalog();
$allowedBranches = get_user_branches($user);
try {
$customers = $customers = [];
$suppliers = db()->query('SELECT id, name, phone FROM suppliers ORDER BY name ASC')->fetchAll();
} catch (Throwable $e) {
$customers = [];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$branchCode = trim((string) ($_POST['branch_code'] ?? ''));
$supplierName = trim((string) ($_POST['supplier_name'] ?? ''));
$paymentMethod = trim((string) ($_POST['payment_method'] ?? 'cash'));
$purchaseStatus = trim((string) ($_POST['purchase_status'] ?? 'completed'));
$notes = trim((string) ($_POST['notes'] ?? ''));
$cartJson = (string) ($_POST['cart_json'] ?? '[]');
$items = json_decode($cartJson, true);
if (!in_array($branchCode, $allowedBranches, true)) {
$error = tr('اختر فرعاً صالحاً لهذه الصلاحية.', 'Choose a valid branch for this role.');
} 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 invoice.');
} else {
$normalized = [];
$subtotal = 0.0;
$totalVat = 0.0;
$globalVat = (float) get_setting('vat_percentage', 5);
$itemCount = 0;
foreach ($items as $item) {
$sku = (string) ($item['sku'] ?? '');
$qty = (int) ($item['qty'] ?? 0);
if (!isset($catalog[$sku]) || $qty < 1) {
continue;
}
$product = $catalog[$sku];
$price = isset($item['price']) ? (float)$item['price'] : (float)($product['cost_price'] ?? $product['price']);
$lineTotal = $price * $qty;
$normalized[] = [
'sku' => $sku,
'name_ar' => $product['name_ar'],
'name_en' => $product['name_en'],
'qty' => $qty,
'price' => $price,
'line_total' => $lineTotal,
];
$subtotal += $lineTotal;
$itemVat = $lineTotal * ($globalVat / 100);
$totalVat += $itemVat;
$itemCount += $qty;
}
if ($normalized === []) {
$error = tr('الفاتورة غير صالحة بعد التحقق من الأصناف.', 'The invoice is invalid after product validation.');
} else {
$cashierName = current_lang() === 'ar' ? $user['name_ar'] : $user['name_en'];
$purchaseId = create_purchase([
'reference_no' => purchase_reference_code(),
'branch_code' => $branchCode,
'user_username' => $user['username'],
'user_name' => $cashierName,
'role_name' => $user['role'],
'supplier_name' => $supplierName !== '' ? $supplierName : null,
'items' => $normalized,
'item_count' => $itemCount,
'subtotal' => $subtotal,
'vat_amount' => $totalVat,
'total_amount' => $subtotal + $totalVat,
'status' => $purchaseStatus,
'notes' => $notes !== '' ? $notes : null,
]);
set_flash('success', tr('تم حفظ فاتورة المشتريات بنجاح. وتم تحديث المخزون.', 'Purchase invoice saved successfully and stock updated.'));
redirect_to('purchases.php');
}
}
}
require __DIR__ . '/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="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 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('المورد', 'Supplier')) ?></label>
<div class="input-group">
<input type="text" id="formSupplier" name="supplier_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="openNewSupplierModal()" title="<?= h(tr('إضافة مورد', 'Add Supplier')) ?>">
<i class="bi bi-person-plus-fill"></i>
</button>
</div>
<div id="formSupplierDropdown" 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="purchase_status">
<option value="completed"><?= h(tr('مكتمل (تم الاستلام)', 'Completed (Received)')) ?></option>
<option value="order"><?= h(tr('طلب شراء (قيد الانتظار)', 'Purchase Order (Pending)')) ?></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('الضريبة (' . get_setting('vat_percentage', 5) . '%)', 'VAT (' . get_setting('vat_percentage', 5) . '%)')) ?></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 Invoice')) ?>
</button>
</div>
</div>
</div>
</div>
</form>
</div>
<!-- New Customer Modal -->
<div class="modal fade" id="newSupplierModal" 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 Supplier')) ?></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>
<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 = {};
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 suppliersData = <?= json_encode($suppliers, JSON_UNESCAPED_UNICODE) ?>;
const custInput = document.getElementById('formSupplier');
const custDropdown = document.getElementById('formSupplierDropdown');
custInput.addEventListener('input', function() {
const q = this.value.toLowerCase().trim();
custDropdown.innerHTML = '';
if (q.length < 2) {
custDropdown.classList.remove('show');
return;
}
const matches = suppliersData.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 newSupplierModalObj = null;
function openNewSupplierModal() {
if (!newSupplierModalObj) {
newSupplierModalObj = new bootstrap.Modal(document.getElementById('newSupplierModal'));
}
document.getElementById('ncName').value = '';
document.getElementById('ncPhone').value = '';
newSupplierModalObj.show();
}
async function saveNewCustomer() {
const name = document.getElementById('ncName').value.trim();
const phone = document.getElementById('ncPhone').value.trim();
if (!name) {
Swal.fire({icon: 'warning', text: '<?= h(tr('الاسم مطلوب', 'Name is required')) ?>'});
return;
}
const formData = new FormData();
formData.append('name', name);
formData.append('phone', phone);
try {
const res = await fetch('api/suppliers.php', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.success) {
suppliersData.push(data.supplier);
custInput.value = data.supplier.name + (data.supplier.phone ? ' - ' + data.supplier.phone : '');
newSupplierModalObj.hide();
const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 2000 });
Toast.fire({ icon: 'success', title: '<?= h(tr('تم إضافة العميل', 'Customer added')) ?>' });
} else {
Swal.fire({icon: 'warning', text: data.error});
}
} catch(err) {
Swal.fire({icon: 'warning', text: '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.cost_price || 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.cost_price || item.price || 0),
qty: 1
};
}
renderInvoice();
}
function changePrice(sku, newPrice) {
const price = parseFloat(newPrice);
if (!isNaN(price) && price >= 0) {
invoiceItems[sku].price = price;
}
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 globalVat = <?= get_setting('vat_percentage', 5) ?>;
const cartData = [];
skus.forEach(sku => {
const item = invoiceItems[sku];
const lineTotal = item.qty * item.price;
totalAmount += lineTotal;
totalVat += lineTotal * (globalVat / 100);
cartData.push({ sku: item.sku, qty: item.qty, price: item.price });
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 align-middle">
<input type="number" step="0.001" class="qty-control mx-auto fw-medium" min="0" value="${item.price.toFixed(3)}" onchange="changePrice('${sku}', this.value)" onkeyup="if(event.key==='Enter') changePrice('${sku}', this.value)">
</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;
}
// 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();
Swal.fire({icon: 'warning', text: '<?= h(tr('الرجاء إضافة أصناف للفاتورة أولاً.', 'Please add items to the invoice first.')) ?>'});
}
});
</script>
<?php require __DIR__ . '/footer.php'; ?>

1017
includes/sale_form.php Normal file

File diff suppressed because it is too large Load Diff

608
index.php
View File

@ -1,150 +1,470 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once __DIR__ . '/includes/app.php';
$user = require_auth();
$pageTitle = tr('لوحة التحكم', 'Dashboard');
$activeNav = 'dashboard';
$dbError = null;
$metrics = ['today_sales' => 0, 'today_revenue' => 0.0, 'pos_count' => 0, 'normal_count' => 0, 'recent' => []];
$reportMetrics = ['branch_totals' => [], 'payment_totals' => [], 'product_totals' => []];
try {
$metrics = dashboard_metrics();
$reportMetrics = report_metrics();
} catch (Throwable $e) {
$dbError = $e->getMessage();
}
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
$branchLabels = [];
$branchData = [];
foreach ($reportMetrics['branch_totals'] as $code => $total) {
$branchLabels[] = branch_label((string)$code);
$branchData[] = (float)$total;
}
$productLabels = [];
$productData = [];
$topProducts = array_slice($reportMetrics['product_totals'], 0, 5, true);
foreach ($topProducts as $sku => $qty) {
$productLabels[] = product_label((string)$sku);
$productData[] = (int)$qty;
}
$monthlyLabels = [];
$monthlyData = [];
if (isset($reportMetrics['monthly_totals'])) {
foreach ($reportMetrics['monthly_totals'] as $month => $total) {
// format month: "2026-04" -> "Apr 2026"
$time = strtotime($month . '-01');
$monthlyLabels[] = date('M Y', $time);
$monthlyData[] = (float)$total;
}
}
require __DIR__ . '/includes/header.php';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php if ($dbError): ?>
<div class="alert alert-danger mb-4"><i class="bi bi-exclamation-triangle me-2"></i><?= h(tr('تعذر تحميل بيانات قاعدة البيانات حالياً:', 'Database data could not be loaded right now:')) ?> <?= h($dbError) ?></div>
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
<!-- Metrics Row -->
<div class="row g-4 mb-4">
<div class="col-sm-6 col-xl-3">
<div class="card h-100 border-0 bg-primary text-white shadow-sm">
<div class="card-body d-flex flex-column justify-content-between">
<div class="d-flex justify-content-between align-items-start mb-3">
<h6 class="text-white-50 text-uppercase mb-0 fw-bold"><?= h(tr('مبيعات اليوم', 'Today sales')) ?></h6>
<div class="p-2 bg-white bg-opacity-25 rounded"><i class="bi bi-receipt fs-5 text-white"></i></div>
</div>
<h2 class="display-6 fw-bold mb-0"><?= h((string) $metrics['today_sales']) ?></h2>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</body>
</html>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card h-100 border-0 bg-success text-white shadow-sm">
<div class="card-body d-flex flex-column justify-content-between">
<div class="d-flex justify-content-between align-items-start mb-3">
<h6 class="text-white-50 text-uppercase mb-0 fw-bold"><?= h(tr('إيراد اليوم', 'Today revenue')) ?></h6>
<div class="p-2 bg-white bg-opacity-25 rounded"><i class="bi bi-cash-stack fs-5 text-white"></i></div>
</div>
<h2 class="display-6 fw-bold mb-0"><?= h(currency((float) $metrics['today_revenue'])) ?></h2>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card h-100 border-0 bg-info text-white shadow-sm">
<div class="card-body d-flex flex-column justify-content-between">
<div class="d-flex justify-content-between align-items-start mb-3">
<h6 class="text-white-50 text-uppercase mb-0 fw-bold">POS</h6>
<div class="p-2 bg-white bg-opacity-25 rounded"><i class="bi bi-cart fs-5 text-white"></i></div>
</div>
<h2 class="display-6 fw-bold mb-0"><?= h((string) $metrics['pos_count']) ?></h2>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card h-100 border-0 bg-dark text-white shadow-sm">
<div class="card-body d-flex flex-column justify-content-between">
<div class="d-flex justify-content-between align-items-start mb-3">
<h6 class="text-white-50 text-uppercase mb-0 fw-bold"><?= h(tr('فاتورة', 'Invoice')) ?></h6>
<div class="p-2 bg-white bg-opacity-25 rounded"><i class="bi bi-basket fs-5 text-white"></i></div>
</div>
<h2 class="display-6 fw-bold mb-0"><?= h((string) $metrics['normal_count']) ?></h2>
</div>
</div>
</div>
</div>
<!-- Charts Row -->
<div class="row g-4 mb-4">
<div class="col-xl-6">
<div class="card h-100">
<div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-semibold"><i class="bi bi-bar-chart-line me-2 text-primary"></i><?= h(tr('أفضل الأصناف مبيعاً', 'Top Selling Products')) ?></h5>
</div>
<div class="card-body">
<?php if(empty($productLabels)): ?>
<div class="text-center text-muted py-5">
<i class="bi bi-graph-down text-secondary fs-1 d-block mb-3"></i>
<p><?= h(tr('لا توجد بيانات كافية لعرض الرسم البياني', 'Not enough data to show chart')) ?></p>
</div>
<?php else: ?>
<div style="position: relative; height:300px; width:100%">
<canvas id="topProductsChart"></canvas>
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-xl-6">
<div class="card h-100">
<div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-semibold"><i class="bi bi-pie-chart me-2 text-success"></i><?= h(tr('المبيعات حسب الفرع', 'Sales by Branch')) ?></h5>
</div>
<div class="card-body d-flex justify-content-center align-items-center">
<?php if(empty($branchLabels)): ?>
<div class="text-center text-muted py-5">
<i class="bi bi-pie-chart text-secondary fs-1 d-block mb-3"></i>
<p><?= h(tr('لا توجد بيانات كافية لعرض الرسم البياني', 'Not enough data to show chart')) ?></p>
</div>
<?php else: ?>
<div style="position: relative; height:300px; width:100%">
<canvas id="branchSalesChart"></canvas>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<!-- Recent Sales -->
<div class="col-xl-8">
<div class="card h-100">
<div class="card-header bg-white d-flex justify-content-between align-items-center py-3 border-0">
<h5 class="mb-0 fw-semibold"><i class="bi bi-clock-history me-2 text-info"></i><?= h(tr('آخر المبيعات', 'Latest sales')) ?></h5>
<a class="btn btn-sm btn-outline-primary rounded-pill px-3" href="<?= h(url_for('sales.php')) ?>"><?= h(tr('عرض الكل', 'View all')) ?></a>
</div>
<div class="card-body p-0">
<?php if (!$metrics['recent']): ?>
<div class="text-center p-5 text-muted">
<i class="bi bi-inbox fs-1 mb-3 d-block text-secondary"></i>
<h5><?= h(tr('لا توجد مبيعات بعد', 'No sales yet')) ?></h5>
<p class="mb-3"><?= h(tr('ابدأ بأول فاتورة من صفحة نقاط البيع.', 'Start your first receipt from the POS page.')) ?></p>
<a class="btn btn-primary" href="<?= h(url_for('pos.php')) ?>"><?= h(tr('إنشاء بيع POS', 'Create POS sale')) ?></a>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 text-center">
<thead class="bg-light text-muted">
<tr>
<th class="ps-3 border-0 py-3 fw-semibold"><?= h(tr('رقم الإيصال', 'Receipt No')) ?></th>
<th class="border-0 py-3 fw-semibold"><?= h(tr('الفرع', 'Branch')) ?></th>
<th class="border-0 py-3 fw-semibold"><?= h(tr('النوع', 'Type')) ?></th>
<th class="border-0 py-3 fw-semibold"><?= h(tr('التاريخ', 'Date')) ?></th>
<th class="text-end pe-3 border-0 py-3 fw-semibold"><?= h(tr('الإجمالي', 'Total')) ?></th>
</tr>
</thead>
<tbody class="border-top-0">
<?php foreach ($metrics['recent'] as $sale): ?>
<tr>
<td class="ps-3">
<a href="<?= h(url_for('sale.php', ['id' => $sale['id']])) ?>" class="fw-bold text-decoration-none text-primary">
#<?= h($sale['receipt_no']) ?>
</a>
</td>
<td>
<span class="badge bg-light text-dark border"><?= h(branch_label((string) $sale['branch_code'])) ?></span>
</td>
<td>
<?php if($sale['sale_mode'] === 'pos'): ?>
<span class="badge bg-info bg-opacity-10 text-info border border-info"><?= h(sale_mode_label((string) $sale['sale_mode'])) ?></span>
<?php else: ?>
<span class="badge bg-warning bg-opacity-10 text-warning border border-warning"><?= h(sale_mode_label((string) $sale['sale_mode'])) ?></span>
<?php endif; ?>
</td>
<td><small class="text-muted fw-medium"><?= h(date('M d, H:i', strtotime((string) $sale['sale_date']))) ?></small></td>
<td class="text-end pe-3 fw-bold text-success"><?= h(currency((float) $sale['total_amount'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- Quick Actions / Status -->
<div class="col-xl-4">
<div class="card h-100">
<div class="card-header bg-white py-3 border-0">
<h5 class="mb-0 fw-semibold"><i class="bi bi-lightning-charge me-2 text-warning"></i><?= h(tr('إجراءات سريعة', 'Quick Actions')) ?></h5>
</div>
<div class="card-body">
<div class="d-grid gap-3">
<a href="<?= h(url_for('pos.php')) ?>" class="btn btn-primary btn-lg d-flex align-items-center justify-content-between" style="border-radius: 10px; box-shadow: 0 4px 6px rgba(13, 110, 253, 0.2);">
<span><i class="bi bi-cart-plus me-2"></i> <?= h(tr('نقطة بيع جديدة', 'New POS Sale')) ?></span>
<i class="bi bi-chevron-right"></i>
</a>
<a href="<?= h(url_for('normal_sale.php')) ?>" class="btn btn-outline-primary btn-lg d-flex align-items-center justify-content-between" style="border-radius: 10px;">
<span><i class="bi bi-receipt me-2"></i> <?= h(tr('فاتورة', 'Invoice')) ?></span>
<i class="bi bi-chevron-right"></i>
</a>
<button type="button" class="btn btn-light btn-lg d-flex align-items-center justify-content-between border" data-bs-toggle="modal" data-bs-target="#quickNoteModal" style="border-radius: 10px;">
<span class="text-dark"><i class="bi bi-pencil-square me-2"></i> <?= h(tr('ملاحظة سريعة', 'Quick Note')) ?></span>
<i class="bi bi-chevron-right text-dark"></i>
</button>
</div>
<div class="mt-4 pt-4 border-top">
<?php if ($user): ?>
<h6 class="text-muted text-uppercase small fw-bold mb-3"><?= h(tr('معلومات الحساب', 'Account Info')) ?></h6>
<div class="d-flex align-items-center mb-2 p-3 bg-light rounded-3 border">
<div class="bg-white p-2 rounded-circle shadow-sm me-3">
<i class="bi bi-person-badge fs-4 text-primary"></i>
</div>
<div>
<p class="mb-0 fw-bold"><?= h(current_lang() === 'ar' ? $user['name_ar'] : $user['name_en']) ?></p>
<small class="text-muted fw-medium"><?= h(role_label($user['role'])) ?> &bull; <?= h(branch_label($user['branch_code'])) ?></small>
</div>
</div>
<?php else: ?>
<div class="text-center p-3 bg-light rounded-3 border">
<p class="text-muted mb-3 small"><?= h(tr('جرّب المالك أو مدير الفرع أو الكاشير لرؤية اختلاف الصلاحيات.', 'Try owner, branch manager, or cashier to see different permissions.')) ?></p>
<a class="btn btn-dark w-100 rounded-pill" href="<?= h(url_for('login.php')) ?>"><?= h(tr('تسجيل الدخول للبدء', 'Sign in to start')) ?></a>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<!-- Monthly Sales Chart Row -->
<div class="row g-4 mb-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-semibold"><i class="bi bi-graph-up me-2 text-primary"></i><?= h(tr('مبيعات الأشهر', 'Monthly Sales')) ?></h5>
</div>
<div class="card-body">
<?php if(empty($monthlyLabels)): ?>
<div class="text-center text-muted py-5">
<i class="bi bi-graph-up text-secondary fs-1 d-block mb-3"></i>
<p><?= h(tr('لا توجد بيانات كافية لعرض الرسم البياني', 'Not enough data to show chart')) ?></p>
</div>
<?php else: ?>
<div style="position: relative; height:350px; width:100%">
<canvas id="monthlySalesChart"></canvas>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<!-- Modal Form Example -->
<div class="modal fade" id="quickNoteModal" tabindex="-1" aria-labelledby="quickNoteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="border-radius: 16px; border: 0; box-shadow: 0 10px 30px rgba(0,0,0,0.1);">
<form onsubmit="handleQuickNote(event)">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold" id="quickNoteModalLabel"><i class="bi bi-journal-text me-2 text-primary"></i><?= h(tr('إضافة ملاحظة سريعة', 'Add Quick Note')) ?></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 for="noteTitle" class="form-label fw-medium"><?= h(tr('العنوان', 'Title')) ?></label>
<input type="text" class="form-control form-control-lg bg-light border-0" id="noteTitle" required>
</div>
<div class="mb-3">
<label for="noteContent" class="form-label fw-medium"><?= h(tr('التفاصيل', 'Details')) ?></label>
<textarea class="form-control bg-light border-0" id="noteContent" rows="4" required></textarea>
</div>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-light px-4" data-bs-dismiss="modal"><?= h(tr('إلغاء', 'Cancel')) ?></button>
<button type="submit" class="btn btn-primary px-4 shadow-sm"><?= h(tr('حفظ الملاحظة', 'Save Note')) ?></button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
<?php if(!empty($branchLabels)): ?>
const branchCtx = document.getElementById('branchSalesChart').getContext('2d');
new Chart(branchCtx, {
type: 'doughnut',
data: {
labels: <?= json_encode($branchLabels) ?>,
datasets: [{
data: <?= json_encode($branchData) ?>,
backgroundColor: [
'rgba(13, 110, 253, 0.88)',
'rgba(25, 135, 84, 0.88)',
'rgba(13, 202, 240, 0.88)',
'rgba(255, 193, 7, 0.88)',
'rgba(108, 117, 125, 0.82)'
],
borderWidth: 2,
borderColor: '#ffffff',
hoverOffset: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '65%',
plugins: {
legend: { position: 'bottom', labels: { color: '#667085', usePointStyle: true, padding: 20 } },
tooltip: {
backgroundColor: 'rgba(28, 36, 52, 0.94)',
padding: 12,
callbacks: {
label: function(context) {
let value = context.raw;
return ' ' + value.toFixed(3) + ' <?= tr("ر.ع", "OMR") ?>';
}
}
}
}
}
});
<?php endif; ?>
<?php if(!empty($productLabels)): ?>
const productsCtx = document.getElementById('topProductsChart').getContext('2d');
// Create gradient for bars
let gradient = productsCtx.createLinearGradient(0, 0, 0, 300);
gradient.addColorStop(0, 'rgba(13, 110, 253, 0.92)');
gradient.addColorStop(1, 'rgba(13, 202, 240, 0.18)');
new Chart(productsCtx, {
type: 'bar',
data: {
labels: <?= json_encode($productLabels) ?>,
datasets: [{
label: '<?= h(tr("الكمية المباعة", "Qty Sold")) ?>',
data: <?= json_encode($productData) ?>,
backgroundColor: gradient,
borderColor: '#0d6efd',
borderWidth: 1,
borderRadius: 6,
barPercentage: 0.6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(28, 36, 52, 0.94)',
padding: 12
}
},
scales: {
y: {
beginAtZero: true,
ticks: { precision: 0 },
grid: { borderDash: [5, 5], color: 'rgba(148, 163, 184, 0.14)' },
ticks: { color: '#667085' }
},
x: {
grid: { display: false },
ticks: { color: '#98A2B3' }
}
}
}
});
<?php endif; ?>
<?php if(!empty($monthlyLabels)): ?>
const monthlyCtx = document.getElementById('monthlySalesChart').getContext('2d');
// Create gradient for line chart area
let gradientMonthly = monthlyCtx.createLinearGradient(0, 0, 0, 350);
gradientMonthly.addColorStop(0, 'rgba(13, 110, 253, 0.26)');
gradientMonthly.addColorStop(1, 'rgba(13, 110, 253, 0.05)');
new Chart(monthlyCtx, {
type: 'line',
data: {
labels: <?= json_encode($monthlyLabels) ?>,
datasets: [{
label: '<?= h(tr("إجمالي المبيعات", "Total Sales")) ?>',
data: <?= json_encode($monthlyData) ?>,
backgroundColor: gradientMonthly,
borderColor: '#0d6efd',
borderWidth: 3,
pointBackgroundColor: '#ffffff',
pointBorderColor: '#0d6efd',
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(28, 36, 52, 0.94)',
padding: 12,
callbacks: {
label: function(context) {
let value = context.raw;
return ' ' + value.toFixed(3) + ' <?= tr("ر.ع", "OMR") ?>';
}
}
}
},
scales: {
y: {
beginAtZero: true,
grid: { borderDash: [5, 5], color: 'rgba(148, 163, 184, 0.14)' },
ticks: { color: '#667085' }
},
x: {
grid: { display: false },
ticks: { color: '#98A2B3' }
}
}
}
});
<?php endif; ?>
});
function handleQuickNote(e) {
e.preventDefault();
var modalEl = document.getElementById('quickNoteModal');
var modal = bootstrap.Modal.getInstance(modalEl);
modal.hide();
Swal.fire({
title: '<?= h(tr('تم الحفظ!', 'Saved!')) ?>',
text: '<?= h(tr('تم حفظ الملاحظة بنجاح.', 'Note saved successfully.')) ?>',
icon: 'success',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000
});
e.target.reset();
}
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>

226
login.php Normal file
View File

@ -0,0 +1,226 @@
<?php
require_once __DIR__ . '/includes/app.php';
if (current_user()) {
redirect_to('index.php');
}
$error = '';
$flash = pull_flash();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['action']) && $_POST['action'] === 'reset_password') {
$reset_username = trim((string) ($_POST['reset_username'] ?? ''));
if ($reset_username !== '') {
// Mock sending reset link
set_flash('success', tr('تم إرسال رابط إعادة تعيين كلمة المرور إلى بريدك الإلكتروني (تجريبي).', 'Password reset link has been sent to your email (Demo).'));
}
redirect_to('login.php');
}
$username = trim((string) ($_POST['username'] ?? ''));
$password = trim((string) ($_POST['password'] ?? ''));
if ($username === '' || $password === '') {
$error = tr('أدخل اسم المستخدم وكلمة المرور.', 'Enter username and password.');
} elseif (!login_attempt($username, $password)) {
$error = tr('بيانات الدخول غير صحيحة. استخدم أحد الحسابات التجريبية بالأسفل.', 'Invalid credentials. Use one of the demo accounts below.');
} else {
set_flash('success', tr('تم تسجيل الدخول بنجاح.', 'Signed in successfully.'));
redirect_to('index.php');
}
}
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
$projectName = $_SERVER['PROJECT_NAME'] ?? app_name();
$assetVersion = date('YmdHi');
$accounts = [];
?>
<!doctype html>
<html lang="<?= h(current_lang()) ?>" dir="<?= is_rtl() ? 'rtl' : 'ltr' ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= h(tr('تسجيل الدخول', 'Sign in')) ?> · <?= h($projectName) ?></title>
<?php if ($projectDescription): ?>
<meta name="description" content='<?= h($projectDescription) ?>' />
<meta property="og:description" content="<?= h($projectDescription) ?>" />
<meta property="twitter:description" content="<?= h($projectDescription) ?>" />
<?php else: ?>
<meta name="description" content="<?= h(tr('تسجيل الدخول إلى مساحة المبيعات.', 'Sign in to the sales workspace.')) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= h($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>" />
<?php endif; ?>
<?php if (get_setting('company_favicon')): ?>
<link rel="icon" href="<?= h(get_setting('company_favicon')) ?>">
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="assets/css/custom.css?v=<?= h($assetVersion) ?>">
<style>
body {
background-color: #f8f9fa;
display: flex;
align-items: center;
min-height: 100vh;
padding: 2rem 0;
}
.auth-card {
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0,0,0,0.08);
background: #fff;
overflow: hidden;
border: none;
}
.auth-sidebar {
background: linear-gradient(135deg, #0d6efd 0%, #0a58ca 100%);
color: white;
padding: 3rem;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.auth-form-container {
padding: 4rem 3rem;
}
.company-logo {
max-height: 90px;
border-radius: 12px;
margin-bottom: 1.5rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
padding: 10px;
background: #fff;
}
.demo-account {
border-radius: 12px;
transition: all 0.2s;
}
.demo-account:hover {
background-color: #f8f9fa;
transform: translateY(-2px);
border-color: #dee2e6 !important;
}
.stat-chip {
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
}
</style>
</head>
<body>
<main class="container">
<div class="row justify-content-center">
<div class="col-xl-10">
<div class="card auth-card">
<div class="row g-0 align-items-stretch">
<div class="col-lg-5 auth-sidebar d-none d-lg-flex">
<div>
<div class="eyebrow text-white-50 mb-3"><?= h(tr('مرحباً بك مجدداً', 'Welcome Back')) ?></div>
<h2 class="display-6 fw-bold mb-4"><?= h(current_lang() === 'ar' ? get_setting('company_name_ar', 'نظام إدارة') : get_setting('company_name_en', 'Management System')) ?></h2>
<p class="lead opacity-75"><?= h(tr('نظام متكامل لتسجيل المبيعات، وإدارة المخزون، والتقارير في واجهة واحدة.', 'Integrated system for logging sales, managing inventory, and reports in one interface.')) ?></p>
</div>
<div class="mt-5 pt-5 border-top border-light border-opacity-25 text-white-50 small">
&copy; <?= date('Y') ?> <?= h($projectName) ?>
</div>
</div>
<div class="col-lg-7 auth-form-container">
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="language-switcher">
<a class="btn btn-sm <?= current_lang() === 'ar' ? 'btn-primary' : 'btn-light text-dark' ?> rounded-pill px-3" href="<?= h(url_for('login.php', ['lang' => 'ar'])) ?>">AR</a>
<a class="btn btn-sm <?= current_lang() === 'en' ? 'btn-primary' : 'btn-light text-dark' ?> rounded-pill px-3" href="<?= h(url_for('login.php', ['lang' => 'en'])) ?>">EN</a>
</div>
</div>
<div class="text-center mb-5">
<?php if (get_setting('company_logo')): ?>
<img src="<?= h(get_setting('company_logo')) ?>" alt="Logo" class="company-logo">
<?php else: ?>
<div class="company-logo mx-auto bg-primary bg-opacity-10 text-primary d-flex align-items-center justify-content-center" style="width: 90px; height: 90px; font-size: 2.5rem; font-weight: bold;">
<?= h((current_lang() === 'ar' ? 'ن' : 'S')) ?>
</div>
<?php endif; ?>
<h3 class="fw-bold mb-1"><?= h(tr('تسجيل الدخول', 'Sign in to your account')) ?></h3>
<p class="text-muted"><?= h(tr('أدخل بيانات الاعتماد الخاصة بك للوصول', 'Enter your credentials to access your account')) ?></p>
</div>
<?php if ($flash): ?>
<div class="alert alert-<?= h($flash['type'] === 'error' ? 'danger' : $flash['type']) ?> alert-dismissible fade show rounded-3" role="alert">
<?= h($flash['message']) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if ($error !== ''): ?>
<div class="alert alert-danger alert-dismissible fade show rounded-3" role="alert">
<?= h($error) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<form method="post" class="mb-5">
<div class="mb-3">
<label class="form-label fw-semibold" for="username"><?= h(tr('اسم المستخدم', 'Username')) ?></label>
<input id="username" name="username" class="form-control form-control-lg rounded-3 border border-secondary-subtle" autocomplete="username" placeholder="<?= h(tr('اسم المستخدم', 'Username')) ?>" value="<?= h(array_values($accounts)[0]['username'] ?? '') ?>" required>
</div>
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-1">
<label class="form-label fw-semibold mb-0" for="password"><?= h(tr('كلمة المرور', 'Password')) ?></label>
<a href="#" class="text-decoration-none small text-primary fw-medium" data-bs-toggle="modal" data-bs-target="#resetPasswordModal"><?= h(tr('نسيت كلمة المرور؟', 'Forgot password?')) ?></a>
</div>
<input id="password" name="password" type="password" class="form-control form-control-lg rounded-3 border border-secondary-subtle" autocomplete="current-password" placeholder="********" value="<?= h(array_values($accounts)[0]['password'] ?? '') ?>" required>
</div>
<button class="btn btn-primary btn-lg w-100 rounded-3 shadow-sm fw-semibold" type="submit"><?= h(tr('دخول', 'Sign in')) ?></button>
</form>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Reset Password Modal -->
<div class="modal fade" id="resetPasswordModal" tabindex="-1" aria-labelledby="resetPasswordModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header border-bottom-0 pb-0 pt-4 px-4">
<h5 class="modal-title fw-bold" id="resetPasswordModalLabel"><?= h(tr('استعادة كلمة المرور', 'Reset Password')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post">
<input type="hidden" name="action" value="reset_password">
<div class="modal-body px-4 py-4">
<p class="text-muted mb-4"><?= h(tr('أدخل اسم المستخدم أو البريد الإلكتروني وسنقوم بإرسال رابط لإعادة تعيين كلمة المرور الخاصة بك.', 'Enter your username or email and we will send you a link to reset your password.')) ?></p>
<div class="mb-3">
<label for="reset_username" class="form-label fw-semibold"><?= h(tr('البريد الإلكتروني / اسم المستخدم', 'Email / Username')) ?></label>
<input type="text" class="form-control form-control-lg rounded-3 border border-secondary-subtle" id="reset_username" name="reset_username" placeholder="user@example.com" required>
</div>
</div>
<div class="modal-footer border-top-0 pt-0 pb-4 px-4">
<button type="button" class="btn btn-light rounded-3 px-4" data-bs-dismiss="modal"><?= h(tr('إلغاء', 'Cancel')) ?></button>
<button type="submit" class="btn btn-primary rounded-3 px-4 fw-semibold"><?= h(tr('إرسال الرابط', 'Send Link')) ?></button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.demo-account').forEach(btn => {
btn.addEventListener('click', () => {
document.getElementById('username').value = btn.dataset.username;
document.getElementById('password').value = btn.dataset.password;
btn.closest('form')?.submit() || document.querySelector('form').submit();
});
});
});
</script>
</body>
</html>

5
logout.php Normal file
View File

@ -0,0 +1,5 @@
<?php
require_once __DIR__ . '/includes/app.php';
logout_user();
set_flash('success', tr('تم تسجيل الخروج.', 'Signed out successfully.'));
redirect_to('login.php');

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

5
my_cookies.txt Normal file
View File

@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
127.0.0.1 FALSE / FALSE 0 PHPSESSID dkl8f65dp116jrva8sgf07ar29

5
new_cookies.txt Normal file
View File

@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
127.0.0.1 FALSE / FALSE 0 PHPSESSID i717431bue0iep7i5urn7atmi0

2
new_purchase.php Normal file
View File

@ -0,0 +1,2 @@
<?php
require_once __DIR__ . '/includes/purchase_form.php';

3
normal_sale.php Normal file
View File

@ -0,0 +1,3 @@
<?php
$saleMode = 'normal';
require_once __DIR__ . '/includes/sale_form.php';

312
online_orders.php Normal file
View File

@ -0,0 +1,312 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('online_orders', 'show');
$pageTitle = tr('طلبات المتجر', 'Online Orders');
$activeNav = 'online_orders';
$db = db();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if ($_POST['action'] === 'update_status') {
$id = (int)$_POST['id'];
$status = trim((string) ($_POST['status'] ?? 'pending')); // pending, accepted, completed, rejected
$allowedStatuses = ['pending', 'accepted', 'completed', 'rejected'];
if (!in_array($status, $allowedStatuses, true)) {
set_flash('danger', tr('حالة الطلب غير صالحة.', 'Invalid order status.'));
redirect_to('online_orders.php');
}
$beforeStmt = $db->prepare("SELECT * FROM online_orders WHERE id = ?");
$beforeStmt->execute([$id]);
$order = $beforeStmt->fetch(PDO::FETCH_ASSOC);
if ($order) {
$previousItems = json_decode((string) ($order['items_json'] ?? '[]'), true) ?: [];
$oldStatus = (string) ($order['status'] ?? 'pending');
$db->beginTransaction();
try {
sync_online_order_stock_reservation($previousItems, $oldStatus, $previousItems, $status);
$stmt = $db->prepare("UPDATE online_orders SET status = ? WHERE id = ?");
$stmt->execute([$status, $id]);
$db->commit();
} catch (Throwable $e) {
if ($db->inTransaction()) {
$db->rollBack();
}
throw $e;
}
}
if ($order && ($order['status'] ?? 'pending') !== $status) {
$order['status'] = $status;
if (($order['customer_phone'] ?? '') !== '' && wablas_is_configured()) {
wablas_notify_online_order($order, $status);
}
}
set_flash('success', tr('تم تحديث حالة الطلب', 'Order status updated'));
redirect_to('online_orders.php');
} elseif ($_POST['action'] === 'delete') {
$id = (int)$_POST['id'];
$beforeStmt = $db->prepare("SELECT * FROM online_orders WHERE id = ?");
$beforeStmt->execute([$id]);
$order = $beforeStmt->fetch(PDO::FETCH_ASSOC);
$db->beginTransaction();
try {
if ($order) {
$previousItems = json_decode((string) ($order['items_json'] ?? '[]'), true) ?: [];
sync_online_order_stock_reservation($previousItems, (string) ($order['status'] ?? 'pending'), [], 'rejected');
}
$stmt = $db->prepare("DELETE FROM online_orders WHERE id = ?");
$stmt->execute([$id]);
$db->commit();
} catch (Throwable $e) {
if ($db->inTransaction()) {
$db->rollBack();
}
throw $e;
}
set_flash('success', tr('تم حذف الطلب بنجاح', 'Order deleted successfully'));
redirect_to('online_orders.php');
}
}
$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 ? OR CONCAT('968', customer_phone) LIKE ?)";
$params[] = "%$search%";
$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';
?>
<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">
<thead class="table-light text-muted">
<tr>
<th class="ps-4">#</th>
<th><?= h(tr('التاريخ', 'Date')) ?></th>
<th><?= h(tr('العميل', 'Customer')) ?></th>
<th><?= h(tr('الهاتف', 'Telephone')) ?></th>
<th><?= h(tr('العنوان', 'Address')) ?></th>
<th><?= h(tr('الدفع', 'Payment')) ?></th>
<th><?= h(tr('المبلغ', 'Amount')) ?></th>
<th><?= h(tr('الحالة', 'Status')) ?></th>
<th class="pe-4 text-end print-hide"><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody>
<?php if(empty($orders)): ?>
<tr><td colspan="9" class="text-center py-5 text-muted"><?= h(tr('لا توجد طلبات', 'No orders found')) ?></td></tr>
<?php else: ?>
<?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'); }
elseif ($o['status'] === 'accepted') { $statusClass = 'bg-primary'; $statusText = tr('مقبول', 'Accepted'); }
elseif ($o['status'] === 'completed') { $statusClass = 'bg-success'; $statusText = tr('مكتمل', 'Completed'); }
elseif ($o['status'] === 'rejected') { $statusClass = 'bg-danger'; $statusText = tr('مرفوض', 'Rejected'); }
$items = json_decode($o['items_json'], true) ?: [];
?>
<tr>
<td class="ps-4 fw-bold">#<?= h($o['id']) ?></td>
<td><?= h(date('Y-m-d H:i', strtotime($o['created_at']))) ?></td>
<td class="fw-bold"><?= h($o['customer_name']) ?></td>
<td dir="ltr"><?= h(phone_display($o['customer_phone'])) ?></td>
<td style="max-width: 200px;" class="text-truncate" title="<?= h($o['customer_address']) ?>"><?= h($o['customer_address']) ?></td>
<td>
<div class="small fw-semibold"><?= h(online_payment_method_label((string) ($o['payment_method'] ?? 'pay_later'))) ?></div>
<span class="badge <?= h(online_payment_status_badge_class((string) ($o['payment_status'] ?? 'unpaid'))) ?> mt-1"><?= h(online_payment_status_label((string) ($o['payment_status'] ?? 'unpaid'))) ?></span>
</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 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"],
"payment_method" => online_payment_method_label((string) ($o["payment_method"] ?? 'pay_later')),
"payment_status" => online_payment_status_label((string) ($o["payment_status"] ?? 'unpaid')),
"subtotal" => $o["subtotal"] ?? 0, "vat" => $o["vat_amount"] ?? 0, "total" => $o["total_amount"],
"items" => $items
], 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">
<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>
<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="accepted"><button class="dropdown-item" type="submit"><?= h(tr('مقبول', 'Accepted')) ?></button></form></li>
<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="completed"><button class="dropdown-item text-success fw-bold" type="submit"><?= h(tr('مكتمل', 'Completed')) ?></button></form></li>
<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="confirmSwal(event, '<?= 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="6" 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 (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">
<h5 class="modal-title fw-bold" id="orderModalLabel"><?= h(tr('تفاصيل الطلب', 'Order Details')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<div class="bg-light p-3 rounded-3 mb-4">
<div class="row">
<div class="col-6 mb-2"><small class="text-muted d-block"><?= h(tr('العميل', 'Customer')) ?></small><strong id="vName"></strong></div>
<div class="col-6 mb-2"><small class="text-muted d-block"><?= h(tr('الهاتف', 'Phone')) ?></small><strong id="vPhone"></strong></div>
<div class="col-12"><small class="text-muted d-block"><?= h(tr('العنوان', 'Address')) ?></small><strong id="vAddress"></strong></div>
<div class="col-6 mt-2"><small class="text-muted d-block"><?= h(tr('طريقة الدفع', 'Payment Method')) ?></small><strong id="vPaymentMethod"></strong></div>
<div class="col-6 mt-2"><small class="text-muted d-block"><?= h(tr('حالة الدفع', 'Payment Status')) ?></small><strong id="vPaymentStatus"></strong></div>
</div>
</div>
<h6 class="fw-bold border-bottom pb-2 mb-3"><?= h(tr('المنتجات', 'Products')) ?></h6>
<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>
</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('إغلاق', 'Close')) ?></button>
</div>
</div>
</div>
</div>
<script>
function viewOrder(order) {
const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('orderModal'));
document.getElementById('vName').innerText = order.name;
document.getElementById('vPhone').innerText = order.phone ? String(order.phone).replace(/^((00968)|(968))/, '') : '';
document.getElementById('vAddress').innerText = order.address;
document.getElementById('vPaymentMethod').innerText = order.payment_method || '';
document.getElementById('vPaymentStatus').innerText = order.payment_status || '';
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 = '';
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;
modal.show();
}
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>

240
outlets.php Normal file
View File

@ -0,0 +1,240 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('settings', 'show');
$pageTitle = tr('الفروع (المنافذ)', 'Outlets');
$activeNav = 'outlets';
$pdo = db();
// Ensure branches table exists
$pdo->exec("CREATE TABLE IF NOT EXISTS branches (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(50) UNIQUE NOT NULL,
name_ar VARCHAR(100) NOT NULL,
name_en VARCHAR(100) NOT NULL,
city_ar VARCHAR(100),
city_en VARCHAR(100),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)");
// Check if branches table is empty, if so, seed it
$stmt = $pdo->query("SELECT COUNT(*) FROM branches");
if ($stmt->fetchColumn() == 0) {
$defaultBranches = [
['code' => 'muscat', 'name_ar' => 'فرع مسقط', 'name_en' => 'Muscat Branch', 'city_ar' => 'مسقط', 'city_en' => 'Muscat'],
['code' => 'sohar', 'name_ar' => 'فرع صحار', 'name_en' => 'Sohar Branch', 'city_ar' => 'صحار', 'city_en' => 'Sohar'],
['code' => 'nizwa', 'name_ar' => 'فرع نزوى', 'name_en' => 'Nizwa Branch', 'city_ar' => 'نزوى', 'city_en' => 'Nizwa'],
];
$insertStmt = $pdo->prepare("INSERT IGNORE INTO branches (code, name_ar, name_en, city_ar, city_en) VALUES (?, ?, ?, ?, ?)");
foreach ($defaultBranches as $b) {
$insertStmt->execute([$b['code'], $b['name_ar'], $b['name_en'], $b['city_ar'], $b['city_en']]);
}
}
// Handle Form Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'create') {
$stmt = $pdo->prepare('INSERT INTO branches (code, name_ar, name_en, city_ar, city_en) VALUES (?, ?, ?, ?, ?)');
try {
$stmt->execute([$_POST['code'], $_POST['name_ar'], $_POST['name_en'], $_POST['city_ar'] ?? '', $_POST['city_en'] ?? '']);
set_flash('success', tr('تمت إضافة الفرع بنجاح', 'Outlet added successfully'));
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
set_flash('danger', tr('رمز الفرع موجود مسبقاً', 'Outlet code already exists'));
} else {
set_flash('danger', tr('حدث خطأ', 'An error occurred'));
}
}
redirect_to('outlets.php');
} elseif ($action === 'edit') {
$stmt = $pdo->prepare('UPDATE branches SET name_ar = ?, name_en = ?, city_ar = ?, city_en = ? WHERE code = ?');
$stmt->execute([$_POST['name_ar'], $_POST['name_en'], $_POST['city_ar'] ?? '', $_POST['city_en'] ?? '', $_POST['code']]);
set_flash('success', tr('تم التحديث بنجاح', 'Updated successfully'));
redirect_to('outlets.php');
} elseif ($action === 'delete') {
$stmt = $pdo->prepare('DELETE FROM branches WHERE code = ?');
$stmt->execute([$_POST['code']]);
set_flash('success', tr('تم الحذف بنجاح', 'Deleted successfully'));
redirect_to('outlets.php');
}
}
$branchesList = $pdo->query("SELECT * FROM branches ORDER BY id ASC")->fetchAll();
require __DIR__ . '/includes/header.php';
?>
<div class="card shadow-sm border-0 mb-4">
<div class="card-header bg-white border-bottom-0 pt-4 pb-0 d-flex justify-content-between align-items-center">
<h5 class="fw-bold text-primary mb-0"><i class="bi bi-shop me-2"></i> <?= h($pageTitle) ?></h5>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createModal">
<i class="bi bi-plus-lg"></i> <?= h(tr('إضافة فرع', 'Add Outlet')) ?>
</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th><?= h(tr('الرمز', 'Code')) ?></th>
<th><?= h(tr('الاسم (عربي)', 'Name (AR)')) ?></th>
<th><?= h(tr('الاسم (إنجليزي)', 'Name (EN)')) ?></th>
<th><?= h(tr('المدينة', 'City')) ?></th>
<th class="text-end"><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($branchesList as $b): ?>
<tr>
<td><span class="badge bg-secondary"><?= h($b['code']) ?></span></td>
<td><?= h($b['name_ar']) ?></td>
<td><?= h($b['name_en']) ?></td>
<td><?= h(current_lang() === 'ar' ? $b['city_ar'] : $b['city_en']) ?></td>
<td class="text-end">
<button class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#editModal<?= $b['id'] ?>">
<i class="bi bi-pencil"></i>
</button>
<?php if ($b['code'] !== 'muscat'): ?>
<button class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal<?= $b['id'] ?>">
<i class="bi bi-trash"></i>
</button>
<?php endif; ?>
</td>
</tr>
<!-- Edit Modal -->
<div class="modal fade" id="editModal<?= $b['id'] ?>" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<form method="post">
<div class="modal-header bg-light">
<h5 class="modal-title fw-bold"><?= h(tr('تعديل فرع', 'Edit Outlet')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-start">
<input type="hidden" name="action" value="edit">
<input type="hidden" name="code" value="<?= h($b['code']) ?>">
<div class="mb-3">
<label class="form-label"><?= h(tr('الرمز', 'Code')) ?></label>
<input type="text" class="form-control bg-light" value="<?= h($b['code']) ?>" disabled>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('الاسم (عربي)', 'Name (AR)')) ?></label>
<input type="text" name="name_ar" class="form-control" value="<?= h($b['name_ar']) ?>" required>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('الاسم (إنجليزي)', 'Name (EN)')) ?></label>
<input type="text" name="name_en" class="form-control" value="<?= h($b['name_en']) ?>" required>
</div>
<div class="row">
<div class="col-6 mb-3">
<label class="form-label"><?= h(tr('المدينة (عربي)', 'City (AR)')) ?></label>
<input type="text" name="city_ar" class="form-control" value="<?= h($b['city_ar']) ?>">
</div>
<div class="col-6 mb-3">
<label class="form-label"><?= h(tr('المدينة (إنجليزي)', 'City (EN)')) ?></label>
<input type="text" name="city_en" class="form-control" value="<?= h($b['city_en']) ?>">
</div>
</div>
</div>
<div class="modal-footer bg-light">
<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 Modal -->
<div class="modal fade" id="deleteModal<?= $b['id'] ?>" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<form method="post">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title fw-bold"><?= h(tr('تأكيد الحذف', 'Confirm Deletion')) ?></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4 text-center">
<i class="bi bi-exclamation-circle text-danger" style="font-size: 3rem;"></i>
<p class="mt-3 fs-5"><?= h(tr('هل أنت متأكد من حذف هذا الفرع؟', 'Are you sure you want to delete this outlet?')) ?></p>
<p class="text-muted fw-bold"><?= h($b['name_ar'] . ' / ' . $b['name_en']) ?></p>
<input type="hidden" name="action" value="delete">
<input type="hidden" name="code" value="<?= h($b['code']) ?>">
</div>
<div class="modal-footer bg-light justify-content-center">
<button type="button" class="btn btn-secondary px-4" data-bs-dismiss="modal"><?= h(tr('إلغاء', 'Cancel')) ?></button>
<button type="submit" class="btn btn-danger px-4"><?= h(tr('حذف', 'Delete')) ?></button>
</div>
</form>
</div>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($branchesList)): ?>
<tr>
<td colspan="5" class="text-center py-4 text-muted">
<i class="bi bi-inbox fs-2 d-block mb-2"></i>
<?= h(tr('لا توجد فروع', 'No outlets found')) ?>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Create Modal -->
<div class="modal fade" id="createModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<form method="post">
<div class="modal-header bg-light">
<h5 class="modal-title fw-bold"><?= h(tr('إضافة فرع جديد', 'Add New Outlet')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-start">
<input type="hidden" name="action" value="create">
<div class="mb-3">
<label class="form-label"><?= h(tr('الرمز (إنجليزي فقط بدون مسافات)', 'Code (English only, no spaces)')) ?></label>
<input type="text" name="code" class="form-control" placeholder="e.g. dubai" required pattern="[a-zA-Z0-9_-]+">
</div>
<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 class="row">
<div class="col-6 mb-3">
<label class="form-label"><?= h(tr('المدينة (عربي)', 'City (AR)')) ?></label>
<input type="text" name="city_ar" class="form-control">
</div>
<div class="col-6 mb-3">
<label class="form-label"><?= h(tr('المدينة (إنجليزي)', 'City (EN)')) ?></label>
<input type="text" name="city_en" class="form-control">
</div>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><?= h(tr('إلغاء', 'Cancel')) ?></button>
<button type="submit" class="btn btn-primary"><?= h(tr('إضافة', 'Add')) ?></button>
</div>
</form>
</div>
</div>
</div>
<?php require __DIR__ . '/includes/footer.php'; ?>

0
patch.php Normal file
View File

1187
pos.php Normal file

File diff suppressed because it is too large Load Diff

5363
pos_out.html Normal file

File diff suppressed because one or more lines are too long

0
pos_out2.html Normal file
View File

394
print_label_dates.php Normal file
View File

@ -0,0 +1,394 @@
<?php
require_once __DIR__ . '/includes/app.php';
require_permission('stock', 'show');
$pdo = db();
$items = [];
$skus = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['skus'])) {
$skus = is_array($_POST['skus']) ? $_POST['skus'] : explode(',', $_POST['skus']);
$skus = array_values(array_filter(array_map('trim', $skus)));
} elseif (!empty($_GET['sku'])) {
$skus = [$_GET['sku']];
}
if (!empty($skus)) {
$placeholders = str_repeat('?,', count($skus) - 1) . '?';
$stmt = $pdo->prepare("SELECT * FROM items WHERE sku IN ($placeholders)");
$stmt->execute($skus);
$results = $stmt->fetchAll();
// Index by SKU
foreach ($results as $row) {
$items[$row['sku']] = $row;
}
}
$customWidth = $_REQUEST['custom_width'] ?? '40';
$customHeight = $_REQUEST['custom_height'] ?? '25';
// Templates configuration
$templates = [
'custom_roll' => [
'name' => 'Custom Size / مقاس مخصص (أدخل المقاس أدناه)',
'cols' => 1,
'rows' => 1,
'width' => $customWidth . 'mm',
'height' => $customHeight . 'mm',
'margin_top' => '0mm',
'margin_left' => '0mm',
'gap_x' => '0mm',
'gap_y' => '0mm',
'page_width' => $customWidth . 'mm',
'page_height' => $customHeight . 'mm',
],
'roll_40x25' => [
'name' => 'Roll / Zebra (40 x 25 mm)',
'cols' => 1,
'rows' => 1,
'width' => '40mm',
'height' => '25mm',
'margin_top' => '0mm',
'margin_left' => '0mm',
'gap_x' => '0mm',
'gap_y' => '0mm',
'page_width' => '40mm',
'page_height' => '25mm',
],
'roll_25x40' => [
'name' => 'Roll / Zebra (25 x 40 mm)',
'cols' => 1,
'rows' => 1,
'width' => '25mm',
'height' => '40mm',
'margin_top' => '0mm',
'margin_left' => '0mm',
'gap_x' => '0mm',
'gap_y' => '0mm',
'page_width' => '25mm',
'page_height' => '40mm',
],
'roll_38x25' => [
'name' => 'Roll / Zebra (38 x 25 mm)',
'cols' => 1,
'rows' => 1,
'width' => '38mm',
'height' => '25mm',
'margin_top' => '0mm',
'margin_left' => '0mm',
'gap_x' => '0mm',
'gap_y' => '0mm',
'page_width' => '38mm',
'page_height' => '25mm',
],
'avery_65' => [
'name' => 'Avery L7651 - 65 Labels (38.1 x 21.2 mm)',
'cols' => 5,
'rows' => 13,
'width' => '38.1mm',
'height' => '21.2mm',
'margin_top' => '10.5mm',
'margin_left' => '4.7mm',
'gap_x' => '2.5mm',
'gap_y' => '0mm',
'page_width' => '210mm',
'page_height' => '297mm',
],
'avery_54' => [
'name' => 'Avery 54 Labels (32 x 25.4 mm)',
'cols' => 6,
'rows' => 9,
'width' => '32mm',
'height' => '25.4mm',
'margin_top' => '10mm',
'margin_left' => '5mm',
'gap_x' => '2mm',
'gap_y' => '0mm',
'page_width' => '210mm',
'page_height' => '297mm',
],
'avery_45' => [
'name' => 'Avery 45 Labels (38.1 x 29.6 mm)',
'cols' => 5,
'rows' => 9,
'width' => '38.1mm',
'height' => '29.6mm',
'margin_top' => '15mm',
'margin_left' => '5mm',
'gap_x' => '2mm',
'gap_y' => '0mm',
'page_width' => '210mm',
'page_height' => '297mm',
]
];
$templateId = $_REQUEST['template'] ?? 'custom_roll';
$tpl = $templates[$templateId] ?? $templates['custom_roll'];
$pageWidth = $tpl['page_width'] ?? '210mm';
$pageHeight = $tpl['page_height'] ?? '297mm';
$pageCssSize = (isset($tpl['page_width']) && $tpl['page_width'] !== '210mm') ? "{$tpl['page_width']} {$tpl['page_height']}" : "A4 portrait";
if ($templateId !== 'custom_roll') {
$customWidth = floatval(str_replace('mm', '', $tpl['width']));
$customHeight = floatval(str_replace('mm', '', $tpl['height']));
}
// Prepare labels to print
$labelsToPrint = [];
if (!empty($items)) {
// Check if quantities are posted
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['qty'])) {
foreach ($_POST['qty'] as $sku => $qty) {
if (isset($items[$sku])) {
for ($i = 0; $i < (int)$qty; $i++) {
$itemData = $items[$sku];
$itemData['prod_date'] = $_POST['prod_date'][$sku] ?? date('Y-m-d');
$itemData['exp_date'] = $_POST['exp_date'][$sku] ?? date('Y-m-d', strtotime('+1 year'));
$labelsToPrint[] = $itemData;
}
}
}
} else {
// Default 1 per item
foreach ($skus as $sku) {
if (isset($items[$sku])) {
$itemData = $items[$sku];
$itemData['prod_date'] = date('Y-m-d');
$itemData['exp_date'] = date('Y-m-d', strtotime('+1 year'));
$labelsToPrint[] = $itemData;
}
}
}
}
$isSinglePrint = isset($_GET['sku']) && count($skus) === 1;
$companyName = current_lang() === 'ar' ? get_setting('company_name_ar', 'حلوى الريامي') : get_setting('company_name_en', 'Al Riyami Sweets');
?>
<!DOCTYPE html>
<html lang="<?= current_lang() ?>" dir="<?= current_lang() === 'ar' ? 'rtl' : 'ltr' ?>">
<head>
<meta charset="UTF-8">
<title><?= h(tr('طباعة ملصقات مع التواريخ', 'Print Labels with Dates')) ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
<style>
body { background: #f0f2f5; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
.no-print { display: block; }
.print-only { display: none; }
.page {
width: <?= $pageWidth ?>;
height: <?= $pageHeight ?>;
padding-top: <?= $tpl['margin_top'] ?>;
padding-left: <?= $tpl['margin_left'] ?>;
padding-right: <?= $tpl['margin_left'] ?>;
margin: 10mm auto;
border: 1px solid #D3D3D3;
border-radius: 5px;
background: white;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
display: grid;
grid-template-columns: repeat(<?= $tpl['cols'] ?>, <?= $tpl['width'] ?>);
grid-auto-rows: <?= $tpl['height'] ?>;
column-gap: <?= $tpl['gap_x'] ?>;
row-gap: <?= $tpl['gap_y'] ?>;
box-sizing: border-box;
page-break-after: always;
overflow: hidden;
}
.label-item {
width: <?= $tpl['width'] ?>;
height: <?= $tpl['height'] ?>;
border: 1px dashed #ccc;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
box-sizing: border-box;
padding: 2px;
text-align: center;
}
.label-company { font-size: 9px; font-weight: bold; margin-bottom: 1px; text-transform: uppercase; }
.label-name { font-size: 9px; font-weight: bold; line-height: 1.1; margin-bottom: 2px; max-height: 20px; overflow: hidden; }
.label-price { font-size: 10px; font-weight: bold; margin-top: 1px; }
.label-sku { font-size: 8px; color: #555; }
.label-dates { font-size: 6.5px; font-weight: bold; margin-top: 1px; color: #111; letter-spacing: 0.2px; }
.label-barcode { margin: 0; display: flex; align-items: center; justify-content: center; }
.label-barcode svg { width: 100%; height: 100%; max-height: 14px; }
@media print {
@page { size: <?= $pageCssSize ?>; margin: 0; }
html, body {
margin: 0 !important;
padding: 0 !important;
background: white;
height: 100%;
}
.no-print { display: none !important; }
.print-only { display: block; }
.print-preview { margin: 0 !important; padding: 0 !important; border: none !important; }
.page {
margin: 0 !important;
padding: 0 !important;
border: none !important;
width: <?= $pageWidth ?> !important;
height: <?= $pageHeight ?> !important;
box-shadow: none !important;
background: white !important;
page-break-after: auto !important;
page-break-inside: avoid !important;
overflow: hidden;
}
.page + .page {
page-break-before: always !important;
}
.label-item { border: none !important; margin: 0; padding: 2px; box-sizing: border-box; }
}
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
</head>
<body>
<div class="container mt-4 no-print">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0"><?= h(tr('إعدادات الطباعة', 'Print Settings')) ?></h5>
<a href="stock.php" class="btn btn-sm btn-light"><?= h(tr('رجوع للمخزون', 'Back to Stock')) ?></a>
</div>
<div class="card-body">
<form method="POST">
<div class="row g-3 align-items-end mb-3">
<input type="hidden" name="template" value="custom_roll">
<div class="col-md-2 col-6">
<label class="form-label fw-bold"><?= h(tr('العرض', 'Width')) ?> (mm)</label>
<input type="number" name="custom_width" class="form-control" value="<?= h($customWidth) ?>" onchange="this.form.submit()" min="10">
</div>
<div class="col-md-2 col-6">
<label class="form-label fw-bold"><?= h(tr('الارتفاع', 'Height')) ?> (mm)</label>
<input type="number" name="custom_height" class="form-control" value="<?= h($customHeight) ?>" onchange="this.form.submit()" min="10">
</div>
<div class="col-md-auto text-end flex-grow-1">
<button type="button" class="btn btn-success btn-lg shadow" onclick="window.print()">
<i class="bi bi-printer"></i> <?= h(tr('طباعة الآن', 'Print Now')) ?>
</button>
</div>
</div>
<div class="table-responsive border rounded">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>SKU</th>
<th><?= h(tr('الصنف', 'Item')) ?></th>
<th><?= h(tr('السعر', 'Price')) ?></th>
<th><?= h(tr('تاريخ الإنتاج', 'Prod. Date')) ?></th>
<th><?= h(tr('تاريخ الانتهاء', 'Exp. Date')) ?></th>
<th width="120" class="text-center"><?= h(tr('الكمية', 'Qty')) ?></th>
<th width="80" class="text-center"><?= h(tr('إزالة', 'Remove')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach($items as $sku => $item):
?><tr>
<td class="fw-bold text-secondary">
<?= h($sku) ?>
<input type="hidden" name="skus[]" value="<?= h($sku) ?>">
</td>
<td><?= h($item['name']) ?></td>
<td><span class="badge bg-light text-dark border"><?= h(currency($item['price'])) ?></span></td>
<td>
<input type="date" name="prod_date[<?= h($sku) ?>]" class="form-control form-control-sm" value="<?= h($_POST['prod_date'][$sku] ?? date('Y-m-d')) ?>">
</td>
<td>
<input type="date" name="exp_date[<?= h($sku) ?>]" class="form-control form-control-sm" value="<?= h($_POST['exp_date'][$sku] ?? date('Y-m-d', strtotime('+1 year'))) ?>">
</td>
<td>
<input type="number" name="qty[<?= h($sku) ?>]" class="form-control form-control-sm text-center" value="<?= isset($_POST['qty'][$sku]) ? (int)$_POST['qty'][$sku] : 1 ?>" min="1">
</td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-outline-danger rounded-circle" onclick="this.closest('tr').remove();" title="<?= h(tr('إزالة', 'Remove')) ?>">
<i class="bi bi-x-lg"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php if(empty($items)):
?><tr>
<td colspan="7" class="text-center text-muted py-4">
<i class="bi bi-inbox fs-3 d-block mb-2"></i>
<?= h(tr('لم يتم تحديد أصناف', 'No items selected')) ?>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if(!empty($items)):
?><div class="mt-3">
<button type="submit" class="btn btn-primary">
<i class="bi bi-arrow-clockwise"></i> <?= h(tr('تحديث المعاينة', 'Update Preview')) ?>
</button>
</div>
<?php endif; ?>
</form>
</div>
</div>
</div>
<?php if (!empty($labelsToPrint)): ?>
<div class="print-preview mt-4">
<h5 class="text-center no-print text-muted mb-3"><i class="bi bi-view-list"></i> <?= h(tr('معاينة الطباعة', 'Print Preview')) ?></h5>
<?php
$labelsPerPage = $tpl['cols'] * $tpl['rows'];
$pages = array_chunk($labelsToPrint, $labelsPerPage);
foreach ($pages as $pageIdx => $pageLabels):
?>
<div class="page bg-white">
<?php foreach ($pageLabels as $label):
?><div class="label-item">
<div class="label-company"><?= h($companyName) ?></div>
<div class="label-name"><?= h($label['name']) ?></div>
<div class="label-barcode">
<svg class="barcode"
jsbarcode-format="code128"
jsbarcode-value="<?= h($label['sku']) ?>"
jsbarcode-displayvalue="false"
jsbarcode-width="1"
jsbarcode-height="12"
jsbarcode-margin="0">
</svg>
</div>
<div class="label-dates" dir="ltr" style="display: flex; justify-content: space-around; width: 100%;">
<span>P:<?= !empty($label['prod_date']) ? date('d/m/Y', strtotime($label['prod_date'])) : '' ?></span><span>E:<?= !empty($label['exp_date']) ? date('d/m/Y', strtotime($label['exp_date'])) : '' ?></span>
</div>
<div class="label-price"><?= h(currency($label['price'])) ?></div>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<div class="text-center mt-4 mb-5 no-print">
<button type="button" class="btn btn-success btn-lg px-5 py-3 shadow-lg rounded-pill" onclick="window.print()">
<i class="bi bi-printer fs-4"></i> <?= h(tr('الطباعة الآن', 'Print Labels Now')) ?>
</button>
</div>
<?php endif; ?>
<script>
document.addEventListener("DOMContentLoaded", function() {
if (typeof JsBarcode !== 'undefined') {
JsBarcode(".barcode").init();
}
});
</script>
</body>
</html>

368
print_labels.php Normal file
View File

@ -0,0 +1,368 @@
<?php
require_once __DIR__ . '/includes/app.php';
require_permission('stock', 'show');
$pdo = db();
$items = [];
$skus = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['skus'])) {
$skus = is_array($_POST['skus']) ? $_POST['skus'] : explode(',', $_POST['skus']);
$skus = array_values(array_filter(array_map('trim', $skus)));
} elseif (!empty($_GET['sku'])) {
$skus = [$_GET['sku']];
}
if (!empty($skus)) {
$placeholders = str_repeat('?,', count($skus) - 1) . '?';
$stmt = $pdo->prepare("SELECT * FROM items WHERE sku IN ($placeholders)");
$stmt->execute($skus);
$results = $stmt->fetchAll();
// Index by SKU
foreach ($results as $row) {
$items[$row['sku']] = $row;
}
}
$customWidth = $_REQUEST['custom_width'] ?? '40';
$customHeight = $_REQUEST['custom_height'] ?? '25';
// Templates configuration
$templates = [
'custom_roll' => [
'name' => 'Custom Size / مقاس مخصص (أدخل المقاس أدناه)',
'cols' => 1,
'rows' => 1,
'width' => $customWidth . 'mm',
'height' => $customHeight . 'mm',
'margin_top' => '0mm',
'margin_left' => '0mm',
'gap_x' => '0mm',
'gap_y' => '0mm',
'page_width' => $customWidth . 'mm',
'page_height' => $customHeight . 'mm',
],
'avery_65' => [
'name' => 'Roll / Zebra (40 x 25 mm)',
'cols' => 1,
'rows' => 1,
'width' => '40mm',
'height' => '25mm',
'margin_top' => '0mm',
'margin_left' => '0mm',
'gap_x' => '0mm',
'gap_y' => '0mm',
'page_width' => '40mm',
'page_height' => '25mm',
],
'roll_25x40' => [
'name' => 'Roll / Zebra (25 x 40 mm)',
'cols' => 1,
'rows' => 1,
'width' => '25mm',
'height' => '40mm',
'margin_top' => '0mm',
'margin_left' => '0mm',
'gap_x' => '0mm',
'gap_y' => '0mm',
'page_width' => '25mm',
'page_height' => '40mm',
],
'roll_38x25' => [
'name' => 'Roll / Zebra (38 x 25 mm)',
'cols' => 1,
'rows' => 1,
'width' => '38mm',
'height' => '25mm',
'margin_top' => '0mm',
'margin_left' => '0mm',
'gap_x' => '0mm',
'gap_y' => '0mm',
'page_width' => '38mm',
'page_height' => '25mm',
],
'avery_65' => [
'name' => 'Avery L7651 - 65 Labels (38.1 x 21.2 mm)',
'cols' => 5,
'rows' => 13,
'width' => '38.1mm',
'height' => '21.2mm',
'margin_top' => '10.5mm',
'margin_left' => '4.7mm',
'gap_x' => '2.5mm',
'gap_y' => '0mm',
'page_width' => '210mm',
'page_height' => '297mm',
],
'avery_54' => [
'name' => 'Avery 54 Labels (32 x 25.4 mm)',
'cols' => 6,
'rows' => 9,
'width' => '32mm',
'height' => '25.4mm',
'margin_top' => '10mm',
'margin_left' => '5mm',
'gap_x' => '2mm',
'gap_y' => '0mm',
'page_width' => '210mm',
'page_height' => '297mm',
],
'avery_45' => [
'name' => 'Avery 45 Labels (38.1 x 29.6 mm)',
'cols' => 5,
'rows' => 9,
'width' => '38.1mm',
'height' => '29.6mm',
'margin_top' => '15mm',
'margin_left' => '5mm',
'gap_x' => '2mm',
'gap_y' => '0mm',
'page_width' => '210mm',
'page_height' => '297mm',
]
];
$templateId = $_REQUEST['template'] ?? 'avery_65';
$tpl = $templates[$templateId] ?? $templates['avery_65'];
$pageWidth = $tpl['page_width'] ?? '210mm';
$pageHeight = $tpl['page_height'] ?? '297mm';
$pageCssSize = (isset($tpl['page_width']) && $tpl['page_width'] !== '210mm') ? "{$tpl['page_width']} {$tpl['page_height']}" : "A4 portrait";
if ($templateId !== 'custom_roll') {
$customWidth = floatval(str_replace('mm', '', $tpl['width']));
$customHeight = floatval(str_replace('mm', '', $tpl['height']));
}
// Prepare labels to print
$labelsToPrint = [];
if (!empty($items)) {
// Check if quantities are posted
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['qty'])) {
foreach ($_POST['qty'] as $sku => $qty) {
if (isset($items[$sku])) {
for ($i = 0; $i < (int)$qty; $i++) {
$labelsToPrint[] = $items[$sku];
}
}
}
} else {
// Default 1 per item
foreach ($skus as $sku) {
if (isset($items[$sku])) {
$labelsToPrint[] = $items[$sku];
}
}
}
}
$isSinglePrint = isset($_GET['sku']) && count($skus) === 1;
$companyName = current_lang() === 'ar' ? get_setting('company_name_ar', 'حلوى الريامي') : get_setting('company_name_en', 'Al Riyami Sweets');
?>
<!DOCTYPE html>
<html lang="<?= current_lang() ?>" dir="<?= current_lang() === 'ar' ? 'rtl' : 'ltr' ?>">
<head>
<meta charset="UTF-8">
<title><?= h(tr('طباعة ملصقات', 'Print Labels')) ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
<style>
body { background: #f0f2f5; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
.no-print { display: block; }
.print-only { display: none; }
.page {
width: <?= $pageWidth ?>;
height: <?= $pageHeight ?>;
padding-top: <?= $tpl['margin_top'] ?>;
padding-left: <?= $tpl['margin_left'] ?>;
padding-right: <?= $tpl['margin_left'] ?>;
margin: 10mm auto;
border: 1px solid #D3D3D3;
border-radius: 5px;
background: white;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
display: grid;
grid-template-columns: repeat(<?= $tpl['cols'] ?>, <?= $tpl['width'] ?>);
grid-auto-rows: <?= $tpl['height'] ?>;
column-gap: <?= $tpl['gap_x'] ?>;
row-gap: <?= $tpl['gap_y'] ?>;
box-sizing: border-box;
page-break-after: always;
overflow: hidden;
}
.label-item {
width: <?= $tpl['width'] ?>;
height: <?= $tpl['height'] ?>;
border: 1px dashed #ccc;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
box-sizing: border-box;
padding: 2px;
text-align: center;
}
.label-company { font-size: 9px; font-weight: bold; margin-bottom: 1px; text-transform: uppercase; }
.label-name { font-size: 9px; font-weight: bold; line-height: 1.1; margin-bottom: 2px; max-height: 20px; overflow: hidden; }
.label-price { font-size: 10px; font-weight: bold; margin-top: 1px; }
.label-sku { font-size: 8px; color: #555; }
.label-barcode { margin: 0; display: flex; align-items: center; justify-content: center; }
.label-barcode svg { width: 100%; height: 100%; max-height: 14px; }
@media print {
body { background: white; margin: 0; padding: 0; }
.no-print { display: none !important; }
.print-only { display: block; }
.page {
margin: 0;
border: initial;
border-radius: initial;
width: <?= $pageWidth ?>;
height: <?= $pageHeight ?>;
box-shadow: initial;
background: initial;
page-break-after: always;
overflow: hidden;
}
.page:last-of-type {
page-break-after: auto;
}
.label-item { border: none; }
@page { size: <?= $pageCssSize ?>; margin: 0; }
}
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
</head>
<body>
<div class="container mt-4 no-print">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0"><?= h(tr('إعدادات الطباعة', 'Print Settings')) ?></h5>
<a href="stock.php" class="btn btn-sm btn-light"><?= h(tr('رجوع للمخزون', 'Back to Stock')) ?></a>
</div>
<div class="card-body">
<form method="POST">
<div class="row g-3 align-items-end mb-3">
<div class="col-md-4">
<label class="form-label fw-bold"><?= h(tr('قالب الملصقات', 'Label Template')) ?></label>
<select name="template" class="form-select" onchange="this.form.submit()">
<?php foreach($templates as $key => $t):
?><option value="<?= $key ?>" <?= $templateId === $key ? 'selected' : '' ?>><?= $t['name'] ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-auto text-end flex-grow-1">
<button type="button" class="btn btn-success btn-lg shadow" onclick="window.print()">
<i class="bi bi-printer"></i> <?= h(tr('طباعة الآن', 'Print Now')) ?>
</button>
</div>
</div>
<div class="table-responsive border rounded">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>SKU</th>
<th><?= h(tr('الصنف', 'Item')) ?></th>
<th><?= h(tr('السعر', 'Price')) ?></th>
<th width="150" class="text-center"><?= h(tr('عدد الملصقات', 'Labels Count')) ?></th>
<th width="80" class="text-center"><?= h(tr('إزالة', 'Remove')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach($items as $sku => $item):
?><tr>
<td class="fw-bold text-secondary">
<?= h($sku) ?>
<input type="hidden" name="skus[]" value="<?= h($sku) ?>">
</td>
<td><?= h($item['name']) ?></td>
<td><span class="badge bg-light text-dark border"><?= h(currency($item['price'])) ?></span></td>
<td>
<input type="number" name="qty[<?= h($sku) ?>]" class="form-control text-center" value="<?= isset($_POST['qty'][$sku]) ? (int)$_POST['qty'][$sku] : 1 ?>" min="1">
</td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-outline-danger rounded-circle" onclick="this.closest('tr').remove();" title="<?= h(tr('إزالة', 'Remove')) ?>">
<i class="bi bi-x-lg"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php if(empty($items)):
?><tr>
<td colspan="5" class="text-center text-muted py-4">
<i class="bi bi-inbox fs-3 d-block mb-2"></i>
<?= h(tr('لم يتم تحديد أصناف', 'No items selected')) ?>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if(!empty($items)):
?><div class="mt-3">
<button type="submit" class="btn btn-primary">
<i class="bi bi-arrow-clockwise"></i> <?= h(tr('تحديث المعاينة', 'Update Preview')) ?>
</button>
</div>
<?php endif; ?>
</form>
</div>
</div>
</div>
<?php if (!empty($labelsToPrint)): ?>
<div class="print-preview mt-4">
<h5 class="text-center no-print text-muted mb-3"><i class="bi bi-view-list"></i> <?= h(tr('معاينة الطباعة', 'Print Preview')) ?></h5>
<?php
$labelsPerPage = $tpl['cols'] * $tpl['rows'];
$pages = array_chunk($labelsToPrint, $labelsPerPage);
foreach ($pages as $pageIdx => $pageLabels):
?>
<div class="page bg-white">
<?php foreach ($pageLabels as $label):
?><div class="label-item">
<div class="label-company"><?= h($companyName) ?></div>
<div class="label-name"><?= h($label['name']) ?></div>
<div class="label-barcode">
<svg class="barcode"
jsbarcode-format="code128"
jsbarcode-value="<?= h($label['sku']) ?>"
jsbarcode-displayvalue="false"
jsbarcode-width="1"
jsbarcode-height="15"
jsbarcode-margin="0">
</svg>
</div>
<div class="label-sku"><?= h($label['sku']) ?></div>
<div class="label-price"><?= h(currency($label['price'])) ?></div>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<div class="text-center mt-4 mb-5 no-print">
<button type="button" class="btn btn-success btn-lg px-5 py-3 shadow-lg rounded-pill" onclick="window.print()">
<i class="bi bi-printer fs-4"></i> <?= h(tr('الطباعة الآن', 'Print Labels Now')) ?>
</button>
</div>
<?php endif; ?>
<script>
document.addEventListener("DOMContentLoaded", function() {
if (typeof JsBarcode !== 'undefined') {
JsBarcode(".barcode").init();
}
});
</script>
</body>
</html>

320
print_receipt.php Normal file
View File

@ -0,0 +1,320 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('sales', 'show');
$id = (int) ($_GET['id'] ?? 0);
$autoPrint = isset($_GET['print']) && $_GET['print'] === '1';
$sale = null;
$dbError = null;
if ($id > 0) {
try {
$sale = fetch_sale($id);
} catch (Throwable $e) {
$dbError = $e->getMessage();
}
}
if (!$sale) {
die("Sale not found.");
}
$paymentSummary = sale_payment_summary($sale);
$isEidSale = (($sale['order_type'] ?? 'standard') === 'eid');
$backHref = $isEidSale ? url_for('eid_orders.php') : url_for('sales.php');
$backLabel = $isEidSale ? tr('رجوع لطلبات العيد', 'Back to Eid Orders') : tr('رجوع للمبيعات', 'Back to Sales');
// Receipt Configuration
$storeName = current_lang() === 'ar' ? get_setting('company_name_ar', 'حلوى الريامي') : get_setting('company_name_en', 'Al Riyami Sweets');
$storeAddress = get_setting('company_address', '');
$vatNo = get_setting('company_vat_number', '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 -->
<?php if (get_setting('company_logo')): ?>
<img src="<?= h(get_setting('company_logo')) ?>" alt="Logo" style="max-height: 80px; max-width: 150px;">
<?php else: ?>
<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>
<?php endif; ?>
</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')) ?>: <?= h(phone_display(get_setting('company_phone', ''))) ?></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>
<?php if ($isEidSale): ?>
<div class="sale-info">
<span><?= h(tr('التسلسل الموسمي', 'Season serial')) ?>:</span>
<span class="font-bold"><?= h(eid_serial_label((int) ($sale['eid_serial_no'] ?? 0))) ?></span>
</div>
<?php endif; ?>
<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>
<div class="sale-info">
<span><?= h(tr('العميل', 'Customer')) ?>:</span>
<span><?= h((string) ($sale['customer_name'] ?: tr('عميل نقدي', 'Walk-in Customer'))) ?></span>
</div>
<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('ضريبة القيمة المضافة (مضافة)', 'VAT (Added)')) ?></span>
<span><?= number_format((float)($sale['vat_amount'] ?? 0), 3) ?></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('المدفوع', 'Paid')) ?></span>
<span><?= number_format((float)$paymentSummary['paid_amount'], 3) ?> <?= h(tr('ر.ع', 'OMR')) ?></span>
</div>
<div class="totals-row">
<span><?= h(tr('المتبقي', 'Due')) ?></span>
<span><?= number_format((float)$paymentSummary['due_amount'], 3) ?> <?= h(tr('ر.ع', 'OMR')) ?></span>
</div>
<?php
$pm = $sale['payment_method'];
$pmLabel = $pm;
if ($pm === 'cash') $pmLabel = tr('نقدي', 'Cash');
elseif ($pm === 'card') $pmLabel = tr('بطاقة', 'Card');
elseif ($pm === 'transfer') $pmLabel = tr('تحويل', 'Transfer');
elseif ($pm === 'pay_later') $pmLabel = tr('آجل', 'Pay Later');
else $pmLabel = ucfirst(str_replace('_', ' ', (string)$pm));
?>
<div class="totals-row">
<span><?= h(tr('طريقة الدفع', 'Payment Method')) ?></span>
<span><?= h($pmLabel) ?></span>
</div>
<div class="totals-row">
<span><?= h(tr('الحالة', 'Status')) ?></span>
<span><?= h(payment_status_label($paymentSummary['payment_status'])) ?></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="<?= h($backHref) ?>" class="btn btn-secondary"><?= h($backLabel) ?></a>
<button onclick="window.print()" class="btn"><?= h(tr('طباعة الآن', 'Print Now')) ?></button>
</div>
<?php if ($autoPrint): ?>
<script>
// Auto print when page loads only when explicitly requested
window.onload = function() {
setTimeout(() => {
window.print();
}, 500);
};
</script>
<?php endif; ?>
</body>
</html>

377
print_single_label.php Normal file
View File

@ -0,0 +1,377 @@
<?php
require_once __DIR__ . '/includes/app.php';
require_permission('stock', 'show');
$pdo = db();
$items = [];
$skus = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['skus'])) {
$skus = is_array($_POST['skus']) ? $_POST['skus'] : explode(',', $_POST['skus']);
$skus = array_values(array_filter(array_map('trim', $skus)));
} elseif (!empty($_GET['sku'])) {
$skus = [$_GET['sku']];
}
if (!empty($skus)) {
$placeholders = str_repeat('?,', count($skus) - 1) . '?';
$stmt = $pdo->prepare("SELECT * FROM items WHERE sku IN ($placeholders)");
$stmt->execute($skus);
$results = $stmt->fetchAll();
// Index by SKU
foreach ($results as $row) {
$items[$row['sku']] = $row;
}
}
$customWidth = $_REQUEST['custom_width'] ?? '40';
$customHeight = $_REQUEST['custom_height'] ?? '25';
// Templates configuration
$templates = [
'custom_roll' => [
'name' => 'Custom Size / مقاس مخصص (أدخل المقاس أدناه)',
'cols' => 1,
'rows' => 1,
'width' => $customWidth . 'mm',
'height' => $customHeight . 'mm',
'margin_top' => '0mm',
'margin_left' => '0mm',
'gap_x' => '0mm',
'gap_y' => '0mm',
'page_width' => $customWidth . 'mm',
'page_height' => $customHeight . 'mm',
],
'roll_40x25' => [
'name' => 'Roll / Zebra (40 x 25 mm)',
'cols' => 1,
'rows' => 1,
'width' => '40mm',
'height' => '25mm',
'margin_top' => '0mm',
'margin_left' => '0mm',
'gap_x' => '0mm',
'gap_y' => '0mm',
'page_width' => '40mm',
'page_height' => '25mm',
],
'roll_25x40' => [
'name' => 'Roll / Zebra (25 x 40 mm)',
'cols' => 1,
'rows' => 1,
'width' => '25mm',
'height' => '40mm',
'margin_top' => '0mm',
'margin_left' => '0mm',
'gap_x' => '0mm',
'gap_y' => '0mm',
'page_width' => '25mm',
'page_height' => '40mm',
],
'roll_38x25' => [
'name' => 'Roll / Zebra (38 x 25 mm)',
'cols' => 1,
'rows' => 1,
'width' => '38mm',
'height' => '25mm',
'margin_top' => '0mm',
'margin_left' => '0mm',
'gap_x' => '0mm',
'gap_y' => '0mm',
'page_width' => '38mm',
'page_height' => '25mm',
],
'avery_65' => [
'name' => 'Avery L7651 - 65 Labels (38.1 x 21.2 mm)',
'cols' => 5,
'rows' => 13,
'width' => '38.1mm',
'height' => '21.2mm',
'margin_top' => '10.5mm',
'margin_left' => '4.7mm',
'gap_x' => '2.5mm',
'gap_y' => '0mm',
'page_width' => '210mm',
'page_height' => '297mm',
],
'avery_54' => [
'name' => 'Avery 54 Labels (32 x 25.4 mm)',
'cols' => 6,
'rows' => 9,
'width' => '32mm',
'height' => '25.4mm',
'margin_top' => '10mm',
'margin_left' => '5mm',
'gap_x' => '2mm',
'gap_y' => '0mm',
'page_width' => '210mm',
'page_height' => '297mm',
],
'avery_45' => [
'name' => 'Avery 45 Labels (38.1 x 29.6 mm)',
'cols' => 5,
'rows' => 9,
'width' => '38.1mm',
'height' => '29.6mm',
'margin_top' => '15mm',
'margin_left' => '5mm',
'gap_x' => '2mm',
'gap_y' => '0mm',
'page_width' => '210mm',
'page_height' => '297mm',
]
];
$templateId = $_REQUEST['template'] ?? 'custom_roll';
$tpl = $templates[$templateId] ?? $templates['custom_roll'];
$pageWidth = $tpl['page_width'] ?? '210mm';
$pageHeight = $tpl['page_height'] ?? '297mm';
$pageCssSize = (isset($tpl['page_width']) && $tpl['page_width'] !== '210mm') ? "{$tpl['page_width']} {$tpl['page_height']}" : "A4 portrait";
if ($templateId !== 'custom_roll') {
$customWidth = floatval(str_replace('mm', '', $tpl['width']));
$customHeight = floatval(str_replace('mm', '', $tpl['height']));
}
// Prepare labels to print
$labelsToPrint = [];
if (!empty($items)) {
// Check if quantities are posted
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['qty'])) {
foreach ($_POST['qty'] as $sku => $qty) {
if (isset($items[$sku])) {
for ($i = 0; $i < (int)$qty; $i++) {
$labelsToPrint[] = $items[$sku];
}
}
}
} else {
// Default 1 per item
foreach ($skus as $sku) {
if (isset($items[$sku])) {
$labelsToPrint[] = $items[$sku];
}
}
}
}
$isSinglePrint = isset($_GET['sku']) && count($skus) === 1;
$companyName = current_lang() === 'ar' ? get_setting('company_name_ar', 'حلوى الريامي') : get_setting('company_name_en', 'Al Riyami Sweets');
?>
<!DOCTYPE html>
<html lang="<?= current_lang() ?>" dir="<?= current_lang() === 'ar' ? 'rtl' : 'ltr' ?>">
<head>
<meta charset="UTF-8">
<title><?= h(tr('طباعة ملصقات', 'Print Labels')) ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
<style>
body { background: #f0f2f5; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
.no-print { display: block; }
.print-only { display: none; }
.page {
width: <?= $pageWidth ?>;
height: <?= $pageHeight ?>;
padding-top: <?= $tpl['margin_top'] ?>;
padding-left: <?= $tpl['margin_left'] ?>;
padding-right: <?= $tpl['margin_left'] ?>;
margin: 10mm auto;
border: 1px solid #D3D3D3;
border-radius: 5px;
background: white;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
display: grid;
grid-template-columns: repeat(<?= $tpl['cols'] ?>, <?= $tpl['width'] ?>);
grid-auto-rows: <?= $tpl['height'] ?>;
column-gap: <?= $tpl['gap_x'] ?>;
row-gap: <?= $tpl['gap_y'] ?>;
box-sizing: border-box;
page-break-after: always;
overflow: hidden;
}
.label-item {
width: <?= $tpl['width'] ?>;
height: <?= $tpl['height'] ?>;
border: 1px dashed #ccc;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
box-sizing: border-box;
padding: 2px;
text-align: center;
}
.label-company { font-size: 9px; font-weight: bold; margin-bottom: 1px; text-transform: uppercase; }
.label-name { font-size: 9px; font-weight: bold; line-height: 1.1; margin-bottom: 2px; max-height: 20px; overflow: hidden; }
.label-price { font-size: 10px; font-weight: bold; margin-top: 1px; }
.label-sku { font-size: 8px; color: #555; }
.label-barcode { margin: 0; display: flex; align-items: center; justify-content: center; }
.label-barcode svg { width: 100%; height: 100%; max-height: 14px; }
@media print {
@page { size: <?= $pageCssSize ?>; margin: 0; }
html, body {
margin: 0 !important;
padding: 0 !important;
background: white;
height: 100%;
}
.no-print { display: none !important; }
.print-only { display: block; }
.print-preview { margin: 0 !important; padding: 0 !important; border: none !important; }
.page {
margin: 0 !important;
padding: 0 !important;
border: none !important;
width: <?= $pageWidth ?> !important;
height: <?= $pageHeight ?> !important;
box-shadow: none !important;
background: white !important;
page-break-after: auto !important;
page-break-inside: avoid !important;
overflow: hidden;
}
.page + .page {
page-break-before: always !important;
}
.label-item { border: none !important; margin: 0; padding: 2px; box-sizing: border-box; }
}
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
</head>
<body>
<div class="container mt-4 no-print">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0"><?= h(tr('إعدادات الطباعة', 'Print Settings')) ?></h5>
<a href="stock.php" class="btn btn-sm btn-light"><?= h(tr('رجوع للمخزون', 'Back to Stock')) ?></a>
</div>
<div class="card-body">
<form method="POST">
<div class="row g-3 align-items-end mb-3">
<input type="hidden" name="template" value="custom_roll">
<div class="col-md-2 col-6">
<label class="form-label fw-bold"><?= h(tr('العرض', 'Width')) ?> (mm)</label>
<input type="number" name="custom_width" class="form-control" value="<?= h($customWidth) ?>" onchange="this.form.submit()" min="10">
</div>
<div class="col-md-2 col-6">
<label class="form-label fw-bold"><?= h(tr('الارتفاع', 'Height')) ?> (mm)</label>
<input type="number" name="custom_height" class="form-control" value="<?= h($customHeight) ?>" onchange="this.form.submit()" min="10">
</div>
<div class="col-md-auto text-end flex-grow-1">
<button type="button" class="btn btn-success btn-lg shadow" onclick="window.print()">
<i class="bi bi-printer"></i> <?= h(tr('طباعة الآن', 'Print Now')) ?>
</button>
</div>
</div>
<div class="table-responsive border rounded">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>SKU</th>
<th><?= h(tr('الصنف', 'Item')) ?></th>
<th><?= h(tr('السعر', 'Price')) ?></th>
<th width="150" class="text-center"><?= h(tr('عدد الملصقات', 'Labels Count')) ?></th>
<th width="80" class="text-center"><?= h(tr('إزالة', 'Remove')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach($items as $sku => $item):
?><tr>
<td class="fw-bold text-secondary">
<?= h($sku) ?>
<input type="hidden" name="skus[]" value="<?= h($sku) ?>">
</td>
<td><?= h($item['name']) ?></td>
<td><span class="badge bg-light text-dark border"><?= h(currency($item['price'])) ?></span></td>
<td>
<input type="number" name="qty[<?= h($sku) ?>]" class="form-control text-center" value="<?= isset($_POST['qty'][$sku]) ? (int)$_POST['qty'][$sku] : 1 ?>" min="1">
</td>
<td class="text-center">
<button type="button" class="btn btn-sm btn-outline-danger rounded-circle" onclick="this.closest('tr').remove();" title="<?= h(tr('إزالة', 'Remove')) ?>">
<i class="bi bi-x-lg"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php if(empty($items)):
?><tr>
<td colspan="5" class="text-center text-muted py-4">
<i class="bi bi-inbox fs-3 d-block mb-2"></i>
<?= h(tr('لم يتم تحديد أصناف', 'No items selected')) ?>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if(!empty($items)):
?><div class="mt-3">
<button type="submit" class="btn btn-primary">
<i class="bi bi-arrow-clockwise"></i> <?= h(tr('تحديث المعاينة', 'Update Preview')) ?>
</button>
</div>
<?php endif; ?>
</form>
</div>
</div>
</div>
<?php if (!empty($labelsToPrint)): ?>
<div class="print-preview mt-4">
<h5 class="text-center no-print text-muted mb-3"><i class="bi bi-view-list"></i> <?= h(tr('معاينة الطباعة', 'Print Preview')) ?></h5>
<?php
$labelsPerPage = $tpl['cols'] * $tpl['rows'];
$pages = array_chunk($labelsToPrint, $labelsPerPage);
foreach ($pages as $pageIdx => $pageLabels):
?>
<div class="page bg-white">
<?php foreach ($pageLabels as $label):
?><div class="label-item">
<div class="label-company"><?= h($companyName) ?></div>
<div class="label-name"><?= h($label['name']) ?></div>
<div class="label-barcode">
<svg class="barcode"
jsbarcode-format="code128"
jsbarcode-value="<?= h($label['sku']) ?>"
jsbarcode-displayvalue="false"
jsbarcode-width="1"
jsbarcode-height="15"
jsbarcode-margin="0">
</svg>
</div>
<div class="label-sku"><?= h($label['sku']) ?></div>
<div class="label-price"><?= h(currency($label['price'])) ?></div>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</div>
<div class="text-center mt-4 mb-5 no-print">
<button type="button" class="btn btn-success btn-lg px-5 py-3 shadow-lg rounded-pill" onclick="window.print()">
<i class="bi bi-printer fs-4"></i> <?= h(tr('الطباعة الآن', 'Print Labels Now')) ?>
</button>
</div>
<?php endif; ?>
<script>
document.addEventListener("DOMContentLoaded", function() {
if (typeof JsBarcode !== 'undefined') {
JsBarcode(".barcode").init();
}
});
</script>
</body>
</html>

35
privacy-policy.php Normal file
View File

@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/includes/app.php';
$forcePublic = true;
$pageTitle = tr('سياسة الخصوصية', 'Privacy Policy');
$metaDescription = tr('اطّلع على سياسة الخصوصية الخاصة بنا وكيفية التعامل مع البيانات في المتجر والنظام.', 'Read our privacy policy and how data is handled across the store and admin system.');
$privacyContent = trim((string) get_setting('privacy_policy_content'));
require __DIR__ . '/includes/header.php';
?>
<section class="py-5">
<div class="row justify-content-center">
<div class="col-lg-10 col-xl-8">
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-body p-4 p-md-5">
<div class="mb-4">
<span class="badge text-bg-light border mb-3"><?= h(tr('صفحة قانونية', 'Legal page')) ?></span>
<h1 class="h2 fw-bold mb-3"><?= h(tr('سياسة الخصوصية', 'Privacy Policy')) ?></h1>
<p class="text-muted mb-0"><?= h(tr('آخر تحديث يتم بواسطة إدارة النظام عند حفظ النص من الإعدادات.', 'This page is updated by the admin team whenever the content is saved from Settings.')) ?></p>
</div>
<?php if ($privacyContent !== ''): ?>
<article class="legal-content lh-lg"><?= nl2br(h($privacyContent)) ?></article>
<?php else: ?>
<div class="alert alert-warning rounded-4 mb-0">
<strong><?= h(tr('لم تتم إضافة المحتوى بعد.', 'Content not added yet.')) ?></strong>
<div class="small mt-2"><?= h(tr('يمكنك إضافته من الإعدادات ← السياسات والشروط.', 'You can add it from Settings → Policies.')) ?></div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</section>
<?php require __DIR__ . '/includes/footer.php'; ?>

147
profile.php Normal file
View File

@ -0,0 +1,147 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_auth();
$pageTitle = current_lang() === 'ar' ? 'الملف الشخصي' : 'Profile';
$activeNav = 'profile';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'update_profile') {
$name_ar = trim($_POST['name_ar'] ?? '');
$name_en = trim($_POST['name_en'] ?? '');
$password = $_POST['password'] ?? '';
if (empty($name_ar) || empty($name_en)) {
set_flash('danger', tr('الرجاء إدخال الاسم باللغتين', 'Please provide names in both languages'));
} else {
$update_sql = "UPDATE users SET name_ar = ?, name_en = ?";
$params = [$name_ar, $name_en];
// Handle avatar upload
$avatarPath = $user['avatar'] ?? null;
if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/assets/images/users/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$fileTmpPath = $_FILES['avatar']['tmp_name'];
$fileName = $_FILES['avatar']['name'];
$fileExtension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
$allowedfileExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (in_array($fileExtension, $allowedfileExtensions)) {
$newFileName = md5(time() . $fileName) . '.' . $fileExtension;
$dest_path = $uploadDir . $newFileName;
if (move_uploaded_file($fileTmpPath, $dest_path)) {
$avatarPath = 'assets/images/users/' . $newFileName;
$update_sql .= ", avatar = ?";
$params[] = $avatarPath;
}
} else {
set_flash('danger', tr('نوع الملف غير مدعوم للصورة الشخصية.', 'Unsupported file type for profile picture.'));
}
}
if (!empty($password)) {
$update_sql .= ", password = ?";
$params[] = password_hash($password, PASSWORD_DEFAULT);
}
$update_sql .= " WHERE id = ?";
$params[] = $user['id'];
$stmt = db()->prepare($update_sql);
if ($stmt->execute($params)) {
// Update session data
$_SESSION['auth_user']['name_ar'] = $name_ar;
$_SESSION['auth_user']['name_en'] = $name_en;
if (isset($avatarPath)) {
$_SESSION['auth_user']['avatar'] = $avatarPath;
}
set_flash('success', tr('تم تحديث الملف الشخصي بنجاح', 'Profile updated successfully'));
redirect_to('profile.php');
} else {
set_flash('danger', tr('حدث خطأ أثناء التحديث', 'Error updating profile'));
}
}
}
// Refresh user data from session just in case it was updated
$user = $_SESSION['auth_user'];
require_once __DIR__ . '/includes/header.php';
?>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow-sm border-0 rounded-4">
<div class="card-header bg-white text-center py-4 border-0">
<div class="mb-3">
<?php if (!empty($user['avatar'])): ?>
<img src="<?= h($user['avatar']) ?>" alt="Profile Picture" class="rounded-circle shadow-sm" style="width: 120px; height: 120px; object-fit: cover; border: 3px solid #fff;">
<?php else: ?>
<i class="bi bi-person-circle text-primary" style="font-size: 4rem;"></i>
<?php endif; ?>
</div>
<h4 class="mb-0 fw-bold"><?= h($pageTitle) ?></h4>
<p class="text-muted small mt-1"><?= h(role_label($user['role'])) ?> · <?= h(branch_label($user['branch_code'])) ?></p>
</div>
<div class="card-body p-4 pt-0">
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="action" value="update_profile">
<div class="mb-4 text-center">
<label for="avatarInput" class="btn btn-sm btn-outline-primary rounded-pill px-3">
<i class="bi bi-camera me-1"></i> <?= h(tr('تغيير الصورة الشخصية', 'Change Profile Picture')) ?>
</label>
<input type="file" id="avatarInput" name="avatar" class="d-none" accept="image/*" onchange="previewAvatar(this)">
</div>
<div class="mb-3">
<label class="form-label text-muted small fw-bold"><?= h(tr('اسم المستخدم', 'Username')) ?></label>
<input type="text" class="form-control bg-light" value="<?= h($user['username']) ?>" readonly>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label text-muted small fw-bold"><?= h(tr('الاسم (عربي) *', 'Name (AR) *')) ?></label>
<input type="text" name="name_ar" class="form-control" value="<?= h($user['name_ar']) ?>" required>
</div>
<div class="col-md-6 mt-3 mt-md-0">
<label class="form-label text-muted small fw-bold"><?= h(tr('الاسم (إنجليزي) *', 'Name (EN) *')) ?></label>
<input type="text" name="name_en" class="form-control" value="<?= h($user['name_en']) ?>" required>
</div>
</div>
<div class="mb-4">
<label class="form-label text-muted small fw-bold"><?= h(tr('كلمة المرور الجديدة', 'New Password')) ?></label>
<input type="password" name="password" class="form-control" placeholder="<?= h(tr('اتركه فارغاً إذا لم ترغب بالتغيير', 'Leave blank to keep unchanged')) ?>">
</div>
<div class="d-grid mt-4">
<button type="submit" class="btn btn-primary py-2 fw-bold">
<i class="bi bi-save me-1"></i> <?= h(tr('حفظ التغييرات', 'Save Changes')) ?>
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
function previewAvatar(input) {
if (input.files && input.files[0]) {
// You could add a simple JS preview here if desired
// For now, let's just submit or show a quick indication
const fileName = input.files[0].name;
const label = document.querySelector('label[for="avatarInput"]');
label.innerHTML = '<i class="bi bi-check-circle me-1"></i> ' + fileName;
label.classList.replace('btn-outline-primary', 'btn-success');
label.classList.add('text-white');
}
}
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

160
purchases.php Normal file
View File

@ -0,0 +1,160 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('purchases', 'show');
$pageTitle = tr('المشتريات', 'Purchases');
$activeNav = 'purchases';
// Search logic
$search = $_GET['q'] ?? '';
$page = max(1, (int)($_GET['p'] ?? 1));
$limit = 10;
$offset = ($page - 1) * $limit;
$params = [];
$where = ' WHERE 1=1 ';
if ($search) {
$where .= ' AND (supplier_name LIKE :search OR reference_no LIKE :search) ';
$params[':search'] = "%$search%";
}
// Pagination counts
$countSql = 'SELECT COUNT(*) FROM purchase_orders' . $where;
$countStmt = db()->prepare($countSql);
foreach ($params as $key => $value) {
$countStmt->bindValue($key, $value);
}
$countStmt->execute();
$total = $countStmt->fetchColumn();
$totalPages = max(1, ceil($total / $limit));
// Fetch Data
$sql = 'SELECT * FROM purchase_orders' . $where . ' ORDER BY purchase_date DESC LIMIT :limit OFFSET :offset';
$stmt = db()->prepare($sql);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$purchaseRows = $stmt->fetchAll();
require __DIR__ . '/includes/header.php';
?>
<section class="mb-4">
<div class="row g-4 align-items-center mb-3">
<div class="col-lg-8">
<h3 class="h5 mb-2"><i class="bi bi-bag-plus me-2"></i><?= h(tr('قائمة المشتريات', 'Purchase List')) ?></h3>
<p class="text-muted mb-0"><?= h(tr('عرض المشتريات من الموردين وتحديث المخزون.', 'View supplier purchases and inventory updates.')) ?></p>
</div>
<div class="col-lg-4 text-lg-end">
<a href="new_purchase.php" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> <?= h(tr('إضافة فاتورة مشتريات', 'Add Purchase Invoice')) ?>
</a>
</div>
</div>
<form class="d-flex mb-3" method="GET" action="purchases.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>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 text-center">
<thead class="table-light">
<tr>
<th><?= h(tr('المورد', 'Supplier')) ?></th>
<th><?= h(tr('المرجع', 'Reference')) ?></th>
<th><?= h(tr('الفرع', 'Branch')) ?></th>
<th><?= h(tr('المجموع', 'Subtotal')) ?></th>
<th><?= h(tr('الضريبة', 'VAT')) ?></th>
<th><?= h(tr('الإجمالي', 'Total Amount')) ?></th>
<th><?= h(tr('الحالة', 'Status')) ?></th>
<th><?= h(tr('التاريخ', 'Date')) ?></th>
<th><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody class="border-top-0">
<?php if(empty($purchaseRows)): ?>
<tr><td colspan="7" class="text-center text-muted py-4"><?= h(tr('لا توجد بيانات', 'No data found')) ?></td></tr>
<?php endif; ?>
<?php foreach ($purchaseRows as $row): ?>
<tr>
<td><?= h($row['supplier_name'] ?: '-') ?></td>
<td><?= h($row['reference_no']) ?></td>
<td><?= h(branch_label($row['branch_code'])) ?></td>
<td class="text-muted"><?= h(currency((float)$row['total_amount'] - (float)($row['vat_amount'] ?? 0))) ?></td>
<td class="text-muted text-danger"><?= h(currency((float)$row['vat_amount'])) ?></td>
<td class="fw-bold text-success"><?= h(currency((float)$row['total_amount'])) ?></td>
<td>
<?php if ($row['status'] === '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('مكتمل', 'Completed')) ?></span>
<?php endif; ?>
</td>
<td><?= h(date('Y-m-d', strtotime((string)$row['purchase_date']))) ?></td>
<td>
<button class="btn btn-sm btn-outline-primary rounded-circle shadow-sm" style="width: 34px; height: 34px; padding: 0;" onclick="mockEdit()" 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="mockDelete()" title="<?= h(tr('حذف', 'Delete')) ?>">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($totalPages > 1): ?>
<nav class="mt-4">
<ul class="pagination justify-content-center mb-0">
<?php for($i=1; $i<=$totalPages; $i++): ?>
<li class="page-item <?= $i === $page ? 'active' : '' ?>">
<a class="page-link" href="<?= h(url_for('purchases.php', ['p' => $i, 'q' => $search])) ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
</ul>
</nav>
<?php endif; ?>
</section>
<script>
function mockEdit() {
Swal.fire({
title: '<?= h(tr('تعديل (غير متاح)', 'Edit (Disabled)')) ?>',
text: '<?= h(tr('تعديل الفواتير غير متاح حالياً.', 'Editing invoices is currently disabled.')) ?>',
icon: 'info',
confirmButtonText: '<?= h(tr('حسناً', 'OK')) ?>'
});
}
function mockDelete() {
Swal.fire({
title: '<?= h(tr('هل أنت متأكد؟', 'Are you sure?')) ?>',
text: '<?= h(tr('هل تريد حذف هذه الفاتورة؟ (هذه الميزة غير متاحة حالياً)', "Do you want to delete this invoice? (Currently disabled)")) ?>',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#dc3545',
cancelButtonColor: '#6c757d',
confirmButtonText: '<?= h(tr('نعم، احذف', 'Yes, delete it!')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>'
}).then((result) => {
if (result.isConfirmed) {
Swal.fire(
'<?= h(tr('مرفوض!', 'Denied!')) ?>',
'<?= h(tr('حذف الفاتورة غير مدعوم في هذه النسخة.', 'Deleting invoice is not supported in this version.')) ?>',
'error'
);
}
});
}
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>

883
reports.php Normal file
View File

@ -0,0 +1,883 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('reports', 'show');
$pageTitle = tr('التقارير', 'Reports');
$activeNav = 'reports';
$tab = $_GET['tab'] ?? 'summary';
$dbError = null;
if ($tab === 'sales') {
$dateFrom = $_GET['date_from'] ?? date('Y-m-01');
$dateTo = $_GET['date_to'] ?? date('Y-m-t');
$branchFilter = $_GET['branch'] ?? '';
$params = [];
$where = base_sales_query_filters($params, null, $branchFilter ?: null);
$where .= ' AND DATE(sale_date) >= :date_from AND DATE(sale_date) <= :date_to';
$params[':date_from'] = $dateFrom;
$params[':date_to'] = $dateTo;
$sql = 'SELECT * FROM sales_orders' . $where . ' ORDER BY sale_date DESC';
try {
$stmt = db()->prepare($sql);
foreach ($params as $k => $v) {
$stmt->bindValue($k, $v);
}
$stmt->execute();
$salesReport = $stmt->fetchAll();
} catch(Throwable $e) {
$dbError = $e->getMessage();
$salesReport = [];
}
} elseif ($tab === 'orders') {
$branchFilter = $_GET['branch'] ?? '';
$params = [];
$where = base_sales_query_filters($params, null, $branchFilter ?: null);
$where .= " AND status = 'order'";
$sql = 'SELECT * FROM sales_orders' . $where . ' ORDER BY sale_date ASC';
try {
$stmt = db()->prepare($sql);
foreach ($params as $k => $v) {
$stmt->bindValue($k, $v);
}
$stmt->execute();
$followUpOrders = $stmt->fetchAll();
$onlineStmt = db()->query("SELECT * FROM online_orders WHERE status IN ('pending', 'accepted') ORDER BY created_at ASC");
$onlineOrders = $onlineStmt->fetchAll();
} catch(Throwable $e) {
$dbError = $e->getMessage();
$followUpOrders = [];
$onlineOrders = [];
}
} elseif ($tab === 'daily') {
$reportDate = $_GET['date'] ?? date('Y-m-d');
$branchFilter = $_GET['branch'] ?? '';
$dailyTotals = [
'seller' => [],
'outlet' => [],
'payment' => [],
'total' => 0.0,
'invoice_count' => 0,
'date' => $reportDate,
'branch' => $branchFilter,
];
try {
$dailyTotals = daily_sales_breakdown($reportDate, $branchFilter ?: null);
} catch(Throwable $e) {
$dbError = $e->getMessage();
}
} elseif ($tab === 'expenses') {
$dateFrom = $_GET['date_from'] ?? date('Y-m-01');
$dateTo = $_GET['date_to'] ?? date('Y-m-t');
$branchFilter = $_GET['branch'] ?? '';
$categoryFilter = $_GET['category_id'] ?? '';
$params = [];
$whereConditions = ["DATE(e.expense_date) >= :date_from", "DATE(e.expense_date) <= :date_to"];
$params[':date_from'] = $dateFrom;
$params[':date_to'] = $dateTo;
if ($user['role'] !== 'owner') {
$ubranches = get_user_branches($user);
if ($branchFilter && $branchFilter === 'general') {
$whereConditions[] = "e.branch_code IS NULL";
} elseif ($branchFilter && in_array($branchFilter, $ubranches, true)) {
$whereConditions[] = "e.branch_code = :branch";
$params[':branch'] = $branchFilter;
} else {
if (empty($ubranches)) {
$whereConditions[] = "e.branch_code IS NULL";
} else {
$inQuery = implode(',', array_fill(0, count($ubranches), '?'));
// We must use numbered placeholders if mixing with named placeholders?
// PDO might not like mixing ? and :name.
// Let's create named placeholders for in query.
$namedParams = [];
foreach($ubranches as $i => $ub) {
$key = ':ubranch_' . $i;
$namedParams[] = $key;
$params[$key] = $ub;
}
$inQuery = implode(', ', $namedParams);
$whereConditions[] = "(e.branch_code IN ($inQuery) OR e.branch_code IS NULL)";
}
}
} else {
if ($branchFilter) {
if ($branchFilter === 'general') {
$whereConditions[] = "e.branch_code IS NULL";
} else {
$whereConditions[] = "e.branch_code = :branch";
$params[':branch'] = $branchFilter;
}
}
}
if ($categoryFilter) {
$whereConditions[] = "e.category_id = :category_id";
$params[':category_id'] = $categoryFilter;
}
$where = " WHERE " . implode(" AND ", $whereConditions);
$sql = "SELECT e.*, c.name_ar as category_name_ar, c.name_en as category_name_en, u.username as created_by_name
FROM expenses e
LEFT JOIN expense_categories c ON e.category_id = c.id
LEFT JOIN users u ON e.created_by = u.id
" . $where . "
ORDER BY e.expense_date DESC";
try {
$stmt = db()->prepare($sql);
foreach ($params as $k => $v) {
$stmt->bindValue($k, $v);
}
$stmt->execute();
$expensesReport = $stmt->fetchAll();
$catStmt = db()->query("SELECT id, name_ar, name_en FROM expense_categories ORDER BY name_ar");
$expenseCategories = $catStmt->fetchAll();
} catch(Throwable $e) {
$dbError = $e->getMessage();
$expensesReport = [];
$expenseCategories = [];
}
} else {
$report = ['gross' => 0.0, 'branch_totals' => [], 'payment_totals' => [], 'product_totals' => [], 'sales_count' => 0];
try {
$report = report_metrics();
} catch (Throwable $e) {
$dbError = $e->getMessage();
}
}
require __DIR__ . '/includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
<div>
<h1 class="h3 mb-0"><?= h($pageTitle) ?></h1>
</div>
</div>
<ul class="nav nav-tabs mb-4 d-print-none">
<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>
<li class="nav-item">
<a class="nav-link <?= $tab === 'orders' ? 'active' : '' ?>" href="reports.php?tab=orders"><?= h(tr('طلبات للمتابعة', 'Follow-up Orders')) ?></a>
</li>
<li class="nav-item">
<a class="nav-link<?= $tab === 'expenses' ? 'active' : '' ?>" href="reports.php?tab=expenses"><?= h(tr('تقرير المصروفات', 'Expenses Report')) ?></a>
</li>
</ul>
<?php if ($dbError): ?>
<div class="alert alert-danger d-print-none"><?= h($dbError) ?></div>
<?php endif; ?>
<?php if ($tab === 'sales'): ?>
<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="sales">
<div class="col-md-3">
<label class="form-label"><?= h(tr('من تاريخ', 'From Date')) ?></label>
<input type="date" name="date_from" class="form-control" value="<?= h($dateFrom) ?>">
</div>
<div class="col-md-3">
<label class="form-label"><?= h(tr('إلى تاريخ', 'To Date')) ?></label>
<input type="date" name="date_to" class="form-control" value="<?= h($dateTo) ?>">
</div>
<div class="col-md-4">
<label class="form-label"><?= h(tr('الفرع', 'Branch')) ?></label>
<select name="branch" class="form-select">
<option value=""><?= h(tr('جميع الفروع', 'All Branches')) ?></option>
<?php
$availableBranches = get_user_branches_assoc($user);
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>
</form>
</div>
</div>
<div class="d-none d-print-block mb-4 text-center">
<h2><?= h(tr('تقرير المبيعات', 'Sales Report')) ?></h2>
<p><?= h(tr('الفترة من', 'Period from')) ?> <?= h($dateFrom) ?> <?= h(tr('إلى', 'to')) ?> <?= h($dateTo) ?></p>
<?php if ($branchFilter): ?>
<p><?= h(tr('الفرع:', 'Branch:')) ?> <?= h(branch_label($branchFilter)) ?></p>
<?php endif; ?>
</div>
<div class="card border-0 p-4 mb-4">
<div class="d-flex justify-content-between align-items-center mb-3 d-print-none">
<h3 class="h5 mb-0"><?= h(tr('نتائج التقرير', 'Report Results')) ?></h3>
<button class="btn btn-outline-secondary btn-sm" onclick="window.print()"><i class="bi bi-printer"></i> <?= h(tr('طباعة', 'Print')) ?></button>
</div>
<?php if(empty($salesReport)): ?>
<p class="text-muted"><?= h(tr('لا توجد مبيعات في هذه الفترة.', 'No sales found in this period.')) ?></p>
<?php else: ?>
<div class="table-responsive">
<table class="table table-bordered table-striped align-middle">
<thead>
<tr>
<th><?= h(tr('التاريخ', 'Date')) ?></th>
<th><?= h(tr('رقم الإيصال', 'Receipt No')) ?></th>
<th><?= h(tr('الكاشير', 'Cashier')) ?></th>
<th><?= h(tr('الفرع', 'Branch')) ?></th>
<th><?= h(tr('طريقة الدفع', 'Payment Method')) ?></th>
<th><?= h(tr('الحالة', 'Status')) ?></th>
<th class="text-end"><?= h(tr('المجموع', 'Subtotal')) ?></th>
<th class="text-end"><?= h(tr('الضريبة', 'VAT')) ?></th>
<th class="text-end"><?= h(tr('الإجمالي', 'Total')) ?></th>
</tr>
</thead>
<tbody>
<?php
$subtotalSum = 0;
$vatSum = 0;
$totalSum = 0;
foreach($salesReport as $sale):
$vat = (float) ($sale['vat_amount'] ?? 0);
$total = (float) $sale['total_amount'];
$calcSubtotal = $total - $vat;
$subtotalSum += $calcSubtotal;
$vatSum += $vat;
$totalSum += $total;
?>
<tr>
<td><?= h(date('Y-m-d H:i', strtotime((string)$sale['sale_date']))) ?></td>
<td><?= h((string)$sale['receipt_no']) ?></td>
<td><?= h((string)$sale['cashier_name']) ?></td>
<td><?= h(branch_label((string)$sale['branch_code'])) ?></td>
<td><?= h(ucfirst((string)$sale['payment_method'])) ?></td>
<td>
<?php if (($sale['status'] ?? 'completed') === 'order'): ?>
<span class="badge bg-warning text-dark"><i class="bi bi-clock"></i> <?= h(tr('طلب حجز', 'Order')) ?></span>
<?php else: ?>
<span class="badge bg-success"><i class="bi bi-check-circle"></i> <?= h(tr('مدفوع', 'Paid')) ?></span>
<?php endif; ?>
</td>
<td class="text-end"><?= h(currency($calcSubtotal)) ?></td>
<td class="text-end"><?= h(currency($vat)) ?></td>
<td class="text-end fw-bold"><?= h(currency($total)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot class="table-dark">
<tr>
<td colspan="6" class="text-end"><?= h(tr('الإجمالي الكلي', 'Grand Total')) ?></td>
<td class="text-end fw-bold fs-6"><?= h(currency($subtotalSum)) ?></td>
<td class="text-end fw-bold fs-6"><?= h(currency($vatSum)) ?></td>
<td class="text-end fw-bold fs-5"><?= h(currency($totalSum)) ?></td>
</tr>
</tfoot>
</table>
</div>
<?php endif; ?>
</div>
<?php elseif ($tab === 'expenses'): ?>
<style>
@media print {
body { font-size: 13pt !important; background: #fff; }
.table-bordered th, .table-bordered td { border: 1px solid #000 !important; padding: 6px; }
.table th { background-color: #f0f0f0 !important; -webkit-print-color-adjust: exact; color: #000; }
.d-print-none { display: none !important; }
.surface-card { box-shadow: none !important; border: none !important; padding: 0 !important; }
.print-header { display: block !important; margin-bottom: 30px; }
.page-break-inside-avoid { page-break-inside: avoid; }
}
.print-header { display: none; }
</style>
<!-- Filters (Hidden on Print) -->
<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="expenses">
<div class="col-md-3">
<label class="form-label"><?= h(tr('من تاريخ', 'From Date')) ?></label>
<input type="date" name="date_from" class="form-control" value="<?= h($dateFrom) ?>">
</div>
<div class="col-md-3">
<label class="form-label"><?= h(tr('إلى تاريخ', 'To Date')) ?></label>
<input type="date" name="date_to" class="form-control" value="<?= h($dateTo) ?>">
</div>
<div class="col-md-2">
<label class="form-label"><?= h(tr('التصنيف', 'Category')) ?></label>
<select name="category_id" class="form-select">
<option value=""><?= h(tr('الكل', 'All')) ?></option>
<?php foreach($expenseCategories as $cat):
if ($cat['id'] == $categoryFilter) {
$catName = $cat['name_ar'] . ' / ' . $cat['name_en'];
break;
}
?>
<option value="<?= $cat['id'] ?>" <?= $categoryFilter == $cat['id'] ? 'selected' : '' ?> >
<?= h($cat['name_ar'] . ' / ' . $cat['name_en']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label"><?= h(tr('الفرع', 'Branch')) ?></label>
<select name="branch" class="form-select">
<option value=""><?= h(tr('جميع الفروع', 'All Branches')) ?></option>
<option value="general" <?= $branchFilter === 'general' ? 'selected' : '' ?>><?= h(tr('مصروفات عامة', 'General Expenses')) ?></option>
<?php
$availableBranches = get_user_branches_assoc($user);
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>
</form>
</div>
</div>
<!-- Formal Print Header -->
<div class="print-header">
<div class="text-center mb-4" style="border-bottom: 2px solid #000; padding-bottom: 15px;">
<h2 style="font-weight: bold; margin:0;"><?= h(tr('تقرير المصروفات التفصيلي', 'Detailed Expenses Report')) ?></h2>
<p style="margin: 5px 0 0; color: #555;"><?= h(tr('تاريخ الإصدار:', 'Generated on:')) ?> <?= date('Y-m-d H:i') ?></p>
</div>
<div class="row mb-4" style="border: 1px solid #000; padding: 15px; margin: 0;">
<div class="col-8">
<div style="margin-bottom: 5px;"><strong><?= h(tr('الفترة:', 'Period:')) ?></strong> <?= h($dateFrom) ?> <?= h(tr('إلى', 'to')) ?> <?= h($dateTo) ?></div>
<?php if ($categoryFilter):
$catName = '';
foreach ($expenseCategories as $c) {
if ($c['id'] == $categoryFilter) {
$catName = $c['name_ar'] . ' / ' . $c['name_en'];
break;
}
}
?>
<div style="margin-bottom: 5px;"><strong><?= h(tr('التصنيف:', 'Category:')) ?></strong> <?= h($catName) ?></div>
<?php endif; ?>
<div><strong><?= h(tr('الفرع:', 'Branch:')) ?></strong> <?= h($branchFilter ? ($branchFilter === 'general' ? tr('مصروفات عامة', 'General Expenses') : branch_label($branchFilter)) : tr('جميع الفروع', 'All Branches')) ?></div>
</div>
<div class="col-4 text-end">
<div><strong><?= h(tr('بواسطة:', 'Requested By:')) ?></strong> <?= h($user['username']) ?></div>
</div>
</div>
</div>
<!-- Results Table -->
<div class="card border-0 p-4 mb-4">
<div class="d-flex justify-content-between align-items-center mb-3 d-print-none">
<h3 class="h5 mb-0"><?= h(tr('نتائج التقرير', 'Report Results')) ?></h3>
<button class="btn btn-outline-secondary btn-sm" onclick="window.print()"><i class="bi bi-printer"></i> <?= h(tr('طباعة رسمية', 'Formal Print')) ?></button>
</div>
<?php if(empty($expensesReport)): ?>
<p class="text-muted d-print-none"><?= h(tr('لا توجد مصروفات في هذه الفترة.', 'No expenses found in this period.')) ?></p>
<div class="print-header text-center py-5">
<h4><?= h(tr('لا توجد بيانات لهذه الفترة', 'No data for this period')) ?></h4>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-bordered table-striped align-middle" style="width: 100%;">
<thead>
<tr>
<th><?= h(tr('التاريخ', 'Date')) ?></th>
<th><?= h(tr('الفرع', 'Branch')) ?></th>
<th><?= h(tr('التصنيف', 'Category')) ?></th>
<th><?= h(tr('الوصف', 'Description')) ?></th>
<th><?= h(tr('المسجل', 'Logged By')) ?></th>
<th class="text-end"><?= h(tr('المبلغ', 'Amount')) ?></th>
</tr>
</thead>
<tbody>
<?php
$totalSum = 0;
foreach($expensesReport as $exp):
$totalSum += (float) $exp['amount'];
?>
<tr>
<td><?= h(date('Y-m-d', strtotime((string)$exp['expense_date']))) ?></td>
<td>
<?php if (empty($exp['branch_code'])): ?>
<?= h(tr('عام', 'General')) ?>
<?php else:
echo h(branch_label((string)$exp['branch_code']));
endif; ?>
</td>
<td><?= h($exp['category_name_ar'] . ' / ' . $exp['category_name_en']) ?></td>
<td><?= h((string)$exp['description']) ?></td>
<td><?= h((string)$exp['created_by_name']) ?></td>
<td class="text-end fw-bold"><?= h(currency((float)$exp['amount'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot class="table-dark">
<tr>
<td colspan="5" class="text-end"><strong><?= h(tr('الإجمالي الكلي', 'Grand Total')) ?></strong></td>
<td class="text-end fw-bold fs-5"><strong><?= h(currency($totalSum)) ?></strong></td>
</tr>
</tfoot>
</table>
</div>
<!-- Signatures (Print Only) -->
<div class="print-header mt-5 pt-5 page-break-inside-avoid">
<div class="row text-center mt-5">
<div class="col-4">
<p><strong><?= h(tr('المحاسب / المُعد', 'Prepared by / Accountant')) ?></strong></p>
<hr style="width: 60%; margin: 40px auto 0; border-top: 1px solid #000;">
</div>
<div class="col-4">
<p><strong><?= h(tr('المراجع', 'Checked by')) ?></strong></p>
<hr style="width: 60%; margin: 40px auto 0; border-top: 1px solid #000;">
</div>
<div class="col-4">
<p><strong><?= h(tr('المدير العام', 'General Manager')) ?></strong></p>
<hr style="width: 60%; margin: 40px auto 0; border-top: 1px solid #000;">
</div>
</div>
</div>
<?php endif; ?>
</div>
<?php elseif ($tab === 'orders'): ?>
<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="orders">
<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 = get_user_branches_assoc($user);
foreach ($availableBranches as $code => $b):
?>
<option value="<?= h($code) ?>" <?= $branchFilter === $code ? 'selected' : '' ?>><?= h(branch_label($code)) ?></option>
<?php endforeach; ?>
</select>
</div>
</form>
</div>
</div>
<div class="d-none d-print-block mb-4 text-center">
<h2><?= h(tr('طلبات للمتابعة', 'Follow-up Orders')) ?></h2>
<p><?= h(date('Y-m-d H:i')) ?></p>
<?php if ($branchFilter): ?>
<p><?= h(tr('الفرع:', 'Branch:')) ?> <?= h(branch_label($branchFilter)) ?></p>
<?php endif; ?>
</div>
<div class="card border-0 p-4 mb-4">
<div class="d-flex justify-content-between align-items-center mb-3 d-print-none">
<h3 class="h5 mb-0"><?= h(tr('طلبات حجز بانتظار الدفع', 'Reservation orders pending payment')) ?></h3>
<button class="btn btn-outline-secondary btn-sm" onclick="window.print()"><i class="bi bi-printer"></i> <?= h(tr('طباعة', 'Print')) ?></button>
</div>
<?php if(empty($followUpOrders)): ?>
<p class="text-muted"><?= h(tr('لا توجد طلبات للمتابعة.', 'No follow-up orders.')) ?></p>
<?php else: ?>
<div class="table-responsive">
<table class="table table-bordered table-hover align-middle">
<thead>
<tr>
<th><?= h(tr('التاريخ', 'Date')) ?></th>
<th><?= h(tr('رقم الإيصال', 'Receipt No')) ?></th>
<th><?= h(tr('العميل', 'Customer')) ?></th>
<th><?= h(tr('هاتف العميل', 'Customer Phone')) ?></th>
<th><?= h(tr('الفرع', 'Branch')) ?></th>
<th class="text-end"><?= h(tr('المبلغ المستحق', 'Due Amount')) ?></th>
<th class="d-print-none text-end"><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach($followUpOrders as $sale): ?>
<tr>
<td><?= h(date('Y-m-d H:i', strtotime((string)$sale['sale_date']))) ?></td>
<td><?= h((string)$sale['receipt_no']) ?></td>
<?php
$rawCustomerName = (string) ($sale['customer_name'] ?: '-');
$displayPhone = (string) ($sale['customer_phone'] ?? '');
if ($displayPhone === '' && str_contains($rawCustomerName, ' - ')) {
$parts = explode(' - ', $rawCustomerName);
$lastPart = trim(end($parts));
if (preg_match('/^[0-9+\s]+$/', $lastPart)) {
$displayPhone = $lastPart;
array_pop($parts);
$rawCustomerName = trim(implode(' - ', $parts));
}
}
$displayPhone = ltrim(preg_replace('/[^0-9]/', '', $displayPhone), '0');
if ($displayPhone !== '') {
if (str_starts_with($displayPhone, '968') && strlen($displayPhone) > 8) {
$displayPhone = substr($displayPhone, 3);
}
}
?>
<td><?= h($rawCustomerName) ?></td>
<td dir="ltr"><?= h($displayPhone !== '' ? $displayPhone : '-') ?></td>
<td><?= h(branch_label((string)$sale['branch_code'])) ?></td>
<td class="text-end fw-bold text-danger"><?= h(currency((float)$sale['total_amount'])) ?></td>
<td class="d-print-none text-end">
<a class="btn btn-sm btn-light border" href="<?= h(url_for('sale.php', ['id' => $sale['id']])) ?>"><?= h(tr('عرض', 'View')) ?></a>
<button class="btn btn-sm btn-success" onclick="markPaidFromReports(<?= $sale['id'] ?>)"><i class="bi bi-check-lg"></i> <?= h(tr('دفع', 'Pay')) ?></button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<script>
function markPaidFromReports(id) {
Swal.fire({ title: "<?= h(tr('تأكيد الدفع', 'Confirm payment')) ?>", text: "<?= h(tr('هل أنت متأكد من تأكيد دفع هذا الطلب؟', 'Are you sure you want to confirm payment for this order?')) ?>", icon: "warning", showCancelButton: true, confirmButtonText: "<?= h(tr('نعم', 'Yes')) ?>", cancelButtonText: "<?= h(tr('إلغاء', 'Cancel')) ?>" }).then((result) => { if(result.isConfirmed) { window.location.href = 'sales.php?mark_paid=' + id + '&redirect=reports.php?tab=orders'; } });
}
</script>
<?php endif; ?>
</div>
<!-- Online Orders Section -->
<div class="card border-0 p-4 mt-4 mb-4">
<div class="d-flex justify-content-between align-items-center mb-3 d-print-none">
<h3 class="h5 mb-0"><i class="bi bi-globe me-2 text-primary"></i><?= h(tr('طلبات المتجر الإلكتروني (نشطة)', 'Active Online Store Orders')) ?></h3>
<a class="btn btn-outline-primary btn-sm" href="online_orders.php"><i class="bi bi-box-arrow-up-right"></i> <?= h(tr('إدارة الطلبات', 'Manage Orders')) ?></a>
</div>
<?php if(empty($onlineOrders)): ?>
<p class="text-muted"><?= h(tr('لا توجد طلبات متجر نشطة حالياً.', 'No active online orders currently.')) ?></p>
<?php else: ?>
<div class="table-responsive">
<table class="table table-bordered table-hover align-middle">
<thead class="table-light">
<tr>
<th><?= h(tr('التاريخ', 'Date')) ?></th>
<th><?= h(tr('رقم الطلب', 'Order ID')) ?></th>
<th><?= h(tr('العميل', 'Customer')) ?></th>
<th><?= h(tr('هاتف العميل', 'Customer Phone')) ?></th>
<th><?= h(tr('العنوان', 'Address')) ?></th>
<th><?= h(tr('الحالة', 'Status')) ?></th>
<th class="text-end"><?= h(tr('الإجمالي', 'Total')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach($onlineOrders as $order): ?>
<tr>
<td><?= h(date('Y-m-d H:i', strtotime((string)$order['created_at']))) ?></td>
<td>#<?= h((string)$order['id']) ?></td>
<td><?= h((string)($order['customer_name'] ?: '-')) ?></td>
<td dir="ltr"><?= h((string)($order['customer_phone'] ? phone_display($order['customer_phone']) : '-')) ?></td>
<td><?= h((string)($order['customer_address'] ?: '-')) ?></td>
<td>
<?php if ($order['status'] === 'pending'): ?>
<span class="badge bg-warning text-dark"><?= h(tr('قيد الانتظار', 'Pending')) ?></span>
<?php elseif ($order['status'] === 'accepted'): ?>
<span class="badge bg-info text-dark"><?= h(tr('مقبول', 'Accepted')) ?></span>
<?php endif; ?>
</td>
<td class="text-end fw-bold text-primary"><?= h(currency((float)$order['total_amount'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?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-3">
<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-3">
<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 = get_user_branches_assoc($user);
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="submit" class="btn btn-success w-100" formaction="api/wablas_daily_report.php" formmethod="POST"><i class="bi bi-whatsapp"></i> <?= h(tr('إرسال واتساب', 'Send WhatsApp')) ?></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="card border-0 p-4 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="card border-0 p-4 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="card border-0 p-4 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="card border-0 p-4 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 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="card border-0 p-4 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="card border-0 p-4 h-100">
<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-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="card border-0 p-4 h-100">
<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-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; ?>
<?php require __DIR__ . '/includes/footer.php'; ?>

507
sale.php Normal file
View File

@ -0,0 +1,507 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('sales', 'show');
$pageTitle = tr('تفاصيل الفاتورة الضريبية', 'Tax Invoice Details');
$activeNav = 'sales';
$id = (int) ($_GET['id'] ?? 0);
$sale = null;
$dbError = null;
if ($id > 0) {
try {
$sale = fetch_sale($id);
} catch (Throwable $e) {
$dbError = $e->getMessage();
}
}
$paymentSummary = $sale ? sale_payment_summary($sale) : ['paid_amount' => 0, 'due_amount' => 0, 'payment_status' => 'paid'];
$isEidSale = $sale && (($sale['order_type'] ?? 'standard') === 'eid');
$autoPrint = isset($_GET['print']) && $_GET['print'] === '1';
// Company Info for Invoice
$companyName = current_lang() === 'ar' ? get_setting('company_name_ar', 'حلوى الريامي') : get_setting('company_name_en', 'Al Riyami Sweets');
$companyAddress = get_setting('company_address', '');
$companyVat = get_setting('company_vat_number', '300123456789012');
$companyEmail = 'info@flatlogic.com';
$companyPhone = phone_display(get_setting('company_phone', ''));
require __DIR__ . '/includes/header.php';
?>
<style>
/* Formal A4 Invoice Styles */
.invoice-container {
background: #fff;
max-width: 210mm; /* A4 width */
margin: 2rem auto;
padding: 2cm;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
border: 1px solid #ddd;
color: #333;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 13px; /* Smaller, formal text */
line-height: 1.5;
}
.invoice-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 2px solid #333;
padding-bottom: 1.5rem;
margin-bottom: 2rem;
}
.company-logo-info {
display: flex;
align-items: center;
gap: 1.5rem;
}
.invoice-logo {
width: 80px;
height: 80px;
background: #f8f9fa;
border: 1px solid #eee;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.invoice-logo svg {
width: 50px;
height: 50px;
fill: none;
stroke: #333;
stroke-width: 2;
}
.company-details h2 {
font-size: 18px;
font-weight: bold;
margin: 0 0 5px 0;
text-transform: uppercase;
color: #000;
}
.company-details p {
margin: 2px 0;
color: #555;
}
.invoice-title-meta {
text-align: <?= current_lang() === 'ar' ? 'left' : 'right' ?>;
}
.invoice-title-meta h1 {
font-size: 24px;
font-weight: bold;
margin: 0 0 15px 0;
text-transform: uppercase;
color: #000;
letter-spacing: 1px;
}
.meta-grid {
display: grid;
grid-template-columns: auto auto;
gap: 5px 15px;
text-align: <?= current_lang() === 'ar' ? 'right' : 'left' ?>;
}
<?php if (current_lang() === 'ar'): ?>
.meta-grid { direction: rtl; }
<?php endif; ?>
.meta-label {
font-weight: bold;
color: #666;
}
.meta-val {
font-weight: bold;
color: #000;
}
.invoice-parties {
display: flex;
justify-content: space-between;
margin-bottom: 2rem;
}
.party-box {
width: 48%;
}
.party-title {
font-weight: bold;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
margin-bottom: 10px;
color: #000;
text-transform: uppercase;
}
.party-info h4 {
font-size: 14px;
font-weight: bold;
margin: 0 0 5px 0;
color: #000;
}
.party-info p {
margin: 0;
color: #555;
}
.invoice-table-wrapper {
margin-bottom: 2rem;
}
.formal-table {
width: 100%;
border-collapse: collapse;
}
.formal-table th,
.formal-table td {
border: 1px solid #ccc;
padding: 8px 10px;
vertical-align: top;
}
.formal-table th {
background-color: #f9f9f9;
font-weight: bold;
color: #000;
text-transform: uppercase;
}
.formal-table th.text-right,
.formal-table td.text-right {
text-align: right;
}
.formal-table th.text-center,
.formal-table td.text-center {
text-align: center;
}
.item-name {
font-weight: bold;
color: #000;
margin-bottom: 3px;
}
.item-sku {
color: #666;
font-size: 11px;
}
.invoice-summary {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.invoice-notes {
width: 50%;
font-size: 12px;
}
.notes-title {
font-weight: bold;
text-transform: uppercase;
margin-bottom: 5px;
}
.totals-box {
width: 40%;
}
.totals-table {
width: 100%;
border-collapse: collapse;
}
.totals-table td {
padding: 6px 10px;
border-bottom: 1px solid #eee;
}
.totals-table tr:last-child td {
border-bottom: none;
}
.total-label {
font-weight: bold;
color: #666;
}
.total-amount {
text-align: right;
font-weight: bold;
color: #000;
}
.grand-total-row td {
font-size: 16px;
border-top: 2px solid #000;
padding-top: 10px;
}
.grand-total-row .total-label {
color: #000;
text-transform: uppercase;
}
.invoice-footer {
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px solid #ccc;
display: flex;
justify-content: space-between;
align-items: flex-end;
font-size: 12px;
color: #666;
}
.qr-code {
width: 80px;
height: 80px;
background: #fff;
border: 1px solid #ccc;
padding: 5px;
}
.qr-code img {
width: 100%;
height: 100%;
display: block;
}
.print-actions {
position: sticky;
top: 1rem;
z-index: 100;
background: rgba(255, 255, 255, 0.95);
padding: 15px 25px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
border: 1px solid #ddd;
max-width: 210mm;
margin: 0 auto 2rem auto;
}
/* Print Styles */
@media print {
body { background: #fff; }
.main-sidebar, .main-header, .print-actions, .alert, .footer-section { display: none !important; }
.main-content { margin: 0 !important; padding: 0 !important; width: 100% !important; }
.invoice-container {
box-shadow: none;
border: none;
margin: 0;
padding: 0;
max-width: 100%;
width: 100%;
}
@page { size: A4; margin: 15mm; }
}
</style>
<div class="container-fluid mb-5">
<?php if ($dbError): ?>
<div class="alert alert-warning"><?= h($dbError) ?></div>
<?php elseif (!$sale): ?>
<div class="alert alert-info text-center mt-4">
<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 mt-3" href="<?= h(url_for('sales.php')) ?>"><?= h(tr('العودة إلى المبيعات', 'Back to sales')) ?></a>
</div>
<?php else: ?>
<!-- Print Actions -->
<div class="print-actions d-flex justify-content-between align-items-center">
<a href="<?= h($isEidSale ? url_for('eid_orders.php') : url_for('sales.php')) ?>" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-<?= current_lang() === 'ar' ? 'right' : 'left' ?> me-1"></i> <?= h($isEidSale ? tr('رجوع لطلبات العيد', 'Back to Eid Orders') : tr('رجوع للسجل', 'Back to ledger')) ?>
</a>
<div>
<a href="<?= h(url_for('edit_sale.php', ['id' => $sale['id']])) ?>" class="btn btn-outline-primary btn-sm me-2">
<i class="bi bi-pencil me-1"></i><?= h(tr('تعديل الفاتورة', 'Edit Invoice')) ?>
</a>
<button onclick="window.print()" class="btn btn-dark btn-sm px-4">
<i class="bi bi-printer me-2"></i><?= h(tr('طباعة الفاتورة', 'Print Invoice')) ?>
</button>
</div>
</div>
<!-- Formal A4 Invoice -->
<div class="invoice-container">
<div class="invoice-header">
<div class="company-logo-info">
<div class="invoice-logo">
<!-- Formal Box Logo Placeholder -->
<svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<div class="company-details">
<h2><?= h($companyName) ?></h2>
<p><?= h($companyAddress) ?></p>
<p><?= h(tr('الرقم الضريبي:', 'VAT NO:')) ?> <?= h($companyVat) ?></p>
<p><?= h($companyPhone) ?> | <?= h($companyEmail) ?></p>
</div>
</div>
<div class="invoice-title-meta">
<h1><?= ($sale['status'] ?? 'completed') === 'order' ? h(tr('طلب حجز', 'ORDER')) : h(tr('فاتورة ضريبية', 'TAX INVOICE')) ?></h1>
<div class="meta-grid">
<div class="meta-label"><?= h(tr('رقم الفاتورة', 'Invoice No.')) ?>:</div>
<div class="meta-val">#<?= h($sale['receipt_no']) ?></div>
<div class="meta-label"><?= h(tr('تاريخ الإصدار', 'Date Issued')) ?>:</div>
<div class="meta-val"><?= h(date('Y-m-d H:i', strtotime((string) $sale['sale_date']))) ?></div>
<?php
$pm = $sale['payment_method'];
$pmLabel = $pm;
if ($pm === 'cash') $pmLabel = tr('نقدي', 'Cash');
elseif ($pm === 'card') $pmLabel = tr('بطاقة', 'Card');
elseif ($pm === 'transfer') $pmLabel = tr('تحويل', 'Transfer');
elseif ($pm === 'pay_later') $pmLabel = tr('آجل', 'Pay Later');
else $pmLabel = ucfirst(str_replace('_', ' ', (string)$pm));
?>
<div class="meta-label"><?= h(tr('طريقة الدفع', 'Payment')) ?>:</div>
<div class="meta-val"><?= h($pmLabel) ?></div>
<div class="meta-label"><?= h(tr('الحالة', 'Status')) ?>:</div>
<div class="meta-val"><?= h(payment_status_label($paymentSummary['payment_status'])) ?><?= ($sale['status'] ?? 'completed') === 'order' ? ' · ' . h(tr('طلب حجز', 'Order')) : '' ?></div>
<?php if ($isEidSale): ?>
<div class="meta-label"><?= h(tr('التسلسل الموسمي', 'Season serial')) ?>:</div>
<div class="meta-val"><?= h(eid_serial_label((int) ($sale['eid_serial_no'] ?? 0))) ?></div>
<div class="meta-label"><?= h(tr('نوع الطلب', 'Order type')) ?>:</div>
<div class="meta-val"><?= h(sale_order_type_label((string) ($sale['order_type'] ?? 'standard'))) ?></div>
<div class="meta-label"><?= h(tr('تاريخ التسليم', 'Delivery date')) ?>:</div>
<div class="meta-val"><?= h((string) ($sale['delivery_date'] ?: '—')) ?></div>
<div class="meta-label"><?= h(tr('حالة التجهيز', 'Prep status')) ?>:</div>
<div class="meta-val"><?= h(eid_delivery_status_label((string) ($sale['delivery_status'] ?? 'pending'))) ?></div>
<?php endif; ?>
</div>
</div>
</div>
<div class="invoice-parties">
<div class="party-box">
<div class="party-title"><?= h(tr('العميل', 'Customer')) ?></div>
<div class="party-info">
<h4><?= h((string) ($sale['customer_name'] ?: tr('عميل نقدي', 'Walk-in Customer'))) ?></h4>
</div>
</div>
<div class="party-box">
<div class="party-title"><?= h(tr('معلومات المتجر', 'Store Details')) ?></div>
<div class="party-info">
<p><strong><?= h(tr('الفرع:', 'Branch:')) ?></strong> <?= h(branch_label((string) $sale['branch_code'])) ?></p>
<p><strong><?= h(tr('بواسطة:', 'Served By:')) ?></strong> <?= h((string) $sale['cashier_name']) ?></p>
</div>
</div>
</div>
<div class="invoice-table-wrapper">
<table class="formal-table">
<thead>
<tr>
<th width="5%" class="text-center">#</th>
<th width="45%" class="<?= current_lang() === 'ar' ? 'text-right' : 'text-left' ?>"><?= h(tr('وصف الصنف', 'Item Description')) ?></th>
<th width="15%" class="text-center"><?= h(tr('الكمية', 'Qty')) ?></th>
<th width="15%" class="text-center"><?= h(tr('سعر الوحدة', 'Unit Price')) ?></th>
<th width="20%" class="<?= current_lang() === 'ar' ? 'text-left' : 'text-right' ?>"><?= h(tr('الإجمالي', 'Line Total')) ?></th>
</tr>
</thead>
<tbody>
<?php $i = 1; foreach ($sale['items'] as $item): ?>
<tr>
<td class="text-center"><?= $i++ ?></td>
<td>
<div class="item-name"><?= h(current_lang() === 'ar' ? ($item['name_ar'] ?? $item['sku']) : ($item['name_en'] ?? $item['sku'])) ?></div>
<div class="item-sku"><?= h(tr('رمز الصنف:', 'SKU:')) ?> <?= h($item['sku']) ?></div>
</td>
<td class="text-center"><?= h((string) ($item['qty'] ?? 0)) ?></td>
<td class="text-center"><?= h(number_format((float) ($item['price'] ?? 0), 3)) ?></td>
<td class="<?= current_lang() === 'ar' ? 'text-left' : 'text-right' ?>"><?= h(number_format((float) ($item['line_total'] ?? 0), 3)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="invoice-summary">
<div class="invoice-notes">
<?php if (!empty($sale['notes'])): ?>
<div class="notes-title"><?= h(tr('ملاحظات', 'Notes')) ?></div>
<p><?= nl2br(h((string) $sale['notes'])) ?></p>
<?php endif; ?>
</div>
<div class="totals-box">
<table class="totals-table">
<tr>
<td class="total-label"><?= h(tr('المجموع الفرعي', 'Subtotal')) ?></td>
<td class="total-amount"><?= h(number_format((float) $sale['subtotal'], 3)) ?></td>
</tr>
<tr>
<td class="total-label"><?= h(tr('ضريبة القيمة المضافة (مضافة)', 'VAT (Added)')) ?></td>
<td class="total-amount"><?= number_format((float)($sale['vat_amount'] ?? 0), 3) ?></td>
</tr>
<tr class="grand-total-row">
<td class="total-label"><?= h(tr('الإجمالي', 'Total')) ?></td>
<td class="total-amount"><?= h(number_format((float) $sale['total_amount'], 3)) ?> <?= h(tr('ر.ع', 'OMR')) ?></td>
</tr>
<tr>
<td class="total-label"><?= h(tr('المدفوع', 'Paid')) ?></td>
<td class="total-amount"><?= h(number_format((float) $paymentSummary['paid_amount'], 3)) ?> <?= h(tr('ر.ع', 'OMR')) ?></td>
</tr>
<tr>
<td class="total-label"><?= h(tr('المتبقي', 'Due')) ?></td>
<td class="total-amount" style="color: <?= $paymentSummary['due_amount'] > 0 ? '#dc3545' : '#198754' ?>;"><?= h(number_format((float) $paymentSummary['due_amount'], 3)) ?> <?= h(tr('ر.ع', 'OMR')) ?></td>
</tr>
</table>
</div>
</div>
<div class="invoice-footer">
<div class="footer-text">
<p style="margin-bottom: 5px; font-weight: bold; color: #000;">
<?= h(tr('شكراً لتعاملكم معنا!', 'Thank you for your business!')) ?>
</p>
<p style="margin: 0;">
<?= h(tr('هذه الفاتورة معتمدة ضريبياً، يُرجى الاحتفاظ بها لضمان حقوقك.', 'This is a certified tax invoice. Please keep it for your records.')) ?>
</p>
</div>
<!-- Simple QR code simulation using an inline SVG to keep it formal and clean -->
<div class="qr-code">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" fill="#fff"/>
<path d="M10 10h20v20H10zM70 10h20v20H70zM10 70h20v20H10z" fill="#000"/>
<path d="M15 15h10v10H15zM75 15h10v10H75zM15 75h10v10H15z" fill="#fff"/>
<path d="M35 10h10v10H35zM50 10h10v10H50zM35 25h10v10H35zM50 25h20v10H50zM75 35h15v10H75zM10 35h20v10H10zM35 40h10v20H35zM50 50h20v20H50zM10 50h15v10H10zM75 60h15v30H75zM35 70h10v20H35zM50 80h10v10H50z" fill="#000"/>
</svg>
</div>
</div>
</div>
<?php endif; ?>
</div>
<?php require __DIR__ . '/includes/footer.php'; ?>
<?php if ($autoPrint && $sale): ?>
<script>
window.addEventListener('load', function () {
setTimeout(function () {
window.print();
}, 300);
});
</script>
<?php endif; ?>

523
sales.php Normal file
View File

@ -0,0 +1,523 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('sales', 'show');
$mode = isset($_GET['mode']) && in_array($_GET['mode'], ['pos', 'normal'], true) ? $_GET['mode'] : null;
$branch = isset($_GET['branch']) && array_key_exists($_GET['branch'], branches()) ? $_GET['branch'] : null;
$search = trim((string) ($_GET['q'] ?? ''));
$statusFilter = trim((string) ($_GET['status'] ?? ''));
$paymentStatus = trim((string) ($_GET['payment_status'] ?? ''));
$deliveryOptions = eid_delivery_status_options();
$deliveryStatus = trim((string) ($_GET['delivery_status'] ?? ''));
$dateFrom = trim((string) ($_GET['date_from'] ?? ''));
$dateTo = trim((string) ($_GET['date_to'] ?? ''));
$legacyPaymentStatuses = ['paid', 'partial', 'unpaid'];
if ($paymentStatus === '' && in_array($statusFilter, $legacyPaymentStatuses, true)) {
$paymentStatus = $statusFilter;
$statusFilter = '';
}
if (!in_array($statusFilter, ['', 'order', 'completed'], true)) {
$statusFilter = '';
}
if (!in_array($paymentStatus, array_merge([''], $legacyPaymentStatuses), true)) {
$paymentStatus = '';
}
if ($deliveryStatus !== '' && !array_key_exists($deliveryStatus, $deliveryOptions)) {
$deliveryStatus = '';
}
$allowedBranches = $user && $user['role'] !== 'owner' ? get_user_branches($user) : array_keys(branches());
$activeNav = $statusFilter === 'order' ? 'sales_orders' : 'sales';
$pageTitle = $statusFilter === 'order' ? tr('الطلبات', 'Orders') : tr('المبيعات', 'Sales Ledger');
$canDeleteSales = $user['role'] === 'owner' || has_permission('sales', 'del');
if (isset($_GET['mark_paid']) && is_numeric($_GET['mark_paid'])) {
try {
$id = (int) $_GET['mark_paid'];
$sale = fetch_sale($id);
if ($sale) {
$summary = sale_payment_summary($sale);
if ($summary['due_amount'] > 0.0005) {
apply_sale_payment($id, $summary['due_amount'], true);
}
}
} catch (Throwable $e) {}
$redirect = $_GET['redirect'] ?? 'sales.php';
header('Location: ' . $redirect);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'delete_sale') {
if (!$canDeleteSales) {
set_flash('danger', tr('ليس لديك صلاحية حذف الفواتير.', 'You do not have permission to delete invoices.'));
redirect_to('sales.php', $_GET);
}
$id = (int) ($_POST['id'] ?? 0);
try {
ensure_sales_table();
$sale = fetch_sale($id);
if (!$sale) {
set_flash('warning', tr('الفاتورة غير موجودة.', 'Invoice was not found.'));
} elseif ($user['role'] !== 'owner' && !in_array((string) ($sale['branch_code'] ?? ''), $allowedBranches, true)) {
set_flash('danger', tr('لا يمكنك حذف هذه الفاتورة.', 'You cannot delete this invoice.'));
} else {
$stmt = db()->prepare('DELETE FROM sales_orders WHERE id = :id');
$stmt->execute([':id' => $id]);
set_flash('success', tr('تم حذف الفاتورة نهائياً.', 'Invoice deleted permanently.'));
}
} catch (Throwable $e) {
set_flash('danger', tr('تعذر حذف الفاتورة.', 'Failed to delete invoice.'));
}
redirect_to('sales.php', $_GET);
}
$dbError = null;
$sales = [];
$totalPages = 1;
$page = max(1, (int)($_GET['p'] ?? 1));
$limit = 10;
$offset = ($page - 1) * $limit;
try {
ensure_sales_table();
$params = [];
$where = ' WHERE 1=1 ';
if ($mode) {
$where .= ' AND sale_mode = :sale_mode ';
$params[':sale_mode'] = $mode;
}
if ($branch) {
$where .= ' AND branch_code = :branch_code ';
$params[':branch_code'] = $branch;
}
if ($user && $user['role'] !== 'owner') {
$ubranches = get_user_branches($user);
if (empty($ubranches)) {
$where .= ' AND 1=0 ';
} else {
$namedParams = [];
foreach ($ubranches as $i => $ub) {
$key = ':v_branch_' . $i;
$namedParams[] = $key;
$params[$key] = $ub;
}
$where .= ' AND branch_code IN (' . implode(', ', $namedParams) . ') ';
}
}
if ($search !== '') {
$where .= ' AND (receipt_no LIKE :search OR cashier_name LIKE :search OR customer_name LIKE :search)';
$params[':search'] = "%$search%";
}
if ($paymentStatus !== '') {
$where .= ' AND payment_status = :payment_status ';
$params[':payment_status'] = $paymentStatus;
}
if ($deliveryStatus !== '') {
$where .= ' AND delivery_status = :delivery_status ';
$params[':delivery_status'] = $deliveryStatus;
}
if ($dateFrom !== '') {
$where .= ' AND DATE(sale_date) >= :date_from ';
$params[':date_from'] = $dateFrom;
}
if ($dateTo !== '') {
$where .= ' AND DATE(sale_date) <= :date_to ';
$params[':date_to'] = $dateTo;
}
if ($statusFilter === 'order') {
$where .= " AND status = 'order' ";
} elseif ($statusFilter === 'completed') {
$where .= " AND COALESCE(status, 'completed') = 'completed' ";
} else {
$where .= " AND COALESCE(status, 'completed') <> 'order' ";
}
// Pagination counts
$countSql = 'SELECT COUNT(*) FROM sales_orders' . $where;
$countStmt = db()->prepare($countSql);
foreach ($params as $key => $value) {
$countStmt->bindValue($key, $value);
}
$countStmt->execute();
$total = $countStmt->fetchColumn();
$totalPages = max(1, ceil($total / $limit));
// Fetch Data
$sql = 'SELECT * FROM sales_orders' . $where . ' ORDER BY sale_date DESC LIMIT :limit OFFSET :offset';
$stmt = db()->prepare($sql);
foreach ($params as $key => $value) {
$stmt->bindValue($key, $value);
}
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$sales = $stmt->fetchAll();
} catch (Throwable $e) {
$dbError = $e->getMessage();
}
$queryState = static function (array $extra = []) use ($search, $branch, $mode, $statusFilter, $paymentStatus, $deliveryStatus, $dateFrom, $dateTo): array {
return array_filter(array_merge([
'q' => $search,
'branch' => $branch,
'mode' => $mode,
'status' => $statusFilter,
'payment_status' => $paymentStatus,
'delivery_status' => $deliveryStatus,
'date_from' => $dateFrom,
'date_to' => $dateTo,
], $extra), static fn ($value) => $value !== null && $value !== '');
};
$hasAdvancedFilters = $mode !== null || $paymentStatus !== '' || $branch !== null || $deliveryStatus !== '' || $dateFrom !== '' || $dateTo !== '' || $statusFilter !== '';
require __DIR__ . '/includes/header.php';
?>
<section class="mb-4">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3 mb-3">
<div>
<h3 class="h5 mb-1"><i class="bi bi-journal-text me-2"></i><?= h($statusFilter === 'order' ? tr('قائمة الطلبات', 'Orders list') : tr('سجل الفواتير', 'Invoice ledger')) ?></h3>
<div class="small text-muted"><?= h($statusFilter === 'order' ? tr('هذه القائمة تعرض طلبات البيع العادي المحفوظة بحالة طلب.', 'This list shows normal sales saved with order status.') : tr('ابحث بصرياً في أحدث المبيعات مع صلاحيات حسب الدور والفرع.', 'Scan the latest sales with role and branch scoping.')) ?></div>
</div>
<div class="d-flex gap-2 flex-wrap">
<a class="btn btn-sm <?= $mode === null ? 'btn-dark' : 'btn-outline-secondary' ?>" href="<?= h(url_for('sales.php', $queryState(['mode' => null, 'p' => null]))) ?>"><?= h(tr('الكل', 'All')) ?></a>
<a class="btn btn-sm <?= $mode === 'pos' ? 'btn-dark' : 'btn-outline-secondary' ?>" href="<?= h(url_for('sales.php', $queryState(['mode' => 'pos', 'p' => null]))) ?>">POS</a>
<a class="btn btn-sm <?= $mode === 'normal' ? 'btn-dark' : 'btn-outline-secondary' ?>" href="<?= h(url_for('sales.php', $queryState(['mode' => 'normal', 'p' => null]))) ?>"><?= h(tr('فاتورة', 'Invoice')) ?></a>
</div>
</div>
<form class="mb-4" method="GET" action="sales.php">
<div class="d-flex justify-content-end align-items-center mb-3">
<details class="eid-advanced-toggle" <?= $hasAdvancedFilters ? 'open' : '' ?>>
<summary class="btn btn-sm btn-outline-secondary">
<i class="bi bi-sliders me-1"></i><?= h(tr('فلاتر إضافية', 'More filters')) ?>
</summary>
<div class="eid-advanced-panel">
<div class="row g-2">
<div class="col-12 col-md-4">
<label class="form-label mb-1" for="sales-mode"><?= h(tr('القناة', 'Channel')) ?></label>
<select id="sales-mode" class="form-select form-select-sm" name="mode">
<option value=""><?= h(tr('الكل', 'All')) ?></option>
<option value="normal" <?= $mode === 'normal' ? 'selected' : '' ?>><?= h(tr('فاتورة', 'Invoice')) ?></option>
<option value="pos" <?= $mode === 'pos' ? 'selected' : '' ?>>POS</option>
</select>
</div>
<div class="col-12 col-md-4">
<label class="form-label mb-1" for="sales-payment-status"><?= h(tr('حالة الدفع', 'Payment')) ?></label>
<select id="sales-payment-status" class="form-select form-select-sm" name="payment_status">
<option value=""><?= h(tr('كل الحالات', 'All statuses')) ?></option>
<option value="paid" <?= $paymentStatus === 'paid' ? 'selected' : '' ?>><?= h(tr('مدفوع', 'Paid')) ?></option>
<option value="partial" <?= $paymentStatus === 'partial' ? 'selected' : '' ?>><?= h(tr('جزئي', 'Partial')) ?></option>
<option value="unpaid" <?= $paymentStatus === 'unpaid' ? 'selected' : '' ?>><?= h(tr('غير مدفوع', 'Unpaid')) ?></option>
</select>
</div>
<div class="col-12 col-md-4">
<label class="form-label mb-1" for="sales-status"><?= h(tr('نوع السجل', 'Record type')) ?></label>
<select id="sales-status" class="form-select form-select-sm" name="status">
<option value=""><?= h(tr('العرض الافتراضي', 'Default view')) ?></option>
<option value="completed" <?= $statusFilter === 'completed' ? 'selected' : '' ?>><?= h(tr('كل الفواتير المكتملة', 'Completed invoices')) ?></option>
<option value="order" <?= $statusFilter === 'order' ? 'selected' : '' ?>><?= h(tr('طلبات الحجز', 'Order reservations')) ?></option>
</select>
</div>
</div>
</div>
</details>
</div>
<div class="row g-3 align-items-end">
<div class="col-12 col-md-3">
<label class="form-label mb-1" for="sales-search"><?= h(tr('بحث سريع', 'Quick search')) ?></label>
<input id="sales-search" type="text" class="form-control form-control-sm" name="q" value="<?= h($search) ?>" placeholder="<?= h(tr('بحث بالإيصال، الكاشير، العميل أو الهاتف...', 'Search receipt, cashier, customer or phone...')) ?>">
</div>
<div class="col-12 col-md-2">
<label class="form-label mb-1" for="sales-branch"><?= h(tr('الفرع', 'Branch')) ?></label>
<select id="sales-branch" class="form-select form-select-sm" name="branch">
<option value=""><?= h(tr('كل الفروع', 'All branches')) ?></option>
<?php foreach (branches() as $branchCode => $branchLabel): ?>
<?php if ($user['role'] !== 'owner' && !in_array($branchCode, $allowedBranches, true)) { continue; } ?>
<option value="<?= h($branchCode) ?>" <?= $branch === $branchCode ? 'selected' : '' ?>><?= h(branch_label($branchCode)) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-2">
<label class="form-label mb-1" for="sales-delivery-status"><?= h(tr('التجهيز', 'Prep')) ?></label>
<select id="sales-delivery-status" class="form-select form-select-sm" name="delivery_status">
<option value=""><?= h(tr('كل الحالات', 'All statuses')) ?></option>
<?php foreach ($deliveryOptions as $value => $label): ?>
<option value="<?= h($value) ?>" <?= $deliveryStatus === $value ? 'selected' : '' ?>><?= h($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12 col-md-3">
<label class="form-label mb-1">
<?= h($statusFilter === 'order' ? tr('فترة الطلب', 'Order window') : tr('فترة البيع', 'Sale window')) ?>
</label>
<div class="d-flex gap-2">
<input id="sales-date-from" type="date" class="form-control form-control-sm" name="date_from" value="<?= h($dateFrom) ?>" aria-label="<?= h(tr('من تاريخ', 'From date')) ?>">
<input id="sales-date-to" type="date" class="form-control form-control-sm" name="date_to" value="<?= h($dateTo) ?>" aria-label="<?= h(tr('إلى تاريخ', 'To date')) ?>">
</div>
</div>
<div class="col-12 col-md-2 d-flex gap-2">
<button type="submit" class="btn btn-sm btn-dark w-50"><i class="bi bi-funnel me-1"></i><?= h(tr('تطبيق', 'Apply')) ?></button>
<a class="btn btn-sm btn-outline-secondary w-50" href="<?= h(url_for('sales.php', ['status' => $statusFilter])) ?>"><?= h(tr('إعادة ضبط', 'Reset')) ?></a>
</div>
</div>
</form>
<?php if ($dbError): ?>
<div class="alert alert-warning"><?= h($dbError) ?></div>
<?php elseif (!$sales): ?>
<div class="empty-state">
<h4><?= h(tr('لا توجد نتائج', 'No sales found')) ?></h4>
<p><?= h(tr('جرّب إنشاء فاتورة جديدة من صفحة البيع.', 'Try creating a new invoice from the sale page.')) ?></p>
<div class="d-flex gap-2 justify-content-center flex-wrap">
<a class="btn btn-dark" href="<?= h(url_for('pos.php')) ?>">POS</a>
<a class="btn btn-outline-secondary" href="<?= h(url_for('normal_sale.php')) ?>"><?= h(tr('فاتورة', 'Invoice')) ?></a>
</div>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 text-center">
<thead class="table-light">
<tr>
<th><?= h(tr('الإيصال', 'Receipt')) ?></th>
<th><?= h(tr('الفرع', 'Branch')) ?></th>
<th><?= h(tr('النوع', 'Type')) ?></th>
<th><?= h(tr('الكاشير', 'Cashier')) ?></th>
<th><?= h(tr('العميل', 'Customer')) ?></th>
<th><?= h(tr('الهاتف', 'Phone')) ?></th>
<th><?= h(tr('المجموع', 'Subtotal')) ?></th>
<th><?= h(tr('الضريبة', 'VAT')) ?></th>
<th><?= h(tr('الإجمالي', 'Total')) ?></th>
<th><?= h(tr('المدفوع', 'Paid')) ?></th>
<th><?= h(tr('المتبقي', 'Due')) ?></th>
<th><?= h(tr('الحالة', 'Status')) ?></th>
<th><?= h(tr('التاريخ', 'Date')) ?></th>
<th><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody class="border-top-0">
<?php foreach ($sales as $sale): ?>
<?php $paymentSummary = sale_payment_summary($sale); ?>
<tr>
<td>
<div class="fw-semibold"><?= h($sale['receipt_no']) ?></div>
<div class="small text-muted"><?= h((string) $sale['item_count']) ?> <?= h(tr('قطعة', 'items')) ?></div>
</td>
<td><?= h(branch_label((string) $sale['branch_code'])) ?></td>
<td><span class="badge text-bg-light border"><?= h(sale_mode_label((string) $sale['sale_mode'])) ?></span></td>
<td><?= h((string) $sale['cashier_name']) ?></td>
<?php
$rawCustomerName = (string) ($sale['customer_name'] ?: '-');
$displayPhone = '';
if (str_contains($rawCustomerName, ' - ')) {
$parts = explode(' - ', $rawCustomerName);
$lastPart = trim(end($parts));
if (preg_match('/^[0-9+\s]+$/', $lastPart)) {
$displayPhone = $lastPart;
array_pop($parts);
$rawCustomerName = trim(implode(' - ', $parts));
}
}
$displayPhone = ltrim(preg_replace('/[^0-9]/', '', $displayPhone), '0');
if ($displayPhone !== '') {
if (str_starts_with($displayPhone, '968') && strlen($displayPhone) > 8) {
$displayPhone = substr($displayPhone, 3);
}
}
?>
<td><?= h($rawCustomerName) ?></td>
<td dir="ltr"><?= h($displayPhone ?: '-') ?></td>
<td class="text-muted"><?= h(currency((float) $sale['total_amount'] - (float) ($sale['vat_amount'] ?? 0))) ?></td>
<td class="text-muted text-danger"><?= h(currency((float) $sale['vat_amount'])) ?></td>
<td class="fw-bold text-success"><?= h(currency((float) $sale['total_amount'])) ?></td>
<td class="fw-semibold text-primary"><?= h(currency((float) $paymentSummary['paid_amount'])) ?></td>
<td class="fw-semibold <?= $paymentSummary['due_amount'] > 0 ? 'text-danger' : 'text-muted' ?>"><?= h(currency((float) $paymentSummary['due_amount'])) ?></td>
<td>
<span class="badge <?= h(payment_status_badge_class($paymentSummary['payment_status'])) ?> px-3 py-2 rounded-pill">
<?php if ($paymentSummary['payment_status'] === 'partial'): ?>
<i class="bi bi-pie-chart"></i>
<?php elseif ($paymentSummary['payment_status'] === 'unpaid'): ?>
<i class="bi bi-clock-history"></i>
<?php else: ?>
<i class="bi bi-check-circle"></i>
<?php endif; ?>
<?= h(payment_status_label($paymentSummary['payment_status'])) ?>
</span>
<?php if (($sale['status'] ?? 'completed') === 'order'): ?>
<small class="d-block text-muted mt-1"><?= h(tr('طلب حجز', 'Order')) ?></small>
<?php elseif (($sale['payment_method'] ?? '') === 'pay_later'): ?>
<small class="d-block text-muted mt-1"><?= h(tr('دفع آجل', 'Pay Later')) ?></small>
<?php endif; ?>
</td>
<td><?= h(date('Y-m-d H:i', strtotime((string) $sale['sale_date']))) ?></td>
<td class="text-nowrap">
<div class="d-inline-flex align-items-center flex-nowrap gap-1">
<?php if ($paymentSummary['due_amount'] > 0.0005): ?>
<button class="btn btn-sm btn-outline-success rounded-circle shadow-sm" style="width: 30px; height: 30px; padding: 0; line-height: 28px;" onclick="receivePayment(<?= (int) $sale['id'] ?>, <?= json_encode((float) $sale['total_amount']) ?>, <?= json_encode((float) $paymentSummary['paid_amount']) ?>, <?= json_encode((float) $paymentSummary['due_amount']) ?>, <?= ($sale['status'] ?? 'completed') === 'order' ? 'true' : 'false' ?>)" title="<?= h(tr('استلام دفعة', 'Receive Payment')) ?>">
<i class="bi bi-cash-coin"></i>
</button>
<?php endif; ?>
<a class="btn btn-sm btn-light text-primary border rounded-circle shadow-sm" style="width: 30px; height: 30px; padding: 0; line-height: 28px; text-align: center;" href="<?= h(url_for('sale.php', ['id' => $sale['id']])) ?>" title="<?= h(tr('تفاصيل', 'Detail')) ?>">
<i class="bi bi-eye"></i>
</a>
<a class="btn btn-sm btn-outline-secondary rounded-circle shadow-sm" style="width: 30px; height: 30px; padding: 0; line-height: 28px; text-align: center;" href="<?= h(url_for('edit_sale.php', ['id' => $sale['id']])) ?>" title="<?= h(tr('تعديل', 'Edit')) ?>">
<i class="bi bi-pencil"></i>
</a>
<?php if ($canDeleteSales): ?>
<form method="post" action="" class="d-inline-block">
<input type="hidden" name="action" value="delete_sale">
<input type="hidden" name="id" value="<?= h($sale['id']) ?>">
<button type="button" class="btn btn-sm btn-outline-danger rounded-circle shadow-sm" style="width: 30px; height: 30px; padding: 0; line-height: 28px;" onclick="confirmDeleteSale(this.form)" title="<?= h(tr('حذف', 'Delete')) ?>">
<i class="bi bi-trash"></i>
</button>
</form>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($totalPages > 1): ?>
<nav class="mt-4">
<ul class="pagination justify-content-center mb-0">
<?php for($i=1; $i<=$totalPages; $i++): ?>
<li class="page-item <?= $i === $page ? 'active' : '' ?>">
<a class="page-link" href="<?= h(url_for('sales.php', $queryState(['p' => $i]))) ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
</ul>
</nav>
<?php endif; ?>
<?php endif; ?>
</section>
<script>
function formatPaymentPopupAmount(value) {
return Number(value || 0).toFixed(3);
}
async function receivePayment(id, totalAmount, paidAmount, dueAmount, completeOrder = false) {
const popupHtml = `
<div class="text-start">
<div class="row g-2 mb-3">
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('إجمالي الفاتورة', 'Total amount')) ?></div>
<div class="fw-bold text-dark">${formatPaymentPopupAmount(totalAmount)}</div>
</div>
</div>
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('المدفوع سابقاً', 'Already paid')) ?></div>
<div class="fw-bold text-primary">${formatPaymentPopupAmount(paidAmount)}</div>
</div>
</div>
<div class="col-4">
<div class="border rounded-3 p-2 h-100 bg-light">
<div class="small text-muted"><?= h(tr('المتبقي الحالي', 'Current remaining')) ?></div>
<div class="fw-bold text-danger">${formatPaymentPopupAmount(dueAmount)}</div>
</div>
</div>
</div>
<label for="swal-payment-amount" class="form-label fw-semibold mb-2"><?= h(tr('المبلغ المطلوب دفعه الآن', 'Amount to pay now')) ?></label>
<input id="swal-payment-amount" type="number" class="swal2-input mt-0" min="0.001" step="0.001" max="${formatPaymentPopupAmount(dueAmount)}" value="${formatPaymentPopupAmount(dueAmount)}">
<div class="d-flex justify-content-between align-items-center rounded-3 border px-3 py-2 bg-light mt-3">
<span class="small text-muted"><?= h(tr('المتبقي بعد الدفعة', 'Remaining after payment')) ?></span>
<strong id="swal-payment-remaining" class="text-success">0.000</strong>
</div>
</div>`;
const { isConfirmed, value: paymentAmount } = await Swal.fire({
title: '<?= h(tr('استلام دفعة', 'Receive Payment')) ?>',
html: popupHtml,
showCancelButton: true,
confirmButtonColor: '#198754',
confirmButtonText: '<?= h(tr('حفظ الدفعة', 'Save Payment')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>',
focusConfirm: false,
didOpen: () => {
const input = document.getElementById('swal-payment-amount');
const remainingEl = document.getElementById('swal-payment-remaining');
const updateRemaining = () => {
const amount = parseFloat(input.value || '0');
const safeAmount = Number.isFinite(amount) ? amount : 0;
const remaining = Math.max(dueAmount - safeAmount, 0);
remainingEl.textContent = formatPaymentPopupAmount(remaining);
remainingEl.className = remaining > 0.0005 ? 'text-danger' : 'text-success';
};
input.addEventListener('input', updateRemaining);
input.focus();
input.select();
updateRemaining();
},
preConfirm: () => {
const input = document.getElementById('swal-payment-amount');
const amount = parseFloat(input.value || '0');
if (!amount || amount <= 0) {
Swal.showValidationMessage('<?= h(tr('أدخل مبلغاً صحيحاً.', 'Enter a valid amount.')) ?>');
return false;
}
if (amount - dueAmount > 0.0005) {
Swal.showValidationMessage('<?= h(tr('المبلغ لا يمكن أن يتجاوز المتبقي.', 'Amount cannot exceed the due balance.')) ?>');
return false;
}
return formatPaymentPopupAmount(amount);
}
});
if (!isConfirmed || !paymentAmount) {
return;
}
const formData = new FormData();
formData.append('sale_id', String(id));
formData.append('payment_amount', String(paymentAmount));
if (completeOrder) {
formData.append('complete_order', '1');
}
const response = await fetch('api/sales_payment.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.success) {
await Swal.fire({ icon: 'success', text: data.message, confirmButtonText: 'OK' });
window.location.reload();
} else {
Swal.fire({ icon: 'error', text: data.error || '<?= h(tr('تعذر تسجيل الدفعة.', 'Could not record the payment.')) ?>' });
}
}
function mockEdit() {
Swal.fire({
title: '<?= h(tr('تعديل (غير متاح)', 'Edit (Disabled)')) ?>',
text: '<?= h(tr('تعديل الفواتير غير متاح لأسباب محاسبية.', 'Invoice editing is disabled for accounting reasons.')) ?>',
icon: 'info',
confirmButtonText: '<?= h(tr('حسناً', 'OK')) ?>'
});
}
function confirmDeleteSale(form) {
Swal.fire({
title: '<?= h(tr('هل أنت متأكد من حذف الفاتورة؟', 'Are you sure you want to delete this invoice?')) ?>',
text: '<?= h(tr('سيتم حذف الفاتورة نهائياً من النظام. استخدم هذا الخيار فقط عند الضرورة.', 'This will permanently delete the invoice from the system. Use this only when necessary.')) ?>',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#dc3545',
cancelButtonColor: '#6c757d',
confirmButtonText: '<?= h(tr('نعم، احذف', 'Yes, delete it!')) ?>',
cancelButtonText: '<?= h(tr('إلغاء', 'Cancel')) ?>'
}).then((result) => {
if (result.isConfirmed) {
form.submit();
}
});
}
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>

61
seed_items.php Normal file
View File

@ -0,0 +1,61 @@
<?php
require 'db/config.php';
require 'includes/pexels.php';
$db = db();
// Define items to insert
$items = [
['sku' => '10000001', 'name' => 'Laptop Pro 15', 'price' => 1200.000, 'base_stock' => 10, 'category_id' => 1, 'supplier_id' => 1, 'query' => 'laptop'],
['sku' => '10000002', 'name' => 'Wireless Mouse', 'price' => 25.500, 'base_stock' => 50, 'category_id' => 2, 'supplier_id' => 2, 'query' => 'computer mouse'],
['sku' => '10000003', 'name' => 'Mechanical Keyboard', 'price' => 85.000, 'base_stock' => 30, 'category_id' => 2, 'supplier_id' => 1, 'query' => 'keyboard'],
['sku' => '10000004', 'name' => 'USB-C Hub', 'price' => 40.000, 'base_stock' => 100, 'category_id' => 2, 'supplier_id' => 2, 'query' => 'usb cable'],
['sku' => '10000005', 'name' => 'Cotton T-Shirt', 'price' => 15.000, 'base_stock' => 200, 'category_id' => 3, 'supplier_id' => 3, 'query' => 't-shirt'],
];
foreach ($items as $item) {
// Check if sku already exists
$stmt = $db->prepare("SELECT id FROM items WHERE sku = ?");
$stmt->execute([$item['sku']]);
if ($stmt->fetch()) {
echo "SKU {" . $item['sku'] . "} already exists.\n";
continue;
}
$imageUrl = null;
$localImagePath = null;
// Fetch image from pexels
$url = 'https://api.pexels.com/v1/search?query=' . urlencode($item['query']) . '&orientation=square&per_page=1&page=1';
$data = pexels_get($url);
if ($data && !empty($data['photos'])) {
$photo = $data['photos'][0];
$src = $photo['src']['medium'] ?? ($photo['src']['original'] ?? null);
if ($src) {
$dest = __DIR__ . '/assets/images/items/' . $photo['id'] . '.jpg';
if (download_to($src, $dest)) {
$localImagePath = 'assets/images/items/' . $photo['id'] . '.jpg';
}
}
}
if (!$localImagePath) {
$localImagePath = 'https://picsum.photos/400?random=' . rand(1, 1000); // fallback
}
$stmt = $db->prepare("INSERT INTO items (sku, name, price, base_stock, vat, category_id, supplier_id, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
$item['sku'],
$item['name'],
$item['price'],
$item['base_stock'],
5.000, // default VAT
$item['category_id'],
$item['supplier_id'],
$localImagePath
]);
echo "Inserted {" . $item['name'] . "}\n";
}
echo "Done seeding items.\n";

624
shop.php Normal file
View File

@ -0,0 +1,624 @@
<?php
require_once __DIR__ . '/includes/app.php';
$forcePublic = true;
$pageTitle = tr('الطلب عبر الإنترنت', 'Online Ordering');
$shopPaymentStatus = trim((string) ($_GET['payment_status'] ?? ''));
$shopPaymentMessage = trim((string) ($_GET['message'] ?? ''));
$shopCanPayOnline = thawani_is_configured();
$shopShowPayOnline = thawani_is_enabled() || $shopCanPayOnline;
require __DIR__ . '/includes/header.php';
$db = db();
$stmt = $db->query("
SELECT i.*, c.name_ar as cat_ar, c.name_en as cat_en
FROM items i
LEFT JOIN categories c ON i.category_id = c.id
WHERE i.in_catalog = 1
ORDER BY c.id, i.name
");
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
$catalog = [];
foreach ($items as $item) {
$catName = current_lang() === 'ar' ? ($item['cat_ar'] ?? 'عام') : ($item['cat_en'] ?? 'General');
$catalog[$catName][] = $item;
}
?>
<style>
.shop-item-card {
transition: transform 0.2s, box-shadow 0.2s;
border-radius: 12px;
border: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
height: 100%;
}
.shop-item-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 24px rgba(0,0,0,0.1);
}
.shop-item-img {
height: 200px;
object-fit: cover;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
body { background: linear-gradient(135deg, #fdfbfb 0%, #ebedee 100%); background-attachment: fixed; min-height: 100vh; }
/* Hide scrollbar for category filter */
.category-filter-container::-webkit-scrollbar {
display: none;
}
.category-filter-container {
-ms-overflow-style: none;
scrollbar-width: none;
}
.shop-pagination-wrap {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.shop-pagination-status {
color: #6c757d;
font-weight: 600;
}
.shop-pagination .page-link {
border-radius: 999px !important;
margin: 0 4px;
border: 0;
box-shadow: 0 3px 10px rgba(0,0,0,0.06);
color: #0d6efd;
}
.shop-pagination .page-item.active .page-link {
background: #0d6efd;
color: #fff;
}
.shop-pagination .page-item.disabled .page-link {
color: #adb5bd;
box-shadow: none;
}
</style>
<div class="container py-5">
<!-- Sticky Header -->
<div class="sticky-top bg-white p-3 rounded-4 shadow-sm mb-4" style="top: 15px; z-index: 1020;">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3">
<div class="d-flex align-items-center gap-3">
<?php if (get_setting('company_logo')): ?>
<img src="<?= h(get_setting('company_logo')) ?>" alt="Logo" class="rounded shadow-sm bg-white" style="max-height: 55px; max-width: 120px; object-fit: contain; padding: 4px;">
<?php else: ?>
<div class="bg-primary bg-gradient text-white rounded shadow-sm d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
<i class="bi bi-shop fs-4"></i>
</div>
<?php endif; ?>
<div>
<h3 class="fw-bold text-primary mb-1" style="font-size: 1.5rem;"><?= h(get_setting('company_name_' . current_lang(), app_name())) ?></h3>
<p class="text-muted mb-0 d-none d-sm-block" style="font-size: 0.85rem;"><?= h(tr('اطلب الآن وسنقوم بتجهيز طلبك', 'Order now and we will prepare your request')) ?></p>
</div>
</div>
<div class="d-flex align-items-center gap-2 gap-md-3">
<button class="btn btn-primary rounded-pill px-3 px-md-4 py-2 position-relative shadow-sm fw-bold d-flex align-items-center gap-2" onclick="openCart()">
<i class="bi bi-cart3 fs-5"></i>
<span class="d-none d-md-inline"><?= h(tr('سلة المشتريات', 'Cart')) ?></span>
<span class="badge bg-danger rounded-pill fs-6" id="cartCount">0</span>
</button>
<div class="language-switcher bg-light border rounded-pill p-1 d-flex">
<a class="btn btn-sm <?= current_lang() === 'ar' ? 'btn-primary' : 'text-dark' ?> rounded-pill px-2 px-md-3 fw-bold" href="shop.php?lang=ar">AR</a>
<a class="btn btn-sm <?= current_lang() === 'en' ? 'btn-primary' : 'text-dark' ?> rounded-pill px-2 px-md-3 fw-bold" href="shop.php?lang=en">EN</a>
</div>
</div>
</div>
</div>
<?php if ($shopPaymentStatus !== ''): ?>
<?php
$alertClass = 'alert-info';
if ($shopPaymentStatus === 'paid') {
$alertClass = 'alert-success';
} elseif (in_array($shopPaymentStatus, ['cancelled', 'failed'], true)) {
$alertClass = 'alert-warning';
}
$defaultMessage = match ($shopPaymentStatus) {
'paid' => tr('تم الدفع بنجاح وتم استلام طلبك.', 'Payment completed successfully and your order was received.'),
'cancelled' => tr('تم إلغاء عملية الدفع. يمكنك المحاولة مرة أخرى أو اختيار الدفع لاحقاً.', 'Payment was cancelled. You can try again or choose Pay Later.'),
'failed' => tr('تعذر تأكيد الدفع. إذا تم الخصم يرجى مراجعة الإدارة.', 'We could not confirm the payment. If you were charged, please contact the store.'),
default => tr('تم تحديث حالة الطلب.', 'The order status has been updated.'),
};
?>
<div class="alert <?= h($alertClass) ?> rounded-4 shadow-sm mb-4">
<?= h($shopPaymentMessage !== '' ? $shopPaymentMessage : $defaultMessage) ?>
</div>
<?php endif; ?>
<?php if (!empty($catalog)): ?>
<!-- Search and Filter -->
<div class="row mb-4">
<div class="col-md-6 mb-3 mb-md-0">
<div class="input-group input-group-lg shadow-sm rounded-pill overflow-hidden">
<span class="input-group-text bg-white border-0 ps-4"><i class="bi bi-search text-muted"></i></span>
<input type="text" id="searchInput" class="form-control border-0 px-3" placeholder="<?= h(tr('ابحث عن منتج...', 'Search for a product...')) ?>" onkeyup="filterProducts()">
</div>
</div>
<div class="col-md-6">
<div class="d-flex gap-2 overflow-auto py-2 px-1 category-filter-container" style="white-space: nowrap;" id="categoryFilters">
<button class="btn btn-primary rounded-pill px-4 active" data-filter="all" onclick="setCategoryFilter('all', this)"><?= h(tr('الكل', 'All')) ?></button>
<?php foreach (array_keys($catalog) as $catName): ?>
<button class="btn btn-light rounded-pill px-4 text-dark shadow-sm border" data-filter="<?= htmlspecialchars($catName) ?>" onclick="setCategoryFilter(this.getAttribute('data-filter'), this)"><?= h($catName) ?></button>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="shop-pagination-wrap bg-white p-3 rounded-4 shadow-sm mb-4">
<div class="shop-pagination-status" id="paginationStatus">
<?= h(tr('جارٍ تحميل المنتجات...', 'Loading products...')) ?>
</div>
<nav aria-label="<?= h(tr('تنقل الصفحات', 'Product pagination')) ?>">
<ul class="pagination pagination-sm mb-0 shop-pagination" id="paginationTop"></ul>
</nav>
</div>
<?php endif; ?>
<?php if (empty($catalog)): ?>
<div class="text-center py-5">
<i class="bi bi-box-seam display-1 text-muted opacity-50 mb-3 d-block"></i>
<h3 class="text-muted"><?= h(tr('لا توجد منتجات متاحة حالياً', 'No products available currently')) ?></h3>
</div>
<?php else: ?>
<?php foreach ($catalog as $category => $catItems): ?>
<h3 class="fw-bold mb-4 mt-5 border-bottom pb-2 category-title" data-category="<?= htmlspecialchars($category) ?>"><?= h($category) ?></h3>
<div class="row g-4 category-row" data-category="<?= htmlspecialchars($category) ?>">
<?php foreach ($catItems as $item): ?>
<div class="col-sm-6 col-md-4 col-lg-3 product-item" data-name="<?= htmlspecialchars(strtolower($item['name'])) ?>">
<div class="card shop-item-card">
<?php if (!empty($item['image_url'])): ?>
<img src="<?= h($item['image_url']) ?>" class="card-img-top shop-item-img" alt="<?= h($item['name']) ?>">
<?php else: ?>
<div class="bg-light d-flex align-items-center justify-content-center shop-item-img text-muted">
<i class="bi bi-image" style="font-size: 3rem;"></i>
</div>
<?php endif; ?>
<div class="card-body d-flex flex-column">
<h5 class="card-title fw-bold mb-1"><?= h($item['name']) ?></h5>
<p class="text-primary fw-bold fs-5 mb-3"><?= h(currency($item['price'])) ?></p>
<button class="btn btn-outline-primary mt-auto rounded-pill fw-bold" onclick="addToCart(<?= htmlspecialchars(json_encode([
'id' => $item['id'],
'sku' => $item['sku'],
'name' => $item['name'],
'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>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
<!-- No results message -->
<div id="noResultsMsg" class="text-center py-5" style="display: none;">
<i class="bi bi-search display-1 text-muted opacity-50 mb-3 d-block"></i>
<h3 class="text-muted"><?= h(tr('لم يتم العثور على نتائج', 'No results found')) ?></h3>
</div>
<div class="shop-pagination-wrap bg-white p-3 rounded-4 shadow-sm mt-4" id="paginationFooter">
<div class="shop-pagination-status" id="paginationStatusBottom"></div>
<nav aria-label="<?= h(tr('تنقل الصفحات', 'Product pagination')) ?>">
<ul class="pagination pagination-sm mb-0 shop-pagination" id="paginationBottom"></ul>
</nav>
</div>
<?php endif; ?>
</div>
<!-- Cart Modal -->
<div class="modal fade" id="cartModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header border-bottom-0 pb-0 pt-4 px-4">
<h5 class="modal-title fw-bold"><i class="bi bi-cart-check me-2 text-primary"></i><?= h(tr('سلة المشتريات', 'Shopping Cart')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body px-4 py-4">
<div id="cartItemsList" class="mb-4">
<!-- Items will be rendered here -->
</div>
<!-- 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>
<form id="checkoutForm">
<div class="mb-3">
<label class="form-label fw-semibold"><?= h(tr('الاسم', 'Name')) ?> *</label>
<input type="text" class="form-control form-control-lg rounded-3" id="customerName" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold"><?= h(tr('رقم الهاتف', 'Telephone')) ?> *</label>
<div class="input-group input-group-lg" dir="ltr">
<span class="input-group-text">968</span>
<input type="tel" class="form-control form-control-lg rounded-end-3" id="customerPhone" inputmode="numeric" maxlength="8" pattern="\d{8}" placeholder="91234567" required>
</div>
<div class="form-text"><?= h(tr('أدخل 8 أرقام فقط وسيتم إرسالها عبر واتساب مع المقدمة 968.', 'Enter 8 digits only; WhatsApp will send it with the 968 prefix.')) ?></div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold"><?= h(tr('العنوان', 'Address')) ?> *</label>
<textarea class="form-control form-control-lg rounded-3" id="customerAddress" rows="2" required></textarea>
</div>
<div class="mb-3">
<label class="form-label fw-semibold d-block mb-3"><?= h(tr('طريقة الدفع', 'Payment Method')) ?> *</label>
<div class="row g-3">
<?php if ($shopShowPayOnline): ?>
<div class="col-md-6">
<label class="border rounded-4 p-3 w-100 h-100 bg-light-subtle <?= $shopCanPayOnline ? '' : 'opacity-75' ?>">
<input class="form-check-input me-2" type="radio" name="payment_method" value="pay_online" <?= $shopCanPayOnline ? '' : 'disabled' ?>>
<span class="fw-bold d-block"><?= h(tr('ادفع أونلاين', 'Pay Online')) ?></span>
<span class="small text-muted"><?= h(tr('سيتم تحويلك إلى بوابة ثواني لإتمام الدفع مباشرة.', 'You will be redirected to Thawani checkout to complete payment.')) ?></span>
</label>
</div>
<?php endif; ?>
<div class="col-md-6">
<label class="border rounded-4 p-3 w-100 h-100 bg-light-subtle">
<input class="form-check-input me-2" type="radio" name="payment_method" value="pay_later" checked>
<span class="fw-bold d-block"><?= h(tr('ادفع لاحقاً', 'Pay Later')) ?></span>
<span class="small text-muted"><?= h(tr('سيتم إنشاء الطلب الآن وسيتم تحصيل المبلغ لاحقاً.', 'Your order will be created now and payment will be collected later.')) ?></span>
</label>
</div>
</div>
<?php if (!$shopCanPayOnline && $shopShowPayOnline): ?>
<div class="form-text mt-2 text-warning"><?= h(tr('خيار الدفع أونلاين ظاهر لكن مفاتيح ثواني غير مكتملة في الإعدادات بعد.', 'Pay Online is visible, but the Thawani keys are not fully configured in Settings yet.')) ?></div>
<?php endif; ?>
</div>
<div class="mb-2">
<div class="form-check bg-light rounded-4 p-3 border">
<input class="form-check-input" type="checkbox" id="acceptPolicies" required>
<label class="form-check-label small" for="acceptPolicies">
<?= tr('أوافق على <a href="privacy-policy.php" target="_blank" rel="noopener">سياسة الخصوصية</a> و<a href="terms-conditions.php" target="_blank" rel="noopener">الشروط والأحكام</a>.', 'I agree to the <a href="privacy-policy.php" target="_blank" rel="noopener">Privacy Policy</a> and <a href="terms-conditions.php" target="_blank" rel="noopener">Terms &amp; Conditions</a>.') ?>
</label>
</div>
<div class="form-text"><?= h(tr('يجب قبول السياسة والشروط قبل إرسال الطلب.', 'You must accept the policy and terms before placing the order.')) ?></div>
</div>
</form>
</div>
<div class="modal-footer border-top-0 pt-0 pb-4 px-4 d-flex justify-content-between">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal"><?= h(tr('إكمال التسوق', 'Continue Shopping')) ?></button>
<button type="button" class="btn btn-success rounded-pill px-5 fw-bold shadow-sm" id="submitOrderBtn" onclick="submitOrder()">
<i class="bi bi-check2-circle me-1"></i> <?= h(tr('تأكيد الطلب', 'Confirm Order')) ?>
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
let cart = JSON.parse(localStorage.getItem('shop_cart')) || {};
let cartModalInstance = null;
function saveCart() {
localStorage.setItem('shop_cart', JSON.stringify(cart));
updateCartBadge();
}
function updateCartBadge() {
let count = 0;
for (let id in cart) {
count += cart[id].qty;
}
document.getElementById('cartCount').innerText = count;
}
function addToCart(item) {
if (cart[item.id]) {
cart[item.id].qty += 1;
} else {
cart[item.id] = { ...item, qty: 1 };
}
saveCart();
Swal.fire({
title: '<?= h(tr('تمت الإضافة', 'Added')) ?>',
text: item.name,
icon: 'success',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 1500
});
}
function changeQty(id, delta) {
if (cart[id]) {
cart[id].qty += delta;
if (cart[id].qty <= 0) {
delete cart[id];
}
saveCart();
renderCart();
}
}
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>';
} else {
html = '<div class="list-group list-group-flush">';
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">
<div>
<h6 class="mb-1 fw-bold">${item.name}</h6>
<small class="text-muted">${Number(item.price).toFixed(2)}</small>
</div>
<div class="d-flex align-items-center">
<button class="btn btn-sm btn-outline-secondary rounded-circle px-2 me-2" onclick="changeQty(${id}, -1)"><i class="bi bi-dash"></i></button>
<span class="fw-bold px-2">${item.qty}</span>
<button class="btn btn-sm btn-outline-secondary rounded-circle px-2 ms-2 me-4" onclick="changeQty(${id}, 1)"><i class="bi bi-plus"></i></button>
<span class="fw-bold text-primary" style="width: 70px; text-align:right;">${subtotal.toFixed(2)}</span>
</div>
</div>`;
}
html += '</div>';
}
list.innerHTML = html;
document.getElementById('cartSubtotal').innerText = total.toFixed(2);
document.getElementById('cartVat').innerText = totalVat.toFixed(2);
document.getElementById('cartTotal').innerText = (total + totalVat).toFixed(2);
}
function openCart() {
if (!cartModalInstance && typeof bootstrap !== 'undefined') {
cartModalInstance = new bootstrap.Modal(document.getElementById('cartModal'));
}
if (cartModalInstance) {
renderCart();
cartModalInstance.show();
} else {
console.error("Bootstrap is not loaded yet.");
}
}
async function submitOrder() {
if (Object.keys(cart).length === 0) {
Swal.fire('<?= h(tr('تنبيه', 'Warning')) ?>', '<?= h(tr('السلة فارغة', 'Cart is empty')) ?>', 'warning');
return;
}
const form = document.getElementById('checkoutForm');
if (!form.reportValidity()) return;
const btn = document.getElementById('submitOrderBtn');
const origText = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
btn.disabled = true;
const paymentMethodField = document.querySelector('input[name="payment_method"]:checked');
if (!paymentMethodField) {
Swal.fire('<?= h(tr('تنبيه', 'Warning')) ?>', '<?= h(tr('اختر طريقة الدفع.', 'Please choose a payment method.')) ?>', 'warning');
btn.innerHTML = origText;
btn.disabled = false;
return;
}
const data = {
name: document.getElementById('customerName').value,
phone: document.getElementById('customerPhone').value,
address: document.getElementById('customerAddress').value,
payment_method: paymentMethodField.value,
accept_policies: document.getElementById('acceptPolicies').checked,
items: cart
};
try {
const res = await fetch('api/place_order.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const json = await res.json();
if (json.success) {
cart = {};
saveCart();
if (cartModalInstance) cartModalInstance.hide();
if (json.redirect_url) {
window.location.href = json.redirect_url;
return;
}
Swal.fire({
title: '<?= h(tr('تم إرسال الطلب بنجاح!', 'Order submitted successfully!')) ?>',
text: '<?= h(tr('سنتواصل معك قريباً لتأكيد الطلب.', 'We will contact you shortly to confirm the order.')) ?>',
icon: 'success',
confirmButtonText: '<?= h(tr('حسناً', 'OK')) ?>'
});
form.reset();
} else {
Swal.fire('<?= h(tr('خطأ', 'Error')) ?>', json.error || '<?= h(tr('فشل إرسال الطلب', 'Failed to submit order')) ?>', 'error');
}
} catch (e) {
Swal.fire('<?= h(tr('خطأ', 'Error')) ?>', '<?= h(tr('حدث خطأ في الاتصال', 'Network error')) ?>', 'error');
} finally {
btn.innerHTML = origText;
btn.disabled = false;
}
}
// Filtering + Pagination Logic
let currentCategory = 'all';
let currentPage = 1;
const itemsPerPage = 24;
function setCategoryFilter(cat, btn) {
currentCategory = cat;
currentPage = 1;
document.querySelectorAll('#categoryFilters button').forEach(b => {
b.classList.remove('btn-primary', 'active');
b.classList.add('btn-light', 'text-dark');
});
btn.classList.remove('btn-light', 'text-dark');
btn.classList.add('btn-primary', 'active');
filterProducts();
}
function getFilteredItems() {
const searchInput = document.getElementById('searchInput');
const searchVal = searchInput ? searchInput.value.toLowerCase().trim() : '';
return Array.from(document.querySelectorAll('.product-item')).filter(item => {
const itemName = (item.getAttribute('data-name') || '').toLowerCase();
const itemCategory = item.closest('.category-row')?.getAttribute('data-category') || '';
const matchesSearch = itemName.includes(searchVal);
const matchesCategory = currentCategory === 'all' || currentCategory === itemCategory;
return matchesSearch && matchesCategory;
});
}
function renderPagination(totalItems) {
const top = document.getElementById('paginationTop');
const bottom = document.getElementById('paginationBottom');
const statusTop = document.getElementById('paginationStatus');
const statusBottom = document.getElementById('paginationStatusBottom');
const footerWrap = document.getElementById('paginationFooter');
if (!top || !bottom || !statusTop || !statusBottom) return;
const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage));
if (currentPage > totalPages) currentPage = totalPages;
if (totalItems === 0) {
statusTop.textContent = <?= json_encode(tr('0 منتج', '0 products')) ?>;
statusBottom.textContent = statusTop.textContent;
top.innerHTML = '';
bottom.innerHTML = '';
if (footerWrap) footerWrap.style.display = 'none';
return;
}
const start = ((currentPage - 1) * itemsPerPage) + 1;
const end = Math.min(totalItems, currentPage * itemsPerPage);
const statusText = `<?= h(tr('عرض', 'Showing')) ?> ${start}-${end} <?= h(tr('من', 'of')) ?> ${totalItems}`;
statusTop.textContent = statusText;
statusBottom.textContent = statusText;
if (footerWrap) footerWrap.style.display = '';
if (totalPages <= 1) {
top.innerHTML = '';
bottom.innerHTML = '';
return;
}
const pages = [];
pages.push(1);
for (let p = Math.max(2, currentPage - 1); p <= Math.min(totalPages - 1, currentPage + 1); p++) {
pages.push(p);
}
if (totalPages > 1) pages.push(totalPages);
const uniquePages = [...new Set(pages)].sort((a, b) => a - b);
const parts = [];
const addControl = (label, page, disabled = false, active = false) => {
parts.push(`<li class="page-item${disabled ? ' disabled' : ''}${active ? ' active' : ''}"><button class="page-link" type="button" ${disabled ? 'disabled' : ''} onclick="goToPage(${page})">${label}</button></li>`);
};
addControl('', Math.max(1, currentPage - 1), currentPage === 1);
let lastPage = 0;
uniquePages.forEach(page => {
if (lastPage && page > lastPage + 1) {
parts.push('<li class="page-item disabled"><span class="page-link">…</span></li>');
}
addControl(page, page, false, page === currentPage);
lastPage = page;
});
addControl('', Math.min(totalPages, currentPage + 1), currentPage === totalPages);
const html = parts.join('');
top.innerHTML = html;
bottom.innerHTML = html;
}
function goToPage(page) {
currentPage = page;
filterProducts(false);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function filterProducts(resetPage = true) {
if (resetPage) currentPage = 1;
const filteredItems = getFilteredItems();
const totalVisible = filteredItems.length;
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const visibleOnPage = new Set(filteredItems.slice(startIndex, endIndex));
document.querySelectorAll('.category-row').forEach(row => {
let hasVisibleItems = false;
row.querySelectorAll('.product-item').forEach(item => {
if (visibleOnPage.has(item)) {
item.style.display = '';
hasVisibleItems = true;
} else {
item.style.display = 'none';
}
});
const title = row.previousElementSibling;
if (title && title.classList.contains('category-title')) {
title.style.display = hasVisibleItems ? '' : 'none';
row.style.display = hasVisibleItems ? '' : 'none';
}
});
const noResultsMsg = document.getElementById('noResultsMsg');
if (noResultsMsg) {
noResultsMsg.style.display = totalVisible === 0 ? 'block' : 'none';
}
renderPagination(totalVisible);
}
// init
updateCartBadge();
filterProducts();
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>

5
sohar_cookies.txt Normal file
View File

@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
127.0.0.1 FALSE / FALSE 0 PHPSESSID 10595d2a14je99l2dqr2ksv6v4

1056
stock.php Normal file

File diff suppressed because it is too large Load Diff

251
suppliers.php Normal file
View File

@ -0,0 +1,251 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('suppliers', 'show');
$pageTitle = tr('الموردون', 'Suppliers');
$activeNav = 'suppliers';
$pdo = db();
// Handle Form Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'create') {
$stmt = $pdo->prepare('INSERT INTO suppliers (name, contact_person, phone, email, address) VALUES (?, ?, ?, ?, ?)');
$stmt->execute([$_POST['name'], $_POST['contact_person'] ?? '', $_POST['phone'] ?? '', $_POST['email'] ?? '', $_POST['address'] ?? '']);
set_flash('success', tr('تمت إضافة المورد بنجاح', 'Supplier added successfully'));
redirect_to('suppliers.php');
} elseif ($action === 'edit') {
$stmt = $pdo->prepare('UPDATE suppliers SET name = ?, contact_person = ?, phone = ?, email = ?, address = ? WHERE id = ?');
$stmt->execute([$_POST['name'], $_POST['contact_person'] ?? '', $_POST['phone'] ?? '', $_POST['email'] ?? '', $_POST['address'] ?? '', $_POST['id']]);
set_flash('success', tr('تم التحديث بنجاح', 'Updated successfully'));
redirect_to('suppliers.php');
} elseif ($action === 'delete') {
$stmt = $pdo->prepare('DELETE FROM suppliers WHERE id = ?');
$stmt->execute([$_POST['id']]);
set_flash('success', tr('تم الحذف بنجاح', 'Deleted successfully'));
redirect_to('suppliers.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 LIKE ? OR phone LIKE ? OR email LIKE ?)';
$params[] = "%$search%";
$params[] = "%$search%";
$params[] = "%$search%";
}
$totalStmt = $pdo->prepare("SELECT COUNT(*) FROM suppliers WHERE $where");
$totalStmt->execute($params);
$total = $totalStmt->fetchColumn();
$totalPages = ceil($total / $limit);
$queryStmt = $pdo->prepare("SELECT * FROM suppliers WHERE $where ORDER BY id DESC LIMIT $limit OFFSET $offset");
$queryStmt->execute($params);
$items = $queryStmt->fetchAll();
require __DIR__ . '/includes/header.php';
?>
<section class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h3 class="h5 mb-2"><i class="bi bi-truck me-2"></i><?= h($pageTitle) ?></h3>
<p class="text-muted mb-0"><?= h(tr('إدارة حسابات الموردين', 'Manage supplier accounts')) ?></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 Supplier')) ?>
</button>
</div>
<form class="d-flex mb-3" method="GET" action="suppliers.php">
<div class="input-group" style="max-width: 400px;">
<input type="text" name="q" class="form-control" placeholder="<?= h(tr('بحث بالاسم أو الهاتف...', 'Search name or phone...')) ?>" value="<?= h($search) ?>">
<button class="btn btn-outline-secondary" type="submit"><i class="bi bi-search"></i></button>
</div>
</form>
</section>
<section>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 text-center">
<thead class="table-light">
<tr>
<th>ID</th>
<th><?= h(tr('الاسم', 'Name')) ?></th>
<th><?= h(tr('المسؤول', 'Contact Person')) ?></th>
<th><?= h(tr('الهاتف', 'Phone')) ?></th>
<th><?= h(tr('البريد', 'Email')) ?></th>
<th><?= 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['id']) ?></td>
<td class="fw-semibold"><?= h($item['name']) ?></td>
<td><?= h($item['contact_person']) ?></td>
<td><?= h($item['phone']) ?></td>
<td><?= h($item['email']) ?></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('suppliers.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="suppliers.php">
<input type="hidden" name="action" value="create">
<div class="modal-header">
<h5 class="modal-title"><?= h(tr('إضافة مورد', 'Add Supplier')) ?></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')) ?></label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('الشخص المسؤول', 'Contact Person')) ?></label>
<input type="text" name="contact_person" class="form-control">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label"><?= h(tr('الهاتف', 'Phone')) ?></label>
<input type="text" name="phone" class="form-control">
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?= h(tr('البريد الإلكتروني', 'Email')) ?></label>
<input type="email" name="email" class="form-control">
</div>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('العنوان', 'Address')) ?></label>
<textarea name="address" 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="suppliers.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')) ?></label>
<input type="text" name="name" id="edit_name" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('الشخص المسؤول', 'Contact Person')) ?></label>
<input type="text" name="contact_person" id="edit_contact_person" class="form-control">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label"><?= h(tr('الهاتف', 'Phone')) ?></label>
<input type="text" name="phone" id="edit_phone" class="form-control">
</div>
<div class="col-md-6 mb-3">
<label class="form-label"><?= h(tr('البريد الإلكتروني', 'Email')) ?></label>
<input type="email" name="email" id="edit_email" class="form-control">
</div>
</div>
<div class="mb-3">
<label class="form-label"><?= h(tr('العنوان', 'Address')) ?></label>
<textarea name="address" id="edit_address" 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="suppliers.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').value = item.name;
document.getElementById('edit_contact_person').value = item.contact_person || '';
document.getElementById('edit_phone').value = item.phone || '';
document.getElementById('edit_email').value = item.email || '';
document.getElementById('edit_address').value = item.address || '';
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'; ?>

0
temp_debts.html Normal file
View File

445
temp_debts.log Normal file
View File

@ -0,0 +1,445 @@
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>الديون والفواتير الآجلة · حلوى الريامي | Al Riyami Sweets</title>
<meta name="description" content='حلوى الريامي: Simplify sweets sales with multilingual POS, stock management, and detailed reports for multi-branch effic' />
<meta property="og:description" content="حلوى الريامي: Simplify sweets sales with multilingual POS, stock management, and detailed reports for multi-branch effic" />
<meta property="twitter:description" content="حلوى الريامي: Simplify sweets sales with multilingual POS, stock management, and detailed reports for multi-branch effic" />
<meta property="og:image" content="https://project-screens.s3.amazonaws.com/screenshots/39728/app-hero-20260419-014257.png" />
<meta property="twitter:image" content="https://project-screens.s3.amazonaws.com/screenshots/39728/app-hero-20260419-014257.png" />
<meta name="theme-color" content="#343a40" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css?v=202604232013">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
</head>
<body class="">
<!-- Private Admin Layout -->
<div class="d-flex" id="wrapper">
<!-- Sidebar -->
<div class="border-end bg-dark text-white shadow-sm" id="sidebar-wrapper">
<div class="sidebar-heading text-center py-4 fs-5 fw-bold text-uppercase border-bottom border-secondary d-flex flex-column align-items-center">
<i class="bi bi-shop me-2 fs-2 mb-2"></i>
<span>حلوى الريامي</span>
</div>
<div class="p-3 text-center border-bottom border-secondary">
<div class="fw-semibold">كاشير فرع صحار</div>
<div class="text-white-50 small">كاشير · فرع صحار</div>
</div>
<div class="list-group list-group-flush my-3 pb-5" id="sidebar-navigation">
<a class="list-group-item list-group-item-action " href="index.php?lang=ar">
<i class="bi bi-speedometer2"></i> لوحة التحكم </a>
<a class="list-group-item list-group-item-action " href="suppliers.php?lang=ar">
<i class="bi bi-truck"></i> الموردون </a>
<a class="list-group-item list-group-item-action " href="customers.php?lang=ar">
<i class="bi bi-people-fill"></i> العملاء </a>
<a class="list-group-item list-group-item-action active" href="debts.php?lang=ar">
<i class="bi bi-journal-text"></i> الديون والفواتير الآجلة </a>
</div>
</div>
<!-- /#sidebar-wrapper -->
<!-- Page Content -->
<div id="page-content-wrapper" class="d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom top-navbar px-3">
<div class="d-flex align-items-center justify-content-between w-100">
<div class="d-flex align-items-center">
<button class="btn btn-outline-secondary me-2" id="menu-toggle"><i class="bi bi-list"></i></button>
<h4 class="mb-0 ms-2 fw-semibold d-none d-md-block">الديون والفواتير الآجلة</h4>
</div>
<div class="d-flex align-items-center gap-3">
<a href="shop.php" target="_blank" class="btn btn-outline-success btn-sm me-2" title="زيارة المتجر"><i class="bi bi-shop"></i> <span class="d-none d-md-inline">المتجر</span></a>
<div class="language-switcher btn-group" role="group">
<a class="btn btn-sm btn-primary" href="test_debts_cli.php?lang=ar">AR</a>
<a class="btn btn-sm btn-outline-primary" href="test_debts_cli.php?lang=en">EN</a>
</div>
<div class="dropdown">
<button class="btn btn-light dropdown-toggle border" type="button" id="userMenu" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i> كاشير فرع صحار </button>
<ul class="dropdown-menu dropdown-menu-end shadow" aria-labelledby="userMenu">
<li><a class="dropdown-item" href="profile.php?lang=ar"><i class="bi bi-person me-2 text-primary"></i> الملف الشخصي</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="logout.php?lang=ar"><i class="bi bi-box-arrow-right text-danger me-2"></i> تسجيل الخروج</a></li>
</ul>
</div>
</div>
</div>
</nav>
<div class="container-fluid p-4">
<!-- Flash messages integration using SweetAlert2 -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0 text-gray-800">الديون والفواتير الآجلة</h1>
</div>
<div class="row">
<!-- Debts by Customer -->
<div class="col-lg-4 mb-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
<h6 class="m-0 font-weight-bold text-primary"><i class="bi bi-people"></i> الديون حسب العميل</h6>
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
<div>
<strong>Moosa Ali Al-Abri</strong>
<div class="small text-muted" dir="ltr">99359472</div>
<div class="small text-muted">7 فواتير مفتوحة</div>
<div class="small text-warning">3 منها دفعات جزئية</div>
</div>
<span class="badge bg-danger rounded-pill fs-6">280.410 ر.ع</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
<div>
<strong>عميل غير معروف</strong>
<div class="small text-muted">3 فواتير مفتوحة</div>
<div class="small text-warning">1 منها دفعات جزئية</div>
</div>
<span class="badge bg-danger rounded-pill fs-6">8.122 ر.ع</span>
</li>
</ul>
</div>
</div>
</div>
<!-- Unpaid Invoices -->
<div class="col-lg-8 mb-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
<h6 class="m-0 font-weight-bold text-primary"><i class="bi bi-receipt"></i> الفواتير غير المدفوعة والجزئية</h6>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>رقم الفاتورة</th>
<th>العميل</th>
<th>الهاتف</th>
<th>التاريخ</th>
<th>الإجمالي</th>
<th>المدفوع</th>
<th>المتبقي</th>
<th>الحالة</th>
<th>الإجراء</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<a href="sale.php?id=28&amp;lang=ar" class="fw-bold text-decoration-none">
6 </a>
</td>
<td>Moosa Ali Al-Abri</td>
<td dir="ltr">-</td>
<td>2026-04-22</td>
<td class="fw-semibold">3.150 ر.ع</td>
<td class="text-primary">0.000 ر.ع</td>
<td class="fw-bold text-danger">3.150 ر.ع</td>
<td>
<span class="badge bg-danger text-white">غير مدفوعة</span>
</td>
<td>
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="receivePayment(28, 3.15, false)">
<i class="bi bi-cash-coin"></i> استلام دفعة </button>
</td>
</tr>
<tr>
<td>
<a href="sale.php?id=27&amp;lang=ar" class="fw-bold text-decoration-none">
5 </a>
</td>
<td>Moosa Ali Al-Abri</td>
<td dir="ltr">-</td>
<td>2026-04-22</td>
<td class="fw-semibold">1,260.000 ر.ع</td>
<td class="text-primary">1,000.000 ر.ع</td>
<td class="fw-bold text-danger">260.000 ر.ع</td>
<td>
<span class="badge bg-warning text-dark">مدفوعة جزئياً</span>
</td>
<td>
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="receivePayment(27, 260, false)">
<i class="bi bi-cash-coin"></i> استلام دفعة </button>
</td>
</tr>
<tr>
<td>
<a href="sale.php?id=26&amp;lang=ar" class="fw-bold text-decoration-none">
4 </a>
</td>
<td>-</td>
<td dir="ltr">-</td>
<td>2026-04-22</td>
<td class="fw-semibold">2.830 ر.ع</td>
<td class="text-primary">2.828 ر.ع</td>
<td class="fw-bold text-danger">0.002 ر.ع</td>
<td>
<span class="badge bg-warning text-dark">مدفوعة جزئياً</span>
</td>
<td>
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="receivePayment(26, 0.002, false)">
<i class="bi bi-cash-coin"></i> استلام دفعة </button>
</td>
</tr>
<tr>
<td>
<a href="sale.php?id=23&amp;lang=ar" class="fw-bold text-decoration-none">
877100 </a>
</td>
<td>Moosa Ali Al-Abri</td>
<td dir="ltr">-</td>
<td>2026-04-22</td>
<td class="fw-semibold">5.250 ر.ع</td>
<td class="text-primary">2.000 ر.ع</td>
<td class="fw-bold text-danger">3.250 ر.ع</td>
<td>
<span class="badge bg-warning text-dark">مدفوعة جزئياً</span>
</td>
<td>
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="receivePayment(23, 3.25, false)">
<i class="bi bi-cash-coin"></i> استلام دفعة </button>
</td>
</tr>
<tr>
<td>
<a href="sale.php?id=22&amp;lang=ar" class="fw-bold text-decoration-none">
900748 </a>
</td>
<td>Moosa Ali Al-Abri</td>
<td dir="ltr">-</td>
<td>2026-04-21</td>
<td class="fw-semibold">2.830 ر.ع</td>
<td class="text-primary">1.000 ر.ع</td>
<td class="fw-bold text-danger">1.830 ر.ع</td>
<td>
<span class="badge bg-warning text-dark">مدفوعة جزئياً</span>
</td>
<td>
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="receivePayment(22, 1.83, false)">
<i class="bi bi-cash-coin"></i> استلام دفعة </button>
</td>
</tr>
<tr>
<td>
<a href="sale.php?id=19&amp;lang=ar" class="fw-bold text-decoration-none">
132196 </a>
</td>
<td>Moosa Ali Al-Abri</td>
<td dir="ltr">-</td>
<td>2026-04-20</td>
<td class="fw-semibold">5.490 ر.ع</td>
<td class="text-primary">0.000 ر.ع</td>
<td class="fw-bold text-danger">5.490 ر.ع</td>
<td>
<span class="badge bg-danger text-white">غير مدفوعة</span>
</td>
<td>
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="receivePayment(19, 5.49, false)">
<i class="bi bi-cash-coin"></i> استلام دفعة </button>
</td>
</tr>
<tr>
<td>
<a href="sale.php?id=18&amp;lang=ar" class="fw-bold text-decoration-none">
968774 </a>
</td>
<td>Moosa Ali Al-Abri</td>
<td dir="ltr">-</td>
<td>2026-04-20</td>
<td class="fw-semibold">3.150 ر.ع</td>
<td class="text-primary">0.000 ر.ع</td>
<td class="fw-bold text-danger">3.150 ر.ع</td>
<td>
<span class="badge bg-danger text-white">غير مدفوعة</span>
</td>
<td>
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="receivePayment(18, 3.15, false)">
<i class="bi bi-cash-coin"></i> استلام دفعة </button>
</td>
</tr>
<tr>
<td>
<a href="sale.php?id=17&amp;lang=ar" class="fw-bold text-decoration-none">
911033 </a>
</td>
<td>-</td>
<td dir="ltr">-</td>
<td>2026-04-20</td>
<td class="fw-semibold">5.490 ر.ع</td>
<td class="text-primary">0.000 ر.ع</td>
<td class="fw-bold text-danger">5.490 ر.ع</td>
<td>
<span class="badge bg-danger text-white">غير مدفوعة</span>
</td>
<td>
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="receivePayment(17, 5.49, false)">
<i class="bi bi-cash-coin"></i> استلام دفعة </button>
</td>
</tr>
<tr>
<td>
<a href="sale.php?id=16&amp;lang=ar" class="fw-bold text-decoration-none">
859389 </a>
</td>
<td>Moosa Ali Al-Abri</td>
<td dir="ltr">-</td>
<td>2026-04-20</td>
<td class="fw-semibold">3.540 ر.ع</td>
<td class="text-primary">0.000 ر.ع</td>
<td class="fw-bold text-danger">3.540 ر.ع</td>
<td>
<span class="badge bg-danger text-white">غير مدفوعة</span>
</td>
<td>
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="receivePayment(16, 3.54, false)">
<i class="bi bi-cash-coin"></i> استلام دفعة </button>
</td>
</tr>
<tr>
<td>
<a href="sale.php?id=14&amp;lang=ar" class="fw-bold text-decoration-none">
356570 </a>
</td>
<td>Moosa Ali Al-Abri</td>
<td dir="ltr">99359472</td>
<td>2026-04-20</td>
<td class="fw-semibold">2.630 ر.ع</td>
<td class="text-primary">0.000 ر.ع</td>
<td class="fw-bold text-danger">2.630 ر.ع</td>
<td>
<span class="badge bg-danger text-white">غير مدفوعة</span>
</td>
<td>
<button class="btn btn-sm btn-outline-success rounded-pill px-3" onclick="receivePayment(14, 2.63, false)">
<i class="bi bi-cash-coin"></i> استلام دفعة </button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
async function receivePayment(id, dueAmount, completeOrder = false) {
const { value: paymentAmount } = await Swal.fire({
title: 'استلام دفعة',
text: 'أدخل المبلغ المستلم لهذه الفاتورة.',
input: 'number',
inputAttributes: { min: '0.001', step: '0.001', max: String(dueAmount) },
inputValue: Number(dueAmount).toFixed(3),
showCancelButton: true,
confirmButtonColor: '#198754',
confirmButtonText: 'حفظ الدفعة',
cancelButtonText: 'إلغاء',
inputValidator: (value) => {
const amount = parseFloat(value || '0');
if (!amount || amount <= 0) {
return 'أدخل مبلغاً صحيحاً.';
}
if (amount - dueAmount > 0.0005) {
return 'المبلغ لا يمكن أن يتجاوز المتبقي.';
}
return null;
}
});
if (!paymentAmount) {
return;
}
const formData = new FormData();
formData.append('sale_id', String(id));
formData.append('payment_amount', String(paymentAmount));
if (completeOrder) {
formData.append('complete_order', '1');
}
const response = await fetch('api/sales_payment.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.success) {
await Swal.fire({ icon: 'success', text: data.message, confirmButtonText: 'OK' });
window.location.reload();
} else {
Swal.fire({ icon: 'error', text: data.error || 'تعذر تسجيل الدفعة.' });
}
}
</script>
</div> <!-- /.container-fluid -->
<!-- App Footer -->
<footer class="bg-white border-top mt-auto py-3 px-4 text-muted small">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
&copy; 2026 <strong>حلوى الريامي</strong>. جميع الحقوق محفوظة. </div>
<div class="d-flex align-items-center flex-wrap gap-3 mt-2 mt-md-0">
<a href="privacy-policy.php?lang=ar" class="text-decoration-none">سياسة الخصوصية</a>
<a href="terms-conditions.php?lang=ar" class="text-decoration-none">الشروط والأحكام</a>
<div>
<span class="text-secondary">تم التطوير بواسطة </span><a href="https://flatlogic.com" target="_blank" class="text-decoration-none fw-semibold">Flatlogic</a>
</div>
</div>
</div>
</footer>
<!-- /App Footer -->
</div> <!-- /#page-content-wrapper -->
</div> <!-- /#wrapper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script>
// Menu Toggle Script
document.addEventListener('DOMContentLoaded', function() {
var toggleBtn = document.getElementById("menu-toggle");
if (toggleBtn) {
toggleBtn.addEventListener('click', function(e) {
e.preventDefault();
document.getElementById("wrapper").classList.toggle("toggled");
});
}
});
</script>
<script>
function confirmSwal(e, msg) {
e.preventDefault();
const form = e.target;
Swal.fire({
title: msg,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'نعم',
cancelButtonText: 'إلغاء',
confirmButtonColor: '#d33'
}).then((result) => {
if (result.isConfirmed) {
form.submit();
}
});
}
</script>
<script src="assets/js/main.js?v=202604232013"></script>
</body>
</html>

1696
temp_debts_out.html Normal file

File diff suppressed because it is too large Load Diff

35
terms-conditions.php Normal file
View File

@ -0,0 +1,35 @@
<?php
require_once __DIR__ . '/includes/app.php';
$forcePublic = true;
$pageTitle = tr('الشروط والأحكام', 'Terms & Conditions');
$metaDescription = tr('راجع الشروط والأحكام الخاصة باستخدام المتجر والخدمات المقدمة عبر النظام.', 'Review the terms and conditions for using the online store and related services.');
$termsContent = trim((string) get_setting('terms_conditions_content'));
require __DIR__ . '/includes/header.php';
?>
<section class="py-5">
<div class="row justify-content-center">
<div class="col-lg-10 col-xl-8">
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-body p-4 p-md-5">
<div class="mb-4">
<span class="badge text-bg-light border mb-3"><?= h(tr('صفحة قانونية', 'Legal page')) ?></span>
<h1 class="h2 fw-bold mb-3"><?= h(tr('الشروط والأحكام', 'Terms & Conditions')) ?></h1>
<p class="text-muted mb-0"><?= h(tr('آخر تحديث يتم بواسطة إدارة النظام عند حفظ النص من الإعدادات.', 'This page is updated by the admin team whenever the content is saved from Settings.')) ?></p>
</div>
<?php if ($termsContent !== ''): ?>
<article class="legal-content lh-lg"><?= nl2br(h($termsContent)) ?></article>
<?php else: ?>
<div class="alert alert-warning rounded-4 mb-0">
<strong><?= h(tr('لم تتم إضافة المحتوى بعد.', 'Content not added yet.')) ?></strong>
<div class="small mt-2"><?= h(tr('يمكنك إضافته من الإعدادات ← السياسات والشروط.', 'You can add it from Settings → Policies.')) ?></div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
</section>
<?php require __DIR__ . '/includes/footer.php'; ?>

10
test_debts_cli.php Normal file
View File

@ -0,0 +1,10 @@
<?php
$_SERVER['REQUEST_URI'] = '/debts.php';
require_once 'db/config.php';
$pdo = db();
$stmt = $pdo->query("SELECT * FROM users WHERE username = 'cashier_sohar'");
$user = $stmt->fetch(PDO::FETCH_ASSOC);
session_start();
$_SESSION['auth_user'] = $user;
require_once 'debts.php';

96
thawani_return.php Normal file
View File

@ -0,0 +1,96 @@
<?php
require_once __DIR__ . '/includes/app.php';
$result = trim((string) ($_GET['result'] ?? 'cancel'));
$orderId = (int) ($_GET['order_id'] ?? 0);
if ($orderId <= 0) {
redirect_to('shop.php', [
'payment_status' => 'failed',
'message' => tr('تعذر العثور على الطلب.', 'Could not find the order.'),
]);
}
$stmt = db()->prepare('SELECT * FROM online_orders WHERE id = ? LIMIT 1');
$stmt->execute([$orderId]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$order) {
redirect_to('shop.php', [
'payment_status' => 'failed',
'message' => tr('الطلب غير موجود.', 'The order was not found.'),
]);
}
$items = json_decode((string) ($order['items_json'] ?? '[]'), true) ?: [];
$oldStatus = (string) ($order['status'] ?? 'pending');
$currentPaymentStatus = (string) ($order['payment_status'] ?? 'unpaid');
$sessionId = trim((string) ($order['gateway_session_id'] ?? $_GET['session_id'] ?? ''));
$redirectStatus = 'failed';
$redirectMessage = tr('تعذر تأكيد الدفع. حاول مرة أخرى أو اختر الدفع لاحقاً.', 'We could not confirm the payment. Please try again or choose Pay Later.');
$sendCustomerWhatsapp = false;
if ($result === 'success' && $sessionId !== '') {
$verification = thawani_retrieve_session($sessionId);
if (!empty($verification['success']) && thawani_session_paid($verification)) {
$transactionId = thawani_session_transaction_id($verification);
$updateStmt = db()->prepare('UPDATE online_orders SET payment_status = ?, gateway_session_id = ?, gateway_transaction_id = ?, paid_at = NOW() WHERE id = ?');
$updateStmt->execute(['paid', $sessionId, $transactionId !== '' ? $transactionId : null, $orderId]);
$sendCustomerWhatsapp = true;
$redirectStatus = 'paid';
$redirectMessage = tr('تم الدفع بنجاح وتم استلام طلبك.', 'Payment completed successfully and your order was received.');
} else {
if ($oldStatus === 'pending') {
db()->beginTransaction();
try {
sync_online_order_stock_reservation($items, $oldStatus, $items, 'rejected');
$updateStmt = db()->prepare('UPDATE online_orders SET status = ?, payment_status = ? WHERE id = ?');
$updateStmt->execute(['rejected', 'failed', $orderId]);
db()->commit();
} catch (Throwable $e) {
if (db()->inTransaction()) {
db()->rollBack();
}
throw $e;
}
} elseif ($currentPaymentStatus !== 'paid') {
$updateStmt = db()->prepare('UPDATE online_orders SET payment_status = ? WHERE id = ?');
$updateStmt->execute(['failed', $orderId]);
}
}
} else {
if ($currentPaymentStatus !== 'paid' && $oldStatus === 'pending') {
db()->beginTransaction();
try {
sync_online_order_stock_reservation($items, $oldStatus, $items, 'rejected');
$updateStmt = db()->prepare('UPDATE online_orders SET status = ?, payment_status = ? WHERE id = ?');
$updateStmt->execute(['rejected', 'cancelled', $orderId]);
db()->commit();
} catch (Throwable $e) {
if (db()->inTransaction()) {
db()->rollBack();
}
throw $e;
}
} elseif ($currentPaymentStatus !== 'paid') {
$updateStmt = db()->prepare('UPDATE online_orders SET payment_status = ? WHERE id = ?');
$updateStmt->execute(['cancelled', $orderId]);
}
$redirectStatus = 'cancelled';
$redirectMessage = tr('تم إلغاء الدفع. يمكنك إعادة الطلب أو اختيار الدفع لاحقاً.', 'Payment was cancelled. You can place the order again or choose Pay Later.');
}
if ($sendCustomerWhatsapp && wablas_is_configured()) {
try {
wablas_notify_online_order_by_id($orderId, 'created');
} catch (Throwable $e) {
error_log('Customer WhatsApp notify failed after Thawani payment for order #' . $orderId . ': ' . $e->getMessage());
}
}
redirect_to('shop.php', [
'payment_status' => $redirectStatus,
'message' => $redirectMessage,
]);

215
units.php Normal file
View File

@ -0,0 +1,215 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('units', 'show');
$pageTitle = tr('الوحدات', 'Units');
$activeNav = 'units';
$pdo = db();
// Handle Form Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'create') {
$stmt = $pdo->prepare('INSERT INTO units (name_ar, name_en) VALUES (?, ?)');
$stmt->execute([$_POST['name_ar'], $_POST['name_en']]);
set_flash('success', tr('تمت إضافة الوحدة بنجاح', 'Unit added successfully'));
redirect_to('units.php');
} elseif ($action === 'edit') {
$stmt = $pdo->prepare('UPDATE units 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('units.php');
} elseif ($action === 'delete') {
$stmt = $pdo->prepare('DELETE FROM units WHERE id = ?');
$stmt->execute([$_POST['id']]);
set_flash('success', tr('تم الحذف بنجاح', 'Deleted successfully'));
redirect_to('units.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 units WHERE $where");
$totalStmt->execute($params);
$total = $totalStmt->fetchColumn();
$totalPages = ceil($total / $limit);
$queryStmt = $pdo->prepare("SELECT * FROM units WHERE $where ORDER BY id DESC LIMIT $limit OFFSET $offset");
$queryStmt->execute($params);
$items = $queryStmt->fetchAll();
require __DIR__ . '/includes/header.php';
?>
<section class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h3 class="h5 mb-2"><i class="bi bi-rulers me-2"></i><?= h($pageTitle) ?></h3>
<p class="text-muted mb-0"><?= h(tr('إدارة وحدات القياس للأصناف', 'Manage measurement units for items')) ?></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 Unit')) ?>
</button>
</div>
<form class="d-flex mb-3" method="GET" action="units.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>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 text-center">
<thead class="table-light">
<tr>
<th>ID</th>
<th><?= h(tr('الاسم (عربي)', 'Name (AR)')) ?></th>
<th><?= h(tr('الاسم (إنجليزي)', 'Name (EN)')) ?></th>
<th><?= 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('units.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="units.php">
<input type="hidden" name="action" value="create">
<div class="modal-header">
<h5 class="modal-title"><?= h(tr('إضافة وحدة', 'Add Unit')) ?></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="units.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="units.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'; ?>

430
users.php Normal file
View File

@ -0,0 +1,430 @@
<?php
require_once __DIR__ . '/includes/app.php';
$user = require_permission('users', 'show');
$pageTitle = tr('المستخدمون والأدوار', 'Users & Roles');
$activeNav = 'users';
$flash = pull_flash();
// Handle POST actions (Create, Update, Delete)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'add') {
if (!has_permission('users', 'add')) { set_flash('error', tr('ليس لديك صلاحية', 'No permission')); redirect_to('users.php'); }
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$name_ar = trim($_POST['name_ar'] ?? '');
$name_en = trim($_POST['name_en'] ?? '');
$role = $_POST['role'] ?? 'cashier';
$branch_code = $_POST['branch_code'] ?? 'muscat';
if ($username && $password && $name_ar) {
$hash = password_hash($password, PASSWORD_DEFAULT);
try {
$stmt = db()->prepare("INSERT INTO users (username, password, role, branch_code, allowed_branches, name_ar, name_en, permissions) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$perms = isset($_POST["permissions"]) ? json_encode($_POST["permissions"]) : "{}";
$allowed_branches = isset($_POST["allowed_branches"]) && is_array($_POST["allowed_branches"]) ? implode(",", $_POST["allowed_branches"]) : null;
$stmt->execute([$username, $hash, $role, $branch_code, $allowed_branches, $name_ar, $name_en, $perms]);
set_flash('success', tr('تمت إضافة المستخدم بنجاح.', 'User added successfully.'));
} catch (PDOException $e) {
set_flash('error', tr('حدث خطأ، قد يكون اسم المستخدم موجوداً مسبقاً.', 'Error occurred, username might already exist.'));
}
} else {
set_flash('error', tr('يرجى تعبئة الحقول المطلوبة.', 'Please fill required fields.'));
}
redirect_to('users.php');
}
if ($action === 'edit') {
if (!has_permission('users', 'edit')) { set_flash('error', tr('ليس لديك صلاحية', 'No permission')); redirect_to('users.php'); }
$id = (int)($_POST['id'] ?? 0);
$username = trim($_POST['username'] ?? '');
$name_ar = trim($_POST['name_ar'] ?? '');
$name_en = trim($_POST['name_en'] ?? '');
$role = $_POST['role'] ?? 'cashier';
$branch_code = $_POST['branch_code'] ?? 'muscat';
$password = $_POST['password'] ?? '';
if ($id && $username && $name_ar) {
try {
if ($password) {
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = db()->prepare("UPDATE users SET username=?, password=?, role=?, branch_code=?, allowed_branches=?, name_ar=?, name_en=?, permissions=? WHERE id=?");
$perms = isset($_POST["permissions"]) ? json_encode($_POST["permissions"]) : "{}";
$allowed_branches = isset($_POST["allowed_branches"]) && is_array($_POST["allowed_branches"]) ? implode(",", $_POST["allowed_branches"]) : null;
$stmt->execute([$username, $hash, $role, $branch_code, $allowed_branches, $name_ar, $name_en, $perms, $id]);
} else {
$stmt = db()->prepare("UPDATE users SET username=?, role=?, branch_code=?, allowed_branches=?, name_ar=?, name_en=?, permissions=? WHERE id=?");
$perms = isset($_POST["permissions"]) ? json_encode($_POST["permissions"]) : "{}";
$allowed_branches = isset($_POST["allowed_branches"]) && is_array($_POST["allowed_branches"]) ? implode(",", $_POST["allowed_branches"]) : null;
$stmt->execute([$username, $role, $branch_code, $allowed_branches, $name_ar, $name_en, $perms, $id]);
}
set_flash('success', tr('تم تعديل المستخدم بنجاح.', 'User updated successfully.'));
} catch (PDOException $e) {
set_flash('error', tr('حدث خطأ أثناء التعديل.', 'Error occurred during update.'));
}
}
redirect_to('users.php');
}
if ($action === 'delete') {
if (!has_permission('users', 'del')) { set_flash('error', tr('ليس لديك صلاحية', 'No permission')); redirect_to('users.php'); }
$id = (int)($_POST['id'] ?? 0);
if ($id && $id !== $user['id']) {
$stmt = db()->prepare("DELETE FROM users WHERE id=?");
$stmt->execute([$id]);
set_flash('success', tr('تم حذف المستخدم بنجاح.', 'User deleted successfully.'));
} else {
set_flash('error', tr('لا يمكن حذف حسابك الحالي.', 'Cannot delete your own account.'));
}
redirect_to('users.php');
}
}
// Search logic
$search = $_GET['q'] ?? '';
$searchQuery = "%{$search}%";
if ($search) {
$stmt = db()->prepare("SELECT * FROM users WHERE name_ar LIKE ? OR name_en LIKE ? OR username LIKE ? ORDER BY id DESC");
$stmt->execute([$searchQuery, $searchQuery, $searchQuery]);
} else {
$stmt = db()->query("SELECT * FROM users ORDER BY id DESC");
}
$filteredAccounts = $stmt->fetchAll();
// Pagination logic
$page = max(1, (int)($_GET['p'] ?? 1));
$limit = 10;
$total = count($filteredAccounts);
$totalPages = max(1, ceil($total / $limit));
$offset = ($page - 1) * $limit;
$accounts = array_slice($filteredAccounts, $offset, $limit, true);
$availableBranches = branches();
require __DIR__ . '/includes/header.php';
?>
<section class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h3 class="h5 mb-2"><i class="bi bi-people me-2"></i><?= h(tr('الوصول حسب الدور', 'Role-based access')) ?></h3>
<p class="text-muted mb-0"><?= h(tr('إدارة المستخدمين وصلاحيات الوصول للنظام.', 'Manage users and system access permissions.')) ?></p>
</div>
<?php if(has_permission("users", "add")): ?>
<button type="button" class="btn btn-primary" onclick="openAddModal()">
<i class="bi bi-person-plus"></i> <?= h(tr('إضافة مستخدم', 'Add User')) ?>
</button>
<?php endif; ?>
</div>
<form class="d-flex mb-3" method="GET" action="users.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>
<?php if ($flash): ?>
<div class="alert alert-<?= h($flash['type'] === 'error' ? 'danger' : $flash['type']) ?> alert-dismissible fade show mt-3" role="alert">
<?= h($flash['message']) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
</section>
<section>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0 text-center">
<thead class="table-light">
<tr>
<th><?= h(tr('المستخدم', 'User')) ?></th>
<th><?= h(tr('الدور', 'Role')) ?></th>
<th><?= h(tr('الفرع', 'Branch')) ?></th>
<th><?= h(tr("صلاحيات مخصصة", "Custom Permissions")) ?></th>
<th><?= h(tr('إجراءات', 'Actions')) ?></th>
</tr>
</thead>
<tbody class="border-top-0">
<?php if(empty($accounts)): ?>
<tr><td colspan="7" class="text-center text-muted py-4"><?= h(tr('لا توجد بيانات', 'No data found')) ?></td></tr>
<?php endif; ?>
<?php foreach ($accounts as $account): ?>
<tr>
<td>
<div class="fw-semibold"><?= h(current_lang() === 'ar' ? $account['name_ar'] : $account['name_en']) ?></div>
<div class="small text-muted"><?= h($account['username']) ?></div>
</td>
<td><?= h(role_label($account['role'])) ?></td>
<td><?= h(branch_label($account['branch_code'])) ?></td>
<td><span class="badge text-bg-light border"><?= h(empty($account["permissions"]) || $account["permissions"] === "{}" ? tr("الافتراضي", "Default") : tr("مخصصة", "Custom")) ?></span></td>
<td>
<button class="btn btn-sm btn-outline-primary rounded-circle shadow-sm" style="width: 34px; height: 34px; padding: 0;"
onclick='openEditModal(<?= htmlspecialchars(json_encode($account), ENT_QUOTES, "UTF-8") ?>)' title="<?= h(tr('تعديل', 'Edit')) ?>">
<i class="bi bi-pencil"></i>
</button>
<?php if ($account['id'] !== $user['id'] && has_permission("users", "del")): ?>
<form method="POST" class="d-inline" onsubmit="confirmSwal(event, '<?= h(tr('هل أنت متأكد؟', 'Are you sure?')) ?>');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?= h($account['id']) ?>">
<button type="submit" class="btn btn-sm btn-outline-danger rounded-circle shadow-sm ms-1" style="width: 34px; height: 34px; padding: 0;" title="<?= h(tr('حذف', 'Delete')) ?>">
<i class="bi bi-trash"></i>
</button>
</form>
<?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('users.php', ['p' => $i, 'q' => $search])) ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
</ul>
</nav>
<?php endif; ?>
</section>
<!-- User Modal -->
<div class="modal fade" id="userModal" tabindex="-1" aria-labelledby="userModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form id="userForm" method="POST" action="users.php">
<input type="hidden" name="action" id="userAction" value="add">
<input type="hidden" name="id" id="userId" value="">
<div class="modal-header">
<h5 class="modal-title" id="userModalLabel"><?= h(tr('إضافة مستخدم جديد', 'Add New User')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6 mb-3">
<label for="userNameAr" class="form-label"><?= h(tr('الاسم (عربي)', 'Name (AR)')) ?></label>
<input type="text" class="form-control" id="userNameAr" name="name_ar" required>
</div>
<div class="col-md-6 mb-3">
<label for="userNameEn" class="form-label"><?= h(tr('الاسم (إنجليزي)', 'Name (EN)')) ?></label>
<input type="text" class="form-control" id="userNameEn" name="name_en">
</div>
<div class="col-md-6 mb-3">
<label for="username" class="form-label"><?= h(tr('اسم المستخدم للدخول', 'Login Username')) ?></label>
<input type="text" class="form-control" id="username" name="username" required autocomplete="new-username">
</div>
<div class="col-md-6 mb-3">
<label for="password" class="form-label"><?= h(tr('كلمة المرور', 'Password')) ?> <small class="text-muted" id="passwordHelp"></small></label>
<input type="password" class="form-control" id="password" name="password" autocomplete="new-password">
</div>
<div class="col-md-6 mb-3">
<label for="userRole" class="form-label"><?= h(tr('الدور', 'Role')) ?></label>
<select class="form-select" id="userRole" name="role" required>
<option value="cashier"><?= h(tr('كاشير', 'Cashier')) ?></option>
<option value="manager"><?= h(tr('مدير فرع', 'Branch Manager')) ?></option>
<option value="owner"><?= h(tr('مالك', 'Owner')) ?></option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="userBranch" class="form-label"><?= h(tr('الفرع', 'Branch')) ?></label>
<select class="form-select" id="userBranch" name="branch_code" required>
<?php foreach ($availableBranches as $code => $b): ?>
<option value="<?= h($code) ?>"><?= h(current_lang() === 'ar' ? $b['name_ar'] : $b['name_en']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-12 mb-3">
<label class="form-label"><?= h(tr('فروع إضافية يمكن الوصول إليها', 'Additional Accessible Branches')) ?></label>
<div class="d-flex flex-wrap gap-3 p-2 border rounded bg-light">
<?php foreach ($availableBranches as $code => $b): ?>
<div class="form-check">
<input class="form-check-input additional-branch" type="checkbox" name="allowed_branches[]" value="<?= h($code) ?>" id="ab_<?= h($code) ?>">
<label class="form-check-label ms-1" for="ab_<?= h($code) ?>">
<?= h(current_lang() === 'ar' ? $b['name_ar'] : $b['name_en']) ?>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
</div> <!-- Close row -->
<h6 class="mt-4 mb-3"><?= h(tr('صلاحيات الوصول المخصصة', 'Custom Access Permissions')) ?></h6>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead class="table-light">
<tr>
<th>
<div class="form-check d-inline-block mb-0">
<input class="form-check-input" type="checkbox" id="selectAllPerms" onchange="toggleAllPerms(this)">
<label class="form-check-label ms-1 fw-bold" for="selectAllPerms"><?= h(tr('الواجهة', 'Module')) ?></label>
</div>
</th>
<th class="text-center"><?= h(tr('عرض', 'Show')) ?></th>
<th class="text-center"><?= h(tr('إضافة', 'Add')) ?></th>
<th class="text-center"><?= h(tr('تعديل', 'Edit')) ?></th>
<th class="text-center"><?= h(tr('حذف', 'Delete')) ?></th>
</tr>
</thead>
<tbody>
<?php foreach (get_app_modules() as $key => $module): ?>
<tr>
<td>
<div class="form-check mb-0">
<input class="form-check-input row-perm-check" type="checkbox" id="row_<?= $key ?>" onchange="toggleRowPerms(this, '<?= $key ?>')">
<label class="form-check-label ms-1" for="row_<?= $key ?>">
<?= h(current_lang() === 'ar' ? $module['name_ar'] : $module['name_en']) ?>
</label>
</div>
</td>
<?php foreach (['show', 'add', 'edit', 'del'] as $act): ?>
<td class="text-center">
<?php if (in_array($act, $module['actions'])): ?>
<div class="form-check d-flex justify-content-center mb-0">
<input class="form-check-input perm-check perm-row-<?= $key ?>" type="checkbox" name="permissions[<?= $key ?>][<?= $act ?>]" id="perm_<?= $key ?>_<?= $act ?>" value="1">
</div>
<?php else:
?>
<span class="text-muted">-</span>
<?php endif; ?>
</td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</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>
<script>
let userModal;
function toggleAllPerms(source) {
document.querySelectorAll('.perm-check').forEach(cb => {
if (!cb.disabled) cb.checked = source.checked;
});
document.querySelectorAll('.row-perm-check').forEach(cb => {
if (!cb.disabled) cb.checked = source.checked;
});
}
function toggleRowPerms(source, key) {
document.querySelectorAll('.perm-row-' + key).forEach(cb => {
if (!cb.disabled) cb.checked = source.checked;
});
updateSelectAllCheckbox();
}
function updateSelectAllCheckbox() {
let allPerms = document.querySelectorAll('.perm-check');
let allChecked = document.querySelectorAll('.perm-check:checked');
let selectAllCb = document.getElementById('selectAllPerms');
if (selectAllCb) {
selectAllCb.checked = (allPerms.length > 0 && allPerms.length === allChecked.length);
}
}
document.addEventListener('DOMContentLoaded', function() {
userModal = new bootstrap.Modal(document.getElementById('userModal'));
document.querySelectorAll('.perm-check').forEach(cb => {
cb.addEventListener('change', function() {
let rowClass = Array.from(this.classList).find(c => c.startsWith('perm-row-'));
if (rowClass) {
let key = rowClass.replace('perm-row-', '');
let rowPerms = document.querySelectorAll('.' + rowClass);
let rowChecked = document.querySelectorAll('.' + rowClass + ':checked');
let rowCb = document.getElementById('row_' + key);
if (rowCb) {
rowCb.checked = (rowPerms.length > 0 && rowPerms.length === rowChecked.length);
}
}
updateSelectAllCheckbox();
});
});
});
function openAddModal() {
document.getElementById('userAction').value = 'add';
document.getElementById('userId').value = '';
document.getElementById('userForm').reset();
document.querySelectorAll('.additional-branch').forEach(cb => cb.checked = false);
document.querySelectorAll('.perm-check').forEach(cb => cb.checked = false);
document.querySelectorAll('.row-perm-check').forEach(cb => cb.checked = false);
if(document.getElementById('selectAllPerms')) document.getElementById('selectAllPerms').checked = false;
document.getElementById('userModalLabel').innerText = '<?= h(tr('إضافة مستخدم جديد', 'Add New User')) ?>';
document.getElementById('password').required = true;
document.getElementById('passwordHelp').innerText = '';
userModal.show();
}
function openEditModal(account) {
document.getElementById('userAction').value = 'edit';
document.getElementById('userId').value = account.id;
document.getElementById('userNameAr').value = account.name_ar;
document.getElementById('userNameEn').value = account.name_en;
document.getElementById('username').value = account.username;
document.getElementById('userRole').value = account.role;
document.getElementById('userBranch').value = account.branch_code;
document.getElementById('password').required = false;
document.getElementById('password').value = '';
document.getElementById('passwordHelp').innerText = '(<?= h(tr('اتركه فارغاً لعدم التغيير', 'Leave blank to keep unchanged')) ?>)';
document.querySelectorAll('.additional-branch').forEach(cb => cb.checked = false);
if (account.allowed_branches) {
let abs = account.allowed_branches.split(',');
abs.forEach(b => {
let cb = document.getElementById('ab_' + b.trim());
if (cb) cb.checked = true;
});
}
document.querySelectorAll('.perm-check').forEach(cb => cb.checked = false);
document.querySelectorAll('.row-perm-check').forEach(cb => cb.checked = false);
if(document.getElementById('selectAllPerms')) document.getElementById('selectAllPerms').checked = false;
if (account.permissions) {
try {
let perms = typeof account.permissions === 'string' ? JSON.parse(account.permissions) : account.permissions;
for (let mod in perms) {
for (let act in perms[mod]) {
let cb = document.getElementById('perm_' + mod + '_' + act);
if (cb) cb.checked = true;
}
}
} catch (e) {}
}
document.querySelectorAll('.row-perm-check').forEach(rowCb => {
let key = rowCb.id.replace('row_', '');
let rowPerms = document.querySelectorAll('.perm-row-' + key);
let rowChecked = document.querySelectorAll('.perm-row-' + key + ':checked');
rowCb.checked = (rowPerms.length > 0 && rowPerms.length === rowChecked.length);
});
updateSelectAllCheckbox();
document.getElementById('userModalLabel').innerText = '<?= h(tr('تعديل مستخدم', 'Edit User')) ?>';
userModal.show();
}
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>