0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'None',
]);
}
session_start();
if (isset($_GET['action']) && $_GET['action'] === 'download_items_template') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=items_import_template.csv');
$output = fopen('php://output', 'w');
// Add BOM for Excel UTF-8 compatibility
fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF));
fputcsv($output, ['SKU', 'English Name', 'Arabic Name', 'Sale Price', 'Cost Price']);
fclose($output);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
file_put_contents('post_debug.log', date('Y-m-d H:i:s') . " - POST: " . json_encode($_POST) . "\n", FILE_APPEND);
}
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/SimpleXLSX.php';
require_once __DIR__ . '/includes/stock_helper.php';
require_once __DIR__ . '/db/BackupService.php';
// Helper for current outlet
if (!function_exists('current_outlet_id')) {
function current_outlet_id() {
if (session_status() === PHP_SESSION_NONE) session_start();
return (int)($_SESSION['outlet_id'] ?? 1);
}
}
if (!function_exists('db_table_exists')) {
function db_table_exists(string $tableName): bool {
static $cache = [];
$normalized = strtolower($tableName);
if (array_key_exists($normalized, $cache)) {
return $cache[$normalized];
}
try {
$stmt = db()->prepare("SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? LIMIT 1");
$stmt->execute([$tableName]);
$cache[$normalized] = (bool)$stmt->fetchColumn();
} catch (Throwable $e) {
$cache[$normalized] = false;
}
return $cache[$normalized];
}
}
if (!function_exists('db_column_exists')) {
function db_column_exists(string $tableName, string $columnName): bool {
static $cache = [];
$cacheKey = strtolower($tableName . '.' . $columnName);
if (array_key_exists($cacheKey, $cache)) {
return $cache[$cacheKey];
}
try {
$stmt = db()->prepare("SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1");
$stmt->execute([$tableName, $columnName]);
$cache[$cacheKey] = (bool)$stmt->fetchColumn();
} catch (Throwable $e) {
$cache[$cacheKey] = false;
}
return $cache[$cacheKey];
}
}
if (!function_exists('db_first_existing_column')) {
function db_first_existing_column(string $tableName, array $columnNames): ?string {
foreach ($columnNames as $columnName) {
if (db_column_exists($tableName, (string)$columnName)) {
return (string)$columnName;
}
}
return null;
}
}
if (!function_exists('entity_tax_column')) {
function entity_tax_column(string $tableName): ?string {
return db_first_existing_column($tableName, ['tax_id', 'tax_number', 'vat_number', 'tax_no', 'vat_no', 'trn']);
}
}
if (!function_exists('db_insert_sql_for_existing_columns')) {
function db_insert_sql_for_existing_columns(string $tableName, array $columnValueMap): array {
$columns = [];
$values = [];
foreach ($columnValueMap as $columnName => $value) {
if (!db_column_exists($tableName, (string)$columnName)) {
continue;
}
$columns[] = (string)$columnName;
$values[] = $value;
}
if (empty($columns)) {
throw new RuntimeException("No compatible insert columns found for {$tableName}.");
}
$quotedColumns = '`' . implode('`, `', $columns) . '`';
$placeholders = implode(', ', array_fill(0, count($columns), '?'));
return ["INSERT INTO `{$tableName}` ({$quotedColumns}) VALUES ({$placeholders})", $values];
}
}
if (!function_exists('sales_return_reference_column')) {
function sales_return_reference_column(): string {
return db_first_existing_column('sales_returns', ['invoice_id', 'sale_id']) ?? 'invoice_id';
}
}
if (!function_exists('purchase_return_reference_column')) {
function purchase_return_reference_column(): string {
return db_first_existing_column('purchase_returns', ['invoice_id', 'purchase_id']) ?? 'invoice_id';
}
}
if (!function_exists('line_item_vat_amount')) {
function line_item_vat_amount(PDO $db, array $item): float {
if (isset($item['vat_amount']) && $item['vat_amount'] !== '' && $item['vat_amount'] !== null) {
return (float)$item['vat_amount'];
}
$vatRate = 0.0;
if (isset($item['vat_rate']) && $item['vat_rate'] !== '' && $item['vat_rate'] !== null) {
$vatRate = (float)$item['vat_rate'];
} elseif (!empty($item['item_id'])) {
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([(int)$item['item_id']]);
$vatRate = (float)$stmtVat->fetchColumn();
}
$lineTotal = isset($item['total_price']) && $item['total_price'] !== null
? (float)$item['total_price']
: ((float)($item['quantity'] ?? 0) * (float)($item['unit_price'] ?? 0));
return $lineTotal * ($vatRate / 100);
}
}
if (!function_exists('runtime_debug_can_render_details')) {
function runtime_debug_can_render_details(): bool {
if (PHP_SAPI === 'cli') {
return true;
}
$roleName = (string)($_SESSION['user_role_name'] ?? '');
if (strcasecmp($roleName, 'Administrator') === 0 || (int)($_SESSION['user_id'] ?? 0) === 1) {
return true;
}
$remoteAddress = $_SERVER['REMOTE_ADDR'] ?? '';
return in_array($remoteAddress, ['127.0.0.1', '::1'], true);
}
}
if (!function_exists('runtime_debug_infer_schema_hint')) {
function runtime_debug_infer_schema_hint(Throwable $throwable): ?string {
$message = $throwable->getMessage();
if (str_contains($message, 'Call to undefined method DatabaseInstaller::ensureCurrentSchema()')) {
return 'Outdated installer class: re-sync includes/DatabaseInstaller.php with the latest project version.';
}
if (preg_match("/Table '[^']+\.([^']+)' doesn't exist/i", $message, $matches)) {
return 'Missing table: ' . $matches[1];
}
if (preg_match("/Unknown column '([^']+)'/i", $message, $matches)) {
return 'Missing column: ' . $matches[1];
}
if (preg_match('/Base table or view not found: [0-9]+ ([a-zA-Z0-9_]+)/i', $message, $matches)) {
return 'Missing table or view: ' . $matches[1];
}
return null;
}
}
if (!function_exists('runtime_debug_log')) {
function runtime_debug_log(Throwable $throwable): void {
$parts = [
date('Y-m-d H:i:s'),
'[' . get_class($throwable) . ']',
$throwable->getMessage(),
'file=' . $throwable->getFile() . ':' . $throwable->getLine(),
'page=' . ($_GET['page'] ?? 'dashboard'),
'uri=' . ($_SERVER['REQUEST_URI'] ?? 'cli'),
'user_id=' . (string)($_SESSION['user_id'] ?? 0),
];
$hint = runtime_debug_infer_schema_hint($throwable);
if ($hint !== null) {
$parts[] = 'hint=' . $hint;
}
if ($throwable instanceof PDOException && isset($throwable->errorInfo) && is_array($throwable->errorInfo)) {
$parts[] = 'error_info=' . json_encode($throwable->errorInfo, JSON_UNESCAPED_UNICODE);
}
$trace = explode("
", $throwable->getTraceAsString());
if ($trace !== []) {
$parts[] = 'trace=' . implode(' | ', array_slice($trace, 0, 5));
}
@file_put_contents(__DIR__ . '/runtime_debug.log', implode(' || ', $parts) . PHP_EOL, FILE_APPEND);
}
}
if (!function_exists('runtime_debug_render_exception')) {
function runtime_debug_render_exception(Throwable $throwable): void {
runtime_debug_log($throwable);
$showDetails = runtime_debug_can_render_details();
while (ob_get_level() > 0) {
@ob_end_clean();
}
if (!headers_sent()) {
http_response_code(500);
header('Content-Type: text/html; charset=UTF-8');
header('X-Robots-Tag: noindex, nofollow');
}
$hint = runtime_debug_infer_schema_hint($throwable);
$requestUri = (string)($_SERVER['REQUEST_URI'] ?? 'cli');
$page = (string)($_GET['page'] ?? 'dashboard');
$tracePreview = array_slice(explode("
", $throwable->getTraceAsString()), 0, 8);
$traceText = implode("
", $tracePreview);
$title = $showDetails ? 'Application Debug' : 'Application Error';
$summary = $showDetails
? 'The request failed. The details below should help identify the missing table or column.'
: 'An unexpected error occurred while loading this page.';
?>
= htmlspecialchars($title) ?>
HTTP 500
= htmlspecialchars($title) ?>
= htmlspecialchars($summary) ?>
Exception
= htmlspecialchars(get_class($throwable)) ?>
Message
= htmlspecialchars($throwable->getMessage()) ?>
File
= htmlspecialchars($throwable->getFile()) ?>
Line
= (int)$throwable->getLine() ?>
Page
= htmlspecialchars($page) ?>
Request URI
= htmlspecialchars($requestUri) ?>
Schema hint: = htmlspecialchars($hint) ?>
= htmlspecialchars($traceText) ?>
A copy of this failure was written to runtime_debug.log.
prepare("SELECT id FROM outlets WHERE id = ? AND status = 'active'");
$stmt->execute([$target_id]);
if ($stmt->fetchColumn()) {
$_SESSION['outlet_id'] = $target_id;
}
}
}
header("Location: index.php");
exit;
}
// Timezone Setup
try {
$tz_stmt = db()->prepare("SELECT value FROM settings WHERE `key` = 'timezone'");
$tz_stmt->execute();
$app_tz = $tz_stmt->fetchColumn();
if ($app_tz && in_array($app_tz, DateTimeZone::listIdentifiers())) {
date_default_timezone_set($app_tz);
}
} catch (Exception $e) {
// Ignore if DB not ready
}
require_once 'includes/DatabaseInstaller.php';
// Auto-install database if not installed, then ensure pending migrations are applied.
try {
if (!DatabaseInstaller::isInstalled()) {
DatabaseInstaller::install();
} elseif (method_exists('DatabaseInstaller', 'ensureCurrentSchema')) {
DatabaseInstaller::ensureCurrentSchema();
} else {
error_log('Skipping DatabaseInstaller::ensureCurrentSchema() because the loaded DatabaseInstaller class does not define it.');
}
} catch (Throwable $e) {
die("Installation Error: " . $e->getMessage());
}
require_once 'lib/LicenseService.php';
require_once 'includes/lang.php';
// Language Setup
if (isset($_GET['lang'])) {
$_SESSION['lang'] = in_array($_GET['lang'], ['en', 'ar']) ? $_GET['lang'] : 'ar';
}
if (!isset($_SESSION['lang'])) {
$_SESSION['lang'] = 'ar'; // Default to Arabic as requested
}
$lang = $_SESSION['lang'];
$dir = ($lang === 'ar') ? 'rtl' : 'ltr';
// Licensing Middleware
try {
$is_activated = LicenseService::isActivated();
$trial_days = LicenseService::getTrialRemainingDays();
$can_access = LicenseService::canAccess();
} catch (PDOException $e) {
die("Database Connection Error: " . $e->getMessage() . " Please check your db/config.php settings.");
} catch (Exception $e) {
die("Application Error: " . $e->getMessage());
}
$page = $_GET['page'] ?? 'dashboard';
if (!$can_access && $page !== 'activate') {
header("Location: index.php?page=activate");
exit;
}
// Activation Page UI (accessible without login)
if ($page === 'activate') {
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['activate'])) {
$res = LicenseService::activate($_POST['license_key'] ?? '');
if ($res['success']) {
$success = "System activated successfully! Redirecting...";
header("refresh:2;url=index.php");
} else {
$error = $res['error'];
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['start_trial'])) {
// We need a way to call startTrial() which is private in LicenseService
// I'll make it public or use a public wrapper
$res = LicenseService::initTrial();
if ($res['success']) {
$success = "Trial period started! Redirecting...";
header("refresh:2;url=index.php");
} else {
$error = $res['error'];
}
}
?>
= __('activate_product') ?>
= __('activate_product') ?>
= $lang === 'ar' ? 'العربية' : 'English' ?>
= $lang === 'ar' ? 'يرجى إدخال مفتاح التسلسل للمتابعة.' : 'Please enter your serial key to continue using the application.' ?>
= $error ?>
= $success ?>
= $lang === 'ar' ? 'أو ابدأ الفترة التجريبية (15 يوم)' : 'Or start your 15-day trial period' ?>
query($sql);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// Missing helper functions
function getLoyaltyMultiplier($tier) {
switch (strtolower((string)$tier)) {
case 'gold': return 2.0;
case 'silver': return 1.5;
default: return 1.0;
}
}
function numberToWords($num) {
$num = (int)$num;
if ($num == 0) return "zero";
$ones = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"];
$tens = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"];
if ($num < 20) return $ones[$num];
if ($num < 100) return $tens[(int)($num / 10)] . ($num % 10 ? "-" . $ones[$num % 10] : "");
if ($num < 1000) return $ones[(int)($num / 100)] . " hundred" . ($num % 100 ? " and " . numberToWords($num % 100) : "");
if ($num < 1000000) return numberToWords((int)($num / 1000)) . " thousand" . ($num % 1000 ? " " . numberToWords($num % 1000) : "");
return (string)$num;
}
function numberToWordsArabic($num) {
$num = (int)$num;
if ($num == 0) return "صفر";
$ones = ["", "واحد", "اثنان", "ثلاثة", "أربعة", "خمسة", "ستة", "سبعة", "ثمانية", "تسعة", "عشرة", "أحد عشر", "اثنا عشر", "ثلاثة عشر", "أربعة عشر", "خمسة عشر", "ستة عشر", "سبعة عشر", "ثمانية عشر", "تسعة عشر"];
$tens = ["", "", "عشرون", "ثلاثون", "أربعون", "خمسون", "ستون", "سبعون", "ثمانون", "تسعون"];
$hundreds = ["", "مائة", "مائتان", "ثلاثمائة", "أربعمائة", "خمسمائة", "ستمائة", "سبعمائة", "ثمانمائة", "تسعمائة"];
if ($num < 20) return $ones[$num];
if ($num < 100) return ($num % 10 ? $ones[$num % 10] . " و " : "") . $tens[(int)($num / 10)];
if ($num < 1000) return $hundreds[(int)($num / 100)] . ($num % 100 ? " و " . numberToWordsArabic($num % 100) : "");
if ($num < 1000000) {
$thousands = (int)($num / 1000);
$rem = $num % 1000;
$tStr = "ألف";
if ($thousands == 1) $tStr = "ألف";
else if ($thousands == 2) $tStr = "ألفين";
else if ($thousands >= 3 && $thousands <= 10) $tStr = numberToWordsArabic($thousands) . " آلاف";
else $tStr = numberToWordsArabic($thousands) . " ألف";
return $tStr . ($rem ? " و " . numberToWordsArabic($rem) : "");
}
return (string)$num;
}
function renderPagination($currentPage, $totalPages) {
$query = $_GET;
unset($query['p']);
$url = 'index.php?' . http_build_query($query) . '&p=';
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 20;
$limitHtml = "
Rows per page:
5, 'p' => 1])) . "' " . ($limit == 5 ? 'selected' : '') . ">5
20, 'p' => 1])) . "' " . ($limit == 20 ? 'selected' : '') . ">20
40, 'p' => 1])) . "' " . ($limit == 40 ? 'selected' : '') . ">40
100, 'p' => 1])) . "' " . ($limit == 100 ? 'selected' : '') . ">100
200, 'p' => 1])) . "' " . ($limit == 200 ? 'selected' : '') . ">200
500, 'p' => 1])) . "' " . ($limit == 500 ? 'selected' : '') . ">500
";
if ($totalPages <= 1) return $limitHtml;
$html = ' ';
return $html;
}
// Login Logic
$login_error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
$user = trim((string)($_POST['username'] ?? ''));
$pass = (string)($_POST['password'] ?? '');
try {
$stmt = db()->prepare("SELECT u.*, g.name as role_name FROM users u LEFT JOIN role_groups g ON u.group_id = g.id WHERE u.username = ? AND u.status = 'active'");
$stmt->execute([$user]);
$u = $stmt->fetch();
if ($u && password_verify($pass, (string)($u['password'] ?? ''))) {
$_SESSION['user_id'] = $u['id'];
$_SESSION['username'] = $u['username'];
$_SESSION['user_role_name'] = $u['role_name'];
$permissions = [];
if (db_table_exists('role_permissions')) {
$permStmt = db()->prepare("SELECT permission FROM role_permissions WHERE role_id = ?");
$permStmt->execute([$u['group_id']]);
$permissions = $permStmt->fetchAll(PDO::FETCH_COLUMN) ?: [];
}
$_SESSION['user_permissions'] = $permissions;
$_SESSION['profile_pic'] = $u['profile_pic'] ?? null;
$_SESSION['theme'] = $u['theme'] ?? 'default';
$user_outlets = [];
if (db_table_exists('user_outlets')) {
$outletStmt = db()->prepare("SELECT outlet_id FROM user_outlets WHERE user_id = ?");
$outletStmt->execute([$u['id']]);
$user_outlets = $outletStmt->fetchAll(PDO::FETCH_COLUMN) ?: [];
}
if (empty($user_outlets)) {
if (($u['role_name'] ?? '') === 'Administrator' && db_table_exists('outlets')) {
$allOutlets = db()->query("SELECT id FROM outlets WHERE status = 'active'")->fetchAll(PDO::FETCH_COLUMN);
$user_outlets = $allOutlets ?: [1];
} else {
$user_outlets = [1];
}
}
$user_outlets = array_values(array_unique(array_map('intval', $user_outlets)));
if ($user_outlets === []) {
$user_outlets = [1];
}
$_SESSION['user_outlets'] = $user_outlets;
$_SESSION['outlet_id'] = $user_outlets[0];
header("Location: index.php");
exit;
}
$login_error = "Invalid username or password";
$reason = (!$u) ? "User not found or inactive" : "Password mismatch";
file_put_contents('login_debug.log', date('Y-m-d H:i:s') . " - Failed login for '$user'. Reason: $reason\n", FILE_APPEND);
} catch (Throwable $e) {
file_put_contents('login_debug.log', date('Y-m-d H:i:s') . " - Login exception for '$user': " . $e->getMessage() . "\n", FILE_APPEND);
$login_error = $lang === 'ar'
? 'تعذر تسجيل الدخول مؤقتًا. يرجى تحديث الصفحة والمحاولة مرة أخرى.'
: 'Sign-in is temporarily unavailable. Please refresh the page and try again.';
}
}
// Logout
if (isset($_GET['action']) && $_GET['action'] === 'logout') {
session_destroy();
header("Location: index.php");
exit;
}
// --- POS AJAX Handlers ---
if (isset($_GET['action']) || isset($_POST['action'])) {
$action = $_GET['action'] ?? $_POST['action'] ?? '';
if ($action === 'validate_discount') {
header('Content-Type: application/json');
$code = $_GET['code'] ?? '';
$stmt = db()->prepare("SELECT * FROM discount_codes WHERE code = ? AND status = 'active' AND (expiry_date IS NULL OR expiry_date >= CURDATE())");
$stmt->execute([$code]);
$discount = $stmt->fetch(PDO::FETCH_ASSOC);
if ($discount) {
echo json_encode(['success' => true, 'discount' => $discount]);
} else {
echo json_encode(['success' => false, 'error' => 'Invalid or expired discount code']);
}
exit;
}
if ($action === 'search_items') {
file_put_contents('search_debug.log', date('Y-m-d H:i:s') . " - search_items call: q=" . ($_GET['q'] ?? '') . "\n", FILE_APPEND);
header('Content-Type: application/json');
$q = $_GET['q'] ?? '';
if (strlen($q) < 1) {
echo json_encode([]);
exit;
}
$searchTerm = "%$q%";
$oid = current_outlet_id(); $stmt = db()->prepare("SELECT i.*, i.stock_quantity FROM stock_items i WHERE (i.name_en LIKE ? OR i.name_ar LIKE ? OR i.sku LIKE ?) LIMIT 15");
$stmt->execute([$searchTerm, $searchTerm, $searchTerm]);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
exit;
}
if ($action === 'pos_search_items') {
$q = $_GET['q'] ?? '';
$searchTerm = "%$q%";
$oid = current_outlet_id();
$sql = "SELECT * FROM stock_items WHERE outlet_id = ? AND (name_en LIKE ? OR name_ar LIKE ? OR sku LIKE ?) ORDER BY name_en ASC LIMIT 100";
$products_raw = db()->prepare($sql);
$products_raw->execute([$oid, $searchTerm, $searchTerm, $searchTerm]);
while($p = $products_raw->fetch(PDO::FETCH_ASSOC)) {
$p['original_price'] = (float)$p['sale_price'];
$p['sale_price'] = getPromotionalPrice($p);
// Render Card HTML
?>
= htmlspecialchars($p['name_ar']) ?>
= htmlspecialchars($p['name_en']) ?>
= htmlspecialchars($p['name_en']) ?>
OMR = number_format($p['original_price'], 3) ?>
OMR = number_format((float)$p['sale_price'], 3) ?>
= (float)$p['stock_quantity'] ?> left
prepare("SELECT * FROM stock_items WHERE sku = ? AND outlet_id = ? LIMIT 1");
$stmt->execute([$lookupSku, $oid]);
$p = $stmt->fetch(PDO::FETCH_ASSOC);
if ($p) {
$p['original_price'] = (float)$p['sale_price'];
$p['sale_price'] = getPromotionalPrice($p);
$p['price'] = (float)$p['sale_price'];
$p['nameEn'] = $p['name_en'];
$p['nameAr'] = $p['name_ar'];
$p['vatRate'] = $p['vat_rate'];
if ($weightBarcode) {
$qty = 0.0;
if ($weightBarcode['mode'] === 'price') {
if ((float)$p['sale_price'] <= 0) {
echo json_encode(['error' => 'This item cannot use price-based scale barcodes because its sale price is zero.']);
exit;
}
$qty = round(((float)$weightBarcode['value']) / (float)$p['sale_price'], 3);
} else {
$qty = round((float)$weightBarcode['value'], 3);
}
if ($qty <= 0) {
echo json_encode(['error' => 'The weighing scale barcode value is invalid.']);
exit;
}
$p['qty'] = $qty;
$p['is_scale_barcode'] = true;
$p['scale_barcode_mode'] = $weightBarcode['mode'];
$p['scale_barcode_value'] = (float)$weightBarcode['value'];
$p['scanned_barcode'] = $sku;
}
echo json_encode($p);
} else {
echo json_encode(null);
}
exit;
}
if ($action === 'get_payments') {
header('Content-Type: application/json');
$invoice_id = (int)$_GET['invoice_id'];
$stmt = db()->prepare("SELECT * FROM payments WHERE invoice_id = ? ORDER BY payment_date DESC");
$stmt->execute([$invoice_id]);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
exit;
}
if ($action === 'get_payment_details') {
header('Content-Type: application/json');
$payment_id = (int)$_GET['payment_id'];
$stmt = db()->prepare("SELECT p.*, i.customer_id, c.name as customer_name, o.name as outlet_name
FROM payments p
JOIN invoices i ON p.invoice_id = i.id
JOIN customers c ON i.customer_id = c.id
LEFT JOIN outlets o ON i.outlet_id = o.id
WHERE p.id = ?");
$stmt->execute([$payment_id]);
echo json_encode($stmt->fetch(PDO::FETCH_ASSOC));
exit;
}
if ($action === 'get_held_carts') {
header('Content-Type: application/json');
$stmt = db()->query("SELECT h.*, c.name as customer_name FROM pos_held_carts h LEFT JOIN customers c ON h.customer_id = c.id ORDER BY h.created_at DESC");
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
exit;
}
if ($action === 'hold_pos_cart') {
header('Content-Type: application/json');
$name = $_POST['cart_name'] ?? 'Untitled Cart';
$items = $_POST['items'] ?? '[]';
$customer_id = !empty($_POST['customer_id']) ? (int)$_POST['customer_id'] : null;
$stmt = db()->prepare("INSERT INTO pos_held_carts (cart_name, items_json, customer_id) VALUES (?, ?, ?)");
$stmt->execute([$name, $items, $customer_id]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete_held_cart') {
header('Content-Type: application/json');
$id = (int)$_POST['id'];
$stmt = db()->prepare("DELETE FROM pos_held_carts WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'save_pos_transaction') {
header('Content-Type: application/json');
$db = db();
try {
$db->beginTransaction();
$customer_id = !empty($_POST['customer_id']) ? (int)$_POST['customer_id'] : null;
$payments = json_decode($_POST['payments'] ?? '[]', true);
$items = json_decode($_POST['items'] ?? '[]', true);
$total_amount = (float)($_POST['total_amount'] ?? 0);
$tax_amount = (float)($_POST['tax_amount'] ?? 0);
$discount_code_id = !empty($_POST['discount_code_id']) ? (int)$_POST['discount_code_id'] : null;
$discount_amount = (float)($_POST['discount_amount'] ?? 0);
$loyalty_redeemed = (float)($_POST['loyalty_redeemed'] ?? 0);
$net_amount = $total_amount - $discount_amount - $loyalty_redeemed;
$transaction_no = 'POS-' . time() . rand(10, 99);
$session_id = $_SESSION['register_session_id'] ?? null;
if (!$session_id) {
// Fallback: try to find an open session for this user
$check_session = $db->prepare("SELECT id FROM register_sessions WHERE user_id = ? AND status = 'open' LIMIT 1");
$check_session->execute([$_SESSION['user_id']]);
$session_id = $check_session->fetchColumn() ?: null;
if ($session_id) {
$_SESSION['register_session_id'] = $session_id;
}
}
// Insert into unified Invoice table
$items_for_journal = [];
foreach ($items as $item) {
$items_for_journal[] = ['id' => $item['id'], 'qty' => $item['qty']];
}
$outlet_id = current_outlet_id();
if ($outlet_id == -1) $outlet_id = 1; // Default to main branch if All Outlets selected
$stmt = $db->prepare("INSERT INTO invoices (transaction_no, customer_id, invoice_date, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, status, register_session_id, is_pos, discount_amount, loyalty_points_redeemed, created_by, outlet_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'paid', ?, 1, ?, ?, ?, ?)");
$stmt->execute([$transaction_no, $customer_id, date('Y-m-d'), 'pos', $total_amount, $tax_amount, $net_amount, $net_amount, $session_id, $discount_amount, $loyalty_redeemed, $_SESSION['user_id'], $outlet_id]);
$transaction_id = (int)$db->lastInsertId();
// Insert Items & Update Stock
$stmtItem = $db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)");
// $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
foreach ($items as $item) {
$sub = (float)$item['price'] * (float)$item['qty'];
$va = (float)($item['vat_amount'] ?? 0);
$stmtItem->execute([$transaction_id, $item['id'], $item['qty'], $item['price'], $va, $sub]);
update_stock($item['id'], -$item['qty'], $outlet_id);
}
// Insert Payments
require_once 'includes/accounting_helper.php';
$stmtPay = $db->prepare("INSERT INTO payments (invoice_id, amount, payment_date, payment_method, notes) VALUES (?, ?, ?, ?, ?)");
foreach ($payments as $p) {
$stmtPay->execute([$transaction_id, $p['amount'], date('Y-m-d'), $p['method'], 'POS Transaction']);
$payment_id = $db->lastInsertId();
recordPaymentReceivedJournal((int)$payment_id, $p['amount'], date('Y-m-d'), $p['method']);
}
// Update Loyalty Points if customer exists
if ($customer_id) {
// Earn points
$points_earned = floor($net_amount);
$stmtPoints = $db->prepare("UPDATE customers SET loyalty_points = loyalty_points - ? + ? WHERE id = ?");
$stmtPoints->execute([$loyalty_redeemed * 100, $points_earned, $customer_id]);
// Record transactions
if ($points_earned > 0) {
$db->prepare("INSERT INTO loyalty_transactions (customer_id, transaction_id, points_change, transaction_type, description) VALUES (?, ?, ?, 'earned', ?)")
->execute([$customer_id, $transaction_id, $points_earned, "Earned from POS order #$transaction_no"]);
}
if ($loyalty_redeemed > 0) {
$db->prepare("INSERT INTO loyalty_transactions (customer_id, transaction_id, points_change, transaction_type, description) VALUES (?, ?, ?, 'redeemed', ?)")
->execute([$customer_id, $transaction_id, -$loyalty_redeemed * 100, "Redeemed for POS order #$transaction_no"]);
}
// Update invoice with points earned
$db->prepare("UPDATE invoices SET loyalty_points_earned = ? WHERE id = ?")->execute([$points_earned, $transaction_id]);
}
// Record Sale Journal for POS
recordSaleJournal($transaction_id, $net_amount, date('Y-m-d'), $items_for_journal, $tax_amount);
$db->commit();
echo json_encode(['success' => true, 'invoice_id' => $transaction_id, 'transaction_no' => $transaction_no]);
} catch (Exception $e) {
$db->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
if ($action === 'save_theme') {
header('Content-Type: application/json');
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'error' => 'Not authenticated']);
exit;
}
$theme = $_POST['theme'] ?? 'default';
$allowed = ['default', 'dark', 'ocean', 'forest', 'sunset', 'nord', 'dracula', 'citrus'];
if (!in_array($theme, $allowed)) $theme = 'default';
$stmt = db()->prepare("UPDATE users SET theme = ? WHERE id = ?");
$stmt->execute([$theme, $_SESSION['user_id']]);
$_SESSION['theme'] = $theme;
echo json_encode(['success' => true]);
exit;
}
if ($action === 'get_invoice_items') {
header('Content-Type: application/json');
$invoice_id = (int)$_GET['invoice_id'];
$type = $_GET['type'] ?? 'sale';
if ($type === 'purchase') {
$stmt = db()->prepare("SELECT pi.*, i.name_en, i.name_ar, i.sku
FROM purchase_items pi
JOIN stock_items i ON pi.item_id = i.id
WHERE pi.purchase_id = ?");
} else {
$stmt = db()->prepare("SELECT ii.*, i.name_en, i.name_ar, i.sku
FROM invoice_items ii
JOIN stock_items i ON ii.item_id = i.id
WHERE ii.invoice_id = ?");
}
$stmt->execute([$invoice_id]);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
exit;
}
if ($action === 'get_return_details') {
header('Content-Type: application/json');
$return_id = (int)$_GET['return_id'];
$type = $_GET['type'] ?? 'sale';
if ($type === 'purchase') {
$purchaseReturnReferenceColumn = purchase_return_reference_column();
$stmt = db()->prepare("SELECT pr.*, pr.`{$purchaseReturnReferenceColumn}` AS purchase_id, c.name as party_name FROM purchase_returns pr LEFT JOIN suppliers c ON pr.supplier_id = c.id WHERE pr.id = ?");
$stmt->execute([$return_id]);
$return = $stmt->fetch(PDO::FETCH_ASSOC);
if ($return) {
$stmtItems = db()->prepare("SELECT pri.*, i.name_en, i.name_ar, i.sku FROM purchase_return_items pri JOIN stock_items i ON pri.item_id = i.id WHERE pri.return_id = ?");
$stmtItems->execute([$return_id]);
$return['items'] = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
}
} else {
$salesReturnReferenceColumn = sales_return_reference_column();
$stmt = db()->prepare("SELECT sr.*, sr.`{$salesReturnReferenceColumn}` AS invoice_id, c.name as party_name FROM sales_returns sr LEFT JOIN customers c ON sr.customer_id = c.id WHERE sr.id = ?");
$stmt->execute([$return_id]);
$return = $stmt->fetch(PDO::FETCH_ASSOC);
if ($return) {
$stmtItems = db()->prepare("SELECT sri.*, i.name_en, i.name_ar, i.sku FROM sales_return_items sri JOIN stock_items i ON sri.item_id = i.id WHERE sri.return_id = ?");
$stmtItems->execute([$return_id]);
$return['items'] = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
}
}
echo json_encode($return);
exit;
}
if ($action === 'translate') {
header('Content-Type: application/json');
require_once __DIR__ . '/includes/translation_helper.php';
$text = trim((string) ($_POST['text'] ?? ''));
$target = strtolower(trim((string) ($_POST['target'] ?? ''))) === 'en' ? 'en' : 'ar';
if ($text === '') {
echo json_encode(['success' => false, 'error' => 'No text provided'], JSON_UNESCAPED_UNICODE);
exit;
}
$translationResult = app_translate_text($text, $target);
if (!empty($translationResult['success']) && !empty($translationResult['translated'])) {
echo json_encode([
'success' => true,
'translated' => $translationResult['translated'],
'provider' => $translationResult['provider'] ?? 'unknown',
], JSON_UNESCAPED_UNICODE);
} else {
$errorMessage = (string) ($translationResult['error'] ?? 'Translation failed');
error_log("Translation failed for text '$text': " . json_encode($translationResult, JSON_UNESCAPED_UNICODE));
echo json_encode(['success' => false, 'error' => $errorMessage], JSON_UNESCAPED_UNICODE);
}
exit;
}
}
// Redirect to login if not authenticated
if (!isset($_SESSION['user_id'])) {
?>
= __('sign_in') ?> - Admin Panel
= __('welcome_back') ?>
= $lang === 'ar' ? 'يرجى إدخال تفاصيلك لتسجيل الدخول' : 'Please enter your details to sign in' ?>
= $login_error ?>
prepare("SELECT value FROM settings WHERE `key` = ? LIMIT 1");
$stmt->execute([$key]);
$value = $stmt->fetchColumn();
} catch (Throwable $e) {
$value = false;
}
if ($value === false || $value === null || $value === '') $value = $default;
$cache[$key] = $value;
return $value;
}
function getWeightBarcodeConfig(): array {
$prefixStart = (int)(getSettingValue('weight_barcode_prefix_start', '20') ?? '20');
$prefixEnd = (int)(getSettingValue('weight_barcode_prefix_end', '29') ?? '29');
if ($prefixStart < 20 || $prefixStart > 29) $prefixStart = 20;
if ($prefixEnd < 20 || $prefixEnd > 29) $prefixEnd = 29;
if ($prefixStart > $prefixEnd) {
[$prefixStart, $prefixEnd] = [$prefixEnd, $prefixStart];
}
$mode = strtolower((string)(getSettingValue('weight_barcode_mode', 'weight') ?? 'weight'));
if (!in_array($mode, ['weight', 'price'], true)) $mode = 'weight';
return [
'prefix_start' => $prefixStart,
'prefix_end' => $prefixEnd,
'mode' => $mode,
'value_divisor' => 1000,
];
}
function isWeightBarcode(string $barcode): bool {
$barcode = trim($barcode);
if (!preg_match('/^\d{13}$/', $barcode)) return false;
$config = getWeightBarcodeConfig();
$prefix = (int)substr($barcode, 0, 2);
return $prefix >= $config['prefix_start'] && $prefix <= $config['prefix_end'];
}
function parseWeightBarcode(string $barcode): ?array {
$barcode = trim($barcode);
if (!isWeightBarcode($barcode)) return null;
$config = getWeightBarcodeConfig();
$rawValue = (int)substr($barcode, 7, 5);
return [
'full_barcode' => $barcode,
'prefix' => substr($barcode, 0, 2),
'item_code' => substr($barcode, 2, 5),
'raw_value' => $rawValue,
'value' => $rawValue / (float)$config['value_divisor'],
'mode' => $config['mode'],
'check_digit' => substr($barcode, 12, 1),
];
}
function validateItemSkuBarcode(string $sku): ?string {
$sku = trim($sku);
if ($sku === '') return null;
if (!isWeightBarcode($sku)) return null;
$config = getWeightBarcodeConfig();
return "This barcode is reserved for weighing scale scans. 13-digit barcodes starting with {$config['prefix_start']}-{$config['prefix_end']} cannot be saved as item barcodes; please save the 5-digit scale item code instead.";
}
function redirectWithMessage($msg, $url = null) {
if (!$url) {
$url = $_SERVER['REQUEST_URI'];
}
$_SESSION['message'] = $msg;
header("Location: $url");
exit;
}
// Fetch theme if not in session but user is logged in
if (isset($_SESSION['user_id']) && !isset($_SESSION['theme'])) {
$stmt = db()->prepare("SELECT theme FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$_SESSION['theme'] = $stmt->fetchColumn() ?: 'default';
}
function numberToWordsOMR($number) {
$number = number_format((float)$number, 3, '.', '');
list($rials, $baisas) = explode('.', $number);
$rialsWordsEn = numberToWords((int)$rials);
$baisasWordsEn = numberToWords((int)$baisas);
$enResult = $rialsWordsEn . " Omani Rials";
if ((int)$baisas > 0) {
$enResult .= " and " . $baisasWordsEn . " Baisas";
}
$enResult .= " Only";
$rialsWordsAr = numberToWordsArabic((int)$rials);
$baisasWordsAr = numberToWordsArabic((int)$baisas);
$arResult = $rialsWordsAr . " ريال عماني";
if ((int)$baisas > 0) {
$arResult .= " و " . $baisasWordsAr . " بيسة";
}
$arResult .= " فقط";
return $enResult . " / " . $arResult;
}
function getPromotionalPrice($item) {
$price = (float)$item['sale_price'];
if (isset($item['is_promotion']) && $item['is_promotion']) {
$today = date('Y-m-d');
$start = !empty($item['promotion_start']) ? $item['promotion_start'] : null;
$end = !empty($item['promotion_end']) ? $item['promotion_end'] : null;
$active = true;
if ($start && $today < $start) $active = false;
if ($end && $today > $end) $active = false;
if ($active) {
$price = $price * (1 - (float)$item['promotion_percent'] / 100);
}
}
return $price;
}
// --- Inventory & Core Handlers ---
if (isset($_POST['add_item'])) {
$name_en = $_POST['name_en'] ?? '';
$name_ar = $_POST['name_ar'] ?? '';
$category_id = (int)$_POST['category_id'] ?: null;
$unit_id = (int)$_POST['unit_id'] ?: null;
$supplier_id = (int)$_POST['supplier_id'] ?: null;
$sku = trim((string)($_POST['sku'] ?? ''));
if ($sku_error = validateItemSkuBarcode($sku)) {
redirectWithMessage($sku_error, 'index.php?page=items');
}
$sale_price = (float)($_POST['sale_price'] ?? 0);
$purchase_price = (float)($_POST['purchase_price'] ?? 0);
$stock_quantity = (float)($_POST['stock_quantity'] ?? 0);
$min_stock_level = (float)($_POST['min_stock_level'] ?? 0);
$vat_rate = (float)($_POST['vat_rate'] ?? 0);
$expiry_date = !empty($_POST['expiry_date']) ? $_POST['expiry_date'] : null;
$is_promotion = isset($_POST['is_promotion']) ? 1 : 0;
$promotion_start = !empty($_POST['promotion_start']) ? $_POST['promotion_start'] : null;
$promotion_end = !empty($_POST['promotion_end']) ? $_POST['promotion_end'] : null;
$promotion_percent = (float)($_POST['promotion_percent'] ?? 0);
$image_path = null;
if (isset($_FILES['image']) && $_FILES['image']['error'] === 0) {
$ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
$filename = 'uploads/item_' . time() . '.' . $ext;
if (!is_dir('uploads')) mkdir('uploads', 0777, true);
if (move_uploaded_file($_FILES['image']['tmp_name'], $filename)) $image_path = $filename;
}
$current_oid = current_outlet_id();
$stmt = db()->prepare("INSERT INTO stock_items (outlet_id, name_en, name_ar, category_id, unit_id, supplier_id, sku, sale_price, purchase_price, stock_quantity, min_stock_level, image_path, vat_rate, expiry_date, is_promotion, promotion_start, promotion_end, promotion_percent) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$current_oid, $name_en, $name_ar, $category_id, $unit_id, $supplier_id, $sku, $sale_price, $purchase_price, $stock_quantity, $min_stock_level, $image_path, $vat_rate, $expiry_date, $is_promotion, $promotion_start, $promotion_end, $promotion_percent]);
$new_item_id = db()->lastInsertId();
redirectWithMessage("Item added successfully!");
}
if (isset($_POST['edit_item'])) {
$id = (int)$_POST['id'];
$name_en = $_POST['name_en'] ?? '';
$name_ar = $_POST['name_ar'] ?? '';
$category_id = (int)$_POST['category_id'] ?: null;
$unit_id = (int)$_POST['unit_id'] ?: null;
$supplier_id = (int)$_POST['supplier_id'] ?: null;
$sku = trim((string)($_POST['sku'] ?? ''));
if ($sku_error = validateItemSkuBarcode($sku)) {
redirectWithMessage($sku_error, 'index.php?page=items');
}
$sale_price = (float)($_POST['sale_price'] ?? 0);
$purchase_price = (float)($_POST['purchase_price'] ?? 0);
$stock_quantity = (float)($_POST['stock_quantity'] ?? 0);
$min_stock_level = (float)($_POST['min_stock_level'] ?? 0);
$vat_rate = (float)($_POST['vat_rate'] ?? 0);
$expiry_date = !empty($_POST['expiry_date']) ? $_POST['expiry_date'] : null;
$is_promotion = isset($_POST['is_promotion']) ? 1 : 0;
$promotion_start = !empty($_POST['promotion_start']) ? $_POST['promotion_start'] : null;
$promotion_end = !empty($_POST['promotion_end']) ? $_POST['promotion_end'] : null;
$promotion_percent = (float)($_POST['promotion_percent'] ?? 0);
// Update stock_items
$stmt = db()->prepare("UPDATE stock_items SET name_en = ?, name_ar = ?, category_id = ?, unit_id = ?, supplier_id = ?, sku = ?, sale_price = ?, purchase_price = ?, stock_quantity = ?, min_stock_level = ?, vat_rate = ?, expiry_date = ?, is_promotion = ?, promotion_start = ?, promotion_end = ?, promotion_percent = ? WHERE id = ?");
$stmt->execute([$name_en, $name_ar, $category_id, $unit_id, $supplier_id, $sku, $sale_price, $purchase_price, $stock_quantity, $min_stock_level, $vat_rate, $expiry_date, $is_promotion, $promotion_start, $promotion_end, $promotion_percent, $id]);
if (isset($_FILES['image']) && $_FILES['image']['error'] === 0) {
$ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
$filename = 'uploads/item_' . $id . '_' . time() . '.' . $ext;
if (move_uploaded_file($_FILES['image']['tmp_name'], $filename)) db()->prepare("UPDATE stock_items SET image_path = ? WHERE id = ?")->execute([$filename, $id]);
}
redirectWithMessage("Item updated successfully!");
}
if (isset($_POST['delete_item'])) {
db()->prepare("DELETE FROM stock_items WHERE id = ?")->execute([(int)$_POST['id']]);
redirectWithMessage("Item deleted successfully!");
}
if (isset($_POST['cancel_all_promotions'])) {
db()->query("UPDATE stock_items SET is_promotion = 0 WHERE is_promotion = 1");
redirectWithMessage("All active promotions have been cancelled.");
}
// Auto-expire finished promotions
db()->query("UPDATE stock_items SET is_promotion = 0 WHERE is_promotion = 1 AND promotion_end IS NOT NULL AND promotion_end < '" . date('Y-m-d') . "'");
if (isset($_POST['add_category'])) {
$current_oid = current_outlet_id();
db()->prepare("INSERT INTO stock_categories (name_en, name_ar, outlet_id) VALUES (?, ?, ?)")
->execute([$_POST['name_en'] ?? '', $_POST['name_ar'] ?? '', $current_oid]);
redirectWithMessage("Category added!");
}
if (isset($_POST['add_unit'])) {
$current_oid = current_outlet_id();
$unitNameEn = trim((string)($_POST['name_en'] ?? ''));
$unitNameAr = trim((string)($_POST['name_ar'] ?? ''));
if ($unitNameAr === '') $unitNameAr = $unitNameEn;
db()->prepare("INSERT INTO stock_units (name_en, name_ar, short_name_en, short_name_ar, outlet_id) VALUES (?, ?, ?, ?, ?)")
->execute([$unitNameEn, $unitNameAr, $unitNameEn, $unitNameAr, $current_oid]);
redirectWithMessage("Unit added!");
}
if (isset($_POST['edit_category'])) {
db()->prepare("UPDATE stock_categories SET name_en = ?, name_ar = ? WHERE id = ?")->execute([$_POST['name_en'] ?? '', $_POST['name_ar'] ?? '', (int)$_POST['id']]);
redirectWithMessage("Category updated!");
}
if (isset($_POST['delete_category'])) {
db()->prepare("DELETE FROM stock_categories WHERE id = ?")->execute([(int)$_POST['id']]);
redirectWithMessage("Category deleted!");
}
if (isset($_POST['edit_unit'])) {
$unitNameEn = trim((string)($_POST['name_en'] ?? ''));
$unitNameAr = trim((string)($_POST['name_ar'] ?? ''));
if ($unitNameAr === '') $unitNameAr = $unitNameEn;
db()->prepare("UPDATE stock_units SET name_en = ?, name_ar = ?, short_name_en = ?, short_name_ar = ? WHERE id = ?")->execute([$unitNameEn, $unitNameAr, $unitNameEn, $unitNameAr, (int)$_POST['id']]);
redirectWithMessage("Unit updated!");
}
if (isset($_POST['delete_unit'])) {
db()->prepare("DELETE FROM stock_units WHERE id = ?")->execute([(int)$_POST['id']]);
redirectWithMessage("Unit deleted!");
}
if (isset($_POST['add_customer'])) {
$table = ($_POST['type'] ?? '') === 'supplier' ? 'suppliers' : 'customers';
$taxColumn = entity_tax_column($table);
$columns = ['name', 'email', 'phone'];
$placeholders = ['?', '?', '?'];
$params = [$_POST['name'] ?? '', $_POST['email'] ?? '', $_POST['phone'] ?? ''];
if ($taxColumn !== null) {
$columns[] = $taxColumn;
$placeholders[] = '?';
$params[] = $_POST['tax_id'] ?? '';
}
$columns[] = 'balance';
$placeholders[] = '?';
$params[] = (float)($_POST['balance'] ?? 0);
if ($table === 'customers') {
$columns[] = 'loyalty_points';
$placeholders[] = '?';
$params[] = 0;
} else {
$columns[] = 'outlet_id';
$placeholders[] = '?';
$params[] = current_outlet_id();
}
$sql = "INSERT INTO $table (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")";
db()->prepare($sql)->execute($params);
redirectWithMessage("Entity added!");
}
if (isset($_POST['edit_customer'])) {
$table = ($_POST['type'] ?? '') === 'supplier' ? 'suppliers' : 'customers';
$taxColumn = entity_tax_column($table);
$assignments = ['name = ?', 'email = ?', 'phone = ?'];
$params = [$_POST['name'] ?? '', $_POST['email'] ?? '', $_POST['phone'] ?? ''];
if ($taxColumn !== null) {
$assignments[] = $taxColumn . ' = ?';
$params[] = $_POST['tax_id'] ?? '';
}
$assignments[] = 'balance = ?';
$params[] = (float)($_POST['balance'] ?? 0);
$params[] = (int)$_POST['id'];
$sql = "UPDATE $table SET " . implode(', ', $assignments) . " WHERE id = ?";
db()->prepare($sql)->execute($params);
redirectWithMessage("Entity updated!");
}
if (isset($_POST['delete_customer'])) {
$table = ($_POST['type'] ?? '') === 'supplier' ? 'suppliers' : 'customers';
db()->prepare("DELETE FROM $table WHERE id = ?")->execute([(int)$_POST['id']]);
redirectWithMessage("Entity deleted!");
}
// Invoices
if (isset($_POST['add_invoice'])) {
$db = db();
try {
$db->beginTransaction();
$type = $_POST['type'] ?? 'sale';
$table = ($type === 'purchase') ? 'purchases' : 'invoices';
$item_table = ($type === 'purchase') ? 'purchase_items' : 'invoice_items';
$cust_supplier_col = ($type === 'purchase') ? 'supplier_id' : 'customer_id';
$fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
$cust_id = (int)$_POST['customer_id'];
$inv_date = $_POST['invoice_date'] ?: date('Y-m-d');
$due_date = $_POST['due_date'] ?: null;
$status = $_POST['status'] ?? 'pending';
$pay_type = $_POST['payment_type'] ?? 'cash';
$items = $_POST['item_ids'] ?? [];
if (empty($items)) {
throw new Exception("Please add at least one item.");
}
$qtys = $_POST['quantities'] ?? [];
$prices = $_POST['prices'] ?? [];
$total_subtotal = 0;
$total_vat = 0;
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$total_subtotal += $subtotal;
$total_vat += $vatAmount;
}
$total_with_vat = $total_subtotal + $total_vat;
$paid = (float)($_POST['paid_amount'] ?? 0);
if ($status === 'paid') $paid = $total_with_vat;
$stmt = $db->prepare("INSERT INTO $table ($cust_supplier_col, invoice_date, due_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$cust_id, $inv_date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid]);
$inv_id = $db->lastInsertId();
$items_for_journal = [];
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$db->prepare("INSERT INTO $item_table ($fk_col, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$inv_id, $item_id, $qty, $price, $vatAmount, $subtotal]);
// Update stock
$change = ($type === 'sale') ? -$qty : $qty;
update_stock($item_id, $change);
$items_for_journal[] = ['id' => $item_id, 'qty' => $qty];
}
// Accounting
if ($type === 'sale') {
recordSaleJournal($inv_id, $total_with_vat, $inv_date, $items_for_journal, $total_vat);
} else {
// For purchases, you might have recordPurchaseJournal, but let's check if it exists
if (function_exists('recordPurchaseJournal')) {
recordPurchaseJournal($inv_id, $total_with_vat, $inv_date, $items_for_journal, $total_vat);
}
}
$db->commit();
$msg = ($type === 'purchase' ? "Purchase" : "Invoice") . " #$inv_id created!";
redirectWithMessage($msg, "index.php?page=" . ($type === 'purchase' ? 'purchases' : 'sales'));
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST["add_quotation"])) {
$db = db();
try {
$db->beginTransaction();
$cust_id = (int)$_POST["customer_id"];
$quot_date = $_POST["quotation_date"] ?: date("Y-m-d");
$valid_until = $_POST["valid_until"] ?: null;
$status = $_POST["status"] ?? "pending";
$items = $_POST["item_ids"] ?? [];
$qtys = $_POST["quantities"] ?? [];
$prices = $_POST["prices"] ?? [];
$total_subtotal = 0;
$total_vat = 0;
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$total_subtotal += $subtotal;
$total_vat += $vatAmount;
}
$total_with_vat = $total_subtotal + $total_vat;
[$quotationInsertSql, $quotationInsertValues] = db_insert_sql_for_existing_columns('quotations', [
'customer_id' => $cust_id,
'quotation_date' => $quot_date,
'valid_until' => $valid_until,
'status' => $status,
'total_amount' => $total_subtotal,
'vat_amount' => $total_vat,
'total_with_vat' => $total_with_vat,
'outlet_id' => current_outlet_id(),
]);
$stmt = $db->prepare($quotationInsertSql);
$stmt->execute($quotationInsertValues);
$quot_id = $db->lastInsertId();
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)")->execute([$quot_id, $item_id, $qty, $price, $subtotal]);
}
$db->commit();
$msg = "Quotation #$quot_id created!";
redirectWithMessage($msg, "index.php?page=quotations");
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST['edit_quotation'])) {
$db = db();
try {
$db->beginTransaction();
$quot_id = (int)$_POST['quotation_id'];
$cust_id = (int)$_POST['customer_id'];
$quot_date = $_POST['quotation_date'];
$valid_until = $_POST['valid_until'] ?: null;
$status = $_POST['status'] ?? 'pending';
$items = $_POST['item_ids'] ?? [];
$qtys = $_POST['quantities'] ?? [];
$prices = $_POST['prices'] ?? [];
$total_subtotal = 0;
$total_vat = 0;
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$total_subtotal += $subtotal;
$total_vat += $vatAmount;
}
$total_with_vat = $total_subtotal + $total_vat;
$stmt = $db->prepare("UPDATE quotations SET customer_id = ?, quotation_date = ?, valid_until = ?, status = ?, total_amount = ?, vat_amount = ?, total_with_vat = ? WHERE id = ?");
$stmt->execute([$cust_id, $quot_date, $valid_until, $status, $total_subtotal, $total_vat, $total_with_vat, $quot_id]);
// Delete old items
$db->prepare("DELETE FROM quotation_items WHERE quotation_id = ?")->execute([$quot_id]);
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$db->prepare("INSERT INTO quotation_items (quotation_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)")->execute([$quot_id, $item_id, $qty, $price, $subtotal]);
}
$db->commit();
$msg = "Quotation #$quot_id updated!";
redirectWithMessage($msg, "index.php?page=quotations");
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST['delete_quotation'])) {
$id = (int)$_POST['id'];
db()->prepare("DELETE FROM quotations WHERE id = ?")->execute([$id]);
redirectWithMessage("Quotation deleted!", "index.php?page=quotations");
}
if (isset($_POST['add_lpo'])) {
$db = db();
try {
$db->beginTransaction();
$supp_id = (int)$_POST['supplier_id'];
$lpo_date = $_POST['lpo_date'] ?: date('Y-m-d');
$delivery_date = $_POST['delivery_date'] ?: null;
$status = 'pending';
$terms = $_POST['terms_conditions'] ?? '';
$items = $_POST['item_ids'] ?? [];
$qtys = $_POST['quantities'] ?? [];
$prices = $_POST['prices'] ?? [];
$total_subtotal = 0;
$total_vat = 0;
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$total_subtotal += $subtotal;
$total_vat += $vatAmount;
}
$total_with_vat = $total_subtotal + $total_vat;
[$lpoInsertSql, $lpoInsertValues] = db_insert_sql_for_existing_columns('lpos', [
'supplier_id' => $supp_id,
'lpo_date' => $lpo_date,
'delivery_date' => $delivery_date,
'status' => 'pending',
'total_amount' => $total_subtotal,
'vat_amount' => $total_vat,
'total_with_vat' => $total_with_vat,
'terms_conditions' => $terms,
'outlet_id' => current_outlet_id(),
]);
$stmt = $db->prepare($lpoInsertSql);
$stmt->execute($lpoInsertValues);
$lpo_id = $db->lastInsertId();
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$db->prepare("INSERT INTO lpo_items (lpo_id, item_id, quantity, unit_price, vat_percentage, vat_amount, total_amount) VALUES (?, ?, ?, ?, ?, ?, ?)")->execute([$lpo_id, $item_id, $qty, $price, $vatRate, $vatAmount, $subtotal]);
}
$db->commit();
redirectWithMessage("LPO #$lpo_id created!", "index.php?page=lpos");
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST['edit_lpo'])) {
$db = db();
try {
$db->beginTransaction();
$lpo_id = (int)$_POST['lpo_id'];
$supp_id = (int)$_POST['supplier_id'];
$lpo_date = $_POST['lpo_date'];
$delivery_date = $_POST['delivery_date'] ?: null;
$status = $_POST['status'] ?? 'pending';
$terms = $_POST['terms_conditions'] ?? '';
$items = $_POST['item_ids'] ?? [];
$qtys = $_POST['quantities'] ?? [];
$prices = $_POST['prices'] ?? [];
$total_subtotal = 0;
$total_vat = 0;
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$total_subtotal += $subtotal;
$total_vat += $vatAmount;
}
$total_with_vat = $total_subtotal + $total_vat;
$stmt = $db->prepare("UPDATE lpos SET supplier_id = ?, lpo_date = ?, delivery_date = ?, status = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, terms_conditions = ? WHERE id = ?");
$stmt->execute([$supp_id, $lpo_date, $delivery_date, $status, $total_subtotal, $total_vat, $total_with_vat, $terms, $lpo_id]);
$db->prepare("DELETE FROM lpo_items WHERE lpo_id = ?")->execute([$lpo_id]);
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$db->prepare("INSERT INTO lpo_items (lpo_id, item_id, quantity, unit_price, vat_percentage, vat_amount, total_amount) VALUES (?, ?, ?, ?, ?, ?, ?)")->execute([$lpo_id, $item_id, $qty, $price, $vatRate, $vatAmount, $subtotal]);
}
$db->commit();
redirectWithMessage("LPO #$lpo_id updated!", "index.php?page=lpos");
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST['delete_lpo'])) {
$id = (int)$_POST['id'];
db()->prepare("DELETE FROM lpos WHERE id = ?")->execute([$id]);
redirectWithMessage("LPO deleted!", "index.php?page=lpos");
}
if (isset($_POST['convert_to_invoice'])) {
$db = db();
try {
$db->beginTransaction();
$quot_id = (int)$_POST['quotation_id'];
$stmt = $db->prepare("SELECT * FROM quotations WHERE id = ?");
$stmt->execute([$quot_id]);
$quot = $stmt->fetch();
if (!$quot) throw new Exception("Quotation not found.");
if ($quot['status'] === 'converted') throw new Exception("Quotation already converted.");
$stmtItems = $db->prepare("SELECT * FROM quotation_items WHERE quotation_id = ?");
$stmtItems->execute([$quot_id]);
$qItems = $stmtItems->fetchAll();
// Create Invoice
$inv_date = date('Y-m-d');
$stmtInv = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, outlet_id) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0, ?)");
$stmtInv->execute([$quot['customer_id'], $inv_date, $quot['total_amount'], $quot['vat_amount'], $quot['total_with_vat'], current_outlet_id()]);
$inv_id = $db->lastInsertId();
$items_for_journal = [];
foreach ($qItems as $item) {
$lineVatAmount = line_item_vat_amount($db, $item);
$db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$inv_id, $item['item_id'], $item['quantity'], $item['unit_price'], $lineVatAmount, $item['total_price']]);
// Update stock
update_stock($item['item_id'], -$item['quantity']);
$items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']];
}
// Update Quotation status
$db->prepare("UPDATE quotations SET status = 'converted' WHERE id = ?")->execute([$quot_id]);
// Accounting
recordSaleJournal($inv_id, $quot['total_with_vat'], $inv_date, $items_for_journal, $quot['vat_amount']);
$db->commit();
redirectWithMessage("Quotation converted to Invoice #$inv_id successfully!", "index.php?page=sales");
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST['convert_lpo_to_purchase'])) {
$db = db();
try {
$db->beginTransaction();
$lpo_id = (int)$_POST['lpo_id'];
$stmt = $db->prepare("SELECT * FROM lpos WHERE id = ?");
$stmt->execute([$lpo_id]);
$lpo = $stmt->fetch();
if (!$lpo) throw new Exception("LPO not found.");
if ($lpo['status'] === 'converted') throw new Exception("LPO already converted.");
$stmtItems = $db->prepare("SELECT * FROM lpo_items WHERE lpo_id = ?");
$stmtItems->execute([$lpo_id]);
$lItems = $stmtItems->fetchAll();
// Create Purchase Invoice
$pur_date = date('Y-m-d');
$stmtPur = $db->prepare("INSERT INTO purchases (supplier_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, outlet_id) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0, ?)");
$stmtPur->execute([$lpo['supplier_id'], $pur_date, $lpo['total_amount'], $lpo['vat_amount'], $lpo['total_with_vat'], current_outlet_id()]);
$pur_id = $db->lastInsertId();
$items_for_journal = [];
foreach ($lItems as $item) {
$db->prepare("INSERT INTO purchase_items (purchase_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$pur_id, $item['item_id'], $item['quantity'], $item['unit_price'], $item['vat_amount'], $item['total_amount']]);
// Update stock
update_stock($item['item_id'], $item['quantity']);
$items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']];
}
// Update LPO status
$db->prepare("UPDATE lpos SET status = 'converted' WHERE id = ?")->execute([$lpo_id]);
$db->commit();
redirectWithMessage("LPO converted to Purchase Invoice #$pur_id successfully!", "index.php?page=purchases");
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST['record_payment'])) {
$id = (int)$_POST['invoice_id'];
$amount = (float)$_POST['amount'];
$date = $_POST['payment_date'] ?: date('Y-m-d');
$method = $_POST['payment_method'] ?? 'Cash';
$type = ($page === 'purchases') ? 'purchase' : 'sale';
$table = ($type === 'purchase') ? 'purchases' : 'invoices';
$payment_table = ($type === 'purchase') ? 'purchase_payments' : 'payments';
$fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
$db = db();
$db->prepare("INSERT INTO $payment_table ($fk_col, amount, payment_date, payment_method, notes) VALUES (?, ?, ?, ?, ?)")->execute([$id, $amount, $date, $method, $_POST['notes'] ?? '']);
$pay_id = $db->lastInsertId();
$db->prepare("UPDATE $table SET paid_amount = paid_amount + ?, status = IF(paid_amount + ? >= total_with_vat, 'paid', 'partially_paid') WHERE id = ?")->execute([$amount, $amount, $id]);
if ($type === 'sale') recordPaymentReceivedJournal((int)$pay_id, $amount, $date, $method);
else recordPaymentMadeJournal((int)$pay_id, $amount, $date, $method);
$_SESSION['trigger_receipt_modal'] = true;
$_SESSION['show_receipt_id'] = $pay_id;
redirectWithMessage("Payment recorded!", "index.php?page=" . ($type === 'purchase' ? 'purchases' : 'sales'));
}
if (isset($_POST['add_expense'])) {
$amt = (float)$_POST['amount'];
$date = $_POST['expense_date'] ?: date('Y-m-d');
$desc = $_POST['description'] ?? '';
db()->prepare("INSERT INTO expenses (category_id, amount, expense_date, reference_no, description) VALUES (?, ?, ?, ?, ?)")->execute([(int)$_POST['category_id'], $amt, $date, $_POST['reference_no'] ?? '', $desc]);
recordExpenseJournal(db()->lastInsertId(), $amt, $date, $desc);
redirectWithMessage("Expense recorded!", "index.php?page=expenses");
}
# --- Unified Import Logic (Excel & CSV) ---
# --- Unified Import Logic (Excel & CSV) ---
if (isset($_POST['import_items'])) {
error_log("Import items triggered.");
$count = 0;
$skipped = 0;
if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) {
$tmpPath = $_FILES['excel_file']['tmp_name'];
$rows = [];
if ( $xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath) ) {
$rows = $xlsx->rows();
} else {
$handle = fopen($tmpPath, "r");
$firstLine = fgets($handle); rewind($handle);
$sep = (substr_count($firstLine, ';') > substr_count($firstLine, ',')) ? ';' : ',';
$bom = fread($handle, 3); if ($bom !== "") rewind($handle);
while (($data = fgetcsv($handle, 0, $sep)) !== FALSE) $rows[] = $data;
fclose($handle);
}
if (isset($rows[0][0]) && stripos($rows[0][0], 'sku') !== false) array_shift($rows);
$current_oid = current_outlet_id();
foreach ($rows as $row) {
if (empty($row[0])) continue;
$sku = trim((string)$row[0]);
if ($sku_error = validateItemSkuBarcode($sku)) {
$skipped++;
continue;
}
$name_en = trim((string)($row[1] ?? ''));
$name_ar = trim((string)($row[2] ?? ''));
$sale_price = (float)($row[3] ?? 0);
$purchase_price = (float)($row[4] ?? 0);
$qty = (float)($row[5] ?? 0);
$vat_rate = (float)($row[6] ?? 0);
$check = db()->prepare("SELECT id FROM stock_items WHERE sku = ? AND outlet_id = ?");
$check->execute([$sku, $current_oid]);
$exists = $check->fetch(PDO::FETCH_ASSOC);
if ($exists) {
$item_id = $exists['id'];
// Update Item (including stock_quantity)
db()->prepare("UPDATE stock_items SET name_en = ?, name_ar = ?, sale_price = ?, purchase_price = ?, vat_rate = ?, stock_quantity = ? WHERE id = ?")
->execute([$name_en, $name_ar, $sale_price, $purchase_price, $vat_rate, $qty, $item_id]);
} else {
// Insert Item
db()->prepare("INSERT INTO stock_items (outlet_id, sku, name_en, name_ar, sale_price, purchase_price, stock_quantity, vat_rate) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
->execute([$current_oid, $sku, $name_en, $name_ar, $sale_price, $purchase_price, $qty, $vat_rate]);
}
$count++;
}
$weightConfig = getWeightBarcodeConfig();
$summary = "Import items completed! $count processed.";
if ($skipped > 0) {
$summary .= " $skipped skipped because 13-digit barcodes starting with {$weightConfig['prefix_start']}-{$weightConfig['prefix_end']} are reserved for weighing scale barcodes.";
}
redirectWithMessage($summary, "index.php?page=items");
}
}
if (isset($_POST['import_customers']) || isset($_POST['import_suppliers'])) {
$type = isset($_POST['import_customers']) ? 'customers' : 'suppliers';
$table = $type;
error_log("Import $type triggered.");
$count = 0;
if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) {
$tmpPath = $_FILES['excel_file']['tmp_name'];
$rows = [];
if ( $xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath) ) {
$rows = $xlsx->rows();
} else {
$handle = fopen($tmpPath, "r");
$firstLine = fgets($handle); rewind($handle);
$sep = (substr_count($firstLine, ';') > substr_count($firstLine, ',')) ? ';' : ',';
$bom = fread($handle, 3); if ($bom !== "") rewind($handle);
while (($data = fgetcsv($handle, 0, $sep)) !== FALSE) $rows[] = $data;
fclose($handle);
}
if (isset($rows[0][0]) && (stripos($rows[0][0], 'name') !== false || stripos($rows[0][0], 'id') !== false)) array_shift($rows);
$taxColumn = entity_tax_column($table);
$importColumns = ['name', 'email', 'phone'];
$importValues = ['?', '?', '?'];
if ($taxColumn !== null) {
$importColumns[] = $taxColumn;
$importValues[] = '?';
}
$importColumns[] = 'created_at';
$importValues[] = 'NOW()';
if ($table === 'suppliers') {
$importColumns[] = 'outlet_id';
$importValues[] = '?';
}
$importSql = "INSERT INTO $table (" . implode(', ', $importColumns) . ") VALUES (" . implode(', ', $importValues) . ")";
$importStmt = db()->prepare($importSql);
foreach ($rows as $row) {
if (empty($row[0])) continue;
$name = trim((string)$row[0]);
if (!$name) continue;
$email = trim((string)($row[1] ?? ''));
$phone = trim((string)($row[2] ?? ''));
$tax_id = trim((string)($row[3] ?? ''));
$importParams = [$name, $email, $phone];
if ($taxColumn !== null) {
$importParams[] = $tax_id;
}
if ($table === 'suppliers') {
$importParams[] = current_outlet_id();
}
$importStmt->execute($importParams);
$count++;
}
redirectWithMessage("Import $type completed! $count processed.", "index.php?page=$type");
}
}
if (isset($_POST['import_categories'])) {
$count = 0;
if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) {
$tmpPath = $_FILES['excel_file']['tmp_name'];
$rows = [];
if ( $xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath) ) { $rows = $xlsx->rows(); }
else {
$handle = fopen($tmpPath, "r");
while (($data = fgetcsv($handle)) !== FALSE) $rows[] = $data;
fclose($handle);
}
if (isset($rows[0][0]) && stripos($rows[0][0], 'name') !== false) array_shift($rows);
$current_oid = current_outlet_id();
foreach ($rows as $row) {
if (empty($row[0])) continue;
$name_en = trim((string)$row[0]);
$name_ar = trim((string)($row[1] ?? $name_en));
db()->prepare("INSERT INTO stock_categories (name_en, name_ar, outlet_id) VALUES (?, ?, ?)")
->execute([$name_en, $name_ar, $current_oid]);
$count++;
}
redirectWithMessage("Import categories completed! $count processed.", "index.php?page=categories");
}
}
if (isset($_POST['import_units'])) {
$count = 0;
if (isset($_FILES['excel_file']) && $_FILES['excel_file']['error'] === 0) {
$tmpPath = $_FILES['excel_file']['tmp_name'];
$rows = [];
if ( $xlsx = \Shuchkin\SimpleXLSX::parse($tmpPath) ) { $rows = $xlsx->rows(); }
else {
$handle = fopen($tmpPath, "r");
$firstLine = fgets($handle); rewind($handle);
$sep = (substr_count($firstLine, ';') > substr_count($firstLine, ',')) ? ';' : ',';
$bom = fread($handle, 3); if ($bom !== "") rewind($handle);
while (($data = fgetcsv($handle, 0, $sep)) !== FALSE) $rows[] = $data;
fclose($handle);
}
if (isset($rows[0][0]) && (stripos($rows[0][0], 'name') !== false || stripos($rows[0][0], 'id') !== false)) array_shift($rows);
foreach ($rows as $row) {
if (empty($row[0])) continue;
$name_en = trim((string)$row[0]);
$name_ar = trim((string)($row[1] ?? ''));
if ($name_ar === '') $name_ar = $name_en;
db()->prepare("INSERT INTO stock_units (name_en, name_ar, short_name_en, short_name_ar, outlet_id) VALUES (?, ?, ?, ?, ?)")
->execute([$name_en, $name_ar, $name_en, $name_ar, current_outlet_id()]);
$count++;
}
redirectWithMessage("Import units completed! $count processed.", "index.php?page=units");
}
}
if (isset($_POST['add_expense_category'])) {
$name_en = $_POST['name_en'] ?? '';
$name_ar = $_POST['name_ar'] ?? '';
db()->prepare("INSERT INTO expense_categories (name_en, name_ar) VALUES (?, ?)")->execute([$name_en, $name_ar]);
redirectWithMessage("Expense category added!", "index.php?page=expense_categories");
}
if (isset($_POST['add_payment_method'])) {
$name_en = trim((string)($_POST['name_en'] ?? ''));
$name_ar = trim((string)($_POST['name_ar'] ?? ''));
if ($name_en === '' && $name_ar === '') {
redirectWithMessage("Please enter a payment method name.", "index.php?page=payment_methods");
}
if ($name_en === '') {
$name_en = $name_ar;
}
if ($name_ar === '') {
$name_ar = $name_en;
}
if (db_column_exists('payment_methods', 'name_en') || db_column_exists('payment_methods', 'name_ar')) {
$columns = [];
$placeholders = [];
$values = [];
if (db_column_exists('payment_methods', 'name_en')) {
$columns[] = 'name_en';
$placeholders[] = '?';
$values[] = $name_en;
}
if (db_column_exists('payment_methods', 'name_ar')) {
$columns[] = 'name_ar';
$placeholders[] = '?';
$values[] = $name_ar;
}
db()->prepare("INSERT INTO payment_methods (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")")->execute($values);
} elseif (db_column_exists('payment_methods', 'name')) {
db()->prepare("INSERT INTO payment_methods (`name`) VALUES (?)")->execute([$name_en]);
} else {
throw new RuntimeException('payment_methods table is missing a usable name column.');
}
redirectWithMessage("Payment method added!", "index.php?page=payment_methods");
}
if (isset($_POST['edit_payment_method'])) {
$id = (int)($_POST['id'] ?? 0);
$name_en = trim((string)($_POST['name_en'] ?? ''));
$name_ar = trim((string)($_POST['name_ar'] ?? ''));
if ($id <= 0) {
redirectWithMessage("Invalid payment method.", "index.php?page=payment_methods");
}
if ($name_en === '' && $name_ar === '') {
redirectWithMessage("Please enter a payment method name.", "index.php?page=payment_methods");
}
if ($name_en === '') {
$name_en = $name_ar;
}
if ($name_ar === '') {
$name_ar = $name_en;
}
if (db_column_exists('payment_methods', 'name_en') || db_column_exists('payment_methods', 'name_ar')) {
$sets = [];
$values = [];
if (db_column_exists('payment_methods', 'name_en')) {
$sets[] = 'name_en = ?';
$values[] = $name_en;
}
if (db_column_exists('payment_methods', 'name_ar')) {
$sets[] = 'name_ar = ?';
$values[] = $name_ar;
}
$values[] = $id;
db()->prepare("UPDATE payment_methods SET " . implode(', ', $sets) . " WHERE id = ?")->execute($values);
} elseif (db_column_exists('payment_methods', 'name')) {
db()->prepare("UPDATE payment_methods SET `name` = ? WHERE id = ?")->execute([$name_en, $id]);
} else {
throw new RuntimeException('payment_methods table is missing a usable name column.');
}
redirectWithMessage("Payment method updated!", "index.php?page=payment_methods");
}
if (isset($_POST['delete_payment_method'])) {
$id = (int)($_POST['id'] ?? 0);
if ($id > 0) {
db()->prepare("DELETE FROM payment_methods WHERE id = ?")->execute([$id]);
}
redirectWithMessage("Payment method deleted!", "index.php?page=payment_methods");
}
if (isset($_POST['delete_invoice'])) {
$id = (int)$_POST['id'];
$type = ($page === 'purchases') ? 'purchase' : 'sale';
$table = ($type === 'purchase') ? 'purchases' : 'invoices';
$item_table = ($type === 'purchase') ? 'purchase_items' : 'invoice_items';
$fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
db()->prepare("DELETE FROM $table WHERE id = ?")->execute([$id]);
db()->prepare("DELETE FROM $item_table WHERE $fk_col = ?")->execute([$id]);
redirectWithMessage(($type === 'purchase' ? "Purchase" : "Invoice") . " deleted!", "index.php?page=" . ($type === 'purchase' ? 'purchases' : 'sales'));
}
if (isset($_POST['delete_quotation'])) {
$id = (int)$_POST['id'];
db()->prepare("DELETE FROM quotations WHERE id = ?")->execute([$id]);
$message = "Quotation deleted!";
}
if (isset($_POST['add_lpo'])) {
$db = db();
try {
$db->beginTransaction();
$supp_id = (int)$_POST['supplier_id'];
$lpo_date = $_POST['lpo_date'] ?: date('Y-m-d');
$delivery_date = $_POST['delivery_date'] ?: null;
$terms = $_POST['terms_conditions'] ?? '';
$items = $_POST['item_ids'] ?? [];
if (empty($items)) {
throw new Exception("Please add at least one item.");
}
$qtys = $_POST['quantities'] ?? [];
$prices = $_POST['prices'] ?? [];
$total_subtotal = 0;
$total_vat = 0;
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$total_subtotal += $subtotal;
$total_vat += $vatAmount;
}
$total_with_vat = $total_subtotal + $total_vat;
[$lpoInsertSql, $lpoInsertValues] = db_insert_sql_for_existing_columns('lpos', [
'supplier_id' => $supp_id,
'lpo_date' => $lpo_date,
'delivery_date' => $delivery_date,
'status' => 'pending',
'total_amount' => $total_subtotal,
'vat_amount' => $total_vat,
'total_with_vat' => $total_with_vat,
'terms_conditions' => $terms,
'outlet_id' => current_outlet_id(),
]);
$stmt = $db->prepare($lpoInsertSql);
$stmt->execute($lpoInsertValues);
$lpo_id = $db->lastInsertId();
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$db->prepare("INSERT INTO lpo_items (lpo_id, item_id, quantity, unit_price, vat_percentage, vat_amount, total_amount) VALUES (?, ?, ?, ?, ?, ?, ?)")->execute([$lpo_id, $item_id, $qty, $price, $vatRate, $vatAmount, $subtotal]);
}
$db->commit();
redirectWithMessage("LPO #$lpo_id created!", "index.php?page=lpos");
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST['edit_lpo'])) {
$db = db();
try {
$db->beginTransaction();
$lpo_id = (int)$_POST['lpo_id'];
$supp_id = (int)$_POST['supplier_id'];
$lpo_date = $_POST['lpo_date'];
$delivery_date = $_POST['delivery_date'] ?: null;
$status = $_POST['status'] ?? 'pending';
$terms = $_POST['terms_conditions'] ?? '';
$items = $_POST['item_ids'] ?? [];
if (empty($items)) {
throw new Exception("Please add at least one item.");
}
$qtys = $_POST['quantities'] ?? [];
$prices = $_POST['prices'] ?? [];
$total_subtotal = 0;
$total_vat = 0;
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$total_subtotal += $subtotal;
$total_vat += $vatAmount;
}
$total_with_vat = $total_subtotal + $total_vat;
$stmt = $db->prepare("UPDATE lpos SET supplier_id = ?, lpo_date = ?, delivery_date = ?, status = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, terms_conditions = ? WHERE id = ?");
$stmt->execute([$supp_id, $lpo_date, $delivery_date, $status, $total_subtotal, $total_vat, $total_with_vat, $terms, $lpo_id]);
$db->prepare("DELETE FROM lpo_items WHERE lpo_id = ?")->execute([$lpo_id]);
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$db->prepare("INSERT INTO lpo_items (lpo_id, item_id, quantity, unit_price, vat_percentage, vat_amount, total_amount) VALUES (?, ?, ?, ?, ?, ?, ?)")->execute([$lpo_id, $item_id, $qty, $price, $vatRate, $vatAmount, $subtotal]);
}
$db->commit();
$message = "LPO #$lpo_id updated!";
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST['delete_lpo'])) {
$id = (int)$_POST['id'];
db()->prepare("DELETE FROM lpos WHERE id = ?")->execute([$id]);
$message = "LPO deleted!";
}
if (isset($_POST['convert_to_invoice'])) {
$db = db();
try {
$db->beginTransaction();
$quot_id = (int)$_POST['quotation_id'];
$stmt = $db->prepare("SELECT * FROM quotations WHERE id = ?");
$stmt->execute([$quot_id]);
$quot = $stmt->fetch();
if (!$quot) throw new Exception("Quotation not found.");
if ($quot['status'] === 'converted') throw new Exception("Quotation already converted.");
$stmtItems = $db->prepare("SELECT * FROM quotation_items WHERE quotation_id = ?");
$stmtItems->execute([$quot_id]);
$qItems = $stmtItems->fetchAll();
// Create Invoice
$inv_date = date('Y-m-d');
$stmtInv = $db->prepare("INSERT INTO invoices (customer_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, outlet_id) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0, ?)");
$stmtInv->execute([$quot['customer_id'], $inv_date, $quot['total_amount'], $quot['vat_amount'], $quot['total_with_vat'], current_outlet_id()]);
$inv_id = $db->lastInsertId();
$items_for_journal = [];
foreach ($qItems as $item) {
$lineVatAmount = line_item_vat_amount($db, $item);
$db->prepare("INSERT INTO invoice_items (invoice_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$inv_id, $item['item_id'], $item['quantity'], $item['unit_price'], $lineVatAmount, $item['total_price']]);
// Update stock
update_stock($item['item_id'], -$item['quantity']);
$items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']];
}
// Update Quotation status
$db->prepare("UPDATE quotations SET status = 'converted' WHERE id = ?")->execute([$quot_id]);
// Accounting
recordSaleJournal($inv_id, $quot['total_with_vat'], $inv_date, $items_for_journal, $quot['vat_amount']);
$db->commit();
$message = "Quotation converted to Invoice #$inv_id successfully!";
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST['convert_lpo_to_purchase'])) {
$db = db();
try {
$db->beginTransaction();
$lpo_id = (int)$_POST['lpo_id'];
$stmt = $db->prepare("SELECT * FROM lpos WHERE id = ?");
$stmt->execute([$lpo_id]);
$lpo = $stmt->fetch();
if (!$lpo) throw new Exception("LPO not found.");
if ($lpo['status'] === 'converted') throw new Exception("LPO already converted.");
$stmtItems = $db->prepare("SELECT * FROM lpo_items WHERE lpo_id = ?");
$stmtItems->execute([$lpo_id]);
$lItems = $stmtItems->fetchAll();
// Create Purchase Invoice
$inv_date = date('Y-m-d');
$stmtPur = $db->prepare("INSERT INTO purchases (supplier_id, invoice_date, status, payment_type, total_amount, vat_amount, total_with_vat, paid_amount, outlet_id) VALUES (?, ?, 'unpaid', 'credit', ?, ?, ?, 0, ?)");
$stmtPur->execute([$lpo['supplier_id'], $inv_date, $lpo['total_amount'], $lpo['vat_amount'], $lpo['total_with_vat'], current_outlet_id()]);
$pur_id = $db->lastInsertId();
$items_for_journal = [];
foreach ($lItems as $item) {
$db->prepare("INSERT INTO purchase_items (purchase_id, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$pur_id, $item['item_id'], $item['quantity'], $item['unit_price'], $item['vat_amount'], $item['total_amount']]);
// Update stock
update_stock($item['item_id'], $item['quantity']);
$items_for_journal[] = ['id' => $item['item_id'], 'qty' => $item['quantity']];
}
// Update LPO status
$db->prepare("UPDATE lpos SET status = 'converted' WHERE id = ?")->execute([$lpo_id]);
// Accounting (if exists)
if (function_exists('recordPurchaseJournal')) {
recordPurchaseJournal($pur_id, $lpo['total_with_vat'], $inv_date, $items_for_journal, $lpo['vat_amount']);
}
$db->commit();
$message = "LPO converted to Purchase Invoice #$pur_id successfully!";
header("Location: index.php?page=purchases");
exit;
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
if (isset($_POST['record_payment'])) {
$id = (int)$_POST['invoice_id'];
$amount = (float)$_POST['amount'];
$date = $_POST['payment_date'] ?: date('Y-m-d');
$method = $_POST['payment_method'] ?? 'Cash';
$type = ($page === 'purchases') ? 'purchase' : 'sale';
$table = ($type === 'purchase') ? 'purchases' : 'invoices';
$payment_table = ($type === 'purchase') ? 'purchase_payments' : 'payments';
$fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
$db = db();
$db->prepare("INSERT INTO $payment_table ($fk_col, amount, payment_date, payment_method, notes) VALUES (?, ?, ?, ?, ?)")->execute([$id, $amount, $date, $method, $_POST['notes'] ?? '']);
$pay_id = $db->lastInsertId();
$db->prepare("UPDATE $table SET paid_amount = paid_amount + ?, status = IF(paid_amount + ? >= total_with_vat, 'paid', 'partially_paid') WHERE id = ?")->execute([$amount, $amount, $id]);
if ($type === 'sale') recordPaymentReceivedJournal((int)$pay_id, $amount, $date, $method);
else recordPaymentMadeJournal((int)$pay_id, $amount, $date, $method);
$message = "Payment recorded!";
$_SESSION['trigger_receipt_modal'] = true; $_SESSION['show_receipt_id'] = $pay_id;
}
if (isset($_POST['add_expense'])) {
$amt = (float)$_POST['amount'];
$date = $_POST['expense_date'] ?: date('Y-m-d');
$desc = $_POST['description'] ?? '';
db()->prepare("INSERT INTO expenses (category_id, amount, expense_date, reference_no, description) VALUES (?, ?, ?, ?, ?)")->execute([(int)$_POST['category_id'], $amt, $date, $_POST['reference_no'] ?? '', $desc]);
recordExpenseJournal(db()->lastInsertId(), $amt, $date, $desc);
$message = "Expense recorded!";
}
if (isset($_POST['edit_invoice'])) {
$db = db();
try {
$db->beginTransaction();
$id = (int)$_POST['invoice_id'];
$type = ($page === 'purchases') ? 'purchase' : 'sale';
$table = ($type === 'purchase') ? 'purchases' : 'invoices';
$item_table = ($type === 'purchase') ? 'purchase_items' : 'invoice_items';
$cust_supplier_col = ($type === 'purchase') ? 'supplier_id' : 'customer_id';
$fk_col = ($type === 'purchase') ? 'purchase_id' : 'invoice_id';
$cust_id = (int)$_POST['customer_id'];
$date = $_POST['invoice_date'] ?: date('Y-m-d');
$due_date = $_POST['due_date'] ?: null;
$status = $_POST['status'] ?? 'pending';
$pay_type = $_POST['payment_type'] ?? 'cash';
$items = $_POST['item_ids'] ?? [];
$qtys = $_POST['quantities'] ?? [];
$prices = $_POST['prices'] ?? [];
$total_subtotal = 0;
$total_vat = 0;
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$total_subtotal += $subtotal;
$total_vat += $vatAmount;
}
$total_with_vat = $total_subtotal + $total_vat;
$paid = (float)($_POST['paid_amount'] ?? 0);
if ($status === 'paid') $paid = $total_with_vat;
$db->prepare("UPDATE $table SET $cust_supplier_col = ?, invoice_date = ?, due_date = ?, status = ?, payment_type = ?, total_amount = ?, vat_amount = ?, total_with_vat = ?, paid_amount = ? WHERE id = ?")
->execute([$cust_id, $date, $due_date, $status, $pay_type, $total_subtotal, $total_vat, $total_with_vat, $paid, $id]);
// Revert stock for old items
$stmtOld = $db->prepare("SELECT item_id, quantity FROM $item_table WHERE $fk_col = ?");
$stmtOld->execute([$id]);
$oldItems = $stmtOld->fetchAll();
foreach ($oldItems as $old) {
$change = ($type === 'sale') ? (float)$old['quantity'] : -(float)$old['quantity'];
update_stock($old['item_id'], $change);
}
// Delete old items
$db->prepare("DELETE FROM $item_table WHERE $fk_col = ?")->execute([$id]);
// Insert new items and update stock
foreach ($items as $i => $item_id) {
if (!$item_id) continue;
$qty = (float)$qtys[$i];
$price = (float)$prices[$i];
$subtotal = $qty * $price;
$stmtVat = $db->prepare("SELECT vat_rate FROM stock_items WHERE id = ?");
$stmtVat->execute([$item_id]);
$vatRate = (float)$stmtVat->fetchColumn();
$vatAmount = $subtotal * ($vatRate / 100);
$db->prepare("INSERT INTO $item_table ($fk_col, item_id, quantity, unit_price, vat_amount, total_price) VALUES (?, ?, ?, ?, ?, ?)")->execute([$id, $item_id, $qty, $price, $vatAmount, $subtotal]);
$change = ($type === 'sale') ? -$qty : $qty;
update_stock($item_id, $change);
}
$db->commit();
$msg = ($type === 'purchase' ? "Purchase" : "Invoice") . " updated successfully!";
redirectWithMessage($msg, "index.php?page=" . ($type === 'purchase' ? 'purchases' : 'sales'));
} catch (Exception $e) { $db->rollBack(); $message = "Error: " . $e->getMessage(); }
}
// --- HR Handlers ---
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
error_log("POST Request detected. Action: " . (print_r($_POST, true)));
}
if (isset($_POST['add_hr_department'])) {
$name = $_POST['name'] ?? '';
if ($name) {
$stmt = db()->prepare("INSERT INTO hr_departments (name) VALUES (?)");
$stmt->execute([$name]);
$message = "Department added successfully!";
}
}
if (isset($_POST['edit_hr_department'])) {
$id = (int)$_POST['id'];
$name = $_POST['name'] ?? '';
if ($id && $name) {
$stmt = db()->prepare("UPDATE hr_departments SET name = ? WHERE id = ?");
$stmt->execute([$name, $id]);
redirectWithMessage("Department updated successfully!", "index.php?page=hr_departments");
}
}
if (isset($_POST['delete_hr_department'])) {
$id = (int)$_POST['id'];
if ($id) {
$stmt = db()->prepare("DELETE FROM hr_departments WHERE id = ?");
$stmt->execute([$id]);
redirectWithMessage("Department deleted successfully!", "index.php?page=hr_departments");
}
}
if (isset($_POST['add_hr_employee'])) {
$dept_id = (int)$_POST['department_id'];
$biometric_id = $_POST['biometric_id'] ?? '';
$name = $_POST['name'] ?? '';
$email = $_POST['email'] ?? '';
$phone = $_POST['phone'] ?? '';
$pos = $_POST['position'] ?? '';
$salary = (float)$_POST['salary'];
$j_date = $_POST['joining_date'] ?: date('Y-m-d');
if ($name) {
$stmt = db()->prepare("INSERT INTO hr_employees (department_id, biometric_id, name, email, phone, position, salary, joining_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$dept_id, $biometric_id, $name, $email, $phone, $pos, $salary, $j_date]);
redirectWithMessage("Employee added successfully!", "index.php?page=hr_employees");
}
}
if (isset($_POST['edit_hr_employee'])) {
$id = (int)$_POST['id'];
$dept_id = (int)$_POST['department_id'];
$biometric_id = $_POST['biometric_id'] ?? '';
$name = $_POST['name'] ?? '';
$email = $_POST['email'] ?? '';
$phone = $_POST['phone'] ?? '';
$pos = $_POST['position'] ?? '';
$salary = (float)$_POST['salary'];
$j_date = $_POST['joining_date'];
$status = $_POST['status'] ?? 'active';
if ($id && $name) {
$stmt = db()->prepare("UPDATE hr_employees SET department_id = ?, biometric_id = ?, name = ?, email = ?, phone = ?, position = ?, salary = ?, joining_date = ?, status = ? WHERE id = ?");
$stmt->execute([$dept_id, $biometric_id, $name, $email, $phone, $pos, $salary, $j_date, $status, $id]);
redirectWithMessage("Employee updated successfully!", "index.php?page=hr_employees");
}
}
if (isset($_POST['delete_hr_employee'])) {
$id = (int)$_POST['id'];
if ($id) {
$stmt = db()->prepare("DELETE FROM hr_employees WHERE id = ?");
$stmt->execute([$id]);
redirectWithMessage("Employee deleted successfully!", "index.php?page=hr_employees");
}
}
if (isset($_POST['mark_hr_attendance'])) {
$emp_id = (int)$_POST['employee_id'];
$date = $_POST['attendance_date'] ?: date('Y-m-d');
$status = $_POST['status'] ?? 'present';
$in = $_POST['clock_in'] ?: null;
$out = $_POST['clock_out'] ?: null;
if ($emp_id) {
$check = db()->prepare("SELECT id FROM hr_attendance WHERE employee_id = ? AND attendance_date = ?");
$check->execute([$emp_id, $date]);
if ($check->fetch()) {
$stmt = db()->prepare("UPDATE hr_attendance SET status = ?, clock_in = ?, clock_out = ? WHERE employee_id = ? AND attendance_date = ?");
$stmt->execute([$status, $in, $out, $emp_id, $date]);
} else {
$stmt = db()->prepare("INSERT INTO hr_attendance (employee_id, attendance_date, status, clock_in, clock_out) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$emp_id, $date, $status, $in, $out]);
}
redirectWithMessage("Attendance marked successfully!", "index.php?page=hr_attendance&date=$date");
}
}
if (isset($_POST['generate_payroll'])) {
$emp_id = (int)$_POST['employee_id'];
$month = (int)$_POST['month'];
$year = (int)$_POST['year'];
$bonus = (float)$_POST['bonus'];
$deduct = (float)$_POST['deductions'];
$notes = $_POST['notes'] ?? '';
$db = db();
try {
$db->beginTransaction();
$emp = $db->query("SELECT salary FROM hr_employees WHERE id = $emp_id")->fetch();
if (!$emp) throw new Exception("Employee not found.");
$basic = (float)$emp['salary'];
$net = $basic + $bonus - $deduct;
$check = $db->prepare("SELECT id FROM hr_payroll WHERE employee_id = ? AND payroll_month = ? AND payroll_year = ?");
$check->execute([$emp_id, $month, $year]);
if ($check->fetch()) {
throw new Exception("Payroll already exists for this employee in the selected period.");
}
$payrollColumns = ['employee_id', 'payroll_month', 'payroll_year', 'basic_salary', 'bonus', 'deductions', 'net_salary'];
$payrollPlaceholders = array_fill(0, count($payrollColumns), '?');
$payrollValues = [$emp_id, $month, $year, $basic, $bonus, $deduct, $net];
if (db_column_exists('hr_payroll', 'notes')) {
$payrollColumns[] = 'notes';
$payrollPlaceholders[] = '?';
$payrollValues[] = $notes;
}
$stmt = $db->prepare("INSERT INTO hr_payroll (" . implode(', ', $payrollColumns) . ") VALUES (" . implode(', ', $payrollPlaceholders) . ")");
$stmt->execute($payrollValues);
$db->commit();
redirectWithMessage("Payroll generated successfully!", "index.php?page=hr_payroll&month=$month&year=$year");
} catch (Exception $e) {
$db->rollBack();
$message = "Error: " . $e->getMessage();
}
}
if (isset($_POST['pay_payroll'])) {
$id = (int)$_POST['id'];
$db = db();
try {
$db->beginTransaction();
$stmt = $db->prepare("SELECT * FROM hr_payroll WHERE id = ? AND status = 'unpaid'");
$stmt->execute([$id]);
$p = $stmt->fetch();
if ($p) {
$db->prepare("UPDATE hr_payroll SET status = 'paid', payment_date = CURDATE() WHERE id = ?")->execute([$id]);
// Accounting
recordExpenseJournalForPayroll($id, (float)$p['net_salary'], date('Y-m-d'));
$db->commit();
redirectWithMessage("Payroll marked as paid and recorded in accounting!", "index.php?page=hr_payroll&month={$p['payroll_month']}&year={$p['payroll_year']}");
} else {
throw new Exception("Payroll already paid or not found.");
}
} catch (Exception $e) {
$db->rollBack();
$message = "Error: " . $e->getMessage();
}
}
if (isset($_POST['delete_payroll'])) {
$id = (int)$_POST['id'];
$stmt = db()->prepare("SELECT payroll_month, payroll_year FROM hr_payroll WHERE id = ?");
$stmt->execute([$id]);
$p = $stmt->fetch();
if ($p) {
db()->prepare("DELETE FROM hr_payroll WHERE id = ?")->execute([$id]);
redirectWithMessage("Payroll record deleted successfully!", "index.php?page=hr_payroll&month={$p['payroll_month']}&year={$p['payroll_year']}");
}
}
if (isset($_POST['add_sales_return'])) {
$invoice_id = (int)$_POST['invoice_id'];
$return_date = $_POST['return_date'] ?: date('Y-m-d');
$notes = $_POST['notes'] ?? '';
$item_ids = $_POST['item_ids'] ?? [];
$quantities = $_POST['quantities'] ?? [];
$prices = $_POST['prices'] ?? [];
if ($invoice_id && !empty($item_ids)) {
$db = db();
try {
$db->beginTransaction();
// Get customer_id from invoice
$stmtInv = $db->prepare("SELECT customer_id FROM invoices WHERE id = ?");
$stmtInv->execute([$invoice_id]);
$customer_id = $stmtInv->fetchColumn();
$total_return = 0;
foreach ($quantities as $i => $qty) {
$total_return += (float)$qty * (float)$prices[$i];
}
// Insert Sales Return
$salesReturnReferenceColumn = sales_return_reference_column();
[$salesReturnInsertSql, $salesReturnInsertValues] = db_insert_sql_for_existing_columns('sales_returns', [
$salesReturnReferenceColumn => $invoice_id,
'customer_id' => $customer_id,
'return_date' => $return_date,
'total_amount' => $total_return,
'notes' => $notes,
'outlet_id' => current_outlet_id(),
]);
$stmt = $db->prepare($salesReturnInsertSql);
$stmt->execute($salesReturnInsertValues);
$return_id = $db->lastInsertId();
// Insert Return Items and Update Stock
$stmtItem = $db->prepare("INSERT INTO sales_return_items (return_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
// $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity + ? WHERE id = ?");
foreach ($item_ids as $i => $item_id) {
$qty = (float)$quantities[$i];
if ($qty > 0) {
$price = (float)$prices[$i];
$line_total = $qty * $price;
$stmtItem->execute([$return_id, $item_id, $qty, $price, $line_total]);
update_stock($item_id, $qty);
}
}
$db->commit();
redirectWithMessage("Sales Return processed successfully!");
} catch (Exception $e) {
$db->rollBack();
$message = "Error processing return: " . $e->getMessage();
}
}
}
if (isset($_POST['add_purchase_return'])) {
$invoice_id = (int)$_POST['invoice_id'];
$return_date = $_POST['return_date'] ?: date('Y-m-d');
$notes = $_POST['notes'] ?? '';
$item_ids = $_POST['item_ids'] ?? [];
$quantities = $_POST['quantities'] ?? [];
$prices = $_POST['prices'] ?? [];
if ($invoice_id && !empty($item_ids)) {
$db = db();
try {
$db->beginTransaction();
// Get supplier_id from purchase
$stmtInv = $db->prepare("SELECT supplier_id FROM purchases WHERE id = ?");
$stmtInv->execute([$invoice_id]);
$supplier_id = $stmtInv->fetchColumn();
$total_return = 0;
foreach ($quantities as $i => $qty) {
$total_return += (float)$qty * (float)$prices[$i];
}
// Insert Purchase Return
$purchaseReturnReferenceColumn = purchase_return_reference_column();
[$purchaseReturnInsertSql, $purchaseReturnInsertValues] = db_insert_sql_for_existing_columns('purchase_returns', [
$purchaseReturnReferenceColumn => $invoice_id,
'supplier_id' => $supplier_id,
'return_date' => $return_date,
'total_amount' => $total_return,
'notes' => $notes,
'outlet_id' => current_outlet_id(),
]);
$stmt = $db->prepare($purchaseReturnInsertSql);
$stmt->execute($purchaseReturnInsertValues);
$return_id = $db->lastInsertId();
// Insert Return Items and Update Stock
$stmtItem = $db->prepare("INSERT INTO purchase_return_items (return_id, item_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?)");
// $stmtStock = $db->prepare("UPDATE stock_items SET stock_quantity = stock_quantity - ? WHERE id = ?");
foreach ($item_ids as $i => $item_id) {
$qty = (float)$quantities[$i];
if ($qty > 0) {
$price = (float)$prices[$i];
$line_total = $qty * $price;
$stmtItem->execute([$return_id, $item_id, $qty, $price, $line_total]);
update_stock($item_id, -$qty);
}
}
$db->commit();
redirectWithMessage("Purchase Return processed successfully!");
} catch (Exception $e) {
$db->rollBack();
$message = "Error processing return: " . $e->getMessage();
}
}
}
// --- Biometric Devices Handlers ---
if (isset($_POST['add_biometric_device'])) {
$name = $_POST['device_name'] ?? '';
$ip = $_POST['ip_address'] ?? '';
$port = (int)($_POST['port'] ?? 4370);
$io = $_POST['io_address'] ?? '';
$serial = $_POST['serial_number'] ?? '';
if ($name && $ip) {
$stmt = db()->prepare("INSERT INTO hr_biometric_devices (device_name, ip_address, port, io_address, serial_number) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$name, $ip, $port, $io, $serial]);
$message = "Device added successfully!";
}
}
if (isset($_POST['edit_biometric_device'])) {
$id = (int)$_POST['id'];
$name = $_POST['device_name'] ?? '';
$ip = $_POST['ip_address'] ?? '';
$port = (int)($_POST['port'] ?? 4370);
$io = $_POST['io_address'] ?? '';
$serial = $_POST['serial_number'] ?? '';
if ($id && $name && $ip) {
$stmt = db()->prepare("UPDATE hr_biometric_devices SET device_name = ?, ip_address = ?, port = ?, io_address = ?, serial_number = ? WHERE id = ?");
$stmt->execute([$name, $ip, $port, $io, $serial, $id]);
$message = "Device updated successfully!";
}
}
if (isset($_POST['delete_biometric_device'])) {
$id = (int)$_POST['id'];
if ($id) {
$stmt = db()->prepare("DELETE FROM hr_biometric_devices WHERE id = ?");
$stmt->execute([$id]);
$message = "Device deleted successfully!";
}
}
if (isset($_POST['pull_biometric_data'])) {
$devices = db()->query("SELECT * FROM hr_biometric_devices WHERE status = 'active'")->fetchAll();
if (empty($devices)) {
$message = "No active biometric devices found to pull data from.";
} else {
// Simulation of pulling data from multiple devices
$employees = db()->query("SELECT id, biometric_id FROM hr_employees WHERE biometric_id IS NOT NULL")->fetchAll();
$pulled_count = 0;
$device_count = 0;
$date = date('Y-m-d');
foreach ($devices as $device) {
$device_pulled = 0;
foreach ($employees as $emp) {
// Randomly simulate logs for each employee for this device
if (rand(0, 1)) {
$check_in = $date . ' ' . str_pad((string)rand(7, 9), 2, '0', STR_PAD_LEFT) . ':' . str_pad((string)rand(0, 59), 2, '0', STR_PAD_LEFT) . ':00';
$check_out = $date . ' ' . str_pad((string)rand(16, 18), 2, '0', STR_PAD_LEFT) . ':' . str_pad((string)rand(0, 59), 2, '0', STR_PAD_LEFT) . ':00';
// Log check-in
$stmt = db()->prepare("INSERT INTO hr_biometric_logs (biometric_id, device_id, employee_id, timestamp, type) VALUES (?, ?, ?, ?, 'check_in')");
$stmt->execute([$emp['biometric_id'], $device['id'], $emp['id'], $check_in]);
// Log check-out
$stmt = db()->prepare("INSERT INTO hr_biometric_logs (biometric_id, device_id, employee_id, timestamp, type) VALUES (?, ?, ?, ?, 'check_out')");
$stmt->execute([$emp['biometric_id'], $device['id'], $emp['id'], $check_out]);
$device_pulled += 2;
$pulled_count += 2;
$in_time = date('H:i:s', strtotime($check_in));
$out_time = date('H:i:s', strtotime($check_out));
// Update attendance record (earliest in, latest out)
$stmt = db()->prepare("INSERT INTO hr_attendance (employee_id, attendance_date, status, clock_in, clock_out)
VALUES (?, ?, 'present', ?, ?)
ON DUPLICATE KEY UPDATE status = 'present',
clock_in = IF(clock_in IS NULL OR ? < clock_in, ?, clock_in),
clock_out = IF(clock_out IS NULL OR ? > clock_out, ?, clock_out)");
$stmt->execute([$emp['id'], $date, $in_time, $out_time, $in_time, $in_time, $out_time, $out_time]);
}
}
db()->prepare("UPDATE hr_biometric_devices SET last_sync = CURRENT_TIMESTAMP WHERE id = ?")->execute([$device['id']]);
$device_count++;
}
$message = "Successfully synced $device_count devices and pulled $pulled_count records.";
}
}
if (isset($_POST['test_device_connection'])) {
$id = (int)$_POST['id'];
$device = db()->prepare("SELECT * FROM hr_biometric_devices WHERE id = ?");
$device->execute([$id]);
$d = $device->fetch();
if ($d) {
// Simulated connection check
$message = "Connection to device '{$d['device_name']}' ({$d['ip_address']}) was successful! (Simulated)";
}
}
// --- User & Role Groups Handlers ---
if (isset($_POST['add_role_group'])) {
$name = $_POST['name'] ?? '';
$permissions = isset($_POST['permissions']) ? $_POST['permissions'] : [];
if ($name) {
try {
$db = db();
$db->beginTransaction();
$stmt = $db->prepare("INSERT INTO role_groups (name) VALUES (?)");
$stmt->execute([$name]);
$role_id = $db->lastInsertId();
if (!empty($permissions)) {
$stmtPerm = $db->prepare("INSERT INTO role_permissions (role_id, permission) VALUES (?, ?)");
foreach ($permissions as $p) {
$stmtPerm->execute([$role_id, $p]);
}
}
$db->commit();
$message = "Role Group added successfully!";
} catch (PDOException $e) {
if ($db->inTransaction()) $db->rollBack();
$message = "Error adding role group: " . $e->getMessage();
}
}
}
if (isset($_POST['edit_role_group'])) {
$id = (int)$_POST['id'];
$name = $_POST['name'] ?? '';
$permissions = isset($_POST['permissions']) ? $_POST['permissions'] : [];
if ($id && $name) {
try {
$db = db();
$db->beginTransaction();
$stmt = $db->prepare("UPDATE role_groups SET name = ? WHERE id = ?");
$stmt->execute([$name, $id]);
// Refresh permissions
$stmtDel = $db->prepare("DELETE FROM role_permissions WHERE role_id = ?");
$stmtDel->execute([$id]);
if (!empty($permissions)) {
$stmtPerm = $db->prepare("INSERT INTO role_permissions (role_id, permission) VALUES (?, ?)");
foreach ($permissions as $p) {
$stmtPerm->execute([$id, $p]);
}
}
$db->commit();
$message = "Role Group updated successfully!";
} catch (PDOException $e) {
if ($db->inTransaction()) $db->rollBack();
$message = "Error updating role group: " . $e->getMessage();
}
}
}
if (isset($_POST['delete_role_group'])) {
$id = (int)$_POST['id'];
if ($id) {
$stmt = db()->prepare("DELETE FROM role_groups WHERE id = ?");
$stmt->execute([$id]);
$message = "Role Group deleted successfully!";
}
}
// --- POS Devices Handlers ---
if (isset($_POST['add_pos_device'])) {
$name = $_POST['device_name'] ?? '';
$type = $_POST['device_type'] ?? 'scale';
$conn = $_POST['connection_type'] ?? 'usb';
$ip = $_POST['ip_address'] ?? '';
$port = $_POST['port'] ? (int)$_POST['port'] : null;
$baud = $_POST['baud_rate'] ? (int)$_POST['baud_rate'] : null;
if ($name) {
$stmt = db()->prepare("INSERT INTO pos_devices (device_name, device_type, connection_type, ip_address, port, baud_rate) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$name, $type, $conn, $ip, $port, $baud]);
redirectWithMessage("Device added successfully!", "index.php?page=scale_devices");
}
}
if (isset($_POST['edit_pos_device'])) {
$id = (int)$_POST['id'];
$name = $_POST['device_name'] ?? '';
$type = $_POST['device_type'] ?? 'scale';
$conn = $_POST['connection_type'] ?? 'usb';
$ip = $_POST['ip_address'] ?? '';
$port = $_POST['port'] ? (int)$_POST['port'] : null;
$baud = $_POST['baud_rate'] ? (int)$_POST['baud_rate'] : null;
$status = $_POST['status'] ?? 'active';
if ($id && $name) {
$stmt = db()->prepare("UPDATE pos_devices SET device_name = ?, device_type = ?, connection_type = ?, ip_address = ?, port = ?, baud_rate = ?, status = ? WHERE id = ?");
$stmt->execute([$name, $type, $conn, $ip, $port, $baud, $status, $id]);
redirectWithMessage("Device updated successfully!", "index.php?page=scale_devices");
}
}
if (isset($_POST['delete_pos_device'])) {
$id = (int)$_POST['id'];
if ($id) {
$stmt = db()->prepare("DELETE FROM pos_devices WHERE id = ?");
$stmt->execute([$id]);
redirectWithMessage("Device deleted successfully!", "index.php?page=scale_devices");
}
}
if (isset($_POST['update_profile'])) {
$id = $_SESSION['user_id'];
$username = $_POST['username'] ?? '';
$email = $_POST['email'] ?? '';
$phone = $_POST['phone'] ?? '';
if ($id && $username) {
$stmt = db()->prepare("UPDATE users SET username = ?, email = ?, phone = ? WHERE id = ?");
$stmt->execute([$username, $email, $phone, $id]);
$_SESSION['username'] = $username;
if (!empty($_POST['password'])) {
$hashed_password = password_hash($_POST['password'], PASSWORD_DEFAULT);
$stmt = db()->prepare("UPDATE users SET password = ? WHERE id = ?");
$stmt->execute([$hashed_password, $id]);
}
if (isset($_FILES['profile_pic']) && $_FILES['profile_pic']['error'] === 0) {
$ext = pathinfo($_FILES['profile_pic']['name'], PATHINFO_EXTENSION);
$filename = 'uploads/profile_' . $id . '_' . time() . '.' . $ext;
if (!is_dir('uploads')) mkdir('uploads', 0777, true);
if (move_uploaded_file($_FILES['profile_pic']['tmp_name'], $filename)) {
$stmt = db()->prepare("UPDATE users SET profile_pic = ? WHERE id = ?");
$stmt->execute([$filename, $id]);
$_SESSION['profile_pic'] = $filename;
}
}
redirectWithMessage("Profile updated successfully!", "index.php?page=my_profile");
}
}
if (isset($_POST['update_settings'])) {
if (can('settings_view')) {
$db = db();
if (isset($_POST['settings']) && is_array($_POST['settings'])) {
$settings = $_POST['settings'];
$settings['weight_barcode_mode'] = in_array(($settings['weight_barcode_mode'] ?? 'weight'), ['weight', 'price'], true) ? $settings['weight_barcode_mode'] : 'weight';
$licenseAppName = trim((string)($settings['license_app_name'] ?? ''));
$settings['license_app_name'] = $licenseAppName !== '' ? substr($licenseAppName, 0, 190) : '';
$licenseAppSlug = trim((string)($settings['license_app_slug'] ?? ''));
$settings['license_app_slug'] = $licenseAppSlug !== '' ? LicenseService::sanitizeAppSlug($licenseAppSlug, true) : '';
$prefixStart = (int)($settings['weight_barcode_prefix_start'] ?? 20);
$prefixEnd = (int)($settings['weight_barcode_prefix_end'] ?? 29);
if ($prefixStart < 20 || $prefixStart > 29) $prefixStart = 20;
if ($prefixEnd < 20 || $prefixEnd > 29) $prefixEnd = 29;
if ($prefixStart > $prefixEnd) {
[$prefixStart, $prefixEnd] = [$prefixEnd, $prefixStart];
}
$settings['weight_barcode_prefix_start'] = (string)$prefixStart;
$settings['weight_barcode_prefix_end'] = (string)$prefixEnd;
foreach ($settings as $key => $value) {
$stmt = $db->prepare("INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?");
$stmt->execute([$key, $value, $value]);
}
}
// Handle file uploads
$files = ['company_logo', 'favicon', 'manager_signature', 'display_slide_1', 'display_slide_2', 'display_slide_3'];
foreach ($files as $file_key) {
if (isset($_FILES[$file_key]) && $_FILES[$file_key]['error'] === 0) {
$ext = pathinfo($_FILES[$file_key]['name'], PATHINFO_EXTENSION);
$filename = 'uploads/' . $file_key . '_' . time() . '.' . $ext;
if (!is_dir('uploads')) mkdir('uploads', 0777, true);
if (move_uploaded_file($_FILES[$file_key]['tmp_name'], $filename)) {
$stmt = $db->prepare("INSERT INTO settings (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = ?");
$stmt->execute([$file_key, $filename, $filename]);
}
}
}
redirectWithMessage("Settings updated successfully!", "index.php?page=settings");
}
}
// --- Backup Handlers ---
if (isset($_POST['create_backup'])) {
if (can('users_view')) { // Admin check
$res = BackupService::createBackup();
$message = $res['success'] ? "Backup created: " . $res['file'] : "Error: " . $res['error'];
}
}
if (isset($_POST['restore_backup'])) {
if (can('users_view')) {
$filename = $_POST['filename'] ?? '';
$res = BackupService::restoreBackup($filename);
redirectWithMessage($res['success'] ? "Database restored successfully from $filename!" : "Error: " . $res['error'], "index.php?page=backups");
}
}
if (isset($_POST['delete_backup'])) {
if (can('users_view')) {
$filename = basename($_POST['filename'] ?? '');
if (unlink(__DIR__ . '/backups/' . $filename)) {
redirectWithMessage("Backup deleted successfully.", "index.php?page=backups");
} else {
redirectWithMessage("Error deleting backup.", "index.php?page=backups");
}
}
}
if (isset($_POST['save_backup_settings'])) {
$limit = (int)($_POST['backup_limit'] ?? 5);
$auto = $_POST['backup_auto_enabled'] ?? '0';
$time = $_POST['backup_time'] ?? '00:00';
$stmt = $db->prepare("INSERT INTO settings (`key`, `value`) VALUES ('backup_limit', ?), ('backup_auto_enabled', ?), ('backup_time', ?) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)");
$stmt->execute([$limit, $auto, $time]);
redirectWithMessage("Backup settings updated.", "index.php?page=backups");
}
if (isset($_GET['download_backup'])) {
$filename = basename($_GET['download_backup']);
$filepath = __DIR__ . '/backups/' . $filename;
if (file_exists($filepath)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($filepath));
readfile($filepath);
exit;
}
}
// --- Cash Register & Session Handlers ---
if (isset($_POST['add_cash_register'])) {
$name = $_POST['name'] ?? '';
if ($name) {
// Check license limit
$allowed = LicenseService::getAllowedActivations();
$stmt = db()->query("SELECT COUNT(*) FROM cash_registers");
$current_count = (int)$stmt->fetchColumn();
if ($current_count >= $allowed) {
$message = "Error: Activation Limit Reached. Your license only allows $allowed register(s).";
} else {
$stmt = db()->prepare("INSERT INTO cash_registers (name) VALUES (?)");
$stmt->execute([$name]);
redirectWithMessage("Cash Register added successfully!", "index.php?page=cash_registers");
}
}
}
if (isset($_POST['edit_cash_register'])) {
$id = (int)$_POST['id'];
$name = $_POST['name'] ?? '';
$status = $_POST['status'] ?? 'active';
if ($id && $name) {
$stmt = db()->prepare("UPDATE cash_registers SET name = ?, status = ? WHERE id = ?");
$stmt->execute([$name, $status, $id]);
redirectWithMessage("Cash Register updated successfully!", "index.php?page=cash_registers");
}
}
if (isset($_POST['delete_cash_register'])) {
$id = (int)$_POST['id'];
if ($id) {
$stmt = db()->prepare("DELETE FROM cash_registers WHERE id = ?");
$stmt->execute([$id]);
redirectWithMessage("Cash Register deleted successfully!", "index.php?page=cash_registers");
}
}
if (isset($_POST['open_register'])) {
$register_id = (int)$_POST['register_id'];
$user_id = $_SESSION['user_id'];
$opening_balance = (float)$_POST['opening_balance'];
// Check if user already has an open session
$check = db()->prepare("SELECT id FROM register_sessions WHERE user_id = ? AND status = 'open'");
$check->execute([$user_id]);
if ($check->fetch()) {
$message = "Error: You already have an open register session.";
} else {
$stmt = db()->prepare("INSERT INTO register_sessions (register_id, user_id, opening_balance, status) VALUES (?, ?, ?, 'open')");
$stmt->execute([$register_id, $user_id, $opening_balance]);
$_SESSION['register_session_id'] = db()->lastInsertId();
redirectWithMessage("Register opened successfully!", "index.php?page=pos");
}
}
// Removed conflicting close_register handler
if (isset($_POST['delete_backup'])) {
if (can('users_view')) {
$filename = basename($_POST['filename'] ?? '');
if (unlink(__DIR__ . '/backups/' . $filename)) {
$message = "Backup deleted successfully.";
} else {
$message = "Error deleting backup.";
}
}
}
if (isset($_POST['save_backup_settings'])) {
$limit = (int)($_POST['backup_limit'] ?? 5);
$auto = $_POST['backup_auto_enabled'] ?? '0';
$time = $_POST['backup_time'] ?? '00:00';
$stmt = $db->prepare("INSERT INTO settings (`key`, `value`) VALUES ('backup_limit', ?), ('backup_auto_enabled', ?), ('backup_time', ?) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)");
$stmt->execute([$limit, $auto, $time]);
$message = "Backup settings updated.";
}
if (isset($_GET['download_backup'])) {
$filename = basename($_GET['download_backup']);
$filepath = __DIR__ . '/backups/' . $filename;
if (file_exists($filepath)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($filepath));
readfile($filepath);
exit;
}
}
// --- Cash Register & Session Handlers ---
if (isset($_POST['add_cash_register'])) {
$name = $_POST['name'] ?? '';
if ($name) {
// Check license limit
$allowed = LicenseService::getAllowedActivations();
$stmt = db()->query("SELECT COUNT(*) FROM cash_registers");
$current_count = (int)$stmt->fetchColumn();
if ($current_count >= $allowed) {
$message = "Error: Activation Limit Reached. Your license only allows $allowed register(s).";
} else {
$stmt = db()->prepare("INSERT INTO cash_registers (name) VALUES (?)");
$stmt->execute([$name]);
$message = "Cash Register added successfully!";
}
}
}
if (isset($_POST['edit_cash_register'])) {
$id = (int)$_POST['id'];
$name = $_POST['name'] ?? '';
$status = $_POST['status'] ?? 'active';
if ($id && $name) {
$stmt = db()->prepare("UPDATE cash_registers SET name = ?, status = ? WHERE id = ?");
$stmt->execute([$name, $status, $id]);
$message = "Cash Register updated successfully!";
}
}
if (isset($_POST['delete_cash_register'])) {
$id = (int)$_POST['id'];
if ($id) {
$stmt = db()->prepare("DELETE FROM cash_registers WHERE id = ?");
$stmt->execute([$id]);
$message = "Cash Register deleted successfully!";
}
}
if (isset($_POST['open_register'])) {
$register_id = (int)$_POST['register_id'];
$user_id = $_SESSION['user_id'];
$opening_balance = (float)$_POST['opening_balance'];
// Check if user already has an open session
$check = db()->prepare("SELECT id FROM register_sessions WHERE user_id = ? AND status = 'open'");
$check->execute([$user_id]);
if ($check->fetch()) {
$message = "Error: You already have an open register session.";
} else {
$stmt = db()->prepare("INSERT INTO register_sessions (register_id, user_id, opening_balance, status) VALUES (?, ?, ?, 'open')");
$stmt->execute([$register_id, $user_id, $opening_balance]);
$_SESSION['register_session_id'] = db()->lastInsertId();
$message = "Register opened successfully!";
header("Location: index.php?page=pos");
exit;
}
}
if (isset($_POST['close_register'])) {
$session_id = (int)$_POST['session_id'];
$cash_in_hand = (float)$_POST['cash_in_hand'];
$notes = $_POST['notes'] ?? '';
$redirect_to = $_POST['redirect_to'] ?? 'dashboard';
// Calculate expected closing balance
$session = db()->prepare("SELECT opening_balance FROM register_sessions WHERE id = ?");
$session->execute([$session_id]);
$opening = (float)$session->fetchColumn();
// Unified calculation logic (POS Transactions + Invoices)
// Note: POS transactions are saved in invoices table with is_pos=1
$sales_sql = "SELECT SUM(p.amount) FROM payments p JOIN invoices i ON p.invoice_id = i.id WHERE i.register_session_id = ? AND i.status = 'paid' AND i.is_pos = 1 AND LOWER(p.payment_method) = 'cash'";
$sales_stmt = db()->prepare($sales_sql);
$sales_stmt->execute([$session_id]);
$cash_sales = (float)$sales_stmt->fetchColumn();
$expected = $opening + $cash_sales;
$stmt = db()->prepare("UPDATE register_sessions SET closing_balance = ?, cash_in_hand = ?, closed_at = CURRENT_TIMESTAMP, status = 'closed', notes = ? WHERE id = ?");
$stmt->execute([$expected, $cash_in_hand, $notes, $session_id]);
unset($_SESSION['register_session_id']);
$message = "Register closed successfully!";
if ($redirect_to === 'dashboard') {
// For POS users, we want dashboard
header("Location: index.php?page=dashboard");
} else {
// For Admin, we stay on sessions page
redirectWithMessage($message, "index.php?page=" . urlencode($redirect_to));
}
exit;
}
// Routing & Data Fetching
$page = $_GET['page'] ?? 'dashboard';
// Permission map for pages
$page_permissions = [
'pos' => 'pos_view',
'sales' => 'sales_view',
'sales_returns' => 'sales_returns_view',
'purchases' => 'purchases_view',
'purchase_returns' => 'purchase_returns_view',
'quotations' => 'quotations_view',
'lpos' => 'lpos_view',
'accounting' => 'accounting_view',
'expense_categories' => 'expense_categories_view',
'expenses' => 'expenses_view',
'expense_report' => 'expenses_view',
'items' => 'items_view',
'categories' => 'categories_view',
'units' => 'units_view',
'customers' => 'customers_view',
'suppliers' => 'suppliers_view',
'customer_statement' => 'customer_statement_view',
'supplier_statement' => 'supplier_statement_view',
'cashflow_report' => 'cashflow_report_view',
'expiry_report' => 'expiry_report_view',
'low_stock_report' => 'low_stock_report_view',
'loyalty_history' => 'loyalty_history_view',
'payment_methods' => 'payment_methods_view',
'settings' => 'settings_view',
'devices' => 'devices_view',
'hr_departments' => 'hr_departments_view',
'hr_employees' => 'hr_employees_view',
'hr_attendance' => 'hr_attendance_view',
'hr_payroll' => 'hr_payroll_view',
'role_groups' => 'role_groups_view',
'users' => 'users_view',
'scale_devices' => 'scale_devices_view',
'customer_display_settings' => 'customer_display_settings_view',
'backups' => 'backups_view',
'logs' => 'logs_view',
'cash_registers' => 'cash_registers_view',
'register_sessions' => 'register_sessions_view',
];
if (isset($page_permissions[$page]) && !can($page_permissions[$page])) {
$page = 'dashboard';
$message = "Access Denied: You don't have permission to view that module.";
}
$currTitle = ['en' => 'Dashboard', 'ar' => 'لوحة القيادة'];
$titles = [
'dashboard' => ['en' => 'Dashboard', 'ar' => 'لوحة القيادة'],
'pos' => ['en' => 'Point of Sale', 'ar' => 'نقطة البيع'],
'sales' => ['en' => 'Sales', 'ar' => 'المبيعات'],
'sales_returns' => ['en' => 'Sales Returns', 'ar' => 'مرتجعات المبيعات'],
'purchases' => ['en' => 'Purchases', 'ar' => 'المشتريات'],
'purchase_returns' => ['en' => 'Purchase Returns', 'ar' => 'مرتجعات المشتريات'],
'quotations' => ['en' => 'Quotations', 'ar' => 'عروض الأسعار'],
'lpos' => ['en' => 'LPOs', 'ar' => 'أوامر الشراء'],
'accounting' => ['en' => 'Accounting', 'ar' => 'المحاسبة'],
'expense_categories' => ['en' => 'Expense Categories', 'ar' => 'فئات المصروفات'],
'expenses' => ['en' => 'Expenses', 'ar' => 'المصروفات'],
'expense_report' => ['en' => 'Expense Report', 'ar' => 'تقرير المصروفات'],
'items' => ['en' => 'Items', 'ar' => 'الأصناف'],
'categories' => ['en' => 'Categories', 'ar' => 'الفئات'],
'units' => ['en' => 'Units', 'ar' => 'الوحدات'],
'customers' => ['en' => 'Customers', 'ar' => 'العملاء'],
'suppliers' => ['en' => 'Suppliers', 'ar' => 'الموردين'],
'customer_statement' => ['en' => 'Customer Statement', 'ar' => 'كشف حساب عميل'],
'supplier_statement' => ['en' => 'Supplier Statement', 'ar' => 'كشف حساب مورد'],
'cashflow_report' => ['en' => 'Cashflow Report', 'ar' => 'تقرير التدفق النقدي'],
'expiry_report' => ['en' => 'Expiry Report', 'ar' => 'تقرير الصلاحية'],
'low_stock_report' => ['en' => 'Low Stock Report', 'ar' => 'تقرير المخزون المنخفض'],
'loyalty_history' => ['en' => 'Loyalty History', 'ar' => 'سجل الولاء'],
'payment_methods' => ['en' => 'Payment Methods', 'ar' => 'طرق الدفع'],
'settings' => ['en' => 'Settings', 'ar' => 'الإعدادات'],
'devices' => ['en' => 'Biometric Devices', 'ar' => 'أجهزة البصمة'],
'hr_departments' => ['en' => 'Departments', 'ar' => 'الأقسام'],
'hr_employees' => ['en' => 'Employees', 'ar' => 'الموظفين'],
'hr_attendance' => ['en' => 'Attendance', 'ar' => 'الحضور'],
'hr_payroll' => ['en' => 'Payroll', 'ar' => 'الرواتب'],
'role_groups' => ['en' => 'Roles & Permissions', 'ar' => 'الأدوار والصلاحيات'],
'users' => ['en' => 'Users', 'ar' => 'المستخدمين'],
'cash_registers' => ['en' => 'Cash Registers', 'ar' => 'صناديق الكاشير'],
'register_sessions' => ['en' => 'Register Sessions', 'ar' => 'جلسات الصناديق']
];
if (isset($titles[$page])) {
$currTitle = $titles[$page];
}
$data = [
'payment_methods' => [],
'role_groups' => [],
'users' => [],
'expiry_items' => [],
'low_stock_items' => [],
'items' => [],
'cash_transactions' => [],
'monthly_sales' => [],
'yearly_sales' => [],
'opening_balance' => 0,
'stats' => [
'expired_items' => 0,
'near_expiry_items' => 0,
'low_stock_items_count' => 0,
'total_sales' => 0,
'total_received' => 0,
'total_receivable' => 0,
'total_purchases' => 0,
],
'settings' => [],
];
$permission_groups = [
'General' => ['dashboard' => __('dashboard')],
'Inventory' => [
'items' => __('items'),
'categories' => __('categories'),
'units' => __('units')
],
'Customers' => [
'customers' => __('customers')
],
'Suppliers' => [
'suppliers' => __('suppliers')
],
'POS' => [
'pos' => __('pos')
],
'Sales' => [
'sales' => __('sales'),
'sales_returns' => __('sales_returns'),
'quotations' => __('quotations')
],
'Purchases' => [
'purchases' => __('purchases'),
'lpos' => __('lpos'),
'purchase_returns' => __('purchase_returns')
],
'Expenses' => [
'expense_categories' => __('expense_categories'),
'expenses' => __('expenses')
],
'Accounting' => [
'accounting' => __('accounting'),
'trial_balance' => __('trial_balance'),
'profit_loss' => __('profit_loss'),
'balance_sheet' => __('balance_sheet'),
'vat_report' => __('vat_report')
],
'HR' => [
'hr_departments' => __('departments'),
'hr_employees' => __('employees'),
'hr_attendance' => __('attendance'),
'hr_payroll' => __('payroll')
],
'Reports' => [
'customer_statement' => __('customer_statement'),
'supplier_statement' => __('supplier_statement'),
'expense_report' => __('expense_report'),
'cashflow_report' => __('cashflow_report'),
'expiry_report' => __('expiry_report'),
'low_stock_report' => __('low_stock_report'),
'loyalty_history' => __('loyalty_history'),
'register_sessions' => __('register_sessions')
],
'Settings' => [
'payment_methods' => __('payment_methods'),
'devices' => __('devices'),
'settings' => __('settings')
],
'Administration' => [
'role_groups' => __('role_groups'),
'users' => __('users'),
'cash_registers' => __('cash_registers'),
'logs' => 'System Logs'
]
];
if ($page === 'export') {
$type = $_GET['type'] ?? 'sales';
$format = $_GET['format'] ?? 'csv';
$filename = $type . "_export_" . date('Y-m-d') . ($format === 'excel' ? ".xls" : ".csv");
if ($format === 'excel') {
header('Content-Type: application/vnd.ms-excel; charset=utf-8');
header('Content-Disposition: attachment; filename=' . $filename);
echo "";
} else {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=' . $filename);
$output = fopen('php://output', 'w');
// Add UTF-8 BOM for Excel
fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF));
}
$headers = [];
$rows = [];
if ($type === 'sales' || $type === 'purchases') {
$table = ($type === 'sales') ? 'invoices' : 'purchases';
$cust_table = ($type === 'sales') ? 'customers' : 'suppliers';
$cust_col = ($type === 'sales') ? 'customer_id' : 'supplier_id';
$where = ["1=1"];
$params = [];
if (!empty($_GET['search'])) {
$s = $_GET['search'];
$clean_id = preg_replace('/[^0-9]/', '', $s);
if ($clean_id !== '') {
$where[] = "(v.id LIKE ? OR c.name LIKE ? OR v.id = ?)";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = $clean_id;
} else {
$where[] = "(v.id LIKE ? OR c.name LIKE ?)";
$params[] = "%$s%";
$params[] = "%$s%";
}
}
if (!empty($_GET['customer_id'])) { $where[] = "v.$cust_col = ?"; $params[] = $_GET['customer_id']; }
if (!empty($_GET['start_date'])) { $where[] = "v.invoice_date >= ?"; $params[] = $_GET['start_date']; }
if (!empty($_GET['end_date'])) { $where[] = "v.invoice_date <= ?"; $params[] = $_GET['end_date']; }
$whereSql = implode(" AND ", $where);
$stmt = db()->prepare("SELECT v.id, c.name as customer_name, v.invoice_date, v.payment_type, v.status, v.total_with_vat, v.paid_amount, (v.total_with_vat - v.paid_amount) as balance
FROM $table v LEFT JOIN $cust_table c ON v.$cust_col = c.id
WHERE $whereSql ORDER BY v.id DESC");
$stmt->execute($params);
$headers = ['Invoice ID', 'Customer/Supplier', 'Date', 'Payment', 'Status', 'Total', 'Paid', 'Balance'];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row;
} elseif ($type === 'customers' || $type === 'suppliers') {
$table = ($type === 'suppliers') ? 'suppliers' : 'customers';
$taxColumn = entity_tax_column($table);
$where = ["1=1"];
$params = [];
if (!empty($_GET['search'])) {
$taxSearch = $taxColumn !== null ? " OR $taxColumn LIKE ?" : '';
$where[] = "(name LIKE ? OR email LIKE ? OR phone LIKE ?$taxSearch)";
$params[] = "%{$_GET['search']}%";
$params[] = "%{$_GET['search']}%";
$params[] = "%{$_GET['search']}%";
if ($taxColumn !== null) {
$params[] = "%{$_GET['search']}%";
}
}
if (!empty($_GET['start_date'])) { $where[] = "DATE(created_at) >= ?"; $params[] = $_GET['start_date']; }
if (!empty($_GET['end_date'])) { $where[] = "DATE(created_at) <= ?"; $params[] = $_GET['end_date']; }
$whereSql = implode(" AND ", $where);
$taxSelect = $taxColumn !== null ? "$taxColumn AS tax_id" : "'' AS tax_id";
$stmt = db()->prepare("SELECT id, name, email, phone, $taxSelect, balance, created_at FROM $table WHERE $whereSql ORDER BY id DESC");
$stmt->execute($params);
$headers = ['ID', 'Name', 'Email', 'Phone', 'Tax ID', 'Balance', 'Created At'];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row;
} elseif ($type === 'items') {
$where = ["1=1"];
$params = [];
if (!empty($_GET['search'])) { $where[] = "(i.name_en LIKE ? OR i.name_ar LIKE ? OR i.sku LIKE ?)"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; $params[] = "%{$_GET['search']}%"; }
$whereSql = implode(" AND ", $where);
$stmt = db()->prepare("SELECT i.sku, i.name_en, i.name_ar, c.name_en as category, i.purchase_price, i.sale_price, i.stock_quantity, i.vat_rate
FROM stock_items i LEFT JOIN stock_categories c ON i.category_id = c.id
WHERE $whereSql ORDER BY i.id DESC");
$stmt->execute($params);
$headers = ['SKU', 'Name (EN)', 'Name (AR)', 'Category', 'Purchase Price', 'Sale Price', 'Quantity', 'VAT %'];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row;
} elseif ($type === 'expenses') {
$where = ["1=1"];
$params = [];
$stmt = db()->prepare("SELECT e.id, c.name_en as category, e.amount, e.expense_date, e.reference_no, e.description
FROM expenses e JOIN expense_categories c ON e.category_id = c.id
ORDER BY e.expense_date DESC");
$stmt->execute();
$headers = ['ID', 'Category', 'Amount', 'Date', 'Reference', 'Description'];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row;
} elseif ($type === 'quotations') {
$stmt = db()->prepare("SELECT q.id, c.name as customer_name, q.quotation_date, q.total_with_vat, q.status
FROM quotations q JOIN customers c ON q.customer_id = c.id
ORDER BY q.id DESC");
$stmt->execute();
$headers = ['Quotation #', 'Customer', 'Date', 'Total', 'Status'];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row;
} elseif ($type === 'lpos') {
$stmt = db()->prepare("SELECT q.id, s.name as supplier_name, q.lpo_date, q.total_with_vat, q.status
FROM lpos q JOIN suppliers s ON q.supplier_id = s.id
ORDER BY q.id DESC");
$stmt->execute();
$headers = ['LPO #', 'Supplier', 'Date', 'Total', 'Status'];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row;
} elseif ($type === 'categories') {
$stmt = db()->prepare("SELECT id, name_en, name_ar FROM stock_categories WHERE outlet_id = ? ORDER BY id DESC");
$stmt->execute([current_outlet_id()]);
$headers = ['ID', 'Name (EN)', 'Name (AR)'];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row;
} elseif ($type === 'units') {
$stmt = db()->prepare("SELECT id, name_en, name_ar FROM stock_units WHERE outlet_id = ? ORDER BY id DESC");
$stmt->execute([current_outlet_id()]);
$headers = ['ID', 'Name (EN)', 'Name (AR)'];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row;
} elseif ($type === 'sales_returns') {
$salesReturnReferenceColumn = sales_return_reference_column();
$stmt = db()->query("SELECT sr.id, sr.`{$salesReturnReferenceColumn}` AS invoice_id, c.name as customer, sr.return_date, sr.total_amount FROM sales_returns sr LEFT JOIN customers c ON sr.customer_id = c.id ORDER BY sr.id DESC");
$headers = ['Return ID', 'Invoice ID', 'Customer', 'Date', 'Amount'];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row;
} elseif ($type === 'purchase_returns') {
$purchaseReturnReferenceColumn = purchase_return_reference_column();
$stmt = db()->query("SELECT pr.id, pr.`{$purchaseReturnReferenceColumn}` AS purchase_id, s.name as supplier, pr.return_date, pr.total_amount FROM purchase_returns pr LEFT JOIN suppliers s ON pr.supplier_id = s.id ORDER BY pr.id DESC");
$headers = ['Return ID', 'Purchase ID', 'Supplier', 'Date', 'Amount'];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row;
} elseif ($type === 'hr_employees') {
$stmt = db()->query("SELECT e.id, e.name, d.name as department, e.position, e.salary, e.status FROM hr_employees e LEFT JOIN hr_departments d ON e.department_id = d.id ORDER BY e.id DESC");
$headers = ['ID', 'Name', 'Department', 'Position', 'Salary', 'Status'];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row;
} elseif ($type === 'hr_departments') {
$stmt = db()->query("SELECT id, name FROM hr_departments ORDER BY id DESC");
$headers = ['ID', 'Name'];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row;
} elseif ($type === 'hr_attendance') {
$stmt = db()->query("SELECT a.attendance_date, e.name, a.status, a.clock_in, a.clock_out FROM hr_attendance a JOIN hr_employees e ON a.employee_id = e.id ORDER BY a.attendance_date DESC, e.name ASC");
$headers = ['Date', 'Employee', 'Status', 'In', 'Out'];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row;
} elseif ($type === 'hr_payroll') {
$stmt = db()->query("SELECT p.payroll_month, p.payroll_year, e.name, p.basic_salary, p.bonus, p.deductions, p.net_salary, p.status FROM hr_payroll p JOIN hr_employees e ON p.employee_id = e.id ORDER BY p.payroll_year DESC, p.payroll_month DESC");
$headers = ['Month', 'Year', 'Employee', 'Salary', 'Bonus', 'Deductions', 'Net', 'Status'];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row;
} elseif ($type === 'users') {
$stmt = db()->query("SELECT u.id, u.username, u.email, g.name as role, u.status FROM users u LEFT JOIN role_groups g ON u.group_id = g.id ORDER BY u.id DESC");
$headers = ['ID', 'Username', 'Email', 'Role', 'Status'];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) $rows[] = $row;
}
if ($format === 'excel') {
echo "";
foreach ($headers as $h) echo "" . htmlspecialchars($h) . " ";
echo " ";
foreach ($rows as $row) {
echo "";
foreach ($row as $val) echo "" . htmlspecialchars((string)$val) . " ";
echo " ";
}
echo "
";
} else {
fputcsv($output, $headers);
foreach ($rows as $row) fputcsv($output, $row);
fclose($output);
}
exit;
}
// Global data for modals
$current_oid = current_outlet_id();
$stmt = db()->prepare("SELECT * FROM stock_categories WHERE outlet_id = ? ORDER BY name_en ASC");
$stmt->execute([$current_oid]);
$data['categories'] = $stmt->fetchAll();
$stmt = db()->prepare("SELECT * FROM stock_units WHERE outlet_id = ? ORDER BY name_en ASC");
$stmt->execute([$current_oid]);
$data['units'] = $stmt->fetchAll();
$stmt = db()->prepare("SELECT * FROM suppliers WHERE outlet_id = ? ORDER BY name ASC");
$stmt->execute([$current_oid]);
$data['suppliers'] = $stmt->fetchAll();
$data['accounts'] = db()->query("SELECT * FROM acc_accounts ORDER BY code ASC")->fetchAll();
$data['customers_list'] = db()->query("SELECT * FROM customers ORDER BY name ASC")->fetchAll();
$customers = $data['customers_list']; // For backward compatibility in some modals
$settings_raw = db()->query("SELECT * FROM settings")->fetchAll();
$data['settings'] = [];
foreach ($settings_raw as $s) {
$data['settings'][$s['key']] = $s['value'];
}
// Fetch current outlet name
$oid = current_outlet_id();
if ($oid != -1) {
$stmt = db()->prepare("SELECT name FROM outlets WHERE id = ?");
$stmt->execute([$oid]);
$outlet_name = $stmt->fetchColumn();
if ($outlet_name) {
$data['settings']['current_outlet_name'] = $outlet_name;
}
}
$limit = isset($_GET["limit"]) ? max(5, (int)$_GET["limit"]) : 20;
$page_num = isset($_GET["p"]) ? (int)$_GET["p"] : 1;
if ($page_num < 1) $page_num = 1;
$offset = ($page_num - 1) * $limit;
switch ($page) {
case 'suppliers':
$supplierTaxColumn = entity_tax_column('suppliers');
$where = ["outlet_id = " . current_outlet_id()];
$params = [];
if (!empty($_GET['search'])) {
$taxSearch = $supplierTaxColumn !== null ? " OR $supplierTaxColumn LIKE ?" : '';
$where[] = "(name LIKE ? OR email LIKE ? OR phone LIKE ?$taxSearch)";
$params[] = "%{$_GET['search']}%";
$params[] = "%{$_GET['search']}%";
$params[] = "%{$_GET['search']}%";
if ($supplierTaxColumn !== null) {
$params[] = "%{$_GET['search']}%";
}
}
if (!empty($_GET['start_date'])) {
$where[] = "DATE(created_at) >= ?";
$params[] = $_GET['start_date'];
}
if (!empty($_GET['end_date'])) {
$where[] = "DATE(created_at) <= ?";
$params[] = $_GET['end_date'];
}
$whereSql = implode(" AND ", $where);
$countStmt = db()->prepare("SELECT COUNT(*) FROM suppliers WHERE $whereSql");
$countStmt->execute($params);
$total_records = (int)$countStmt->fetchColumn();
$data['total_pages'] = ceil($total_records / $limit);
$data['current_page'] = $page_num;
$stmt = db()->prepare("SELECT * FROM suppliers WHERE $whereSql ORDER BY id DESC LIMIT $limit OFFSET $offset");
$stmt->execute($params);
$data['customers'] = $stmt->fetchAll(); // Keep 'customers' key for template compatibility if needed, or update template
break;
case 'customers':
$customerTaxColumn = entity_tax_column('customers');
$where = ["1=1"];
$params = [];
if (!empty($_GET['search'])) {
$taxSearch = $customerTaxColumn !== null ? " OR $customerTaxColumn LIKE ?" : '';
$where[] = "(name LIKE ? OR email LIKE ? OR phone LIKE ?$taxSearch)";
$params[] = "%{$_GET['search']}%";
$params[] = "%{$_GET['search']}%";
$params[] = "%{$_GET['search']}%";
if ($customerTaxColumn !== null) {
$params[] = "%{$_GET['search']}%";
}
}
if (!empty($_GET['start_date'])) {
$where[] = "DATE(created_at) >= ?";
$params[] = $_GET['start_date'];
}
if (!empty($_GET['end_date'])) {
$where[] = "DATE(created_at) <= ?";
$params[] = $_GET['end_date'];
}
$whereSql = implode(" AND ", $where);
$countStmt = db()->prepare("SELECT COUNT(*) FROM customers WHERE $whereSql");
$countStmt->execute($params);
$total_records = (int)$countStmt->fetchColumn();
$data['total_pages'] = ceil($total_records / $limit);
$data['current_page'] = $page_num;
$stmt = db()->prepare("SELECT * FROM customers WHERE $whereSql ORDER BY id DESC LIMIT $limit OFFSET $offset");
$stmt->execute($params);
$data['customers'] = $stmt->fetchAll();
break;
case 'categories':
// Already fetched globally
break;
case 'units':
// Already fetched globally
break;
case 'items':
file_put_contents('debug.log', date('Y-m-d H:i:s') . " - Items case hit\n", FILE_APPEND);
$where = ["i.outlet_id = " . current_outlet_id()];
$params = [];
if (!empty($_GET['search'])) {
$where[] = "(i.name_en LIKE ? OR i.name_ar LIKE ? OR i.sku LIKE ?)";
$params[] = "%{$_GET['search']}%";
$params[] = "%{$_GET['search']}%";
$params[] = "%{$_GET['search']}%";
}
$whereSql = implode(" AND ", $where);
$countStmt = db()->prepare("SELECT COUNT(*) FROM stock_items i
LEFT JOIN stock_categories c ON i.category_id = c.id
LEFT JOIN stock_units u ON i.unit_id = u.id
LEFT JOIN suppliers s ON i.supplier_id = s.id WHERE $whereSql");
$countStmt->execute($params);
$total_records = (int)$countStmt->fetchColumn();
$data['total_pages'] = ceil($total_records / $limit);
$data['current_page'] = $page_num;
$oid = current_outlet_id();
$stmt = db()->prepare("SELECT i.*, i.stock_quantity, c.name_en as cat_en, c.name_ar as cat_ar, COALESCE(NULLIF(u.name_en, ''), u.short_name_en) as unit_en, COALESCE(NULLIF(u.name_ar, ''), u.short_name_ar, u.name_en, u.short_name_en) as unit_ar, s.name as supplier_name
FROM stock_items i
LEFT JOIN stock_categories c ON i.category_id = c.id
LEFT JOIN stock_units u ON i.unit_id = u.id
LEFT JOIN suppliers s ON i.supplier_id = s.id
WHERE $whereSql
ORDER BY i.id DESC LIMIT $limit OFFSET $offset");
$stmt->execute($params);
$data['items'] = $stmt->fetchAll();
break;
case 'quotations':
$where = ["1=1"];
$params = [];
if (!empty($_GET['search'])) {
$s = $_GET['search'];
$clean_id = preg_replace('/[^0-9]/', '', $s);
if ($clean_id !== '') {
$where[] = "(q.id LIKE ? OR c.name LIKE ? OR q.id = ?)";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = $clean_id;
} else {
$where[] = "(q.id LIKE ? OR c.name LIKE ?)";
$params[] = "%$s%";
$params[] = "%$s%";
}
}
if (!empty($_GET['customer_id'])) {
$where[] = "q.customer_id = ?";
$params[] = $_GET['customer_id'];
}
if (!empty($_GET['start_date'])) {
$where[] = "q.quotation_date >= ?";
$params[] = $_GET['start_date'];
}
if (!empty($_GET['end_date'])) {
$where[] = "q.quotation_date <= ?";
$params[] = $_GET['end_date'];
}
$whereSql = implode(" AND ", $where);
$countStmt = db()->prepare("SELECT COUNT(*) FROM quotations q JOIN customers c ON q.customer_id = c.id WHERE $whereSql");
$countStmt->execute($params);
$total_records = (int)$countStmt->fetchColumn();
$data['total_pages'] = ceil($total_records / $limit);
$data['current_page'] = $page_num;
$stmt = db()->prepare("SELECT q.*, c.name as customer_name
FROM quotations q
JOIN customers c ON q.customer_id = c.id
WHERE $whereSql
ORDER BY q.id DESC
LIMIT $limit OFFSET $offset");
$stmt->execute($params);
$data['quotations'] = $stmt->fetchAll();
break;
case 'lpos':
$where = ["1=1"];
$params = [];
if (!empty($_GET['search'])) {
$s = $_GET['search'];
$clean_id = preg_replace('/[^0-9]/', '', $s);
if ($clean_id !== '') {
$where[] = "(q.id LIKE ? OR s.name LIKE ? OR q.id = ?)";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = $clean_id;
} else {
$where[] = "(q.id LIKE ? OR s.name LIKE ?)";
$params[] = "%$s%";
$params[] = "%$s%";
}
}
if (!empty($_GET['supplier_id'])) {
$where[] = "q.supplier_id = ?";
$params[] = $_GET['supplier_id'];
}
if (!empty($_GET['start_date'])) {
$where[] = "q.lpo_date >= ?";
$params[] = $_GET['start_date'];
}
if (!empty($_GET['end_date'])) {
$where[] = "q.lpo_date <= ?";
$params[] = $_GET['end_date'];
}
$whereSql = implode(" AND ", $where);
$countStmt = db()->prepare("SELECT COUNT(*) FROM lpos q JOIN suppliers s ON q.supplier_id = s.id WHERE $whereSql");
$countStmt->execute($params);
$total_records = (int)$countStmt->fetchColumn();
$data['total_pages'] = ceil($total_records / $limit);
$data['current_page'] = $page_num;
$stmt = db()->prepare("SELECT q.*, s.name as supplier_name
FROM lpos q
JOIN suppliers s ON q.supplier_id = s.id
WHERE $whereSql
ORDER BY q.id DESC
LIMIT $limit OFFSET $offset");
$stmt->execute($params);
$data['lpos'] = $stmt->fetchAll();
break;
case 'payment_methods':
$data['payment_methods'] = db()->query("SELECT * FROM payment_methods ORDER BY id DESC")->fetchAll();
break;
case 'outlets':
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_SESSION['user_role_name'] ?? '') === 'Administrator') {
if (isset($_POST['add_outlet'])) {
$name = trim($_POST['name'] ?? '');
$address = trim($_POST['address'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$status = $_POST['status'] ?? 'active';
if ($name) {
$stmt = db()->prepare("INSERT INTO outlets (name, address, phone, status, created_at) VALUES (?, ?, ?, ?, NOW())");
$stmt->execute([$name, $address, $phone, $status]);
}
} elseif (isset($_POST['edit_outlet'])) {
$id = (int)$_POST['id'];
$name = trim($_POST['name'] ?? '');
$address = trim($_POST['address'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$status = $_POST['status'] ?? 'active';
if ($name && $id) {
$stmt = db()->prepare("UPDATE outlets SET name = ?, address = ?, phone = ?, status = ? WHERE id = ?");
$stmt->execute([$name, $address, $phone, $status, $id]);
}
} elseif (isset($_POST['delete_outlet'])) {
$id = (int)$_POST['id'];
if ($id !== 1) {
db()->prepare("DELETE FROM outlets WHERE id = ?")->execute([$id]);
}
}
header("Location: index.php?page=outlets");
exit;
}
$countStmt = db()->query("SELECT COUNT(*) FROM outlets");
$total_records = (int)$countStmt->fetchColumn();
$data['total_pages'] = ceil($total_records / $limit);
$data['current_page'] = $page_num;
$stmt = db()->prepare("SELECT * FROM outlets ORDER BY id ASC LIMIT $limit OFFSET $offset");
$stmt->execute();
$data['outlets'] = $stmt->fetchAll();
break;
case 'copy_outlet_data': require 'pages/copy_outlet_data_logic.php'; break;
case 'settings':
// Already fetched globally
break;
case 'my_profile':
$stmt = db()->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
$data['user'] = $stmt->fetch();
break;
case 'sales':
case 'purchases':
$type = ($page === 'sales') ? 'sale' : 'purchase';
$table = ($type === 'purchase') ? 'purchases' : 'invoices';
$cust_supplier_col = ($type === 'purchase') ? 'supplier_id' : 'customer_id';
$cust_supplier_table = ($type === 'purchase') ? 'suppliers' : 'customers';
$where = ["1=1"];
$params = [];
if (!empty($_GET['search'])) {
$s = $_GET['search'];
$clean_id = preg_replace('/[^0-9]/', '', $s);
if ($clean_id !== '') {
$where[] = "(v.id LIKE ? OR c.name LIKE ? OR v.id = ?)";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = $clean_id;
} else {
$where[] = "(v.id LIKE ? OR c.name LIKE ?)";
$params[] = "%$s%";
$params[] = "%$s%";
}
}
if (!empty($_GET['customer_id'])) {
$where[] = "v.$cust_supplier_col = ?";
$params[] = $_GET['customer_id'];
}
if (!empty($_GET['start_date'])) {
$where[] = "v.invoice_date >= ?";
$params[] = $_GET['start_date'];
}
if (!empty($_GET['end_date'])) {
$where[] = "v.invoice_date <= ?";
$params[] = $_GET['end_date'];
}
$oid = current_outlet_id();
if ($oid !== -1) {
$where[] = "v.outlet_id = ?";
$params[] = $oid;
}
$whereSql = implode(" AND ", $where);
$countStmt = db()->prepare("SELECT COUNT(*) FROM $table v LEFT JOIN $cust_supplier_table c ON v.$cust_supplier_col = c.id WHERE $whereSql");
$countStmt->execute($params);
$total_records = (int)$countStmt->fetchColumn();
$data['total_pages'] = ceil($total_records / $limit);
$data['current_page'] = $page_num;
$customerTaxColumn = entity_tax_column($cust_supplier_table);
$customerTaxSelect = $customerTaxColumn !== null ? "c.$customerTaxColumn" : "''";
$stmt = db()->prepare("SELECT v.*, c.name as customer_name, $customerTaxSelect as customer_tax_id, c.phone as customer_phone, o.name as outlet_name
FROM $table v
LEFT JOIN $cust_supplier_table c ON v.$cust_supplier_col = c.id
LEFT JOIN outlets o ON v.outlet_id = o.id
WHERE $whereSql
ORDER BY v.id DESC LIMIT $limit OFFSET $offset");
$stmt->execute($params);
$data['invoices'] = $stmt->fetchAll();
foreach ($data['invoices'] as &$inv) {
$inv['total_in_words'] = numberToWordsOMR($inv['total_with_vat']);
if ($type === 'sale') {
$item_stmt = db()->prepare("SELECT ii.*, i.name_en, i.name_ar, i.vat_rate FROM invoice_items ii LEFT JOIN stock_items i ON ii.item_id = i.id WHERE ii.invoice_id = ?");
$item_stmt->execute([$inv['id']]);
$inv['items'] = $item_stmt->fetchAll(PDO::FETCH_ASSOC);
} else {
$item_stmt = db()->prepare("SELECT pi.*, i.name_en, i.name_ar, i.vat_rate FROM purchase_items pi LEFT JOIN stock_items i ON pi.item_id = i.id WHERE pi.purchase_id = ?");
$item_stmt->execute([$inv['id']]);
$inv['items'] = $item_stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
unset($inv);
$oid = current_outlet_id(); $items_list_raw = db()->query("SELECT i.id, i.name_en, i.name_ar, i.sale_price, i.purchase_price, i.stock_quantity, i.vat_rate, i.is_promotion, i.promotion_start, i.promotion_end, i.promotion_percent FROM stock_items i ORDER BY i.name_en ASC")->fetchAll(PDO::FETCH_ASSOC);
foreach ($items_list_raw as &$item) {
$item['sale_price'] = getPromotionalPrice($item);
}
$data['items_list'] = $items_list_raw;
$data['customers_list'] = db()->query("SELECT id, name FROM $cust_supplier_table ORDER BY name ASC")->fetchAll();
$oid = current_outlet_id();
$outlet_sql = ($oid !== -1) ? "WHERE outlet_id = $oid" : "";
if ($type === 'sale') {
$data['sales_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices $outlet_sql ORDER BY id DESC")->fetchAll();
} else {
$data['purchase_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM purchases $outlet_sql ORDER BY id DESC")->fetchAll();
}
break;
case 'sales_returns':
$salesReturnReferenceColumn = sales_return_reference_column();
$where = ["1=1"];
$params = [];
if (!empty($_GET['search'])) {
$s = $_GET['search'];
$clean_id = preg_replace('/[^0-9]/', '', $s);
if ($clean_id !== '') {
$where[] = "(sr.id LIKE ? OR c.name LIKE ? OR sr.`{$salesReturnReferenceColumn}` LIKE ? OR sr.id = ? OR sr.`{$salesReturnReferenceColumn}` = ?)";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = $clean_id;
$params[] = $clean_id;
} else {
$where[] = "(sr.id LIKE ? OR c.name LIKE ? OR sr.`{$salesReturnReferenceColumn}` LIKE ?)";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = "%$s%";
}
}
$whereSql = implode(" AND ", $where);
$stmt = db()->prepare("SELECT sr.*, sr.`{$salesReturnReferenceColumn}` AS invoice_id, c.name as customer_name, i.total_with_vat as invoice_total
FROM sales_returns sr
LEFT JOIN customers c ON sr.customer_id = c.id
LEFT JOIN invoices i ON sr.`{$salesReturnReferenceColumn}` = i.id
WHERE $whereSql
ORDER BY sr.id DESC");
$stmt->execute($params);
$data['returns'] = $stmt->fetchAll();
$data['sales_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM invoices ORDER BY id DESC")->fetchAll();
break;
case 'purchase_returns':
$purchaseReturnReferenceColumn = purchase_return_reference_column();
$where = ["1=1"];
$params = [];
if (!empty($_GET['search'])) {
$s = $_GET['search'];
$clean_id = preg_replace('/[^0-9]/', '', $s);
if ($clean_id !== '') {
$where[] = "(pr.id LIKE ? OR c.name LIKE ? OR pr.`{$purchaseReturnReferenceColumn}` LIKE ? OR pr.id = ? OR pr.`{$purchaseReturnReferenceColumn}` = ?)";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = $clean_id;
$params[] = $clean_id;
} else {
$where[] = "(pr.id LIKE ? OR c.name LIKE ? OR pr.`{$purchaseReturnReferenceColumn}` LIKE ?)";
$params[] = "%$s%";
$params[] = "%$s%";
$params[] = "%$s%";
}
}
$whereSql = implode(" AND ", $where);
$stmt = db()->prepare("SELECT pr.*, pr.`{$purchaseReturnReferenceColumn}` AS purchase_id, c.name as supplier_name, i.total_with_vat as invoice_total
FROM purchase_returns pr
LEFT JOIN suppliers c ON pr.supplier_id = c.id
LEFT JOIN purchases i ON pr.`{$purchaseReturnReferenceColumn}` = i.id
WHERE $whereSql
ORDER BY pr.id DESC");
$stmt->execute($params);
$data['returns'] = $stmt->fetchAll();
$data['purchase_invoices'] = db()->query("SELECT id, invoice_date, total_with_vat FROM purchases ORDER BY id DESC")->fetchAll();
break;
case 'customer_statement':
case 'supplier_statement':
$isCustomer = ($page === 'customer_statement');
$entityTable = $isCustomer ? 'customers' : 'suppliers';
$invoiceTable = $isCustomer ? 'invoices' : 'purchases';
$paymentTable = $isCustomer ? 'payments' : 'purchase_payments';
$fkColumn = $isCustomer ? 'customer_id' : 'supplier_id';
$invFkColumn = $isCustomer ? 'invoice_id' : 'purchase_id';
$data['entities'] = db()->query("SELECT id, name, balance FROM $entityTable ORDER BY name ASC")->fetchAll();
$entity_id = (int)($_GET['entity_id'] ?? 0);
if ($entity_id) {
$data['selected_entity'] = db()->query("SELECT * FROM $entityTable WHERE id = $entity_id")->fetch();
$start_date = $_GET['start_date'] ?? date('Y-m-01');
$end_date = $_GET['end_date'] ?? date('Y-m-d');
$stmt = db()->prepare("SELECT 'invoice' as trans_type, id, invoice_date as trans_date, total_with_vat as amount, status, id as ref_no
FROM $invoiceTable
WHERE $fkColumn = ? AND invoice_date BETWEEN ? AND ?");
$stmt->execute([$entity_id, $start_date, $end_date]);
$invoices = $stmt->fetchAll(PDO::FETCH_ASSOC);
$stmt = db()->prepare("SELECT 'payment' as trans_type, p.id, p.payment_date as trans_date, p.amount, p.payment_method, p.$invFkColumn as ref_no
FROM $paymentTable p
JOIN $invoiceTable i ON p.$invFkColumn = i.id
WHERE i.$fkColumn = ? AND p.payment_date BETWEEN ? AND ?");
$stmt->execute([$entity_id, $start_date, $end_date]);
$payments = $stmt->fetchAll(PDO::FETCH_ASSOC);
$transactions = array_merge($invoices, $payments);
usort($transactions, function($a, $b) {
return strtotime($a['trans_date']) <=> strtotime($b['trans_date']);
});
$data['transactions'] = $transactions;
}
break;
case 'expense_categories':
$data['expense_categories'] = db()->query("SELECT * FROM expense_categories ORDER BY name_en ASC")->fetchAll();
break;
case 'expenses':
$where = ["1=1"];
$params = [];
if (!empty($_GET['category_id'])) {
$where[] = "e.category_id = ?";
$params[] = $_GET['category_id'];
}
if (!empty($_GET['start_date'])) {
$where[] = "e.expense_date >= ?";
$params[] = $_GET['start_date'];
}
if (!empty($_GET['end_date'])) {
$where[] = "e.expense_date <= ?";
$params[] = $_GET['end_date'];
}
$whereSql = implode(" AND ", $where);
$stmt = db()->prepare("SELECT e.*, c.name_en as cat_en, c.name_ar as cat_ar
FROM expenses e
LEFT JOIN expense_categories c ON e.category_id = c.id
WHERE $whereSql
ORDER BY e.expense_date DESC, e.id DESC");
$stmt->execute($params);
$data['expenses'] = $stmt->fetchAll();
break;
case 'role_groups':
$data['role_groups'] = db()->query("SELECT * FROM role_groups ORDER BY name ASC")->fetchAll();
break;
case 'users':
require 'pages/users_logic.php';
break;
case 'backups':
$data['backups'] = BackupService::getBackups();
$stmt = db()->prepare("SELECT * FROM settings WHERE `key` IN ('backup_limit', 'backup_auto_enabled', 'backup_time')");
$stmt->execute();
$data['backup_settings'] = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
break;
case 'accounting':
require 'pages/accounting_logic.php';
break;
case 'expense_report':
$start_date = $_GET['start_date'] ?? date('Y-m-01');
$end_date = $_GET['end_date'] ?? date('Y-m-d');
$category_id = $_GET['category_id'] ?? '';
$where = "WHERE e.expense_date BETWEEN ? AND ?";
$params = [$start_date, $end_date];
if ($category_id !== '') {
$where .= " AND e.category_id = ?";
$params[] = $category_id;
}
$stmt = db()->prepare("SELECT c.name_en, c.name_ar, SUM(e.amount) as total
FROM expenses e
JOIN expense_categories c ON e.category_id = c.id
$where
GROUP BY c.id
ORDER BY total DESC");
$stmt->execute($params);
$data['report_by_category'] = $stmt->fetchAll();
$stmt = db()->prepare("SELECT SUM(amount) FROM expenses e $where");
$stmt->execute($params);
$data['total_expenses'] = $stmt->fetchColumn() ?: 0;
$data['expense_categories'] = db()->query("SELECT * FROM expense_categories ORDER BY name_en ASC")->fetchAll();
break;
case 'expiry_report':
$where = ["expiry_date IS NOT NULL"];
$params = [];
$filter = $_GET['filter'] ?? 'all';
if ($filter === 'expired') {
$where[] = "expiry_date <= CURDATE()";
} elseif ($filter === 'near_expiry') {
$where[] = "expiry_date > CURDATE() AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)";
}
$whereSql = implode(" AND ", $where);
$stmt = db()->prepare("SELECT i.*, c.name_en as cat_en, c.name_ar as cat_ar
FROM stock_items i
LEFT JOIN stock_categories c ON i.category_id = c.id
WHERE $whereSql
ORDER BY i.expiry_date ASC");
$stmt->execute($params);
$data['expiry_items'] = $stmt->fetchAll();
break;
case 'low_stock_report':
$oid = current_outlet_id();
$stmt = db()->prepare("SELECT i.*, i.stock_quantity, c.name_en as cat_en, c.name_ar as cat_ar, s.name as supplier_name
FROM stock_items i
LEFT JOIN stock_categories c ON i.category_id = c.id
LEFT JOIN suppliers s ON i.supplier_id = s.id
WHERE i.outlet_id = $oid AND i.stock_quantity <= i.min_stock_level
ORDER BY (i.min_stock_level - i.stock_quantity) DESC");
$stmt->execute();
$data['low_stock_items'] = $stmt->fetchAll();
break;
case 'cashflow_report':
$start_date = $_GET['start_date'] ?? date('Y-m-01');
$end_date = $_GET['end_date'] ?? date('Y-m-d');
// Fetch Cash & Bank Account IDs
$cash_accounts = db()->query("SELECT id FROM acc_accounts WHERE code IN (1100, 1200)")->fetchAll(PDO::FETCH_COLUMN);
$cash_ids_str = implode(',', $cash_accounts);
if (!empty($cash_ids_str)) {
// Opening Balance
$stmt = db()->prepare("SELECT SUM(debit - credit) FROM acc_ledger l JOIN acc_journal_entries je ON l.journal_entry_id = je.id WHERE l.account_id IN ($cash_ids_str) AND je.entry_date < ?");
$stmt->execute([$start_date]);
$data['opening_balance'] = $stmt->fetchColumn() ?: 0;
// Transactions in range
$stmt = db()->prepare("SELECT
je.entry_date,
je.description,
l.debit as inflow,
l.credit as outflow,
a.name_en as other_account,
a.type as other_type
FROM acc_ledger l
JOIN acc_journal_entries je ON l.journal_entry_id = je.id
LEFT JOIN acc_ledger l2 ON l2.journal_entry_id = je.id AND l2.id != l.id
LEFT JOIN acc_accounts a ON l2.account_id = a.id
WHERE l.account_id IN ($cash_ids_str)
AND je.entry_date BETWEEN ? AND ?
ORDER BY je.entry_date ASC, je.id ASC");
$stmt->execute([$start_date, $end_date]);
$data['cash_transactions'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
} else {
$data['opening_balance'] = 0;
$data['cash_transactions'] = [];
}
break;
case 'hr_departments':
$data['departments'] = db()->query("SELECT * FROM hr_departments ORDER BY id DESC")->fetchAll();
break;
case 'hr_employees':
$data['employees'] = db()->query("SELECT e.*, d.name as dept_name FROM hr_employees e LEFT JOIN hr_departments d ON e.department_id = d.id ORDER BY e.id DESC")->fetchAll();
$data['departments'] = db()->query("SELECT * FROM hr_departments ORDER BY name ASC")->fetchAll();
break;
case 'hr_attendance':
$date = $_GET['date'] ?? date('Y-m-d');
$data['attendance_date'] = $date;
$data['employees'] = db()->query("SELECT e.id, e.name, d.name as dept_name, a.status, a.clock_in, a.clock_out
FROM hr_employees e
LEFT JOIN hr_departments d ON e.department_id = d.id
LEFT JOIN hr_attendance a ON e.id = a.employee_id AND a.attendance_date = '$date'
WHERE e.status = 'active' ORDER BY e.name ASC")->fetchAll();
break;
case 'hr_payroll':
$month = (int)($_GET['month'] ?? date('m'));
$year = (int)($_GET['year'] ?? date('Y'));
$data['month'] = $month;
$data['year'] = $year;
$data['payroll'] = db()->query("SELECT p.*, e.name as emp_name FROM hr_payroll p JOIN hr_employees e ON p.employee_id = e.id WHERE p.payroll_month = $month AND p.payroll_year = $year ORDER BY p.id DESC")->fetchAll();
$data['employees'] = db()->query("SELECT id, name, salary FROM hr_employees WHERE status = 'active' ORDER BY name ASC")->fetchAll();
break;
case 'loyalty_history':
$where = ["1=1"];
$params = [];
if (!empty($_GET['customer_id'])) {
$where[] = "lt.customer_id = ?";
$params[] = (int)$_GET['customer_id'];
}
if (!empty($_GET['type'])) {
$where[] = "lt.transaction_type = ?";
$params[] = $_GET['type'];
}
$whereSql = implode(" AND ", $where);
$stmt = db()->prepare("SELECT lt.*, c.name as customer_name, c.loyalty_tier, c.loyalty_points
FROM loyalty_transactions lt
JOIN customers c ON lt.customer_id = c.id
WHERE $whereSql
ORDER BY lt.created_at DESC");
$stmt->execute($params);
$data['loyalty_transactions'] = $stmt->fetchAll();
break;
case 'devices':
$data['devices'] = db()->query("SELECT * FROM hr_biometric_devices ORDER BY id DESC")->fetchAll();
break;
case 'scale_devices':
$data['scale_devices'] = db()->query("SELECT * FROM pos_devices ORDER BY id DESC")->fetchAll();
break;
case 'cash_registers':
$data['cash_registers'] = db()->query("SELECT * FROM cash_registers ORDER BY id DESC")->fetchAll();
break;
case 'register_sessions':
$where = ["1=1"];
$params = [];
// Filter by user if provided and user has permission
if (isset($_GET['user_id']) && !empty($_GET['user_id'])) {
if (can('users_view')) {
$where[] = "s.user_id = ?";
$params[] = $_GET['user_id'];
}
}
if (!can('users_view')) {
$where[] = "s.user_id = ?";
$params[] = $_SESSION['user_id'];
}
// Filter by date range
if (isset($_GET['date_from']) && !empty($_GET['date_from'])) {
$where[] = "s.opened_at >= ?";
$params[] = $_GET['date_from'] . ' 00:00:00';
}
if (isset($_GET['date_to']) && !empty($_GET['date_to'])) {
$where[] = "s.opened_at <= ?";
$params[] = $_GET['date_to'] . ' 23:59:59';
}
$whereSql = implode(" AND ", $where);
$stmt = db()->prepare("SELECT s.*, r.name as register_name, u.username
FROM register_sessions s
LEFT JOIN cash_registers r ON s.register_id = r.id
LEFT JOIN users u ON s.user_id = u.id
WHERE $whereSql
ORDER BY s.id DESC");
$stmt->execute($params);
$data['sessions'] = $stmt->fetchAll();
$data['cash_registers'] = db()->query("SELECT * FROM cash_registers WHERE status = 'active'")->fetchAll();
$data['users'] = db()->query("SELECT id, username FROM users ORDER BY username ASC")->fetchAll();
break;
default:
if (can('dashboard_view')) {
$data['customers'] = db()->query("SELECT * FROM customers ORDER BY id DESC LIMIT 5")->fetchAll();
// Statistics with Outlet Filter
$current_oid = current_outlet_id();
$inv_cond = ($current_oid > 0) ? " WHERE outlet_id = $current_oid " : "";
$pos_cond = " WHERE status = 'completed' " . (($current_oid > 0) ? " AND outlet_id = $current_oid " : "");
$pay_inv_cond = ($current_oid > 0) ? " WHERE i.outlet_id = $current_oid " : "";
$pay_pos_cond = ($current_oid > 0) ? " WHERE t.outlet_id = $current_oid " : "";
$pur_cond = ($current_oid > 0) ? " WHERE outlet_id = $current_oid " : "";
$pay_pur_cond = ($current_oid > 0) ? " WHERE p.outlet_id = $current_oid " : "";
$low_stock_query = ($current_oid > 0)
? "SELECT COUNT(*) FROM stock_items WHERE outlet_id = $current_oid AND stock_quantity <= min_stock_level"
: "SELECT COUNT(*) FROM stock_items WHERE stock_quantity <= min_stock_level";
$data['stats'] = [
'total_customers' => db()->query("SELECT COUNT(*) FROM customers")->fetchColumn(),
'total_items' => db()->query("SELECT COUNT(*) FROM stock_items")->fetchColumn(),
'total_sales' => (db()->query("SELECT SUM(total_with_vat) FROM invoices $inv_cond")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(net_amount) FROM pos_transactions $pos_cond")->fetchColumn() ?: 0),
'total_received' => (db()->query("SELECT SUM(p.amount) FROM payments p JOIN invoices i ON p.invoice_id = i.id $pay_inv_cond")->fetchColumn() ?: 0) + (db()->query("SELECT SUM(pp.amount) FROM pos_payments pp JOIN pos_transactions t ON pp.transaction_id = t.id $pay_pos_cond")->fetchColumn() ?: 0),
'total_purchases' => db()->query("SELECT SUM(total_with_vat) FROM purchases $pur_cond")->fetchColumn() ?: 0,
'total_paid' => db()->query("SELECT SUM(pp.amount) FROM purchase_payments pp JOIN purchases p ON pp.purchase_id = p.id $pay_pur_cond")->fetchColumn() ?: 0,
'expired_items' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date <= CURDATE()")->fetchColumn(),
'near_expiry_items' => db()->query("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date > CURDATE() AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)")->fetchColumn(),
'low_stock_items_count' => db()->query($low_stock_query)->fetchColumn(),
];
$data['stats']['total_receivable'] = $data['stats']['total_sales'] - $data['stats']['total_received'];
$data['stats']['total_payable'] = $data['stats']['total_purchases'] - $data['stats']['total_paid'];
// Sales Chart Data
$data['monthly_sales'] = db()->query("SELECT DATE_FORMAT(invoice_date, '%M %Y') as label, SUM(total_with_vat) as total FROM invoices GROUP BY DATE_FORMAT(invoice_date, '%Y-%m') ORDER BY invoice_date ASC LIMIT 12")->fetchAll(PDO::FETCH_ASSOC);
$data['yearly_sales'] = db()->query("SELECT YEAR(invoice_date) as label, SUM(total_with_vat) as total FROM invoices GROUP BY label ORDER BY label ASC LIMIT 5")->fetchAll(PDO::FETCH_ASSOC);
}
break;
}
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
?>
= htmlspecialchars(__($page)) ?> - Admin Panel
0): ?>
= $lang === 'ar' ? "نسخة تجريبية: متبقي $trial_days يوم" : "Trial Version: $trial_days days remaining" ?>.
= __('activate_now') ?>
= __($page) ?>
= count($purchaseAlerts) ?>
= $lang === 'ar' ? 'تنبيهات المدفوعات' : 'Payment Alerts' ?>
= $lang === 'ar' ? 'المظهر' : 'Theme' ?>
= $lang === 'ar' ? 'English' : 'العربية' ?>
1 || $is_admin):
$current_oid = current_outlet_id();
$current_oname = $current_oid === -1 ? (__('All Outlets') ?: 'All Outlets') : (db()->query("SELECT name FROM outlets WHERE id = $current_oid")->fetchColumn() ?: 'Outlet ' . $current_oid);
?>
= htmlspecialchars($current_oname) ?>
= htmlspecialchars((string)($_SESSION['user_role_name'] ?? '')) ?>
0 || $data['stats']['near_expiry_items'] > 0 || $data['stats']['low_stock_items_count'] > 0 || $purchaseAlertsCount > 0): ?>
Administrative Alerts:
0): ?>
= $data['stats']['expired_items'] ?> items have expired.
0): ?>
= $data['stats']['near_expiry_items'] ?> items are expiring soon.
0): ?>
= $data['stats']['low_stock_items_count'] ?> items are below minimum level.
0): ?>
= $purchaseAlertsCount ?> purchase invoices are due or overdue.
= __('total_sales') ?>
OMR = number_format((float)($data['stats']['total_sales'] ?? 0), 3) ?>
= __('total_received') ?>
OMR = number_format((float)($data['stats']['total_received'] ?? 0), 3) ?>
= __('customer_due') ?>
OMR = number_format((float)($data['stats']['total_receivable'] ?? 0), 3) ?>
= __('total_purchases') ?>
OMR = number_format((float)($data['stats']['total_purchases'] ?? 0), 3) ?>
= __('total_paid') ?>
OMR = number_format((float)($data['stats']['total_paid'] ?? 0), 3) ?>
= __('supplier_due') ?>
OMR = number_format((float)($data['stats']['total_payable'] ?? 0), 3) ?>
= __('total_customers') ?>
= (int)($data['stats']['total_customers'] ?? 0) ?>
Total Items
= (int)($data['stats']['total_items'] ?? 0) ?>
Sales Performance
Monthly
Yearly
Recent Customers
View All
Name
Phone
Balance
= htmlspecialchars((string)($c['name'] ?? '')) ?>
= htmlspecialchars((string)($c['phone'] ?? '')) ?>
= $lang === 'ar' ? number_format((float)$c['balance'], 3) . ' ر.ع.' : 'OMR ' . number_format((float)$c['balance'], 3) ?>
= htmlspecialchars($data['settings']['company_name'] ?? 'Company Name') ?>
= $currTitle['en'] ?> Management
Name
Tax ID
Email
Phone
Balance
Actions
= htmlspecialchars((string)($c['name'] ?? '')) ?>
= htmlspecialchars((string)($c['tax_id'] ?? '---')) ?>
= htmlspecialchars((string)($c['email'] ?? '')) ?>
= htmlspecialchars((string)($c['phone'] ?? '')) ?>
= $lang === 'ar' ? number_format((float)$c['balance'], 3) . ' ر.ع.' : 'OMR ' . number_format((float)$c['balance'], 3) ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Stock Categories Management
Export
Import Excel
Add Category
ID
Name (EN)
Name (AR)
Actions
= $cat['id'] ?>
= htmlspecialchars($cat['name_en']) ?>
= htmlspecialchars($cat['name_ar']) ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Units (= count($data['units'] ?? []) ?>)
Manage the English and Arabic unit names used across items, invoices, and reports.
Name (EN)
Name (AR)
Actions
No units yet
Add your first unit so items can reuse the same labels in invoices and reports.
Create first unit
= htmlspecialchars($unitNameEn !== '' ? $unitNameEn : '---') ?>
= htmlspecialchars($unitNameAr !== '' ? $unitNameAr : '---') ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Stock Items (= count($data['items'] ?? []) ?>)
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
SKU
Item Name
Category
Stock Level
Expiry Date
Status
No items found.
= htmlspecialchars($item['sku']) ?>
= htmlspecialchars($item['name_en']) ?>
= htmlspecialchars($item['name_ar']) ?>
= htmlspecialchars($item['cat_en'] ?? '---') ?>
= number_format((float)$item['stock_quantity'], 3) ?>
= $expiry_date !== '' ? htmlspecialchars((string)$expiry_date) : '---' ?>
Expired
Near Expiry
Good
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Low Stock Report
Print
SKU
Item Name
Category
Supplier
Min Level
Current Stock
Shortage
All items are above minimum levels.
= htmlspecialchars($item['sku']) ?>
= htmlspecialchars($item['name_en']) ?>
= htmlspecialchars($item['name_ar']) ?>
= htmlspecialchars($item['cat_en'] ?? '---') ?>
= htmlspecialchars($item['supplier_name'] ?? '---') ?>
= number_format((float)$item['min_stock_level'], 2) ?>
= number_format((float)$item['stock_quantity'], 3) ?>
= number_format((float)$shortage, 3) ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Loyalty Transaction History
Print
Date
Customer
Tier
Type
Points
Description
No transactions found.
= date('Y-m-d H:i', strtotime($lt['created_at'])) ?>
= htmlspecialchars($lt['customer_name']) ?>
Current Balance: = number_format($lt['loyalty_points'], 0) ?> pts
= $tier ?>
= ucfirst($type) ?>
= (float)$lt['points_change'] > 0 ? '+' : '' ?>= number_format($lt['points_change'], 0) ?>
= htmlspecialchars($lt['description']) ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
prepare("SELECT * FROM register_sessions WHERE user_id = ? AND status = 'open'");
$stmt->execute([$_SESSION['user_id']]);
$active_session = $stmt->fetch(PDO::FETCH_ASSOC);
$_SESSION['register_session_id'] = $active_session['id'] ?? null;
$registers = db()->query("SELECT * FROM cash_registers WHERE status = 'active'")->fetchAll();
$allow_zero_stock_sell = ($data['settings']['allow_zero_stock_sell'] ?? '1') === '1';
$oid = current_outlet_id();
$sql = "SELECT * FROM stock_items WHERE outlet_id = $oid ORDER BY name_en ASC LIMIT 100";
$products_raw = db()->query($sql)->fetchAll(PDO::FETCH_ASSOC);
$products = [];
foreach ($products_raw as $p) {
$p['original_price'] = (float)$p['sale_price'];
$p['sale_price'] = getPromotionalPrice($p);
$products[] = $p;
}
$customers = db()->query("SELECT * FROM customers ORDER BY name ASC")->fetchAll(PDO::FETCH_ASSOC);
?>
= htmlspecialchars($p['name_ar']) ?>
= htmlspecialchars($p['name_en']) ?>
= htmlspecialchars($p['name_en']) ?>
OMR = number_format($p['original_price'], 3) ?>
OMR = number_format((float)$p['sale_price'], 3) ?>
= (float)$p['stock_quantity'] ?> left
Cart
Customer Screen
Close
Customer
Walk-in Customer
= htmlspecialchars($c['name']) ?>
Spend more to unlock Silver
Subtotal (Excl. VAT)
= __('currency') ?> 0.000
VAT
= __('currency') ?> 0.000
Total
= __('currency') ?> 0.000
PLACE ORDER
Quotations
Create New Quotation
Quotation #
Date
Valid Until
Customer
Status
Total
Actions
prepare("SELECT qi.*, i.name_en, i.name_ar, i.vat_rate
FROM quotation_items qi
JOIN stock_items i ON qi.item_id = i.id
WHERE qi.quotation_id = ?");
$items->execute([$q['id']]);
$q['items'] = $items->fetchAll(PDO::FETCH_ASSOC);
?>
QUO-= str_pad((string)$q['id'], 5, '0', STR_PAD_LEFT) ?>
= $q['quotation_date'] ?>
= $q['valid_until'] ?: '---' ?>
= htmlspecialchars($q['customer_name'] ?? '---') ?>
= htmlspecialchars($q['status']) ?>
OMR = number_format((float)$q['total_with_vat'], 3) ?>
No quotations found
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Local Purchase Orders (LPO)
Create New LPO
LPO #
Date
Delivery Date
Supplier
Status
Total
Actions
prepare("SELECT li.*, i.name_en, i.name_ar, i.vat_rate
FROM lpo_items li
JOIN stock_items i ON li.item_id = i.id
WHERE li.lpo_id = ?");
$items->execute([$q['id']]);
$q['items'] = $items->fetchAll(PDO::FETCH_ASSOC);
?>
LPO-= str_pad((string)$q['id'], 5, '0', STR_PAD_LEFT) ?>
= $q['lpo_date'] ?>
= $q['delivery_date'] ?: '---' ?>
= htmlspecialchars($q['supplier_name'] ?? '---') ?>
= htmlspecialchars($q['status']) ?>
OMR = number_format((float)$q['total_with_vat'], 3) ?>
No LPOs found
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>
= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?>
VAT: = htmlspecialchars($data['settings']['vat_number'] ?? '') ?>
= $currTitle['en'] ?> Report
Date: = date('Y-m-d') ?>
= $page === 'sales' ? 'Customer' : 'Supplier' ?>:
|
Period: = !empty($_GET['start_date']) ? $_GET['start_date'] : 'All' ?> to = !empty($_GET['end_date']) ? $_GET['end_date'] : 'All' ?>
Invoice #
Date
Due Date
= $page === 'sales' ? 'Customer' : 'Supplier' ?>
Status
Total
Paid
Balance
Actions
prepare("SELECT ii.*, i.name_en, i.name_ar, i.vat_rate
FROM $itemTable ii
JOIN stock_items i ON ii.item_id = i.id
WHERE ii.$fkCol = ?");
$items->execute([$inv['id']]);
$inv['items'] = $items->fetchAll(PDO::FETCH_ASSOC);
$prefix = ($page === 'purchases') ? 'PUR' : 'INV';
?>
= !empty($inv['is_pos']) && !empty($inv['transaction_no']) ? htmlspecialchars($inv['transaction_no']) : $prefix . '-' . str_pad((string)$inv['id'], 5, '0', STR_PAD_LEFT) ?>
= $inv['invoice_date'] ?>
= $inv['due_date'] ?>
---
= htmlspecialchars($inv['customer_name'] ?? '---') ?>
= htmlspecialchars(str_replace('_', ' ', $inv['status'])) ?>
OMR = number_format((float)$inv['total_with_vat'], 3) ?>
OMR = number_format((float)$inv['paid_amount'], 3) ?>
OMR = number_format((float)($inv['total_with_vat'] - $inv['paid_amount']), 3) ?>
Totals
OMR = number_format((float)$total_all, 3) ?>
OMR = number_format((float)$total_paid, 3) ?>
OMR = number_format((float)$total_balance, 3) ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
= $lang === 'ar' ? $page_title_ar : $page_title_en ?>
= $lang === 'ar' ? 'طباعة' : 'Print' ?>
= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>
= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?>
= $lang === 'ar' ? 'كشف حساب' : 'Statement of Account' ?> - = htmlspecialchars($data['selected_entity']['name']) ?>
= htmlspecialchars($data['selected_entity']['name']) ?>
= htmlspecialchars($data['selected_entity']['email']) ?> | = htmlspecialchars($data['selected_entity']['phone']) ?>= $lang === 'ar' ? 'الفترة' : 'Period' ?> : = $_GET['start_date'] ?> = $lang === 'ar' ? 'إلى' : 'to' ?> = $_GET['end_date'] ?>
= $lang === 'ar' ? 'التاريخ' : 'Date' ?>
= $lang === 'ar' ? 'المرجع' : 'Reference' ?>
= $lang === 'ar' ? 'الوصف' : 'Description' ?>
= $lang === 'ar' ? 'مدين' : 'Debit' ?>
= $lang === 'ar' ? 'دائن' : 'Credit' ?>
= $lang === 'ar' ? 'الرصيد' : 'Balance' ?>
= $t['trans_date'] ?>
= $t['trans_type'] === 'invoice' ? ($lang === 'ar' ? ($page === 'supplier_statement' ? 'شراء-' : 'بيع-') : ($page === 'supplier_statement' ? 'PUR-' : 'INV-')).str_pad((string)$t['ref_no'], 5, '0', STR_PAD_LEFT) : ($lang === 'ar' ? 'قبض-' : 'RCP-').str_pad((string)$t['id'], 5, '0', STR_PAD_LEFT) ?>
= $lang === 'ar' ? 'فاتورة ضريبية' : 'Tax Invoice' ?>
= $lang === 'ar' ? 'دفع' : 'Payment' ?> - = $lang === 'ar' ? ($t['payment_method'] === 'cash' ? 'نقد' : ($t['payment_method'] === 'card' ? 'بطاقة ائتمان' : 'آجل')) : $t['payment_method'] ?>
= number_format($debit, 3) ?>
= number_format($credit, 3) ?>
= number_format($running_balance, 3) ?>
= $lang === 'ar' ? 'رصيد الإقفال' : 'Closing Balance' ?>
= $lang === 'ar' ? number_format($running_balance, 3) . ' ر.ع.' : 'OMR ' . number_format($running_balance, 3) ?>
Printed on = date('Y-m-d H:i:s') ?>
Please select an entity and date range to generate the statement.
Cashflow Statement
Print
= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>
= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?>
Cashflow Statement
Period: = htmlspecialchars($_GET['start_date'] ?? date('Y-m-01')) ?> to = htmlspecialchars($_GET['end_date'] ?? date('Y-m-d')) ?>
Description
Amount (OMR)
Opening Cash Balance
= number_format($data['opening_balance'], 3) ?>
Operating Activities
0) $op_inflow += $amt; else $op_outflow += abs($amt);
} elseif ($t['other_type'] === 'asset' && !in_array($t['other_account'], ['Accounts Receivable', 'Inventory'])) {
// Fixed assets etc
if ($amt > 0) $inv_inflow += $amt; else $inv_outflow += abs($amt);
} elseif ($t['other_type'] === 'equity' || $t['other_type'] === 'liability') {
if ($amt > 0) $fin_inflow += $amt; else $fin_outflow += abs($amt);
} else {
// Default to operating if unsure
if ($amt > 0) $op_inflow += $amt; else $op_outflow += abs($amt);
}
}
?>
Cash Received from Customers & Others
= number_format($op_inflow, 3) ?>
Cash Paid to Suppliers & Expenses
(= number_format($op_outflow, 3) ?>)
Net Cash from Operating Activities
= number_format($op_inflow - $op_outflow, 3) ?>
Investing Activities
Net Cash from Investing Activities
= number_format($inv_inflow - $inv_outflow, 3) ?>
Financing Activities
Net Cash from Financing Activities
= number_format($fin_inflow - $fin_outflow, 3) ?>
Net Change in Cash
= number_format($net_change, 3) ?>
Closing Cash Balance
= number_format($data['opening_balance'] + $net_change, 3) ?>
___________________ Prepared By
___________________ Approved By
Payment Methods
Add Payment Method
ID
Name (EN)
Name (AR)
Actions
No payment methods found yet. Add one to get started.
= $paymentMethodId ?>
= htmlspecialchars($paymentMethodNameEn) ?>
= htmlspecialchars($paymentMethodNameAr) ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Expense Categories
Add Category
ID
Name (EN)
Name (AR)
Actions
= $cat['id'] ?>
= htmlspecialchars($cat['name_en']) ?>
= htmlspecialchars($cat['name_ar']) ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Expenses List
Add Expense
Date
Reference
Category
Description
Amount
Actions
= $exp['expense_date'] ?>
= htmlspecialchars($exp['reference_no'] ?: '---') ?>
= htmlspecialchars($exp['cat_en'] ?? 'Unknown') ?>
= htmlspecialchars($exp['description']) ?>
OMR = number_format((float)$exp['amount'], 3) ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>
= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?>
VAT: = htmlspecialchars($data['settings']['vat_number'] ?? '') ?>
Expense Report
Date: = date('Y-m-d') ?>
Period: = htmlspecialchars($_GET['start_date'] ?? date('Y-m-01')) ?> - = htmlspecialchars($_GET['end_date'] ?? date('Y-m-d')) ?>
Expense Report
Print
Total Expenses
OMR = number_format((float)$data['total_expenses'], 3) ?>
For the selected period
Category
Total Amount
% of Total
No expenses found for this period.
0 ? ($row['total'] / $data['total_expenses'] * 100) : 0;
?>
= htmlspecialchars($row['name_en']) ?>
= htmlspecialchars($row['name_ar']) ?>
OMR = number_format((float)$row['total'], 3) ?>
= number_format($percent, 1) ?>%
= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>
= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?>
VAT: = htmlspecialchars($data['settings']['vat_number'] ?? '') ?>
Sales Returns Report
Date: = date('Y-m-d') ?>
Return #
Date
Invoice #
Customer
Total Amount
Actions
RET-= str_pad((string)$ret['id'], 5, '0', STR_PAD_LEFT) ?>
= $ret['return_date'] ?>
INV-= str_pad((string)$ret['invoice_id'], 5, '0', STR_PAD_LEFT) ?>
= htmlspecialchars($ret['customer_name'] ?? 'Walk-in') ?>
OMR = number_format((float)$ret['total_amount'], 3) ?>
No returns found
Return #
Date
Invoice #
Supplier
Total Amount
Actions
PRET-= str_pad((string)$ret['id'], 5, '0', STR_PAD_LEFT) ?>
= $ret['return_date'] ?>
PUR-= str_pad((string)$ret['purchase_id'], 5, '0', STR_PAD_LEFT) ?>
= htmlspecialchars($ret['supplier_name'] ?? 'Unknown') ?>
OMR = number_format((float)$ret['total_amount'], 3) ?>
No returns found
HR Departments
Add Department
ID
Department Name
Actions
= $d['id'] ?>
= htmlspecialchars($d['name']) ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
HR Employees
Add Employee
Name
Biometric ID
Department
Position
Salary
Status
Actions
= htmlspecialchars($e['name']) ?>
= htmlspecialchars($e['email']) ?>
= htmlspecialchars($e['biometric_id'] ?? '---') ?>
= htmlspecialchars($e['dept_name'] ?? '---') ?>
= htmlspecialchars($e['position']) ?>
OMR = number_format($e['salary'], 3) ?>
= $e['status'] ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Employee
Department
Status
Clock In
Clock Out
Action
= htmlspecialchars($e['name']) ?>
= htmlspecialchars($e['dept_name'] ?? '---') ?>
= $e['status'] ?>
Not Marked
= $e['clock_in'] ?? '---' ?>
= $e['clock_out'] ?? '---' ?>
Mark
Status
>Present
>Absent
>On Leave
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
To sync attendance from your biometric device, use the following API endpoint:
= (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]" ?>/api/biometric_sync.php
Expected JSON format:
[
{
"biometric_id": "101",
"device_id": 1,
"timestamp": "2026-02-17 08:30:00",
"type": "in"
},
{
"biometric_id": "101",
"device_id": 1,
"timestamp": "2026-02-17 17:30:00",
"type": "out"
}
]
Note: Ensure Employee Biometric IDs match those in the device logs.
HR Payroll
>= date('F', mktime(0, 0, 0, $m, 1)) ?>
=date('Y')-2; $y--): ?>
>= $y ?>
Generate
Employee
Basic
Bonus
Deductions
Net Salary
Status
Actions
= htmlspecialchars($p['emp_name']) ?>
OMR = number_format($p['basic_salary'], 3) ?>
+ OMR = number_format($p['bonus'], 3) ?>
- OMR = number_format($p['deductions'], 3) ?>
OMR = number_format($p['net_salary'], 3) ?>
= $p['status'] ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Employee
--- Select ---
= htmlspecialchars($e['name']) ?> (Basic: = number_format($e['salary'], 3) ?>)
Biometric Devices
Add Device
Device Name
IP / IO Address
Port
Serial
Last Sync
Status
Actions
= htmlspecialchars($d['device_name']) ?>
IP: = htmlspecialchars($d['ip_address']) ?>
IO: = htmlspecialchars($d['io_address']) ?>
= $d['port'] ?>
= htmlspecialchars($d['serial_number'] ?? '---') ?>
= $d['last_sync'] ?? 'Never' ?>
= $d['status'] ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Device Name
Type
Connection
Details
Status
Actions
= htmlspecialchars($d['device_name']) ?>
= $d['device_type'] ?>
= $d['connection_type'] ?>
= htmlspecialchars((string)$d['ip_address']) ?>:= $d['port'] ?>
Baud: = $d['baud_rate'] ?>
USB Interface
= $d['status'] ?>
Edit
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Profile Picture
= htmlspecialchars($data['user']['username']) ?>
= htmlspecialchars($_SESSION['user_role_name'] ?? 'User') ?>
'Saved in settings',
'environment' => 'Environment variable',
'derived' => 'Derived from app name',
'default' => 'Built-in fallback',
];
$savedLicenseAppName = trim((string)($data['settings']['license_app_name'] ?? ''));
$savedLicenseAppSlug = trim((string)($data['settings']['license_app_slug'] ?? ''));
$licenseAppNameInput = $savedLicenseAppName;
$licenseAppSlugInput = $savedLicenseAppSlug;
?>
Contact Information
Physical Address
= htmlspecialchars($data['settings']['company_address'] ?? '') ?>
System Configuration
System Timezone
$tz";
}
?>
Stock Policy
data-en="Prevent selling out of stock" data-ar="منع البيع عند نفاذ المخزون">Prevent selling out of stock
data-en="Allow selling out of stock" data-ar="السماح بالبيع عند نفاذ المخزون">Allow selling out of stock
Scale Barcode Mode
data-en="Use embedded weight" data-ar="استخدام الوزن">Use embedded weight
data-en="Use embedded price" data-ar="استخدام السعر">Use embedded price
Scale Prefix From
Scale Prefix To
13-digit scale barcode format: 2-digit prefix + 5-digit item code + 5-digit value + 1 check digit. Full 13-digit scale barcodes are reserved and cannot be saved on items or imported.
License App Identity
Name source : = htmlspecialchars($licenseSourceLabels[$licenseIdentity['app_name_source']] ?? 'Built-in fallback') ?>
·
Slug source : = htmlspecialchars($licenseSourceLabels[$licenseIdentity['app_slug_source']] ?? 'Built-in fallback') ?>
Use one stable identity per product.
This is the product identity your central license manager sees. Change the slug only when you intentionally rename or split a product.
Current effective identity:
= htmlspecialchars((string)$licenseIdentity['app_name']) ?>
·
= htmlspecialchars((string)$licenseIdentity['app_slug']) ?>
Loyalty Program
Loyalty Status
data-en="Disabled" data-ar="معطل">Disabled
data-en="Active" data-ar="نشط">Active
Save All Changes
Group Name
Created Date
Status
Actions
= htmlspecialchars((string)$group['name']) ?>
= date('M d, Y', strtotime((string)$group['created_at'])) ?>
Active
Group Name
Permissions
Select All
Deselect All
prepare("SELECT permission FROM role_permissions WHERE role_id = ?");
$stmtP->execute([$group['id']]);
$perms = $stmtP->fetchAll(PDO::FETCH_COLUMN);
foreach ($permission_groups as $group_name => $modules): ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Customer Display Settings
Manual Backup
Create a database backup immediately.
Backup Now
Filename
Size
Date
Actions
No backups found.
= htmlspecialchars($b['name']) ?>
= htmlspecialchars($b['size']) ?>
= htmlspecialchars($b['date']) ?>
Cash Registers Management
Define your shop counters and registers.
License Limit: = $current_regs ?> / = $allowed_acts ?>
Registers
Add Register
ID
Name
Status
Created At
Actions
#= $r['id'] ?>
= htmlspecialchars($r['name']) ?>
= ucfirst($r['status']) ?>
= $r['created_at'] ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Register Sessions (= count($data['sessions']) ?>)
Manage daily opening and closing of cash registers.
prepare("SELECT s.*, r.name as register_name FROM register_sessions s JOIN cash_registers r ON s.register_id = r.id WHERE s.user_id = ? AND s.status = 'open'");
$active_session->execute([$_SESSION['user_id']]);
$session = $active_session->fetch();
?>
Open Register
Close Register
Current Open Register: = htmlspecialchars($session['register_name']) ?> |
Opened At: = $session['opened_at'] ?> |
Opening Balance: OMR = number_format((float)$session['opening_balance'], 3) ?>
ID
Register
Cashier
Opened At
Closed At
Opening Bal.
Cash Sale
Credit Card
Credit
Total Sale
Balance
Status
Report
#= $s['id'] ?>
= htmlspecialchars($s['register_name'] ?? 'N/A') ?>
= htmlspecialchars($s['username'] ?? 'N/A') ?>
= $s['opened_at'] ?>
= $s['closed_at'] ?? '---' ?>
OMR = number_format((float)$s['opening_balance'], 3) ?>
prepare("SELECT
SUM(CASE WHEN LOWER(payment_method) = 'cash' THEN amount ELSE 0 END) as cash_total,
SUM(CASE WHEN LOWER(payment_method) IN ('card', 'credit card', 'visa', 'mastercard') THEN amount ELSE 0 END) as card_total,
SUM(CASE WHEN LOWER(payment_method) = 'credit' THEN amount ELSE 0 END) as credit_total,
SUM(CASE WHEN LOWER(payment_method) LIKE '%transfer%' OR LOWER(payment_method) LIKE '%bank%' THEN amount ELSE 0 END) as transfer_total,
SUM(amount) as total_sales
FROM (
SELECT p.payment_method, p.amount FROM payments p JOIN invoices i ON p.invoice_id = i.id WHERE i.register_session_id = ? AND i.status = 'paid' AND i.is_pos = 1
) as combined_payments");
$stats_stmt->execute([$s['id']]);
$st = $stats_stmt->fetch();
$c_total = (float)($st['cash_total'] ?? 0);
$cd_total = (float)($st['card_total'] ?? 0);
$cr_total = (float)($st['credit_total'] ?? 0);
$tr_total = (float)($st['transfer_total'] ?? 0);
$t_sales = (float)($st['total_sales'] ?? 0);
$row_expected_cash = (float)$s['opening_balance'] + $c_total;
?>
OMR = number_format((float)$c_total, 3) ?>
OMR = number_format((float)$cd_total, 3) ?>
OMR = number_format((float)$cr_total, 3) ?>
OMR = number_format((float)$t_sales, 3) ?>
0 ? 'text-info' : 'text-danger');
?>
OMR = number_format((float)$diff, 3) ?>
---
= ucfirst($s['status']) ?>
= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
Select Register / Counter
= htmlspecialchars($reg['name']) ?>
Before closing, please count all cash in your register.
prepare("SELECT payment_method, SUM(amount) as total FROM payments p JOIN invoices i ON p.invoice_id = i.id WHERE i.register_session_id = ? AND i.status = 'paid' AND i.is_pos = 1 GROUP BY payment_method");
$curBreakdown->execute([$session['id']]);
$curMethods = $curBreakdown->fetchAll();
$cash_sales = 0;
$card_sales = 0;
$credit_sales = 0;
$bank_transfer_sales = 0;
foreach ($curMethods as $m) {
$method = strtolower($m['payment_method']);
if ($method === 'cash') $cash_sales = $m['total'];
elseif ($method === 'card' || strpos($method, 'card') !== false) $card_sales = $m['total'];
elseif ($method === 'credit') $credit_sales = $m['total'];
elseif (strpos($method, 'transfer') !== false || strpos($method, 'bank') !== false) $bank_transfer_sales = $m['total'];
else $cash_sales += $m['total'];
}
$total_sales = $cash_sales + $card_sales + $credit_sales + $bank_transfer_sales;
$expected_cash = (float)$session['opening_balance'] + $cash_sales;
$total_all = (float)$session['opening_balance'] + $total_sales;
?>
Session Summary
Opening Balance:
OMR = number_format((float)$session['opening_balance'], 3) ?>
Cash Sales:
OMR = number_format((float)$cash_sales, 3) ?>
Credit Card Sales:
OMR = number_format((float)$card_sales, 3) ?>
Credit:
OMR = number_format((float)$credit_sales, 3) ?>
Bank Transfer:
OMR = number_format((float)$bank_transfer_sales, 3) ?>
Total Sales:
OMR = number_format((float)$total_sales, 3) ?>
Balance (Total):
OMR = number_format((float)$total_all, 3) ?>
Expected Cash:
OMR = number_format((float)$expected_cash, 3) ?>
Transaction Details
Time
Order #
Customer
Method
Amount
prepare("SELECT i.*, c.name as customer_name, GROUP_CONCAT(p.payment_method SEPARATOR ', ') as methods FROM invoices i LEFT JOIN payments p ON i.id = p.invoice_id LEFT JOIN customers c ON i.customer_id = c.id WHERE i.register_session_id = ? AND i.status = 'paid' AND i.is_pos = 1 GROUP BY i.id ORDER BY i.created_at DESC");
$txs_stmt->execute([$session['id']]);
$txs = $txs_stmt->fetchAll();
foreach ($txs as $tx):
?>
= date('H:i', strtotime($tx['created_at'])) ?>
= htmlspecialchars($tx['transaction_no']) ?>
= htmlspecialchars($tx['customer_name'] ?: 'Walk-in') ?>
= htmlspecialchars($tx['methods'] ?: '---') ?>
= number_format($tx['total_with_vat'], 3) ?>
No transactions
Total Cash in Hand (Actual Counted)
Closing Notes / Comments
--- " . htmlspecialchars(basename($file)) . " --- ";
$lines = shell_exec("tail -n 50 " . escapeshellarg($path));
echo "
" . htmlspecialchars((string)$lines) . " ";
}
}
if (!$found_logs) {
echo "
No accessible log files found.
";
}
?>
Supported columns
Name (EN), Name (AR)
CSV and XLSX files are accepted.
If Arabic is blank, the English name will be reused automatically.
Returned Items
Item
Returned Qty
Unit Price
Total Price
Total Amount:
Items for Return
Item
Purchased Qty
Return Qty
Price
Total
Total Return Amount:
= __('currency') ?> 0.000
Notes / Reason for Return
Items for Return
Item
Sold Qty
Return Qty
Price
Total
Total Return Amount:
= __('currency') ?> 0.000
Notes / Reason for Return
Journal Details
Journal is not balanced! Difference: 0.000
Item Details
Qty
Unit Price
VAT
Total
Subtotal
= __('currency') ?> 0.000
Total VAT
= __('currency') ?> 0.000
Grand Total
= __('currency') ?> 0.000
Item Details
Qty
Unit Price
VAT
Total
Subtotal
= __('currency') ?> 0.000
Total VAT
= __('currency') ?> 0.000
Grand Total
= __('currency') ?> 0.000
Item Details
Qty
Unit Price
VAT
Total
Subtotal
= __('currency') ?> 0.000
Total VAT
= __('currency') ?> 0.000
Grand Total
= __('currency') ?> 0.000
Item Details
Qty
Unit Price
VAT
Total
Subtotal
= __('currency') ?> 0.000
Total VAT
= __('currency') ?> 0.000
Grand Total
= __('currency') ?> 0.000
Item Details
Qty
Unit Price
VAT
Total
Subtotal
= __('currency') ?> 0.000
Total VAT
= __('currency') ?> 0.000
Grand Total
= __('currency') ?> 0.000
Item Details
Qty
Unit Price
VAT
Total
Subtotal
= __('currency') ?> 0.000
Total VAT
= __('currency') ?> 0.000
Grand Total
= __('currency') ?> 0.000
Bill To / فاتورة إلى
VAT / الضريبة:
Phone / الهاتف:
Payment Details / تفاصيل الدفع
Method / الطريقة:
Currency / العملة: OMR / ريال عماني
Amount in Words / المبلغ بالحروف
Terms & Conditions / الشروط والأحكام
Goods once sold will not be taken back or exchanged.
Payment is due within the agreed credit period.
Subtotal (Excl. VAT) / المجموع الفرعي (دون الضريبة)
VAT Amount / مبلغ الضريبة
Grand Total / المجموع الكلي
Payment Tracking / تتبع الدفع
Date / التاريخ
Method / الطريقة
Amount / المبلغ
Paid Amount / المبلغ المدفوع
Balance Due / الرصيد المتبقي
Customer Signature / توقيع العميل
Authorized Signatory / التوقيع المعتمد
Generated by = htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?> | Visit us at = $_SERVER['HTTP_HOST'] ?>
= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>
= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?>
Payment Receipt / سند قبض
Received From / استلمنا من
Against Invoice / مقابل فاتورة
Payment Method / طريقة الدفع
Amount Paid / المبلغ المدفوع
Receiver's Signature / توقيع المستلم
Authorized Signatory / التوقيع المعتمد
Customer
Walk-in Customer
Select Credit Customer
--- Select Customer ---
= htmlspecialchars($c['name']) ?> (= htmlspecialchars($c['phone'] ?? '') ?>)
Add Payment Method
Cash
Credit Card
Credit
Bank Transfer
Total Tendered (Cash)
0.000
* Change is calculated based on cash payments only.
Label Layout
3 x 7 (21 Labels per sheet)
3 x 8 (24 Labels per sheet)
4 x 10 (40 Labels per sheet)
L7651 (5 x 13 - 65 Labels)
L4736 (2 x 7 - 14 Labels)
L7431 (6 x 8 - 48 Labels)
L4716 (6 x 8 - 48 Labels - Round)
Copies (Set All)
Print A4 Sheet
Quantities per Item
Select items to adjust quantities.