12226 lines
700 KiB
PHP
12226 lines
700 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
// Sessions setup
|
|
$sessions_dir = __DIR__ . '/sessions';
|
|
if (!is_dir($sessions_dir)) {
|
|
@mkdir($sessions_dir, 0777, true);
|
|
}
|
|
if (is_writable($sessions_dir)) {
|
|
session_save_path("0;0660;" . $sessions_dir);
|
|
}
|
|
|
|
// Check for required extensions
|
|
$required_extensions = ['pdo', 'pdo_mysql', 'curl', 'json'];
|
|
$missing_extensions = [];
|
|
foreach ($required_extensions as $ext) {
|
|
if (!extension_loaded($ext)) {
|
|
$missing_extensions[] = $ext;
|
|
}
|
|
}
|
|
|
|
if (!empty($missing_extensions)) {
|
|
die("Error: The following PHP extensions are required but missing: " . implode(', ', $missing_extensions) . ". Please contact your hosting provider to enable them.");
|
|
}
|
|
|
|
// Enhanced session security and iframe compatibility
|
|
if ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')) {
|
|
session_set_cookie_params([
|
|
'lifetime' => 0,
|
|
'path' => '/',
|
|
'secure' => true,
|
|
'httponly' => true,
|
|
'samesite' => 'None',
|
|
]);
|
|
}
|
|
|
|
session_start();
|
|
|
|
if (!function_exists('app_file_debug_logging_enabled')) {
|
|
function app_file_debug_logging_enabled(): bool {
|
|
static $enabled = null;
|
|
if ($enabled !== null) {
|
|
return $enabled;
|
|
}
|
|
|
|
$candidates = [
|
|
getenv('APP_FILE_DEBUG_LOGS'),
|
|
$_ENV['APP_FILE_DEBUG_LOGS'] ?? null,
|
|
$_SERVER['APP_FILE_DEBUG_LOGS'] ?? null,
|
|
];
|
|
|
|
foreach ($candidates as $candidate) {
|
|
if ($candidate === false || $candidate === null) {
|
|
continue;
|
|
}
|
|
|
|
$value = strtolower(trim((string)$candidate));
|
|
if ($value === '') {
|
|
continue;
|
|
}
|
|
|
|
if (in_array($value, ['1', 'true', 'yes', 'on'], true)) {
|
|
$enabled = true;
|
|
return true;
|
|
}
|
|
|
|
if (in_array($value, ['0', 'false', 'no', 'off'], true)) {
|
|
$enabled = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$enabled = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('app_debug_file_log')) {
|
|
function app_debug_file_log(string $filename, string $message): void {
|
|
if (!app_file_debug_logging_enabled()) {
|
|
return;
|
|
}
|
|
|
|
$path = __DIR__ . '/' . ltrim($filename, '/');
|
|
@file_put_contents($path, rtrim($message, "\r\n") . PHP_EOL, FILE_APPEND);
|
|
}
|
|
}
|
|
|
|
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') {
|
|
app_debug_file_log('post_debug.log', date('Y-m-d H:i:s') . " - POST: " . json_encode($_POST));
|
|
}
|
|
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('current_outlet_name')) {
|
|
function current_outlet_name(): string {
|
|
static $cache = [];
|
|
|
|
$oid = current_outlet_id();
|
|
if ($oid === -1 || $oid === 0) {
|
|
return __('All Outlets') ?: 'All Outlets';
|
|
}
|
|
|
|
if (array_key_exists($oid, $cache)) {
|
|
return $cache[$oid];
|
|
}
|
|
|
|
if (!db_table_exists('outlets')) {
|
|
$cache[$oid] = 'Outlet ' . $oid;
|
|
return $cache[$oid];
|
|
}
|
|
|
|
try {
|
|
$stmt = db()->prepare("SELECT name FROM outlets WHERE id = ? LIMIT 1");
|
|
$stmt->execute([$oid]);
|
|
$cache[$oid] = (string)($stmt->fetchColumn() ?: ('Outlet ' . $oid));
|
|
} catch (Throwable $e) {
|
|
$cache[$oid] = 'Outlet ' . $oid;
|
|
}
|
|
|
|
return $cache[$oid];
|
|
}
|
|
}
|
|
|
|
if (!function_exists('outlet_scope_sql')) {
|
|
function outlet_scope_sql(string $tableName, string $qualifiedColumn = 'outlet_id', bool $includeLegacyNull = true): array {
|
|
if (!db_column_exists($tableName, 'outlet_id')) {
|
|
return ['sql' => '1=1', 'params' => []];
|
|
}
|
|
|
|
$oid = current_outlet_id();
|
|
if ($oid === -1) {
|
|
return ['sql' => '1=1', 'params' => []];
|
|
}
|
|
|
|
$sql = $includeLegacyNull
|
|
? "({$qualifiedColumn} = ? OR {$qualifiedColumn} IS NULL)"
|
|
: "{$qualifiedColumn} = ?";
|
|
|
|
return ['sql' => $sql, 'params' => [$oid]];
|
|
}
|
|
}
|
|
|
|
if (!function_exists('dashboard_sales_series')) {
|
|
function dashboard_sales_series(string $period = 'month', int $limit = 12): array {
|
|
$period = strtolower($period) === 'year' ? 'year' : 'month';
|
|
$limit = max(1, (int)$limit);
|
|
$sources = [];
|
|
$params = [];
|
|
$db = db();
|
|
|
|
if (db_table_exists('invoices')) {
|
|
$invoiceDateColumn = db_first_existing_column('invoices', ['invoice_date', 'created_at']);
|
|
$invoiceTotalExpression = db_column_exists('invoices', 'total_with_vat')
|
|
? 'COALESCE(total_with_vat, 0)'
|
|
: (db_column_exists('invoices', 'total_amount') ? 'COALESCE(total_amount, 0)' : '0');
|
|
|
|
if ($invoiceDateColumn !== null) {
|
|
$invoiceScope = outlet_scope_sql('invoices', 'outlet_id');
|
|
if ($period === 'year') {
|
|
$sources[] = "SELECT YEAR(`{$invoiceDateColumn}`) AS period_key, CAST(YEAR(`{$invoiceDateColumn}`) AS CHAR) AS label, {$invoiceTotalExpression} AS total FROM invoices WHERE `{$invoiceDateColumn}` IS NOT NULL AND {$invoiceScope['sql']}";
|
|
} else {
|
|
$sources[] = "SELECT DATE_FORMAT(`{$invoiceDateColumn}`, '%Y-%m') AS period_key, DATE_FORMAT(`{$invoiceDateColumn}`, '%b %Y') AS label, {$invoiceTotalExpression} AS total FROM invoices WHERE `{$invoiceDateColumn}` IS NOT NULL AND {$invoiceScope['sql']}";
|
|
}
|
|
$params = array_merge($params, $invoiceScope['params']);
|
|
}
|
|
}
|
|
|
|
if (db_table_exists('pos_transactions')) {
|
|
$posDateColumn = db_first_existing_column('pos_transactions', ['created_at', 'transaction_date', 'sale_date']);
|
|
$posTotalExpression = db_column_exists('pos_transactions', 'net_amount')
|
|
? 'COALESCE(net_amount, 0)'
|
|
: (db_column_exists('pos_transactions', 'total_amount') ? 'COALESCE(total_amount, 0)' : '0');
|
|
|
|
if ($posDateColumn !== null) {
|
|
$posScope = outlet_scope_sql('pos_transactions', 'outlet_id');
|
|
$posStatusPredicate = db_column_exists('pos_transactions', 'status') ? "status = 'completed' AND " : '';
|
|
if ($period === 'year') {
|
|
$sources[] = "SELECT YEAR(`{$posDateColumn}`) AS period_key, CAST(YEAR(`{$posDateColumn}`) AS CHAR) AS label, {$posTotalExpression} AS total FROM pos_transactions WHERE {$posStatusPredicate}`{$posDateColumn}` IS NOT NULL AND {$posScope['sql']}";
|
|
} else {
|
|
$sources[] = "SELECT DATE_FORMAT(`{$posDateColumn}`, '%Y-%m') AS period_key, DATE_FORMAT(`{$posDateColumn}`, '%b %Y') AS label, {$posTotalExpression} AS total FROM pos_transactions WHERE {$posStatusPredicate}`{$posDateColumn}` IS NOT NULL AND {$posScope['sql']}";
|
|
}
|
|
$params = array_merge($params, $posScope['params']);
|
|
}
|
|
}
|
|
|
|
if ($sources === []) {
|
|
return [];
|
|
}
|
|
|
|
$sql = "SELECT period_key, label, SUM(total) AS total FROM (" . implode(' UNION ALL ', $sources) . ") dashboard_sales_rollup GROUP BY period_key, label ORDER BY period_key DESC LIMIT {$limit}";
|
|
$stmt = $db->prepare($sql);
|
|
$stmt->execute($params);
|
|
$rows = array_reverse($stmt->fetchAll(PDO::FETCH_ASSOC));
|
|
|
|
foreach ($rows as &$row) {
|
|
$row['label'] = (string)($row['label'] ?? '');
|
|
$row['total'] = (float)($row['total'] ?? 0);
|
|
}
|
|
unset($row);
|
|
|
|
return $rows;
|
|
}
|
|
}
|
|
|
|
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.';
|
|
?>
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="robots" content="noindex, nofollow">
|
|
<title><?= htmlspecialchars($title) ?></title>
|
|
<style>
|
|
body { margin: 0; font-family: Inter, Arial, sans-serif; background: #f6f8fb; color: #1f2937; }
|
|
.wrap { max-width: 900px; margin: 48px auto; padding: 0 20px; }
|
|
.card { background: #fff; border-radius: 18px; box-shadow: 0 18px 60px rgba(15, 23, 42, 0.08); padding: 28px; }
|
|
h1 { margin: 0 0 10px; font-size: 28px; }
|
|
p { color: #4b5563; line-height: 1.6; }
|
|
.badge { display: inline-block; padding: 6px 10px; border-radius: 999px; background: #fee2e2; color: #991b1b; font-weight: 600; font-size: 13px; }
|
|
.grid { display: grid; grid-template-columns: 180px 1fr; gap: 12px 16px; margin-top: 22px; }
|
|
.label { font-weight: 700; color: #111827; }
|
|
.value { word-break: break-word; }
|
|
.hint { margin-top: 18px; padding: 14px 16px; border-radius: 14px; background: #fff7ed; color: #9a3412; border: 1px solid #fed7aa; }
|
|
pre { margin: 18px 0 0; padding: 16px; background: #0f172a; color: #e2e8f0; border-radius: 14px; overflow: auto; font-size: 12px; line-height: 1.5; }
|
|
.actions { margin-top: 22px; display: flex; gap: 12px; flex-wrap: wrap; }
|
|
.btn { display: inline-block; text-decoration: none; border-radius: 12px; padding: 10px 14px; font-weight: 600; }
|
|
.btn-primary { background: #2563eb; color: #fff; }
|
|
.btn-secondary { background: #e5e7eb; color: #111827; }
|
|
.note { margin-top: 20px; font-size: 14px; color: #6b7280; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main class="wrap">
|
|
<section class="card">
|
|
<span class="badge">HTTP 500</span>
|
|
<h1><?= htmlspecialchars($title) ?></h1>
|
|
<p><?= htmlspecialchars($summary) ?></p>
|
|
|
|
<?php if ($showDetails): ?>
|
|
<div class="grid">
|
|
<div class="label">Exception</div>
|
|
<div class="value"><?= htmlspecialchars(get_class($throwable)) ?></div>
|
|
<div class="label">Message</div>
|
|
<div class="value"><?= htmlspecialchars($throwable->getMessage()) ?></div>
|
|
<div class="label">File</div>
|
|
<div class="value"><?= htmlspecialchars($throwable->getFile()) ?></div>
|
|
<div class="label">Line</div>
|
|
<div class="value"><?= (int)$throwable->getLine() ?></div>
|
|
<div class="label">Page</div>
|
|
<div class="value"><?= htmlspecialchars($page) ?></div>
|
|
<div class="label">Request URI</div>
|
|
<div class="value"><?= htmlspecialchars($requestUri) ?></div>
|
|
</div>
|
|
|
|
<?php if ($hint !== null): ?>
|
|
<div class="hint"><strong>Schema hint:</strong> <?= htmlspecialchars($hint) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($traceText !== ''): ?>
|
|
<pre><?= htmlspecialchars($traceText) ?></pre>
|
|
<?php endif; ?>
|
|
|
|
<div class="actions">
|
|
<a class="btn btn-primary" href="schema_debug.php">Open schema debug report</a>
|
|
<a class="btn btn-secondary" href="index.php">Retry dashboard</a>
|
|
</div>
|
|
|
|
<p class="note">A copy of this failure was written to <code>runtime_debug.log</code>.</p>
|
|
<?php else: ?>
|
|
<div class="actions">
|
|
<a class="btn btn-secondary" href="index.php">Back to home</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
</section>
|
|
</main>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
exit;
|
|
}
|
|
}
|
|
|
|
if (!defined('APP_RUNTIME_DEBUG_HANDLER_REGISTERED')) {
|
|
define('APP_RUNTIME_DEBUG_HANDLER_REGISTERED', true);
|
|
set_exception_handler(static function (Throwable $throwable): void {
|
|
runtime_debug_render_exception($throwable);
|
|
});
|
|
}
|
|
|
|
// Handle Outlet Switch
|
|
if (isset($_GET['action']) && $_GET['action'] === 'switch_outlet' && isset($_GET['id'])) {
|
|
$target_id = (int)$_GET['id'];
|
|
$allowed_outlets = $_SESSION['user_outlets'] ?? [1];
|
|
$is_admin = ($_SESSION['user_role_name'] ?? '') === 'Administrator';
|
|
|
|
if ($target_id === -1) {
|
|
$_SESSION['outlet_id'] = -1;
|
|
} elseif ($is_admin || in_array($target_id, $allowed_outlets)) {
|
|
if (!db_table_exists('outlets')) {
|
|
$_SESSION['outlet_id'] = $target_id;
|
|
} else {
|
|
$stmt = db()->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';
|
|
|
|
if (!function_exists('localized_option_label')) {
|
|
function localized_option_label(array $row, ?string $forceLang = null, string $fallback = '---'): string
|
|
{
|
|
global $lang;
|
|
$targetLang = $forceLang ?? $lang;
|
|
$keys = $targetLang === 'ar'
|
|
? ['name_ar', 'short_name_ar', 'name_en', 'short_name_en']
|
|
: ['name_en', 'short_name_en', 'name_ar', 'short_name_ar'];
|
|
|
|
foreach ($keys as $key) {
|
|
$value = trim((string)($row[$key] ?? ''));
|
|
if ($value !== '') {
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
return $fallback;
|
|
}
|
|
}
|
|
|
|
// Licensing Middleware
|
|
try {
|
|
$is_activated = LicenseService::isActivated();
|
|
$trial_days = LicenseService::getTrialRemainingDays();
|
|
$can_access = LicenseService::canAccess();
|
|
} catch (PDOException $e) {
|
|
die("Database Connection Error: " . $e->getMessage() . "<br><br>Please check your <b>db/config.php</b> 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'];
|
|
}
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="<?= $lang ?>" dir="<?= $dir ?>">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title><?= __('activate_product') ?></title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap<?= $dir === 'rtl' ? '.rtl' : '' ?>.min.css" rel="stylesheet">
|
|
<style>
|
|
body { background: #f4f7f6; height: 100vh; display: flex; align-items: center; justify-content: center; font-family: 'Inter', sans-serif; }
|
|
.activation-card { background: white; padding: 2rem; border-radius: 1rem; box-shadow: 0 10px 25px rgba(0,0,0,0.05); width: 100%; max-width: 450px; }
|
|
.fingerprint { background: #eee; padding: 0.5rem; border-radius: 0.5rem; font-family: monospace; font-size: 0.8rem; word-break: break-all; }
|
|
[dir="rtl"] { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="activation-card">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h3 class="fw-bold mb-0"><?= __('activate_product') ?></h3>
|
|
<div class="dropdown">
|
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<?= $lang === 'ar' ? 'العربية' : 'English' ?>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-menu" href="?page=activate&lang=en">English</a></li>
|
|
<li><a class="dropdown-menu" href="?page=activate&lang=ar">العربية</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<p class="text-muted small mb-4"><?= $lang === 'ar' ? 'يرجى إدخال مفتاح التسلسل للمتابعة.' : 'Please enter your serial key to continue using the application.' ?></p>
|
|
|
|
<?php if ($error): ?>
|
|
<div class="alert alert-danger small"><?= htmlspecialchars((string)$error) ?></div>
|
|
<?php endif; ?>
|
|
<?php if ($success): ?>
|
|
<div class="alert alert-success small"><?= htmlspecialchars((string)$success) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<form method="POST">
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold"><?= __('serial_key') ?></label>
|
|
<input type="text" name="license_key" class="form-control" placeholder="FLAT-XXXX-XXXX-XXXX" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold text-muted"><?= $lang === 'ar' ? 'بصمة السيرفر' : 'Server Fingerprint' ?></label>
|
|
<div class="fingerprint text-muted"><?= LicenseService::getFingerprint() ?></div>
|
|
</div>
|
|
<button type="submit" name="activate" class="btn btn-primary w-100 py-2 mb-3"><?= __('activate_now') ?></button>
|
|
</form>
|
|
|
|
<?php if (!$is_activated && $trial_days <= 0): ?>
|
|
<div class="text-center">
|
|
<hr>
|
|
<p class="text-muted small"><?= $lang === 'ar' ? 'أو ابدأ الفترة التجريبية (15 يوم)' : 'Or start your 15-day trial period' ?></p>
|
|
<form method="POST">
|
|
<button type="submit" name="start_trial" class="btn btn-outline-secondary w-100 py-2"><?= $lang === 'ar' ? 'بدء الفترة التجريبية' : 'Start Free Trial' ?></button>
|
|
</form>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
exit;
|
|
}
|
|
|
|
require_once 'includes/accounting_helper.php';
|
|
|
|
// Helper to check permissions
|
|
function can(string $permission): bool {
|
|
if (($_SESSION["user_id"] ?? 0) == 1) return true;
|
|
if (strcasecmp($_SESSION["user_role_name"] ?? "", "Administrator") === 0) return true;
|
|
if (!isset($_SESSION['user_id'])) return false;
|
|
if (($_SESSION['user_role_name'] ?? '') === 'Administrator') return true;
|
|
$user_perms = $_SESSION['user_permissions'] ?? [];
|
|
if ($user_perms === 'all') return true;
|
|
if (is_array($user_perms)) {
|
|
return in_array('all', $user_perms) || in_array($permission, $user_perms);
|
|
}
|
|
$perms = json_decode((string)$user_perms, true);
|
|
return is_array($perms) && (in_array('all', $perms) || in_array($permission, $perms));
|
|
}
|
|
|
|
function getPurchaseAlerts() {
|
|
if (!can('dashboard_view') || !db_table_exists('purchases')) return [];
|
|
|
|
$db = db();
|
|
$hasSupplierJoin = db_table_exists('suppliers') && db_column_exists('purchases', 'supplier_id');
|
|
$dueDateExpression = db_column_exists('purchases', 'due_date') ? 'p.due_date' : 'NULL';
|
|
$totalExpression = db_column_exists('purchases', 'total_with_vat')
|
|
? 'COALESCE(p.total_with_vat, 0)'
|
|
: (db_column_exists('purchases', 'total_amount') ? 'COALESCE(p.total_amount, 0)' : '0');
|
|
$supplierExpression = $hasSupplierJoin ? 's.name' : 'NULL';
|
|
$joinClause = $hasSupplierJoin ? ' LEFT JOIN suppliers s ON p.supplier_id = s.id' : '';
|
|
$orderBy = db_column_exists('purchases', 'due_date') ? ' ORDER BY p.due_date ASC' : ' ORDER BY p.id DESC';
|
|
|
|
$where = [];
|
|
$params = [];
|
|
|
|
if (db_column_exists('purchases', 'status')) {
|
|
$where[] = "p.status != 'paid'";
|
|
}
|
|
|
|
if (db_column_exists('purchases', 'due_date')) {
|
|
$where[] = 'p.due_date IS NOT NULL';
|
|
$where[] = 'p.due_date <= DATE_ADD(CURDATE(), INTERVAL 7 DAY)';
|
|
}
|
|
|
|
$outletScope = outlet_scope_sql('purchases', 'p.outlet_id');
|
|
if ($outletScope['sql'] !== '1=1') {
|
|
$where[] = $outletScope['sql'];
|
|
$params = array_merge($params, $outletScope['params']);
|
|
}
|
|
|
|
$whereSql = $where === [] ? '1=1' : implode(' AND ', $where);
|
|
$sql = "SELECT p.id, {$dueDateExpression} AS due_date, {$totalExpression} AS total_with_vat, {$supplierExpression} AS supplier_name"
|
|
. ' FROM purchases p'
|
|
. $joinClause
|
|
. ' WHERE '
|
|
. $whereSql
|
|
. $orderBy;
|
|
|
|
$stmt = $db->prepare($sql);
|
|
$stmt->execute($params);
|
|
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 = "
|
|
<div class='d-flex justify-content-end align-items-center mb-2'>
|
|
<label class='me-2 small text-muted' data-en='Rows per page:' data-ar='الصفوف لكل صفحة:'>Rows per page:</label>
|
|
<select class='form-select form-select-sm w-auto' onchange='window.location.href=this.value'>
|
|
<option value='index.php?" . http_build_query(array_merge($_GET, ['limit' => 5, 'p' => 1])) . "' " . ($limit == 5 ? 'selected' : '') . ">5</option>
|
|
<option value='index.php?" . http_build_query(array_merge($_GET, ['limit' => 20, 'p' => 1])) . "' " . ($limit == 20 ? 'selected' : '') . ">20</option>
|
|
<option value='index.php?" . http_build_query(array_merge($_GET, ['limit' => 40, 'p' => 1])) . "' " . ($limit == 40 ? 'selected' : '') . ">40</option>
|
|
<option value='index.php?" . http_build_query(array_merge($_GET, ['limit' => 100, 'p' => 1])) . "' " . ($limit == 100 ? 'selected' : '') . ">100</option>
|
|
<option value='index.php?" . http_build_query(array_merge($_GET, ['limit' => 200, 'p' => 1])) . "' " . ($limit == 200 ? 'selected' : '') . ">200</option>
|
|
<option value='index.php?" . http_build_query(array_merge($_GET, ['limit' => 500, 'p' => 1])) . "' " . ($limit == 500 ? 'selected' : '') . ">500</option>
|
|
</select>
|
|
</div>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
let scriptTag = document.currentScript;
|
|
if(scriptTag) {
|
|
let container = scriptTag.parentElement;
|
|
let grid = container.previousElementSibling;
|
|
if (grid && grid.classList.contains('table-responsive')) {
|
|
grid.parentNode.insertBefore(container.querySelector('.d-flex'), grid);
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
";
|
|
|
|
if ($totalPages <= 1) return $limitHtml;
|
|
|
|
$html = '<nav aria-label="Page navigation" class="mt-4"><ul class="pagination justify-content-center">';
|
|
|
|
// Previous
|
|
$disabled = ($currentPage <= 1) ? 'disabled' : '';
|
|
$html .= '<li class="page-item ' . $disabled . '"><a class="page-link" href="' . $url . ($currentPage - 1) . '"><i class="bi bi-chevron-left"></i></a></li>';
|
|
|
|
// Pages
|
|
$start = max(1, $currentPage - 2);
|
|
$end = min($totalPages, $currentPage + 2);
|
|
|
|
if ($start > 1) {
|
|
$html .= '<li class="page-item"><a class="page-link" href="' . $url . '1">1</a></li>';
|
|
if ($start > 2) $html .= '<li class="page-item disabled"><span class="page-link">...</span></li>';
|
|
}
|
|
|
|
for ($i = $start; $i <= $end; $i++) {
|
|
$active = ($i == $currentPage) ? 'active' : '';
|
|
$html .= '<li class="page-item ' . $active . '"><a class="page-link" href="' . $url . $i . '">' . $i . '</a></li>';
|
|
}
|
|
|
|
if ($end < $totalPages) {
|
|
if ($end < $totalPages - 1) $html .= '<li class="page-item disabled"><span class="page-link">...</span></li>';
|
|
$html .= '<li class="page-item"><a class="page-link" href="' . $url . $totalPages . '">' . $totalPages . '</a></li>';
|
|
}
|
|
|
|
// Next
|
|
$disabled = ($currentPage >= $totalPages) ? 'disabled' : '';
|
|
$html .= '<li class="page-item ' . $disabled . '"><a class="page-link" href="' . $url . ($currentPage + 1) . '"><i class="bi bi-chevron-right"></i></a></li>';
|
|
|
|
$html .= '</ul></nav>';
|
|
return $html;
|
|
}
|
|
|
|
// Login Logic
|
|
$login_error = '';
|
|
$login_username = '';
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login'])) {
|
|
$user = trim((string)($_POST['username'] ?? ''));
|
|
$login_username = $user;
|
|
$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";
|
|
app_debug_file_log('login_debug.log', date('Y-m-d H:i:s') . " - Failed login for '$user'. Reason: $reason");
|
|
} catch (Throwable $e) {
|
|
app_debug_file_log('login_debug.log', date('Y-m-d H:i:s') . " - Login exception for '$user': " . $e->getMessage());
|
|
$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') {
|
|
header('Content-Type: application/json');
|
|
$q = trim((string)($_GET['q'] ?? ''));
|
|
app_debug_file_log('search_debug.log', date('Y-m-d H:i:s') . " - search_items call: q=" . $q);
|
|
if ($q === '') {
|
|
echo json_encode([]);
|
|
exit;
|
|
}
|
|
|
|
$searchTerm = "%$q%";
|
|
$startsWith = $q . '%';
|
|
$oid = current_outlet_id();
|
|
$sql = "SELECT i.*, i.stock_quantity FROM stock_items i WHERE ";
|
|
$params = [];
|
|
|
|
if ($oid > 0) {
|
|
$sql .= "i.outlet_id = ? AND ";
|
|
$params[] = $oid;
|
|
}
|
|
|
|
$sql .= "(i.name_en LIKE ? OR i.name_ar LIKE ? OR i.sku LIKE ?) ";
|
|
$sql .= "ORDER BY CASE WHEN i.sku = ? THEN 0 WHEN i.sku LIKE ? THEN 1 WHEN i.name_en LIKE ? THEN 2 WHEN i.name_ar LIKE ? THEN 3 ELSE 4 END, i.name_en ASC LIMIT 15";
|
|
|
|
$params[] = $searchTerm;
|
|
$params[] = $searchTerm;
|
|
$params[] = $searchTerm;
|
|
$params[] = $q;
|
|
$params[] = $startsWith;
|
|
$params[] = $startsWith;
|
|
$params[] = $startsWith;
|
|
|
|
$stmt = db()->prepare($sql);
|
|
$stmt->execute($params);
|
|
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
|
|
?>
|
|
<div class="product-card" data-id="<?= $p['id'] ?>" data-name-en="<?= htmlspecialchars($p['name_en']) ?>" data-name-ar="<?= htmlspecialchars($p['name_ar']) ?>" data-price="<?= $p['sale_price'] ?>" data-sku="<?= htmlspecialchars($p['sku']) ?>" data-stock-quantity="<?= (float)$p['stock_quantity'] ?>" data-vat-rate="<?= $p['vat_rate'] ?>">
|
|
<?php if ($p['image_path']): ?>
|
|
<img src="<?= htmlspecialchars($p['image_path']) ?>" alt="<?= htmlspecialchars($p['name_en']) ?>">
|
|
<?php else: ?>
|
|
<div class="bg-light d-flex align-items-center justify-content-center rounded mb-2" style="height: 120px;">
|
|
<i class="bi bi-box-seam text-muted" style="font-size: 3rem;"></i>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="mb-1 product-name" data-en="<?= htmlspecialchars($p['name_en']) ?>" data-ar="<?= htmlspecialchars($p['name_ar']) ?>">
|
|
<?php if(!empty($p['name_ar'])): ?>
|
|
<div><?= htmlspecialchars($p['name_ar']) ?></div>
|
|
<div class="small text-secondary" style="font-size: 0.75rem; line-height: 1.1;"><?= htmlspecialchars($p['name_en']) ?></div>
|
|
<?php else: ?>
|
|
<div><?= htmlspecialchars($p['name_en']) ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="d-flex justify-content-between align-items-center mt-auto">
|
|
<div class="d-flex flex-column">
|
|
<?php if ($p['sale_price'] < $p['original_price']): ?>
|
|
<span class="text-muted smaller text-decoration-line-through">OMR <?= number_format($p['original_price'], 3) ?></span>
|
|
<?php endif; ?>
|
|
<span class="price text-primary fw-bold">OMR <?= number_format((float)$p['sale_price'], 3) ?></span>
|
|
</div>
|
|
<span class="badge bg-light text-dark small"><?= (float)$p['stock_quantity'] ?> left</span>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'pos_get_item_by_sku') {
|
|
header('Content-Type: application/json');
|
|
$sku = trim((string)($_GET['sku'] ?? ''));
|
|
if ($sku === '') { echo json_encode(null); exit; }
|
|
|
|
$weightBarcode = parseWeightBarcode($sku);
|
|
$lookupSku = $weightBarcode['item_code'] ?? $sku;
|
|
|
|
$oid = current_outlet_id();
|
|
$stmt = db()->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'])) {
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="<?= $lang ?>" dir="<?= $dir ?>">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="description" content="<?= htmlspecialchars($lang === 'ar' ? 'بوابة الدخول إلى لوحة الإدارة والمخزون والمبيعات والمحاسبة.' : 'Secure sign-in for the inventory, sales, POS, and accounting admin panel.') ?>">
|
|
<meta name="robots" content="noindex, nofollow">
|
|
<title><?= __('sign_in') ?> - Admin Panel</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap<?= $dir === 'rtl' ? '.rtl' : '' ?>.min.css" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
color-scheme: light;
|
|
}
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
display: grid;
|
|
place-items: center;
|
|
padding: 1.5rem;
|
|
background:
|
|
radial-gradient(circle at top left, rgba(56, 189, 248, 0.18), transparent 30%),
|
|
radial-gradient(circle at bottom right, rgba(34, 197, 94, 0.16), transparent 24%),
|
|
linear-gradient(135deg, #f8fafc 0%, #eef2ff 55%, #f8fafc 100%);
|
|
font-family: 'Inter', sans-serif;
|
|
color: #0f172a;
|
|
}
|
|
.login-shell {
|
|
width: 100%;
|
|
max-width: 1040px;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1.05fr) minmax(360px, 420px);
|
|
gap: 1.5rem;
|
|
align-items: stretch;
|
|
}
|
|
.login-panel,
|
|
.login-card {
|
|
border: none;
|
|
border-radius: 28px;
|
|
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.12);
|
|
overflow: hidden;
|
|
}
|
|
.login-panel {
|
|
position: relative;
|
|
padding: 2.25rem;
|
|
background: linear-gradient(145deg, #0f172a 0%, #1d4ed8 50%, #38bdf8 100%);
|
|
color: #e2e8f0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
min-height: 600px;
|
|
}
|
|
.login-panel::after {
|
|
content: '';
|
|
position: absolute;
|
|
inset: auto -12% -18% auto;
|
|
width: 240px;
|
|
height: 240px;
|
|
border-radius: 50%;
|
|
background: rgba(255, 255, 255, 0.12);
|
|
filter: blur(12px);
|
|
}
|
|
.login-kicker {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.45rem;
|
|
padding: 0.45rem 0.85rem;
|
|
border-radius: 999px;
|
|
background: rgba(255, 255, 255, 0.14);
|
|
color: #f8fafc;
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
.login-panel h1 {
|
|
font-size: clamp(2.2rem, 3vw, 3.3rem);
|
|
line-height: 1.05;
|
|
margin: 1.25rem 0 1rem;
|
|
}
|
|
.login-panel p {
|
|
max-width: 34rem;
|
|
color: rgba(226, 232, 240, 0.82);
|
|
font-size: 1rem;
|
|
line-height: 1.7;
|
|
}
|
|
.login-feature-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 0.85rem;
|
|
margin-top: 2rem;
|
|
}
|
|
.login-feature {
|
|
padding: 1rem;
|
|
border-radius: 1.1rem;
|
|
background: rgba(255, 255, 255, 0.12);
|
|
backdrop-filter: blur(12px);
|
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
}
|
|
.login-feature span {
|
|
display: block;
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
opacity: 0.7;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.login-feature strong {
|
|
display: block;
|
|
font-size: 1.05rem;
|
|
color: #fff;
|
|
}
|
|
.login-panel-footer {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.65rem;
|
|
margin-top: 2rem;
|
|
}
|
|
.login-panel-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.45rem;
|
|
padding: 0.55rem 0.85rem;
|
|
border-radius: 999px;
|
|
background: rgba(15, 23, 42, 0.2);
|
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
|
color: #f8fafc;
|
|
font-size: 0.82rem;
|
|
font-weight: 600;
|
|
}
|
|
.login-card {
|
|
background: rgba(255, 255, 255, 0.92);
|
|
backdrop-filter: blur(18px);
|
|
padding: 1.75rem;
|
|
align-self: center;
|
|
}
|
|
.login-card__header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.login-language-btn {
|
|
border-radius: 999px;
|
|
padding-inline: 0.9rem;
|
|
font-weight: 600;
|
|
}
|
|
.login-brand {
|
|
display: grid;
|
|
gap: 0.85rem;
|
|
}
|
|
.login-brand__icon {
|
|
width: 64px;
|
|
height: 64px;
|
|
display: grid;
|
|
place-items: center;
|
|
border-radius: 1.4rem;
|
|
background: linear-gradient(135deg, rgba(29, 78, 216, 0.12), rgba(56, 189, 248, 0.18));
|
|
color: #1d4ed8;
|
|
font-size: 1.8rem;
|
|
}
|
|
.login-brand h2 {
|
|
margin: 0;
|
|
font-size: 1.7rem;
|
|
font-weight: 800;
|
|
color: #0f172a;
|
|
}
|
|
.login-brand p {
|
|
margin: 0;
|
|
color: #64748b;
|
|
line-height: 1.65;
|
|
}
|
|
.login-form label {
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
margin-bottom: 0.55rem;
|
|
}
|
|
.login-form .form-control {
|
|
border-radius: 16px;
|
|
padding: 0.9rem 1rem;
|
|
border: 1px solid rgba(148, 163, 184, 0.35);
|
|
box-shadow: none;
|
|
}
|
|
.login-form .form-control:focus {
|
|
border-color: #38bdf8;
|
|
box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.14);
|
|
}
|
|
.password-group {
|
|
position: relative;
|
|
}
|
|
.password-toggle {
|
|
border-radius: 0 16px 16px 0;
|
|
border: 1px solid rgba(148, 163, 184, 0.35);
|
|
border-left: none;
|
|
background: #fff;
|
|
min-width: 54px;
|
|
}
|
|
[dir="rtl"] .password-toggle {
|
|
border-radius: 16px 0 0 16px;
|
|
border-left: 1px solid rgba(148, 163, 184, 0.35);
|
|
border-right: none;
|
|
}
|
|
.password-group .form-control {
|
|
border-radius: 16px 0 0 16px;
|
|
}
|
|
[dir="rtl"] .password-group .form-control {
|
|
border-radius: 0 16px 16px 0;
|
|
}
|
|
.login-note {
|
|
border-radius: 18px;
|
|
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
|
padding: 0.95rem 1rem;
|
|
color: #475569;
|
|
font-size: 0.92rem;
|
|
}
|
|
.btn-primary {
|
|
border-radius: 16px;
|
|
padding: 0.95rem 1rem;
|
|
font-weight: 700;
|
|
background: linear-gradient(135deg, #1d4ed8, #38bdf8);
|
|
border: none;
|
|
box-shadow: 0 14px 28px rgba(29, 78, 216, 0.24);
|
|
}
|
|
.btn-primary:hover,
|
|
.btn-primary:focus {
|
|
background: linear-gradient(135deg, #1e40af, #0ea5e9);
|
|
}
|
|
.caps-lock-note {
|
|
display: none;
|
|
color: #92400e;
|
|
font-size: 0.82rem;
|
|
margin-top: 0.55rem;
|
|
}
|
|
.caps-lock-note.is-visible {
|
|
display: block;
|
|
}
|
|
.login-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.6rem;
|
|
margin-top: 1.2rem;
|
|
}
|
|
.login-meta span {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.45rem;
|
|
padding: 0.45rem 0.75rem;
|
|
border-radius: 999px;
|
|
background: #eff6ff;
|
|
color: #1e40af;
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
}
|
|
.login-footer {
|
|
margin-top: 1.4rem;
|
|
font-size: 0.86rem;
|
|
color: #64748b;
|
|
}
|
|
[dir="rtl"] {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
}
|
|
@media (max-width: 991.98px) {
|
|
body {
|
|
padding: 1rem;
|
|
}
|
|
.login-shell {
|
|
grid-template-columns: 1fr;
|
|
max-width: 480px;
|
|
}
|
|
.login-panel {
|
|
display: none;
|
|
}
|
|
.login-card {
|
|
padding: 1.35rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main class="login-shell">
|
|
<section class="login-panel d-none d-lg-flex" aria-hidden="true">
|
|
<div>
|
|
<span class="login-kicker">
|
|
<i class="bi bi-stars"></i>
|
|
<?= $lang === 'ar' ? 'لوحة تشغيل موحدة' : 'Unified operations workspace' ?>
|
|
</span>
|
|
<h1><?= $lang === 'ar' ? 'قم بإدارة المخزون والمبيعات والمحاسبة من مكان واحد.' : 'Run inventory, sales, POS, and accounting from one clean workspace.' ?></h1>
|
|
<p><?= $lang === 'ar' ? 'واجهة إدارية سريعة مع تتبع الأصناف والفواتير والعملاء والموردين والتقارير اليومية — مصممة لتقليل الخطوات وتسريع العمل.' : 'A faster admin experience for items, invoices, suppliers, customers, and daily reporting—designed to keep routine operations focused and efficient.' ?></p>
|
|
<div class="login-feature-grid">
|
|
<div class="login-feature">
|
|
<span><?= $lang === 'ar' ? 'المخزون' : 'Inventory' ?></span>
|
|
<strong><?= $lang === 'ar' ? 'أصناف ومخزون حي' : 'Live items & stock' ?></strong>
|
|
</div>
|
|
<div class="login-feature">
|
|
<span><?= $lang === 'ar' ? 'المبيعات' : 'Sales' ?></span>
|
|
<strong><?= $lang === 'ar' ? 'فواتير ودفع ونقطة بيع' : 'Invoices, payments & POS' ?></strong>
|
|
</div>
|
|
<div class="login-feature">
|
|
<span><?= $lang === 'ar' ? 'المحاسبة' : 'Accounting' ?></span>
|
|
<strong><?= $lang === 'ar' ? 'تقارير ورقابة يومية' : 'Daily reporting & control' ?></strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="login-panel-footer">
|
|
<span class="login-panel-pill"><i class="bi bi-lightning-charge"></i> <?= $lang === 'ar' ? 'سريع' : 'Fast' ?></span>
|
|
<span class="login-panel-pill"><i class="bi bi-shield-check"></i> <?= $lang === 'ar' ? 'آمن' : 'Secure' ?></span>
|
|
<span class="login-panel-pill"><i class="bi bi-bar-chart"></i> <?= $lang === 'ar' ? 'واضح' : 'Insightful' ?></span>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="login-card">
|
|
<div class="login-card__header">
|
|
<div class="login-brand">
|
|
<div class="login-brand__icon">
|
|
<i class="bi bi-shield-lock"></i>
|
|
</div>
|
|
<div>
|
|
<h2><?= __('welcome_back') ?></h2>
|
|
<p><?= $lang === 'ar' ? 'سجّل الدخول للوصول إلى لوحة الإدارة ومتابعة العمليات اليومية.' : 'Sign in to continue to your admin workspace and daily operations.' ?></p>
|
|
</div>
|
|
</div>
|
|
<a href="?lang=<?= $lang === 'ar' ? 'en' : 'ar' ?>" class="btn btn-sm btn-light border login-language-btn">
|
|
<?= $lang === 'ar' ? 'English' : 'العربية' ?>
|
|
</a>
|
|
</div>
|
|
|
|
<?php if ($login_error): ?>
|
|
<div class="alert alert-danger small py-2 mb-4" role="alert" aria-live="polite"><?= htmlspecialchars($login_error) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<form method="POST" class="login-form">
|
|
<div class="mb-3">
|
|
<label class="form-label" for="loginUsername"><?= __('username') ?></label>
|
|
<input
|
|
type="text"
|
|
id="loginUsername"
|
|
name="username"
|
|
class="form-control"
|
|
placeholder="<?= $lang === 'ar' ? 'اسم المستخدم' : 'Username' ?>"
|
|
value="<?= htmlspecialchars($login_username) ?>"
|
|
required
|
|
autofocus
|
|
autocomplete="username"
|
|
autocapitalize="none"
|
|
spellcheck="false"
|
|
>
|
|
</div>
|
|
|
|
<div class="mb-2">
|
|
<label class="form-label" for="loginPassword"><?= __('password') ?></label>
|
|
<div class="input-group password-group">
|
|
<input
|
|
type="password"
|
|
id="loginPassword"
|
|
name="password"
|
|
class="form-control"
|
|
placeholder="<?= $lang === 'ar' ? 'كلمة المرور' : 'Password' ?>"
|
|
required
|
|
autocomplete="current-password"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="btn btn-light password-toggle"
|
|
id="togglePasswordButton"
|
|
aria-label="<?= htmlspecialchars($lang === 'ar' ? 'إظهار أو إخفاء كلمة المرور' : 'Show or hide password') ?>"
|
|
aria-controls="loginPassword"
|
|
aria-pressed="false"
|
|
>
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
</div>
|
|
<div id="capsLockNotice" class="caps-lock-note">
|
|
<i class="bi bi-exclamation-circle me-1"></i>
|
|
<?= $lang === 'ar' ? 'زر Caps Lock مفعّل.' : 'Caps Lock is on.' ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="login-note mb-4">
|
|
<?= $lang === 'ar'
|
|
? 'إذا فشل تسجيل الدخول، يتم الاحتفاظ باسم المستخدم لتجربة أسرع في المحاولة التالية.'
|
|
: 'Your username stays filled in after a failed sign-in so retrying is faster.' ?>
|
|
</div>
|
|
|
|
<button type="submit" name="login" class="btn btn-primary w-100">
|
|
<?= __('sign_in') ?>
|
|
</button>
|
|
|
|
<div class="login-meta">
|
|
<span><i class="bi bi-grid-1x2"></i> <?= $lang === 'ar' ? 'لوحة إدارية' : 'Admin panel' ?></span>
|
|
<span><i class="bi bi-receipt"></i> <?= $lang === 'ar' ? 'فواتير ونقطة بيع' : 'Invoices & POS' ?></span>
|
|
<span><i class="bi bi-clipboard-data"></i> <?= $lang === 'ar' ? 'تقارير وتشغيل' : 'Reports & ops' ?></span>
|
|
</div>
|
|
|
|
<div class="login-footer">
|
|
<?= $lang === 'ar' ? 'نصيحة: بدّل اللغة من الزر العلوي قبل تسجيل الدخول إذا كنت تفضل العربية أو الإنجليزية.' : 'Tip: switch language from the top button before signing in if you prefer Arabic or English.' ?>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
</main>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
(function () {
|
|
const passwordInput = document.getElementById('loginPassword');
|
|
const toggleButton = document.getElementById('togglePasswordButton');
|
|
const capsLockNotice = document.getElementById('capsLockNotice');
|
|
|
|
if (!passwordInput || !toggleButton) {
|
|
return;
|
|
}
|
|
|
|
const updateCapsLock = function (event) {
|
|
if (!capsLockNotice || typeof event.getModifierState !== 'function') {
|
|
return;
|
|
}
|
|
const isOn = event.getModifierState('CapsLock');
|
|
capsLockNotice.classList.toggle('is-visible', isOn);
|
|
};
|
|
|
|
toggleButton.addEventListener('click', function () {
|
|
const nextType = passwordInput.type === 'password' ? 'text' : 'password';
|
|
passwordInput.type = nextType;
|
|
this.setAttribute('aria-pressed', nextType === 'text' ? 'true' : 'false');
|
|
this.innerHTML = nextType === 'text'
|
|
? '<i class="bi bi-eye-slash"></i>'
|
|
: '<i class="bi bi-eye"></i>';
|
|
passwordInput.focus();
|
|
if (typeof passwordInput.setSelectionRange === 'function') {
|
|
const end = passwordInput.value.length;
|
|
passwordInput.setSelectionRange(end, end);
|
|
}
|
|
});
|
|
|
|
passwordInput.addEventListener('keydown', updateCapsLock);
|
|
passwordInput.addEventListener('keyup', updateCapsLock);
|
|
passwordInput.addEventListener('blur', function () {
|
|
if (capsLockNotice) {
|
|
capsLockNotice.classList.remove('is-visible');
|
|
}
|
|
});
|
|
}());
|
|
</script>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
exit;
|
|
}
|
|
|
|
// Handle POST Requests
|
|
$message = $_SESSION['message'] ?? '';
|
|
unset($_SESSION['message']);
|
|
|
|
function getSettingValue(string $key, ?string $default = null): ?string {
|
|
static $cache = [];
|
|
if (array_key_exists($key, $cache)) return $cache[$key];
|
|
try {
|
|
$stmt = db()->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!");
|
|
}
|
|
|
|
// Sales/Purchases save/update handlers
|
|
require_once __DIR__ . '/pages/sales_purchases_save_logic.php';
|
|
|
|
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!";
|
|
}
|
|
|
|
|
|
// --- 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' => [],
|
|
'dashboard_scope_label' => '',
|
|
'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,
|
|
'total_paid' => 0,
|
|
'total_payable' => 0,
|
|
'total_customers' => 0,
|
|
'total_items' => 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 "<table border='1'>";
|
|
} 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 = [];
|
|
$referenceSearchColumn = db_column_exists($table, 'transaction_no') ? 'transaction_no' : null;
|
|
if (!empty($_GET['search'])) {
|
|
$s = trim((string)$_GET['search']);
|
|
$clean_id = preg_replace('/[^0-9]/', '', $s);
|
|
$searchClauses = ["CAST(v.id AS CHAR) LIKE ?", "c.name LIKE ?"];
|
|
$searchParams = ["%$s%", "%$s%"];
|
|
|
|
if ($referenceSearchColumn !== null) {
|
|
$searchClauses[] = "v.$referenceSearchColumn LIKE ?";
|
|
$searchParams[] = "%$s%";
|
|
}
|
|
|
|
if ($clean_id !== '') {
|
|
$searchClauses[] = "v.id = ?";
|
|
$searchParams[] = $clean_id;
|
|
}
|
|
|
|
$where[] = '(' . implode(' OR ', $searchClauses) . ')';
|
|
$params = array_merge($params, $searchParams);
|
|
}
|
|
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 "<tr>";
|
|
foreach ($headers as $h) echo "<th>" . htmlspecialchars($h) . "</th>";
|
|
echo "</tr>";
|
|
foreach ($rows as $row) {
|
|
echo "<tr>";
|
|
foreach ($row as $val) echo "<td>" . htmlspecialchars((string)$val) . "</td>";
|
|
echo "</tr>";
|
|
}
|
|
echo "</table>";
|
|
} 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':
|
|
app_debug_file_log('debug.log', date('Y-m-d H:i:s') . " - Items case hit");
|
|
$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':
|
|
require 'pages/sales_purchases_logic.php';
|
|
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':
|
|
$data['expense_categories'] = db()->query("SELECT * FROM expense_categories ORDER BY name_en ASC")->fetchAll();
|
|
$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')) {
|
|
$db = db();
|
|
$scalar = static function (string $sql, array $params = []) use ($db) {
|
|
$stmt = $db->prepare($sql);
|
|
$stmt->execute($params);
|
|
return $stmt->fetchColumn();
|
|
};
|
|
|
|
$data['dashboard_scope_label'] = current_outlet_name();
|
|
|
|
$customerScope = outlet_scope_sql('customers', 'outlet_id');
|
|
if (db_table_exists('customers')) {
|
|
$customerStmt = $db->prepare("SELECT * FROM customers WHERE {$customerScope['sql']} ORDER BY id DESC LIMIT 5");
|
|
$customerStmt->execute($customerScope['params']);
|
|
$data['customers'] = $customerStmt->fetchAll();
|
|
$data['stats']['total_customers'] = (int)($scalar("SELECT COUNT(*) FROM customers WHERE {$customerScope['sql']}", $customerScope['params']) ?: 0);
|
|
}
|
|
|
|
$itemScope = outlet_scope_sql('stock_items', 'outlet_id');
|
|
if (db_table_exists('stock_items')) {
|
|
$data['stats']['total_items'] = (int)($scalar("SELECT COUNT(*) FROM stock_items WHERE {$itemScope['sql']}", $itemScope['params']) ?: 0);
|
|
|
|
if (db_column_exists('stock_items', 'expiry_date')) {
|
|
$data['stats']['expired_items'] = (int)($scalar("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date <= CURDATE() AND {$itemScope['sql']}", $itemScope['params']) ?: 0);
|
|
$data['stats']['near_expiry_items'] = (int)($scalar("SELECT COUNT(*) FROM stock_items WHERE expiry_date IS NOT NULL AND expiry_date > CURDATE() AND expiry_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY) AND {$itemScope['sql']}", $itemScope['params']) ?: 0);
|
|
}
|
|
|
|
if (db_column_exists('stock_items', 'stock_quantity') && db_column_exists('stock_items', 'min_stock_level')) {
|
|
$data['stats']['low_stock_items_count'] = (int)($scalar("SELECT COUNT(*) FROM stock_items WHERE stock_quantity <= min_stock_level AND {$itemScope['sql']}", $itemScope['params']) ?: 0);
|
|
}
|
|
}
|
|
|
|
if (db_table_exists('invoices')) {
|
|
$invoiceScope = outlet_scope_sql('invoices', 'outlet_id');
|
|
$invoiceTotalExpression = db_column_exists('invoices', 'total_with_vat') ? 'COALESCE(total_with_vat, 0)' : (db_column_exists('invoices', 'total_amount') ? 'COALESCE(total_amount, 0)' : '0');
|
|
$data['stats']['total_sales'] += (float)($scalar("SELECT COALESCE(SUM({$invoiceTotalExpression}), 0) FROM invoices WHERE {$invoiceScope['sql']}", $invoiceScope['params']) ?: 0);
|
|
}
|
|
|
|
if (db_table_exists('pos_transactions')) {
|
|
$posScope = outlet_scope_sql('pos_transactions', 'outlet_id');
|
|
$posTotalExpression = db_column_exists('pos_transactions', 'net_amount') ? 'COALESCE(net_amount, 0)' : (db_column_exists('pos_transactions', 'total_amount') ? 'COALESCE(total_amount, 0)' : '0');
|
|
$posWhere = [];
|
|
$posParams = [];
|
|
if (db_column_exists('pos_transactions', 'status')) {
|
|
$posWhere[] = 'status = ?';
|
|
$posParams[] = 'completed';
|
|
}
|
|
if ($posScope['sql'] !== '1=1') {
|
|
$posWhere[] = $posScope['sql'];
|
|
$posParams = array_merge($posParams, $posScope['params']);
|
|
}
|
|
$posWhereSql = $posWhere === [] ? '1=1' : implode(' AND ', $posWhere);
|
|
$data['stats']['total_sales'] += (float)($scalar("SELECT COALESCE(SUM({$posTotalExpression}), 0) FROM pos_transactions WHERE {$posWhereSql}", $posParams) ?: 0);
|
|
}
|
|
|
|
if (db_table_exists('payments') && db_table_exists('invoices') && db_column_exists('payments', 'invoice_id')) {
|
|
$paymentInvoiceScope = outlet_scope_sql('invoices', 'i.outlet_id');
|
|
$paymentInvoiceWhere = $paymentInvoiceScope['sql'] === '1=1' ? '1=1' : $paymentInvoiceScope['sql'];
|
|
$data['stats']['total_received'] += (float)($scalar("SELECT COALESCE(SUM(p.amount), 0) FROM payments p JOIN invoices i ON p.invoice_id = i.id WHERE {$paymentInvoiceWhere}", $paymentInvoiceScope['params']) ?: 0);
|
|
}
|
|
|
|
if (db_table_exists('pos_payments') && db_table_exists('pos_transactions') && db_column_exists('pos_payments', 'transaction_id')) {
|
|
$paymentPosScope = outlet_scope_sql('pos_transactions', 't.outlet_id');
|
|
$paymentPosWhere = [];
|
|
$paymentPosParams = [];
|
|
if (db_column_exists('pos_transactions', 'status')) {
|
|
$paymentPosWhere[] = 't.status = ?';
|
|
$paymentPosParams[] = 'completed';
|
|
}
|
|
if ($paymentPosScope['sql'] !== '1=1') {
|
|
$paymentPosWhere[] = $paymentPosScope['sql'];
|
|
$paymentPosParams = array_merge($paymentPosParams, $paymentPosScope['params']);
|
|
}
|
|
$paymentPosWhereSql = $paymentPosWhere === [] ? '1=1' : implode(' AND ', $paymentPosWhere);
|
|
$data['stats']['total_received'] += (float)($scalar("SELECT COALESCE(SUM(pp.amount), 0) FROM pos_payments pp JOIN pos_transactions t ON pp.transaction_id = t.id WHERE {$paymentPosWhereSql}", $paymentPosParams) ?: 0);
|
|
}
|
|
|
|
if (db_table_exists('purchases')) {
|
|
$purchaseScope = outlet_scope_sql('purchases', 'outlet_id');
|
|
$purchaseTotalExpression = db_column_exists('purchases', 'total_with_vat') ? 'COALESCE(total_with_vat, 0)' : (db_column_exists('purchases', 'total_amount') ? 'COALESCE(total_amount, 0)' : '0');
|
|
$data['stats']['total_purchases'] = (float)($scalar("SELECT COALESCE(SUM({$purchaseTotalExpression}), 0) FROM purchases WHERE {$purchaseScope['sql']}", $purchaseScope['params']) ?: 0);
|
|
}
|
|
|
|
if (db_table_exists('purchase_payments') && db_table_exists('purchases') && db_column_exists('purchase_payments', 'purchase_id')) {
|
|
$purchasePaymentScope = outlet_scope_sql('purchases', 'p.outlet_id');
|
|
$purchasePaymentWhere = $purchasePaymentScope['sql'] === '1=1' ? '1=1' : $purchasePaymentScope['sql'];
|
|
$data['stats']['total_paid'] = (float)($scalar("SELECT COALESCE(SUM(pp.amount), 0) FROM purchase_payments pp JOIN purchases p ON pp.purchase_id = p.id WHERE {$purchasePaymentWhere}", $purchasePaymentScope['params']) ?: 0);
|
|
}
|
|
|
|
$data['stats']['total_receivable'] = max((float)$data['stats']['total_sales'] - (float)$data['stats']['total_received'], 0);
|
|
$data['stats']['total_payable'] = max((float)$data['stats']['total_purchases'] - (float)$data['stats']['total_paid'], 0);
|
|
|
|
$data['monthly_sales'] = dashboard_sales_series('month', 12);
|
|
$data['yearly_sales'] = dashboard_sales_series('year', 5);
|
|
}
|
|
break;
|
|
}
|
|
|
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Accounting System';
|
|
?>
|
|
<!doctype html>
|
|
<html lang="<?= $lang ?>" dir="<?= $dir ?>">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title><?= htmlspecialchars(__($page)) ?> - Admin Panel</title>
|
|
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
|
<?php if (!empty($data['settings']['favicon'])): ?>
|
|
<link rel="icon" href="<?= htmlspecialchars($data['settings']['favicon']) ?>?v=<?= time() ?>">
|
|
<?php endif; ?>
|
|
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap<?= $dir === 'rtl' ? '.rtl' : '' ?>.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script src="assets/js/main.js?v=<?= time() ?>"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?= time() ?>">
|
|
<style>
|
|
/* Force RTL Sidebar Position */
|
|
[dir="rtl"] .sidebar {
|
|
right: 0 !important;
|
|
left: auto !important;
|
|
}
|
|
[dir="rtl"] .main-content {
|
|
margin-left: 0 !important;
|
|
margin-right: 210px !important;
|
|
}
|
|
@media (max-width: 1199.98px) {
|
|
[dir="rtl"] .sidebar {
|
|
right: -210px !important;
|
|
left: auto !important;
|
|
}
|
|
[dir="rtl"] .sidebar.show {
|
|
right: 0 !important;
|
|
left: auto !important;
|
|
}
|
|
[dir="rtl"] .main-content {
|
|
margin-right: 0 !important;
|
|
margin-left: 0 !important;
|
|
}
|
|
.pos-container {
|
|
flex-direction: column !important;
|
|
height: auto !important;
|
|
}
|
|
.pos-cart {
|
|
width: 100% !important;
|
|
height: auto !important;
|
|
position: sticky;
|
|
bottom: 0;
|
|
z-index: 1001;
|
|
}
|
|
.pos-products {
|
|
height: auto !important;
|
|
max-height: 500px;
|
|
}
|
|
}
|
|
|
|
/* General Responsive Helpers */
|
|
@media (max-width: 767.98px) {
|
|
.table:not(.table-borderless):not(.table-sm) {
|
|
display: block;
|
|
width: 100%;
|
|
overflow-x: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
.card {
|
|
padding: 1rem !important;
|
|
}
|
|
.topbar {
|
|
padding: 0.75rem 1rem !important;
|
|
margin: -1rem -1rem 1rem -1rem !important;
|
|
}
|
|
.main-content {
|
|
padding: 1rem !important;
|
|
}
|
|
.h4, h4 {
|
|
font-size: 1.1rem;
|
|
}
|
|
.btn-sm-square {
|
|
width: 32px;
|
|
height: 32px;
|
|
padding: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.topbar {
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 990;
|
|
}
|
|
.topbar h4 {
|
|
width: 100%;
|
|
order: 2;
|
|
font-size: 1.1rem;
|
|
margin-top: 5px !important;
|
|
}
|
|
.topbar > div:first-child {
|
|
order: 1;
|
|
}
|
|
.topbar > div:last-child {
|
|
order: 3;
|
|
width: 100%;
|
|
justify-content: space-between;
|
|
border-top: 1px solid var(--border);
|
|
padding-top: 5px;
|
|
}
|
|
.topbar .btn span {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@media print {
|
|
.sidebar, .topbar, .d-print-none, .no-print, .btn-group, .btn, .badge i { display: none !important; }
|
|
.main-content { margin: 0 !important; padding: 0 !important; background: white !important; width: 100% !important; }
|
|
.card { border: none !important; box-shadow: none !important; padding: 0 !important; margin: 0 !important; }
|
|
.table { border-collapse: collapse !important; width: 100% !important; margin-top: 20px !important; }
|
|
.table th, .table td { border: 1px solid #000 !important; padding: 8px !important; font-size: 11px !important; color: #000 !important; }
|
|
.table thead th { background-color: #eee !important; color: #000 !important; font-weight: bold !important; text-transform: uppercase; }
|
|
.print-only { display: block !important; }
|
|
.text-success, .text-danger, .text-primary, .text-warning { color: #000 !important; }
|
|
.badge { border: none !important; padding: 0 !important; color: #000 !important; background: transparent !important; font-weight: normal !important; }
|
|
body { font-size: 12px !important; color: #000 !important; background: #fff !important; }
|
|
@page { margin: 1cm; }
|
|
}
|
|
.print-only { display: none; }
|
|
.units-page-card {
|
|
background: linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(247,249,252,0.95) 100%);
|
|
border: 1px solid rgba(15, 23, 42, 0.06);
|
|
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.08);
|
|
}
|
|
.units-shell {
|
|
display: grid;
|
|
gap: 1.5rem;
|
|
}
|
|
.units-hero {
|
|
position: relative;
|
|
overflow: hidden;
|
|
border-radius: 1.5rem;
|
|
padding: 1.75rem;
|
|
background: linear-gradient(135deg, #ffffff 0%, #eef7f2 42%, #fff5ea 100%);
|
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
}
|
|
.units-hero::before,
|
|
.units-hero::after {
|
|
content: '';
|
|
position: absolute;
|
|
border-radius: 999px;
|
|
pointer-events: none;
|
|
}
|
|
.units-hero::before {
|
|
width: 18rem;
|
|
height: 18rem;
|
|
right: -6rem;
|
|
top: -8rem;
|
|
background: radial-gradient(circle, rgba(14, 165, 233, 0.18) 0%, rgba(14, 165, 233, 0) 72%);
|
|
}
|
|
.units-hero::after {
|
|
width: 14rem;
|
|
height: 14rem;
|
|
left: -4rem;
|
|
bottom: -7rem;
|
|
background: radial-gradient(circle, rgba(34, 197, 94, 0.18) 0%, rgba(34, 197, 94, 0) 72%);
|
|
}
|
|
.units-hero__copy,
|
|
.units-toolbar,
|
|
.units-stats {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
.units-eyebrow {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.45rem;
|
|
padding: 0.45rem 0.8rem;
|
|
margin-bottom: 0.85rem;
|
|
border-radius: 999px;
|
|
background: rgba(255, 255, 255, 0.84);
|
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
color: #0f172a;
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
}
|
|
.units-toolbar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.75rem;
|
|
}
|
|
.units-toolbar .btn {
|
|
border-radius: 999px;
|
|
padding: 0.75rem 1rem;
|
|
font-weight: 600;
|
|
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.08);
|
|
}
|
|
.units-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 1rem;
|
|
margin-top: 1.5rem;
|
|
}
|
|
.units-stat {
|
|
padding: 1rem 1.15rem;
|
|
border-radius: 1.25rem;
|
|
background: rgba(255, 255, 255, 0.84);
|
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
backdrop-filter: blur(8px);
|
|
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05);
|
|
}
|
|
.units-stat__label {
|
|
display: block;
|
|
font-size: 0.78rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: #64748b;
|
|
margin-bottom: 0.35rem;
|
|
}
|
|
.units-stat strong {
|
|
display: block;
|
|
font-size: 1.5rem;
|
|
line-height: 1;
|
|
color: #0f172a;
|
|
}
|
|
.units-stat small {
|
|
display: block;
|
|
margin-top: 0.45rem;
|
|
color: #64748b;
|
|
}
|
|
.units-table-card {
|
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
border-radius: 1.25rem;
|
|
background: #fff;
|
|
overflow: hidden;
|
|
}
|
|
.units-table-card__header {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 1.25rem 1.5rem;
|
|
border-bottom: 1px solid rgba(15, 23, 42, 0.06);
|
|
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
|
}
|
|
.units-helper-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.45rem;
|
|
padding: 0.55rem 0.85rem;
|
|
border-radius: 999px;
|
|
background: #f1f5f9;
|
|
color: #0f172a;
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
}
|
|
.units-table thead th {
|
|
font-size: 0.78rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: #64748b;
|
|
border-bottom-color: rgba(15, 23, 42, 0.06);
|
|
padding: 1rem 1.25rem;
|
|
}
|
|
.units-table tbody td {
|
|
padding: 1.15rem 1.25rem;
|
|
border-bottom-color: rgba(15, 23, 42, 0.06);
|
|
}
|
|
.units-table tbody tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
.units-name-stack {
|
|
display: grid;
|
|
gap: 0.2rem;
|
|
}
|
|
.units-name-stack__primary {
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
}
|
|
.units-name-stack__secondary {
|
|
color: #64748b;
|
|
font-size: 0.95rem;
|
|
}
|
|
.units-short-stack {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
.units-short-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 3.25rem;
|
|
padding: 0.45rem 0.75rem;
|
|
border-radius: 0.8rem;
|
|
background: #ecfdf5;
|
|
color: #166534;
|
|
font-weight: 700;
|
|
font-size: 0.82rem;
|
|
border: 1px solid rgba(22, 101, 52, 0.1);
|
|
}
|
|
.units-short-badge--muted {
|
|
background: #fff7ed;
|
|
color: #9a3412;
|
|
border-color: rgba(154, 52, 18, 0.1);
|
|
}
|
|
.units-readiness {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.45rem;
|
|
}
|
|
.units-status-badge {
|
|
font-weight: 600;
|
|
padding: 0.45rem 0.7rem;
|
|
border-radius: 999px;
|
|
}
|
|
.units-status-badge.is-ready {
|
|
background: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
.units-status-badge.is-pending {
|
|
background: #fee2e2;
|
|
color: #b91c1c;
|
|
}
|
|
.units-actions {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
.units-actions .btn {
|
|
border-radius: 0.85rem;
|
|
padding: 0.45rem 0.75rem;
|
|
}
|
|
.units-empty-state {
|
|
max-width: 24rem;
|
|
margin: 0 auto;
|
|
display: grid;
|
|
gap: 0.75rem;
|
|
justify-items: center;
|
|
}
|
|
.units-empty-state__icon {
|
|
width: 4rem;
|
|
height: 4rem;
|
|
display: grid;
|
|
place-items: center;
|
|
border-radius: 1.2rem;
|
|
background: linear-gradient(135deg, #e0f2fe, #dcfce7);
|
|
color: #0f172a;
|
|
font-size: 1.5rem;
|
|
}
|
|
.unit-modal .modal-header {
|
|
padding: 1.5rem 1.5rem 0;
|
|
}
|
|
.units-modal-kicker {
|
|
display: inline-flex;
|
|
padding: 0.35rem 0.7rem;
|
|
border-radius: 999px;
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
background: #e0f2fe;
|
|
color: #0c4a6e;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
.unit-form-shell {
|
|
display: grid;
|
|
gap: 1rem;
|
|
}
|
|
.unit-form-section {
|
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
border-radius: 1.2rem;
|
|
padding: 1rem;
|
|
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
|
}
|
|
.unit-form-section__header {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.75rem;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 1rem;
|
|
}
|
|
.unit-form-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
.unit-field label {
|
|
font-weight: 600;
|
|
color: #0f172a;
|
|
}
|
|
.unit-field .form-text {
|
|
color: #64748b;
|
|
font-size: 0.82rem;
|
|
}
|
|
.unit-preview-card {
|
|
border-radius: 1rem;
|
|
padding: 1rem;
|
|
background: linear-gradient(135deg, #0f172a, #1e293b);
|
|
color: #f8fafc;
|
|
}
|
|
.unit-preview-card__label {
|
|
display: block;
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
opacity: 0.72;
|
|
margin-bottom: 0.65rem;
|
|
}
|
|
.unit-preview-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
.unit-preview-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 0.45rem 0.7rem;
|
|
border-radius: 999px;
|
|
background: rgba(255, 255, 255, 0.12);
|
|
backdrop-filter: blur(6px);
|
|
}
|
|
.unit-preview-chip.is-muted {
|
|
background: rgba(255, 255, 255, 0.2);
|
|
}
|
|
.unit-import-note {
|
|
border-radius: 1rem;
|
|
padding: 1rem;
|
|
background: linear-gradient(135deg, #eff6ff 0%, #fefce8 100%);
|
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
|
}
|
|
.unit-import-note ul {
|
|
padding-left: 1.25rem;
|
|
margin-bottom: 0;
|
|
color: #475569;
|
|
}
|
|
[dir="rtl"] .unit-import-note ul {
|
|
padding-left: 0;
|
|
padding-right: 1.25rem;
|
|
}
|
|
@media (max-width: 991.98px) {
|
|
.units-stats {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
}
|
|
@media (max-width: 767.98px) {
|
|
.units-hero {
|
|
padding: 1.25rem;
|
|
}
|
|
.units-stats,
|
|
.unit-form-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.units-table-card__header {
|
|
padding: 1rem;
|
|
}
|
|
.units-table thead th,
|
|
.units-table tbody td {
|
|
padding: 0.95rem 0.85rem;
|
|
}
|
|
.unit-modal .modal-header {
|
|
padding: 1.25rem 1.25rem 0;
|
|
}
|
|
}
|
|
[dir="rtl"] { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
|
|
</style>
|
|
</head>
|
|
<body class="theme-<?= htmlspecialchars($_SESSION['theme'] ?? 'default') ?>">
|
|
|
|
<?php if (!$is_activated && $trial_days > 0): ?>
|
|
<div class="alert alert-warning text-center mb-0 rounded-0 d-print-none py-2" style="position: sticky; top: 0; z-index: 2000; font-size: 0.85rem;">
|
|
<i class="bi bi-info-circle-fill me-2"></i>
|
|
<?= $lang === 'ar' ? "نسخة تجريبية: متبقي $trial_days يوم" : "Trial Version: $trial_days days remaining" ?>.
|
|
<a href="index.php?page=activate" class="alert-link ms-2 fw-bold"><?= __('activate_now') ?></a>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="sidebar">
|
|
<div class="sidebar-header text-center">
|
|
<div class="text-white fw-bold"><i class="fas fa-balance-scale me-2"></i> Meezan Accounting System</div>
|
|
<div class="text-white-50 small" style="font-size: 0.7rem;">System v1.2.5</div>
|
|
</div>
|
|
<nav class="mt-4">
|
|
<!-- General Section -->
|
|
<a href="index.php?page=dashboard" class="nav-link <?= !isset($_GET['page']) || $_GET['page'] === 'dashboard' ? 'active' : '' ?>">
|
|
<i class="fas fa-chart-pie"></i> <span><?= __('dashboard') ?></span>
|
|
</a>
|
|
|
|
<!-- POS Section -->
|
|
<?php if (can('pos_view')): ?>
|
|
<a href="index.php?page=pos" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'pos' ? 'active' : '' ?>">
|
|
<i class="fas fa-cash-register"></i> <span><?= __('pos') ?></span>
|
|
</a>
|
|
<?php endif; ?>
|
|
|
|
<!-- Inventory Section -->
|
|
<?php if (can('items_view') || can('categories_view') || can('units_view')): ?>
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['items', 'categories', 'units']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#stock-collapse">
|
|
<span><i class="fas fa-boxes-stacked group-icon"></i><span><?= __('inventory') ?></span></span>
|
|
<i class="fas fa-chevron-down chevron"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['items', 'categories', 'units']) ? 'show' : '' ?>" id="stock-collapse">
|
|
<?php if (can('items_view')): ?>
|
|
<a href="index.php?page=items" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'items' ? 'active' : '' ?>">
|
|
<i class="fas fa-box"></i> <span><?= __('items') ?></span>
|
|
</a>
|
|
<?php endif; ?>
|
|
<?php if (can('categories_view')): ?>
|
|
<a href="index.php?page=categories" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'categories' ? 'active' : '' ?>">
|
|
<i class="fas fa-tags"></i> <span><?= __('categories') ?></span>
|
|
</a>
|
|
<?php endif; ?>
|
|
<?php if (can('units_view')): ?>
|
|
<a href="index.php?page=units" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'units' ? 'active' : '' ?>">
|
|
<i class="fas fa-ruler-combined"></i> <span><?= __('units') ?></span>
|
|
</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Customers Section -->
|
|
<?php if (can('customers_view')): ?>
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['customers']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#customers-collapse">
|
|
<span><i class="fas fa-users group-icon"></i><span><?= __('customers') ?></span></span>
|
|
<i class="fas fa-chevron-down chevron"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['customers']) ? 'show' : '' ?>" id="customers-collapse">
|
|
<a href="index.php?page=customers" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'customers' ? 'active' : '' ?>">
|
|
<i class="fas fa-users"></i> <span><?= __('customers') ?></span>
|
|
</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Suppliers Section -->
|
|
<?php if (can('suppliers_view')): ?>
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['suppliers']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#suppliers-collapse">
|
|
<span><i class="fas fa-truck-field group-icon"></i><span><?= __('suppliers') ?></span></span>
|
|
<i class="fas fa-chevron-down chevron"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['suppliers']) ? 'show' : '' ?>" id="suppliers-collapse">
|
|
<a href="index.php?page=suppliers" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'suppliers' ? 'active' : '' ?>">
|
|
<i class="fas fa-truck-field"></i> <span><?= __('suppliers') ?></span>
|
|
</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Sales Section -->
|
|
<?php if (can('sales_view') || can('sales_returns_view') || can('quotations_view')): ?>
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['sales', 'sales_returns', 'quotations']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#sales-collapse">
|
|
<span><i class="fas fa-file-invoice-dollar group-icon"></i><span><?= __('sales') ?></span></span>
|
|
<i class="fas fa-chevron-down chevron"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['sales', 'sales_returns', 'quotations']) ? 'show' : '' ?>" id="sales-collapse">
|
|
<?php if (can('sales_view')): ?>
|
|
<a href="index.php?page=sales" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'sales' ? 'active' : '' ?>">
|
|
<i class="fas fa-file-invoice-dollar"></i> <span><?= __('sales') ?></span>
|
|
</a>
|
|
<?php endif; ?>
|
|
<?php if (can('sales_returns_view')): ?>
|
|
<a href="index.php?page=sales_returns" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'sales_returns' ? 'active' : '' ?>">
|
|
<i class="fas fa-reply"></i> <span><?= __('sales_returns') ?></span>
|
|
</a>
|
|
<?php endif; ?>
|
|
<?php if (can('quotations_view')): ?>
|
|
<a href="index.php?page=quotations" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'quotations' ? 'active' : '' ?>">
|
|
<i class="fas fa-file-lines"></i> <span><?= __('quotations') ?></span>
|
|
</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Purchases Section -->
|
|
<?php if (can('purchases_view') || can('lpos_view') || can('purchase_returns_view')): ?>
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['purchases', 'lpos', 'purchase_returns']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#purchases-collapse">
|
|
<span><i class="fas fa-cart-shopping group-icon"></i><span><?= __('purchases') ?></span></span>
|
|
<i class="fas fa-chevron-down chevron"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['purchases', 'lpos', 'purchase_returns']) ? 'show' : '' ?>" id="purchases-collapse">
|
|
<?php if (can('purchases_view')): ?>
|
|
<a href="index.php?page=purchases" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'purchases' ? 'active' : '' ?>">
|
|
<i class="fas fa-cart-shopping"></i> <span><?= __('purchases') ?></span>
|
|
</a>
|
|
<?php endif; ?>
|
|
<?php if (can('lpos_view')): ?>
|
|
<a href="index.php?page=lpos" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'lpos' ? 'active' : '' ?>">
|
|
<i class="fas fa-file-contract"></i> <span><?= __('lpos') ?></span>
|
|
</a>
|
|
<?php endif; ?>
|
|
<?php if (can('purchase_returns_view')): ?>
|
|
<a href="index.php?page=purchase_returns" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'purchase_returns' ? 'active' : '' ?>">
|
|
<i class="fas fa-share"></i> <span><?= __('purchase_returns') ?></span>
|
|
</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Expenses Section -->
|
|
<?php if (can('accounting_view')): ?>
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['expense_categories', 'expenses']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#expenses-collapse">
|
|
<span><i class="fas fa-wallet group-icon"></i><span><?= __('expenses') ?></span></span>
|
|
<i class="fas fa-chevron-down chevron"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['expense_categories', 'expenses']) ? 'show' : '' ?>" id="expenses-collapse">
|
|
<a href="index.php?page=expense_categories" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'expense_categories' ? 'active' : '' ?>">
|
|
<i class="fas fa-layer-group"></i> <span><?= __('expense_categories') ?></span>
|
|
</a>
|
|
<a href="index.php?page=expenses" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'expenses' ? 'active' : '' ?>">
|
|
<i class="fas fa-file-invoice"></i> <span><?= __('expenses') ?></span>
|
|
</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Accounting Section -->
|
|
<?php if (can('accounting_view')): ?>
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['accounting', 'expense_report']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#accounting-collapse">
|
|
<span><i class="fas fa-calculator group-icon"></i><span><?= __('accounting') ?></span></span>
|
|
<i class="fas fa-chevron-down chevron"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['accounting', 'expense_report']) ? 'show' : '' ?>" id="accounting-collapse">
|
|
<a href="index.php?page=accounting" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'accounting' && !isset($_GET['view']) ? 'active' : '' ?>">
|
|
<i class="fas fa-book-open"></i> <span><?= __('accounting') ?></span>
|
|
</a>
|
|
<a href="index.php?page=accounting&view=coa" class="nav-link <?= isset($_GET['view']) && $_GET['view'] === 'coa' ? 'active' : '' ?>">
|
|
<i class="fas fa-sitemap"></i> <span data-en="Accounts" data-ar="الحسابات">Accounts</span>
|
|
</a>
|
|
<a href="index.php?page=accounting&view=trial_balance" class="nav-link <?= isset($_GET['view']) && $_GET['view'] === 'trial_balance' ? 'active' : '' ?>">
|
|
<i class="fas fa-scale-balanced"></i> <span><?= __('trial_balance') ?></span>
|
|
</a>
|
|
<a href="index.php?page=accounting&view=profit_loss" class="nav-link <?= isset($_GET['view']) && $_GET['view'] === 'profit_loss' ? 'active' : '' ?>">
|
|
<i class="fas fa-chart-column"></i> <span><?= __('profit_loss') ?></span>
|
|
</a>
|
|
<a href="index.php?page=accounting&view=balance_sheet" class="nav-link <?= isset($_GET['view']) && $_GET['view'] === 'balance_sheet' ? 'active' : '' ?>">
|
|
<i class="fas fa-file-contract"></i> <span><?= __('balance_sheet') ?></span>
|
|
</a>
|
|
<a href="index.php?page=accounting&view=vat_report" class="nav-link <?= isset($_GET['view']) && $_GET['view'] === 'vat_report' ? 'active' : '' ?>">
|
|
<i class="fas fa-receipt"></i> <span><?= __('vat_report') ?></span>
|
|
</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- HR Section -->
|
|
<?php if (can('hr_view')): ?>
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['hr_employees', 'hr_departments', 'hr_attendance', 'hr_payroll']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#hr-collapse">
|
|
<span><i class="fas fa-user-tie group-icon"></i><span><?= __('hr') ?></span></span>
|
|
<i class="fas fa-chevron-down chevron"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['hr_employees', 'hr_departments', 'hr_attendance', 'hr_payroll']) ? 'show' : '' ?>" id="hr-collapse">
|
|
<a href="index.php?page=hr_departments" class="nav-link <?= $page === 'hr_departments' ? 'active' : '' ?>">
|
|
<i class="fas fa-building-user"></i> <span><?= __('departments') ?></span>
|
|
</a>
|
|
<a href="index.php?page=hr_employees" class="nav-link <?= $page === 'hr_employees' ? 'active' : '' ?>">
|
|
<i class="fas fa-user-badge"></i> <span><?= __('employees') ?></span>
|
|
</a>
|
|
<a href="index.php?page=hr_attendance" class="nav-link <?= $page === 'hr_attendance' ? 'active' : '' ?>">
|
|
<i class="fas fa-user-check"></i> <span><?= __('attendance') ?></span>
|
|
</a>
|
|
<a href="index.php?page=hr_payroll" class="nav-link <?= $page === 'hr_payroll' ? 'active' : '' ?>">
|
|
<i class="fas fa-sack-dollar"></i> <span><?= __('payroll') ?></span>
|
|
</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Reports Section -->
|
|
<?php if (can('sales_view') || can('purchases_view') || can('items_view') || can('customers_view') || can('suppliers_view')): ?>
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['customer_statement', 'supplier_statement', 'cashflow_report', 'expense_report', 'expiry_report', 'low_stock_report', 'loyalty_history', 'register_sessions']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#reports-collapse">
|
|
<span><i class="fas fa-chart-line group-icon"></i><span><?= __('reports') ?></span></span>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['customer_statement', 'supplier_statement', 'cashflow_report', 'expense_report', 'expiry_report', 'low_stock_report', 'loyalty_history', 'register_sessions', 'register_sessions']) ? 'show' : '' ?>" id="reports-collapse">
|
|
<?php if (can('expenses_view')): ?>
|
|
<a href="index.php?page=expense_report" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'expense_report' ? 'active' : '' ?>">
|
|
<i class="fas fa-file-invoice-dollar"></i> <span><?= __('expense_report') ?></span>
|
|
</a>
|
|
<?php endif; ?>
|
|
<a href="index.php?page=customer_statement" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'customer_statement' ? 'active' : '' ?>">
|
|
<i class="fas fa-file-invoice"></i> <span><?= __('customer_statement') ?></span>
|
|
</a>
|
|
<?php if (can('suppliers_view')): ?>
|
|
<a href="index.php?page=supplier_statement" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'supplier_statement' ? 'active' : '' ?>">
|
|
<i class="fas fa-file-lines"></i> <span><?= __('supplier_statement') ?></span>
|
|
</a>
|
|
<?php endif; ?>
|
|
<?php if (can('accounting_view')): ?>
|
|
<a href="index.php?page=cashflow_report" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'cashflow_report' ? 'active' : '' ?>">
|
|
<i class="fas fa-money-bill-transfer"></i> <span><?= __('cashflow_report') ?></span>
|
|
</a>
|
|
<?php endif; ?>
|
|
<?php if (can('items_view')): ?>
|
|
<a href="index.php?page=expiry_report" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'expiry_report' ? 'active' : '' ?>">
|
|
<i class="fas fa-calendar-xmark"></i> <span><?= __('expiry_report') ?></span>
|
|
</a>
|
|
<a href="index.php?page=low_stock_report" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'low_stock_report' ? 'active' : '' ?>">
|
|
<i class="fas fa-arrow-trend-down"></i> <span><?= __('low_stock_report') ?></span>
|
|
</a>
|
|
<?php endif; ?>
|
|
<?php if (can('customers_view')): ?>
|
|
<a href="index.php?page=loyalty_history" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'loyalty_history' ? 'active' : '' ?>">
|
|
<i class="fas fa-award"></i> <span><?= __('loyalty_history') ?></span>
|
|
</a>
|
|
<?php endif; ?>
|
|
<a href="index.php?page=register_sessions" class="nav-link <?= $page === 'register_sessions' ? 'active' : '' ?>">
|
|
<i class="fas fa-clock-rotate-left"></i> <span><?= __('register_sessions') ?></span>
|
|
</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Configurations Section -->
|
|
<?php if (can('settings_view')): ?>
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['payment_methods', 'settings', 'devices']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#config-collapse">
|
|
<span><i class="fas fa-sliders group-icon"></i><span><?= __('settings') ?></span></span>
|
|
<i class="fas fa-chevron-down chevron"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['payment_methods', 'settings', 'devices']) ? 'show' : '' ?>" id="config-collapse">
|
|
<a href="index.php?page=payment_methods" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'payment_methods' ? 'active' : '' ?>">
|
|
<i class="fas fa-credit-card"></i> <span><?= __('payment_methods') ?></span>
|
|
</a>
|
|
<a href="index.php?page=devices" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'devices' ? 'active' : '' ?>">
|
|
<i class="fas fa-id-card"></i> <span><?= __('devices') ?></span>
|
|
</a>
|
|
<a href="index.php?page=settings" class="nav-link <?= isset($_GET['page']) && $_GET['page'] === 'settings' ? 'active' : '' ?>">
|
|
<i class="fas fa-gear"></i> <span><?= __('settings') ?></span>
|
|
</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Administrations Section -->
|
|
<?php if (can('users_view')): ?>
|
|
<div class="nav-section-title px-4 mt-3 mb-1 text-uppercase text-muted <?= !in_array($page, ['role_groups', 'users', 'cash_registers', 'scale_devices', 'backups', 'customer_display_settings', 'outlets']) ? 'collapsed' : '' ?>" data-bs-toggle="collapse" data-bs-target="#admin-collapse">
|
|
<span><i class="fas fa-user-gear group-icon"></i><span><?= __('admin') ?></span></span>
|
|
<i class="fas fa-chevron-down chevron"></i>
|
|
</div>
|
|
<div class="collapse <?= in_array($page, ['role_groups', 'users', 'cash_registers', 'scale_devices', 'backups', 'customer_display_settings', 'outlets']) ? 'show' : '' ?>" id="admin-collapse">
|
|
<a href="index.php?page=role_groups" class="nav-link <?= $page === 'role_groups' ? 'active' : '' ?>">
|
|
<i class="fas fa-user-shield"></i> <span><?= __('role_groups') ?></span>
|
|
</a>
|
|
<a href="index.php?page=users" class="nav-link <?= $page === 'users' ? 'active' : '' ?>">
|
|
<i class="fas fa-users-gear"></i> <span><?= __('users') ?></span>
|
|
</a>
|
|
<a href="index.php?page=cash_registers" class="nav-link <?= $page === 'cash_registers' ? 'active' : '' ?>">
|
|
<i class="fas fa-cash-register"></i> <span><?= __('cash_registers') ?></span>
|
|
</a>
|
|
<a href="index.php?page=scale_devices" class="nav-link <?= $page === 'scale_devices' ? 'active' : '' ?>">
|
|
<i class="fas fa-microchip"></i> <span><?= __('scale_devices') ?></span>
|
|
</a>
|
|
<a href="index.php?page=customer_display_settings" class="nav-link <?= $page === 'customer_display_settings' ? 'active' : '' ?>">
|
|
<i class="fas fa-desktop"></i> <span><?= __('customer_display') ?></span>
|
|
</a>
|
|
<a href="index.php?page=backups" class="nav-link <?= $page === 'backups' ? 'active' : '' ?>">
|
|
<i class="fas fa-database"></i> <span><?= __('backups') ?></span>
|
|
</a>
|
|
<a href="index.php?page=outlets" class="nav-link <?= $page === 'outlets' ? 'active' : '' ?>">
|
|
<i class="fas fa-shop"></i> <span>Manage Outlets</span>
|
|
</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Version & Logs -->
|
|
<div class="mt-5 px-4 pb-4 border-top pt-4 sidebar-footer">
|
|
<div class="text-muted small fw-bold mb-1">Accounting System</div>
|
|
<div class="d-flex align-items-center justify-content-between text-muted small mb-2">
|
|
<span><i class="bi bi-info-circle me-1"></i> v1.2.5</span>
|
|
|
|
</div>
|
|
<a href="?page=logs" class="text-decoration-none text-muted small d-block">
|
|
<i class="bi bi-journal-text me-1"></i> <span>System Logs</span>
|
|
</a>
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="main-content">
|
|
<header class="topbar">
|
|
<!-- DEBUG: page=<?= $page ?> can_access=<?= $can_access ? 'yes' : 'no' ?> is_activated=<?= $is_activated ? 'yes' : 'no' ?> trial_days=<?= $trial_days ?> -->
|
|
|
|
<div class="d-flex align-items-center">
|
|
<button id="sidebarToggle" class="btn btn-link text-dark p-0 me-3 d-xl-none">
|
|
<i class="bi bi-list fs-3"></i>
|
|
</button>
|
|
<h4 class="m-0"><?= __($page) ?></h4>
|
|
</div>
|
|
<div class="d-flex align-items-center">
|
|
<?php
|
|
$purchaseAlerts = getPurchaseAlerts();
|
|
if (!empty($purchaseAlerts)):
|
|
?>
|
|
<div class="dropdown me-3">
|
|
<button class="btn btn-outline-danger btn-sm position-relative" type="button" data-bs-toggle="dropdown">
|
|
<i class="fas fa-bell"></i>
|
|
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
|
|
<?= count($purchaseAlerts) ?>
|
|
</span>
|
|
</button>
|
|
<div class="dropdown-menu dropdown-menu-end shadow border-0 p-0" style="width: 300px;">
|
|
<div class="p-3 border-bottom bg-light">
|
|
<h6 class="m-0 fw-bold"><?= $lang === 'ar' ? 'تنبيهات المدفوعات' : 'Payment Alerts' ?></h6>
|
|
</div>
|
|
<div class="list-group list-group-flush" style="max-height: 300px; overflow-y: auto;">
|
|
<?php foreach ($purchaseAlerts as $alert):
|
|
$isOverdue = strtotime($alert['due_date']) < time();
|
|
?>
|
|
<a href="index.php?page=purchases&search=<?= $alert['id'] ?>" class="list-group-item list-group-item-action p-3">
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<span class="badge <?= $isOverdue ? 'bg-danger' : 'bg-warning text-dark' ?>">
|
|
<?= $isOverdue ? ($lang === 'ar' ? 'متأخر' : 'Overdue') : ($lang === 'ar' ? 'مستحق قريباً' : 'Due Soon') ?>
|
|
</span>
|
|
<small class="text-muted"><?= htmlspecialchars($alert['due_date']) ?></small>
|
|
</div>
|
|
<div class="fw-bold small"><?= htmlspecialchars($alert['supplier_name']) ?></div>
|
|
<div class="text-primary small">OMR <?= number_format($alert['total_with_vat'], 3) ?></div>
|
|
</a>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="dropdown me-3">
|
|
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-palette"></i> <span><?= $lang === 'ar' ? 'المظهر' : 'Theme' ?></span>
|
|
</button>
|
|
<ul class="dropdown-menu shadow-sm border-0">
|
|
<li><a class="dropdown-item d-flex align-items-center theme-select" href="#" data-theme="default"><span class="rounded-circle me-2" style="width:12px; height:12px; background:#0f172a;"></span> <?= $lang === 'ar' ? 'الافتراضي' : 'Default' ?></a></li>
|
|
<li><a class="dropdown-item d-flex align-items-center theme-select" href="#" data-theme="dark"><span class="rounded-circle me-2" style="width:12px; height:12px; background:#1e293b;"></span> <?= $lang === 'ar' ? 'ليلي' : 'Midnight' ?></a></li>
|
|
<li><a class="dropdown-item d-flex align-items-center theme-select" href="#" data-theme="ocean"><span class="rounded-circle me-2" style="width:12px; height:12px; background:#083344;"></span> <?= $lang === 'ar' ? 'محيط' : 'Ocean' ?></a></li>
|
|
<li><a class="dropdown-item d-flex align-items-center theme-select" href="#" data-theme="forest"><span class="rounded-circle me-2" style="width:12px; height:12px; background:#064e3b;"></span> <?= $lang === 'ar' ? 'غابة' : 'Forest' ?></a></li>
|
|
<li><a class="dropdown-item d-flex align-items-center theme-select" href="#" data-theme="sunset"><span class="rounded-circle me-2" style="width:12px; height:12px; background:#451a03;"></span> <?= $lang === 'ar' ? 'غروب' : 'Sunset' ?></a></li>
|
|
<li><a class="dropdown-item d-flex align-items-center theme-select" href="#" data-theme="nord"><span class="rounded-circle me-2" style="width:12px; height:12px; background:#88c0d0;"></span> <?= $lang === 'ar' ? 'نورد' : 'Nord' ?></a></li>
|
|
<li><a class="dropdown-item d-flex align-items-center theme-select" href="#" data-theme="dracula"><span class="rounded-circle me-2" style="width:12px; height:12px; background:#bd93f9;"></span> <?= $lang === 'ar' ? 'دراكولا' : 'Dracula' ?></a></li>
|
|
<li><a class="dropdown-item d-flex align-items-center theme-select" href="#" data-theme="citrus"><span class="rounded-circle me-2" style="width:12px; height:12px; background:#84cc16;"></span> <?= $lang === 'ar' ? 'حمضيات' : 'Citrus' ?></a></li>
|
|
</ul>
|
|
</div>
|
|
<a href="?lang=<?= $lang === 'ar' ? 'en' : 'ar' ?>" class="btn btn-outline-secondary btn-sm me-3">
|
|
<i class="bi bi-translate"></i> <span><?= $lang === 'ar' ? 'English' : 'العربية' ?></span>
|
|
</a>
|
|
<div class="dropdown d-flex align-items-center">
|
|
<!-- Outlet Switcher -->
|
|
<?php
|
|
$user_outlets_list = $_SESSION['user_outlets'] ?? [1];
|
|
$is_admin = ($_SESSION['user_role_name'] ?? '') === 'Administrator';
|
|
|
|
if (count($user_outlets_list) > 1 || $is_admin):
|
|
$current_oid = current_outlet_id();
|
|
$current_oname = current_outlet_name();
|
|
?>
|
|
<div class="dropdown d-inline-block me-3">
|
|
<button class="btn btn-outline-secondary dropdown-toggle btn-sm" type="button" data-bs-toggle="dropdown">
|
|
<i class="fas fa-store me-1"></i> <?= htmlspecialchars($current_oname) ?>
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li><a class="dropdown-item <?= $current_oid == -1 ? 'active' : '' ?>" href="index.php?action=switch_outlet&id=-1"><?= __('All Outlets') ?: 'All Outlets' ?></a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<?php
|
|
$availOutlets = $user_outlets_list;
|
|
if ($is_admin) {
|
|
$availOutlets = db()->query("SELECT id FROM outlets WHERE status='active'")->fetchAll(PDO::FETCH_COLUMN);
|
|
}
|
|
foreach($availOutlets as $oid):
|
|
$oname = db()->query("SELECT name FROM outlets WHERE id=$oid")->fetchColumn();
|
|
?>
|
|
<li>
|
|
<a class="dropdown-item <?= $oid == $current_oid ? 'active' : '' ?>" href="index.php?action=switch_outlet&id=<?= $oid ?>">
|
|
<?= htmlspecialchars($oname) ?>
|
|
</a>
|
|
</li>
|
|
<?php endforeach; ?>
|
|
</ul>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="me-3 d-none d-md-block text-end">
|
|
<div class="fw-bold small">
|
|
<a href="index.php?page=my_profile" class="text-dark text-decoration-none">
|
|
<?= htmlspecialchars((string)($_SESSION['username'] ?? 'User')) ?>
|
|
</a>
|
|
</div>
|
|
<div class="text-muted" style="font-size: 0.7rem;"><?= htmlspecialchars((string)($_SESSION['user_role_name'] ?? '')) ?></div>
|
|
</div>
|
|
|
|
<a href="index.php?page=my_profile" class="btn btn-light rounded-circle p-0 overflow-hidden shadow-sm d-inline-block position-relative" style="width: 40px; height: 40px;" title="<?= __('edit') ?>">
|
|
<?php if (!empty($_SESSION['profile_pic'])): ?>
|
|
<img src="<?= htmlspecialchars($_SESSION['profile_pic']) ?>?v=<?= time() ?>" alt="Profile" style="width: 100%; height: 100%; object-fit: cover;">
|
|
<?php else: ?>
|
|
<i class="bi bi-person fs-5" style="line-height: 40px;"></i>
|
|
<?php endif; ?>
|
|
<span class="position-absolute bottom-0 end-0 bg-primary rounded-circle text-white d-flex align-items-center justify-content-center" style="width: 15px; height: 15px; font-size: 0.6rem;">
|
|
<i class="bi bi-pencil-fill"></i>
|
|
</span>
|
|
</a>
|
|
<button class="btn btn-link text-dark p-0 ms-1" type="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-chevron-down small"></i>
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end border-0 shadow-sm rounded-3 mt-2">
|
|
<li><a class="dropdown-item py-2" href="index.php?page=my_profile"><i class="bi bi-person-badge me-2"></i> <?= $lang === 'ar' ? 'ملفي الشخصي' : 'My Profile' ?></a></li>
|
|
<li><a class="dropdown-item py-2" href="index.php?page=settings"><i class="bi bi-gear me-2"></i> <?= $lang === 'ar' ? 'إعدادات الشركة' : 'Company Settings' ?></a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item py-2 text-danger" href="index.php?action=logout"><i class="bi bi-box-arrow-right me-2"></i> <?= __('logout') ?></a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<script>
|
|
const companySettings = <?= json_encode($data['settings']) ?>;
|
|
</script>
|
|
|
|
<?php if ($message): ?>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
let msg = <?= json_encode($message) ?>;
|
|
let type = 'success';
|
|
let title = 'Success';
|
|
|
|
if (msg.toLowerCase().includes('error') || msg.toLowerCase().includes('failed')) {
|
|
type = 'error';
|
|
title = 'Error';
|
|
}
|
|
|
|
Swal.fire({
|
|
icon: type,
|
|
title: title,
|
|
text: msg,
|
|
timer: 3000,
|
|
timerProgressBar: true,
|
|
showConfirmButton: false
|
|
});
|
|
});
|
|
</script>
|
|
<?php endif; ?>
|
|
<?php if ($page === 'dashboard'): ?>
|
|
|
|
<?php if (can('dashboard_view')): ?>
|
|
<?php
|
|
$purchaseAlertsCount = count(getPurchaseAlerts());
|
|
if ($data['stats']['expired_items'] > 0 || $data['stats']['near_expiry_items'] > 0 || $data['stats']['low_stock_items_count'] > 0 || $purchaseAlertsCount > 0): ?>
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="alert alert-warning border-0 shadow-sm d-flex align-items-center mb-0">
|
|
<i class="bi bi-exclamation-triangle-fill h4 mb-0 me-3 text-warning"></i>
|
|
<div class="flex-grow-1">
|
|
<span data-en="Administrative Alerts:" data-ar="تنبيهات إدارية:"><strong>Administrative Alerts:</strong></span>
|
|
<?php if ($data['stats']['expired_items'] > 0): ?>
|
|
<span class="ms-2" data-en="<?= $data['stats']['expired_items'] ?> items have expired." data-ar="هنالك <?= $data['stats']['expired_items'] ?> صنف منتهي الصلاحية."><?= $data['stats']['expired_items'] ?> items have expired.</span>
|
|
<?php endif; ?>
|
|
<?php if ($data['stats']['near_expiry_items'] > 0): ?>
|
|
<span class="ms-2" data-en="<?= $data['stats']['near_expiry_items'] ?> items are expiring soon." data-ar="هنالك <?= $data['stats']['near_expiry_items'] ?> صنف ستنتهي صلاحيتها قريباً."><?= $data['stats']['near_expiry_items'] ?> items are expiring soon.</span>
|
|
<?php endif; ?>
|
|
<?php if ($data['stats']['low_stock_items_count'] > 0): ?>
|
|
<span class="ms-2" data-en="<?= $data['stats']['low_stock_items_count'] ?> items are below minimum level." data-ar="هنالك <?= $data['stats']['low_stock_items_count'] ?> صنف تحت الحد الأدنى للمخزون."><?= $data['stats']['low_stock_items_count'] ?> items are below minimum level.</span>
|
|
<?php endif; ?>
|
|
<?php if ($purchaseAlertsCount > 0): ?>
|
|
<span class="ms-2 text-danger fw-bold" data-en="<?= $purchaseAlertsCount ?> purchase invoices are due or overdue." data-ar="هنالك <?= $purchaseAlertsCount ?> فاتورة مشتريات مستحقة أو متأخرة."><?= $purchaseAlertsCount ?> purchase invoices are due or overdue.</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<?php if ($purchaseAlertsCount > 0): ?>
|
|
<a href="index.php?page=purchases" class="btn btn-primary btn-sm" data-en="View Purchases" data-ar="عرض المشتريات">View Purchases</a>
|
|
<?php endif; ?>
|
|
<a href="index.php?page=expiry_report" class="btn btn-warning btn-sm" data-en="Expiry Report" data-ar="تقرير الانتهاء">Expiry Report</a>
|
|
<a href="index.php?page=low_stock_report" class="btn btn-danger btn-sm" data-en="Low Stock Report" data-ar="تقرير النواقص">Low Stock Report</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-primary border-4 h-100">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="text-muted small"><?= __('total_sales') ?></div>
|
|
<div class="h4 m-0">OMR <?= number_format((float)($data['stats']['total_sales'] ?? 0), 3) ?></div>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="bi bi-cart h2 text-primary op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-warning border-4 h-100">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="text-muted small"><?= __('total_received') ?></div>
|
|
<div class="h4 m-0">OMR <?= number_format((float)($data['stats']['total_received'] ?? 0), 3) ?></div>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="bi bi-cash-stack h2 text-warning op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-danger border-4 h-100">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="text-muted small"><?= __('customer_due') ?></div>
|
|
<div class="h4 m-0">OMR <?= number_format((float)($data['stats']['total_receivable'] ?? 0), 3) ?></div>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="bi bi-person-exclamation h2 text-danger op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-dark border-4 h-100 bg-light">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="text-muted small"><?= __('total_purchases') ?></div>
|
|
<div class="h4 m-0">OMR <?= number_format((float)($data['stats']['total_purchases'] ?? 0), 3) ?></div>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="bi bi-bag h2 text-dark op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-info border-4 h-100 bg-light">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="text-muted small"><?= __('total_paid') ?></div>
|
|
<div class="h4 m-0">OMR <?= number_format((float)($data['stats']['total_paid'] ?? 0), 3) ?></div>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="bi bi-wallet2 h2 text-info op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-secondary border-4 h-100 bg-light">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="text-muted small"><?= __('supplier_due') ?></div>
|
|
<div class="h4 m-0">OMR <?= number_format((float)($data['stats']['total_payable'] ?? 0), 3) ?></div>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="bi bi-truck h2 text-secondary op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-success border-4 h-100">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="text-muted small"><?= __('total_customers') ?></div>
|
|
<div class="h4 m-0"><?= (int)($data['stats']['total_customers'] ?? 0) ?></div>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="bi bi-people h2 text-success op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card p-3 border-start border-info border-4 h-100">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-grow-1">
|
|
<div class="text-muted small" data-en="Total Items" data-ar="إجمالي الأصناف">Total Items</div>
|
|
<div class="h4 m-0"><?= (int)($data['stats']['total_items'] ?? 0) ?></div>
|
|
</div>
|
|
<div class="ms-3">
|
|
<i class="bi bi-box-seam h2 text-info op-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-12">
|
|
<div class="card p-4">
|
|
<?php $dashboardScopeLabel = (string)($data['dashboard_scope_label'] ?? current_outlet_name()); ?>
|
|
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
|
|
<div>
|
|
<h5 class="m-0" data-en="Sales Performance" data-ar="أداء المبيعات">Sales Performance</h5>
|
|
<div class="small text-muted mt-1">
|
|
<i class="fas fa-store me-1"></i>
|
|
<span data-en="<?= htmlspecialchars('Scope: ' . $dashboardScopeLabel, ENT_QUOTES) ?>" data-ar="<?= htmlspecialchars('النطاق: ' . $dashboardScopeLabel, ENT_QUOTES) ?>"><?= $lang === 'ar' ? 'النطاق: ' : 'Scope: ' ?><?= htmlspecialchars($dashboardScopeLabel) ?></span>
|
|
</div>
|
|
</div>
|
|
<div class="btn-group btn-group-sm">
|
|
<button type="button" class="btn btn-outline-primary active" id="btnMonthly" data-en="Monthly" data-ar="شهري">Monthly</button>
|
|
<button type="button" class="btn btn-outline-primary" id="btnYearly" data-en="Yearly" data-ar="سنوي">Yearly</button>
|
|
</div>
|
|
</div>
|
|
<div style="height: 300px;">
|
|
<canvas id="salesChart"></canvas>
|
|
</div>
|
|
<div id="salesChartEmptyState" class="small text-muted mt-3 d-none" data-en="No completed sales recorded for this outlet yet." data-ar="لا توجد مبيعات مكتملة مسجلة لهذا الفرع حتى الآن.">No completed sales recorded for this outlet yet.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Recent Customers" data-ar="العملاء الحاليين">Recent Customers</h5>
|
|
<a href="index.php?page=customers" class="btn btn-outline-primary btn-sm">
|
|
<span data-en="View All" data-ar="عرض الكل">View All</span>
|
|
</a>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover table-bordered align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Name" data-ar="الاسم">Name</th>
|
|
<th data-en="Phone" data-ar="الهاتف">Phone</th>
|
|
<th data-en="Balance" data-ar="الرصيد" class="text-end">Balance</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['customers'] as $c): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars((string)($c['name'] ?? '')) ?></td>
|
|
<td><?= htmlspecialchars((string)($c['phone'] ?? '')) ?></td>
|
|
<td class="text-end" data-en="OMR <?= number_format((float)$c['balance'], 3) ?>" data-ar="<?= number_format((float)$c['balance'], 3) ?> ر.ع."><?= $lang === 'ar' ? number_format((float)$c['balance'], 3) . ' ر.ع.' : 'OMR ' . number_format((float)$c['balance'], 3) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card p-4">
|
|
<h5 class="mb-4" data-en="Quick Links" data-ar="روابط سريعة">Quick Links</h5>
|
|
<div class="list-group list-group-flush">
|
|
<button class="list-group-item list-group-item-action border-0 px-0" data-bs-toggle="modal" data-bs-target="#addCustomerModal">
|
|
<i class="bi bi-person-plus text-primary"></i> <span data-en="Add Customer" data-ar="إضافة عميل">Add Customer</span>
|
|
</button>
|
|
<button class="list-group-item list-group-item-action border-0 px-0" data-bs-toggle="modal" data-bs-target="#addItemModal">
|
|
<i class="bi bi-box-seam text-success"></i> <span data-en="Add Item" data-ar="إضافة صنف">Add Item</span>
|
|
</button>
|
|
<button class="list-group-item list-group-item-action border-0 px-0" data-bs-toggle="modal" data-bs-target="#importItemsModal">
|
|
<i class="bi bi-file-earmark-excel text-success"></i> <span data-en="Import Items" data-ar="استيراد أصناف">Import Items</span>
|
|
</button>
|
|
<a href="index.php?page=sales" class="list-group-item list-group-item-action border-0 px-0">
|
|
<i class="bi bi-cart text-primary"></i> <span data-en="Sales Tax Invoices" data-ar="فواتير المبيعات الضريبية">Sales Tax Invoices</span>
|
|
</a>
|
|
<a href="index.php?page=purchases" class="list-group-item list-group-item-action border-0 px-0">
|
|
<i class="bi bi-bag text-warning"></i> <span data-en="Purchase Tax Invoices" data-ar="فواتير المشتريات الضريبية">Purchase Tax Invoices</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="d-flex flex-column justify-content-center align-items-center h-100" style="min-height: 70vh;">
|
|
<?php if (!empty($data['settings']['company_logo'])): ?>
|
|
<img src="<?= htmlspecialchars($data['settings']['company_logo']) ?>" alt="Company Logo" class="mb-4" style="max-height: 200px; max-width: 350px; object-fit: contain;">
|
|
<?php endif; ?>
|
|
<h1 class="display-4 fw-bold text-muted text-center mt-3"><?= htmlspecialchars($data['settings']['company_name'] ?? 'Company Name') ?></h1>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php elseif ($page === 'customers' || $page === 'suppliers'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="<?= $currTitle['en'] ?> Management" data-ar="إدارة <?= $currTitle['ar'] ?>"><?= $currTitle['en'] ?> Management</h5>
|
|
<div>
|
|
<?php if (can('customers_add') || can('suppliers_add')): ?>
|
|
<a href="index.php?<?= http_build_query(array_merge($_GET, ['page' => 'export', 'type' => $page, 'format' => 'excel'])) ?>" class="btn btn-outline-success me-2">
|
|
<i class="bi bi-file-earmark-excel"></i> <span data-en="Export to Excel" data-ar="تصدير إلى اكسل">Export to Excel</span>
|
|
</a>
|
|
<button class="btn btn-outline-success me-2" data-bs-toggle="modal" data-bs-target="<?= $page === 'suppliers' ? '#importSuppliersModal' : '#importCustomersModal' ?>">
|
|
<i class="bi bi-file-earmark-excel"></i> <span data-en="Import Excel" data-ar="استيراد من اكسل">Import Excel</span>
|
|
</button>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addCustomerModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add <?= $currTitle['en'] ?>" data-ar="إضافة <?= $currTitle['ar'] ?>">Add <?= $currTitle['en'] ?></span>
|
|
</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search & Filter Bar -->
|
|
<div class="bg-light p-3 rounded mb-4">
|
|
<form method="GET" class="row g-2 align-items-end">
|
|
<input type="hidden" name="page" value="<?= $page ?>">
|
|
<div class="col-md-4">
|
|
<label class="form-label small" data-en="Search" data-ar="بحث">Search</label>
|
|
<input type="text" name="search" class="form-control" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="Name, email, or phone..." data-en="Name, email, or phone..." data-ar="الاسم، البريد، أو الهاتف...">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small" data-en="From Date" data-ar="من تاريخ">From Date</label>
|
|
<input type="date" name="start_date" class="form-control" value="<?= htmlspecialchars($_GET['start_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small" data-en="To Date" data-ar="إلى تاريخ">To Date</label>
|
|
<input type="date" name="end_date" class="form-control" value="<?= htmlspecialchars($_GET['end_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-4 d-flex gap-1">
|
|
<button type="submit" class="btn btn-primary flex-grow-1">
|
|
<i class="bi bi-filter"></i> <span data-en="Filter" data-ar="تصفية">Filter</span>
|
|
</button>
|
|
<div class="dropdown d-inline-block">
|
|
<button class="btn btn-outline-success dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-download"></i> <span data-en="Export" data-ar="تصدير">Export</span>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="index.php?<?= http_build_query(array_merge($_GET, ['page' => 'export', 'type' => $page, 'format' => 'csv'])) ?>"><i class="bi bi-filetype-csv me-2"></i> CSV</a></li>
|
|
<li><a class="dropdown-item" href="index.php?<?= http_build_query(array_merge($_GET, ['page' => 'export', 'type' => $page, 'format' => 'excel'])) ?>"><i class="bi bi-file-earmark-excel me-2"></i> Excel</a></li>
|
|
</ul>
|
|
</div>
|
|
<?php if (!empty($_GET['search']) || !empty($_GET['start_date']) || !empty($_GET['end_date'])): ?>
|
|
<a href="index.php?page=<?= $page ?>" class="btn btn-outline-secondary">
|
|
<i class="bi bi-x-lg"></i>
|
|
</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="col-md-auto ms-auto d-flex align-items-end mt-2 mt-md-0">
|
|
<div class="input-group input-group-sm w-auto">
|
|
<span class="input-group-text" data-en="Limit" data-ar="الحد">Limit</span>
|
|
<select name="limit" class="form-select" onchange="this.form.submit()">
|
|
<option value="20" <?= (($_GET['limit'] ?? 20) == 20) ? 'selected' : '' ?>>20</option>
|
|
<option value="40" <?= (($_GET['limit'] ?? 20) == 40) ? 'selected' : '' ?>>40</option>
|
|
<option value="60" <?= (($_GET['limit'] ?? 20) == 60) ? 'selected' : '' ?>>60</option>
|
|
<option value="100" <?= (($_GET['limit'] ?? 20) == 100) ? 'selected' : '' ?>>100</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Name" data-ar="الاسم">Name</th>
|
|
<th data-en="Tax ID" data-ar="الرقم الضريبي">Tax ID</th>
|
|
<th data-en="Email" data-ar="البريد">Email</th>
|
|
<th data-en="Phone" data-ar="الهاتف">Phone</th>
|
|
<th data-en="Balance" data-ar="الرصيد" class="text-end">Balance</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['customers'] as $c): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars((string)($c['name'] ?? '')) ?></td>
|
|
<td><?= htmlspecialchars((string)($c['tax_id'] ?? '---')) ?></td>
|
|
<td><?= htmlspecialchars((string)($c['email'] ?? '')) ?></td>
|
|
<td><?= htmlspecialchars((string)($c['phone'] ?? '')) ?></td>
|
|
<td class="text-end" data-en="OMR <?= number_format((float)$c['balance'], 3) ?>" data-ar="<?= number_format((float)$c['balance'], 3) ?> ر.ع."><?= $lang === 'ar' ? number_format((float)$c['balance'], 3) . ' ر.ع.' : 'OMR ' . number_format((float)$c['balance'], 3) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<?php if (can('customers_edit') || can('suppliers_edit')): ?>
|
|
<button class="btn btn-outline-primary" title="Edit" data-bs-toggle="modal" data-bs-target="#editCustomerModal<?= $c['id'] ?>"><i class="bi bi-pencil"></i></button>
|
|
<?php endif; ?>
|
|
<?php if (can('customers_delete') || can('suppliers_delete')): ?>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $c['id'] ?>">
|
|
<button type="submit" name="delete_customer" class="btn btn-outline-danger" title="Delete"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Edit Customer Modal -->
|
|
<div class="modal fade" id="editCustomerModal<?= $c['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<span data-en="Edit <?= $currTitle['en'] ?>" data-ar="تعديل <?= $currTitle['ar'] ?>">Edit <?= $currTitle['en'] ?></span>
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $c['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Name" data-ar="الاسم">Name</label>
|
|
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($c['name']) ?>" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Email" data-ar="البريد الإلكتروني">Email</label>
|
|
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($c['email'] ?? '') ?>">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Phone" data-ar="الهاتف">Phone</label>
|
|
<input type="text" name="phone" class="form-control" value="<?= htmlspecialchars($c['phone'] ?? '') ?>">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Tax ID / VAT No" data-ar="الرقم الضريبي">Tax ID / VAT No</label>
|
|
<input type="text" name="tax_id" class="form-control" value="<?= htmlspecialchars($c['tax_id'] ?? '') ?>">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Balance" data-ar="الرصيد">Balance</label>
|
|
<input type="number" step="0.001" name="balance" class="form-control" value="<?= (float)$c['balance'] ?>">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_customer" class="btn btn-primary" data-en="Update" data-ar="تحديث">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
<?php elseif ($page === 'categories'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Stock Categories Management" data-ar="إدارة فئات المخزون">Stock Categories Management</h5>
|
|
<div>
|
|
<?php if (can('categories_add')): ?>
|
|
<a href="index.php?page=export&type=categories&format=excel" class="btn btn-outline-success me-2">
|
|
<i class="bi bi-download"></i> <span data-en="Export" data-ar="تصدير">Export</span>
|
|
</a>
|
|
<button class="btn btn-outline-success me-2" data-bs-toggle="modal" data-bs-target="#importCategoriesModal">
|
|
<i class="bi bi-file-earmark-excel"></i> <span data-en="Import Excel" data-ar="استيراد من اكسل">Import Excel</span>
|
|
</button>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addCategoryModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Category" data-ar="إضافة فئة">Add Category</span>
|
|
</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="ID" data-ar="المعرف">ID</th>
|
|
<th data-en="Name (EN)" data-ar="الاسم (إنجليزي)">Name (EN)</th>
|
|
<th data-en="Name (AR)" data-ar="الاسم (عربي)">Name (AR)</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['categories'] as $cat): ?>
|
|
<tr>
|
|
<td><?= $cat['id'] ?></td>
|
|
<td><?= htmlspecialchars($cat['name_en']) ?></td>
|
|
<td><?= htmlspecialchars($cat['name_ar']) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<?php if (can('categories_edit')): ?>
|
|
<button class="btn btn-outline-primary" title="Edit" data-bs-toggle="modal" data-bs-target="#editCatModal<?= $cat['id'] ?>"><i class="bi bi-pencil"></i></button>
|
|
<?php endif; ?>
|
|
<?php if (can('categories_delete')): ?>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
|
<button type="submit" name="delete_category" class="btn btn-outline-danger" title="Delete"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Edit Modal -->
|
|
<div class="modal fade" id="editCatModal<?= $cat['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Edit Category</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Name (EN)</label>
|
|
<div class="input-group">
|
|
<input type="text" name="name_en" id="edit_cat_name_en_<?= $cat['id'] ?>" class="form-control" value="<?= htmlspecialchars($cat['name_en']) ?>" required>
|
|
<button class="btn btn-outline-secondary btn-translate" type="button" data-source="edit_cat_name_ar_<?= $cat['id'] ?>" data-target="edit_cat_name_en_<?= $cat['id'] ?>" data-to="en">
|
|
<i class="bi bi-translate"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Name (AR)</label>
|
|
<div class="input-group">
|
|
<input type="text" name="name_ar" id="edit_cat_name_ar_<?= $cat['id'] ?>" class="form-control" value="<?= htmlspecialchars($cat['name_ar']) ?>" required>
|
|
<button class="btn btn-outline-secondary btn-translate" type="button" data-source="edit_cat_name_en_<?= $cat['id'] ?>" data-target="edit_cat_name_ar_<?= $cat['id'] ?>" data-to="ar">
|
|
<i class="bi bi-translate"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_category" class="btn btn-primary" data-en="Update" data-ar="تحديث">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<?php elseif ($page === 'units'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-4">
|
|
<div>
|
|
<h5 class="m-0"><span data-en="Units" data-ar="الوحدات">Units</span> (<?= count($data['units'] ?? []) ?>)</h5>
|
|
<p class="text-muted small mb-0" data-en="Manage the English and Arabic unit names used across items, invoices, and reports." data-ar="قم بإدارة أسماء الوحدات بالإنجليزية والعربية المستخدمة في الأصناف والفواتير والتقارير.">Manage the English and Arabic unit names used across items, invoices, and reports.</p>
|
|
</div>
|
|
<?php if (can('units_add')): ?>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<a href="index.php?page=export&type=units&format=excel" class="btn btn-outline-success">
|
|
<i class="bi bi-download"></i> <span data-en="Export" data-ar="تصدير">Export</span>
|
|
</a>
|
|
<button class="btn btn-outline-success" data-bs-toggle="modal" data-bs-target="#importUnitsModal">
|
|
<i class="bi bi-file-earmark-arrow-up"></i> <span data-en="Import" data-ar="استيراد">Import</span>
|
|
</button>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addUnitModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Unit" data-ar="إضافة وحدة">Add Unit</span>
|
|
</button>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="table-responsive border rounded-3 bg-white">
|
|
<table class="table align-middle units-table mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Name (EN)" data-ar="الاسم (إنجليزي)">Name (EN)</th>
|
|
<th data-en="Name (AR)" data-ar="الاسم (عربي)">Name (AR)</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($data['units'])): ?>
|
|
<tr>
|
|
<td colspan="3" class="text-center py-5">
|
|
<div class="units-empty-state">
|
|
<div class="units-empty-state__icon"><i class="bi bi-rulers"></i></div>
|
|
<h6 class="mb-0" data-en="No units yet" data-ar="لا توجد وحدات بعد">No units yet</h6>
|
|
<p class="text-muted mb-0" data-en="Add your first unit so items can reuse the same labels in invoices and reports." data-ar="أضف أول وحدة حتى تستخدم الأصناف نفس التسميات في الفواتير والتقارير.">Add your first unit so items can reuse the same labels in invoices and reports.</p>
|
|
<?php if (can('units_add')): ?>
|
|
<button class="btn btn-primary mt-2" data-bs-toggle="modal" data-bs-target="#addUnitModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Create first unit" data-ar="إنشاء أول وحدة">Create first unit</span>
|
|
</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php else: ?>
|
|
<?php foreach ($data['units'] as $u): ?>
|
|
<?php
|
|
$unitNameEn = trim((string)($u['name_en'] ?? ''));
|
|
$unitNameAr = trim((string)($u['name_ar'] ?? ''));
|
|
?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($unitNameEn !== '' ? $unitNameEn : '---') ?></td>
|
|
<td><span lang="ar" dir="rtl" class="d-inline-block"><?= htmlspecialchars($unitNameAr !== '' ? $unitNameAr : '---') ?></span></td>
|
|
<td class="text-end">
|
|
<div class="units-actions justify-content-end">
|
|
<?php if (can('units_edit')): ?>
|
|
<button class="btn btn-outline-primary btn-sm" title="Edit" data-bs-toggle="modal" data-bs-target="#editUnitModal<?= $u['id'] ?>"><i class="bi bi-pencil"></i></button>
|
|
<?php endif; ?>
|
|
<?php if (can('units_delete')): ?>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $u['id'] ?>">
|
|
<button type="submit" name="delete_unit" class="btn btn-outline-danger btn-sm" title="Delete"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="modal fade" id="editUnitModal<?= $u['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content border-0 shadow-lg text-start unit-modal">
|
|
<div class="modal-header">
|
|
<div>
|
|
<span class="units-modal-kicker" data-en="Refine labels" data-ar="تحسين التسميات">Refine labels</span>
|
|
<h5 class="modal-title" data-en="Edit Unit" data-ar="تعديل الوحدة">Edit Unit</h5>
|
|
<p class="text-muted small mb-0" data-en="Update the English and Arabic unit names used across stock items and invoices." data-ar="حدّث أسماء الوحدة بالإنجليزية والعربية المستخدمة في الأصناف والفواتير.">Update the English and Arabic unit names used across stock items and invoices.</p>
|
|
</div>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $u['id'] ?>">
|
|
<div class="modal-body pt-4">
|
|
<div class="unit-form-shell">
|
|
<section class="unit-form-section">
|
|
<div class="unit-form-section__header">
|
|
<div>
|
|
<h6 class="mb-1" data-en="Display names" data-ar="الأسماء الكاملة">Display names</h6>
|
|
<p class="text-muted small mb-0" data-en="Shown in item forms, reports, and detailed views." data-ar="تظهر في نماذج الأصناف والتقارير والعروض التفصيلية.">Shown in item forms, reports, and detailed views.</p>
|
|
</div>
|
|
<span class="units-helper-pill" data-en="Bilingual" data-ar="ثنائي اللغة">Bilingual</span>
|
|
</div>
|
|
<div class="unit-form-grid">
|
|
<div class="unit-field">
|
|
<label class="form-label" data-en="Name (EN)" data-ar="الاسم (إنجليزي)">Name (EN)</label>
|
|
<div class="input-group">
|
|
<input type="text" name="name_en" id="edit_unit_name_en_<?= $u['id'] ?>" class="form-control" value="<?= htmlspecialchars($unitNameEn) ?>" required>
|
|
<button class="btn btn-outline-secondary btn-translate" type="button" data-source="edit_unit_name_ar_<?= $u['id'] ?>" data-target="edit_unit_name_en_<?= $u['id'] ?>" data-to="en">
|
|
<i class="bi bi-translate"></i>
|
|
</button>
|
|
</div>
|
|
<div class="form-text" data-en="Example: Kilogram" data-ar="مثال: Kilogram">Example: Kilogram</div>
|
|
</div>
|
|
<div class="unit-field">
|
|
<label class="form-label" data-en="Name (AR)" data-ar="الاسم (عربي)">Name (AR)</label>
|
|
<div class="input-group">
|
|
<input type="text" name="name_ar" id="edit_unit_name_ar_<?= $u['id'] ?>" class="form-control" value="<?= htmlspecialchars($unitNameAr) ?>" required>
|
|
<button class="btn btn-outline-secondary btn-translate" type="button" data-source="edit_unit_name_en_<?= $u['id'] ?>" data-target="edit_unit_name_ar_<?= $u['id'] ?>" data-to="ar">
|
|
<i class="bi bi-translate"></i>
|
|
</button>
|
|
</div>
|
|
<div class="form-text" data-en="Example: كيلوغرام" data-ar="مثال: كيلوغرام">Example: كيلوغرام</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section class="unit-form-section">
|
|
<div class="unit-form-section__header">
|
|
<div>
|
|
<h6 class="mb-1" data-en="Name preview" data-ar="معاينة الأسماء">Name preview</h6>
|
|
<p class="text-muted small mb-0" data-en="This is how the unit name will appear across the app." data-ar="هكذا سيظهر اسم الوحدة في جميع أجزاء التطبيق.">This is how the unit name will appear across the app.</p>
|
|
</div>
|
|
<span class="units-helper-pill" data-en="Live label" data-ar="التسمية النهائية">Live label</span>
|
|
</div>
|
|
<div class="unit-preview-card mt-0">
|
|
<span class="unit-preview-card__label" data-en="Current preview" data-ar="المعاينة الحالية">Current preview</span>
|
|
<div class="unit-preview-row">
|
|
<span class="unit-preview-chip"><?= htmlspecialchars($unitNameEn !== '' ? $unitNameEn : 'Name (EN)') ?></span>
|
|
<span class="unit-preview-chip"><?= htmlspecialchars($unitNameAr !== '' ? $unitNameAr : 'الاسم') ?></span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer pt-0 border-0">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_unit" class="btn btn-primary" data-en="Update" data-ar="تحديث">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
|
|
<?php elseif ($page === 'items'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Stock Items" data-ar="أصناف المخزون">Stock Items (<?= count($data['items'] ?? []) ?>)</h5>
|
|
<div class="d-flex align-items-center">
|
|
<button class="btn btn-dark me-2" id="bulkBarcodeBtn" style="display:none;" onclick="openAveryModal()">
|
|
<i class="bi bi-printer"></i> <span data-en="Avery Labels" data-ar="ملصقات إيفري">Avery Labels</span>
|
|
</button>
|
|
<a href="promo-catalog.php" class="btn btn-outline-danger me-2">
|
|
<i class="bi bi-file-pdf"></i> <span data-en="Promo Catalog" data-ar="كتالوج العروض">Promo Catalog</span>
|
|
</a>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure you want to cancel all active promotions?');">
|
|
<button type="submit" name="cancel_all_promotions" class="btn btn-outline-danger me-2">
|
|
<i class="bi bi-x-circle"></i> <span data-en="Cancel All Promotions" data-ar="إلغاء جميع العروض">Cancel All Promotions</span>
|
|
</button>
|
|
</form>
|
|
<button class="btn btn-outline-success me-2" data-bs-toggle="modal" data-bs-target="#importItemsModal">
|
|
<i class="bi bi-file-earmark-excel"></i> <span data-en="Import Excel" data-ar="استيراد من اكسل">Import Excel</span>
|
|
</button>
|
|
<a href="index.php?<?= http_build_query(array_merge($_GET, ['page' => 'export', 'type' => 'items', 'format' => 'excel'])) ?>" class="btn btn-outline-success me-2">
|
|
<i class="bi bi-file-earmark-excel"></i> <span data-en="Export to Excel" data-ar="تصدير إلى اكسل">Export to Excel</span>
|
|
</a>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addItemModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Item" data-ar="إضافة صنف">Add Item</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Bar -->
|
|
<div class="bg-light p-3 rounded mb-4">
|
|
<form method="GET" class="row g-2 align-items-end">
|
|
<input type="hidden" name="page" value="items">
|
|
<div class="col-md-7">
|
|
<label class="form-label small" data-en="Search" data-ar="بحث">Search</label>
|
|
<input type="text" name="search" class="form-control" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="Name or SKU..." data-en="Name or SKU..." data-ar="الاسم أو الباركود...">
|
|
</div>
|
|
<div class="col-md-5 d-flex gap-1">
|
|
<button type="submit" class="btn btn-primary flex-grow-1">
|
|
<i class="bi bi-search"></i> <span data-en="Search" data-ar="بحث">Search</span>
|
|
</button>
|
|
<div class="dropdown d-inline-block">
|
|
<button class="btn btn-outline-success dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-download"></i> <span data-en="Export" data-ar="تصدير">Export</span>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="index.php?<?= http_build_query(array_merge($_GET, ['page' => 'export', 'type' => 'items', 'format' => 'csv'])) ?>"><i class="bi bi-filetype-csv me-2"></i> CSV</a></li>
|
|
<li><a class="dropdown-item" href="index.php?<?= http_build_query(array_merge($_GET, ['page' => 'export', 'type' => 'items', 'format' => 'excel'])) ?>"><i class="bi bi-file-earmark-excel me-2"></i> Excel</a></li>
|
|
</ul>
|
|
</div>
|
|
<?php if (!empty($_GET['search'])): ?>
|
|
<a href="index.php?page=items" class="btn btn-outline-secondary">
|
|
<i class="bi bi-x-lg"></i>
|
|
</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="col-md-auto ms-auto d-flex align-items-end mt-2 mt-md-0">
|
|
<div class="input-group input-group-sm w-auto">
|
|
<span class="input-group-text" data-en="Limit" data-ar="الحد">Limit</span>
|
|
<select name="limit" class="form-select" onchange="this.form.submit()">
|
|
<option value="20" <?= (($_GET['limit'] ?? 20) == 20) ? 'selected' : '' ?>>20</option>
|
|
<option value="40" <?= (($_GET['limit'] ?? 20) == 40) ? 'selected' : '' ?>>40</option>
|
|
<option value="60" <?= (($_GET['limit'] ?? 20) == 60) ? 'selected' : '' ?>>60</option>
|
|
<option value="100" <?= (($_GET['limit'] ?? 20) == 100) ? 'selected' : '' ?>>100</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 40px;"><input type="checkbox" id="selectAllItems" class="form-check-input"></th>
|
|
<th data-en="Image" data-ar="الصورة">Image</th>
|
|
<th data-en="SKU" data-ar="الباركود">SKU</th>
|
|
<th data-en="Name" data-ar="الاسم">Name</th>
|
|
<th data-en="Category" data-ar="الفئة">Category</th>
|
|
<th data-en="Supplier" data-ar="المورد">Supplier</th>
|
|
<th data-en="Stock Level" data-ar="المخزون">Stock Level</th>
|
|
<th data-en="Expiry" data-ar="تاريخ الانتهاء">Expiry</th>
|
|
<th data-en="VAT" data-ar="الضريبة">VAT</th>
|
|
<th data-en="Actions" data-ar="الإجراءات">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['items'] as $item): ?>
|
|
<tr>
|
|
<td><input type="checkbox" class="form-check-input item-checkbox" data-id="<?= $item['id'] ?>" data-sku="<?= htmlspecialchars($item['sku']) ?>" data-name-ar="<?= htmlspecialchars($item['name_ar']) ?>" data-name-en="<?= htmlspecialchars($item['name_en']) ?>" data-name="<?= htmlspecialchars($item['name_en'] . ' - ' . $item['name_ar']) ?>" data-price="<?= number_format((float)$item['sale_price'] * (1 + (float)($item['vat_rate'] ?? 0) / 100), 3) ?>"></td>
|
|
<td>
|
|
<?php if ($item['image_path']): ?>
|
|
<img src="<?= htmlspecialchars($item['image_path']) ?>" alt="item" style="width: 40px; height: 40px; object-fit: cover;" class="rounded">
|
|
<?php else: ?>
|
|
<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
|
<i class="bi bi-image text-muted"></i>
|
|
</div>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?= htmlspecialchars($item['sku']) ?></td>
|
|
<td>
|
|
<div class="fw-bold">
|
|
<?= htmlspecialchars((string)($item['name_en'] ?? '')) ?>
|
|
<?php if (isset($item['is_promotion']) && $item['is_promotion']): ?>
|
|
<span class="badge bg-success ms-1" style="font-size: 0.65rem;" data-en="Promo" data-ar="عرض">Promo</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="small text-muted"><?= htmlspecialchars((string)($item['name_ar'] ?? '')) ?></div>
|
|
</td>
|
|
<td><span data-en="<?= htmlspecialchars((string)($item['cat_en'] ?? '')) ?>" data-ar="<?= htmlspecialchars((string)($item['cat_ar'] ?? '')) ?>"><?= htmlspecialchars((string)($item['cat_en'] ?? '')) ?></span></td>
|
|
<td><?= htmlspecialchars((string)($item['supplier_name'] ?? '---')) ?></td>
|
|
<td>
|
|
<div class="text-end">
|
|
<strong><?= number_format((float)$item['stock_quantity'], 3) ?></strong>
|
|
<div class="small text-muted">Min: <?= number_format((float)$item['min_stock_level'], 3) ?></div>
|
|
<?php if ($item['stock_quantity'] <= $item['min_stock_level']): ?>
|
|
<span class="badge bg-danger" data-en="Low Stock" data-ar="مخزون منخفض">Low Stock</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</td>
|
|
<td><?= !empty($item['expiry_date']) ? htmlspecialchars((string)$item['expiry_date']) : '---' ?></td>
|
|
<td><?= number_format((float)$item['vat_rate'], 2) ?>%</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-info" title="View" data-bs-toggle="modal" data-bs-target="#viewItemModal<?= $item['id'] ?>"><i class="bi bi-eye"></i></button>
|
|
<button class="btn btn-outline-primary" title="Edit" data-bs-toggle="modal" data-bs-target="#editItemModal<?= $item['id'] ?>"><i class="bi bi-pencil"></i></button>
|
|
<button class="btn btn-outline-dark" title="Barcode" onclick="printItemBarcode('<?= htmlspecialchars($item['sku']) ?>', '<?= htmlspecialchars($item['name_ar']) ?>', '<?= htmlspecialchars($item['name_en']) ?>', '<?= number_format((float)$item['sale_price'] * (1 + (float)($item['vat_rate'] ?? 0) / 100), 3) ?>')"><i class="bi bi-upc"></i></button>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $item['id'] ?>">
|
|
<button type="submit" name="delete_item" class="btn btn-outline-danger" title="Delete"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- View Item Modal -->
|
|
<div class="modal fade" id="viewItemModal<?= $item['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><?= htmlspecialchars((string)($item['name_en'] ?? '')) ?></h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="text-center mb-3">
|
|
<?php if ($item['image_path']): ?>
|
|
<img src="<?= htmlspecialchars((string)$item['image_path']) ?>" class="img-fluid rounded shadow-sm" style="max-height: 200px;">
|
|
<?php else: ?>
|
|
<div class="bg-light rounded d-flex align-items-center justify-content-center mx-auto" style="width: 150px; height: 150px;">
|
|
<i class="bi bi-image text-muted" style="font-size: 3rem;"></i>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<table class="table table-sm">
|
|
<tr><th class="text-muted">SKU</th><td><?= htmlspecialchars((string)($item['sku'] ?? '')) ?></td></tr>
|
|
<tr><th class="text-muted" data-en="Category" data-ar="الفئة">Category</th><td><?= htmlspecialchars($item['cat_en'] ?? '---') ?></td></tr>
|
|
<tr><th class="text-muted" data-en="Supplier" data-ar="المورد">Supplier</th><td><?= htmlspecialchars($item['supplier_name'] ?? '---') ?></td></tr>
|
|
<tr><th class="text-muted">Sale Price</th><td>OMR <?= number_format((float)$item['sale_price'], 3) ?></td></tr>
|
|
<tr><th class="text-muted">Stock Level</th><td><?= number_format((float)$item['stock_quantity'], 3) ?></td></tr>
|
|
<tr><th class="text-muted">VAT Rate</th><td><?= number_format((float)$item['vat_rate'], 2) ?>%</td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
|
|
<div class="modal-footer">
|
|
<button class="btn btn-outline-dark" onclick="printItemBarcode('<?= htmlspecialchars($item['sku']) ?>', '<?= htmlspecialchars($item['name_ar']) ?>', '<?= htmlspecialchars($item['name_en']) ?>', '<?= number_format((float)$item['sale_price'] * (1 + (float)($item['vat_rate'] ?? 0) / 100), 3) ?>')"><i class="bi bi-printer"></i> Print Barcode</button>
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Close" data-ar="إغلاق">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Item Modal -->
|
|
<div class="modal fade" id="editItemModal<?= $item['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Edit Item" data-ar="تعديل صنف">Edit Item</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<input type="hidden" name="id" value="<?= $item['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="form-grid-3">
|
|
<div class="col-md-4"><label class="form-label" data-en="Name (EN)" data-ar="الاسم (إنجليزي)">Name (EN)</label><div class="input-group"><input type="text" name="name_en" id="editItemNameEn<?= $item['id'] ?>" class="form-control" value="<?= htmlspecialchars((string)($item['name_en'] ?? '')) ?>" required><button class="btn btn-outline-secondary btn-translate" type="button" data-source="editItemNameAr<?= $item['id'] ?>" data-target="editItemNameEn<?= $item['id'] ?>" data-to="en"><i class="bi bi-translate"></i> EN</button></div></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Name (AR)" data-ar="الاسم (عربي)">Name (AR)</label><div class="input-group"><input type="text" name="name_ar" id="editItemNameAr<?= $item['id'] ?>" class="form-control" value="<?= htmlspecialchars((string)($item['name_ar'] ?? '')) ?>" required><button class="btn btn-outline-secondary btn-translate" type="button" data-source="editItemNameEn<?= $item['id'] ?>" data-target="editItemNameAr<?= $item['id'] ?>" data-to="ar"><i class="bi bi-translate"></i> AR</button></div></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="SKU / Barcode" data-ar="الباركود">SKU / Barcode</label><input type="text" name="sku" class="form-control" value="<?= htmlspecialchars((string)($item['sku'] ?? '')) ?>"><div class="form-text" data-en="Reserved 13-digit scale barcodes cannot be saved here. Use the 5-digit scale item code for weighing products." data-ar="لا يمكن حفظ باركود الميزان الكامل المكوّن من 13 رقمًا هنا. استخدم كود الصنف المكوّن من 5 أرقام لأصناف الميزان.">Reserved 13-digit scale barcodes cannot be saved here. Use the 5-digit scale item code for weighing products.</div></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Category" data-ar="الفئة">Category</label><select name="category_id" class="form-select"><option value="" data-en="---" data-ar="---">---</option><?php foreach ($data['categories'] ?? [] as $c): ?><option value="<?= $c['id'] ?>" <?= $c['id'] == $item['category_id'] ? 'selected' : '' ?> data-en="<?= htmlspecialchars(localized_option_label($c, 'en')) ?>" data-ar="<?= htmlspecialchars(localized_option_label($c, 'ar')) ?>"><?= htmlspecialchars(localized_option_label($c)) ?></option><?php endforeach; ?></select></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Unit" data-ar="الوحدة">Unit</label><select name="unit_id" class="form-select"><option value="" data-en="---" data-ar="---">---</option><?php foreach ($data['units'] ?? [] as $u): ?><option value="<?= $u['id'] ?>" <?= $u['id'] == $item['unit_id'] ? 'selected' : '' ?> data-en="<?= htmlspecialchars(localized_option_label($u, 'en')) ?>" data-ar="<?= htmlspecialchars(localized_option_label($u, 'ar')) ?>"><?= htmlspecialchars(localized_option_label($u)) ?></option><?php endforeach; ?></select></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Supplier" data-ar="المورد">Supplier</label><select name="supplier_id" class="form-select"><option value="">---</option><?php foreach ($data['suppliers'] ?? [] as $s): ?><option value="<?= $s['id'] ?>" <?= $s['id'] == $item['supplier_id'] ? 'selected' : '' ?>><?= htmlspecialchars($s['name']) ?></option><?php endforeach; ?></select></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Sale Price" data-ar="سعر البيع">Sale Price</label><input type="number" step="0.001" name="sale_price" class="form-control" value="<?= (float)$item['sale_price'] ?>"></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Purchase Price" data-ar="سعر الشراء">Purchase Price</label><input type="number" step="0.001" name="purchase_price" class="form-control" value="<?= (float)$item['purchase_price'] ?>"></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Stock Qty" data-ar="كمية المخزون">Stock Qty</label><input type="number" step="0.001" name="stock_quantity" class="form-control" value="<?= (float)$item['stock_quantity'] ?>"></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Min Stock Level" data-ar="الحد الأدنى للمخزون">Min Stock Level</label><input type="number" step="0.001" name="min_stock_level" class="form-control" value="<?= (float)$item['min_stock_level'] ?>"></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="VAT Rate (%)" data-ar="ضريبة القيمة المضافة (%)">VAT Rate (%)</label><input type="number" step="0.01" name="vat_rate" class="form-control" value="<?= number_format((float)$item['vat_rate'], 2, '.', '') ?>"></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Item Picture" data-ar="صورة الصنف">Item Picture</label><input type="file" name="image" class="form-control" accept="image/*"></div>
|
|
<div class="col-12 full-width"><hr><h6 data-en="Promotion Details" data-ar="تفاصيل العرض">Promotion Details</h6></div>
|
|
<div class="col-md-12 full-width"><div class="form-check form-switch mt-2"><input class="form-check-input isPromotionToggleEdit" type="checkbox" name="is_promotion" value="1" <?= $item['is_promotion'] ? 'checked' : '' ?> id="isPromotionToggleEdit<?= $item['id'] ?>" data-id="<?= $item['id'] ?>"><label class="form-check-label" for="isPromotionToggleEdit<?= $item['id'] ?>" data-en="On Promotion?" data-ar="في العرض؟">On Promotion?</label></div></div>
|
|
<div class="col-12 full-width promotionFieldsContainerEdit" id="promotionFieldsContainerEdit<?= $item['id'] ?>" style="display: <?= $item['is_promotion'] ? 'block' : 'none' ?>;"><div class="form-grid-3"><div><label class="form-label" data-en="Start Date" data-ar="تاريخ البداية">Start Date</label><input type="date" name="promotion_start" class="form-control" value="<?= $item['promotion_start'] ?>"></div><div><label class="form-label" data-en="End Date" data-ar="تاريخ النهاية">End Date</label><input type="date" name="promotion_end" class="form-control" value="<?= $item['promotion_end'] ?>"></div><div><label class="form-label" data-en="Percent (%)" data-ar="النسبة (%)">Percent (%)</label><input type="number" step="0.01" name="promotion_percent" class="form-control" value="<?= (float)$item['promotion_percent'] ?>"></div></div></div>
|
|
</div></div><div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_item" class="btn btn-primary" data-en="Update Item" data-ar="تحديث الصنف">Update Item</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<?php elseif ($page === 'expiry_report'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Expiry Report" data-ar="تقرير انتهاء الصلاحية">Expiry Report</h5>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div class="dropdown d-inline-block">
|
|
<button class="btn btn-outline-success btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-download"></i> <span data-en="Export" data-ar="تصدير">Export</span>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="index.php?<?= http_build_query(array_merge($_GET, ['page' => 'export', 'type' => 'expiry_report', 'format' => 'csv'])) ?>"><i class="bi bi-filetype-csv me-2"></i> CSV</a></li>
|
|
<li><a class="dropdown-item" href="index.php?<?= http_build_query(array_merge($_GET, ['page' => 'export', 'type' => 'expiry_report', 'format' => 'excel'])) ?>"><i class="bi bi-file-earmark-excel me-2"></i> Excel</a></li>
|
|
</ul>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<a href="index.php?page=expiry_report&filter=all" class="btn btn-sm <?= !isset($_GET['filter']) || $_GET['filter'] === 'all' ? 'btn-primary' : 'btn-outline-primary' ?>" data-en="All" data-ar="الكل">All</a>
|
|
<a href="index.php?page=expiry_report&filter=expired" class="btn btn-sm <?= isset($_GET['filter']) && $_GET['filter'] === 'expired' ? 'btn-danger' : 'btn-outline-danger' ?>" data-en="Expired" data-ar="منتهي">Expired</a>
|
|
<a href="index.php?page=expiry_report&filter=near_expiry" class="btn btn-sm <?= isset($_GET['filter']) && $_GET['filter'] === 'near_expiry' ? 'btn-warning' : 'btn-outline-warning' ?>" data-en="Near Expiry" data-ar="قريب الانتهاء">Near Expiry</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="SKU" data-ar="الباركود">SKU</th>
|
|
<th data-en="Item Name" data-ar="اسم الصنف">Item Name</th>
|
|
<th data-en="Category" data-ar="الفئة">Category</th>
|
|
<th data-en="Stock Level" data-ar="المخزون">Stock Level</th>
|
|
<th data-en="Expiry Date" data-ar="تاريخ الانتهاء">Expiry Date</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($data['expiry_items'])): ?>
|
|
<tr>
|
|
<td colspan="6" class="text-center text-muted p-4" data-en="No items found." data-ar="لا توجد أصناف.">No items found.</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
<?php foreach ($data['expiry_items'] as $item): ?>
|
|
<?php
|
|
$expiry_date = $item['expiry_date'] ?? '';
|
|
$expiry_ts = $expiry_date !== '' ? strtotime($expiry_date) : null;
|
|
$is_expired = $expiry_ts ? $expiry_ts <= strtotime(date('Y-m-d')) : false;
|
|
$is_near = $expiry_ts ? (!$is_expired && $expiry_ts <= strtotime(date('Y-m-d', strtotime('+30 days')))) : false;
|
|
?>
|
|
<tr class="<?= $is_expired ? 'table-danger' : ($is_near ? 'table-warning' : '') ?>">
|
|
<td><?= htmlspecialchars($item['sku']) ?></td>
|
|
<td>
|
|
<div class="fw-bold"><?= htmlspecialchars($item['name_en']) ?></div>
|
|
<div class="small text-muted"><?= htmlspecialchars($item['name_ar']) ?></div>
|
|
</td>
|
|
<td><?= htmlspecialchars($item['cat_en'] ?? '---') ?></td>
|
|
<td><?= number_format((float)$item['stock_quantity'], 3) ?></td>
|
|
<td><?= $expiry_date !== '' ? htmlspecialchars((string)$expiry_date) : '---' ?></td>
|
|
<td>
|
|
<?php if ($is_expired): ?>
|
|
<span class="badge bg-danger" data-en="Expired" data-ar="منتهي">Expired</span>
|
|
<?php elseif ($is_near): ?>
|
|
<span class="badge bg-warning text-dark" data-en="Near Expiry" data-ar="قريب الانتهاء">Near Expiry</span>
|
|
<?php else: ?>
|
|
<span class="badge bg-success" data-en="Good" data-ar="صالح">Good</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<?php elseif ($page === 'low_stock_report'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Low Stock Report" data-ar="تقرير نواقص المخزون">Low Stock Report</h5>
|
|
<button class="btn btn-outline-primary btn-sm d-print-none" onclick="window.print()">
|
|
<i class="bi bi-printer"></i> <span data-en="Print" data-ar="طباعة">Print</span>
|
|
</button>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="SKU" data-ar="الباركود">SKU</th>
|
|
<th data-en="Item Name" data-ar="اسم الصنف">Item Name</th>
|
|
<th data-en="Category" data-ar="الفئة">Category</th>
|
|
<th data-en="Supplier" data-ar="المورد">Supplier</th>
|
|
<th data-en="Min Level" data-ar="أدنى مستوى">Min Level</th>
|
|
<th data-en="Current Stock" data-ar="المخزون الحالي">Current Stock</th>
|
|
<th data-en="Shortage" data-ar="النقص">Shortage</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($data['low_stock_items'])): ?>
|
|
<tr>
|
|
<td colspan="7" class="text-center text-muted p-4" data-en="All items are above minimum levels." data-ar="جميع الأصناف فوق الحد الأدنى.">All items are above minimum levels.</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
<?php foreach ($data['low_stock_items'] as $item): ?>
|
|
<?php $shortage = (float)$item['min_stock_level'] - (float)$item['stock_quantity']; ?>
|
|
<tr class="<?= (float)$item['stock_quantity'] <= 0 ? 'table-danger' : 'table-warning' ?>">
|
|
<td><?= htmlspecialchars($item['sku']) ?></td>
|
|
<td>
|
|
<div class="fw-bold"><?= htmlspecialchars($item['name_en']) ?></div>
|
|
<div class="small text-muted"><?= htmlspecialchars($item['name_ar']) ?></div>
|
|
</td>
|
|
<td><?= htmlspecialchars($item['cat_en'] ?? '---') ?></td>
|
|
<td><?= htmlspecialchars($item['supplier_name'] ?? '---') ?></td>
|
|
<td><?= number_format((float)$item['min_stock_level'], 2) ?></td>
|
|
<td>
|
|
<span class="badge <?= (float)$item['stock_quantity'] <= 0 ? 'bg-danger' : 'bg-warning text-dark' ?>">
|
|
<?= number_format((float)$item['stock_quantity'], 3) ?>
|
|
</span>
|
|
</td>
|
|
<td class="fw-bold text-danger"><?= number_format((float)$shortage, 3) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<?php elseif ($page === 'loyalty_history'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Loyalty Transaction History" data-ar="سجل عمليات الولاء">Loyalty Transaction History</h5>
|
|
<button class="btn btn-outline-primary btn-sm d-print-none" onclick="window.print()">
|
|
<i class="bi bi-printer"></i> <span data-en="Print" data-ar="طباعة">Print</span>
|
|
</button>
|
|
</div>
|
|
<div class="bg-light p-3 rounded mb-4 d-print-none">
|
|
<form method="GET" class="row g-3 align-items-end">
|
|
<input type="hidden" name="page" value="loyalty_history">
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-bold" data-en="Customer" data-ar="العميل">Customer</label>
|
|
<select name="customer_id" class="form-select select2">
|
|
<option value="">All Customers</option>
|
|
<?php foreach ($data['customers_list'] as $c): ?>
|
|
<option value="<?= $c['id'] ?>" <?= (($_GET['customer_id'] ?? '') == $c['id']) ? 'selected' : '' ?>><?= htmlspecialchars($c['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="Type" data-ar="النوع">Type</label>
|
|
<select name="type" class="form-select">
|
|
<option value="">All Types</option>
|
|
<option value="earned" <?= (($_GET['type'] ?? '') == 'earned') ? 'selected' : '' ?>>Earned</option>
|
|
<option value="redeemed" <?= (($_GET['type'] ?? '') == 'redeemed') ? 'selected' : '' ?>>Redeemed</option>
|
|
<option value="adjustment" <?= (($_GET['type'] ?? '') == 'adjustment') ? 'selected' : '' ?>>Adjustment</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button type="submit" class="btn btn-primary w-100">
|
|
<i class="bi bi-filter"></i> <span data-en="Filter" data-ar="تصفية">Filter</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Date" data-ar="التاريخ">Date</th>
|
|
<th data-en="Customer" data-ar="العميل">Customer</th>
|
|
<th data-en="Tier" data-ar="الفئة">Tier</th>
|
|
<th data-en="Type" data-ar="النوع">Type</th>
|
|
<th data-en="Points" data-ar="النقاط">Points</th>
|
|
<th data-en="Description" data-ar="الوصف">Description</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($data['loyalty_transactions'])): ?>
|
|
<tr><td colspan="6" class="text-center py-4 text-muted">No transactions found.</td></tr>
|
|
<?php endif; ?>
|
|
<?php foreach ($data['loyalty_transactions'] as $lt): ?>
|
|
<tr>
|
|
<td><?= date('Y-m-d H:i', strtotime($lt['created_at'])) ?></td>
|
|
<td>
|
|
<div class="fw-bold"><?= htmlspecialchars($lt['customer_name']) ?></div>
|
|
<div class="smaller text-muted">Current Balance: <?= number_format($lt['loyalty_points'], 0) ?> pts</div>
|
|
</td>
|
|
<td>
|
|
<?php
|
|
$tier = $lt['loyalty_tier'];
|
|
$badge = ($tier === 'gold') ? 'bg-warning text-dark' : (($tier === 'silver') ? 'bg-info text-dark' : 'bg-secondary');
|
|
?>
|
|
<span class="badge text-uppercase <?= $badge ?>"><?= $tier ?></span>
|
|
</td>
|
|
<td>
|
|
<?php
|
|
$type = $lt['transaction_type'];
|
|
$typeBadge = ($type === 'earned') ? 'bg-success' : (($type === 'redeemed') ? 'bg-danger' : 'bg-info');
|
|
?>
|
|
<span class="badge <?= $typeBadge ?>"><?= ucfirst($type) ?></span>
|
|
</td>
|
|
<td class="fw-bold <?= (float)$lt['points_change'] > 0 ? 'text-success' : 'text-danger' ?>">
|
|
<?= (float)$lt['points_change'] > 0 ? '+' : '' ?><?= number_format($lt['points_change'], 0) ?>
|
|
</td>
|
|
<td class="small"><?= htmlspecialchars($lt['description']) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<?php elseif ($page === 'pos'): ?>
|
|
<?php
|
|
// Check for active session
|
|
$stmt = db()->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);
|
|
?>
|
|
<div class="pos-container">
|
|
<div class="pos-products">
|
|
<div class="bg-white p-3 rounded mb-3 shadow-sm d-flex gap-2">
|
|
<div class="input-group flex-grow-1">
|
|
<span class="input-group-text bg-transparent border-end-0"><i class="bi bi-search"></i></span>
|
|
<input type="text" id="productSearch" class="form-control border-start-0" placeholder="Search products by name or SKU..." data-en="Search products..." data-ar="بحث عن منتجات...">
|
|
</div>
|
|
<div class="input-group flex-grow-1">
|
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-upc-scan"></i></span>
|
|
<input type="text" id="barcodeInput" class="form-control border-start-0" placeholder="Scan barcode..." data-en="Scan barcode..." data-ar="امسح الباركود..." autofocus>
|
|
</div>
|
|
<button class="btn btn-warning d-flex align-items-center gap-2" onclick="cart.openHeldCartsModal()">
|
|
<i class="bi bi-pause-btn-fill"></i>
|
|
<span class="d-none d-xl-inline" data-en="Held List" data-ar="قائمة الانتظار">Held List</span>
|
|
</button>
|
|
</div>
|
|
<div class="product-grid" id="productGrid">
|
|
<?php foreach ($products as $p): ?>
|
|
<div class="product-card" data-id="<?= $p['id'] ?>" data-name-en="<?= htmlspecialchars($p['name_en']) ?>" data-name-ar="<?= htmlspecialchars($p['name_ar']) ?>" data-price="<?= $p['sale_price'] ?>" data-sku="<?= htmlspecialchars($p['sku']) ?>" data-stock-quantity="<?= (float)$p['stock_quantity'] ?>" data-vat-rate="<?= $p['vat_rate'] ?>">
|
|
<?php if ($p['image_path']): ?>
|
|
<img src="<?= htmlspecialchars($p['image_path']) ?>" alt="<?= htmlspecialchars($p['name_en']) ?>">
|
|
<?php else: ?>
|
|
<div class="bg-light d-flex align-items-center justify-content-center rounded mb-2" style="height: 120px;">
|
|
<i class="bi bi-box-seam text-muted" style="font-size: 3rem;"></i>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="mb-1 product-name" data-en="<?= htmlspecialchars($p['name_en']) ?>" data-ar="<?= htmlspecialchars($p['name_ar']) ?>">
|
|
<?php if(!empty($p['name_ar'])): ?>
|
|
<div><?= htmlspecialchars($p['name_ar']) ?></div>
|
|
<div class="small text-secondary" style="font-size: 0.75rem; line-height: 1.1;"><?= htmlspecialchars($p['name_en']) ?></div>
|
|
<?php else: ?>
|
|
<div><?= htmlspecialchars($p['name_en']) ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="d-flex justify-content-between align-items-center mt-auto">
|
|
<div class="d-flex flex-column">
|
|
<?php if ($p['sale_price'] < $p['original_price']): ?>
|
|
<span class="text-muted smaller text-decoration-line-through">OMR <?= number_format($p['original_price'], 3) ?></span>
|
|
<?php endif; ?>
|
|
<span class="price text-primary fw-bold">OMR <?= number_format((float)$p['sale_price'], 3) ?></span>
|
|
</div>
|
|
<span class="badge bg-light text-dark small"><?= (float)$p['stock_quantity'] ?> left</span>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pos-cart">
|
|
<div class="p-3 border-bottom d-flex justify-content-between align-items-center">
|
|
<h6 class="m-0 fw-bold"><i class="bi bi-cart3 me-2"></i>Cart</h6>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-sm btn-outline-info" onclick="window.open('customer-display.php?v=<?= time() ?>', 'CustomerDisplay', 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=' + screen.availWidth + ',height=' + screen.availHeight + ',left=0,top=0')" title="Customer Display"><i class="bi bi-display me-1"></i> Customer Screen</button>
|
|
<?php if ($active_session): ?>
|
|
<button class="btn btn-sm btn-outline-dark" data-bs-toggle="modal" data-bs-target="#closeRegisterModal" title="Close Register"><i class="bi bi-x-circle me-1"></i data-en="Close" data-ar="إغلاق">Close</button>
|
|
<?php endif; ?>
|
|
<button class="btn btn-sm btn-outline-warning" onclick="cart.openHeldCartsModal()" title="Held List"><i class="bi bi-list-task"></i></button>
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="cart.hold()" title="Hold Cart"><i class="bi bi-pause-circle"></i></button>
|
|
<button class="btn btn-sm btn-outline-danger" onclick="cart.clear()" title="Clear Cart"><i class="bi bi-trash"></i></button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-3 bg-light border-bottom">
|
|
<div class="mb-2">
|
|
<label class="small fw-bold mb-1" data-en="Customer" data-ar="العميل">Customer</label>
|
|
<div class="d-flex gap-2">
|
|
<select id="posCustomer" class="form-select form-select-sm" onchange="cart.onCustomerChange()">
|
|
<option value="">Walk-in Customer</option>
|
|
<?php foreach ($customers as $c): ?>
|
|
<option value="<?= $c['id'] ?>"
|
|
data-points="<?= $c['loyalty_points'] ?>"
|
|
data-tier="<?= $c['loyalty_tier'] ?>"
|
|
data-multiplier="<?= getLoyaltyMultiplier($c['loyalty_tier'] ?? 'bronze') ?>"
|
|
data-spent="<?= $c['total_spent'] ?>">
|
|
<?= htmlspecialchars($c['name']) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addCustomerModal"><i class="bi bi-plus"></i></button>
|
|
</div>
|
|
<div id="loyaltyDisplay" class="mt-2 p-2 rounded bg-light border border-primary-subtle" style="display:none">
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<div>
|
|
<span id="tierBadge" class="badge text-uppercase">Bronze</span>
|
|
<span class="fw-bold ms-1 text-primary"><span id="customerPoints">0</span> pts</span>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" id="redeemLoyalty" onchange="cart.render()">
|
|
<label class="form-check-label small fw-bold" for="redeemLoyalty">Redeem</label>
|
|
</div>
|
|
</div>
|
|
<div class="progress" style="height: 4px;">
|
|
<div id="tierProgress" class="progress-bar bg-primary" role="progressbar" style="width: 0%"></div>
|
|
</div>
|
|
<div id="nextTierInfo" class="smaller text-muted mt-1">Spend more to unlock Silver</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="small fw-bold mb-1">Discount Code</label>
|
|
<div class="input-group input-group-sm">
|
|
<input type="text" id="discountCode" class="form-control" placeholder="Code">
|
|
<button class="btn btn-outline-primary" type="button" onclick="cart.applyDiscount()">Apply</button>
|
|
</div>
|
|
<div id="appliedDiscountInfo" class="smaller text-primary mt-1" style="display:none"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cart-items" id="cartItems">
|
|
<!-- Cart items will be injected here -->
|
|
<div class="text-center text-muted mt-5">
|
|
<i class="bi bi-cart-x" style="font-size: 3rem;"></i>
|
|
<p>Cart is empty</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cart-total">
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span data-en="Subtotal (Excl. VAT)" data-ar="المجموع (بدون الضريبة)">Subtotal (Excl. VAT)</span>
|
|
<span id="posSubtotal"><?= __('currency') ?> 0.000</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span data-en="VAT" data-ar="الضريبة">VAT</span>
|
|
<span id="posVat"><?= __('currency') ?> 0.000</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-3 fw-bold fs-5 border-top pt-2">
|
|
<span data-en="Total" data-ar="الإجمالي">Total</span>
|
|
<span id="posTotal" class="text-primary"><?= __('currency') ?> 0.000</span>
|
|
</div>
|
|
<button class="btn btn-primary w-100 py-2 fw-bold" id="checkoutBtn" onclick="cart.checkout()">
|
|
PLACE ORDER
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const cart = {
|
|
items: [],
|
|
discount: null,
|
|
customerPoints: 0,
|
|
selectedPaymentMethod: 'cash',
|
|
payments: [],
|
|
loyaltySettings: {
|
|
enabled: <?= json_encode($data['settings']['loyalty_enabled'] ?? '0') ?>,
|
|
pointsPerUnit: parseFloat(<?= json_encode($data['settings']['loyalty_points_per_unit'] ?? '1') ?>),
|
|
redeemPointsPerUnit: parseFloat(<?= json_encode($data['settings']['loyalty_redeem_points_per_unit'] ?? '100') ?>)
|
|
},
|
|
allCreditCustomers: <?php
|
|
$custData = [];
|
|
foreach ($customers as $c) {
|
|
$custData[] = [
|
|
'value' => (string)$c['id'],
|
|
'text' => $c['name'] . ' (' . ($c['phone'] ?? '') . ')'
|
|
];
|
|
}
|
|
echo json_encode($custData);
|
|
?>,
|
|
broadcast() {
|
|
try {
|
|
// Ensure items is an array
|
|
if (!Array.isArray(this.items)) this.items = [];
|
|
|
|
const subtotal = this.items.reduce((sum, item) => sum + (parseFloat(item.price) * parseFloat(item.qty)), 0);
|
|
const totalVat = this.items.reduce((sum, item) => {
|
|
const price = parseFloat(item.price) || 0;
|
|
const qty = parseFloat(item.qty) || 0;
|
|
const vatRate = (item.vatRate !== undefined && item.vatRate !== null) ? item.vatRate : 0;
|
|
return sum + (price * qty * (vatRate / (100 + vatRate)));
|
|
}, 0);
|
|
let discountAmount = 0;
|
|
if (this.discount) {
|
|
discountAmount = this.discount.type === 'percentage' ? subtotal * (parseFloat(this.discount.value) / 100) : parseFloat(this.discount.value);
|
|
}
|
|
const redeemSwitch = document.getElementById('redeemLoyalty');
|
|
const redeemRate = (this.loyaltySettings && this.loyaltySettings.redeemPointsPerUnit) ? this.loyaltySettings.redeemPointsPerUnit : 100;
|
|
let loyaltyRedeemed = (redeemSwitch && redeemSwitch.checked) ? Math.min(Math.max(0, subtotal - discountAmount), (parseFloat(this.customerPoints) || 0) / redeemRate) : 0;
|
|
const total = Math.max(0, subtotal - discountAmount - loyaltyRedeemed);
|
|
|
|
const customerSelect = document.getElementById('posCustomer');
|
|
const customerName = customerSelect ? customerSelect.options[customerSelect.selectedIndex].text : '';
|
|
|
|
const payload = {
|
|
items: this.items.map(i => {
|
|
const price = parseFloat(i.price) || 0;
|
|
const qty = parseFloat(i.qty) || 0;
|
|
const vatRate = (i.vatRate !== undefined && i.vatRate !== null) ? i.vatRate : 0;
|
|
const vatAmount = price * qty * (vatRate / (100 + vatRate));
|
|
return {
|
|
name: (function(){ let n = ''; if(i.nameAr) n += '<div>'+i.nameAr+'</div>'; if(i.nameEn) n += '<div>'+i.nameEn+'</div>'; return n || 'Unknown Item'; })(),
|
|
price: price,
|
|
qty: qty,
|
|
vat: vatAmount
|
|
};
|
|
}),
|
|
subtotal: parseFloat(subtotal) || 0,
|
|
vat: parseFloat(totalVat) || 0,
|
|
discount: parseFloat(discountAmount) || 0,
|
|
loyalty: parseFloat(loyaltyRedeemed) || 0,
|
|
total: parseFloat(total) || 0,
|
|
currency: "<?= __('currency') ?>",
|
|
customerName: customerName,
|
|
theme: document.body.className,
|
|
timestamp: Date.now()
|
|
};
|
|
|
|
localStorage.setItem('pos_cart_update', JSON.stringify(payload));
|
|
} catch (e) { console.error('Broadcast error', e); }
|
|
},
|
|
add(product) {
|
|
if (!this.items) this.items = [];
|
|
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
|
|
const currentStock = parseFloat(product.stock_quantity) || 0;
|
|
const addQty = Math.max(parseFloat(product.qty) || 1, 0.001);
|
|
const unitPrice = (product.price !== undefined && product.price !== null) ? (parseFloat(product.price) || 0) : (parseFloat(product.sale_price) || 0);
|
|
const normalizedProduct = {...product, price: unitPrice};
|
|
|
|
const existing = this.items.find(item => item.id === product.id);
|
|
if (existing) {
|
|
if (!allowZeroStock && (existing.qty + addQty) > currentStock) {
|
|
Swal.fire('Error', 'Insufficient stock!', 'error');
|
|
return;
|
|
}
|
|
existing.qty = Number((existing.qty + addQty).toFixed(3));
|
|
existing.price = unitPrice;
|
|
} else {
|
|
if (!allowZeroStock && currentStock < addQty) {
|
|
Swal.fire('Error', 'Insufficient stock!', 'error');
|
|
return;
|
|
}
|
|
this.items.push({...normalizedProduct, qty: Number(addQty.toFixed(3))});
|
|
}
|
|
|
|
this.render();
|
|
|
|
// Add visual feedback
|
|
const lang = document.documentElement.lang || 'en';
|
|
const displayName = lang === 'ar' ? (product.nameAr || product.nameEn) : (product.nameEn || product.nameAr);
|
|
Swal.fire({
|
|
toast: true,
|
|
position: 'top-end',
|
|
icon: 'success',
|
|
title: (lang === 'ar' ? 'تم إضافة: ' : 'Added: ') + displayName,
|
|
showConfirmButton: false,
|
|
timer: 800
|
|
});
|
|
},
|
|
remove(id) {
|
|
this.items = this.items.filter(item => item.id !== id);
|
|
this.render();
|
|
},
|
|
updateQty(id, delta) {
|
|
if (!this.items) return;
|
|
const item = this.items.find(i => i.id === id);
|
|
if (item) {
|
|
const allowZeroStock = (typeof companySettings !== 'undefined' && String(companySettings.allow_zero_stock_sell) === '1');
|
|
const currentStock = parseFloat(item.stock_quantity) || 0;
|
|
|
|
if (delta > 0 && !allowZeroStock && (item.qty + delta) > currentStock) {
|
|
Swal.fire('Error', 'Insufficient stock!', 'error');
|
|
return;
|
|
}
|
|
|
|
item.qty += delta;
|
|
if (item.qty <= 0) this.remove(id);
|
|
else this.render();
|
|
}
|
|
},
|
|
clear() {
|
|
this.items = [];
|
|
this.discount = null;
|
|
this.customerPoints = 0;
|
|
const discInput = document.getElementById('discountCode');
|
|
if (discInput) discInput.value = '';
|
|
const discInfo = document.getElementById('appliedDiscountInfo');
|
|
if (discInfo) discInfo.style.display = 'none';
|
|
const redeemSwitch = document.getElementById('redeemLoyalty');
|
|
if (redeemSwitch) redeemSwitch.checked = false;
|
|
const loyaltyDisplay = document.getElementById('loyaltyDisplay');
|
|
if (loyaltyDisplay) loyaltyDisplay.style.display = 'none';
|
|
this.render();
|
|
},
|
|
onCustomerChange() {
|
|
const select = document.getElementById('posCustomer');
|
|
const option = select.options[select.selectedIndex];
|
|
const display = document.getElementById('loyaltyDisplay');
|
|
|
|
if (!select.value || this.loyaltySettings.enabled !== '1') {
|
|
if (display) display.style.display = 'none';
|
|
this.customerPoints = 0;
|
|
this.customerTier = 'bronze';
|
|
this.customerMultiplier = 1.0;
|
|
const redeemSwitch = document.getElementById('redeemLoyalty');
|
|
if (redeemSwitch) redeemSwitch.checked = false;
|
|
this.render();
|
|
return;
|
|
}
|
|
|
|
this.customerPoints = parseFloat(option.dataset.points) || 0;
|
|
this.customerTier = option.dataset.tier || 'bronze';
|
|
this.customerMultiplier = parseFloat(option.dataset.multiplier) || 1.0;
|
|
const spent = parseFloat(option.dataset.spent) || 0;
|
|
|
|
document.getElementById('customerPoints').innerText = Math.floor(this.customerPoints);
|
|
const badge = document.getElementById('tierBadge');
|
|
badge.innerText = this.customerTier;
|
|
badge.className = 'badge text-uppercase ' + (this.customerTier === 'gold' ? 'bg-warning text-dark' : (this.customerTier === 'silver' ? 'bg-info text-dark' : 'bg-secondary'));
|
|
|
|
const progressBar = document.getElementById('tierProgress');
|
|
const nextTierInfo = document.getElementById('nextTierInfo');
|
|
let progress = 0;
|
|
if (this.customerTier === 'bronze') {
|
|
progress = (spent / 500) * 100;
|
|
nextTierInfo.innerText = `Spend ${(500 - spent).toFixed(3)} OMR more for Silver (1.2x points)`;
|
|
} else if (this.customerTier === 'silver') {
|
|
progress = ((spent - 500) / 1000) * 100;
|
|
nextTierInfo.innerText = `Spend ${(1500 - spent).toFixed(3)} OMR more for Gold (1.5x points)`;
|
|
} else {
|
|
progress = 100;
|
|
nextTierInfo.innerText = 'You are a Gold member! (1.5x points)';
|
|
}
|
|
progressBar.style.width = Math.min(100, progress) + '%';
|
|
|
|
display.style.display = 'block';
|
|
this.render();
|
|
},
|
|
async applyDiscount() {
|
|
const code = document.getElementById('discountCode').value.trim();
|
|
if (!code) return;
|
|
try {
|
|
const resp = await fetch(`index.php?action=validate_discount&code=${code}`);
|
|
const res = await resp.json();
|
|
if (res.success) {
|
|
this.discount = res.discount;
|
|
const info = document.getElementById('appliedDiscountInfo');
|
|
info.innerText = `Applied: ${this.discount.code} (${this.discount.type === 'percentage' ? this.discount.value + '%' : 'OMR ' + parseFloat(this.discount.value).toFixed(3)})`;
|
|
info.style.display = 'block';
|
|
this.render();
|
|
} else {
|
|
Swal.fire('Error', res.error, 'error');
|
|
}
|
|
} catch (err) { console.error(err); }
|
|
},
|
|
async hold() {
|
|
if (this.items.length === 0) return;
|
|
const { value: name } = await Swal.fire({
|
|
title: 'Hold Cart',
|
|
input: 'text',
|
|
inputLabel: 'Enter a name for this cart',
|
|
inputValue: 'Cart ' + new Date().toLocaleTimeString(),
|
|
showCancelButton: true
|
|
});
|
|
if (name) {
|
|
const formData = new FormData();
|
|
formData.append('action', 'hold_pos_cart');
|
|
formData.append('cart_name', name);
|
|
formData.append('items', JSON.stringify(this.items));
|
|
formData.append('customer_id', document.getElementById('posCustomer').value);
|
|
const resp = await fetch('index.php', { method: 'POST', body: formData });
|
|
const res = await resp.json();
|
|
if (res.success) {
|
|
this.clear();
|
|
Swal.fire('Held', 'Cart has been parked', 'success');
|
|
}
|
|
}
|
|
},
|
|
async openHeldCartsModal() {
|
|
try {
|
|
const resp = await fetch('index.php?action=get_held_carts');
|
|
const text = await resp.text();
|
|
let carts;
|
|
try {
|
|
carts = JSON.parse(text);
|
|
} catch (e) {
|
|
console.error('Failed to parse held carts:', text);
|
|
throw new Error('Invalid server response');
|
|
}
|
|
const lang = document.documentElement.lang || 'en';
|
|
let html = '<div class="list-group list-group-flush shadow-sm rounded">';
|
|
if (carts.length === 0) {
|
|
html += `
|
|
<div class="text-center p-5 text-muted">
|
|
<i class="bi bi-folder2-open mb-3 d-block" style="font-size: 3rem;"></i>
|
|
<p data-en="No held carts found" data-ar="لا توجد طلبات معلقة">${lang === 'ar' ? 'لا توجد طلبات معلقة' : 'No held carts found'}</p>
|
|
</div>`;
|
|
}
|
|
carts.forEach(c => {
|
|
html += `
|
|
<div class="list-group-item d-flex justify-content-between align-items-center p-3 hover-bg-light border-start-0 border-end-0">
|
|
<div class="text-start">
|
|
<div class="fw-bold text-primary">${c.cart_name}</div>
|
|
<div class="small text-muted">
|
|
<i class="bi bi-person me-1"></i>${c.customer_name || (lang === 'ar' ? 'عميل عابر' : 'Walk-in')}
|
|
<span class="mx-2 text-silver">|</span>
|
|
<i class="bi bi-clock me-1"></i>${new Date(c.created_at).toLocaleString()}
|
|
</div>
|
|
</div>
|
|
<div class="btn-group">
|
|
<button class="btn btn-sm btn-primary" onclick="cart.resume(${c.id})">
|
|
<i class="bi bi-arrow-repeat me-1"></i><span data-en="Resume" data-ar="استرجاع">${lang === 'ar' ? 'استرجاع' : 'Resume'}</span>
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-danger" onclick="cart.deleteHeld(${c.id})">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
Swal.fire({
|
|
title: lang === 'ar' ? 'الطلبات المعلقة' : 'Held Carts',
|
|
html: html,
|
|
showConfirmButton: false,
|
|
width: '700px',
|
|
customClass: {
|
|
container: 'held-carts-swal'
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
Swal.fire('Error', 'Failed to load held carts: ' + err.message, 'error');
|
|
}
|
|
},
|
|
async resume(id) {
|
|
try {
|
|
const resp = await fetch('index.php?action=get_held_carts');
|
|
const carts = await resp.json();
|
|
const c = carts.find(x => x.id == id);
|
|
if (c) {
|
|
this.items = JSON.parse(c.items_json);
|
|
document.getElementById('posCustomer').value = c.customer_id || '';
|
|
await this.onCustomerChange();
|
|
await this.deleteHeld(id, true);
|
|
Swal.close();
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
Swal.fire('Error', 'Failed to resume cart', 'error');
|
|
}
|
|
},
|
|
async deleteHeld(id, silent = false) {
|
|
const formData = new FormData();
|
|
formData.append('action', 'delete_held_cart');
|
|
formData.append('id', id);
|
|
await fetch('index.php', { method: 'POST', body: formData });
|
|
if (!silent) this.openHeldCartsModal();
|
|
},
|
|
render() {
|
|
try {
|
|
const container = document.getElementById('cartItems');
|
|
if (!container) return;
|
|
|
|
const lang = document.documentElement.lang || 'en';
|
|
const items = Array.isArray(this.items) ? this.items : [];
|
|
|
|
if (items.length === 0) {
|
|
container.innerHTML = `<div class="text-center text-muted mt-5"><i class="bi bi-cart-x" style="font-size: 3rem;"></i><p data-en="Cart is empty" data-ar="السلة فارغة">${lang === 'ar' ? 'السلة فارغة' : 'Cart is empty'}</p></div>`;
|
|
const subtotalEl = document.getElementById('posSubtotal');
|
|
const totalEl = document.getElementById('posTotal');
|
|
const checkoutBtn = document.getElementById('checkoutBtn');
|
|
|
|
if (subtotalEl) subtotalEl.innerText = '<?= __('currency') ?> 0.000';
|
|
if (totalEl) totalEl.innerText = '<?= __('currency') ?> 0.000';
|
|
if (checkoutBtn) checkoutBtn.disabled = true;
|
|
this.broadcast();
|
|
return;
|
|
}
|
|
|
|
let subtotal = 0;
|
|
let totalVat = 0;
|
|
container.innerHTML = items.map(item => {
|
|
const price = parseFloat(item.price) || 0;
|
|
const qty = parseFloat(item.qty) || 0;
|
|
const itemTotal = price * qty;
|
|
subtotal += itemTotal;
|
|
const vatRate = (item.vatRate !== undefined && item.vatRate !== null) ? item.vatRate : 0;
|
|
const itemVat = itemTotal * (vatRate / (100 + vatRate));
|
|
totalVat += itemVat;
|
|
|
|
let nameHtml = '';
|
|
if (item.nameAr) nameHtml += `<div>${item.nameAr}</div>`;
|
|
if (item.nameEn) nameHtml += `<div>${item.nameEn}</div>`;
|
|
if (!nameHtml) nameHtml = '<div>' + (item.nameAr || item.nameEn || 'Unknown Item') + '</div>';
|
|
|
|
return `
|
|
<div class="cart-item">
|
|
<div class="flex-grow-1">
|
|
<div class="small">${nameHtml}</div>
|
|
<div class="text-muted smaller"><?= __('currency') ?> ${price.toFixed(3)} <span class="badge bg-light text-dark smaller">VAT ${parseFloat(vatRate || 0).toFixed(2)}%</span></div>
|
|
</div>
|
|
<div class="qty-controls mx-3">
|
|
<button class="qty-btn" onclick="cart.updateQty(${item.id}, -1)">-</button>
|
|
<span class="small fw-bold">${qty}</span>
|
|
<button class="qty-btn" onclick="cart.updateQty(${item.id}, 1)">+</button>
|
|
</div>
|
|
<div class="text-end" style="min-width: 80px;">
|
|
<div class="fw-bold small"><?= __('currency') ?> ${itemTotal.toFixed(3)}</div>
|
|
<div class="smaller text-muted">VAT: ${itemVat.toFixed(2)}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
let discountAmount = 0;
|
|
if (this.discount) {
|
|
if (this.discount.type === 'percentage') {
|
|
discountAmount = subtotal * (parseFloat(this.discount.value) / 100);
|
|
} else {
|
|
discountAmount = parseFloat(this.discount.value);
|
|
}
|
|
}
|
|
|
|
let loyaltyRedeemedValue = 0;
|
|
const redeemSwitch = document.getElementById('redeemLoyalty');
|
|
if (redeemSwitch && redeemSwitch.checked) {
|
|
const maxRedeemValue = subtotal - discountAmount;
|
|
const redeemRate = (this.loyaltySettings && this.loyaltySettings.redeemPointsPerUnit) ? this.loyaltySettings.redeemPointsPerUnit : 100;
|
|
const availableRedeemValue = (parseFloat(this.customerPoints) || 0) / redeemRate;
|
|
loyaltyRedeemedValue = Math.min(Math.max(0, maxRedeemValue), availableRedeemValue);
|
|
}
|
|
|
|
const total = Math.max(0, subtotal - discountAmount - loyaltyRedeemedValue);
|
|
const multiplier = parseFloat(this.customerMultiplier) || 1.0;
|
|
const pointsToEarn = Math.floor(total * multiplier);
|
|
|
|
const subtotalDisplay = document.getElementById('posSubtotal');
|
|
if (subtotalDisplay) subtotalDisplay.innerText = '<?= __('currency') ?> ' + (subtotal - totalVat).toFixed(3);
|
|
|
|
const vatDisplay = document.getElementById('posVat');
|
|
if (vatDisplay) vatDisplay.innerText = '<?= __('currency') ?> ' + totalVat.toFixed(2);
|
|
|
|
let totalHtml = '';
|
|
if (discountAmount > 0) totalHtml += `<div class="smaller text-danger">- Disc: <?= __('currency') ?> ${discountAmount.toFixed(3)}</div>`;
|
|
if (loyaltyRedeemedValue > 0) totalHtml += `<div class="smaller text-success">- Loyalty: <?= __('currency') ?> ${loyaltyRedeemedValue.toFixed(3)}</div>`;
|
|
|
|
const customerId = document.getElementById('posCustomer') ? document.getElementById('posCustomer').value : '';
|
|
if (customerId) {
|
|
totalHtml += `<div class="smaller text-info">+ Earn: ${pointsToEarn} pts</div>`;
|
|
}
|
|
|
|
totalHtml += '<?= __('currency') ?> ' + total.toFixed(3);
|
|
|
|
const totalDisplay = document.getElementById('posTotal');
|
|
if (totalDisplay) {
|
|
totalDisplay.innerHTML = totalHtml;
|
|
}
|
|
|
|
const checkoutBtn = document.getElementById('checkoutBtn');
|
|
if (checkoutBtn) checkoutBtn.disabled = false;
|
|
this.broadcast();
|
|
} catch (e) {
|
|
console.error('Cart render error:', e);
|
|
}
|
|
},
|
|
async checkout() {
|
|
if (this.items.length === 0) return;
|
|
|
|
const customerSelect = document.getElementById('posCustomer');
|
|
const customerName = customerSelect.options[customerSelect.selectedIndex].text;
|
|
document.getElementById('paymentCustomerName').innerText = customerName;
|
|
|
|
const subtotal = this.items.reduce((sum, item) => sum + (item.price * item.qty), 0);
|
|
let discountAmount = 0;
|
|
if (this.discount) {
|
|
discountAmount = this.discount.type === 'percentage' ? subtotal * (parseFloat(this.discount.value) / 100) : parseFloat(this.discount.value);
|
|
}
|
|
const redeemSwitch = document.getElementById('redeemLoyalty');
|
|
let loyaltyRedeemedValue = (redeemSwitch && redeemSwitch.checked) ? Math.min(subtotal - discountAmount, this.customerPoints / 100) : 0;
|
|
const total = subtotal - discountAmount - loyaltyRedeemedValue;
|
|
|
|
this.payments = [];
|
|
this.renderPayments();
|
|
document.getElementById('paymentAmountDue').innerText = total.toFixed(3);
|
|
document.getElementById('partialAmount').value = total.toFixed(3);
|
|
|
|
// Sync credit customer selection if credit is default or already selected
|
|
const creditSection = document.getElementById('creditCustomerSection');
|
|
if (this.selectedPaymentMethod === 'credit') {
|
|
creditSection.style.display = 'block';
|
|
const creditSelect = $('#paymentCreditCustomer');
|
|
creditSelect.val(customerSelect.value).trigger('change');
|
|
} else {
|
|
creditSection.style.display = 'none';
|
|
}
|
|
|
|
this.updateRemaining();
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('posPaymentModal'));
|
|
modal.show();
|
|
},
|
|
selectMethod(method, btn) {
|
|
this.selectedPaymentMethod = method;
|
|
document.querySelectorAll('.payment-method-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
|
|
const creditSection = document.getElementById('creditCustomerSection');
|
|
if (method === 'credit') {
|
|
creditSection.style.display = 'block';
|
|
// Sync with main customer select
|
|
const creditSelect = $('#paymentCreditCustomer');
|
|
creditSelect.val(document.getElementById('posCustomer').value).trigger('change');
|
|
} else {
|
|
creditSection.style.display = 'none';
|
|
}
|
|
|
|
this.updateRemaining();
|
|
},
|
|
fillPartial(amount) {
|
|
const input = document.getElementById('partialAmount');
|
|
input.value = parseFloat(amount).toFixed(3);
|
|
this.updateRemaining();
|
|
},
|
|
addPaymentLine() {
|
|
const amount = parseFloat(document.getElementById('partialAmount').value) || 0;
|
|
if (amount <= 0) return;
|
|
|
|
this.payments.push({
|
|
method: this.selectedPaymentMethod,
|
|
amount: amount
|
|
});
|
|
this.renderPayments();
|
|
this.updateRemaining();
|
|
|
|
// Auto-fill remaining for next line if any
|
|
const remaining = this.getRemaining();
|
|
document.getElementById('partialAmount').value = remaining > 0 ? remaining.toFixed(3) : '0.000';
|
|
},
|
|
removePaymentLine(index) {
|
|
this.payments.splice(index, 1);
|
|
this.renderPayments();
|
|
this.updateRemaining();
|
|
},
|
|
getGrandTotal() {
|
|
const subtotal = this.items.reduce((sum, item) => sum + (item.price * item.qty), 0);
|
|
let discountAmount = 0;
|
|
if (this.discount) {
|
|
discountAmount = this.discount.type === 'percentage' ? subtotal * (parseFloat(this.discount.value) / 100) : parseFloat(this.discount.value);
|
|
}
|
|
const redeemSwitch = document.getElementById('redeemLoyalty');
|
|
let loyaltyRedeemedValue = (redeemSwitch && redeemSwitch.checked) ? Math.min(subtotal - discountAmount, this.customerPoints / this.loyaltySettings.redeemPointsPerUnit) : 0;
|
|
return subtotal - discountAmount - loyaltyRedeemedValue;
|
|
},
|
|
getRemaining() {
|
|
const total = this.getGrandTotal();
|
|
const paid = this.payments.reduce((sum, p) => sum + p.amount, 0);
|
|
return total - paid;
|
|
},
|
|
renderPayments() {
|
|
const container = document.getElementById('paymentList');
|
|
const methodLabels = {
|
|
'cash': 'Cash',
|
|
'card': 'Credit Card',
|
|
'credit': 'Credit',
|
|
'transfer': 'Bank Transfer'
|
|
};
|
|
container.innerHTML = this.payments.map((p, i) => `
|
|
<div class="payment-line">
|
|
<div>
|
|
<span class="method">${methodLabels[p.method] || p.method}</span>
|
|
<span class="ms-2 badge bg-secondary small">${p.amount.toFixed(3)}</span>
|
|
</div>
|
|
<button class="btn btn-sm btn-outline-danger border-0" onclick="cart.removePaymentLine(${i})">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
`).join('');
|
|
},
|
|
updateRemaining() {
|
|
const remaining = this.getRemaining();
|
|
const currentInput = parseFloat(document.getElementById('partialAmount').value) || 0;
|
|
const display = document.getElementById('paymentRemaining');
|
|
display.innerText = Math.max(0, remaining).toFixed(3);
|
|
|
|
// Calculate potential change if the user types an amount > remaining
|
|
const totalPaid = this.payments.reduce((sum, p) => sum + p.amount, 0);
|
|
const grandTotal = this.getGrandTotal();
|
|
const actualChange = Math.max(0, totalPaid - grandTotal);
|
|
const potentialChange = Math.max(0, currentInput - remaining);
|
|
const displayChange = Math.max(actualChange, potentialChange);
|
|
|
|
const changeDisplay = document.getElementById('changeDue');
|
|
if (changeDisplay) {
|
|
changeDisplay.innerText = displayChange.toFixed(3);
|
|
const cashSection = document.getElementById('cashPaymentSection');
|
|
if (displayChange > 0 || this.selectedPaymentMethod === 'cash' || this.payments.some(p => p.method === 'cash')) {
|
|
cashSection.style.display = 'block';
|
|
} else {
|
|
cashSection.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
if (remaining <= 0.0001 || currentInput >= remaining - 0.0001) {
|
|
display.classList.remove('text-danger');
|
|
display.classList.add('text-success');
|
|
document.getElementById('confirmPaymentBtn').disabled = false;
|
|
} else {
|
|
display.classList.remove('text-success');
|
|
display.classList.add('text-danger');
|
|
document.getElementById('confirmPaymentBtn').disabled = true;
|
|
}
|
|
},
|
|
async completeOrder() {
|
|
if (this.items.length === 0) {
|
|
Swal.fire('Error', 'Cart is empty', 'error');
|
|
return;
|
|
}
|
|
|
|
// If there's an amount in the input and payments are not enough, add it
|
|
const remainingBefore = this.getRemaining();
|
|
const currentInput = parseFloat(document.getElementById('partialAmount').value) || 0;
|
|
|
|
if (remainingBefore > 0.0001 && currentInput >= remainingBefore - 0.0001) {
|
|
this.payments.push({
|
|
method: this.selectedPaymentMethod,
|
|
amount: currentInput
|
|
});
|
|
} else if (this.payments.length === 0) {
|
|
const total = this.getGrandTotal();
|
|
this.payments.push({
|
|
method: this.selectedPaymentMethod,
|
|
amount: total
|
|
});
|
|
}
|
|
|
|
const remaining = this.getRemaining();
|
|
if (remaining > 0.001) {
|
|
Swal.fire('Error', 'Payment is incomplete', 'error');
|
|
return;
|
|
}
|
|
|
|
const customerId = document.getElementById('posCustomer').value;
|
|
if (this.payments.some(p => p.method === 'credit') && !customerId) {
|
|
Swal.fire('Error', 'Credit payment is only allowed for registered customers', 'error');
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('confirmPaymentBtn');
|
|
const originalText = btn.innerText;
|
|
btn.disabled = true;
|
|
btn.innerText = 'PROCESSING...';
|
|
|
|
const subtotal = this.items.reduce((sum, item) => sum + (parseFloat(item.price) * item.qty), 0);
|
|
const totalVat = this.items.reduce((sum, item) => {
|
|
const vatRate = (item.vatRate !== undefined && item.vatRate !== null) ? item.vatRate : 0;
|
|
return sum + ((parseFloat(item.price) * item.qty) * (vatRate / (100 + vatRate)));
|
|
}, 0);
|
|
let discountAmount = 0;
|
|
if (this.discount) {
|
|
discountAmount = this.discount.type === 'percentage' ? subtotal * (parseFloat(this.discount.value) / 100) : parseFloat(this.discount.value);
|
|
}
|
|
const redeemSwitch = document.getElementById('redeemLoyalty');
|
|
let loyaltyRedeemed = (redeemSwitch && redeemSwitch.checked) ? Math.min(subtotal - discountAmount, this.customerPoints / this.loyaltySettings.redeemPointsPerUnit) : 0;
|
|
|
|
const formData = new FormData();
|
|
formData.append('action', 'save_pos_transaction');
|
|
formData.append('customer_id', customerId);
|
|
formData.append('payments', JSON.stringify(this.payments));
|
|
formData.append('total_amount', subtotal);
|
|
formData.append('tax_amount', totalVat);
|
|
formData.append('discount_code_id', this.discount ? this.discount.id : '');
|
|
formData.append('discount_amount', discountAmount);
|
|
formData.append('loyalty_redeemed', loyaltyRedeemed);
|
|
formData.append('items', JSON.stringify(this.items.map(i => {
|
|
const vr = (i.vatRate !== undefined && i.vatRate !== null) ? i.vatRate : 0;
|
|
const va = (parseFloat(i.price) * i.qty) * (vr / (100 + vr));
|
|
return {id: i.id, qty: i.qty, price: i.price, vat_rate: vr, vat_amount: va};
|
|
})));
|
|
|
|
try {
|
|
const resp = await fetch('index.php', { method: 'POST', body: formData });
|
|
const text = await resp.text();
|
|
let result;
|
|
try {
|
|
result = JSON.parse(text);
|
|
} catch (e) {
|
|
console.error('Invalid JSON response:', text);
|
|
throw new Error('Server returned an invalid response');
|
|
}
|
|
|
|
if (result.success) {
|
|
const payModal = bootstrap.Modal.getInstance(document.getElementById('posPaymentModal'));
|
|
if (payModal) payModal.hide();
|
|
this.showReceipt(result.invoice_id, discountAmount, loyaltyRedeemed, result.transaction_no);
|
|
} else {
|
|
Swal.fire('Error', result.error, 'error');
|
|
btn.disabled = false;
|
|
btn.innerText = originalText;
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
Swal.fire('Error', err.message || 'Something went wrong', 'error');
|
|
btn.disabled = false;
|
|
btn.innerText = originalText;
|
|
}
|
|
},
|
|
showReceipt(invId, discountAmount, loyaltyRedeemed, transactionNo) {
|
|
const container = document.getElementById('posReceiptContent');
|
|
const customerSelect = document.getElementById('posCustomer');
|
|
const customerName = (customerSelect && customerSelect.selectedIndex >= 0 && customerSelect.value !== '') ? customerSelect.options[customerSelect.selectedIndex].text : '<?= $translations['ar']['walk_in_customer'] ?> / <?= $translations['en']['walk_in_customer'] ?>';
|
|
const paymentsHtml = this.payments.map(p => {
|
|
let m = p.method.toLowerCase();
|
|
let methodAr = m === 'cash' ? 'نقد' : (m === 'card' ? 'بطاقة ائتمان' : (m === 'credit' ? 'آجل' : (m === 'transfer' ? 'تحويل بنكي' : m)));
|
|
let methodEn = m === 'cash' ? 'Cash' : (m === 'card' ? 'Credit Card' : (m === 'credit' ? 'Credit' : (m === 'transfer' ? 'Bank Transfer' : m.charAt(0).toUpperCase() + m.slice(1))));
|
|
return `
|
|
<div class="d-flex justify-content-between small">
|
|
<span class="text-uppercase">${methodAr} / ${methodEn}</span>
|
|
<span><?= __('currency') ?> ${p.amount.toFixed(3)}</span>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
const date = new Date().toLocaleString();
|
|
|
|
let itemsHtml = this.items.map(item => {
|
|
const itemTotal = item.price * item.qty;
|
|
const vatRate = (item.vatRate !== undefined && item.vatRate !== null) ? item.vatRate : 0; // Default to 5 if not set
|
|
const vatAmount = itemTotal * (vatRate / (100 + vatRate));
|
|
const exclVat = itemTotal - vatAmount;
|
|
return `
|
|
<tr>
|
|
<td>
|
|
<div class="fw-bold">${item.nameAr || ''}</div>
|
|
<div>${item.nameEn}</div>
|
|
<small>${item.qty} x ${parseFloat(item.price).toFixed(3)}</small>
|
|
</td>
|
|
<td style="text-align: right; vertical-align: bottom;">${vatAmount.toFixed(2)}</td>
|
|
<td style="text-align: right; vertical-align: bottom;">${itemTotal.toFixed(3)}</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
|
|
const subtotal = this.items.reduce((sum, item) => sum + (item.price * item.qty), 0);
|
|
const totalVat = this.items.reduce((sum, item) => {
|
|
const vatRate = (item.vatRate !== undefined && item.vatRate !== null) ? item.vatRate : 0;
|
|
return sum + ((item.price * item.qty) * (vatRate / (100 + vatRate)));
|
|
}, 0);
|
|
const total = subtotal - discountAmount - loyaltyRedeemed;
|
|
const companyName = "<?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?>";
|
|
const outletName = "<?= htmlspecialchars($data['settings']['current_outlet_name'] ?? '') ?>";
|
|
const companyPhone = "<?= htmlspecialchars($data['settings']['company_phone'] ?? '') ?>";
|
|
const companyVat = "<?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?>";
|
|
const companyLogo = "<?= htmlspecialchars($data['settings']['company_logo'] ?? '') ?>";
|
|
|
|
container.innerHTML = `
|
|
<div class="thermal-receipt <?= $lang === 'ar' ? 'rtl' : '' ?>">
|
|
<div class="center">
|
|
${companyLogo ? `<img src="${companyLogo}" alt="Logo" style="max-height: 60px; width: auto; margin-bottom: 10px; display: block; margin-left: auto; margin-right: auto;">` : ''}
|
|
<h5 class="mb-0 fw-bold">${companyName}</h5>
|
|
${outletName ? `<div class="fw-bold text-uppercase">${outletName}</div>` : ''}
|
|
${companyPhone ? `<div>هاتف / Tel: ${companyPhone}</div>` : ''}
|
|
${companyVat ? `<div>الرقم الضريبي / VAT No: ${companyVat}</div>` : ''}
|
|
<div class="separator"></div>
|
|
<h6 class="fw-bold text-uppercase">فاتورة ضريبية / TAX INVOICE</h6>
|
|
<div>رقم الفاتورة / Invoice No: ${transactionNo || 'POS-'+invId}</div>
|
|
<div>التاريخ / Date: ${date}</div>
|
|
<div class="separator"></div>
|
|
</div>
|
|
<div>
|
|
<strong>العميل / Customer:</strong> ${customerName}
|
|
</div>
|
|
<div class="mt-1">
|
|
<strong>المدفوعات / Payments:</strong>
|
|
${paymentsHtml}
|
|
</div>
|
|
<div class="separator"></div>
|
|
<table class="table-borderless">
|
|
<thead>
|
|
<tr>
|
|
<th>البند / Item</th>
|
|
<th style="text-align: right;">ضريبة / VAT</th>
|
|
<th style="text-align: right;">الإجمالي / Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${itemsHtml}
|
|
</tbody>
|
|
</table>
|
|
<div class="separator"></div>
|
|
<div class="d-flex justify-content-between">
|
|
<span>المجموع الفرعي (غير شامل الضريبة) / Subtotal (Excl. VAT)</span>
|
|
<span><?= __('currency') ?> ${(subtotal - totalVat).toFixed(3)}</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between">
|
|
<span>الضريبة / VAT</span>
|
|
<span><?= __('currency') ?> ${totalVat.toFixed(2)}</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between fw-bold">
|
|
<span>المجموع شامل الضريبة / Total (Incl. VAT)</span>
|
|
<span><?= __('currency') ?> ${subtotal.toFixed(3)}</span>
|
|
</div>
|
|
${discountAmount > 0 ? `<div class="d-flex justify-content-between text-danger"><span>خصم / Discount</span><span>- <?= __('currency') ?> ${parseFloat(discountAmount).toFixed(3)}</span></div>` : ''}
|
|
${loyaltyRedeemed > 0 ? `<div class="d-flex justify-content-between text-success"><span>الولاء / Loyalty</span><span>- <?= __('currency') ?> ${parseFloat(loyaltyRedeemed).toFixed(3)}</span></div>` : ''}
|
|
<div class="separator"></div>
|
|
<div class="d-flex justify-content-between total-row">
|
|
<span>الإجمالي / Total</span>
|
|
<span><?= __('currency') ?> ${total.toFixed(3)}</span>
|
|
</div>
|
|
<div class="separator"></div>
|
|
<div class="center small">
|
|
شكراً لتعاملكم معنا! / Thank you for your business!<br>
|
|
يرجى الاحتفاظ بالإيصال. / Please keep the receipt.
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('posReceiptModal'));
|
|
modal.show();
|
|
|
|
this.clear();
|
|
|
|
document.getElementById('posReceiptModal').addEventListener('hidden.bs.modal', function () {
|
|
location.reload();
|
|
}, { once: true });
|
|
},
|
|
|
|
async syncCustomer(val) {
|
|
document.getElementById('posCustomer').value = val;
|
|
const customerSelect = document.getElementById('posCustomer');
|
|
const customerName = customerSelect.options[customerSelect.selectedIndex].text;
|
|
document.getElementById('paymentCustomerName').innerText = customerName;
|
|
await this.onCustomerChange();
|
|
}
|
|
};
|
|
|
|
|
|
// Event Delegation for clicking on cards
|
|
document.getElementById('productGrid').addEventListener('click', (e) => {
|
|
const card = e.target.closest('.product-card');
|
|
if (card) {
|
|
addToCartFromCard(card);
|
|
}
|
|
});
|
|
|
|
function addToCartFromCard(card) {
|
|
const product = {
|
|
id: parseInt(card.dataset.id),
|
|
nameEn: card.dataset.nameEn,
|
|
nameAr: card.dataset.nameAr,
|
|
price: parseFloat(card.dataset.price),
|
|
sku: card.dataset.sku,
|
|
stock_quantity: parseFloat(card.dataset.stockQuantity),
|
|
vatRate: parseFloat(card.dataset.vatRate) || 0
|
|
};
|
|
cart.add(product);
|
|
}
|
|
|
|
let searchTimeout;
|
|
document.getElementById('productSearch').addEventListener('input', (e) => {
|
|
const q = e.target.value.trim();
|
|
clearTimeout(searchTimeout);
|
|
|
|
searchTimeout = setTimeout(() => {
|
|
const grid = document.getElementById('productGrid');
|
|
grid.style.opacity = '0.5';
|
|
|
|
fetch('index.php?action=pos_search_items&q=' + encodeURIComponent(q))
|
|
.then(response => response.text())
|
|
.then(html => {
|
|
grid.innerHTML = html;
|
|
grid.style.opacity = '1';
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
grid.style.opacity = '1';
|
|
});
|
|
}, 300);
|
|
});
|
|
|
|
document.getElementById('barcodeInput').addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
const barcode = e.target.value.trim();
|
|
if (!barcode) return;
|
|
|
|
// Try finding in DOM first (current view)
|
|
const card = Array.from(document.querySelectorAll('.product-card')).find(c => c.dataset.sku === barcode);
|
|
|
|
if (card) {
|
|
addToCartFromCard(card);
|
|
e.target.value = '';
|
|
} else {
|
|
// Not found in current view, check server
|
|
fetch('index.php?action=pos_get_item_by_sku&sku=' + encodeURIComponent(barcode))
|
|
.then(response => response.json())
|
|
.then(product => {
|
|
if (product && product.error) {
|
|
Swal.fire({
|
|
toast: true, position: 'top-end', icon: 'error',
|
|
title: product.error, showConfirmButton: false, timer: 1800
|
|
});
|
|
e.target.select();
|
|
return;
|
|
}
|
|
|
|
if (product) {
|
|
cart.add(product);
|
|
e.target.value = '';
|
|
Swal.fire({
|
|
toast: true, position: 'top-end', icon: 'success',
|
|
title: (document.documentElement.lang === 'ar' ? 'تم إضافة: ' : 'Added: ') + (document.documentElement.lang === 'ar' ? (product.nameAr || product.nameEn) : (product.nameEn || product.nameAr)),
|
|
showConfirmButton: false, timer: 1000
|
|
});
|
|
} else {
|
|
Swal.fire({
|
|
toast: true, position: 'top-end', icon: 'error',
|
|
title: 'Product not found', showConfirmButton: false, timer: 1500
|
|
});
|
|
e.target.select();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
// Keep barcode input focused
|
|
document.addEventListener('click', () => {
|
|
if (document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'SELECT' && document.activeElement.tagName !== 'TEXTAREA') {
|
|
const bc = document.getElementById('barcodeInput');
|
|
if (bc) bc.focus();
|
|
}
|
|
});
|
|
|
|
$(document).ready(function() {
|
|
$('#posCustomer').select2({
|
|
width: '100%',
|
|
placeholder: 'Select Customer'
|
|
});
|
|
$('#paymentCreditCustomer').select2({
|
|
width: '100%',
|
|
placeholder: 'Select Customer',
|
|
dropdownParent: $('#posPaymentModal')
|
|
});
|
|
|
|
// Initial broadcast to sync customer display (theme & empty state)
|
|
if (typeof cart !== 'undefined' && cart.broadcast) {
|
|
cart.broadcast();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<?php if (!$active_session): ?>
|
|
<!-- Open Register Modal -->
|
|
<div class="modal fade" id="openRegisterModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Open Cash Register</h5>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<input type="hidden" name="open_register" value="1">
|
|
<div class="mb-3">
|
|
<label class="form-label">Select Register</label>
|
|
<select name="register_id" class="form-select" required>
|
|
<?php if (isset($registers)): foreach ($registers as $r): ?>
|
|
<option value="<?= $r['id'] ?>"><?= htmlspecialchars($r['name']) ?></option>
|
|
<?php endforeach; endif; ?>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Opening Balance</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text">OMR</span>
|
|
<input type="number" step="0.001" name="opening_balance" class="form-control" required placeholder="0.000">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<a href="index.php?page=dashboard" class="btn btn-secondary">Cancel & Go to Dashboard</a>
|
|
<button type="submit" class="btn btn-primary">Open Session</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if ($active_session): ?>
|
|
<!-- Close Register Modal -->
|
|
<div class="modal fade" id="closeRegisterModal" tabindex="-1">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Close Cash Register</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<input type="hidden" name="close_register" value="1">
|
|
<input type="hidden" name="redirect_to" value="dashboard">
|
|
<input type="hidden" name="session_id" value="<?= $_SESSION['register_session_id'] ?? '' ?>">
|
|
<div class="mb-3">
|
|
<label class="form-label">Cash in Hand (Counted)</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text">OMR</span>
|
|
<input type="number" step="0.001" name="cash_in_hand" class="form-control" required placeholder="0.000">
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Notes</label>
|
|
<textarea name="notes" class="form-control" rows="3" placeholder="Any discrepancies or notes..."></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" class="btn btn-danger">Close Session</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
<?php if (!$active_session): ?>
|
|
var openModal = new bootstrap.Modal(document.getElementById('openRegisterModal'));
|
|
openModal.show();
|
|
<?php endif; ?>
|
|
});
|
|
</script>
|
|
|
|
<?php elseif ($page === 'quotations'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Quotations" data-ar="عروض الأسعار">Quotations</h5>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addQuotationModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Create New Quotation" data-ar="إنشاء عرض سعر جديد">Create New Quotation</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Filters Section -->
|
|
<div class="bg-light p-3 rounded mb-4">
|
|
<form method="GET" class="form-grid-3">
|
|
<input type="hidden" name="page" value="quotations">
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="Search" data-ar="بحث">Search</label>
|
|
<input type="text" name="search" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="Quot # or Name...">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="Customer" data-ar="العميل">Customer</label>
|
|
<select name="customer_id" class="form-select form-select-sm">
|
|
<option value="" data-en="All" data-ar="الكل">All</option>
|
|
<?php foreach ($data['customers_list'] as $c): ?>
|
|
<option value="<?= $c['id'] ?>" <?= (($_GET['customer_id'] ?? '') == $c['id']) ? 'selected' : '' ?>><?= htmlspecialchars($c['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small fw-bold" data-en="Start Date" data-ar="من تاريخ">Start Date</label>
|
|
<input type="date" name="start_date" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['start_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small fw-bold" data-en="End Date" data-ar="إلى تاريخ">End Date</label>
|
|
<input type="date" name="end_date" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['end_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-2 d-flex align-items-end gap-1">
|
|
<button type="submit" class="btn btn-primary btn-sm flex-grow-1">
|
|
<i class="bi bi-filter"></i> <span data-en="Filter" data-ar="تصفية">Filter</span>
|
|
</button>
|
|
<div class="dropdown d-inline-block">
|
|
<button class="btn btn-outline-success btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-download"></i> <span data-en="Export" data-ar="تصدير">Export</span>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="index.php?<?= http_build_query(array_merge($_GET, ['page' => 'export', 'type' => 'quotations', 'format' => 'csv'])) ?>"><i class="bi bi-filetype-csv me-2"></i> CSV</a></li>
|
|
<li><a class="dropdown-item" href="index.php?<?= http_build_query(array_merge($_GET, ['page' => 'export', 'type' => 'quotations', 'format' => 'excel'])) ?>"><i class="bi bi-file-earmark-excel me-2"></i> Excel</a></li>
|
|
</ul>
|
|
</div>
|
|
<a href="index.php?page=quotations" class="btn btn-outline-secondary btn-sm flex-grow-1">
|
|
<i class="bi bi-x-circle"></i> <span data-en="Clear" data-ar="مسح">Clear</span>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="col-md-auto ms-auto d-flex align-items-end mt-2 mt-md-0">
|
|
<div class="input-group input-group-sm w-auto">
|
|
<span class="input-group-text" data-en="Limit" data-ar="الحد">Limit</span>
|
|
<select name="limit" class="form-select" onchange="this.form.submit()">
|
|
<option value="20" <?= (($_GET['limit'] ?? 20) == 20) ? 'selected' : '' ?>>20</option>
|
|
<option value="40" <?= (($_GET['limit'] ?? 20) == 40) ? 'selected' : '' ?>>40</option>
|
|
<option value="60" <?= (($_GET['limit'] ?? 20) == 60) ? 'selected' : '' ?>>60</option>
|
|
<option value="100" <?= (($_GET['limit'] ?? 20) == 100) ? 'selected' : '' ?>>100</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Quotation #" data-ar="رقم العرض">Quotation #</th>
|
|
<th data-en="Date" data-ar="التاريخ">Date</th>
|
|
<th data-en="Valid Until" data-ar="صالح حتى">Valid Until</th>
|
|
<th data-en="Customer" data-ar="العميل">Customer</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th data-en="Total" data-ar="الإجمالي" class="text-end">Total</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
foreach ($data['quotations'] as $q):
|
|
$items = db()->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);
|
|
?>
|
|
<tr>
|
|
<td>QUO-<?= str_pad((string)$q['id'], 5, '0', STR_PAD_LEFT) ?></td>
|
|
<td><?= $q['quotation_date'] ?></td>
|
|
<td><?= $q['valid_until'] ?: '---' ?></td>
|
|
<td><?= htmlspecialchars($q['customer_name'] ?? '---') ?></td>
|
|
<td>
|
|
<?php
|
|
$statusClass = 'bg-secondary';
|
|
if ($q['status'] === 'converted') $statusClass = 'bg-success';
|
|
elseif ($q['status'] === 'pending') $statusClass = 'bg-warning text-dark';
|
|
elseif ($q['status'] === 'expired' || $q['status'] === 'cancelled') $statusClass = 'bg-danger';
|
|
?>
|
|
<span class="badge text-uppercase <?= $statusClass ?>"><?= htmlspecialchars($q['status']) ?></span>
|
|
</td>
|
|
<td class="text-end fw-bold">OMR <?= number_format((float)$q['total_with_vat'], 3) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-info view-quotation-btn" data-json="<?= htmlspecialchars(json_encode($q)) ?>" title="View"><i class="bi bi-eye"></i></button>
|
|
<button class="btn btn-outline-secondary" onclick="window.viewAndPrintQuotation(<?= htmlspecialchars(json_encode($q)) ?>)" title="Print"><i class="bi bi-printer"></i></button>
|
|
<button class="btn btn-outline-primary edit-quotation-btn" data-json="<?= htmlspecialchars(json_encode($q)) ?>" data-bs-toggle="modal" data-bs-target="#editQuotationModal" title="Edit"><i class="bi bi-pencil-square"></i></button>
|
|
<?php if ($q['status'] === 'pending'): ?>
|
|
<button class="btn btn-outline-success convert-quotation-btn" data-id="<?= $q['id'] ?>" title="Convert to Invoice"><i class="bi bi-receipt"></i></button>
|
|
<?php endif; ?>
|
|
<button class="btn btn-outline-danger" onclick="if(confirm('Delete this quotation?')) { const f = document.createElement('form'); f.method='POST'; f.innerHTML='<input type=hidden name=delete_quotation><input type=hidden name=id value=<?= $q['id'] ?>>'; document.body.appendChild(f); f.submit(); }" title="Delete"><i class="bi bi-trash"></i></button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($data['quotations'])): ?>
|
|
<tr><td colspan="7" class="text-center py-4 text-muted" data-en="No quotations found" data-ar="لا توجد عروض أسعار">No quotations found</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<?php elseif ($page === 'lpos'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Local Purchase Orders (LPO)" data-ar="أوامر الشراء المحلية (LPO)">Local Purchase Orders (LPO)</h5>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addLpoModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Create New LPO" data-ar="إنشاء أمر شراء جديد">Create New LPO</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Filters Section -->
|
|
<div class="bg-light p-3 rounded mb-4">
|
|
<form method="GET" class="form-grid-3">
|
|
<input type="hidden" name="page" value="lpos">
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="Search" data-ar="بحث">Search</label>
|
|
<input type="text" name="search" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="LPO # or Name...">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="Supplier" data-ar="المورد">Supplier</label>
|
|
<select name="supplier_id" class="form-select form-select-sm">
|
|
<option value="" data-en="All" data-ar="الكل">All</option>
|
|
<?php foreach ($data['suppliers'] as $s): ?>
|
|
<option value="<?= $s['id'] ?>" <?= (($_GET['supplier_id'] ?? '') == $s['id']) ? 'selected' : '' ?>><?= htmlspecialchars($s['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small fw-bold" data-en="Start Date" data-ar="من تاريخ">Start Date</label>
|
|
<input type="date" name="start_date" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['start_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small fw-bold" data-en="End Date" data-ar="إلى تاريخ">End Date</label>
|
|
<input type="date" name="end_date" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['end_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-2 d-flex align-items-end gap-1">
|
|
<button type="submit" class="btn btn-primary btn-sm flex-grow-1">
|
|
<i class="bi bi-filter"></i> <span data-en="Filter" data-ar="تصفية">Filter</span>
|
|
</button>
|
|
<div class="dropdown d-inline-block">
|
|
<button class="btn btn-outline-success btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-download"></i> <span data-en="Export" data-ar="تصدير">Export</span>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="index.php?<?= http_build_query(array_merge($_GET, ['page' => 'export', 'type' => 'lpos', 'format' => 'csv'])) ?>"><i class="bi bi-filetype-csv me-2"></i> CSV</a></li>
|
|
<li><a class="dropdown-item" href="index.php?<?= http_build_query(array_merge($_GET, ['page' => 'export', 'type' => 'lpos', 'format' => 'excel'])) ?>"><i class="bi bi-file-earmark-excel me-2"></i> Excel</a></li>
|
|
</ul>
|
|
</div>
|
|
<a href="index.php?page=lpos" class="btn btn-outline-secondary btn-sm flex-grow-1">
|
|
<i class="bi bi-x-circle"></i> <span data-en="Clear" data-ar="مسح">Clear</span>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="col-md-auto ms-auto d-flex align-items-end mt-2 mt-md-0">
|
|
<div class="input-group input-group-sm w-auto">
|
|
<span class="input-group-text" data-en="Limit" data-ar="الحد">Limit</span>
|
|
<select name="limit" class="form-select" onchange="this.form.submit()">
|
|
<option value="20" <?= (($_GET['limit'] ?? 20) == 20) ? 'selected' : '' ?>>20</option>
|
|
<option value="40" <?= (($_GET['limit'] ?? 20) == 40) ? 'selected' : '' ?>>40</option>
|
|
<option value="60" <?= (($_GET['limit'] ?? 20) == 60) ? 'selected' : '' ?>>60</option>
|
|
<option value="100" <?= (($_GET['limit'] ?? 20) == 100) ? 'selected' : '' ?>>100</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="LPO #" data-ar="رقم الأمر">LPO #</th>
|
|
<th data-en="Date" data-ar="التاريخ">Date</th>
|
|
<th data-en="Delivery Date" data-ar="تاريخ التسليم">Delivery Date</th>
|
|
<th data-en="Supplier" data-ar="المورد">Supplier</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th data-en="Total" data-ar="الإجمالي" class="text-end">Total</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
foreach ($data['lpos'] as $q):
|
|
$items = db()->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);
|
|
?>
|
|
<tr>
|
|
<td>LPO-<?= str_pad((string)$q['id'], 5, '0', STR_PAD_LEFT) ?></td>
|
|
<td><?= $q['lpo_date'] ?></td>
|
|
<td><?= $q['delivery_date'] ?: '---' ?></td>
|
|
<td><?= htmlspecialchars($q['supplier_name'] ?? '---') ?></td>
|
|
<td>
|
|
<?php
|
|
$statusClass = 'bg-secondary';
|
|
if ($q['status'] === 'converted') $statusClass = 'bg-success';
|
|
elseif ($q['status'] === 'pending') $statusClass = 'bg-warning text-dark';
|
|
elseif ($q['status'] === 'cancelled') $statusClass = 'bg-danger';
|
|
?>
|
|
<span class="badge text-uppercase <?= $statusClass ?>"><?= htmlspecialchars($q['status']) ?></span>
|
|
</td>
|
|
<td class="text-end fw-bold">OMR <?= number_format((float)$q['total_with_vat'], 3) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-info view-lpo-btn" data-json="<?= htmlspecialchars(json_encode($q)) ?>" title="View"><i class="bi bi-eye"></i></button>
|
|
<?php if ($q['status'] !== 'converted'): ?>
|
|
<button class="btn btn-outline-success" onclick="if(confirm('Convert this LPO to Purchase Invoice?')) { const f = document.createElement('form'); f.method='POST'; f.innerHTML='<input type=hidden name=convert_lpo_to_purchase><input type=hidden name=lpo_id value=<?= $q['id'] ?>>'; document.body.appendChild(f); f.submit(); }" title="Convert to Purchase"><i class="bi bi-arrow-repeat"></i></button>
|
|
<button class="btn btn-outline-primary edit-lpo-btn" data-json="<?= htmlspecialchars(json_encode($q)) ?>" data-bs-toggle="modal" data-bs-target="#editLpoModal" title="Edit"><i class="bi bi-pencil-square"></i></button>
|
|
<?php endif; ?>
|
|
<button class="btn btn-outline-danger" onclick="if(confirm('Delete this LPO?')) { const f = document.createElement('form'); f.method='POST'; f.innerHTML='<input type=hidden name=delete_lpo><input type=hidden name=id value=<?= $q['id'] ?>>'; document.body.appendChild(f); f.submit(); }" title="Delete"><i class="bi bi-trash"></i></button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($data['lpos'])): ?>
|
|
<tr><td colspan="7" class="text-center py-4 text-muted" data-en="No LPOs found" data-ar="لا توجد أوامر شراء">No LPOs found</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
|
|
</div>
|
|
|
|
<?php elseif ($page === 'sales' || $page === 'purchases'): ?>
|
|
<?php require 'pages/sales_purchases_view.php'; ?>
|
|
|
|
<?php elseif ($page === 'customer_statement' || $page === 'supplier_statement'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
|
|
<?php
|
|
$entity_suffix = isset($data['selected_entity']['name']) ? ' - ' . htmlspecialchars($data['selected_entity']['name']) : '';
|
|
$page_title_en = $currTitle['en'] . $entity_suffix;
|
|
$page_title_ar = $currTitle['ar'] . $entity_suffix;
|
|
?>
|
|
<h5 class="m-0" data-en="<?= $page_title_en ?>" data-ar="<?= $page_title_ar ?>"><?= $lang === 'ar' ? $page_title_ar : $page_title_en ?></h5>
|
|
<button class="btn btn-outline-secondary d-print-none" onclick="window.print()">
|
|
<i class="bi bi-printer"></i> <span data-en="Print" data-ar="طباعة"><?= $lang === 'ar' ? 'طباعة' : 'Print' ?></span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="bg-light p-3 rounded mb-4 d-print-none">
|
|
<form method="GET" class="row g-3 align-items-end">
|
|
<input type="hidden" name="page" value="<?= $page ?>">
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-bold" data-en="Select <?= $page === 'customer_statement' ? 'Customer' : 'Supplier' ?>" data-ar="اختر <?= $page === 'customer_statement' ? 'العميل' : 'المورد' ?>"><?= $lang === 'ar' ? ($page === 'customer_statement' ? 'اختر العميل' : 'اختر المورد') : 'Select ' . ($page === 'customer_statement' ? 'Customer' : 'Supplier') ?></label>
|
|
<select name="entity_id" class="form-select select2" required>
|
|
<option value="">---</option>
|
|
<?php foreach ($data['entities'] as $e): ?>
|
|
<option value="<?= $e['id'] ?>" <?= (($_GET['entity_id'] ?? '') == $e['id']) ? 'selected' : '' ?>><?= htmlspecialchars($e['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="From Date" data-ar="من تاريخ"><?= $lang === 'ar' ? 'من تاريخ' : 'From Date' ?></label>
|
|
<input type="date" name="start_date" class="form-control" value="<?= htmlspecialchars($_GET['start_date'] ?? date('Y-m-01')) ?>">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="To Date" data-ar="إلى تاريخ"><?= $lang === 'ar' ? 'إلى تاريخ' : 'To Date' ?></label>
|
|
<input type="date" name="end_date" class="form-control" value="<?= htmlspecialchars($_GET['end_date'] ?? date('Y-m-d')) ?>">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button type="submit" class="btn btn-primary w-100">
|
|
<i class="bi bi-search"></i> <span data-en="View Report" data-ar="عرض التقرير"><?= $lang === 'ar' ? 'عرض التقرير' : 'View Report' ?></span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="col-md-auto ms-auto d-flex align-items-end mt-2 mt-md-0">
|
|
<div class="input-group input-group-sm w-auto">
|
|
<span class="input-group-text" data-en="Limit" data-ar="الحد">Limit</span>
|
|
<select name="limit" class="form-select" onchange="this.form.submit()">
|
|
<option value="20" <?= (($_GET['limit'] ?? 20) == 20) ? 'selected' : '' ?>>20</option>
|
|
<option value="40" <?= (($_GET['limit'] ?? 20) == 40) ? 'selected' : '' ?>>40</option>
|
|
<option value="60" <?= (($_GET['limit'] ?? 20) == 60) ? 'selected' : '' ?>>60</option>
|
|
<option value="100" <?= (($_GET['limit'] ?? 20) == 100) ? 'selected' : '' ?>>100</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<?php if (isset($data['transactions'])): ?>
|
|
<div id="statement-print">
|
|
<div class="row mb-4">
|
|
<div class="col-6">
|
|
<h3 class="mb-0"><?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?></h3>
|
|
<p class="text-muted small"><?= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?></p>
|
|
</div>
|
|
<div class="col-6 text-end">
|
|
<h2 class="text-uppercase text-muted" data-en="Statement of Account - <?= htmlspecialchars($data['selected_entity']['name']) ?>" data-ar="كشف حساب - <?= htmlspecialchars($data['selected_entity']['name']) ?>"><?= $lang === 'ar' ? 'كشف حساب' : 'Statement of Account' ?> - <?= htmlspecialchars($data['selected_entity']['name']) ?></h2>
|
|
<p class="mb-0"><strong><?= htmlspecialchars($data['selected_entity']['name']) ?></strong></p>
|
|
<p class="text-muted small"><?= htmlspecialchars($data['selected_entity']['email']) ?> | <?= htmlspecialchars($data['selected_entity']['phone']) ?><br><span data-en="Period" data-ar="الفترة"><?= $lang === 'ar' ? 'الفترة' : 'Period' ?></span>: <?= $_GET['start_date'] ?> <span data-en="to" data-ar="إلى"><?= $lang === 'ar' ? 'إلى' : 'to' ?></span> <?= $_GET['end_date'] ?></p>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered table-sm">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th data-en="Date" data-ar="التاريخ"><?= $lang === 'ar' ? 'التاريخ' : 'Date' ?></th>
|
|
<th data-en="Reference" data-ar="المرجع"><?= $lang === 'ar' ? 'المرجع' : 'Reference' ?></th>
|
|
<th data-en="Description" data-ar="الوصف"><?= $lang === 'ar' ? 'الوصف' : 'Description' ?></th>
|
|
<th data-en="Debit" data-ar="مدين" class="text-end"><?= $lang === 'ar' ? 'مدين' : 'Debit' ?></th>
|
|
<th data-en="Credit" data-ar="دائن" class="text-end"><?= $lang === 'ar' ? 'دائن' : 'Credit' ?></th>
|
|
<th data-en="Balance" data-ar="الرصيد" class="text-end"><?= $lang === 'ar' ? 'الرصيد' : 'Balance' ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
$running_balance = 0;
|
|
foreach ($data['transactions'] as $t):
|
|
$debit = 0; $credit = 0;
|
|
if ($t['trans_type'] === 'invoice') {
|
|
if ($page === 'customer_statement') $debit = (float)$t['amount']; else $credit = (float)$t['amount'];
|
|
} else {
|
|
if ($page === 'customer_statement') $credit = (float)$t['amount']; else $debit = (float)$t['amount'];
|
|
}
|
|
$running_balance += ($debit - $credit);
|
|
?>
|
|
<tr>
|
|
<td><?= $t['trans_date'] ?></td>
|
|
<td><?= $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) ?></td>
|
|
<td>
|
|
<?php if ($t['trans_type'] === 'invoice'): ?>
|
|
<span data-en="Tax Invoice" data-ar="فاتورة ضريبية"><?= $lang === 'ar' ? 'فاتورة ضريبية' : 'Tax Invoice' ?></span>
|
|
<?php else: ?>
|
|
<span data-en="Payment" data-ar="دفع"><?= $lang === 'ar' ? 'دفع' : 'Payment' ?></span> - <span data-en="<?= $t['payment_method'] ?>" data-ar="<?= $t['payment_method'] === 'cash' ? 'نقد' : ($t['payment_method'] === 'card' ? 'بطاقة ائتمان' : 'آجل') ?>"><?= $lang === 'ar' ? ($t['payment_method'] === 'cash' ? 'نقد' : ($t['payment_method'] === 'card' ? 'بطاقة ائتمان' : 'آجل')) : $t['payment_method'] ?></span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td class="text-end"><?= number_format($debit, 3) ?></td>
|
|
<td class="text-end"><?= number_format($credit, 3) ?></td>
|
|
<td class="text-end"><?= number_format($running_balance, 3) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
<tfoot class="bg-light fw-bold">
|
|
<tr>
|
|
<td colspan="5" class="text-end" data-en="Closing Balance" data-ar="رصيد الإقفال"><?= $lang === 'ar' ? 'رصيد الإقفال' : 'Closing Balance' ?></td>
|
|
<td class="text-end" data-en="OMR <?= number_format($running_balance, 3) ?>" data-ar="<?= number_format($running_balance, 3) ?> ر.ع."><?= $lang === 'ar' ? number_format($running_balance, 3) . ' ر.ع.' : 'OMR ' . number_format($running_balance, 3) ?></td>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Print Footer / Signatures -->
|
|
<div class="print-only mt-5">
|
|
<div class="row text-center mt-5">
|
|
<div class="col-4">
|
|
<div class="border-top pt-2" data-en="Prepared By" data-ar="أعد بواسطة">Prepared By</div>
|
|
</div>
|
|
<div class="col-4">
|
|
<div class="border-top pt-2" data-en="Checked By" data-ar="روجع بواسطة">Checked By</div>
|
|
</div>
|
|
<div class="col-4">
|
|
<div class="border-top pt-2" data-en="Authorized Signature" data-ar="التوقيع المعتمد">Authorized Signature</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 text-center text-muted small">
|
|
<span data-en="Printed on" data-ar="طبع في">Printed on</span> <?= date('Y-m-d H:i:s') ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="text-center py-5 text-muted"><p data-en="Please select an entity and date range to generate the statement." data-ar="يرجى اختيار جهة ونطاق تاريخي لتوليد كشف الحساب.">Please select an entity and date range to generate the statement.</p></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'cashflow_report'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
|
|
<h5 class="m-0" data-en="Cashflow Statement" data-ar="قائمة التدفقات النقدية">Cashflow Statement</h5>
|
|
<button class="btn btn-outline-secondary d-print-none" onclick="window.print()">
|
|
<i class="bi bi-printer"></i> <span data-en="Print" data-ar="طباعة">Print</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="bg-light p-3 rounded mb-4 d-print-none">
|
|
<form method="GET" class="row g-3 align-items-end">
|
|
<input type="hidden" name="page" value="<?= $page ?>">
|
|
<div class="col-md-5">
|
|
<label class="form-label small fw-bold" data-en="From Date" data-ar="من تاريخ">From Date</label>
|
|
<input type="date" name="start_date" class="form-control" value="<?= htmlspecialchars($_GET['start_date'] ?? date('Y-m-01')) ?>">
|
|
</div>
|
|
<div class="col-md-5">
|
|
<label class="form-label small fw-bold" data-en="To Date" data-ar="إلى تاريخ">To Date</label>
|
|
<input type="date" name="end_date" class="form-control" value="<?= htmlspecialchars($_GET['end_date'] ?? date('Y-m-d')) ?>">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<button type="submit" class="btn btn-primary w-100">
|
|
<i class="bi bi-search"></i> <span data-en="Generate" data-ar="توليد">Generate</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="col-md-auto ms-auto d-flex align-items-end mt-2 mt-md-0">
|
|
<div class="input-group input-group-sm w-auto">
|
|
<span class="input-group-text" data-en="Limit" data-ar="الحد">Limit</span>
|
|
<select name="limit" class="form-select" onchange="this.form.submit()">
|
|
<option value="20" <?= (($_GET['limit'] ?? 20) == 20) ? 'selected' : '' ?>>20</option>
|
|
<option value="40" <?= (($_GET['limit'] ?? 20) == 40) ? 'selected' : '' ?>>40</option>
|
|
<option value="60" <?= (($_GET['limit'] ?? 20) == 60) ? 'selected' : '' ?>>60</option>
|
|
<option value="100" <?= (($_GET['limit'] ?? 20) == 100) ? 'selected' : '' ?>>100</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div id="cashflow-print">
|
|
<div class="row mb-4">
|
|
<div class="col-6">
|
|
<h3 class="mb-0"><?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?></h3>
|
|
<p class="text-muted small"><?= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?></p>
|
|
</div>
|
|
<div class="col-6 text-end">
|
|
<h2 class="text-uppercase text-muted" data-en="Cashflow Statement" data-ar="قائمة التدفقات النقدية">Cashflow Statement</h2>
|
|
<p class="text-muted small">Period: <?= htmlspecialchars($_GET['start_date'] ?? date('Y-m-01')) ?> to <?= htmlspecialchars($_GET['end_date'] ?? date('Y-m-d')) ?></p>
|
|
</div>
|
|
</div>
|
|
|
|
<table class="table table-bordered">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th data-en="Description" data-ar="الوصف">Description</th>
|
|
<th data-en="Amount (OMR)" data-ar="المبلغ (ريال عماني)" class="text-end">Amount (OMR)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr class="fw-bold table-light">
|
|
<td data-en="Opening Cash Balance" data-ar="رصيد النقدية الافتتاحي">Opening Cash Balance</td>
|
|
<td class="text-end"><?= number_format($data['opening_balance'], 3) ?></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td colspan="2" class="fw-bold bg-light" data-en="Operating Activities" data-ar="الأنشطة التشغيلية">Operating Activities</td>
|
|
</tr>
|
|
<?php
|
|
$op_inflow = 0; $op_outflow = 0;
|
|
$inv_inflow = 0; $inv_outflow = 0;
|
|
$fin_inflow = 0; $fin_outflow = 0;
|
|
|
|
foreach ($data['cash_transactions'] as $t) {
|
|
$amt = (float)$t['inflow'] - (float)$t['outflow'];
|
|
// Very simple categorization based on account type
|
|
if ($t['other_type'] === 'revenue' || $t['other_type'] === 'expense' || in_array($t['other_account'], ['Accounts Receivable', 'Accounts Payable', 'VAT Input', 'VAT Payable'])) {
|
|
if ($amt > 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);
|
|
}
|
|
}
|
|
?>
|
|
<tr>
|
|
<td class="ps-4" data-en="Cash Received from Customers & Others" data-ar="المقبوضات النقدية من العملاء وغيرهم">Cash Received from Customers & Others</td>
|
|
<td class="text-end text-success"><?= number_format($op_inflow, 3) ?></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-4" data-en="Cash Paid to Suppliers & Expenses" data-ar="المدفوعات النقدية للموردين والمصروفات">Cash Paid to Suppliers & Expenses</td>
|
|
<td class="text-end text-danger">(<?= number_format($op_outflow, 3) ?>)</td>
|
|
</tr>
|
|
<tr class="fw-bold">
|
|
<td data-en="Net Cash from Operating Activities" data-ar="صافي النقد من الأنشطة التشغيلية">Net Cash from Operating Activities</td>
|
|
<td class="text-end border-top"><?= number_format($op_inflow - $op_outflow, 3) ?></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td colspan="2" class="fw-bold bg-light" data-en="Investing Activities" data-ar="الأنشطة الاستثمارية">Investing Activities</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-4" data-en="Net Cash from Investing Activities" data-ar="صافي النقد من الأنشطة الاستثمارية">Net Cash from Investing Activities</td>
|
|
<td class="text-end"><?= number_format($inv_inflow - $inv_outflow, 3) ?></td>
|
|
</tr>
|
|
|
|
<tr>
|
|
<td colspan="2" class="fw-bold bg-light" data-en="Financing Activities" data-ar="الأنشطة التمويلية">Financing Activities</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="ps-4" data-en="Net Cash from Financing Activities" data-ar="صافي النقد من الأنشطة التمويلية">Net Cash from Financing Activities</td>
|
|
<td class="text-end"><?= number_format($fin_inflow - $fin_outflow, 3) ?></td>
|
|
</tr>
|
|
|
|
<tr class="fw-bold table-primary">
|
|
<?php $net_change = ($op_inflow - $op_outflow) + ($inv_inflow - $inv_outflow) + ($fin_inflow - $fin_outflow); ?>
|
|
<td data-en="Net Change in Cash" data-ar="صافي التغير في النقدية">Net Change in Cash</td>
|
|
<td class="text-end"><?= number_format($net_change, 3) ?></td>
|
|
</tr>
|
|
<tr class="fw-bold table-success">
|
|
<td data-en="Closing Cash Balance" data-ar="رصيد النقدية الختامي">Closing Cash Balance</td>
|
|
<td class="text-end"><?= number_format($data['opening_balance'] + $net_change, 3) ?></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="mt-4 d-none d-print-block">
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<p>___________________<br>Prepared By</p>
|
|
</div>
|
|
<div class="col-6 text-end">
|
|
<p>___________________<br>Approved By</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'payment_methods'): ?>
|
|
<div class="card p-4 d-print-none">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Payment Methods" data-ar="طرق الدفع">Payment Methods</h5>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addPaymentMethodModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Payment Method" data-ar="إضافة طريقة دفع">Add Payment Method</span>
|
|
</button>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="ID" data-ar="المعرف">ID</th>
|
|
<th data-en="Name (EN)" data-ar="الاسم (EN)">Name (EN)</th>
|
|
<th data-en="Name (AR)" data-ar="الاسم (AR)">Name (AR)</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($data['payment_methods'])): ?>
|
|
<tr>
|
|
<td colspan="4" class="text-center text-muted py-4" data-en="No payment methods found yet. Add one to get started." data-ar="لا توجد طرق دفع حتى الآن. أضف طريقة دفع للبدء.">No payment methods found yet. Add one to get started.</td>
|
|
</tr>
|
|
<?php else: ?>
|
|
<?php foreach ($data['payment_methods'] as $pm): ?>
|
|
<?php
|
|
$paymentMethodId = (int)($pm['id'] ?? 0);
|
|
$paymentMethodNameEn = (string)($pm['name_en'] ?? $pm['name'] ?? '');
|
|
$paymentMethodNameAr = (string)($pm['name_ar'] ?? $pm['name_en'] ?? $pm['name'] ?? '');
|
|
?>
|
|
<tr>
|
|
<td><?= $paymentMethodId ?></td>
|
|
<td><?= htmlspecialchars($paymentMethodNameEn) ?></td>
|
|
<td><?= htmlspecialchars($paymentMethodNameAr) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary" title="Edit" data-bs-toggle="modal" data-bs-target="#editPaymentMethodModal<?= $paymentMethodId ?>"><i class="bi bi-pencil"></i></button>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $paymentMethodId ?>">
|
|
<button type="submit" name="delete_payment_method" class="btn btn-outline-danger" title="Delete"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="modal fade" id="editPaymentMethodModal<?= $paymentMethodId ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Edit Payment Method" data-ar="تعديل طريقة الدفع">Edit Payment Method</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $paymentMethodId ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Name (EN)" data-ar="الاسم (EN)">Name (EN)</label>
|
|
<input type="text" name="name_en" class="form-control" value="<?= htmlspecialchars($paymentMethodNameEn) ?>" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Name (AR)" data-ar="الاسم (AR)">Name (AR)</label>
|
|
<input type="text" name="name_ar" class="form-control" value="<?= htmlspecialchars($paymentMethodNameAr) ?>" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_payment_method" class="btn btn-primary" data-en="Update" data-ar="تحديث">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<!-- Add Payment Method Modal -->
|
|
<div class="modal fade" id="addPaymentMethodModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Add Payment Method" data-ar="إضافة طريقة دفع">Add Payment Method</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Name (EN)" data-ar="الاسم (EN)">Name (EN)</label>
|
|
<input type="text" name="name_en" class="form-control" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Name (AR)" data-ar="الاسم (AR)">Name (AR)</label>
|
|
<input type="text" name="name_ar" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_payment_method" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php elseif ($page === 'expense_categories'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Expense Categories" data-ar="فئات المصروفات">Expense Categories</h5>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addExpenseCategoryModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Category" data-ar="إضافة فئة">Add Category</span>
|
|
</button>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="ID" data-ar="المعرف">ID</th>
|
|
<th data-en="Name (EN)" data-ar="الاسم (إنجليزي)">Name (EN)</th>
|
|
<th data-en="Name (AR)" data-ar="الاسم (عربي)">Name (AR)</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['expense_categories'] as $cat): ?>
|
|
<tr>
|
|
<td><?= $cat['id'] ?></td>
|
|
<td><?= htmlspecialchars($cat['name_en']) ?></td>
|
|
<td><?= htmlspecialchars($cat['name_ar']) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editExpCatModal<?= $cat['id'] ?>"><i class="bi bi-pencil"></i></button>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
|
<button type="submit" name="delete_expense_category" class="btn btn-outline-danger"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Edit Modal -->
|
|
<div class="modal fade" id="editExpCatModal<?= $cat['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Edit Category</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $cat['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Name (EN)</label>
|
|
<div class="input-group">
|
|
<input type="text" name="name_en" id="editExpCatEn<?= $cat['id'] ?>" class="form-control" value="<?= htmlspecialchars($cat['name_en']) ?>" required>
|
|
<button class="btn btn-outline-secondary btn-translate" type="button" data-source="editExpCatAr<?= $cat['id'] ?>" data-target="editExpCatEn<?= $cat['id'] ?>" data-to="en">
|
|
<i class="bi bi-translate"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Name (AR)</label>
|
|
<div class="input-group">
|
|
<input type="text" name="name_ar" id="editExpCatAr<?= $cat['id'] ?>" class="form-control" value="<?= htmlspecialchars($cat['name_ar']) ?>" required>
|
|
<button class="btn btn-outline-secondary btn-translate" type="button" data-source="editExpCatEn<?= $cat['id'] ?>" data-target="editExpCatAr<?= $cat['id'] ?>" data-to="ar">
|
|
<i class="bi bi-translate"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_expense_category" class="btn btn-primary" data-en="Update" data-ar="تحديث">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<!-- Add Modal -->
|
|
<div class="modal fade" id="addExpenseCategoryModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Add Category</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Name (EN)</label>
|
|
<input type="text" name="name_en" class="form-control" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Name (AR)</label>
|
|
<input type="text" name="name_ar" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_expense_category" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'accounting'): ?>
|
|
<?php require 'pages/accounting_view.php'; ?>
|
|
<?php elseif ($page === 'expenses'): ?>
|
|
<?php $expenseCategories = $data['expense_categories'] ?? []; ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Expenses List" data-ar="قائمة المصروفات">Expenses List</h5>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addExpenseModal" <?= empty($expenseCategories) ? 'disabled aria-disabled="true" title="Add an expense category first"' : '' ?>>
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Expense" data-ar="إضافة مصروف">Add Expense</span>
|
|
</button>
|
|
</div>
|
|
|
|
<?php if (empty($expenseCategories)): ?>
|
|
<div class="alert alert-warning d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2" role="alert">
|
|
<div>
|
|
<strong data-en="No expense categories found." data-ar="لا توجد فئات مصروفات.">No expense categories found.</strong>
|
|
<span data-en=" Add at least one category before creating or editing expenses." data-ar=" أضف فئة واحدة على الأقل قبل إنشاء أو تعديل المصروفات."> Add at least one category before creating or editing expenses.</span>
|
|
</div>
|
|
<a href="index.php?page=expense_categories" class="btn btn-sm btn-outline-dark" data-en="Manage Categories" data-ar="إدارة الفئات">Manage Categories</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="bg-light p-3 rounded mb-4">
|
|
<form method="GET" class="form-grid-3">
|
|
<input type="hidden" name="page" value="expenses">
|
|
<div class="col-md-3">
|
|
<label class="form-label small" data-en="Category" data-ar="الفئة">Category</label>
|
|
<select name="category_id" class="form-select">
|
|
<option value="">All</option>
|
|
<?php foreach ($expenseCategories as $c): ?>
|
|
<option value="<?= $c['id'] ?>" <?= ($_GET['category_id'] ?? '') == $c['id'] ? 'selected' : '' ?>><?= htmlspecialchars($c['name_en']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small">Start Date</label>
|
|
<input type="date" name="start_date" class="form-control" value="<?= htmlspecialchars($_GET['start_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small">End Date</label>
|
|
<input type="date" name="end_date" class="form-control" value="<?= htmlspecialchars($_GET['end_date'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-3 d-flex align-items-end gap-1">
|
|
<button type="submit" class="btn btn-primary flex-grow-1" data-en="Filter" data-ar="تصفية">Filter</button>
|
|
<div class="dropdown d-inline-block">
|
|
<button class="btn btn-outline-success dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-download"></i> <span data-en="Export" data-ar="تصدير">Export</span>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="index.php?<?= http_build_query(array_merge($_GET, ['page' => 'export', 'type' => 'expenses', 'format' => 'csv'])) ?>"><i class="bi bi-filetype-csv me-2"></i> CSV</a></li>
|
|
<li><a class="dropdown-item" href="index.php?<?= http_build_query(array_merge($_GET, ['page' => 'export', 'type' => 'expenses', 'format' => 'excel'])) ?>"><i class="bi bi-file-earmark-excel me-2"></i> Excel</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Date" data-ar="التاريخ">Date</th>
|
|
<th>Reference</th>
|
|
<th data-en="Category" data-ar="الفئة">Category</th>
|
|
<th data-en="Description" data-ar="الوصف">Description</th>
|
|
<th class="text-end" data-en="Amount" data-ar="المبلغ">Amount</th>
|
|
<th class="text-end" data-en="Actions" data-ar="إجراءات">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['expenses'] as $exp): ?>
|
|
<tr>
|
|
<td><?= $exp['expense_date'] ?></td>
|
|
<td><?= htmlspecialchars($exp['reference_no'] ?: '---') ?></td>
|
|
<td><?= htmlspecialchars($exp['cat_en'] ?? 'Unknown') ?></td>
|
|
<td><?= htmlspecialchars($exp['description']) ?></td>
|
|
<td class="text-end fw-bold">OMR <?= number_format((float)$exp['amount'], 3) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editExpenseModal<?= $exp['id'] ?>"><i class="bi bi-pencil"></i></button>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $exp['id'] ?>">
|
|
<button type="submit" name="delete_expense" class="btn btn-outline-danger"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Edit Modal -->
|
|
<div class="modal fade" id="editExpenseModal<?= $exp['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Edit Expense</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $exp['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Category" data-ar="الفئة">Category</label>
|
|
<select name="category_id" class="form-select select2" required>
|
|
<?php foreach ($expenseCategories as $c): ?>
|
|
<option value="<?= $c['id'] ?>" <?= $c['id'] == $exp['category_id'] ? 'selected' : '' ?>><?= htmlspecialchars($c['name_en']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Date" data-ar="التاريخ">Date</label>
|
|
<input type="date" name="expense_date" class="form-control" value="<?= $exp['expense_date'] ?>" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Amount" data-ar="المبلغ">Amount</label>
|
|
<input type="number" step="0.001" name="amount" class="form-control" value="<?= (float)$exp['amount'] ?>" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Reference No</label>
|
|
<input type="text" name="reference_no" class="form-control" value="<?= htmlspecialchars($exp['reference_no']) ?>">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Description" data-ar="الوصف">Description</label>
|
|
<textarea name="description" class="form-control"><?= htmlspecialchars($exp['description']) ?></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_expense" class="btn btn-primary" data-en="Update" data-ar="تحديث">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<!-- Add Expense Modal -->
|
|
<div class="modal fade" id="addExpenseModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Add Expense</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Category" data-ar="الفئة">Category</label>
|
|
<select name="category_id" class="form-select" required>
|
|
<option value="">Select Category</option>
|
|
<?php foreach ($expenseCategories as $c): ?>
|
|
<option value="<?= $c['id'] ?>"><?= htmlspecialchars($c['name_en']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Date" data-ar="التاريخ">Date</label>
|
|
<input type="date" name="expense_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Amount" data-ar="المبلغ">Amount</label>
|
|
<input type="number" step="0.001" name="amount" class="form-control" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Reference No</label>
|
|
<input type="text" name="reference_no" class="form-control">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Description" data-ar="الوصف">Description</label>
|
|
<textarea name="description" class="form-control"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_expense" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'expense_report'): ?>
|
|
<div class="card p-4">
|
|
<!-- Formal Print Header -->
|
|
<div class="print-only mb-4">
|
|
<div class="row align-items-center">
|
|
<div class="col-6">
|
|
<?php if (!empty($data['settings']['company_logo'])): ?>
|
|
<img src="<?= htmlspecialchars($data['settings']['company_logo']) ?>" alt="Logo" style="max-height: 80px;" class="mb-2">
|
|
<?php endif; ?>
|
|
<h3 class="mb-1 fw-bold"><?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?></h3>
|
|
<p class="text-muted small mb-0"><?= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?></p>
|
|
<p class="text-muted small mb-0">VAT: <?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?></p>
|
|
</div>
|
|
<div class="col-6 text-end">
|
|
<h2 class="text-uppercase text-muted" data-en="Expense Report" data-ar="تقرير المصروفات">Expense Report</h2>
|
|
<p class="mb-0">Date: <?= date('Y-m-d') ?></p>
|
|
<p class="mb-0">Period: <?= htmlspecialchars($_GET['start_date'] ?? date('Y-m-01')) ?> - <?= htmlspecialchars($_GET['end_date'] ?? date('Y-m-d')) ?></p>
|
|
</div>
|
|
</div>
|
|
<hr>
|
|
</div>
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
|
|
<h5 class="m-0" data-en="Expense Report" data-ar="تقرير المصروفات">Expense Report</h5>
|
|
<button class="btn btn-outline-secondary" onclick="window.print()">
|
|
<i class="bi bi-printer"></i> <span data-en="Print" data-ar="طباعة">Print</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="bg-light p-3 rounded mb-4 d-print-none">
|
|
<form method="GET" class="form-grid-3">
|
|
<input type="hidden" name="page" value="expense_report">
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="From Date" data-ar="من تاريخ">From Date</label>
|
|
<input type="date" name="start_date" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['start_date'] ?? date('Y-m-01')) ?>">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="To Date" data-ar="إلى تاريخ">To Date</label>
|
|
<input type="date" name="end_date" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['end_date'] ?? date('Y-m-d')) ?>">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="Category" data-ar="الفئة">Category</label>
|
|
<select name="category_id" class="form-select form-select-sm">
|
|
<option value="" data-en="All Categories" data-ar="كل الفئات">All Categories</option>
|
|
<?php foreach ($data['expense_categories'] as $cat): ?>
|
|
<option value="<?= $cat['id'] ?>" <?= (isset($_GET['category_id']) && $_GET['category_id'] == $cat['id']) ? 'selected' : '' ?>>
|
|
<?= htmlspecialchars($cat['name_en']) ?> / <?= htmlspecialchars($cat['name_ar']) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3 d-flex align-items-end">
|
|
<button type="submit" class="btn btn-primary btn-sm w-100" data-en="Generate Report" data-ar="توليد التقرير">Generate Report</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="row mb-4">
|
|
<div class="col-md-12">
|
|
<div class="card bg-light border-0 shadow-sm">
|
|
<div class="card-body text-center">
|
|
<h6 class="text-muted text-uppercase mb-2" data-en="Total Expenses" data-ar="إجمالي المصروفات">Total Expenses</h6>
|
|
<h2 class="text-danger mb-0">OMR <?= number_format((float)$data['total_expenses'], 3) ?></h2>
|
|
<small class="text-muted" data-en="For the selected period" data-ar="للفترة المختارة">For the selected period</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered table-striped">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th data-en="Category" data-ar="الفئة">Category</th>
|
|
<th class="text-end" data-en="Total Amount" data-ar="إجمالي المبلغ">Total Amount</th>
|
|
<th class="text-end" data-en="% of Total" data-ar="نسبة الإجمالي">% of Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($data['report_by_category'])): ?>
|
|
<tr><td colspan="3" class="text-center text-muted" data-en="No expenses found for this period." data-ar="لم يتم العثور على مصروفات لهذه الفترة.">No expenses found for this period.</td></tr>
|
|
<?php else: ?>
|
|
<?php foreach ($data['report_by_category'] as $row):
|
|
$percent = $data['total_expenses'] > 0 ? ($row['total'] / $data['total_expenses'] * 100) : 0;
|
|
?>
|
|
<tr>
|
|
<td>
|
|
<div class="fw-bold"><?= htmlspecialchars($row['name_en']) ?></div>
|
|
<div class="text-muted small"><?= htmlspecialchars($row['name_ar']) ?></div>
|
|
</td>
|
|
<td class="text-end fw-bold text-dark">OMR <?= number_format((float)$row['total'], 3) ?></td>
|
|
<td class="text-end">
|
|
<div class="progress" style="height: 5px;">
|
|
<div class="progress-bar bg-danger" role="progressbar" style="width: <?= $percent ?>%"></div>
|
|
</div>
|
|
<small><?= number_format($percent, 1) ?>%</small>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="print-only mt-5">
|
|
<div class="row">
|
|
<div class="col-4 text-center">
|
|
<hr class="mx-4">
|
|
<p data-en="Prepared By" data-ar="أعد بواسطة">Prepared By</p>
|
|
</div>
|
|
<div class="col-4 text-center">
|
|
</div>
|
|
<div class="col-4 text-center">
|
|
<hr class="mx-4">
|
|
<p data-en="Approved By" data-ar="اعتمد بواسطة">Approved By</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'sales_returns'): ?>
|
|
<div class="card p-4">
|
|
<!-- Print Header -->
|
|
<div class="print-only mb-4">
|
|
<div class="row align-items-center">
|
|
<div class="col-6">
|
|
<?php if (!empty($data['settings']['company_logo'])): ?>
|
|
<img src="<?= htmlspecialchars($data['settings']['company_logo']) ?>" alt="Logo" style="max-height: 80px;" class="mb-2">
|
|
<?php endif; ?>
|
|
<h3 class="mb-1 fw-bold"><?= htmlspecialchars($data['settings']['company_name'] ?? 'Accounting System') ?></h3>
|
|
<p class="text-muted small mb-0"><?= nl2br(htmlspecialchars($data['settings']['company_address'] ?? '')) ?></p>
|
|
<p class="text-muted small mb-0">VAT: <?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?></p>
|
|
</div>
|
|
<div class="col-6 text-end">
|
|
<h2 class="text-uppercase text-muted">Sales Returns Report</h2>
|
|
<p class="mb-0">Date: <?= date('Y-m-d') ?></p>
|
|
</div>
|
|
</div>
|
|
<hr>
|
|
</div>
|
|
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
|
|
<h5 class="m-0" data-en="Sales Returns" data-ar="مرتجع المبيعات">Sales Returns</h5>
|
|
<div class="d-flex gap-2">
|
|
<a href="index.php?page=export&type=sales_returns&format=excel" class="btn btn-outline-success">
|
|
<i class="bi bi-download"></i> <span data-en="Export" data-ar="تصدير">Export</span>
|
|
</a>
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addSalesReturnModal" id="createSalesReturnBtn">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Create New Return" data-ar="إنشاء مرتجع جديد">Create New Return</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-light p-3 rounded mb-4">
|
|
<form method="GET" class="form-grid-3">
|
|
<input type="hidden" name="page" value="sales_returns">
|
|
<div class="col-md-9">
|
|
<input type="text" name="search" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="Search by Return ID, Customer or Invoice ID...">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<button type="submit" class="btn btn-primary btn-sm w-100" data-en="Filter" data-ar="تصفية">Filter</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Return #" data-ar="رقم المرتجع">Return #</th>
|
|
<th data-en="Date" data-ar="التاريخ">Date</th>
|
|
<th data-en="Invoice #" data-ar="رقم الفاتورة">Invoice #</th>
|
|
<th data-en="Customer" data-ar="العميل">Customer</th>
|
|
<th data-en="Total Amount" data-ar="إجمالي المرتجع" class="text-end">Total Amount</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['returns'] as $ret): ?>
|
|
<tr>
|
|
<td>RET-<?= str_pad((string)$ret['id'], 5, '0', STR_PAD_LEFT) ?></td>
|
|
<td><?= $ret['return_date'] ?></td>
|
|
<td>INV-<?= str_pad((string)$ret['invoice_id'], 5, '0', STR_PAD_LEFT) ?></td>
|
|
<td><?= htmlspecialchars($ret['customer_name'] ?? 'Walk-in') ?></td>
|
|
<td class="text-end fw-bold text-danger">OMR <?= number_format((float)$ret['total_amount'], 3) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-info view-return-btn" data-id="<?= $ret['id'] ?>"><i class="bi bi-eye"></i></button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($data['returns'])): ?>
|
|
<tr><td colspan="6" class="text-center py-4 text-muted">No returns found</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'purchase_returns'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Purchase Returns" data-ar="مرتجع المشتريات">Purchase Returns</h5>
|
|
<div class="d-flex gap-2">
|
|
<a href="index.php?page=export&type=purchase_returns&format=excel" class="btn btn-outline-success">
|
|
<i class="bi bi-download"></i> <span data-en="Export" data-ar="تصدير">Export</span>
|
|
</a>
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addPurchaseReturnModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Create New Return" data-ar="إنشاء مرتجع جديد">Create New Return</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-light p-3 rounded mb-4">
|
|
<form method="GET" class="form-grid-3">
|
|
<input type="hidden" name="page" value="purchase_returns">
|
|
<div class="col-md-9">
|
|
<input type="text" name="search" class="form-control form-control-sm" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>" placeholder="Search by Return ID, Supplier or Invoice ID...">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<button type="submit" class="btn btn-primary btn-sm w-100" data-en="Filter" data-ar="تصفية">Filter</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Return #" data-ar="رقم المرتجع">Return #</th>
|
|
<th data-en="Date" data-ar="التاريخ">Date</th>
|
|
<th data-en="Invoice #" data-ar="رقم الفاتورة">Invoice #</th>
|
|
<th data-en="Supplier" data-ar="المورد">Supplier</th>
|
|
<th data-en="Total Amount" data-ar="إجمالي المرتجع" class="text-end">Total Amount</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['returns'] as $ret): ?>
|
|
<tr>
|
|
<td>PRET-<?= str_pad((string)$ret['id'], 5, '0', STR_PAD_LEFT) ?></td>
|
|
<td><?= $ret['return_date'] ?></td>
|
|
<td>PUR-<?= str_pad((string)$ret['purchase_id'], 5, '0', STR_PAD_LEFT) ?></td>
|
|
<td><?= htmlspecialchars($ret['supplier_name'] ?? 'Unknown') ?></td>
|
|
<td class="text-end fw-bold text-danger">OMR <?= number_format((float)$ret['total_amount'], 3) ?></td>
|
|
<td class="text-end">
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-info view-return-btn" data-id="<?= $ret['id'] ?>"><i class="bi bi-eye"></i></button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($data['returns'])): ?>
|
|
<tr><td colspan="6" class="text-center py-4 text-muted">No returns found</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'hr_departments'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="HR Departments" data-ar="أقسام الموارد البشرية">HR Departments</h5>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addHrDepartmentModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Department" data-ar="إضافة قسم">Add Department</span>
|
|
</button>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="ID" data-ar="المعرف">ID</th>
|
|
<th data-en="Department Name" data-ar="اسم القسم">Department Name</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['departments'] as $d): ?>
|
|
<tr>
|
|
<td><?= $d['id'] ?></td>
|
|
<td><?= htmlspecialchars($d['name']) ?></td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editHrDepartmentModal<?= $d['id'] ?>"><i class="bi bi-pencil"></i></button>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $d['id'] ?>">
|
|
<button type="submit" name="delete_hr_department" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<!-- Edit Dept Modal -->
|
|
<div class="modal fade" id="editHrDepartmentModal<?= $d['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Edit Department" data-ar="تعديل القسم">Edit Department</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $d['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Department Name" data-ar="اسم القسم">Department Name</label>
|
|
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($d['name']) ?>" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_hr_department" class="btn btn-primary" data-en="Update" data-ar="تحديث">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<?php elseif ($page === 'hr_employees'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="HR Employees" data-ar="موظفي الموارد البشرية">HR Employees</h5>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addHrEmployeeModal">
|
|
<i class="bi bi-plus-lg"></i> <span data-en="Add Employee" data-ar="إضافة موظف">Add Employee</span>
|
|
</button>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Name" data-ar="الاسم">Name</th>
|
|
<th data-en="Biometric ID" data-ar="معرف البصمة">Biometric ID</th>
|
|
<th data-en="Department" data-ar="القسم">Department</th>
|
|
<th data-en="Position" data-ar="المنصب">Position</th>
|
|
<th data-en="Salary" data-ar="الراتب">Salary</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['employees'] as $e): ?>
|
|
<tr>
|
|
<td>
|
|
<div class="fw-bold"><?= htmlspecialchars($e['name']) ?></div>
|
|
<div class="small text-muted"><?= htmlspecialchars($e['email']) ?></div>
|
|
</td>
|
|
<td><span class="badge bg-light text-dark border"><?= htmlspecialchars($e['biometric_id'] ?? '---') ?></span></td>
|
|
<td><?= htmlspecialchars($e['dept_name'] ?? '---') ?></td>
|
|
<td><?= htmlspecialchars($e['position']) ?></td>
|
|
<td>OMR <?= number_format((float)($e['salary'] ?? 0), 3) ?></td>
|
|
<td>
|
|
<span class="badge <?= $e['status'] === 'active' ? 'bg-success' : 'bg-danger' ?> text-uppercase">
|
|
<?= $e['status'] ?>
|
|
</span>
|
|
</td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editHrEmployeeModal<?= $e['id'] ?>"><i class="bi bi-pencil"></i></button>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $e['id'] ?>">
|
|
<button type="submit" name="delete_hr_employee" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<!-- Edit Employee Modal -->
|
|
<div class="modal fade" id="editHrEmployeeModal<?= $e['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Edit Employee" data-ar="تعديل الموظف">Edit Employee</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $e['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="form-grid-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Full Name" data-ar="الاسم الكامل">Full Name</label>
|
|
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($e['name']) ?>" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Department" data-ar="القسم">Department</label>
|
|
<select name="department_id" class="form-select">
|
|
<option value="">--- Select ---</option>
|
|
<?php foreach ($data['departments'] as $d): ?>
|
|
<option value="<?= $d['id'] ?>" <?= $e['department_id'] == $d['id'] ? 'selected' : '' ?>><?= htmlspecialchars($d['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Email" data-ar="البريد">Email</label>
|
|
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($e['email']) ?>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Phone" data-ar="الهاتف">Phone</label>
|
|
<input type="text" name="phone" class="form-control" value="<?= htmlspecialchars($e['phone']) ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Position" data-ar="المنصب">Position</label>
|
|
<input type="text" name="position" class="form-control" value="<?= htmlspecialchars($e['position']) ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Basic Salary" data-ar="الراتب الأساسي">Basic Salary</label>
|
|
<input type="number" step="0.001" name="salary" class="form-control" value="<?= $e['salary'] ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Biometric ID" data-ar="معرف البصمة">Biometric ID</label>
|
|
<input type="text" name="biometric_id" class="form-control" value="<?= htmlspecialchars($e['biometric_id'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Joining Date" data-ar="تاريخ الانضمام">Joining Date</label>
|
|
<input type="date" name="joining_date" class="form-control" value="<?= $e['joining_date'] ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Status" data-ar="الحالة">Status</label>
|
|
<select name="status" class="form-select">
|
|
<option value="active" <?= $e['status'] === 'active' ? 'selected' : '' ?>>Active</option>
|
|
<option value="inactive" <?= $e['status'] === 'inactive' ? 'selected' : '' ?>>Inactive</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_hr_employee" class="btn btn-primary" data-en="Update" data-ar="تحديث">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<?php elseif ($page === 'hr_attendance'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="HR Attendance" data-ar="حضور الموارد البشرية">HR Attendance</h5>
|
|
<div class="d-flex gap-2">
|
|
<form method="POST" class="d-inline">
|
|
<button type="submit" name="pull_biometric_data" class="btn btn-primary btn-sm">
|
|
<i class="bi bi-cloud-download"></i> <span data-en="Pull Data from Devices" data-ar="سحب البيانات من الأجهزة">Pull Data from Devices</span>
|
|
</button>
|
|
</form>
|
|
<button class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#biometricInfoModal">
|
|
<i class="bi bi-fingerprint"></i> <span data-en="Biometric Sync" data-ar="مزامنة البصمة">Biometric Sync</span>
|
|
</button>
|
|
<form method="GET" class="d-flex gap-2">
|
|
<input type="hidden" name="page" value="hr_attendance">
|
|
<input type="date" name="date" class="form-control form-control-sm" value="<?= $data['attendance_date'] ?>" onchange="this.form.submit()">
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Employee" data-ar="الموظف">Employee</th>
|
|
<th data-en="Department" data-ar="القسم">Department</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th data-en="Clock In" data-ar="وقت الدخول">Clock In</th>
|
|
<th data-en="Clock Out" data-ar="وقت الخروج">Clock Out</th>
|
|
<th data-en="Action" data-ar="إجراء" class="text-end">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['employees'] as $e): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($e['name']) ?></td>
|
|
<td><?= htmlspecialchars($e['dept_name'] ?? '---') ?></td>
|
|
<td>
|
|
<?php if ($e['status']): ?>
|
|
<span class="badge <?= $e['status'] === 'present' ? 'bg-success' : ($e['status'] === 'absent' ? 'bg-danger' : 'bg-warning') ?> text-uppercase">
|
|
<?= $e['status'] ?>
|
|
</span>
|
|
<?php else: ?>
|
|
<span class="badge bg-secondary text-uppercase">Not Marked</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?= $e['clock_in'] ?? '---' ?></td>
|
|
<td><?= $e['clock_out'] ?? '---' ?></td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#markAttendanceModal<?= $e['id'] ?>">
|
|
<i class="bi bi-calendar-check"></i> <span data-en="Mark" data-ar="تسجيل">Mark</span>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<!-- Attendance Modal -->
|
|
<div class="modal fade" id="markAttendanceModal<?= $e['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Mark Attendance" data-ar="تسجيل الحضور">Mark Attendance - <?= htmlspecialchars($e['name']) ?></h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="employee_id" value="<?= $e['id'] ?>">
|
|
<input type="hidden" name="attendance_date" value="<?= $data['attendance_date'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Status" data-ar="الحالة">Status</label>
|
|
<select name="status" class="form-select">
|
|
<option value="present" <?= $e['status'] === 'present' ? 'selected' : '' ?>>Present</option>
|
|
<option value="absent" <?= $e['status'] === 'absent' ? 'selected' : '' ?>>Absent</option>
|
|
<option value="on_leave" <?= $e['status'] === 'on_leave' ? 'selected' : '' ?>>On Leave</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-grid-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Clock In" data-ar="وقت الدخول">Clock In</label>
|
|
<input type="time" name="clock_in" class="form-control" value="<?= $e['clock_in'] ?>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Clock Out" data-ar="وقت الخروج">Clock Out</label>
|
|
<input type="time" name="clock_out" class="form-control" value="<?= $e['clock_out'] ?>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="mark_attendance" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<!-- Biometric Info Modal -->
|
|
<div class="modal fade" id="biometricInfoModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Biometric Integration Info" data-ar="معلومات تكامل البصمة">Biometric Integration Info</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p data-en="To sync attendance from your biometric device, use the following API endpoint:" data-ar="لمزامنة الحضور من جهاز البصمة الخاص بك، استخدم نقطة نهاية API التالية:">
|
|
To sync attendance from your biometric device, use the following API endpoint:
|
|
</p>
|
|
<div class="bg-light p-3 rounded mb-3 border">
|
|
<code><?= (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]" ?>/api/biometric_sync.php</code>
|
|
</div>
|
|
<p data-en="Expected JSON format:" data-ar="تنسيق JSON المتوقع:">Expected JSON format:</p>
|
|
<pre class="bg-dark text-light p-3 rounded">
|
|
[
|
|
{
|
|
"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"
|
|
}
|
|
]
|
|
</pre>
|
|
<p class="small text-muted" data-en="Note: Ensure Employee Biometric IDs match those in the device logs." data-ar="ملاحظة: تأكد من مطابقة معرفات الموظفين الحيوية مع تلك الموجودة في سجلات الجهاز.">
|
|
Note: Ensure Employee Biometric IDs match those in the device logs.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'hr_payroll'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="HR Payroll" data-ar="رواتب الموارد البشرية">HR Payroll</h5>
|
|
<div class="d-flex gap-2">
|
|
<form method="GET" class="d-flex gap-2">
|
|
<input type="hidden" name="page" value="hr_payroll">
|
|
<select name="month" class="form-select form-select-sm" onchange="this.form.submit()">
|
|
<?php for($m=1; $m<=12; $m++): ?>
|
|
<option value="<?= $m ?>" <?= $data['month'] == $m ? 'selected' : '' ?>><?= date('F', mktime(0, 0, 0, $m, 1)) ?></option>
|
|
<?php endfor; ?>
|
|
</select>
|
|
<select name="year" class="form-select form-select-sm" onchange="this.form.submit()">
|
|
<?php for($y=date('Y'); $y>=date('Y')-2; $y--): ?>
|
|
<option value="<?= $y ?>" <?= $data['year'] == $y ? 'selected' : '' ?>><?= $y ?></option>
|
|
<?php endfor; ?>
|
|
</select>
|
|
</form>
|
|
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#generatePayrollModal">
|
|
<i class="bi bi-gear"></i> <span data-en="Generate" data-ar="توليد">Generate</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Employee" data-ar="الموظف">Employee</th>
|
|
<th data-en="Basic" data-ar="الأساسي">Basic</th>
|
|
<th data-en="Bonus" data-ar="مكافأة">Bonus</th>
|
|
<th data-en="Deductions" data-ar="استقطاعات">Deductions</th>
|
|
<th data-en="Net Salary" data-ar="صافي الراتب">Net Salary</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['payroll'] as $p): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($p['emp_name']) ?></td>
|
|
<td>OMR <?= number_format((float)($p['basic_salary'] ?? 0), 3) ?></td>
|
|
<td class="text-success">+ OMR <?= number_format((float)($p['bonus'] ?? 0), 3) ?></td>
|
|
<td class="text-danger">- OMR <?= number_format((float)($p['deductions'] ?? 0), 3) ?></td>
|
|
<td class="fw-bold">OMR <?= number_format((float)($p['net_salary'] ?? 0), 3) ?></td>
|
|
<td>
|
|
<span class="badge <?= $p['status'] === 'paid' ? 'bg-success' : 'bg-warning' ?> text-uppercase">
|
|
<?= $p['status'] ?>
|
|
</span>
|
|
</td>
|
|
<td class="text-end">
|
|
<?php if ($p['status'] === 'pending'): ?>
|
|
<form method="POST" class="d-inline">
|
|
<input type="hidden" name="id" value="<?= $p['id'] ?>">
|
|
<button type="submit" name="pay_payroll" class="btn btn-sm btn-success" title="Mark Paid"><i class="bi bi-check-circle"></i></button>
|
|
</form>
|
|
<?php endif; ?>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $p['id'] ?>">
|
|
<button type="submit" name="delete_payroll" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<!-- Generate Payroll Modal -->
|
|
<div class="modal fade" id="generatePayrollModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Generate Payroll" data-ar="توليد الرواتب">Generate Payroll</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="month" value="<?= $data['month'] ?>">
|
|
<input type="hidden" name="year" value="<?= $data['year'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Employee" data-ar="الموظف">Employee</label>
|
|
<select name="employee_id" class="form-select select2" required>
|
|
<option value="">--- Select ---</option>
|
|
<?php foreach ($data['employees'] as $e): ?>
|
|
<option value="<?= $e['id'] ?>"><?= htmlspecialchars($e['name']) ?> (Basic: <?= number_format((float)($e['salary'] ?? 0), 3) ?>)</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="form-grid-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Bonus" data-ar="مكافأة">Bonus</label>
|
|
<input type="number" step="0.001" name="bonus" class="form-control" value="0.000">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Deductions" data-ar="استقطاعات">Deductions</label>
|
|
<input type="number" step="0.001" name="deductions" class="form-control" value="0.000">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="generate_payroll" class="btn btn-primary" data-en="Generate" data-ar="توليد">Generate</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'devices'): ?>
|
|
<div class="card p-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h5 class="m-0" data-en="Biometric Devices" data-ar="أجهزة البصمة">Biometric Devices</h5>
|
|
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addDeviceModal">
|
|
<i class="bi bi-plus-circle"></i> <span data-en="Add Device" data-ar="إضافة جهاز">Add Device</span>
|
|
</button>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="Device Name" data-ar="اسم الجهاز">Device Name</th>
|
|
<th data-en="IP / IO Address" data-ar="عنوان IP / IO">IP / IO Address</th>
|
|
<th data-en="Port" data-ar="المنفذ">Port</th>
|
|
<th data-en="Serial" data-ar="الرقم التسلسلي">Serial</th>
|
|
<th data-en="Last Sync" data-ar="آخر مزامنة">Last Sync</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['devices'] as $d): ?>
|
|
<tr>
|
|
<td>
|
|
<strong><?= htmlspecialchars($d['device_name']) ?></strong>
|
|
</td>
|
|
<td>
|
|
<div><small class="text-muted">IP:</small> <?= htmlspecialchars($d['ip_address']) ?></div>
|
|
<?php if ($d['io_address']): ?>
|
|
<div><small class="text-muted">IO:</small> <?= htmlspecialchars($d['io_address']) ?></div>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?= $d['port'] ?></td>
|
|
<td><?= htmlspecialchars($d['serial_number'] ?? '---') ?></td>
|
|
<td><?= $d['last_sync'] ?? 'Never' ?></td>
|
|
<td>
|
|
<span class="badge <?= $d['status'] === 'active' ? 'bg-success' : 'bg-danger' ?> text-uppercase">
|
|
<?= $d['status'] ?>
|
|
</span>
|
|
</td>
|
|
<td class="text-end">
|
|
<form method="POST" class="d-inline">
|
|
<input type="hidden" name="id" value="<?= $d['id'] ?>">
|
|
<button type="submit" name="test_device_connection" class="btn btn-sm btn-outline-info" title="Test Connection"><i class="bi bi-broadcast"></i></button>
|
|
</form>
|
|
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editDeviceModal<?= $d['id'] ?>"><i class="bi bi-pencil"></i></button>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $d['id'] ?>">
|
|
<button type="submit" name="delete_biometric_device" class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- Edit Device Modal -->
|
|
<div class="modal fade" id="editDeviceModal<?= $d['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Edit Device" data-ar="تعديل الجهاز">Edit Device</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $d['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Device Name" data-ar="اسم الجهاز">Device Name</label>
|
|
<input type="text" name="device_name" class="form-control" value="<?= htmlspecialchars($d['device_name']) ?>" required>
|
|
</div>
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-8">
|
|
<label class="form-label" data-en="IP Address" data-ar="عنوان IP">IP Address</label>
|
|
<input type="text" name="ip_address" class="form-control" value="<?= htmlspecialchars($d['ip_address']) ?>" required>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Port" data-ar="المنفذ">Port</label>
|
|
<input type="number" name="port" class="form-control" value="<?= $d['port'] ?>" required>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="IO Address" data-ar="عنوان IO">IO Address</label>
|
|
<input type="text" name="io_address" class="form-control" value="<?= htmlspecialchars($d['io_address']) ?>">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Serial Number" data-ar="الرقم التسلسلي">Serial Number</label>
|
|
<input type="text" name="serial_number" class="form-control" value="<?= htmlspecialchars($d['serial_number']) ?>">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_biometric_device" class="btn btn-primary" data-en="Update" data-ar="تحديث">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<!-- Add Device Modal -->
|
|
<div class="modal fade" id="addDeviceModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Add Biometric Device" data-ar="إضافة جهاز بصمة">Add Biometric Device</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Device Name" data-ar="اسم الجهاز">Device Name</label>
|
|
<input type="text" name="device_name" class="form-control" required placeholder="e.g. Main Entrance">
|
|
</div>
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-8">
|
|
<label class="form-label" data-en="IP Address" data-ar="عنوان IP">IP Address</label>
|
|
<input type="text" name="ip_address" class="form-control" required placeholder="192.168.1.201">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Port" data-ar="المنفذ">Port</label>
|
|
<input type="number" name="port" class="form-control" value="4370" required>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="IO Address" data-ar="عنوان IO">IO Address</label>
|
|
<input type="text" name="io_address" class="form-control" placeholder="Optional IO address">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Serial Number" data-ar="الرقم التسلسلي">Serial Number</label>
|
|
<input type="text" name="serial_number" class="form-control" placeholder="Device Serial Number">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_biometric_device" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'scale_devices'): ?>
|
|
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
|
|
<div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0 fw-bold" data-en="POS Devices" data-ar="أجهزة نقاط البيع">POS Devices</h5>
|
|
<button class="btn btn-primary rounded-3" data-bs-toggle="modal" data-bs-target="#addScaleDeviceModal">
|
|
<i class="fas fa-plus me-1"></i> <span data-en="Add Device" data-ar="إضافة جهاز">Add Device</span>
|
|
</button>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th data-en="Device Name" data-ar="اسم الجهاز">Device Name</th>
|
|
<th data-en="Type" data-ar="النوع">Type</th>
|
|
<th data-en="Connection" data-ar="الاتصال">Connection</th>
|
|
<th data-en="Details" data-ar="التفاصيل">Details</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th class="text-end" data-en="Actions" data-ar="الإجراءات">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['scale_devices'] as $d): ?>
|
|
<tr>
|
|
<td>
|
|
<div class="fw-bold"><?= htmlspecialchars($d['device_name']) ?></div>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-info-subtle text-info text-capitalize"><?= $d['device_type'] ?></span>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-secondary-subtle text-secondary text-uppercase"><?= $d['connection_type'] ?></span>
|
|
</td>
|
|
<td class="small text-muted">
|
|
<?php if ($d['connection_type'] === 'network'): ?>
|
|
<?= htmlspecialchars((string)$d['ip_address']) ?>:<?= $d['port'] ?>
|
|
<?php elseif ($d['connection_type'] === 'serial'): ?>
|
|
Baud: <?= $d['baud_rate'] ?>
|
|
<?php else: ?>
|
|
USB Interface
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-<?= $d['status'] === 'active' ? 'success' : 'danger' ?>-subtle text-<?= $d['status'] === 'active' ? 'success' : 'danger' ?> text-capitalize"><?= $d['status'] ?></span>
|
|
</td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-light rounded-pill px-3" data-bs-toggle="modal" data-bs-target="#editScaleDeviceModal<?= $d['id'] ?>">
|
|
<i class="fas fa-edit me-1"></i> <span data-en="Edit" data-ar="تعديل">Edit</span>
|
|
</button>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $d['id'] ?>">
|
|
<button type="submit" name="delete_pos_device" class="btn btn-sm btn-outline-danger border-0 rounded-pill">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- Edit Device Modal -->
|
|
<div class="modal fade" id="editScaleDeviceModal<?= $d['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog border-0">
|
|
<div class="modal-content shadow border-0 rounded-4">
|
|
<div class="modal-header border-0">
|
|
<h5 class="modal-title fw-bold" data-en="Edit Device" data-ar="تعديل الجهاز">Edit Device</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $d['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Device Name" data-ar="اسم الجهاز">Device Name</label>
|
|
<input type="text" name="device_name" class="form-control" value="<?= htmlspecialchars($d['device_name']) ?>" required>
|
|
</div>
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Device Type" data-ar="نوع الجهاز">Device Type</label>
|
|
<select name="device_type" class="form-select">
|
|
<option value="scale" <?= $d['device_type'] === 'scale' ? 'selected' : '' ?>>Weight Scale</option>
|
|
<option value="printer" <?= $d['device_type'] === 'printer' ? 'selected' : '' ?>>Receipt Printer</option>
|
|
<option value="display" <?= $d['device_type'] === 'display' ? 'selected' : '' ?>>Customer Display</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Connection Type" data-ar="نوع الاتصال">Connection Type</label>
|
|
<select name="connection_type" class="form-select">
|
|
<option value="usb" <?= $d['connection_type'] === 'usb' ? 'selected' : '' ?>>USB</option>
|
|
<option value="network" <?= $d['connection_type'] === 'network' ? 'selected' : '' ?>>Network (TCP/IP)</option>
|
|
<option value="serial" <?= $d['connection_type'] === 'serial' ? 'selected' : '' ?>>Serial (RS232)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-8">
|
|
<label class="form-label" data-en="IP Address" data-ar="عنوان IP">IP Address</label>
|
|
<input type="text" name="ip_address" class="form-control" value="<?= htmlspecialchars((string)$d['ip_address']) ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Port" data-ar="المنفذ">Port</label>
|
|
<input type="number" name="port" class="form-control" value="<?= $d['port'] ?>">
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Baud Rate" data-ar="معدل الباود">Baud Rate</label>
|
|
<input type="number" name="baud_rate" class="form-control" value="<?= $d['baud_rate'] ?>">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Status" data-ar="الحالة">Status</label>
|
|
<select name="status" class="form-select">
|
|
<option value="active" <?= $d['status'] === 'active' ? 'selected' : '' ?>>Active</option>
|
|
<option value="inactive" <?= $d['status'] === 'inactive' ? 'selected' : '' ?>>Inactive</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_pos_device" class="btn btn-primary" data-en="Update" data-ar="تحديث">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<!-- Add Scale Device Modal -->
|
|
<div class="modal fade" id="addScaleDeviceModal" tabindex="-1">
|
|
<div class="modal-dialog border-0">
|
|
<div class="modal-content shadow border-0 rounded-4">
|
|
<div class="modal-header border-0">
|
|
<h5 class="modal-title fw-bold" data-en="Add POS Device" data-ar="إضافة جهاز">Add POS Device</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Device Name" data-ar="اسم الجهاز">Device Name</label>
|
|
<input type="text" name="device_name" class="form-control" required placeholder="e.g. Counter 1 Scale">
|
|
</div>
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Device Type" data-ar="نوع الجهاز">Device Type</label>
|
|
<select name="device_type" class="form-select">
|
|
<option value="scale">Weight Scale</option>
|
|
<option value="printer">Receipt Printer</option>
|
|
<option value="display">Customer Display</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Connection Type" data-ar="نوع الاتصال">Connection Type</label>
|
|
<select name="connection_type" class="form-select">
|
|
<option value="usb">USB</option>
|
|
<option value="network">Network (TCP/IP)</option>
|
|
<option value="serial">Serial (RS232)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-8">
|
|
<label class="form-label" data-en="IP Address" data-ar="عنوان IP">IP Address</label>
|
|
<input type="text" name="ip_address" class="form-control" placeholder="192.168.1.50">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Port" data-ar="المنفذ">Port</label>
|
|
<input type="number" name="port" class="form-control" placeholder="9100">
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Baud Rate" data-ar="معدل الباود">Baud Rate</label>
|
|
<input type="number" name="baud_rate" class="form-control" placeholder="9600">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_pos_device" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'my_profile'): ?>
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<div class="card p-4 text-center border-0 shadow-sm rounded-4">
|
|
<h5 class="mb-4 fw-bold" data-en="Profile Picture" data-ar="صورة الملف الشخصي">Profile Picture</h5>
|
|
<div class="mb-3">
|
|
<?php if (!empty($data['user']['profile_pic'])): ?>
|
|
<img src="<?= htmlspecialchars($data['user']['profile_pic']) ?>?v=<?= time() ?>" alt="Profile" class="rounded-circle shadow-sm" style="width: 150px; height: 150px; object-fit: cover; border: 5px solid #fff;">
|
|
<?php else: ?>
|
|
<div class="rounded-circle bg-light d-inline-flex align-items-center justify-content-center shadow-sm" style="width: 150px; height: 150px; border: 5px solid #fff;">
|
|
<i class="bi bi-person text-muted" style="font-size: 5rem;"></i>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<div class="mt-3">
|
|
<h6 class="fw-bold mb-0"><?= htmlspecialchars($data['user']['username']) ?></h6>
|
|
<p class="text-muted small"><?= htmlspecialchars($_SESSION['user_role_name'] ?? 'User') ?></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-8">
|
|
<div class="card p-4 border-0 shadow-sm rounded-4">
|
|
<h5 class="mb-4 fw-bold" data-en="Edit Profile" data-ar="تعديل الملف الشخصي">Edit Profile</h5>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="form-grid-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold" data-en="Username" data-ar="اسم المستخدم">Username</label>
|
|
<input type="text" name="username" class="form-control rounded-3" value="<?= htmlspecialchars($data['user']['username'] ?? '') ?>" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold" data-en="Email" data-ar="البريد الإلكتروني">Email</label>
|
|
<input type="email" name="email" class="form-control rounded-3" value="<?= htmlspecialchars($data['user']['email'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold" data-en="Phone" data-ar="الهاتف">Phone</label>
|
|
<input type="text" name="phone" class="form-control rounded-3" value="<?= htmlspecialchars($data['user']['phone'] ?? '') ?>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold" data-en="New Password" data-ar="كلمة مرور جديدة">New Password</label>
|
|
<input type="password" name="password" class="form-control rounded-3" placeholder="Leave blank to keep current">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold" data-en="Change Profile Picture" data-ar="تغيير صورة الملف الشخصي">Change Profile Picture</label>
|
|
<input type="file" name="profile_pic" class="form-control rounded-3" accept="image/*">
|
|
</div>
|
|
<div class="col-md-12 mt-4">
|
|
<button type="submit" name="update_profile" class="btn btn-primary rounded-pill px-4">
|
|
<i class="bi bi-check-circle me-1"></i> <span data-en="Save Changes" data-ar="حفظ التغييرات">Save Changes</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === "outlets" && ($_SESSION["user_role_name"] ?? "") === "Administrator"): ?>
|
|
<?php require "outlets_html.php"; ?>
|
|
<?php elseif ($page === 'copy_outlet_data'): require 'pages/copy_outlet_data_view.php'; ?><?php elseif ($page === 'settings'): ?>
|
|
<div class="row justify-content-center">
|
|
<div class="col-xl-10">
|
|
<div class="card border-0 shadow-sm rounded-4">
|
|
<div class="card-header bg-white py-3 border-bottom-0 d-flex justify-content-between align-items-center">
|
|
<div class="d-flex align-items-center gap-3">
|
|
<div class="rounded-circle bg-primary bg-opacity-10 p-3 text-primary">
|
|
<i class="bi bi-building-gear fs-4"></i>
|
|
</div>
|
|
<div>
|
|
<h5 class="m-0 fw-bold text-dark" data-en="Company Profile & Settings" data-ar="ملف الشركة والإعدادات">Company Profile & Settings</h5>
|
|
<p class="text-muted small mb-0" data-en="Manage your business identity and system preferences" data-ar="إدارة هوية عملك وتفضيلات النظام">Manage your business identity and system preferences</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<a href="index.php?page=copy_outlet_data" class="btn btn-outline-primary btn-sm">
|
|
<i class="bi bi-arrow-repeat me-1"></i> <span data-en="Sync Outlets" data-ar="مزامنة الفروع">Sync Outlets</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<?php
|
|
$licenseIdentity = LicenseService::getClientIdentity();
|
|
$licenseSourceLabels = [
|
|
'settings' => '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;
|
|
?>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
|
|
<!-- Company Details Section -->
|
|
<div class="mb-5">
|
|
<h6 class="fw-bold text-primary mb-3 border-bottom pb-2">
|
|
<i class="bi bi-info-circle me-2"></i><span data-en="Company Details" data-ar="تفاصيل الشركة">Company Details</span>
|
|
</h6>
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label text-muted small fw-semibold" data-en="Company Name" data-ar="اسم الشركة">Company Name</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-building"></i></span>
|
|
<input type="text" name="settings[company_name]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($data['settings']['company_name'] ?? '') ?>" placeholder="e.g. Tech Solutions LLC">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label text-muted small fw-semibold" data-en="CTR No (Commercial Registration)" data-ar="رقم السجل التجاري">CTR No (Commercial Registration)</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-file-text"></i></span>
|
|
<input type="text" name="settings[ctr_no]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($data['settings']['ctr_no'] ?? '') ?>" placeholder="e.g. 1234567">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label text-muted small fw-semibold" data-en="VAT Number" data-ar="الرقم الضريبي">VAT Number</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-receipt"></i></span>
|
|
<input type="text" name="settings[vat_number]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($data['settings']['vat_number'] ?? '') ?>" placeholder="e.g. OM123456789">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contact Information Section -->
|
|
<div class="mb-5">
|
|
<h6 class="fw-bold text-primary mb-3 border-bottom pb-2">
|
|
<i class="bi bi-telephone me-2"></i><span data-en="Contact Information" data-ar="معلومات الاتصال">Contact Information</span>
|
|
</h6>
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label text-muted small fw-semibold" data-en="Phone Number" data-ar="رقم الهاتف">Phone Number</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-phone"></i></span>
|
|
<input type="text" name="settings[company_phone]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($data['settings']['company_phone'] ?? '') ?>" placeholder="+968 9999 9999">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label text-muted small fw-semibold" data-en="Email Address" data-ar="البريد الإلكتروني">Email Address</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-envelope"></i></span>
|
|
<input type="email" name="settings[company_email]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($data['settings']['company_email'] ?? '') ?>" placeholder="info@example.com">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label text-muted small fw-semibold" data-en="Physical Address" data-ar="العنوان الفعلي">Physical Address</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-geo-alt"></i></span>
|
|
<textarea name="settings[company_address]" class="form-control border-start-0 ps-0" rows="2" placeholder="Street, Building, City..."><?= htmlspecialchars($data['settings']['company_address'] ?? '') ?></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System Configuration Section -->
|
|
<div class="mb-5">
|
|
<h6 class="fw-bold text-primary mb-3 border-bottom pb-2">
|
|
<i class="bi bi-sliders me-2"></i><span data-en="System Configuration" data-ar="تكوين النظام">System Configuration</span>
|
|
</h6>
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label text-muted small fw-semibold" data-en="System Timezone" data-ar="المنطقة الزمنية للنظام">System Timezone</label>
|
|
<select name="settings[timezone]" class="form-select">
|
|
<?php
|
|
$tz_identifiers = DateTimeZone::listIdentifiers();
|
|
$current_tz = $data['settings']['timezone'] ?? date_default_timezone_get();
|
|
foreach ($tz_identifiers as $tz) {
|
|
$selected = ($tz === $current_tz) ? 'selected' : '';
|
|
echo "<option value=\"\"$tz\" $selected>$tz</option>";
|
|
}
|
|
?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label text-muted small fw-semibold" data-en="Stock Policy" data-ar="سياسة المخزون">Stock Policy</label>
|
|
<select name="settings[allow_zero_stock_sell]" class="form-select">
|
|
<option value="0" <?= ($data['settings']['allow_zero_stock_sell'] ?? '1') === '0' ? 'selected' : '' ?> data-en="Prevent selling out of stock" data-ar="منع البيع عند نفاذ المخزون">Prevent selling out of stock</option>
|
|
<option value="1" <?= ($data['settings']['allow_zero_stock_sell'] ?? '1') === '1' ? 'selected' : '' ?> data-en="Allow selling out of stock" data-ar="السماح بالبيع عند نفاذ المخزون">Allow selling out of stock</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label text-muted small fw-semibold" data-en="Scale Barcode Mode" data-ar="وضع باركود الميزان">Scale Barcode Mode</label>
|
|
<select name="settings[weight_barcode_mode]" class="form-select">
|
|
<option value="weight" <?= ($data['settings']['weight_barcode_mode'] ?? 'weight') === 'weight' ? 'selected' : '' ?> data-en="Use embedded weight" data-ar="استخدام الوزن">Use embedded weight</option>
|
|
<option value="price" <?= ($data['settings']['weight_barcode_mode'] ?? '') === 'price' ? 'selected' : '' ?> data-en="Use embedded price" data-ar="استخدام السعر">Use embedded price</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label text-muted small fw-semibold" data-en="Scale Prefix From" data-ar="بادئة الميزان من">Scale Prefix From</label>
|
|
<input type="number" min="20" max="29" name="settings[weight_barcode_prefix_start]" class="form-control" value="<?= htmlspecialchars($data['settings']['weight_barcode_prefix_start'] ?? '20') ?>">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label text-muted small fw-semibold" data-en="Scale Prefix To" data-ar="بادئة الميزان إلى">Scale Prefix To</label>
|
|
<input type="number" min="20" max="29" name="settings[weight_barcode_prefix_end]" class="form-control" value="<?= htmlspecialchars($data['settings']['weight_barcode_prefix_end'] ?? '29') ?>">
|
|
</div>
|
|
<div class="col-12">
|
|
<div class="form-text" data-en="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." data-ar="صيغة باركود الميزان 13 رقمًا: بادئة من رقمين + كود صنف من 5 أرقام + قيمة من 5 أرقام + رقم تحقق. الباركود الكامل 13 رقمًا محجوز ولا يمكن حفظه أو استيراده كصنف.">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.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- License Identity Section -->
|
|
<div class="mb-5">
|
|
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-3 border-bottom pb-2">
|
|
<h6 class="fw-bold text-primary m-0">
|
|
<i class="bi bi-shield-check me-2"></i><span data-en="License App Identity" data-ar="هوية ترخيص التطبيق">License App Identity</span>
|
|
</h6>
|
|
<span class="badge rounded-pill text-bg-light border">
|
|
<span data-en="Name source" data-ar="مصدر الاسم">Name source</span>: <?= htmlspecialchars($licenseSourceLabels[$licenseIdentity['app_name_source']] ?? 'Built-in fallback') ?>
|
|
·
|
|
<span data-en="Slug source" data-ar="مصدر المعرّف">Slug source</span>: <?= htmlspecialchars($licenseSourceLabels[$licenseIdentity['app_slug_source']] ?? 'Built-in fallback') ?>
|
|
</span>
|
|
</div>
|
|
<div class="alert alert-info border-0 shadow-sm small mb-3">
|
|
<strong data-en="Use one stable identity per product." data-ar="استخدم هوية ثابتة لكل منتج.">Use one stable identity per product.</strong>
|
|
<span data-en="This is the product identity your central license manager sees. Change the slug only when you intentionally rename or split a product." data-ar="هذه هي هوية المنتج التي يراها مدير التراخيص المركزي. غيّر المعرّف فقط عندما تقصد إعادة تسمية المنتج أو تقسيمه.">This is the product identity your central license manager sees. Change the slug only when you intentionally rename or split a product.</span>
|
|
</div>
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label text-muted small fw-semibold" data-en="App Name for Licensing" data-ar="اسم التطبيق للترخيص">App Name for Licensing</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-window"></i></span>
|
|
<input type="text" id="license-app-name" name="settings[license_app_name]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($licenseAppNameInput) ?>" placeholder="<?= htmlspecialchars((string)$licenseIdentity['app_name']) ?>">
|
|
</div>
|
|
<div class="form-text" data-en="Leave blank only if you want to fall back to environment/default values." data-ar="اتركه فارغًا فقط إذا كنت تريد الرجوع إلى قيم البيئة أو القيم الافتراضية.">Leave blank only if you want to fall back to environment/default values.</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label text-muted small fw-semibold" data-en="Stable App Slug" data-ar="المعرّف الثابت للتطبيق">Stable App Slug</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-tag"></i></span>
|
|
<input type="text" id="license-app-slug" name="settings[license_app_slug]" data-explicit="<?= $savedLicenseAppSlug !== '' ? '1' : '0' ?>" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($licenseAppSlugInput) ?>" placeholder="<?= htmlspecialchars((string)$licenseIdentity['app_slug']) ?>">
|
|
<button type="button" class="btn btn-outline-secondary" id="suggest-license-slug" data-en="Suggest" data-ar="اقتراح">Suggest</button>
|
|
</div>
|
|
<div class="form-text" data-en="Use one slug per product, not per customer or installation." data-ar="استخدم معرّفًا واحدًا لكل منتج، وليس لكل عميل أو تثبيت.">Use one slug per product, not per customer or installation.</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<div class="form-text">
|
|
<span data-en="Current effective identity:" data-ar="هوية التطبيق الفعالة حاليًا:">Current effective identity:</span>
|
|
<code><?= htmlspecialchars((string)$licenseIdentity['app_name']) ?></code>
|
|
·
|
|
<code><?= htmlspecialchars((string)$licenseIdentity['app_slug']) ?></code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Visual Identity Section -->
|
|
<div class="mb-5">
|
|
<h6 class="fw-bold text-primary mb-3 border-bottom pb-2">
|
|
<i class="bi bi-palette me-2"></i><span data-en="Visual Identity" data-ar="الهوية البصرية">Visual Identity</span>
|
|
</h6>
|
|
<div class="row g-4">
|
|
<div class="col-md-4">
|
|
<div class="card h-100 border-dashed bg-light text-center p-3">
|
|
<label class="form-label fw-semibold mb-2" data-en="Company Logo" data-ar="شعار الشركة">Company Logo</label>
|
|
<div class="mb-3 d-flex justify-content-center align-items-center" style="height: 100px;">
|
|
<?php if (!empty($data['settings']['company_logo'])): ?>
|
|
<img src="<?= htmlspecialchars($data['settings']['company_logo']) ?>?v=<?= time() ?>" alt="Logo" class="img-fluid" style="max-height: 80px;">
|
|
<?php else:
|
|
?>
|
|
<i class="bi bi-image text-muted fs-1"></i>
|
|
<?php endif; ?>
|
|
</div>
|
|
<input type="file" name="company_logo" class="form-control form-control-sm" accept="image/*">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card h-100 border-dashed bg-light text-center p-3">
|
|
<label class="form-label fw-semibold mb-2" data-en="Website Favicon" data-ar="أيقونة الموقع">Website Favicon</label>
|
|
<div class="mb-3 d-flex justify-content-center align-items-center" style="height: 100px;">
|
|
<?php if (!empty($data['settings']['favicon'])): ?>
|
|
<img src="<?= htmlspecialchars($data['settings']['favicon']) ?>?v=<?= time() ?>" alt="Favicon" class="img-fluid" style="max-height: 32px;">
|
|
<?php else:
|
|
?>
|
|
<i class="bi bi-globe text-muted fs-1"></i>
|
|
<?php endif; ?>
|
|
</div>
|
|
<input type="file" name="favicon" class="form-control form-control-sm" accept="image/*">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card h-100 border-dashed bg-light text-center p-3">
|
|
<label class="form-label fw-semibold mb-2" data-en="Manager Signature" data-ar="توقيع المدير">Manager Signature</label>
|
|
<div class="mb-3 d-flex justify-content-center align-items-center" style="height: 100px;">
|
|
<?php if (!empty($data['settings']['manager_signature'])): ?>
|
|
<img src="<?= htmlspecialchars($data['settings']['manager_signature']) ?>?v=<?= time() ?>" alt="Signature" class="img-fluid" style="max-height: 80px;">
|
|
<?php else:
|
|
?>
|
|
<i class="bi bi-pen text-muted fs-1"></i>
|
|
<?php endif; ?>
|
|
</div>
|
|
<input type="file" name="manager_signature" class="form-control form-control-sm" accept="image/*">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loyalty Configuration Section -->
|
|
<div class="mb-5">
|
|
<h6 class="fw-bold text-primary mb-3 border-bottom pb-2">
|
|
<i class="bi bi-award me-2"></i><span data-en="Loyalty Program" data-ar="برنامج الولاء">Loyalty Program</span>
|
|
</h6>
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label text-muted small fw-semibold" data-en="Loyalty Status" data-ar="حالة الولاء">Loyalty Status</label>
|
|
<select name="settings[loyalty_enabled]" class="form-select">
|
|
<option value="0" <?= ($data['settings']['loyalty_enabled'] ?? '0') === '0' ? 'selected' : '' ?> data-en="Disabled" data-ar="معطل">Disabled</option>
|
|
<option value="1" <?= ($data['settings']['loyalty_enabled'] ?? '0') === '1' ? 'selected' : '' ?> data-en="Active" data-ar="نشط">Active</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label text-muted small fw-semibold" data-en="Earning Rule (Points/1 OMR)" data-ar="قاعدة الكسب (نقاط/1 ريال)">Earning Rule (Points/1 OMR)</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-arrow-up-circle"></i></span>
|
|
<input type="number" step="0.01" name="settings[loyalty_points_per_unit]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($data['settings']['loyalty_points_per_unit'] ?? '1') ?>">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label text-muted small fw-semibold" data-en="Redemption Rule (Points/1 OMR)" data-ar="قاعدة الاسترداد (نقاط/1 ريال)">Redemption Rule (Points/1 OMR)</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light border-end-0"><i class="bi bi-arrow-down-circle"></i></span>
|
|
<input type="number" step="0.01" name="settings[loyalty_redeem_points_per_unit]" class="form-control border-start-0 ps-0" value="<?= htmlspecialchars($data['settings']['loyalty_redeem_points_per_unit'] ?? '100') ?>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex justify-content-end pt-3">
|
|
<button type="submit" name="update_settings" class="btn btn-primary btn-lg rounded-pill px-5 shadow-sm">
|
|
<i class="bi bi-check-lg me-2"></i> <span data-en="Save All Changes" data-ar="حفظ جميع التغييرات">Save All Changes</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
var nameInput = document.getElementById('license-app-name');
|
|
var slugInput = document.getElementById('license-app-slug');
|
|
var suggestButton = document.getElementById('suggest-license-slug');
|
|
if (!nameInput || !slugInput || !suggestButton) {
|
|
return;
|
|
}
|
|
|
|
var slugWasExplicit = slugInput.dataset.explicit === '1';
|
|
var slugify = function (value) {
|
|
return String(value || '')
|
|
.toLowerCase()
|
|
.trim()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '');
|
|
};
|
|
|
|
nameInput.addEventListener('input', function () {
|
|
if (!slugWasExplicit || slugInput.value.trim() === '') {
|
|
slugInput.value = slugify(nameInput.value);
|
|
}
|
|
});
|
|
|
|
slugInput.addEventListener('input', function () {
|
|
slugWasExplicit = slugInput.value.trim() !== '';
|
|
slugInput.dataset.explicit = slugWasExplicit ? '1' : '0';
|
|
});
|
|
|
|
suggestButton.addEventListener('click', function () {
|
|
slugInput.value = slugify(nameInput.value);
|
|
slugWasExplicit = slugInput.value.trim() !== '';
|
|
slugInput.dataset.explicit = slugWasExplicit ? '1' : '0';
|
|
slugInput.focus();
|
|
});
|
|
});
|
|
</script>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'role_groups'): ?>
|
|
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
|
|
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center border-0">
|
|
<div>
|
|
<h5 class="m-0 fw-bold text-primary" data-en="Role Groups" data-ar="مجموعات الأدوار">Role Groups</h5>
|
|
<p class="text-muted small mb-0" data-en="Manage access levels and permissions" data-ar="إدارة مستويات الوصول والصلاحيات">Manage access levels and permissions</p>
|
|
</div>
|
|
<button class="btn btn-primary rounded-pill px-4" data-bs-toggle="modal" data-bs-target="#addRoleGroupModal">
|
|
<i class="bi bi-shield-plus me-1"></i> <span data-en="Create New Group" data-ar="إنشاء مجموعة جديدة">Create New Group</span>
|
|
</button>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th class="ps-4" data-en="Group Name" data-ar="اسم المجموعة">Group Name</th>
|
|
<th data-en="Created Date" data-ar="تاريخ الإنشاء">Created Date</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th data-en="Actions" data-ar="الإجراءات" class="text-end pe-4">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['role_groups'] as $group): ?>
|
|
<tr>
|
|
<td class="ps-4">
|
|
<div class="d-flex align-items-center">
|
|
<div class="rounded-circle bg-primary bg-opacity-10 p-2 me-3 text-primary">
|
|
<i class="bi bi-shield-check"></i>
|
|
</div>
|
|
<span class="fw-semibold text-dark"><?= htmlspecialchars((string)$group['name']) ?></span>
|
|
</div>
|
|
</td>
|
|
<td><span class="text-muted small"><?= date('M d, Y', strtotime((string)$group['created_at'])) ?></span></td>
|
|
<td><span class="badge rounded-pill bg-success bg-opacity-10 text-success px-3">Active</span></td>
|
|
<td class="text-end pe-4">
|
|
<div class="dropdown">
|
|
<button class="btn btn-light btn-sm rounded-circle" type="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-three-dots-vertical"></i>
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
|
|
<?php if (can('users_edit')): ?>
|
|
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#editRoleGroupModal<?= $group['id'] ?>"><i class="bi bi-pencil me-2 text-primary"></i> Edit Permissions</a></li>
|
|
<?php endif; ?>
|
|
<?php if (can('users_delete')): ?>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li>
|
|
<form method="POST" onsubmit="return confirm('Delete this role group? This cannot be undone.')">
|
|
<input type="hidden" name="id" value="<?= $group['id'] ?>">
|
|
<button type="submit" name="delete_role_group" class="dropdown-item text-danger"><i class="bi bi-trash me-2"></i> Delete Group</button>
|
|
</form>
|
|
</li>
|
|
<?php endif; ?>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Edit Role Group Modal -->
|
|
<div class="modal fade" id="editRoleGroupModal<?= $group['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content border-0 shadow text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title fw-bold" data-en="Edit Role Group" data-ar="تعديل مجموعة الأدوار">Edit Role Group</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="id" value="<?= $group['id'] ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label fw-semibold" data-en="Group Name" data-ar="اسم المجموعة">Group Name</label>
|
|
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($group['name']) ?>" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<label class="form-label fw-semibold mb-0" data-en="Permissions" data-ar="الصلاحيات">Permissions</label>
|
|
<div class="d-flex gap-2">
|
|
<button type="button" class="btn btn-xs btn-outline-primary py-0 px-2 small select-all-btn" data-modal="#editRoleGroupModal<?= $group['id'] ?>">Select All</button>
|
|
<button type="button" class="btn btn-xs btn-outline-secondary py-0 px-2 small deselect-all-btn" data-modal="#editRoleGroupModal<?= $group['id'] ?>">Deselect All</button>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3 p-2 bg-light rounded d-flex justify-content-between align-items-center flex-wrap gap-2">
|
|
<span class="small fw-bold me-2">Global Actions:</span>
|
|
<div class="d-flex gap-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input select-all-action" type="checkbox" data-action="view" id="selectAllView<?= $group['id'] ?>">
|
|
<label class="form-check-label small" for="selectAllView<?= $group['id'] ?>">View</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input select-all-action" type="checkbox" data-action="add" id="selectAllAdd<?= $group['id'] ?>">
|
|
<label class="form-check-label small" for="selectAllAdd<?= $group['id'] ?>">Add</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input select-all-action" type="checkbox" data-action="edit" id="selectAllEdit<?= $group['id'] ?>">
|
|
<label class="form-check-label small" for="selectAllEdit<?= $group['id'] ?>" data-en="Edit" data-ar="تعديل">Edit</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input select-all-action" type="checkbox" data-action="delete" id="selectAllDelete<?= $group['id'] ?>">
|
|
<label class="form-check-label small" for="selectAllDelete<?= $group['id'] ?>" data-en="Delete" data-ar="حذف">Delete</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row overflow-auto pe-2" style="max-height: 500px;">
|
|
<?php
|
|
$stmtP = db()->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): ?>
|
|
<div class="permission-group-container col-12 mb-4">
|
|
<div class="mt-3 mb-2 bg-secondary bg-opacity-10 p-2 d-flex justify-content-between align-items-center rounded border-start border-primary border-3">
|
|
<span class="fw-bold text-uppercase small text-primary"><?= $group_name ?></span>
|
|
<div class="form-check mb-0">
|
|
<input class="form-check-input select-all-group" type="checkbox" id="group_<?= $group['id'] ?>_<?= strtolower(str_replace(' ', '_', $group_name)) ?>">
|
|
<label class="form-check-label small fw-bold" for="group_<?= $group['id'] ?>_<?= strtolower(str_replace(' ', '_', $group_name)) ?>">Group All</label>
|
|
</div>
|
|
</div>
|
|
<div class="row g-3">
|
|
<?php foreach ($modules as $m => $label): ?>
|
|
<div class="col-md-6 mb-2 border-bottom pb-2 module-row">
|
|
<div class="small fw-bold mb-2 text-dark border-start border-2 ps-2 border-info d-flex justify-content-between align-items-center">
|
|
<span><?= $label ?></span>
|
|
<div class="form-check mb-0">
|
|
<input class="form-check-input select-all-row" type="checkbox" id="row_all_<?= $group['id'] ?>_<?= $m ?>">
|
|
<label class="form-check-label smaller text-muted mb-0 ms-1" style="font-size: 0.7rem;" for="row_all_<?= $group['id'] ?>_<?= $m ?>">Select All</label>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex gap-3 flex-wrap ps-2">
|
|
<?php foreach (['view', 'add', 'edit', 'delete'] as $a):
|
|
$p = $m . '_' . $a;
|
|
?>
|
|
<div class="form-check">
|
|
<input class="form-check-input perm-check" type="checkbox" name="permissions[]" value="<?= $p ?>" data-action="<?= $a ?>" id="perm_<?= $group['id'] ?>_<?= $p ?>" <?= in_array($p, (array)$perms) ? 'checked' : '' ?>>
|
|
<label class="form-check-label small" for="perm_<?= $group['id'] ?>_<?= $p ?>"><?= ucfirst($a) ?></label>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light rounded-pill px-3" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_role_group" class="btn btn-primary rounded-pill px-4" data-en="Update" data-ar="تحديث">Update</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
<?php require 'pages/users_role_permissions_script.php'; ?>
|
|
<?php elseif ($page === 'customer_display_settings'): ?>
|
|
<div class="card p-4">
|
|
<h5 class="mb-4" data-en="Customer Display Settings" data-ar="إعدادات شاشة العميل">Customer Display Settings</h5>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
|
|
<div class="form-grid-3">
|
|
<div class="col-md-12">
|
|
<h6 class="text-muted border-bottom pb-2" data-en="Greeting Message" data-ar="رسالة الترحيب">Greeting Message</h6>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Title" data-ar="العنوان">Title</label>
|
|
<input type="text" name="settings[customer_display_greeting_title]" class="form-control" value="<?= htmlspecialchars($data['settings']['customer_display_greeting_title'] ?? 'Welcome') ?>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Subtitle" data-ar="العنوان الفرعي">Subtitle</label>
|
|
<input type="text" name="settings[customer_display_greeting_text]" class="form-control" value="<?= htmlspecialchars($data['settings']['customer_display_greeting_text'] ?? 'We are ready to serve you.') ?>">
|
|
</div>
|
|
|
|
<div class="col-md-12 mt-4">
|
|
<h6 class="text-muted border-bottom pb-2" data-en="Slideshow Images" data-ar="صور العرض">Slideshow Images</h6>
|
|
<p class="text-muted small">Upload images for the customer display slideshow (1920x1080 recommended).</p>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<label class="form-label">Slide 1</label>
|
|
<input type="file" name="display_slide_1" class="form-control" accept="image/*">
|
|
<?php if (!empty($data['settings']['display_slide_1'])): ?>
|
|
<div class="mt-2 position-relative">
|
|
<img src="<?= htmlspecialchars($data['settings']['display_slide_1']) ?>?v=<?= time() ?>" alt="Slide 1" class="img-thumbnail" style="max-height: 120px;">
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<label class="form-label">Slide 2</label>
|
|
<input type="file" name="display_slide_2" class="form-control" accept="image/*">
|
|
<?php if (!empty($data['settings']['display_slide_2'])): ?>
|
|
<div class="mt-2 position-relative">
|
|
<img src="<?= htmlspecialchars($data['settings']['display_slide_2']) ?>?v=<?= time() ?>" alt="Slide 2" class="img-thumbnail" style="max-height: 120px;">
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<label class="form-label">Slide 3</label>
|
|
<input type="file" name="display_slide_3" class="form-control" accept="image/*">
|
|
<?php if (!empty($data['settings']['display_slide_3'])): ?>
|
|
<div class="mt-2 position-relative">
|
|
<img src="<?= htmlspecialchars($data['settings']['display_slide_3']) ?>?v=<?= time() ?>" alt="Slide 3" class="img-thumbnail" style="max-height: 120px;">
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="col-md-12 mt-4">
|
|
<button type="submit" name="update_settings" class="btn btn-primary">
|
|
<i class="bi bi-save"></i> <span data-en="Save Changes" data-ar="حفظ التغييرات">Save Changes</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'backups'): ?>
|
|
<div class="row g-4">
|
|
<div class="col-md-4">
|
|
<div class="card border-0 shadow-sm mb-4">
|
|
<div class="card-header bg-white py-3 border-0">
|
|
<h5 class="card-title mb-0" data-en="Backup Settings" data-ar="إعدادات النسخ الاحتياطي">Backup Settings</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="POST">
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-semibold" data-en="Keep Last N Backups" data-ar="الاحتفاظ بآخر N نسخ">Keep Last N Backups</label>
|
|
<input type="number" name="backup_limit" class="form-control" value="<?= htmlspecialchars($data['backup_settings']['backup_limit'] ?? '5') ?>" min="1" max="50">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-semibold" data-en="Auto Backup Time" data-ar="وقت النسخ التلقائي">Auto Backup Time</label>
|
|
<input type="time" name="backup_time" class="form-control" value="<?= htmlspecialchars($data['backup_settings']['backup_time'] ?? '00:00') ?>">
|
|
</div>
|
|
<div class="mb-4">
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" name="backup_auto_enabled" value="1" id="autoBackupSwitch" <?= ($data['backup_settings']['backup_auto_enabled'] ?? '0') === '1' ? 'checked' : '' ?>>
|
|
<label class="form-check-label small fw-semibold" for="autoBackupSwitch" data-en="Enable Automated Backups" data-ar="تفعيل النسخ الاحتياطي التلقائي">Enable Automated Backups</label>
|
|
</div>
|
|
<small class="text-muted" data-en="Requires a cron job running cron_backup.php every minute to respect the scheduled time." data-ar="يتطلب تعيين مهمة مجدولة (cron) لتشغيل cron_backup.php كل دقيقة للالتزام بالوقت المحدد.">Requires a cron job running cron_backup.php every minute to respect the scheduled time.</small>
|
|
</div>
|
|
<button type="submit" name="save_backup_settings" class="btn btn-primary w-100" data-en="Save Settings" data-ar="حفظ الإعدادات">Save Settings</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-body text-center py-4">
|
|
<div class="bg-primary bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 60px; height: 60px;">
|
|
<i class="bi bi-cloud-upload text-primary fs-3"></i>
|
|
</div>
|
|
<h5 data-en="Manual Backup" data-ar="نسخ يدوي">Manual Backup</h5>
|
|
<p class="text-muted small" data-en="Create a database backup immediately." data-ar="إنشاء نسخة احتياطية من قاعدة البيانات فوراً.">Create a database backup immediately.</p>
|
|
<form method="POST">
|
|
<button type="submit" name="create_backup" class="btn btn-outline-primary" data-en="Backup Now" data-ar="نسخ الآن">Backup Now</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-8">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center">
|
|
<h5 class="card-title mb-0" data-en="Available Backups" data-ar="النسخ المتاحة">Available Backups</h5>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th class="ps-4" data-en="Filename" data-ar="اسم الملف">Filename</th>
|
|
<th data-en="Size" data-ar="الحجم">Size</th>
|
|
<th data-en="Date" data-ar="التاريخ">Date</th>
|
|
<th class="text-end pe-4" data-en="Actions" data-ar="الإجراءات">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($data['backups'])): ?>
|
|
<tr>
|
|
<td colspan="4" class="text-center py-4 text-muted" data-en="No backups found." data-ar="لا توجد نسخ احتياطية.">No backups found.</td>
|
|
</tr>
|
|
<?php else: ?>
|
|
<?php foreach ($data['backups'] as $b): ?>
|
|
<tr>
|
|
<td class="ps-4 fw-medium"><?= htmlspecialchars($b['name']) ?></td>
|
|
<td><?= htmlspecialchars($b['size']) ?></td>
|
|
<td><?= htmlspecialchars($b['date']) ?></td>
|
|
<td class="text-end pe-4">
|
|
<div class="btn-group">
|
|
<a href="index.php?download_backup=<?= urlencode($b['name']) ?>" class="btn btn-sm btn-outline-primary" title="Local Download" data-en="Local Download" data-ar="تحميل محلي"><i class="fas fa-download"></i></a>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm(document.documentElement.lang === 'ar' ? 'هل تريد استعادة هذه النسخة؟ سيتم الكتابة فوق البيانات الحالية!' : 'Restore this backup? Current data will be overwritten!');">
|
|
<input type="hidden" name="filename" value="<?= htmlspecialchars($b['name']) ?>">
|
|
<button type="submit" name="restore_backup" class="btn btn-sm btn-outline-success ms-1" title="Restore" data-en="Restore" data-ar="استعادة"><i class="bi bi-arrow-counterclockwise"></i></button>
|
|
</form>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm(document.documentElement.lang === 'ar' ? 'هل تريد حذف هذه النسخة نهائياً؟' : 'Permanently delete this backup?');">
|
|
<input type="hidden" name="filename" value="<?= htmlspecialchars($b['name']) ?>">
|
|
<button type="submit" name="delete_backup" class="btn btn-sm btn-outline-danger ms-1" title="Delete" data-en="Delete" data-ar="حذف"><i class="bi bi-trash"></i></button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'users'): ?>
|
|
<?php require 'pages/users_view.php'; ?>
|
|
<?php elseif ($page === 'cash_registers'): ?>
|
|
<div class="card p-4 rounded-4 shadow-sm border-0">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h5 class="m-0 fw-bold text-primary" data-en="Cash Registers Management" data-ar="إدارة خزائن الكاشير">Cash Registers Management</h5>
|
|
<p class="text-muted small mb-0">Define your shop counters and registers.</p>
|
|
<div class="mt-2">
|
|
<?php
|
|
$allowed_acts = LicenseService::getAllowedActivations();
|
|
$current_regs = count($data['cash_registers'] ?? []);
|
|
?>
|
|
<span class="badge bg-light text-dark border">
|
|
<i class="bi bi-info-circle me-1"></i>
|
|
<span data-en="License Limit:" data-ar="حد الترخيص:">License Limit:</span> <?= $current_regs ?> / <?= $allowed_acts ?>
|
|
<span data-en="Registers" data-ar="خزينة">Registers</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-primary rounded-pill px-4" data-bs-toggle="modal" data-bs-target="#addRegisterModal">
|
|
<i class="bi bi-plus-lg me-2"></i> <span data-en="Add Register" data-ar="إضافة خزينة">Add Register</span>
|
|
</button>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="ID" data-ar="المعرف">ID</th>
|
|
<th data-en="Name" data-ar="الاسم">Name</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th data-en="Created At" data-ar="تاريخ الإنشاء">Created At</th>
|
|
<th class="text-end" data-en="Actions" data-ar="الإجراءات">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['cash_registers'] as $r): ?>
|
|
<tr>
|
|
<td>#<?= $r['id'] ?></td>
|
|
<td class="fw-bold"><?= htmlspecialchars($r['name']) ?></td>
|
|
<td>
|
|
<span class="badge rounded-pill <?= $r['status'] === 'active' ? 'bg-success' : 'bg-danger' ?>">
|
|
<?= ucfirst($r['status']) ?>
|
|
</span>
|
|
</td>
|
|
<td><?= $r['created_at'] ?></td>
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-light rounded-circle" data-bs-toggle="modal" data-bs-target="#editRegisterModal<?= $r['id'] ?>"><i class="bi bi-pencil text-primary"></i></button>
|
|
<form method="POST" class="d-inline" onsubmit="return confirm('Are you sure?')">
|
|
<input type="hidden" name="id" value="<?= $r['id'] ?>">
|
|
<button type="submit" name="delete_cash_register" class="btn btn-sm btn-light rounded-circle"><i class="bi bi-trash text-danger"></i></button>
|
|
</form>
|
|
|
|
<!-- Edit Modal -->
|
|
<div class="modal fade" id="editRegisterModal<?= $r['id'] ?>" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Edit Register</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body text-start">
|
|
<input type="hidden" name="id" value="<?= $r['id'] ?>">
|
|
<div class="mb-3">
|
|
<label class="form-label">Register Name</label>
|
|
<input type="text" name="name" class="form-control" value="<?= htmlspecialchars($r['name']) ?>" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Status" data-ar="الحالة">Status</label>
|
|
<select name="status" class="form-select">
|
|
<option value="active" <?= $r['status'] === 'active' ? 'selected' : '' ?>>Active</option>
|
|
<option value="inactive" <?= $r['status'] === 'inactive' ? 'selected' : '' ?>>Inactive</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="edit_cash_register" class="btn btn-primary">Save Changes</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<!-- Add Modal -->
|
|
<div class="modal fade" id="addRegisterModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Add Cash Register</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Register Name</label>
|
|
<input type="text" name="name" class="form-control" placeholder="Counter 1" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_cash_register" class="btn btn-primary">Add Register</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php elseif ($page === 'register_sessions'): ?>
|
|
<div class="card p-4 rounded-4 shadow-sm border-0 mb-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h5 class="m-0 fw-bold text-primary"><span data-en="Register Sessions" data-ar="جلسات الكاشير">Register Sessions</span> (<?= count($data['sessions']) ?>)</h5>
|
|
<p class="text-muted small mb-0">Manage daily opening and closing of cash registers.</p>
|
|
</div>
|
|
<div>
|
|
<?php
|
|
$active_session = db()->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();
|
|
?>
|
|
<?php if (!$session): ?>
|
|
<button class="btn btn-success rounded-pill px-4" data-bs-toggle="modal" data-bs-target="#openRegisterModal">
|
|
<i class="bi bi-unlock me-2"></i> <span data-en="Open Register" data-ar="فتح الخزينة">Open Register</span>
|
|
</button>
|
|
<?php else: ?>
|
|
<button class="btn btn-danger rounded-pill px-4" data-bs-toggle="modal" data-bs-target="#closeRegisterModal">
|
|
<i class="bi bi-lock me-2"></i> <span data-en="Close Register" data-ar="إغلاق الخزينة">Close Register</span>
|
|
</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($session): ?>
|
|
<div class="alert alert-info border-0 shadow-sm d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<i class="bi bi-info-circle-fill me-2"></i>
|
|
Current Open Register: <strong><?= htmlspecialchars($session['register_name']) ?></strong> |
|
|
Opened At: <strong><?= $session['opened_at'] ?></strong> |
|
|
Opening Balance: <strong>OMR <?= number_format((float)$session['opening_balance'], 3) ?></strong>
|
|
</div>
|
|
<div>
|
|
<a href="index.php?page=pos" class="btn btn-sm btn-primary rounded-pill px-3">Go to POS</a>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Filter Form -->
|
|
<div class="card border-0 bg-light rounded-4 mb-4">
|
|
<div class="card-body">
|
|
<form action="index.php" method="GET" class="row g-3 align-items-end">
|
|
<input type="hidden" name="page" value="register_sessions">
|
|
|
|
<?php if (can('users_view')): ?>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="Cashier" data-ar="الكاشير">Cashier</label>
|
|
<select name="user_id" class="form-select rounded-3">
|
|
<option value="" data-en="All Cashiers" data-ar="كل الكاشير">All Cashiers</option>
|
|
<?php foreach ($data['users'] as $u): ?>
|
|
<option value="<?= $u['id'] ?>" <?= (isset($_GET['user_id']) && $_GET['user_id'] == $u['id']) ? 'selected' : '' ?>>
|
|
<?= htmlspecialchars($u['username']) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="Date From" data-ar="من تاريخ">Date From</label>
|
|
<input type="date" name="date_from" class="form-control rounded-3" value="<?= htmlspecialchars($_GET['date_from'] ?? '') ?>">
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold" data-en="Date To" data-ar="إلى تاريخ">Date To</label>
|
|
<input type="date" name="date_to" class="form-control rounded-3" value="<?= htmlspecialchars($_GET['date_to'] ?? '') ?>">
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="d-flex gap-2">
|
|
<button type="submit" class="btn btn-primary rounded-3 w-100">
|
|
<i class="bi bi-filter me-1"></i> <span data-en="Filter" data-ar="تصفية">Filter</span>
|
|
</button>
|
|
<a href="index.php?page=register_sessions" class="btn btn-outline-secondary rounded-3 w-100">
|
|
<i class="bi bi-x-circle me-1"></i> <span data-en="Clear" data-ar="مسح">Clear</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle">
|
|
<thead>
|
|
<tr>
|
|
<th data-en="ID" data-ar="المعرف">ID</th>
|
|
<th data-en="Register" data-ar="الخزينة">Register</th>
|
|
<th data-en="Cashier" data-ar="الكاشير">Cashier</th>
|
|
<th data-en="Opened At" data-ar="وقت الفتح">Opened At</th>
|
|
<th data-en="Closed At" data-ar="وقت الإغلاق">Closed At</th>
|
|
<th data-en="Opening Bal." data-ar="رصيد الافتتاح">Opening Bal.</th>
|
|
<th data-en="Cash Sale" data-ar="مبيعات نقدية">Cash Sale</th>
|
|
<th data-en="Credit Card" data-ar="بطاقة ائتمان">Credit Card</th>
|
|
<th data-en="Credit" data-ar="آجل">Credit</th>
|
|
<th data-en="Total Sale" data-ar="إجمالي المبيعات">Total Sale</th>
|
|
<th data-en="Balance" data-ar="الرصيد">Balance</th>
|
|
<th data-en="Status" data-ar="الحالة">Status</th>
|
|
<th class="text-end" data-en="Report" data-ar="تقرير">Report</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($data['sessions'] as $s): ?>
|
|
<tr>
|
|
<td>#<?= $s['id'] ?></td>
|
|
<td class="fw-bold"><?= htmlspecialchars($s['register_name'] ?? 'N/A') ?></td>
|
|
<td><?= htmlspecialchars($s['username'] ?? 'N/A') ?></td>
|
|
<td><?= $s['opened_at'] ?></td>
|
|
<td><?= $s['closed_at'] ?? '---' ?></td>
|
|
<td>OMR <?= number_format((float)$s['opening_balance'], 3) ?></td>
|
|
<?php
|
|
$stats_stmt = db()->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;
|
|
?>
|
|
<td>OMR <?= number_format((float)$c_total, 3) ?></td>
|
|
<td>OMR <?= number_format((float)$cd_total, 3) ?></td>
|
|
<td>OMR <?= number_format((float)$cr_total, 3) ?></td>
|
|
<td class="fw-bold">OMR <?= number_format((float)$t_sales, 3) ?></td>
|
|
<td>
|
|
<?php if ($s['status'] === 'closed'):
|
|
$diff = (float)($s['cash_in_hand'] ?? 0) - $row_expected_cash;
|
|
$color = $diff == 0 ? 'text-success' : ($diff > 0 ? 'text-info' : 'text-danger');
|
|
?>
|
|
<span class="<?= $color ?> fw-bold">OMR <?= number_format((float)$diff, 3) ?></span>
|
|
<?php else: ?>---<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<span class="badge rounded-pill <?= $s['status'] === 'open' ? 'bg-success' : 'bg-secondary' ?>">
|
|
<?= ucfirst($s['status']) ?>
|
|
</span>
|
|
</td>
|
|
|
|
<td class="text-end">
|
|
<button class="btn btn-sm btn-light rounded-circle" onclick="loadSessionReport(<?= $s['id'] ?>)"><i class="bi bi-file-earmark-text text-primary"></i></button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?= renderPagination($data['current_page'] ?? 1, $data['total_pages'] ?? 1) ?>
|
|
|
|
</div>
|
|
|
|
<!-- Modals outside loop for better structure -->
|
|
<!-- Session Report Modal (Universal) -->
|
|
<div class="modal fade" id="universalSessionReportModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content border-0 shadow">
|
|
<div id="sessionReportContent">
|
|
<div class="modal-body text-center p-5">
|
|
<div class="spinner-border text-primary"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php require 'pages/register_session_report_script.php'; ?>
|
|
|
|
<!-- Open Register Modal -->
|
|
<div class="modal fade" id="openRegisterModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Open Register Session</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Select Register / Counter</label>
|
|
<select name="register_id" class="form-select" required>
|
|
<?php foreach ($data['cash_registers'] as $reg): ?>
|
|
<option value="<?= $reg['id'] ?>"><?= htmlspecialchars($reg['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Opening Cash Balance (OMR)</label>
|
|
<input type="number" step="0.001" name="opening_balance" class="form-control" value="0.000" required>
|
|
<div class="form-text">Enter the amount of cash already in the drawer.</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="open_register" class="btn btn-success">Open Register</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Close Register Modal -->
|
|
<?php if ($session): ?>
|
|
<div class="modal fade" id="closeRegisterModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Close Register Session (#<?= $session['id'] ?>)</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<input type="hidden" name="session_id" value="<?= $session['id'] ?>">
|
|
<input type="hidden" name="redirect_to" value="register_sessions">
|
|
<div class="alert alert-warning py-2 small border-0 shadow-sm">
|
|
<i class="bi bi-exclamation-triangle me-2"></i> Before closing, please count all cash in your register.
|
|
</div>
|
|
|
|
<?php
|
|
// Unified breakdown for closing
|
|
$curBreakdown = db()->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;
|
|
?>
|
|
<div class="card bg-light border-0 mb-3 small">
|
|
<div class="card-body">
|
|
<h6 class="card-title fw-bold small text-uppercase mb-2">Session Summary</h6>
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span class="text-muted">Opening Balance:</span>
|
|
<span class="fw-bold">OMR <?= number_format((float)$session['opening_balance'], 3) ?></span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span class="text-muted">Cash Sales:</span>
|
|
<span class="fw-bold">OMR <?= number_format((float)$cash_sales, 3) ?></span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span class="text-muted">Credit Card Sales:</span>
|
|
<span class="fw-bold">OMR <?= number_format((float)$card_sales, 3) ?></span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span class="text-muted">Credit:</span>
|
|
<span class="fw-bold">OMR <?= number_format((float)$credit_sales, 3) ?></span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span class="text-muted">Bank Transfer:</span>
|
|
<span class="fw-bold">OMR <?= number_format((float)$bank_transfer_sales, 3) ?></span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-1 border-top pt-1 mt-1 fw-bold text-dark">
|
|
<span>Total Sales:</span>
|
|
<span>OMR <?= number_format((float)$total_sales, 3) ?></span>
|
|
</div>
|
|
<hr class="my-2">
|
|
<div class="d-flex justify-content-between fw-bold text-primary mb-1 h6">
|
|
<span>Balance (Total):</span>
|
|
<span>OMR <?= number_format((float)$total_all, 3) ?></span>
|
|
</div>
|
|
<div class="d-flex justify-content-between fw-bold text-success">
|
|
<span>Expected Cash:</span>
|
|
<span>OMR <?= number_format((float)$expected_cash, 3) ?></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<h6 class="fw-bold small text-uppercase mb-2">Transaction Details</h6>
|
|
<div class="table-responsive" style="max-height: 200px; overflow-y: auto;">
|
|
<table class="table table-sm table-bordered mb-0 small">
|
|
<thead class="bg-light sticky-top">
|
|
<tr>
|
|
<th>Time</th>
|
|
<th>Order #</th>
|
|
<th data-en="Customer" data-ar="العميل">Customer</th>
|
|
<th>Method</th>
|
|
<th class="text-end" data-en="Amount" data-ar="المبلغ">Amount</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php
|
|
// Fetch from invoices
|
|
$txs_stmt = db()->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):
|
|
?>
|
|
<tr>
|
|
<td><?= date('H:i', strtotime($tx['created_at'])) ?></td>
|
|
<td><?= htmlspecialchars($tx['transaction_no']) ?></td>
|
|
<td><?= htmlspecialchars($tx['customer_name'] ?: 'Walk-in') ?></td>
|
|
<td class="text-capitalize small"><?= htmlspecialchars($tx['methods'] ?: '---') ?></td>
|
|
<td class="text-end"><?= number_format($tx['total_with_vat'], 3) ?></td>
|
|
</tr>
|
|
<?php endforeach; if(empty($txs)): ?>
|
|
<tr><td colspan="5" class="text-center text-muted">No transactions</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Total Cash in Hand (Actual Counted)</label>
|
|
<input type="number" step="0.001" name="cash_in_hand" class="form-control" required placeholder="0.000">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Closing Notes / Comments</label>
|
|
<textarea name="notes" class="form-control" rows="3" placeholder="Any discrepancies or remarks..."></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="close_register" class="btn btn-danger">Close Register & Submit</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php elseif ($page === 'logs'): ?>
|
|
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
|
|
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center border-0">
|
|
<div>
|
|
<h5 class="m-0 fw-bold text-primary" data-en="System Logs" data-ar="سجلات النظام">System Logs</h5>
|
|
<p class="text-muted small mb-0" data-en="Monitor system activity and errors" data-ar="مراقبة نشاط النظام والأخطاء">Monitor system activity and errors</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button onclick="window.location.reload()" class="btn btn-outline-primary btn-sm rounded-pill">
|
|
<i class="bi bi-arrow-clockwise"></i> Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="bg-dark text-light p-4 font-monospace small" style="max-height: 600px; overflow-y: auto;">
|
|
<?php
|
|
$log_files = ['runtime_debug.log', 'error_log', 'login_debug.log', 'post_debug.log', 'search_debug.log', 'debug.log'];
|
|
$found_logs = false;
|
|
foreach ($log_files as $file) {
|
|
$path = __DIR__ . '/' . $file;
|
|
if (file_exists($path) && is_readable($path)) {
|
|
$found_logs = true;
|
|
echo "<div class='mb-4'><h6 class='text-warning border-bottom border-secondary pb-2'>--- " . htmlspecialchars(basename($file)) . " ---</h6>";
|
|
$lines = shell_exec("tail -n 50 " . escapeshellarg($path));
|
|
echo "<pre class='mb-0 text-info'>" . htmlspecialchars((string)$lines) . "</pre></div>";
|
|
}
|
|
}
|
|
if (!$found_logs) {
|
|
echo "<div class='text-center py-5'><i class='bi bi-journal-x fs-1 opacity-25'></i><p class='mt-2 opacity-50'>No accessible log files found.</p></div>";
|
|
}
|
|
?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Add HR Department Modal -->
|
|
<div class="modal fade" id="addHrDepartmentModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Add HR Department" data-ar="إضافة قسم">Add HR Department</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Department Name" data-ar="اسم القسم">Department Name</label>
|
|
<input type="text" name="name" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_hr_department" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add HR Employee Modal -->
|
|
<div class="modal fade" id="addHrEmployeeModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Add HR Employee" data-ar="إضافة موظف">Add HR Employee</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<div class="form-grid-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Full Name" data-ar="الاسم الكامل">Full Name</label>
|
|
<input type="text" name="name" class="form-control" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Department" data-ar="القسم">Department</label>
|
|
<select name="department_id" class="form-select">
|
|
<option value="">--- Select ---</option>
|
|
<?php foreach ($data['departments'] ?? [] as $d): ?>
|
|
<option value="<?= $d['id'] ?>"><?= htmlspecialchars($d['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Email" data-ar="البريد">Email</label>
|
|
<input type="email" name="email" class="form-control">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" data-en="Phone" data-ar="الهاتف">Phone</label>
|
|
<input type="text" name="phone" class="form-control">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Position" data-ar="المنصب">Position</label>
|
|
<input type="text" name="position" class="form-control">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Basic Salary" data-ar="الراتب الأساسي">Basic Salary</label>
|
|
<input type="number" step="0.001" name="salary" class="form-control" value="0.000">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Biometric ID" data-ar="معرف البصمة">Biometric ID</label>
|
|
<input type="text" name="biometric_id" class="form-control">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" data-en="Joining Date" data-ar="تاريخ الانضمام">Joining Date</label>
|
|
<input type="date" name="joining_date" class="form-control" value="<?= date('Y-m-d') ?>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_hr_employee" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Role Group Modal -->
|
|
<div class="modal fade" id="addRoleGroupModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Add Role Group" data-ar="إضافة مجموعة أدوار">Add Role Group</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Group Name" data-ar="اسم المجموعة">Group Name</label>
|
|
<input type="text" name="name" class="form-control" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<label class="form-label fw-semibold mb-0" data-en="Permissions" data-ar="الصلاحيات">Permissions</label>
|
|
<div class="d-flex gap-2">
|
|
<button type="button" class="btn btn-xs btn-outline-primary py-0 px-2 small select-all-btn" data-modal="#addRoleGroupModal">Select All</button>
|
|
<button type="button" class="btn btn-xs btn-outline-secondary py-0 px-2 small deselect-all-btn" data-modal="#addRoleGroupModal">Deselect All</button>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3 p-2 bg-light rounded d-flex justify-content-between align-items-center flex-wrap gap-2">
|
|
<span class="small fw-bold me-2">Global Actions:</span>
|
|
<div class="d-flex gap-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input select-all-action" type="checkbox" data-action="view" id="addSelectAllView">
|
|
<label class="form-check-label small" for="addSelectAllView" data-en="View" data-ar="عرض">View</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input select-all-action" type="checkbox" data-action="add" id="addSelectAllAdd">
|
|
<label class="form-check-label small" for="addSelectAllAdd" data-en="Add" data-ar="إضافة">Add</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input select-all-action" type="checkbox" data-action="edit" id="addSelectAllEdit">
|
|
<label class="form-check-label small" for="addSelectAllEdit" data-en="Edit" data-ar="تعديل" data-en="Edit" data-ar="تعديل">Edit</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input select-all-action" type="checkbox" data-action="delete" id="addSelectAllDelete">
|
|
<label class="form-check-label small" for="addSelectAllDelete" data-en="Delete" data-ar="حذف" data-en="Delete" data-ar="حذف">Delete</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="row overflow-auto pe-2" style="max-height: 500px;">
|
|
<?php
|
|
foreach ($permission_groups as $group_name => $modules): ?>
|
|
<div class="permission-group-container col-12 mb-4">
|
|
<div class="mt-3 mb-2 bg-secondary bg-opacity-10 p-2 d-flex justify-content-between align-items-center rounded border-start border-primary border-3">
|
|
<span class="fw-bold text-uppercase small text-primary"><?= $group_name ?></span>
|
|
<div class="form-check mb-0">
|
|
<input class="form-check-input select-all-group" type="checkbox" id="add_group_<?= strtolower(str_replace(' ', '_', $group_name)) ?>">
|
|
<label class="form-check-label small fw-bold" for="add_group_<?= strtolower(str_replace(' ', '_', $group_name)) ?>">Group All</label>
|
|
</div>
|
|
</div>
|
|
<div class="row g-3">
|
|
<?php foreach ($modules as $m => $label): ?>
|
|
<div class="col-md-6 mb-2 border-bottom pb-2">
|
|
<div class="small fw-bold mb-2 text-dark border-start border-2 ps-2 border-info"><?= $label ?></div>
|
|
<div class="d-flex gap-3 flex-wrap ps-2">
|
|
<?php foreach (['view', 'add', 'edit', 'delete'] as $a):
|
|
$p = $m . '_' . $a;
|
|
?>
|
|
<div class="form-check">
|
|
<input class="form-check-input perm-check" type="checkbox" name="permissions[]" value="<?= $p ?>" data-action="<?= $a ?>" id="add_perm_<?= $p ?>">
|
|
<label class="form-check-label small" for="add_perm_<?= $p ?>"><?= ucfirst($a) ?></label>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_role_group" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add User Modal -->
|
|
|
|
|
|
<!-- Add Customer Modal -->
|
|
<div class="modal fade" id="addCustomerModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<?php if ($page === 'suppliers'): ?>
|
|
<span data-en="Add New Supplier" data-ar="إضافة مورد جديد">Add New Supplier</span>
|
|
<?php else: ?>
|
|
<span data-en="Add New Customer" data-ar="إضافة عميل جديد">Add New Customer</span>
|
|
<?php endif; ?>
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="type" value="<?= $page === 'suppliers' ? 'supplier' : 'customer' ?>">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Name" data-ar="الاسم">Name</label>
|
|
<input type="text" name="name" class="form-control" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Email" data-ar="البريد الإلكتروني">Email</label>
|
|
<input type="email" name="email" class="form-control">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Phone" data-ar="الهاتف">Phone</label>
|
|
<input type="text" name="phone" class="form-control">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Tax ID / VAT No" data-ar="الرقم الضريبي">Tax ID / VAT No</label>
|
|
<input type="text" name="tax_id" class="form-control">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Initial Balance" data-ar="الرصيد الافتتاحي">Initial Balance</label>
|
|
<input type="number" step="0.001" name="balance" class="form-control" value="0.000">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_customer" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Item Modal -->
|
|
<div class="modal fade" id="addItemModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Add New Item" data-ar="إضافة صنف جديد">Add New Item</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="modal-body">
|
|
<div class="form-grid-3">
|
|
<div class="col-md-4"><label class="form-label" data-en="Name (EN)" data-ar="الاسم (إنجليزي)">Name (EN)</label><div class="input-group"><input type="text" name="name_en" id="addItemNameEn" class="form-control" required><button class="btn btn-outline-secondary btn-translate" type="button" data-source="addItemNameAr" data-target="addItemNameEn" data-to="en"><i class="bi bi-translate"></i> EN</button></div></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Name (AR)" data-ar="الاسم (عربي)">Name (AR)</label><div class="input-group"><input type="text" name="name_ar" id="addItemNameAr" class="form-control" required><button class="btn btn-outline-secondary btn-translate" type="button" data-source="addItemNameEn" data-target="addItemNameAr" data-to="ar"><i class="bi bi-translate"></i> AR</button></div></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="SKU / Barcode" data-ar="الباركود">SKU / Barcode</label><div class="input-group"><input type="text" name="sku" id="skuInput" class="form-control"><button class="btn btn-outline-secondary" type="button" id="suggestSkuBtn" data-en="Suggest" data-ar="اقتراح">Suggest</button></div><div class="form-text" data-en="Reserved 13-digit scale barcodes cannot be saved here. Use the 5-digit scale item code for weighing products." data-ar="لا يمكن حفظ باركود الميزان الكامل المكوّن من 13 رقمًا هنا. استخدم كود الصنف المكوّن من 5 أرقام لأصناف الميزان.">Reserved 13-digit scale barcodes cannot be saved here. Use the 5-digit scale item code for weighing products.</div></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Category" data-ar="الفئة">Category</label><select name="category_id" class="form-select"><option value="" data-en="---" data-ar="---">---</option><?php foreach ($data['categories'] ?? [] as $c): ?><option value="<?= $c['id'] ?>" data-en="<?= htmlspecialchars(localized_option_label($c, 'en')) ?>" data-ar="<?= htmlspecialchars(localized_option_label($c, 'ar')) ?>"><?= htmlspecialchars(localized_option_label($c)) ?></option><?php endforeach; ?></select></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Unit" data-ar="الوحدة">Unit</label><select name="unit_id" class="form-select"><option value="" data-en="---" data-ar="---">---</option><?php foreach ($data['units'] ?? [] as $u): ?><option value="<?= $u['id'] ?>" data-en="<?= htmlspecialchars(localized_option_label($u, 'en')) ?>" data-ar="<?= htmlspecialchars(localized_option_label($u, 'ar')) ?>"><?= htmlspecialchars(localized_option_label($u)) ?></option><?php endforeach; ?></select></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Supplier" data-ar="المورد">Supplier</label><select name="supplier_id" class="form-select"><option value="">---</option><?php foreach ($data['suppliers'] ?? [] as $s): ?><option value="<?= $s['id'] ?>"><?= htmlspecialchars($s['name']) ?></option><?php endforeach; ?></select></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Sale Price" data-ar="سعر البيع">Sale Price</label><input type="number" step="0.001" name="sale_price" class="form-control" value="0.000"></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Purchase Price" data-ar="سعر الشراء">Purchase Price</label><input type="number" step="0.001" name="purchase_price" class="form-control" value="0.000"></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Initial Stock" data-ar="المخزون الحالي">Initial Stock</label><input type="number" step="0.001" name="stock_quantity" class="form-control" value="0.000"></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Min Stock Level" data-ar="الحد الأدنى للمخزون">Min Stock Level</label><input type="number" step="0.001" name="min_stock_level" class="form-control" value="0.000"></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="VAT Rate (%)" data-ar="ضريبة القيمة المضافة (%)">VAT Rate (%)</label><input type="number" step="0.01" name="vat_rate" class="form-control" value="0"></div>
|
|
<div class="col-md-4"><label class="form-label" data-en="Item Picture" data-ar="صورة الصنف">Item Picture</label><input type="file" name="image" class="form-control" accept="image/*"></div>
|
|
<div class="col-md-12 full-width"><div class="form-check form-switch mt-4"><input class="form-check-input" type="checkbox" id="hasExpiryToggle"><label class="form-check-label" for="hasExpiryToggle" data-en="Has Expiry Date?" data-ar="هل له تاريخ انتهاء؟">Has Expiry Date?</label></div></div>
|
|
<div class="col-md-12 full-width" id="expiryDateContainer" style="display: none;"><label class="form-label" data-en="Expiry Date" data-ar="تاريخ الانتهاء">Expiry Date</label><input type="date" name="expiry_date" class="form-control"></div>
|
|
<div class="col-12 full-width mt-3"><hr><h6 data-en="Promotion Details" data-ar="تفاصيل العرض">Promotion Details</h6></div>
|
|
<div class="col-md-12 full-width promotionFieldsContainer" id="promotionFieldsContainer" style="display: none;"><div class="form-grid-3"><div><label class="form-label" data-en="Start Date" data-ar="تاريخ البداية">Start Date</label><input type="date" name="promotion_start" class="form-control"></div><div><label class="form-label" data-en="End Date" data-ar="تاريخ النهاية">End Date</label><input type="date" name="promotion_end" class="form-control"></div><div><label class="form-label" data-en="Percent (%)" data-ar="النسبة (%)">Percent (%)</label><input type="number" step="0.01" name="promotion_percent" class="form-control"></div></div></div>
|
|
</div></div><div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_item" class="btn btn-primary" data-en="Save Item" data-ar="حفظ الصنف">Save Item</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import Items Modal -->
|
|
<div class="modal fade" id="importItemsModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Import Items from Excel (CSV)" data-ar="استيراد الأصناف من اكسل (CSV)">Import Items from Excel (CSV)</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="modal-body">
|
|
<div class="alert alert-info py-2">
|
|
<div class="mb-2">
|
|
<small data-en="Please upload a CSV file with the following columns: SKU, English Name, Arabic Name, Sale Price, Cost Price. Reserved 13-digit scale barcodes will be skipped; save weighing products with their 5-digit item code instead." data-ar="يرجى رفع ملف CSV بالأعمدة التالية: الباركود، الاسم الإنجليزي، الاسم العربي، سعر البيع، سعر التكلفة. سيتم تجاهل باركود الميزان الكامل المكوّن من 13 رقمًا؛ احفظ أصناف الميزان باستخدام كود الصنف المكوّن من 5 أرقام.">
|
|
Please upload a CSV file with the following columns: SKU, English Name, Arabic Name, Sale Price, Cost Price. Reserved 13-digit scale barcodes will be skipped; save weighing products with their 5-digit item code instead.
|
|
</small>
|
|
</div>
|
|
<a href="index.php?action=download_items_template" class="btn btn-sm btn-primary">
|
|
<i class="bi bi-download"></i> <span data-en="Download Template" data-ar="تحميل القالب">Download Template</span>
|
|
</a>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Choose CSV File" data-ar="اختر ملف CSV">Choose CSV File</label>
|
|
<input type="file" name="excel_file" class="form-control" accept=".csv" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="import_items" class="btn btn-success" data-en="Import Now" data-ar="استيراد الآن">Import Now</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import Customers Modal -->
|
|
<div class="modal fade" id="importCustomersModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Import Customers from Excel (CSV)" data-ar="استيراد العملاء من اكسل (CSV)">Import Customers from Excel (CSV)</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="modal-body">
|
|
<div class="alert alert-info py-2">
|
|
<small data-en="Please upload a CSV file with the following columns: Name, Email, Phone, Tax ID, Balance." data-ar="يرجى رفع ملف CSV بالأعمدة التالية: الاسم، البريد الإلكتروني، الهاتف، الرقم الضريبي، الرصيد.">
|
|
Please upload a CSV file with the following columns: Name, Email, Phone, Tax ID, Balance.
|
|
</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Choose CSV File" data-ar="اختر ملف CSV">Choose CSV File</label>
|
|
<input type="file" name="excel_file" class="form-control" accept=".csv" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="import_customers" class="btn btn-success" data-en="Import Now" data-ar="استيراد الآن">Import Now</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import Suppliers Modal -->
|
|
<div class="modal fade" id="importSuppliersModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Import Suppliers from Excel (CSV)" data-ar="استيراد الموردين من اكسل (CSV)">Import Suppliers from Excel (CSV)</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="modal-body">
|
|
<div class="alert alert-info py-2">
|
|
<small data-en="Please upload a CSV file with the following columns: Name, Email, Phone, Tax ID, Balance." data-ar="يرجى رفع ملف CSV بالأعمدة التالية: الاسم، البريد الإلكتروني، الهاتف، الرقم الضريبي، الرصيد.">
|
|
Please upload a CSV file with the following columns: Name, Email, Phone, Tax ID, Balance.
|
|
</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Choose CSV File" data-ar="اختر ملف CSV">Choose CSV File</label>
|
|
<input type="file" name="excel_file" class="form-control" accept=".csv" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="import_suppliers" class="btn btn-success" data-en="Import Now" data-ar="استيراد الآن">Import Now</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import Categories Modal -->
|
|
<div class="modal fade" id="importCategoriesModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow text-start">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Import Categories from Excel (CSV)" data-ar="استيراد الفئات من اكسل (CSV)">Import Categories from Excel (CSV)</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="modal-body">
|
|
<div class="alert alert-info py-2">
|
|
<small data-en="Please upload a CSV file with the following columns: Name (EN), Name (AR)." data-ar="يرجى رفع ملف CSV بالأعمدة التالية: الاسم (إنجليزي)، الاسم (عربي).">
|
|
Please upload a CSV file with the following columns: Name (EN), Name (AR).
|
|
</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Choose CSV File" data-ar="اختر ملف CSV">Choose CSV File</label>
|
|
<input type="file" name="excel_file" class="form-control" accept=".csv" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="import_categories" class="btn btn-success" data-en="Import Now" data-ar="استيراد الآن">Import Now</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import Units Modal -->
|
|
<div class="modal fade" id="importUnitsModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content border-0 shadow-lg text-start unit-modal">
|
|
<div class="modal-header">
|
|
<div>
|
|
<span class="units-modal-kicker" data-en="Bulk setup" data-ar="إعداد جماعي">Bulk setup</span>
|
|
<h5 class="modal-title" data-en="Import Units" data-ar="استيراد الوحدات">Import Units</h5>
|
|
<p class="text-muted small mb-0" data-en="Upload one file to create multiple units at once." data-ar="ارفع ملفاً واحداً لإنشاء عدة وحدات دفعة واحدة.">Upload one file to create multiple units at once.</p>
|
|
</div>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST" enctype="multipart/form-data">
|
|
<div class="modal-body pt-4">
|
|
<div class="unit-import-note mb-3">
|
|
<strong data-en="Supported columns" data-ar="الأعمدة المدعومة">Supported columns</strong>
|
|
<ul class="mt-2 small">
|
|
<li data-en="Name (EN), Name (AR)" data-ar="الاسم (إنجليزي)، الاسم (عربي)">Name (EN), Name (AR)</li>
|
|
<li data-en="CSV and XLSX files are accepted." data-ar="يتم قبول ملفات CSV و XLSX.">CSV and XLSX files are accepted.</li>
|
|
<li data-en="If Arabic is blank, the English name will be reused automatically." data-ar="إذا كان الاسم العربي فارغاً فسيتم استخدام الاسم الإنجليزي تلقائياً.">If Arabic is blank, the English name will be reused automatically.</li>
|
|
</ul>
|
|
</div>
|
|
<div class="unit-form-section">
|
|
<div class="unit-field">
|
|
<label class="form-label" data-en="Choose file" data-ar="اختر الملف">Choose file</label>
|
|
<input type="file" name="excel_file" class="form-control" accept=".csv,.xlsx" required>
|
|
<div class="form-text" data-en="Tip: export current units first if you want a formatting reference." data-ar="نصيحة: صدّر الوحدات الحالية أولاً إذا كنت تريد مرجعاً للتنسيق.">Tip: export current units first if you want a formatting reference.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer pt-0 border-0">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="import_units" class="btn btn-success" data-en="Import Now" data-ar="استيراد الآن">Import Now</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Add Category Modal -->
|
|
<div class="modal fade" id="addCategoryModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" data-en="Add Category" data-ar="إضافة فئة">Add Category</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Name (EN)" data-ar="الاسم (إنجليزي)">Name (EN)</label>
|
|
<div class="input-group">
|
|
<input type="text" name="name_en" id="addCatNameEn" class="form-control" required>
|
|
<button class="btn btn-outline-secondary btn-translate" type="button" data-source="addCatNameAr" data-target="addCatNameEn" data-to="en">
|
|
<i class="bi bi-translate"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label" data-en="Name (AR)" data-ar="الاسم (عربي)">Name (AR)</label>
|
|
<div class="input-group">
|
|
<input type="text" name="name_ar" id="addCatNameAr" class="form-control" required>
|
|
<button class="btn btn-outline-secondary btn-translate" type="button" data-source="addCatNameEn" data-target="addCatNameAr" data-to="ar">
|
|
<i class="bi bi-translate"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_category" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Unit Modal -->
|
|
<div class="modal fade" id="addUnitModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content border-0 shadow-lg text-start unit-modal">
|
|
<div class="modal-header">
|
|
<div>
|
|
<span class="units-modal-kicker" data-en="Create unit" data-ar="إنشاء وحدة">Create unit</span>
|
|
<h5 class="modal-title" data-en="Add Unit" data-ar="إضافة وحدة">Add Unit</h5>
|
|
<p class="text-muted small mb-0" data-en="Use the full unit name in English and Arabic so stock screens stay consistent." data-ar="استخدم اسم الوحدة الكامل بالإنجليزية والعربية حتى تبقى شاشات المخزون متناسقة.">Use the full unit name in English and Arabic so stock screens stay consistent.</p>
|
|
</div>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form method="POST">
|
|
<div class="modal-body pt-4">
|
|
<div class="unit-form-shell">
|
|
<section class="unit-form-section">
|
|
<div class="unit-form-section__header">
|
|
<div>
|
|
<h6 class="mb-1" data-en="Display names" data-ar="الأسماء الكاملة">Display names</h6>
|
|
<p class="text-muted small mb-0" data-en="Shown in item forms, reports, and detailed views." data-ar="تظهر في نماذج الأصناف والتقارير والعروض التفصيلية.">Shown in item forms, reports, and detailed views.</p>
|
|
</div>
|
|
<span class="units-helper-pill" data-en="Bilingual" data-ar="ثنائي اللغة">Bilingual</span>
|
|
</div>
|
|
<div class="unit-form-grid">
|
|
<div class="unit-field">
|
|
<label class="form-label" data-en="Name (EN)" data-ar="الاسم (إنجليزي)">Name (EN)</label>
|
|
<div class="input-group">
|
|
<input type="text" name="name_en" id="addUnitNameEn" class="form-control" required>
|
|
<button class="btn btn-outline-secondary btn-translate" type="button" data-source="addUnitNameAr" data-target="addUnitNameEn" data-to="en">
|
|
<i class="bi bi-translate"></i>
|
|
</button>
|
|
</div>
|
|
<div class="form-text" data-en="Example: Kilogram" data-ar="مثال: Kilogram">Example: Kilogram</div>
|
|
</div>
|
|
<div class="unit-field">
|
|
<label class="form-label" data-en="Name (AR)" data-ar="الاسم (عربي)">Name (AR)</label>
|
|
<div class="input-group">
|
|
<input type="text" name="name_ar" id="addUnitNameAr" class="form-control" required>
|
|
<button class="btn btn-outline-secondary btn-translate" type="button" data-source="addUnitNameEn" data-target="addUnitNameAr" data-to="ar">
|
|
<i class="bi bi-translate"></i>
|
|
</button>
|
|
</div>
|
|
<div class="form-text" data-en="Example: كيلوغرام" data-ar="مثال: كيلوغرام">Example: كيلوغرام</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section class="unit-form-section">
|
|
<div class="unit-form-section__header">
|
|
<div>
|
|
<h6 class="mb-1" data-en="Name preview" data-ar="معاينة الأسماء">Name preview</h6>
|
|
<p class="text-muted small mb-0" data-en="This is how the unit name will appear across the app." data-ar="هكذا سيظهر اسم الوحدة في جميع أجزاء التطبيق.">This is how the unit name will appear across the app.</p>
|
|
</div>
|
|
<span class="units-helper-pill" data-en="Live label" data-ar="التسمية النهائية">Live label</span>
|
|
</div>
|
|
<div class="unit-preview-card mt-0">
|
|
<span class="unit-preview-card__label" data-en="Example preview" data-ar="معاينة مثال">Example preview</span>
|
|
<div class="unit-preview-row">
|
|
<span class="unit-preview-chip">Kilogram</span>
|
|
<span class="unit-preview-chip">كيلوغرام</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer pt-0 border-0">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="submit" name="add_unit" class="btn btn-primary" data-en="Save" data-ar="حفظ">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.log("DOM Content Loaded - Accounting System");
|
|
try {
|
|
// Initialize Select2 for all searchable dropdowns
|
|
$('.select2').each(function() {
|
|
$(this).select2({
|
|
width: '100%',
|
|
dropdownParent: $(this).closest('.modal').length ? $(this).closest('.modal') : $(document.body)
|
|
});
|
|
});
|
|
|
|
const hasExpiryToggle = document.getElementById('hasExpiryToggle');
|
|
const expiryDateContainer = document.getElementById('expiryDateContainer');
|
|
const suggestSkuBtn = document.getElementById('suggestSkuBtn');
|
|
const skuInput = document.getElementById('skuInput');
|
|
|
|
if (suggestSkuBtn && skuInput) {
|
|
suggestSkuBtn.addEventListener('click', function() {
|
|
const sku = Math.floor(100000000000 + Math.random() * 900000000000).toString();
|
|
skuInput.value = sku;
|
|
});
|
|
|
|
skuInput.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
console.log("Barcode scan detected in SKU field, preventing form submission");
|
|
}
|
|
});
|
|
}
|
|
|
|
// Toggle Expiry Date visibility
|
|
if (hasExpiryToggle && expiryDateContainer) {
|
|
hasExpiryToggle.addEventListener('change', function() {
|
|
expiryDateContainer.style.display = this.checked ? 'block' : 'none';
|
|
});
|
|
}
|
|
|
|
const isPromotionToggle = document.getElementById('isPromotionToggle');
|
|
const promotionFieldsContainer = document.getElementById('promotionFieldsContainer');
|
|
if (isPromotionToggle && promotionFieldsContainer) {
|
|
isPromotionToggle.addEventListener('change', function() {
|
|
promotionFieldsContainer.style.display = this.checked ? 'flex' : 'none';
|
|
});
|
|
}
|
|
|
|
// Translation Logic
|
|
document.querySelectorAll('.btn-translate').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const sourceId = this.getAttribute('data-source');
|
|
const targetId = this.getAttribute('data-target');
|
|
const targetLang = this.getAttribute('data-to');
|
|
const sourceText = document.getElementById(sourceId).value;
|
|
|
|
if (!sourceText) {
|
|
alert(targetLang === 'ar' ? 'يرجى إدخال النص أولاً' : 'Please enter text first');
|
|
return;
|
|
}
|
|
|
|
const originalHtml = this.innerHTML;
|
|
this.disabled = true;
|
|
this.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
|
|
|
const formData = new FormData();
|
|
formData.append('action', 'translate');
|
|
formData.append('text', sourceText);
|
|
formData.append('target', targetLang);
|
|
|
|
fetch('index.php', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
document.getElementById(targetId).value = data.translated;
|
|
} else {
|
|
alert('Translation error: ' + (data.error || 'Unknown error'));
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
alert('Connection error');
|
|
})
|
|
.finally(() => {
|
|
this.disabled = false;
|
|
this.innerHTML = originalHtml;
|
|
});
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.isPromotionToggleEdit').forEach(toggle => {
|
|
toggle.addEventListener('change', function() {
|
|
const id = this.getAttribute('data-id');
|
|
const container = document.getElementById('promotionFieldsContainerEdit' + id);
|
|
if (container) {
|
|
container.style.display = this.checked ? 'flex' : 'none';
|
|
}
|
|
});
|
|
});
|
|
|
|
<?php require 'pages/sales_purchases_payment_receipt_script.php'; ?>
|
|
|
|
// Handle Expiry toggle in Edit Modals
|
|
document.querySelectorAll('.hasExpiryToggleEdit').forEach(toggle => {
|
|
toggle.addEventListener('change', function() {
|
|
const container = this.closest('.row').querySelector('.expiryDateContainerEdit');
|
|
if (container) {
|
|
container.style.display = this.checked ? 'block' : 'none';
|
|
if (!this.checked) {
|
|
container.querySelector('input').value = '';
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
<?php require 'pages/sales_purchases_invoice_form_helpers.php'; ?>
|
|
|
|
<?php require 'pages/lpo_quotation_script.php'; ?>
|
|
|
|
<?php require 'pages/sales_purchases_invoice_actions_script.php'; ?>
|
|
|
|
} catch (e) { console.error("JS Error in DOMContentLoaded:", e); }
|
|
});
|
|
</script>
|
|
<?php require 'pages/sales_purchases_modals.php'; ?>
|
|
|
|
<style>
|
|
#posPaymentModal .modal-body { padding: 0.75rem; font-size: 0.8rem; }
|
|
#posPaymentModal .amount-due-box { background: #f8f9fa; border-radius: 12px; padding: 10px 0; border: 1px solid #eee; }
|
|
#posPaymentModal .amount-due-box .label { font-size: 0.65rem; text-transform: uppercase; font-weight: 700; color: #64748b; }
|
|
#posPaymentModal .amount-due-box .value { font-size: 1rem; font-weight: 800; color: #1e293b; }
|
|
#posPaymentModal .payment-methods-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
|
|
#posPaymentModal .payment-method-btn {
|
|
padding: 6px 4px; border-radius: 10px; border: 2px solid #f1f5f9; cursor: pointer;
|
|
text-align: center; transition: all 0.2s; background: white;
|
|
}
|
|
#posPaymentModal .payment-method-btn.active { border-color: #3b82f6; background: #eff6ff; color: #1d4ed8; }
|
|
#posPaymentModal .payment-method-btn i { font-size: 1rem; display: block; margin-bottom: 2px; }
|
|
#posPaymentModal .payment-method-btn span { font-size: 0.65rem; }
|
|
#posPaymentModal .quick-pay-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; }
|
|
#posPaymentModal .quick-pay-btn {
|
|
padding: 5px; border-radius: 8px; border: 1px solid #e2e8f0; background: white;
|
|
font-weight: bold; text-align: center; cursor: pointer; font-size: 0.75rem;
|
|
}
|
|
#posPaymentModal .payment-line {
|
|
display: flex; justify-content: space-between; align-items: center;
|
|
padding: 6px 10px; background: #f1f5f9; border-radius: 8px; margin-bottom: 4px;
|
|
}
|
|
#posPaymentModal .payment-line .method { font-weight: 600; color: #475569; font-size: 0.75rem; }
|
|
#posPaymentModal .form-control { font-size: 0.85rem; padding: 0.35rem 0.6rem; }
|
|
#posPaymentModal .btn-primary { padding: 0.35rem 0.8rem; font-size: 0.85rem; }
|
|
#posPaymentModal .modal-header { padding: 0.75rem 1rem 0.25rem; }
|
|
#posPaymentModal .modal-footer { padding: 0.25rem 1rem 0.75rem; }
|
|
</style>
|
|
|
|
<!-- POS Payment Modal -->
|
|
<div class="modal fade" id="posPaymentModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0">
|
|
<div class="modal-header border-0 pb-0">
|
|
<h5 class="modal-title fw-bold">Payment</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-2 p-2 border rounded bg-light shadow-sm">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<span class="small text-muted d-block" data-en="Customer" data-ar="العميل">Customer</span>
|
|
<span class="h6 fw-bold m-0 text-primary" id="paymentCustomerName">Walk-in Customer</span>
|
|
</div>
|
|
<i class="bi bi-person-circle fs-3 text-secondary"></i>
|
|
</div>
|
|
<div id="creditCustomerSection" class="mt-2 pt-2 border-top" style="display:none;">
|
|
<label class="form-label smaller fw-bold mb-1">Select Credit Customer</label>
|
|
<select id="paymentCreditCustomer" class="form-select form-select-sm select2" onchange="cart.syncCustomer(this.value)">
|
|
<option value="">--- Select Customer ---</option>
|
|
<?php foreach ($customers as $c): ?>
|
|
<option value="<?= $c['id'] ?>" data-search="<?= htmlspecialchars(strtolower($c['name'] . ' ' . ($c['phone'] ?? ''))) ?>"><?= htmlspecialchars($c['name']) ?> (<?= htmlspecialchars($c['phone'] ?? '') ?>)</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="amount-due-box mb-2">
|
|
<div class="d-flex justify-content-between px-3">
|
|
<div class="text-start">
|
|
<div class="label">Amount Due</div>
|
|
<div class="value" id="paymentAmountDue">0.000</div>
|
|
</div>
|
|
<div class="text-end">
|
|
<div class="label text-danger">Remaining</div>
|
|
<div class="value text-danger" id="paymentRemaining">0.000</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="paymentList" class="mb-2">
|
|
<!-- Added payments will appear here -->
|
|
</div>
|
|
|
|
<div class="mb-2 p-2 border rounded bg-light">
|
|
<label class="form-label small fw-bold mb-1">Add Payment Method</label>
|
|
<div class="payment-methods-grid mb-2">
|
|
<div class="payment-method-btn active" data-method="cash" onclick="cart.selectMethod('cash', this)">
|
|
<i class="bi bi-cash-stack"></i>
|
|
<span class="small fw-bold">Cash</span>
|
|
</div>
|
|
<div class="payment-method-btn" data-method="card" onclick="cart.selectMethod('card', this)">
|
|
<i class="bi bi-credit-card"></i>
|
|
<span class="small fw-bold">Credit Card</span>
|
|
</div>
|
|
<div class="payment-method-btn" data-method="credit" onclick="cart.selectMethod('credit', this)">
|
|
<i class="bi bi-person-badge"></i>
|
|
<span class="small fw-bold">Credit</span>
|
|
</div>
|
|
<div class="payment-method-btn" data-method="transfer" onclick="cart.selectMethod('transfer', this)">
|
|
<i class="bi bi-bank"></i>
|
|
<span class="small fw-bold">Bank Transfer</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-2 align-items-end">
|
|
<div class="col">
|
|
<label class="form-label smaller fw-bold mb-1" data-en="Amount" data-ar="المبلغ">Amount</label>
|
|
<div class="input-group">
|
|
<input type="number" step="0.001" id="partialAmount" class="form-control" placeholder="0.000" oninput="cart.updateRemaining()">
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<button type="button" class="btn btn-primary" onclick="cart.addPaymentLine()">
|
|
<i class="bi bi-plus-lg"></i data-en="Add" data-ar="إضافة">ADD</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="quick-pay-grid mt-2">
|
|
<div class="quick-pay-btn" onclick="cart.fillPartial(1)">1</div>
|
|
<div class="quick-pay-btn" onclick="cart.fillPartial(5)">5</div>
|
|
<div class="quick-pay-btn" onclick="cart.fillPartial(10)">10</div>
|
|
<div class="quick-pay-btn" onclick="cart.fillPartial(20)">20</div>
|
|
<div class="quick-pay-btn" onclick="cart.fillPartial(50)">50</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="cashPaymentSection" style="display: none;">
|
|
<div class="d-flex justify-content-between align-items-center p-3 bg-primary-subtle rounded border border-primary-subtle">
|
|
<span class="fw-bold">Total Tendered (Cash)</span>
|
|
<span class="h6 m-0 fw-bold text-primary" id="changeDue">0.000</span>
|
|
</div>
|
|
<div class="small text-muted mt-1">* Change is calculated based on cash payments only.</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Cancel" data-ar="إلغاء">Cancel</button>
|
|
<button type="button" class="btn btn-primary px-4" id="confirmPaymentBtn" onclick="cart.completeOrder()">
|
|
PAY & COMPLETE
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- POS Receipt Modal -->
|
|
<div class="modal fade" id="posReceiptModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content border-0">
|
|
<div class="modal-header border-0 pb-0">
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body pt-0">
|
|
<div id="posReceiptContent">
|
|
<!-- Receipt content will be generated here -->
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0">
|
|
<button type="button" class="btn btn-primary w-100" onclick="printPosReceipt()">
|
|
<i class="bi bi-printer me-2"></i>PRINT RECEIPT
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="posPrintArea" class="d-none d-print-block"></div>
|
|
|
|
<!-- Barcode Print Modal -->
|
|
<div class="modal fade" id="barcodePrintModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Print Barcode Label</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body text-center">
|
|
<div id="barcodeContainer" class="p-4 bg-white border mb-3 mx-auto" style="width: fit-content;">
|
|
<div id="barcodeLabelName" class="fw-bold small mb-1"></div>
|
|
<svg id="barcodeSvg"></svg>
|
|
<div id="barcodeLabelPrice" class="fw-bold mt-1"></div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label small">Number of Labels</label>
|
|
<input type="number" id="barcodeQty" class="form-control form-control-sm mx-auto" value="1" min="1" style="width: 80px;">
|
|
</div>
|
|
<div class="row mb-3 mx-auto" style="max-width: 200px;">
|
|
<div class="col-6">
|
|
<label class="form-label small">Width (mm)</label>
|
|
<input type="number" id="barcodeWidth" class="form-control form-control-sm" value="40" min="10">
|
|
</div>
|
|
<div class="col-6">
|
|
<label class="form-label small">Height (mm)</label>
|
|
<input type="number" id="barcodeHeight" class="form-control form-control-sm" value="25" min="10">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal" data-en="Close" data-ar="إغلاق">Close</button>
|
|
<button type="button" class="btn btn-primary" onclick="executeBarcodePrint()"><i class="bi bi-printer me-2"></i>Print Now</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Avery Labels Modal -->
|
|
<div class="modal fade" id="averyLabelsModal" tabindex="-1">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header d-print-none">
|
|
<h5 class="modal-title">Avery Barcode Labels (A4)</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row mb-4 d-print-none">
|
|
<div class="col-md-4">
|
|
<label class="form-label small">Label Layout</label>
|
|
<select id="averyLayout" class="form-select form-select-sm" onchange="updateAveryPreview()">
|
|
<option value="3x7">3 x 7 (21 Labels per sheet)</option>
|
|
<option value="3x8">3 x 8 (24 Labels per sheet)</option>
|
|
<option value="4x10">4 x 10 (40 Labels per sheet)</option>
|
|
<option value="L7651">L7651 (5 x 13 - 65 Labels)</option>
|
|
<option value="L4736">L4736 (2 x 7 - 14 Labels)</option>
|
|
<option value="L7431">L7431 (6 x 8 - 48 Labels)</option>
|
|
<option value="L4716">L4716 (6 x 8 - 48 Labels - Round)</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label small">Copies (Set All)</label>
|
|
<input type="number" id="averyCopies" class="form-control form-control-sm" value="1" min="1" oninput="updateAllItemQuantities()" onchange="updateAllItemQuantities()">
|
|
</div>
|
|
<div class="col-md-4 d-flex align-items-end">
|
|
<button class="btn btn-primary btn-sm w-100" onclick="window.print()"><i class="bi bi-printer me-2"></i>Print A4 Sheet</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-3 d-print-none">
|
|
<div class="col-12">
|
|
<label class="form-label small fw-bold" data-en="Quantities per Item" data-ar="الكميات لكل صنف">Quantities per Item</label>
|
|
<div id="averyItemQuantities" class="border rounded p-2 bg-light" style="max-height: 150px; overflow-y: auto;">
|
|
<small class="text-muted">Select items to adjust quantities.</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="averyPrintArea" class="avery-container">
|
|
<!-- Labels will be generated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<footer class="main-footer d-print-none">
|
|
<div class="text-center py-3 border-top mt-5">
|
|
<div class="text-muted small">
|
|
Powered By <strong>Accounting</strong> • omanapp.cloud • aalabry@gmail.com • Whatsapp: +968 99359472
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
|
|
<style>
|
|
/* Avery Label Printing */
|
|
.avery-container {
|
|
background: white;
|
|
width: 210mm; /* A4 Width */
|
|
min-height: 297mm; /* A4 Height */
|
|
padding: 10mm 5mm;
|
|
margin: 0 auto;
|
|
display: grid;
|
|
gap: 2mm;
|
|
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
|
}
|
|
.avery-layout-3x7 { grid-template-columns: repeat(3, 1fr); grid-auto-rows: 38mm; }
|
|
.avery-layout-3x8 { grid-template-columns: repeat(3, 1fr); grid-auto-rows: 34mm; }
|
|
.avery-layout-4x10 { grid-template-columns: repeat(4, 1fr); grid-auto-rows: 27mm; }
|
|
.avery-layout-L7651 { grid-template-columns: repeat(5, 1fr); grid-auto-rows: 21mm; }
|
|
.avery-layout-L4736 { grid-template-columns: repeat(2, 1fr); grid-auto-rows: 38mm; }
|
|
.avery-layout-L7431 { grid-template-columns: repeat(6, 1fr); grid-auto-rows: 33mm; }
|
|
.avery-layout-L4716 { grid-template-columns: repeat(6, 1fr); grid-auto-rows: 33mm; }
|
|
|
|
.avery-layout-L4716 .avery-label { border-radius: 50%; }
|
|
.avery-layout-L7431 .avery-label, .avery-layout-L4716 .avery-label { padding: 1mm; }
|
|
.avery-layout-L7431 .avery-label div, .avery-layout-L4716 .avery-label div { font-size: 8px !important; }
|
|
.avery-layout-L7431 .avery-label svg, .avery-layout-L4716 .avery-label svg { height: 20px; }
|
|
|
|
.avery-layout-L7651 .avery-label { padding: 2mm; }
|
|
.avery-layout-L7651 .avery-label svg { height: 25px; }
|
|
.avery-layout-L7651 .avery-label div { font-size: 8px !important; }
|
|
|
|
.avery-label {
|
|
border: 1px dashed #eee;
|
|
padding: 5mm;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
overflow: hidden;
|
|
background: white;
|
|
}
|
|
|
|
.avery-label svg {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
@media print {
|
|
body.printing-avery .sidebar,
|
|
body.printing-avery .topbar,
|
|
body.printing-avery .modal-header,
|
|
body.printing-avery .d-print-none,
|
|
body.printing-avery .modal-backdrop {
|
|
display: none !important;
|
|
}
|
|
body.printing-avery .modal {
|
|
position: absolute !important;
|
|
left: 0 !important;
|
|
top: 0 !important;
|
|
width: 100% !important;
|
|
display: block !important;
|
|
visibility: visible !important;
|
|
background: white !important;
|
|
}
|
|
body.printing-avery .modal-dialog {
|
|
max-width: 100% !important;
|
|
width: 100% !important;
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
}
|
|
body.printing-avery .modal-content {
|
|
border: none !important;
|
|
}
|
|
body.printing-avery .avery-label {
|
|
border: none !important;
|
|
}
|
|
.avery-container {
|
|
margin: 0 !important;
|
|
padding: 10mm 5mm !important;
|
|
box-shadow: none !important;
|
|
border: none !important;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<?php require 'pages/avery_label_script.php'; ?>
|
|
|
|
<script>
|
|
<?php require 'pages/barcode_pos_script.php'; ?>
|
|
|
|
<?php require 'pages/language_dashboard_script.php'; ?>
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|