Compare commits

...

40 Commits

Author SHA1 Message Date
Flatlogic Bot
0f040f6eca add qr to admin page 2026-03-10 09:41:24 +00:00
Flatlogic Bot
c441d54a7a adding qr to reciept 2026-03-10 09:34:51 +00:00
Flatlogic Bot
c8388831c1 adding branches to rating 2026-03-10 09:27:28 +00:00
Flatlogic Bot
c6d83786d4 adding landing page for rating 2026-03-10 09:20:20 +00:00
Flatlogic Bot
df53811d22 update rating 2 2026-03-10 08:29:36 +00:00
Flatlogic Bot
b1bb8c3b26 update rating 2026-03-10 08:24:46 +00:00
Flatlogic Bot
d76a592792 update company profile 2026-03-09 10:56:52 +00:00
Flatlogic Bot
595c9b31cf update install2 2026-03-09 10:29:02 +00:00
Flatlogic Bot
52be768699 update install file 2026-03-09 10:13:52 +00:00
Flatlogic Bot
9f43a69a1d some new updates 2026-03-09 09:46:11 +00:00
Flatlogic Bot
5f8d3b1aed update reciept 2026-03-04 03:47:39 +00:00
Flatlogic Bot
57a6e7ed99 modify pos 2026-03-04 03:40:50 +00:00
Flatlogic Bot
84add80342 translation update 2026-03-04 03:21:53 +00:00
Flatlogic Bot
f8ccd73ba6 Autosave: 20260304-030410 2026-03-04 03:04:10 +00:00
Flatlogic Bot
f08a6b75bb updaing items 3 2026-03-03 18:27:29 +00:00
Flatlogic Bot
9060a6d586 updating items 2026-03-03 18:19:55 +00:00
Flatlogic Bot
35d8fe23f8 update items 2026-03-03 18:08:33 +00:00
Flatlogic Bot
a7d442be5d updating items 2026-03-03 17:59:49 +00:00
Flatlogic Bot
c34a6ddf37 editing items 2026-03-03 17:37:36 +00:00
Flatlogic Bot
a2fc645f85 add .env file 2026-03-03 17:17:29 +00:00
Flatlogic Bot
0b22cc2f02 fixing errors 2026-03-03 17:12:24 +00:00
Flatlogic Bot
a251e37178 update services 2026-03-03 16:57:49 +00:00
Flatlogic Bot
f49527d7c4 update install file 2026-03-03 13:59:04 +00:00
Flatlogic Bot
9a5d45c4c5 adding installation file 2026-03-03 13:50:49 +00:00
Flatlogic Bot
e135bdd9e0 Autosave: 20260303-131517 2026-03-03 13:15:17 +00:00
Flatlogic Bot
538704f9d0 updating service 2026-03-03 10:35:21 +00:00
Flatlogic Bot
1de3f1d7d1 Autosave: 20260303-082124 2026-03-03 08:21:24 +00:00
Flatlogic Bot
ec6f16b511 update pos 2026-03-03 07:40:36 +00:00
Flatlogic Bot
2a16f1f83e adding whatapp 2026-03-03 06:59:12 +00:00
Flatlogic Bot
d2a2461bda Autosave: 20260303-065452 2026-03-03 06:54:52 +00:00
Flatlogic Bot
e9ceb4546d Autosave: 20260303-053821 2026-03-03 05:38:21 +00:00
Flatlogic Bot
78166012df update users who has multi branch 2026-03-03 05:22:06 +00:00
Flatlogic Bot
2a38c2ffdb Autosave: 20260302-152644 2026-03-02 15:26:44 +00:00
Flatlogic Bot
275baf63cd update pos 2026-03-02 13:43:08 +00:00
Flatlogic Bot
3f1e18552b formating order number 2026-03-02 13:35:27 +00:00
Flatlogic Bot
2807096c56 update orders 2026-03-02 13:26:09 +00:00
Flatlogic Bot
268b4cc1cf Autosave: 20260302-132419 2026-03-02 13:24:19 +00:00
Flatlogic Bot
62b332799d Autosave: 20260302-110501 2026-03-02 11:05:01 +00:00
Flatlogic Bot
ab23c4540e adding services 2026-03-02 10:39:33 +00:00
Flatlogic Bot
874d750960 Autosave: 20260302-103011 2026-03-02 10:30:11 +00:00
70 changed files with 9996 additions and 597 deletions

402
admin.php
View File

@ -1,166 +1,266 @@
<?php <?php
require_once __DIR__ . '/db/config.php'; $title = 'dashboard';
require_once __DIR__ . '/includes/header.php';
// Simple handling of form submissions // Stats logic
if ($_SERVER['REQUEST_METHOD'] === 'POST') { $branch_id = $_SESSION['branch_id'];
if (isset($_POST['action']) && $_POST['action'] === 'add') { $is_super = ($current_role === 'super_admin');
$keywords = $_POST['keywords'] ?? ''; $is_limited = ($current_role === 'limited_viewer');
$answer = $_POST['answer'] ?? '';
if ($keywords && $answer) {
$stmt = db()->prepare("INSERT INTO faqs (keywords, answer) VALUES (?, ?)");
$stmt->execute([$keywords, $answer]);
}
} elseif (isset($_POST['action']) && $_POST['action'] === 'delete') {
$id = $_POST['id'] ?? 0;
if ($id) {
$stmt = db()->prepare("DELETE FROM faqs WHERE id = ?");
$stmt->execute([$id]);
}
} elseif (isset($_POST['action']) && $_POST['action'] === 'update_settings') {
$token = $_POST['telegram_token'] ?? '';
$stmt = db()->prepare("INSERT INTO settings (setting_key, setting_value) VALUES ('telegram_token', ?) ON DUPLICATE KEY UPDATE setting_value = ?");
$stmt->execute([$token, $token]);
}
header("Location: admin.php");
exit;
}
$faqs = db()->query("SELECT * FROM faqs ORDER BY created_at DESC")->fetchAll(); // For super_admin, "all" means no branch filtering.
$messages = db()->query("SELECT * FROM messages ORDER BY created_at DESC LIMIT 50")->fetchAll(); // Otherwise, we filter by the selected branch.
$filter_branch = ($branch_id !== 'all');
$telegramToken = ''; if (!$is_limited) {
$stmt = db()->query("SELECT setting_value FROM settings WHERE setting_key = 'telegram_token'"); $stats = [
$row = $stmt->fetch(); 'today_revenue' => 0,
if ($row) { 'active_orders' => 0,
$telegramToken = $row['setting_value']; 'new_customers' => 0,
'ready_orders' => 0,
];
// Today's revenue
$rev_where = !$filter_branch ? "" : " AND order_id IN (SELECT id FROM orders WHERE branch_id = ?)";
$rev_params = !$filter_branch ? [] : [$branch_id];
$stmt = db()->prepare("SELECT SUM(amount) as total FROM payments WHERE created_at >= CURDATE() $rev_where");
$stmt->execute($rev_params);
$stats['today_revenue'] = $stmt->fetch()['total'] ?? 0;
// Active orders (received, processing)
$ord_where = !$filter_branch ? "WHERE status IN ('received', 'processing')" : "WHERE branch_id = ? AND status IN ('received', 'processing')";
$ord_params = !$filter_branch ? [] : [$branch_id];
$stmt = db()->prepare("SELECT COUNT(*) as count FROM orders $ord_where");
$stmt->execute($ord_params);
$stats['active_orders'] = $stmt->fetch()['count'] ?? 0;
// Ready orders
$ready_where = !$filter_branch ? "WHERE status = 'ready'" : "WHERE branch_id = ? AND status = 'ready'";
$ready_params = !$filter_branch ? [] : [$branch_id];
$stmt = db()->prepare("SELECT COUNT(*) as count FROM orders $ready_where");
$stmt->execute($ready_params);
$stats['ready_orders'] = $stmt->fetch()['count'] ?? 0;
// New customers today
$cust_where = !$filter_branch ? "WHERE created_at >= CURDATE()" : "WHERE branch_id = ? AND created_at >= CURDATE()";
$cust_params = !$filter_branch ? [] : [$branch_id];
$stmt = db()->prepare("SELECT COUNT(*) as count FROM customers $cust_where");
$stmt->execute($cust_params);
$stats['new_customers'] = $stmt->fetch()['count'] ?? 0;
// Monthly Orders (Last 12 months)
$monthly_where = !$filter_branch ? "WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)" : "WHERE branch_id = ? AND created_at >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)";
$monthly_params = !$filter_branch ? [] : [$branch_id];
$stmt = db()->prepare("SELECT DATE_FORMAT(created_at, '%Y-%m') as month_year, COUNT(*) as count
FROM orders
$monthly_where
GROUP BY month_year
ORDER BY month_year ASC");
$stmt->execute($monthly_params);
$monthly_orders = $stmt->fetchAll();
// Charts data for Dashboard (Last 7 days)
$chart_where = !$filter_branch ? "WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)" : "WHERE branch_id = ? AND created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)";
$chart_params = !$filter_branch ? [] : [$branch_id];
// Daily Revenue
$stmt = db()->prepare("SELECT DATE(created_at) as date, SUM(total_price) as revenue
FROM orders
$chart_where
GROUP BY DATE(created_at)
ORDER BY DATE(created_at) ASC");
$stmt->execute($chart_params);
$daily_revenue = $stmt->fetchAll();
// Orders by Status (All time or current)
$status_where = !$filter_branch ? "" : "WHERE branch_id = ?";
$status_params = !$filter_branch ? [] : [$branch_id];
$stmt = db()->prepare("SELECT status, COUNT(*) as count FROM orders $status_where GROUP BY status");
$stmt->execute($status_params);
$orders_by_status = $stmt->fetchAll();
} }
?> ?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin - FAQ Manager</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<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=<?php echo time(); ?>">
<style>
.btn-delete {
background: #dc3545;
color: white;
border: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
}
.btn-add {
background: #212529;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
margin-top: 1rem;
}
</style>
</head>
<body>
<div class="bg-animations">
<div class="blob blob-1"></div>
<div class="blob blob-2"></div>
<div class="blob blob-3"></div>
</div>
<div class="admin-container">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h1>FAQ Manager</h1>
<a href="index.php" class="admin-link">Back to Chat</a>
</div>
<div class="admin-card" style="background: rgba(255, 255, 255, 0.6); padding: 2rem; border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.5); margin-bottom: 2.5rem; box-shadow: 0 10px 30px rgba(0,0,0,0.05);"> <?php if ($is_limited): ?>
<h3 style="margin-top: 0; margin-bottom: 1.5rem; font-weight: 700;">Telegram Bot Settings</h3> <div class="d-flex flex-column align-items-center justify-content-center" style="min-height: 60vh;">
<form method="POST"> <div class="text-center p-5 card shadow-sm" style="border-radius: 30px; max-width: 600px;">
<input type="hidden" name="action" value="update_settings"> <?php if ($company_info['logo']): ?>
<div class="form-group"> <img src="<?= $company_info['logo'] ?>" alt="Logo" class="img-fluid mb-4" style="max-height: 150px;">
<label for="telegram_token">Telegram Bot Token</label> <?php endif; ?>
<input type="text" name="telegram_token" id="telegram_token" class="form-control" placeholder="Paste your bot token from @BotFather" value="<?= htmlspecialchars($telegramToken) ?>"> <h1 class="fw-bold text-primary mb-3">
</div> <?= htmlspecialchars(is_arabic() ? $company_info['name_ar'] : $company_info['name_en']) ?>
<p style="font-size: 0.85em; color: #555; margin-top: 0.5rem;"> </h1>
Webhook URL: <code>https://<?= $_SERVER['HTTP_HOST'] ?>/api/telegram_webhook.php</code> <p class="text-muted fs-5">
<?= is_arabic() ? 'مرحباً بك في لوحة التحكم' : 'Welcome to your dashboard' ?>
</p> </p>
<button type="submit" class="btn-add" style="background: #0088cc; color: white; border: none; padding: 0.8rem 1.5rem; border-radius: 12px; cursor: pointer; font-weight: 600; width: 100%; transition: all 0.3s ease;">Save Token</button> </div>
</form>
</div> </div>
<?php else: ?>
<div class="admin-card" style="background: rgba(255, 255, 255, 0.6); padding: 2rem; border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.5); margin-bottom: 2.5rem; box-shadow: 0 10px 30px rgba(0,0,0,0.05);"> <div class="row g-4 mb-4">
<h3 style="margin-top: 0; margin-bottom: 1.5rem; font-weight: 700;">Add New FAQ</h3> <div class="col-md-3">
<form method="POST"> <div class="card p-3">
<input type="hidden" name="action" value="add"> <div class="d-flex align-items-center">
<div class="form-group"> <div class="bg-primary text-white p-3 rounded-4 me-3">
<label for="keywords">Keywords (comma separated)</label> <i class="bi bi-cash-stack fs-3"></i>
<input type="text" name="keywords" id="keywords" class="form-control" placeholder="e.g. price, cost, dollar" required> </div>
<div>
<div class="text-muted small"><?= __('today_revenue') ?></div>
<div class="fw-bold fs-5"><?= format_amount($stats['today_revenue']) ?></div>
</div>
</div>
</div> </div>
<div class="form-group"> </div>
<label for="answer">Answer</label> <div class="col-md-3">
<textarea name="answer" id="answer" class="form-control" rows="3" placeholder="Enter the answer..." required></textarea> <div class="card p-3">
<div class="d-flex align-items-center">
<div class="bg-warning text-white p-3 rounded-4 me-3">
<i class="bi bi-clock-history fs-3"></i>
</div>
<div>
<div class="text-muted small"><?= __('active_orders') ?></div>
<div class="fw-bold fs-5"><?= $stats['active_orders'] ?></div>
</div>
</div>
</div> </div>
<button type="submit" class="btn-add" style="background: #212529; color: white; border: none; padding: 0.8rem 1.5rem; border-radius: 12px; cursor: pointer; font-weight: 600; width: 100%; transition: all 0.3s ease;">Save FAQ</button> </div>
</form> <div class="col-md-3">
<div class="card p-3">
<div class="d-flex align-items-center">
<div class="bg-success text-white p-3 rounded-4 me-3">
<i class="bi bi-check-circle fs-3"></i>
</div>
<div>
<div class="text-muted small"><?= __('ready_orders') ?></div>
<div class="fw-bold fs-5"><?= $stats['ready_orders'] ?></div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card p-3">
<div class="d-flex align-items-center">
<div class="bg-info text-white p-3 rounded-4 me-3">
<i class="bi bi-people fs-3"></i>
</div>
<div>
<div class="text-muted small"><?= __('new_customers') ?></div>
<div class="fw-bold fs-5"><?= $stats['new_customers'] ?></div>
</div>
</div>
</div>
</div>
</div> </div>
<h3>Existing FAQs</h3> <div class="row g-4 mb-4">
<table class="table"> <div class="col-md-8">
<thead> <div class="card p-4 h-100">
<tr> <h6 class="fw-bold mb-4"><?= __('revenue_last_7_days') ?? 'Revenue (Last 7 Days)' ?></h6>
<th>Keywords</th> <canvas id="revenueChart" height="100"></canvas>
<th>Answer</th> </div>
<th>Actions</th> </div>
</tr> <div class="col-md-4">
</thead> <div class="card p-4 h-100">
<tbody> <h6 class="fw-bold mb-4"><?= __('orders_by_status') ?></h6>
<?php foreach ($faqs as $faq): ?> <canvas id="statusChart"></canvas>
<tr> </div>
<td><?= htmlspecialchars($faq['keywords']) ?></td> </div>
<td><?= htmlspecialchars($faq['answer']) ?></td>
<td>
<form method="POST" style="display:inline;" onsubmit="return confirm('Delete this FAQ?');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?= $faq['id'] ?>">
<button type="submit" class="btn-delete">Delete</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<h3 style="margin-top: 3rem; margin-bottom: 1rem;">Recent Chat History (Last 50)</h3>
<div style="overflow-x: auto; background: rgba(255, 255, 255, 0.4); padding: 1rem; border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.3);">
<table class="table" style="width: 100%;">
<thead>
<tr>
<th style="width: 15%;">Time</th>
<th style="width: 35%;">User Message</th>
<th style="width: 50%;">AI Response</th>
</tr>
</thead>
<tbody>
<?php if (empty($messages)): ?>
<tr>
<td colspan="3" style="text-align: center; color: #777;">No messages yet.</td>
</tr>
<?php else: ?>
<?php foreach ($messages as $msg): ?>
<tr>
<td style="white-space: nowrap; font-size: 0.85em; color: #555;"><?= htmlspecialchars($msg['created_at']) ?></td>
<td style="background: rgba(255, 255, 255, 0.3); border-radius: 8px; padding: 8px;"><?= htmlspecialchars($msg['user_message']) ?></td>
<td style="background: rgba(255, 255, 255, 0.5); border-radius: 8px; padding: 8px;"><?= htmlspecialchars($msg['ai_response']) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div> </div>
</div>
</body> <div class="row g-4">
</html> <div class="col-md-8">
<div class="card p-4 h-100">
<h5 class="fw-bold mb-4"><?= __('orders_per_month') ?></h5>
<canvas id="monthlyOrdersChart" height="120"></canvas>
</div>
</div>
<div class="col-md-4">
<div class="card p-4 h-100 bg-primary text-white">
<h5 class="fw-bold mb-4"><?= __('quick_actions') ?></h5>
<a href="pos.php" class="btn btn-light w-100 mb-3 text-start py-3 px-4" style="border-radius: 15px; font-weight: 600;">
<i class="bi bi-plus-circle-fill me-2"></i> <?= __('new_order') ?>
</a>
<a href="customers.php" class="btn btn-light w-100 mb-3 text-start py-3 px-4" style="border-radius: 15px; font-weight: 600;">
<i class="bi bi-person-plus-fill me-2"></i> <?= __('add_new_customer') ?>
</a>
<a href="reports.php" class="btn btn-light w-100 text-start py-3 px-4" style="border-radius: 15px; font-weight: 600;">
<i class="bi bi-file-earmark-bar-graph-fill me-2"></i> <?= __('reports') ?>
</a>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Revenue Chart
if (document.getElementById('revenueChart')) {
new Chart(document.getElementById('revenueChart'), {
type: 'line',
data: {
labels: <?= json_encode(array_column($daily_revenue, 'date')) ?>,
datasets: [{
label: '<?= __('revenue') ?>',
data: <?= json_encode(array_column($daily_revenue, 'revenue')) ?>,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: { y: { beginAtZero: true } }
}
});
}
// Status Chart
if (document.getElementById('statusChart')) {
new Chart(document.getElementById('statusChart'), {
type: 'doughnut',
data: {
labels: <?= json_encode(array_map(function($s) { return __($s['status']); }, $orders_by_status)) ?>,
datasets: [{
data: <?= json_encode(array_column($orders_by_status, 'count')) ?>,
backgroundColor: ['#6c757d', '#3b82f6', '#10b981', '#111827', '#ef4444']
}]
},
options: {
responsive: true,
plugins: { legend: { position: 'bottom' } }
}
});
}
// Monthly Orders Chart
if (document.getElementById('monthlyOrdersChart')) {
new Chart(document.getElementById('monthlyOrdersChart'), {
type: 'bar',
data: {
labels: <?= json_encode(array_column($monthly_orders, 'month_year')) ?>,
datasets: [{
label: '<?= __('orders') ?>',
data: <?= json_encode(array_column($monthly_orders, 'count')) ?>,
backgroundColor: '#10b981',
borderRadius: 8
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
}
</script>
<?php endif; ?>
<?php
require_once __DIR__ . '/includes/footer.php';
?>

View File

@ -2,41 +2,11 @@
// OpenAI proxy configuration (workspace scope). // OpenAI proxy configuration (workspace scope).
// Reads values from environment variables or executor/.env. // Reads values from environment variables or executor/.env.
require_once __DIR__ . '/../includes/dotenv.php';
$projectUuid = getenv('PROJECT_UUID'); $projectUuid = getenv('PROJECT_UUID');
$projectId = getenv('PROJECT_ID'); $projectId = getenv('PROJECT_ID');
if (
($projectUuid === false || $projectUuid === null || $projectUuid === '') ||
($projectId === false || $projectId === null || $projectId === '')
) {
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
if ($envPath && is_readable($envPath)) {
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
if (!str_contains($line, '=')) {
continue;
}
[$key, $value] = array_map('trim', explode('=', $line, 2));
if ($key === '') {
continue;
}
$value = trim($value, "\"' ");
if (getenv($key) === false || getenv($key) === '') {
putenv("{$key}={$value}");
}
}
$projectUuid = getenv('PROJECT_UUID');
$projectId = getenv('PROJECT_ID');
}
}
$projectUuid = ($projectUuid === false) ? null : $projectUuid;
$projectId = ($projectId === false) ? null : $projectId;
$baseUrl = 'https://flatlogic.com'; $baseUrl = 'https://flatlogic.com';
$responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null; $responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null;
@ -49,4 +19,4 @@ return [
'default_model' => 'gpt-5-mini', 'default_model' => 'gpt-5-mini',
'timeout' => 30, 'timeout' => 30,
'verify_tls' => true, 'verify_tls' => true,
]; ];

37
api/add_customer.php Normal file
View File

@ -0,0 +1,37 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
session_start();
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
$phone = $_POST['phone'] ?? '';
$name_en = $_POST['name_en'] ?? '';
$name_ar = $_POST['name_ar'] ?? '';
$branch_id = $_SESSION['branch_id'];
if (!$phone || !$name_en) {
echo json_encode(['success' => false, 'error' => 'Phone and English Name are required']);
exit;
}
try {
$stmt = db()->prepare("INSERT INTO customers (phone, name_en, name_ar) VALUES (?, ?, ?)");
$stmt->execute([$phone, $name_en, $name_ar]);
$customer_id = db()->lastInsertId();
echo json_encode([
'success' => true,
'customer' => [
'id' => $customer_id,
'phone' => $phone,
'name_en' => $name_en,
'name_ar' => $name_ar
]
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

View File

@ -0,0 +1,24 @@
<?php
require_once __DIR__ . '/../db/config.php';
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: ../login.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$phone = $_POST['phone'] ?? '';
$name_en = $_POST['name_en'] ?? '';
$name_ar = $_POST['name_ar'] ?? '';
$email = $_POST['email'] ?? '';
$branch_id = $_SESSION['branch_id'];
if ($phone && $name_en) {
$stmt = db()->prepare("INSERT INTO customers (phone, name_en, name_ar, email) VALUES (?, ?, ?, ?)");
$stmt->execute([$phone, $name_en, $name_ar, $email]);
}
}
header('Location: ../customers.php');
exit;

170
api/checkout.php Normal file
View File

@ -0,0 +1,170 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/whatsapp.php';
session_start();
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$order_id = $input['order_id'] ?? null;
$customer_id = $input['customer_id'] ?: null;
$items = $input['items'] ?? [];
$vat_total = (float)($input['vat_total'] ?? 0);
$total_price = (float)($input['total_price'] ?? 0);
$payment_method = $input['payment_method'] ?? null;
$points_to_redeem = (float)($input['points_to_redeem'] ?? 0);
$branch_id = $_SESSION['branch_id'];
$user_id = $_SESSION['user_id'];
// Check if branch_id is 'all'
if ($branch_id === 'all') {
try {
$pdo = db();
$stmt_u = $pdo->prepare("SELECT branch_id FROM users WHERE id = ?");
$stmt_u->execute([$user_id]);
$u_branch = $stmt_u->fetchColumn();
if ($u_branch && $u_branch != 'all') {
$branch_id = $u_branch;
} else {
$stmt_ub = $pdo->prepare("SELECT branch_id FROM user_branches WHERE user_id = ? LIMIT 1");
$stmt_ub->execute([$user_id]);
$ub_branch = $stmt_ub->fetchColumn();
$branch_id = $ub_branch ?: $pdo->query("SELECT id FROM branches LIMIT 1")->fetchColumn();
}
} catch (Exception $e) {}
}
if (empty($items)) {
echo json_encode(['success' => false, 'error' => 'Cart is empty']);
exit;
}
try {
$pdo = db();
$pdo->beginTransaction();
$payment_status = ($payment_method && $payment_method !== 'pay_later') ? 'paid' : 'unpaid';
if ($order_id) {
$stmt = $pdo->prepare("UPDATE orders SET customer_id = ?, total_price = ?, vat_total = ?, payment_status = ? WHERE id = ? AND branch_id = ?");
$stmt->execute([$customer_id, $total_price, $vat_total, $payment_status, $order_id, $branch_id]);
$stmt = $pdo->prepare("DELETE FROM order_items WHERE order_id = ?");
$stmt->execute([$order_id]);
} else {
$stmt = $pdo->prepare("INSERT INTO orders (branch_id, customer_id, user_id, order_number, total_price, vat_total, status, payment_status)
VALUES (?, ?, ?, NULL, ?, ?, 'received', ?)");
$stmt->execute([$branch_id, $customer_id, $user_id, $total_price, $vat_total, $payment_status]);
$order_id = $pdo->lastInsertId();
// Order Number Generation
$stmt_prefix = $pdo->prepare("SELECT prefix FROM branches WHERE id = ?");
$stmt_prefix->execute([$branch_id]);
$prefix = strtoupper(str_pad(substr($stmt_prefix->fetchColumn() ?: 'ORD', 0, 3), 3, 'X'));
$stmt_max = $pdo->prepare("SELECT order_number FROM orders WHERE branch_id = ? AND order_number LIKE ? ORDER BY id DESC LIMIT 100");
$stmt_max->execute([$branch_id, "$prefix-%"]);
$existing = $stmt_max->fetchAll(PDO::FETCH_COLUMN);
$max_serial = 0;
foreach ($existing as $onum) {
if (preg_match('/' . preg_quote($prefix) . '-(\d{6})/', $onum, $matches)) {
if ((int)$matches[1] > $max_serial) $max_serial = (int)$matches[1];
}
}
$order_number = $prefix . '-' . str_pad($max_serial + 1, 6, '0', STR_PAD_LEFT);
$pdo->prepare("UPDATE orders SET order_number = ? WHERE id = ?")->execute([$order_number, $order_id]);
}
$stmt_item = $pdo->prepare("INSERT INTO order_items (order_id, item_id, variant_id, service_id, quantity, unit_price, vat_amount, subtotal) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
foreach ($items as $item) {
$stmt_item->execute([$order_id, $item['itemId'], $item['variantId'] ?: null, $item['serviceId'], $item['quantity'], $item['price'], $item['vatAmount'] ?: 0, ($item['price'] * $item['quantity'])]);
}
// Loyalty Redemption Logic
$redeem_amount = 0;
if ($customer_id && $points_to_redeem > 0 && get_setting('loyalty_enabled') === '1') {
$stmt_c = $pdo->prepare("SELECT loyalty_points FROM customers WHERE id = ? FOR UPDATE");
$stmt_c->execute([$customer_id]);
$curr_points = (float)$stmt_c->fetchColumn();
if ($curr_points >= $points_to_redeem) {
$point_val = (float)get_setting('loyalty_currency_per_point', 0.05);
$redeem_amount = $points_to_redeem * $point_val;
// Cap redemption at order total
if ($redeem_amount > $total_price) {
$redeem_amount = $total_price;
$points_to_redeem = $redeem_amount / $point_val;
}
// Deduct points
$pdo->prepare("UPDATE customers SET loyalty_points = loyalty_points - ? WHERE id = ?")->execute([$points_to_redeem, $customer_id]);
// Record loyalty transaction
$pdo->prepare("INSERT INTO loyalty_transactions (customer_id, order_id, points, type, description) VALUES (?, ?, ?, 'redeemed', ?)")
->execute([$customer_id, $order_id, -$points_to_redeem, "Redeemed for order $order_number"]);
// Record loyalty payment
$pdo->prepare("INSERT INTO payments (order_id, amount, payment_method) VALUES (?, ?, 'loyalty')")
->execute([$order_id, $redeem_amount]);
// Save loyalty discount to order
$pdo->prepare("UPDATE orders SET loyalty_discount = ? WHERE id = ?")
->execute([$redeem_amount, $order_id]);
}
}
// Handle Remaining Payment
$remaining_to_pay = $total_price - $redeem_amount;
if ($payment_method && $payment_method !== 'pay_later' && $remaining_to_pay > 0) {
$pdo->prepare("INSERT INTO payments (order_id, amount, payment_method) VALUES (?, ?, ?)")
->execute([$order_id, $remaining_to_pay, $payment_method]);
}
// Loyalty Earning Logic
if ($customer_id && get_setting('loyalty_enabled') === '1' && $payment_status === 'paid') {
$pts_per_curr = (float)get_setting('loyalty_points_per_currency', 1);
$points_earned = $remaining_to_pay * $pts_per_curr;
if ($points_earned > 0) {
$pdo->prepare("UPDATE customers SET loyalty_points = loyalty_points + ? WHERE id = ?")->execute([$points_earned, $customer_id]);
$pdo->prepare("INSERT INTO loyalty_transactions (customer_id, order_id, points, type, description) VALUES (?, ?, ?, 'earned', ?)")
->execute([$customer_id, $order_id, $points_earned, "Earned from order $order_number"]);
}
}
$customer = null;
if ($customer_id) {
$stmt_cust = $pdo->prepare('SELECT name_ar, phone FROM customers WHERE id = ?');
$stmt_cust->execute([$customer_id]);
$customer = $stmt_cust->fetch();
}
$pdo->commit();
// WhatsApp
try {
if (get_setting('whatsapp_enabled') === '1' && $customer && !empty($customer['phone'])) {
$template = get_setting('msg_order_created_ar');
if (!empty($template)) {
$details_parts = [];
foreach ($items as $item) {
$stmt_names = $pdo->prepare('SELECT i.name_ar as item_name, s.name_ar as service_name FROM items i, services s WHERE i.id = ? AND s.id = ?');
$stmt_names->execute([$item['itemId'], $item['serviceId']]);
$names = $stmt_names->fetch();
$details_parts[] = ($names['item_name'] ?? 'صنف') . ' (' . ($names['service_name'] ?? 'خدمة') . ') x' . $item['quantity'];
}
$message = str_replace(['{customer_name}', '{order_number}', '{order_details}', '{total_price}'], [$customer['name_ar'], $order_number, implode(', ', $details_parts), $total_price], $template);
send_whatsapp_message($customer['phone'], $message);
}
}
} catch (Exception $e) {}
echo json_encode(['success' => true, 'order_id' => $order_id]);
} catch (Exception $e) {
if (isset($pdo)) $pdo->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

31
api/delete_order.php Normal file
View File

@ -0,0 +1,31 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
session_start();
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? null;
if (!$id) {
echo json_encode(['success' => false, 'error' => 'Missing order ID']);
exit;
}
try {
$pdo = db();
$stmt = $pdo->prepare("DELETE FROM orders WHERE id = ?");
$stmt->execute([$id]);
if ($stmt->rowCount() > 0) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Order not found or already deleted']);
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

35
api/get_order_items.php Normal file
View File

@ -0,0 +1,35 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/lang.php';
$order_id = $_GET['id'] ?? null;
if (!$order_id) {
echo json_encode(['success' => false, 'error' => 'Order ID is required']);
exit;
}
try {
$stmt = db()->prepare("SELECT oi.quantity, i.name_en as item_en, i.name_ar as item_ar,
s.name_en as service_en, s.name_ar as service_ar
FROM order_items oi
JOIN items i ON oi.item_id = i.id
JOIN services s ON oi.service_id = s.id
WHERE oi.order_id = ?");
$stmt->execute([$order_id]);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Fetch order number for the modal title
$stmt = db()->prepare("SELECT order_number FROM orders WHERE id = ?");
$stmt->execute([$order_id]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'order_number' => $order['order_number'] ?? '',
'items' => $items
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

View File

@ -0,0 +1,21 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/lang.php';
if (!isset($_SESSION['user_id'])) {
echo json_encode(['error' => 'Unauthorized']);
exit;
}
$user_id = $_GET['user_id'] ?? null;
if (!$user_id) {
echo json_encode(['error' => 'User ID required']);
exit;
}
$stmt = db()->prepare("SELECT page, can_view, can_add, can_edit, can_delete FROM user_permissions WHERE user_id = ?");
$stmt->execute([$user_id]);
$permissions = $stmt->fetchAll();
echo json_encode($permissions);

25
api/search_customers.php Normal file
View File

@ -0,0 +1,25 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
session_start();
if (!isset($_SESSION['user_id'])) {
echo json_encode([]);
exit;
}
$query = $_GET['query'] ?? '';
if (strlen($query) < 2) {
echo json_encode([]);
exit;
}
try {
$stmt = db()->prepare("SELECT id, name_en, name_ar, phone, loyalty_points FROM customers WHERE name_en LIKE ? OR name_ar LIKE ? OR phone LIKE ? LIMIT 10");
$stmt->execute(["%$query%", "%$query%", "%$query%"]);
$customers = $stmt->fetchAll();
echo json_encode($customers);
} catch (Exception $e) {
echo json_encode([]);
}

42
api/translate.php Normal file
View File

@ -0,0 +1,42 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../ai/LocalAIApi.php';
session_start();
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
$text = $_POST['text'] ?? '';
$direction = $_POST['direction'] ?? 'en-ar'; // en-ar or ar-en
if (empty($text)) {
echo json_encode(['success' => false, 'error' => 'Text is empty']);
exit;
}
$target_lang = $direction === 'en-ar' ? 'Arabic' : 'English';
$source_lang = $direction === 'en-ar' ? 'English' : 'Arabic';
try {
$prompt = "Translate the following text from $source_lang to $target_lang. Provide ONLY the translated text, no explanation or extra characters.\n\nText: $text";
$response = LocalAIApi::createResponse([
'input' => [
['role' => 'system', 'content' => 'You are a professional translator specializing in business and laundry service terminology.'],
['role' => 'user', 'content' => $prompt],
],
]);
if (!empty($response['success'])) {
$translatedText = LocalAIApi::extractText($response);
echo json_encode(['success' => true, 'translation' => trim($translatedText)]);
} else {
echo json_encode(['success' => false, 'error' => $response['error'] ?? 'AI translation failed']);
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

View File

@ -0,0 +1,64 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/whatsapp.php';
session_start();
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? null;
$status = $input['status'] ?? null;
if (!$id || !$status) {
echo json_encode(['success' => false, 'error' => 'Missing order ID or status']);
exit;
}
$valid_statuses = ['received', 'processing', 'ready', 'delivered', 'cancelled'];
if (!in_array($status, $valid_statuses)) {
echo json_encode(['success' => false, 'error' => 'Invalid status']);
exit;
}
try {
$pdo = db();
$stmt = $pdo->prepare("UPDATE orders SET status = ? WHERE id = ?");
$stmt->execute([$status, $id]);
if ($stmt->rowCount() > 0) {
// WhatsApp Notification for Order Ready
try {
if ($status === 'ready') {
if (get_setting('whatsapp_enabled') === '1') {
$stmt_details = $pdo->prepare('SELECT o.order_number, c.name_ar, c.phone FROM orders o JOIN customers c ON o.customer_id = c.id WHERE o.id = ?');
$stmt_details->execute([$id]);
$order = $stmt_details->fetch();
if ($order && !empty($order['phone'])) {
$template = get_setting('msg_order_ready_ar');
if (!empty($template)) {
$message = str_replace(
['{customer_name}', '{order_number}'],
[$order['name_ar'], $order['order_number']],
$template
);
send_whatsapp_message($order['phone'], $message);
}
}
}
}
} catch (Exception $e) {
// Silently fail for notification
}
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Order not found or status already same']);
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

39
api/update_setting.php Normal file
View File

@ -0,0 +1,39 @@
<?php
session_start();
require_once __DIR__ . '/../db/config.php';
header('Content-Type: application/json');
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'super_admin') {
http_response_code(403);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Method not allowed']);
exit;
}
$key = $_POST['key'] ?? '';
$value = $_POST['value'] ?? '';
if (empty($key)) {
echo json_encode(['success' => false, 'error' => 'Missing key']);
exit;
}
// Basic security check to ensure we only update allowed settings
$allowed_keys = ['whatsapp_enabled', 'wablas_token', 'wablas_server', 'wablas_security_key'];
if (!in_array($key, $allowed_keys)) {
echo json_encode(['success' => false, 'error' => 'Invalid key']);
exit;
}
try {
set_setting($key, $value);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

View File

@ -1,302 +1,141 @@
:root {
--bs-primary: #3b82f6;
--bs-primary-rgb: 59, 130, 246;
--bs-success: #10b981;
--bs-warning: #f59e0b;
--bs-danger: #ef4444;
}
body { body {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); background-color: #f3f4f6;
background-size: 400% 400%;
animation: gradient 15s ease infinite;
color: #212529;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
margin: 0;
min-height: 100vh;
} }
.main-wrapper { .card {
display: flex; border-radius: 1.25rem;
align-items: center; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.025);
justify-content: center;
min-height: 100vh;
width: 100%;
padding: 20px;
box-sizing: border-box;
position: relative;
z-index: 1;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.chat-container {
width: 100%;
max-width: 600px;
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 20px;
display: flex;
flex-direction: column;
height: 85vh;
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
overflow: hidden;
}
.chat-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background: rgba(255, 255, 255, 0.5);
font-weight: 700;
font-size: 1.1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
.message {
max-width: 85%;
padding: 0.85rem 1.1rem;
border-radius: 16px;
line-height: 1.5;
font-size: 0.95rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.message.visitor {
align-self: flex-end;
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
color: #fff;
border-bottom-right-radius: 4px;
}
.message.bot {
align-self: flex-start;
background: #ffffff;
color: #212529;
border-bottom-left-radius: 4px;
}
.chat-input-area {
padding: 1.25rem;
background: rgba(255, 255, 255, 0.5);
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.chat-input-area form {
display: flex;
gap: 0.75rem;
}
.chat-input-area input {
flex: 1;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 0.75rem 1rem;
outline: none;
background: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
}
.chat-input-area input:focus {
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
}
.chat-input-area button {
background: #212529;
color: #fff;
border: none; border: none;
padding: 0.75rem 1.5rem; }
border-radius: 12px;
cursor: pointer; .btn-primary {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
border-radius: 0.75rem;
}
.btn-primary:hover {
background-color: #2563eb;
border-color: #2563eb;
}
.sidebar {
background: #111827 !important;
}
.sidebar .nav-link {
font-weight: 500;
transition: all 0.2s;
}
.sidebar .nav-link:hover {
background: rgba(255, 255, 255, 0.05);
}
.sidebar .nav-link.active {
background: var(--bs-primary) !important;
}
.badge {
font-weight: 600; font-weight: 600;
transition: all 0.3s ease; padding: 0.5em 1em;
} }
.chat-input-area button:hover { /* Soft Badges */
background: #000; .badge-soft-success {
transform: translateY(-2px); background-color: rgba(16, 185, 129, 0.15) !important;
box-shadow: 0 5px 15px rgba(0,0,0,0.2); color: #10b981 !important;
}
.badge-soft-danger {
background-color: rgba(239, 68, 68, 0.15) !important;
color: #ef4444 !important;
}
.badge-soft-warning {
background-color: rgba(245, 158, 11, 0.15) !important;
color: #f59e0b !important;
}
.badge-soft-primary {
background-color: rgba(59, 130, 246, 0.15) !important;
color: #3b82f6 !important;
}
.badge-soft-info {
background-color: rgba(6, 182, 212, 0.15) !important;
color: #06b6d4 !important;
}
.badge-soft-secondary {
background-color: rgba(107, 114, 128, 0.15) !important;
color: #6b7280 !important;
} }
/* Background Animations */ .table thead th {
.bg-animations { background-color: #f9fafb;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
pointer-events: none;
}
.blob {
position: absolute;
width: 500px;
height: 500px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
filter: blur(80px);
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
}
.blob-1 {
top: -10%;
left: -10%;
background: rgba(238, 119, 82, 0.4);
}
.blob-2 {
bottom: -10%;
right: -10%;
background: rgba(35, 166, 213, 0.4);
animation-delay: -7s;
width: 600px;
height: 600px;
}
.blob-3 {
top: 40%;
left: 30%;
background: rgba(231, 60, 126, 0.3);
animation-delay: -14s;
width: 450px;
height: 450px;
}
@keyframes move {
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
100% { transform: translate(0, 0) rotate(360deg) scale(1); }
}
.admin-link {
font-size: 14px;
color: #fff;
text-decoration: none;
background: rgba(0, 0, 0, 0.2);
padding: 0.5rem 1rem;
border-radius: 8px;
transition: all 0.3s ease;
}
.admin-link:hover {
background: rgba(0, 0, 0, 0.4);
text-decoration: none;
}
/* Admin Styles */
.admin-container {
max-width: 900px;
margin: 3rem auto;
padding: 2.5rem;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 24px;
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
border: 1px solid rgba(255, 255, 255, 0.4);
position: relative;
z-index: 1;
}
.admin-container h1 {
margin-top: 0;
color: #212529;
font-weight: 800;
}
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
margin-top: 1.5rem;
}
.table th {
background: transparent;
border: none;
padding: 1rem;
color: #6c757d;
font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
font-size: 0.75rem; font-size: 0.75rem;
letter-spacing: 1px; letter-spacing: 0.05em;
color: #6b7280;
border-bottom-width: 1px;
} }
.table td { .form-control, .form-select {
background: #fff; border-radius: 0.75rem;
padding: 1rem; padding: 0.625rem 1rem;
border: none; border: 1px solid #d1d5db;
background-color: #fff;
} }
.table tr td:first-child { border-radius: 12px 0 0 12px; } .form-control:focus, .form-select:focus {
.table tr td:last-child { border-radius: 0 12px 12px 0; } border-color: var(--bs-primary);
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
.form-group {
margin-bottom: 1.25rem;
} }
.form-group label { /* RTL Support for Cairo Font */
display: block; [dir="rtl"] {
margin-bottom: 0.5rem; font-family: 'Cairo', sans-serif !important;
font-weight: 600;
font-size: 0.9rem;
} }
.form-control { .cursor-pointer {
width: 100%; cursor: pointer;
padding: 0.75rem 1rem;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
background: #fff;
transition: all 0.3s ease;
box-sizing: border-box;
} }
.form-control:focus { .item-card:hover {
outline: none; transform: scale(1.02);
border-color: #23a6d5; }
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
/* Print Styles */
@media print {
.sidebar, .navbar, .no-print, .btn, .card form, header, .breadcrumb, footer {
display: none !important;
}
body {
background-color: white !important;
margin: 0;
padding: 0;
}
.main-content {
margin-left: 0 !important;
margin-right: 0 !important;
width: 100% !important;
padding: 0 !important;
}
.card {
box-shadow: none !important;
border: 1px solid #eee !important;
margin-bottom: 20px !important;
page-break-inside: avoid;
}
.container-fluid {
padding: 0 !important;
}
canvas {
max-width: 100% !important;
height: auto !important;
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -3,37 +3,39 @@ document.addEventListener('DOMContentLoaded', () => {
const chatInput = document.getElementById('chat-input'); const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages'); const chatMessages = document.getElementById('chat-messages');
const appendMessage = (text, sender) => { if (chatForm && chatInput && chatMessages) {
const msgDiv = document.createElement('div'); const appendMessage = (text, sender) => {
msgDiv.classList.add('message', sender); const msgDiv = document.createElement('div');
msgDiv.textContent = text; msgDiv.classList.add('message', sender);
chatMessages.appendChild(msgDiv); msgDiv.textContent = text;
chatMessages.scrollTop = chatMessages.scrollHeight; chatMessages.appendChild(msgDiv);
}; chatMessages.scrollTop = chatMessages.scrollHeight;
};
chatForm.addEventListener('submit', async (e) => { chatForm.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const message = chatInput.value.trim(); const message = chatInput.value.trim();
if (!message) return; if (!message) return;
appendMessage(message, 'visitor'); appendMessage(message, 'visitor');
chatInput.value = ''; chatInput.value = '';
try { try {
const response = await fetch('api/chat.php', { const response = await fetch('api/chat.php', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }) body: JSON.stringify({ message })
}); });
const data = await response.json(); const data = await response.json();
// Artificial delay for realism // Artificial delay for realism
setTimeout(() => { setTimeout(() => {
appendMessage(data.reply, 'bot'); appendMessage(data.reply, 'bot');
}, 500); }, 500);
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
appendMessage("Sorry, something went wrong. Please try again.", 'bot'); appendMessage("Sorry, something went wrong. Please try again.", 'bot');
} }
}); });
}); }
});

255
branches.php Normal file
View File

@ -0,0 +1,255 @@
<?php
// ACTION HANDLING FIRST
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/lang.php';
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
// Initial view check
if (!has_permission('view')) {
header('Location: admin.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
if ($action === 'add_branch' && !has_permission('add')) {
header('Location: branches.php?error=no_permission');
exit;
}
if ($action === 'edit_branch' && !has_permission('edit')) {
header('Location: branches.php?error=no_permission');
exit;
}
if ($action === 'add_branch') {
$name_en = $_POST['name_en'];
$name_ar = $_POST['name_ar'];
$company_id = $_POST['company_id'];
$phone = $_POST['phone'];
$prefix = strtoupper(substr($_POST['prefix'] ?? '', 0, 3));
$stmt = db()->prepare("INSERT INTO branches (name_en, name_ar, company_id, phone, prefix) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$name_en, $name_ar, $company_id, $phone, $prefix]);
header('Location: branches.php?success=branch_added');
exit;
}
if ($action === 'edit_branch') {
$id = $_POST['id'];
$name_en = $_POST['name_en'];
$name_ar = $_POST['name_ar'];
$company_id = $_POST['company_id'];
$phone = $_POST['phone'];
$prefix = strtoupper(substr($_POST['prefix'] ?? '', 0, 3));
$stmt = db()->prepare("UPDATE branches SET name_en = ?, name_ar = ?, company_id = ?, phone = ?, prefix = ? WHERE id = ?");
$stmt->execute([$name_en, $name_ar, $company_id, $phone, $prefix, $id]);
header('Location: branches.php?success=branch_updated');
exit;
}
}
if (isset($_GET['delete'])) {
if (!has_permission('delete')) {
header('Location: branches.php?error=no_permission');
exit;
}
$id = $_GET['delete'];
$stmt = db()->prepare("DELETE FROM branches WHERE id = ?");
$stmt->execute([$id]);
header('Location: branches.php?success=branch_deleted');
exit;
}
// NOW Include header
$title = 'branches';
require_once __DIR__ . '/includes/header.php';
$branches = db()->query("SELECT b.*, c.name_en as company_name_en FROM branches b JOIN companies c ON b.company_id = c.id")->fetchAll();
$companies = db()->query("SELECT * FROM companies")->fetchAll();
?>
<?php if(isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show rounded-4 shadow-sm mb-4 border-0" role="alert">
<i class="bi bi-check-circle-fill me-2"></i>
<?= __($_GET['success']) ?? 'Action completed successfully' ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if(isset($_GET['error'])): ?>
<div class="alert alert-danger alert-dismissible fade show rounded-4 shadow-sm mb-4 border-0" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<?= __($_GET['error']) ?? 'Permission Denied' ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="row">
<div class="col-md-4">
<div class="card p-4 shadow-sm border-0 mb-4" style="border-radius: 20px;">
<?php if (has_permission('add')): ?>
<h5 class="fw-bold mb-4"><?= __('add_new_branch') ?? 'Add New Branch' ?></h5>
<form method="POST">
<input type="hidden" name="action" value="add_branch">
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('company') ?? 'Company' ?></label>
<select name="company_id" class="form-select" style="border-radius: 12px;">
<?php foreach($companies as $c): ?>
<option value="<?= $c['id'] ?>"><?= $lang === 'ar' ? $c['name_ar'] : $c['name_en'] ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('name_en') ?></label>
<input type="text" name="name_en" class="form-control" required style="border-radius: 12px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('name_ar') ?></label>
<input type="text" name="name_ar" class="form-control" style="border-radius: 12px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('initial_letters') ?></label>
<input type="text" name="prefix" class="form-control" maxlength="3" minlength="3" required style="border-radius: 12px;" placeholder="e.g. MCT">
<div class="form-text small"><?= __('exactly_3_letters') ?? 'Exactly 3 letters' ?></div>
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('phone') ?></label>
<input type="text" name="phone" class="form-control" style="border-radius: 12px;">
</div>
<button type="submit" class="btn btn-primary w-100 py-3 fw-bold shadow-sm" style="border-radius: 15px;"><?= __('add_branch') ?></button>
</form>
<?php else: ?>
<div class="text-center py-4">
<i class="bi bi-lock fs-1 text-muted opacity-25"></i>
<p class="text-muted mt-2 small">You don't have permission to add branches.</p>
</div>
<?php endif; ?>
</div>
</div>
<div class="col-md-8">
<div class="card p-0 shadow-sm border-0" style="border-radius: 20px; overflow: hidden;">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr class="py-3">
<th class="ps-4 py-3">#</th>
<th class="py-3"><?= __('name') ?></th>
<th class="py-3"><?= __('initial_letters') ?></th>
<th class="py-3"><?= __('company') ?></th>
<th class="py-3"><?= __('phone') ?></th>
<th class="pe-4 py-3 text-end"><?= __('actions') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($branches as $b): ?>
<tr>
<td class="ps-4"><?= $b['id'] ?></td>
<td class="fw-bold"><?= $lang === 'ar' ? ($b['name_ar'] ?: $b['name_en']) : $b['name_en'] ?></td>
<td class="text-primary fw-bold"><?= $b['prefix'] ?></td>
<td><?= $b['company_name_en'] ?></td>
<td><?= $b['phone'] ?></td>
<td class="pe-4 text-end">
<?php if (has_permission('edit')): ?>
<button class="btn btn-sm btn-outline-primary border-0 edit-branch"
data-id="<?= $b['id'] ?>"
data-name_en="<?= htmlspecialchars($b['name_en']) ?>"
data-name_ar="<?= htmlspecialchars($b['name_ar']) ?>"
data-company_id="<?= $b['company_id'] ?>"
data-phone="<?= htmlspecialchars($b['phone']) ?>"
data-prefix="<?= htmlspecialchars($b['prefix']) ?>"
style="border-radius: 8px;">
<i class="bi bi-pencil"></i>
</button>
<?php endif; ?>
<?php if (has_permission('delete')): ?>
<a href="?delete=<?= $b['id'] ?>" class="btn btn-sm btn-outline-danger border-0"
onclick="return confirm('<?= __('are_you_sure') ?>')"
style="border-radius: 8px;">
<i class="bi bi-trash"></i>
</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($branches)): ?>
<tr>
<td colspan="6" class="text-center py-5 text-muted">
<i class="bi bi-shop fs-1 d-block mb-3 opacity-25"></i>
No branches found.
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Edit Branch Modal -->
<?php if (has_permission('edit')): ?>
<div class="modal fade" id="editBranchModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg" style="border-radius: 20px;">
<div class="modal-header border-0 pb-0">
<h5 class="fw-bold"><?= __('edit') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="edit_branch">
<input type="hidden" name="id" id="edit_id">
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('company') ?? 'Company' ?></label>
<select name="company_id" id="edit_company_id" class="form-select" style="border-radius: 12px;">
<?php foreach($companies as $c): ?>
<option value="<?= $c['id'] ?>"><?= $lang === 'ar' ? $c['name_ar'] : $c['name_en'] ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('name_en') ?></label>
<input type="text" name="name_en" id="edit_name_en" class="form-control" required style="border-radius: 12px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('name_ar') ?></label>
<input type="text" name="name_ar" id="edit_name_ar" class="form-control" style="border-radius: 12px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('initial_letters') ?></label>
<input type="text" name="prefix" id="edit_prefix" class="form-control" maxlength="3" minlength="3" required style="border-radius: 12px;">
<div class="form-text small"><?= __('exactly_3_letters') ?? 'Exactly 3 letters' ?></div>
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('phone') ?></label>
<input type="text" name="phone" id="edit_phone" class="form-control" style="border-radius: 12px;">
</div>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-light fw-bold" data-bs-dismiss="modal" style="border-radius: 12px;"><?= __('cancel') ?></button>
<button type="submit" class="btn btn-primary fw-bold shadow-sm" style="border-radius: 12px;"><?= __('save_changes') ?></button>
</div>
</form>
</div>
</div>
</div>
<script>
document.querySelectorAll('.edit-branch').forEach(btn => {
btn.addEventListener('click', function() {
const d = this.dataset;
document.getElementById('edit_id').value = d.id;
document.getElementById('edit_name_en').value = d.name_en;
document.getElementById('edit_name_ar').value = d.name_ar;
document.getElementById('edit_company_id').value = d.company_id;
document.getElementById('edit_phone').value = d.phone;
document.getElementById('edit_prefix').value = d.prefix;
new bootstrap.Modal(document.getElementById('editBranchModal')).show();
});
});
</script>
<?php endif; ?>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

307
company_profile.php Normal file
View File

@ -0,0 +1,307 @@
<?php
$title = 'company_profile';
require_once __DIR__ . '/includes/header.php';
if ($current_role === 'limited_viewer') { header('Location: admin.php'); exit; }
if ($current_role !== 'super_admin') {
echo '<div class="alert alert-danger">' . __('Access Denied') . '</div>';
require_once __DIR__ . '/includes/footer.php';
exit;
}
$success = '';
$error = '';
$is_super = ($current_role === 'super_admin');
$stmt = db()->query("SELECT * FROM companies LIMIT 1");
$company = $stmt->fetch();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['save_company_profile'])) {
$name_en = $_POST['name_en'] ?? '';
$name_ar = $_POST['name_ar'] ?? '';
$email = $_POST['email'] ?? '';
$phone = $_POST['phone'] ?? '';
$address_en = $_POST['address_en'] ?? '';
$address_ar = $_POST['address_ar'] ?? '';
$vat_no = $_POST['vat_no'] ?? '';
$ctr_no = $_POST['ctr_no'] ?? '';
$logo = $company['logo'];
if (isset($_FILES['logo']) && !empty($_FILES['logo']['name'])) {
if ($_FILES['logo']['error'] === UPLOAD_ERR_OK) {
$ext = pathinfo($_FILES['logo']['name'], PATHINFO_EXTENSION);
$filename = 'logo_' . time() . '.' . $ext;
$rel_path = 'assets/images/company/' . $filename;
$abs_path = __DIR__ . '/' . $rel_path;
if (!is_dir(dirname($abs_path))) mkdir(dirname($abs_path), 0775, true);
if (move_uploaded_file($_FILES['logo']['tmp_name'], $abs_path)) {
$logo = $rel_path;
} else {
$error .= "Failed to move uploaded logo. ";
}
} else {
$error .= "Error uploading logo (Code: " . $_FILES['logo']['error'] . "). ";
}
}
$favicon = $company['favicon'];
if (isset($_FILES['favicon']) && !empty($_FILES['favicon']['name'])) {
if ($_FILES['favicon']['error'] === UPLOAD_ERR_OK) {
$ext = pathinfo($_FILES['favicon']['name'], PATHINFO_EXTENSION);
$filename = 'favicon_' . time() . '.' . $ext;
$rel_path = 'assets/images/company/' . $filename;
$abs_path = __DIR__ . '/' . $rel_path;
if (!is_dir(dirname($abs_path))) mkdir(dirname($abs_path), 0775, true);
if (move_uploaded_file($_FILES['favicon']['tmp_name'], $abs_path)) {
$favicon = $rel_path;
} else {
$error .= "Failed to move uploaded favicon. ";
}
} else {
$error .= "Error uploading favicon (Code: " . $_FILES['favicon']['error'] . "). ";
}
}
try {
$stmt = db()->prepare("UPDATE companies SET name_en = ?, name_ar = ?, logo = ?, favicon = ?, email = ?, phone = ?, address_en = ?, address_ar = ?, vat_number = ?, ctr_no = ?, vat_no = ? WHERE id = ?");
$stmt->execute([$name_en, $name_ar, $logo, $favicon, $email, $phone, $address_en, $address_ar, $vat_no, $ctr_no, $vat_no, $company['id']]);
$success = __('success_update');
$stmt = db()->query("SELECT * FROM companies LIMIT 1");
$company = $stmt->fetch();
} catch (Exception $e) { $error = __('error_update') . ' ' . $e->getMessage(); }
}
if ($is_super && isset($_POST['save_whatsapp_settings'])) {
set_setting('whatsapp_enabled', $_POST['whatsapp_enabled'] ?? '0');
set_setting('wablas_token', $_POST['wablas_token'] ?? '');
set_setting('wablas_server', $_POST['wablas_server'] ?? '');
set_setting('wablas_security_key', $_POST['wablas_security_key'] ?? '');
set_setting('msg_order_created_ar', $_POST['msg_order_created_ar'] ?? '');
set_setting('msg_order_ready_ar', $_POST['msg_order_ready_ar'] ?? '');
set_setting('msg_payment_ar', $_POST['msg_payment_ar'] ?? '');
$success = __('success_update');
}
if ($is_super && isset($_POST['save_loyalty_settings'])) {
set_setting('loyalty_enabled', $_POST['loyalty_enabled'] ?? '0');
set_setting('loyalty_points_per_currency', $_POST['loyalty_points_per_currency'] ?? '1');
set_setting('loyalty_currency_per_point', $_POST['loyalty_currency_per_point'] ?? '0.05');
$success = __('success_update');
}
if ($is_super && isset($_POST['save_smtp_settings'])) {
set_setting('smtp_host', $_POST['smtp_host'] ?? '');
set_setting('smtp_port', $_POST['smtp_port'] ?? '587');
set_setting('smtp_user', $_POST['smtp_user'] ?? '');
set_setting('smtp_pass', $_POST['smtp_pass'] ?? '');
set_setting('smtp_secure', $_POST['smtp_secure'] ?? 'tls');
$success = __('success_update');
}
}
// Ensure SMTP settings exist for display fallback
if ($is_super) {
if (get_setting('smtp_host') === null) {
$env_host = getenv('SMTP_HOST');
set_setting('smtp_host', $env_host !== false ? $env_host : '');
$env_port = getenv('SMTP_PORT');
set_setting('smtp_port', $env_port !== false ? $env_port : '587');
$env_user = getenv('SMTP_USER');
set_setting('smtp_user', $env_user !== false ? $env_user : '');
$env_pass = getenv('SMTP_PASS');
set_setting('smtp_pass', $env_pass !== false ? $env_pass : '');
$env_secure = getenv('SMTP_SECURE');
set_setting('smtp_secure', $env_secure !== false ? $env_secure : 'tls');
}
}
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h3 mb-0"><?= __('company_profile') ?></h2>
</div>
<?php if ($success): ?>
<div class="alert alert-success alert-dismissible fade show rounded-4 border-0 shadow-sm" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> <?= $success ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show rounded-4 border-0 shadow-sm" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> <?= $error ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<ul class="nav nav-pills mb-4" id="settingsTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active px-4 rounded-pill me-2 fw-semibold" id="main-tab" data-bs-toggle="pill" data-bs-target="#main-settings" type="button" role="tab"><i class="bi bi-building me-2"></i> <?= is_arabic() ? 'الإعدادات العامة' : 'Main Settings' ?></button>
</li>
<?php if ($is_super): ?>
<li class="nav-item" role="presentation">
<button class="nav-link px-4 rounded-pill me-2 fw-semibold" id="whatsapp-tab" data-bs-toggle="pill" data-bs-target="#whatsapp-settings" type="button" role="tab"><i class="bi bi-whatsapp me-2"></i> <?= __('whatsapp_settings') ?></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link px-4 rounded-pill me-2 fw-semibold" id="loyalty-tab" data-bs-toggle="pill" data-bs-target="#loyalty-settings" type="button" role="tab"><i class="bi bi-star-fill me-2"></i> <?= __('loyalty_settings') ?></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link px-4 rounded-pill fw-semibold" id="smtp-tab" data-bs-toggle="pill" data-bs-target="#smtp-settings" type="button" role="tab"><i class="bi bi-envelope me-2"></i> <?= __('smtp_settings') ?></button>
</li>
<?php endif; ?>
</ul>
<div class="tab-content" id="settingsTabsContent">
<!-- MAIN SETTINGS -->
<div class="tab-pane fade show active" id="main-settings" role="tabpanel">
<div class="card p-4 shadow-sm border-0" style="border-radius: 20px;">
<h5 class="fw-bold mb-4"><i class="bi bi-building me-2"></i> <?= is_arabic() ? 'معلومات الشركة' : 'Company Information' ?></h5>
<form action="company_profile.php" method="POST" enctype="multipart/form-data">
<div class="row">
<div class="col-md-4 mb-3"><label class="form-label small fw-bold"><?= __('name_en') ?></label><input type="text" name="name_en" class="form-control form-control-sm rounded-3" value="<?= htmlspecialchars($company['name_en'] ?? '') ?>" required></div>
<div class="col-md-4 mb-3"><label class="form-label small fw-bold"><?= __('name_ar') ?></label><input type="text" name="name_ar" class="form-control form-control-sm rounded-3" value="<?= htmlspecialchars($company['name_ar'] ?? '') ?>" required></div>
<div class="col-md-4 mb-3"><label class="form-label small fw-bold"><?= __('email') ?></label><input type="email" name="email" class="form-control form-control-sm rounded-3" value="<?= htmlspecialchars($company['email'] ?? '') ?>"></div>
<div class="col-md-4 mb-3"><label class="form-label small fw-bold"><?= __('phone') ?></label><input type="text" name="phone" class="form-control form-control-sm rounded-3" value="<?= htmlspecialchars($company['phone'] ?? '') ?>"></div>
<div class="col-md-4 mb-3"><label class="form-label small fw-bold"><?= __('ctr_no') ?></label><input type="text" name="ctr_no" class="form-control form-control-sm rounded-3" value="<?= htmlspecialchars($company['ctr_no'] ?? '') ?>"></div>
<div class="col-md-4 mb-3"><label class="form-label small fw-bold"><?= __('vat_no') ?></label><input type="text" name="vat_no" class="form-control form-control-sm rounded-3" value="<?= htmlspecialchars($company['vat_no'] ?? '') ?>"></div>
<div class="col-md-8 mb-3">
<div class="row">
<div class="col-md-6"><label class="form-label small fw-bold"><?= __('logo') ?></label><input type="file" name="logo" class="form-control form-control-sm rounded-3" accept="image/*"><?php if ($company['logo']): ?><img src="<?= $company['logo'] ?>" alt="Logo" class="mt-2 rounded border" style="max-height: 50px;"><?php endif; ?></div>
<div class="col-md-6"><label class="form-label small fw-bold"><?= __('favicon') ?></label><input type="file" name="favicon" class="form-control form-control-sm rounded-3" accept="image/*"><?php if ($company['favicon']): ?><img src="<?= $company['favicon'] ?>" alt="Favicon" class="mt-2 rounded border" style="max-height: 32px;"><?php endif; ?></div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3"><label class="form-label small fw-bold"><?= __('address_en') ?></label><textarea name="address_en" class="form-control form-control-sm rounded-3" rows="3"><?= htmlspecialchars($company['address_en'] ?? '') ?></textarea></div>
<div class="col-md-6 mb-3"><label class="form-label small fw-bold"><?= __('address_ar') ?></label><textarea name="address_ar" class="form-control form-control-sm rounded-3" rows="3"><?= htmlspecialchars($company['address_ar'] ?? '') ?></textarea></div>
</div>
<div class="text-end mt-2"><button type="submit" name="save_company_profile" class="btn btn-primary px-5 rounded-4 shadow-sm"><i class="bi bi-save me-2"></i> <?= __('save_changes') ?></button></div>
</form>
</div>
</div>
<?php if ($is_super): ?>
<!-- WHATSAPP SETTINGS -->
<div class="tab-pane fade" id="whatsapp-settings" role="tabpanel">
<div class="card p-4 shadow-sm border-0 mb-4" style="border-radius: 20px;">
<h5 class="fw-bold mb-4"><i class="bi bi-whatsapp text-success me-2"></i> <?= __('whatsapp_settings') ?></h5>
<form method="POST">
<div class="form-check form-switch mb-4">
<input class="form-check-input" type="checkbox" id="whatsapp_enabled" name="whatsapp_enabled" value="1" <?= get_setting('whatsapp_enabled') == '1' ? 'checked' : '' ?>>
<label class="form-check-label fw-semibold" for="whatsapp_enabled"><?= __('whatsapp_enabled') ?></label>
</div>
<div class="row">
<div class="col-md-4 mb-3"><label class="form-label small fw-bold"><?= __('wablas_token') ?></label><input type="text" class="form-control form-control-sm rounded-3" name="wablas_token" value="<?= htmlspecialchars(get_setting('wablas_token', '')) ?>"></div>
<div class="col-md-4 mb-3"><label class="form-label small fw-bold"><?= __('wablas_server') ?></label><input type="text" class="form-control form-control-sm rounded-3" name="wablas_server" value="<?= htmlspecialchars(get_setting('wablas_server', 'https://console.wablas.com')) ?>"></div>
<div class="col-md-4 mb-4"><label class="form-label small fw-bold"><?= __('wablas_security_key') ?></label><input type="text" class="form-control form-control-sm rounded-3" name="wablas_security_key" value="<?= htmlspecialchars(get_setting('wablas_security_key', '')) ?>"></div>
</div>
<hr class="text-muted opacity-25 my-4">
<h5 class="fw-bold mb-3 mt-3"><i class="bi bi-chat-dots me-2"></i> <?= is_arabic() ? 'قوالب الرسائل' : 'Message Templates' ?></h5>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label small fw-bold"><?= __('msg_order_created_ar') ?></label>
<textarea class="form-control form-control-sm rounded-3 mb-1" name="msg_order_created_ar" rows="4"><?= htmlspecialchars(get_setting('msg_order_created_ar', '')) ?></textarea>
<small class="text-muted" style="font-size: 0.75rem;">Tags: {customer_name}, {order_number}, {order_details}, {total_price}</small>
</div>
<div class="col-md-4 mb-3">
<label class="form-label small fw-bold"><?= __('msg_order_ready_ar') ?></label>
<textarea class="form-control form-control-sm rounded-3 mb-1" name="msg_order_ready_ar" rows="4"><?= htmlspecialchars(get_setting('msg_order_ready_ar', '')) ?></textarea>
<small class="text-muted" style="font-size: 0.75rem;">Tags: {customer_name}, {order_number}</small>
</div>
<div class="col-md-4 mb-4">
<label class="form-label small fw-bold"><?= __('msg_payment_ar') ?></label>
<textarea class="form-control form-control-sm rounded-3 mb-1" name="msg_payment_ar" rows="4"><?= htmlspecialchars(get_setting('msg_payment_ar', '')) ?></textarea>
<small class="text-muted" style="font-size: 0.75rem;">Tags: {customer_name}, {order_number}, {amount}, {remaining_balance}</small>
</div>
</div>
<div class="text-end"><button type="submit" name="save_whatsapp_settings" class="btn btn-primary px-4 rounded-3 shadow-sm"><?= __('save') ?></button></div>
</form>
</div>
</div>
<!-- LOYALTY SETTINGS -->
<div class="tab-pane fade" id="loyalty-settings" role="tabpanel">
<div class="card p-4 shadow-sm border-0 mb-4" style="border-radius: 20px;">
<h5 class="fw-bold mb-4"><i class="bi bi-star-fill text-warning me-2"></i> <?= __('loyalty_settings') ?></h5>
<form method="POST">
<div class="form-check form-switch mb-4">
<input class="form-check-input" type="checkbox" id="loyalty_enabled" name="loyalty_enabled" value="1" <?= get_setting('loyalty_enabled') == '1' ? 'checked' : '' ?>>
<label class="form-check-label fw-semibold" for="loyalty_enabled"><?= __('loyalty_enabled') ?></label>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label small fw-bold"><?= __('loyalty_points_per_currency') ?></label>
<input type="number" step="0.01" class="form-control form-control-sm rounded-3" name="loyalty_points_per_currency" value="<?= htmlspecialchars(get_setting('loyalty_points_per_currency', '1')) ?>">
</div>
<div class="col-md-4 mb-4">
<label class="form-label small fw-bold"><?= __('loyalty_currency_per_point') ?></label>
<input type="number" step="0.001" class="form-control form-control-sm rounded-3" name="loyalty_currency_per_point" value="<?= htmlspecialchars(get_setting('loyalty_currency_per_point', '0.05')) ?>">
</div>
</div>
<div class="text-end"><button type="submit" name="save_loyalty_settings" class="btn btn-primary px-4 rounded-3 shadow-sm"><?= __('save') ?></button></div>
</form>
</div>
</div>
<!-- SMTP SETTINGS -->
<div class="tab-pane fade" id="smtp-settings" role="tabpanel">
<div class="card p-4 shadow-sm border-0 mb-4" style="border-radius: 20px;">
<h5 class="fw-bold mb-4"><i class="bi bi-envelope text-primary me-2"></i> <?= __('smtp_settings') ?></h5>
<form method="POST">
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label small fw-bold"><?= __('smtp_host') ?></label>
<input type="text" class="form-control form-control-sm rounded-3" name="smtp_host" value="<?= htmlspecialchars(get_setting('smtp_host', '')) ?>">
</div>
<div class="col-md-4 mb-3">
<label class="form-label small fw-bold"><?= __('smtp_port') ?></label>
<input type="number" class="form-control form-control-sm rounded-3" name="smtp_port" value="<?= htmlspecialchars(get_setting('smtp_port', '587')) ?>">
</div>
<div class="col-md-4 mb-3">
<label class="form-label small fw-bold"><?= __('smtp_secure') ?></label>
<select class="form-select form-select-sm rounded-3" name="smtp_secure">
<option value="tls" <?= get_setting('smtp_secure', 'tls') == 'tls' ? 'selected' : '' ?>>TLS</option>
<option value="ssl" <?= get_setting('smtp_secure') == 'ssl' ? 'selected' : '' ?>>SSL</option>
<option value="" <?= get_setting('smtp_secure') == '' ? 'selected' : '' ?>>None</option>
</select>
</div>
<div class="col-md-4 mb-4">
<label class="form-label small fw-bold"><?= __('smtp_user') ?></label>
<input type="text" class="form-control form-control-sm rounded-3" name="smtp_user" value="<?= htmlspecialchars(get_setting('smtp_user', '')) ?>">
</div>
<div class="col-md-4 mb-4">
<label class="form-label small fw-bold"><?= __('smtp_pass') ?></label>
<input type="password" class="form-control form-control-sm rounded-3" name="smtp_pass" value="<?= htmlspecialchars(get_setting('smtp_pass', '')) ?>">
</div>
</div>
<div class="text-end"><button type="submit" name="save_smtp_settings" class="btn btn-primary px-4 rounded-3 shadow-sm"><?= __('save') ?></button></div>
</form>
</div>
</div>
<?php endif; ?>
</div>
<script>
// Save active tab in localStorage
document.addEventListener("DOMContentLoaded", function() {
let activeTab = localStorage.getItem('activeSettingsTab');
if (activeTab) {
let tabEl = document.querySelector('button[data-bs-target="' + activeTab + '"]');
if (tabEl) {
let tab = new bootstrap.Tab(tabEl);
tab.show();
}
}
document.querySelectorAll('button[data-bs-toggle="pill"]').forEach(function(el) {
el.addEventListener('shown.bs.tab', function(e) {
localStorage.setItem('activeSettingsTab', e.target.getAttribute('data-bs-target'));
});
});
});
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

5
cookies.txt Normal file
View File

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

194
customer_statement.php Normal file
View File

@ -0,0 +1,194 @@
<?php
$title = 'customer_statement';
require_once __DIR__ . '/includes/header.php';
$customer_id = $_GET['id'] ?? null;
if (!$customer_id) { header('Location: customers.php'); exit; }
$stmt = db()->prepare("SELECT * FROM customers WHERE id = ?");
$stmt->execute([$customer_id]);
$customer = $stmt->fetch();
if (!$customer) { header('Location: customers.php'); exit; }
$from_date = $_GET['from_date'] ?? '';
$to_date = $_GET['to_date'] ?? '';
$active_tab = $_GET['tab'] ?? 'financial';
// Financial Logic
$orders_sql = "SELECT id, order_number, total_price, created_at FROM orders WHERE customer_id = ?";
$payments_sql = "SELECT p.id, p.amount, p.payment_method, p.created_at, o.order_number FROM payments p JOIN orders o ON p.order_id = o.id WHERE o.customer_id = ?";
if ($from_date) { $orders_sql .= " AND DATE(created_at) >= ?"; $payments_sql .= " AND DATE(p.created_at) >= ?"; }
if ($to_date) { $orders_sql .= " AND DATE(created_at) <= ?"; $payments_sql .= " AND DATE(p.created_at) <= ?"; }
$stmt_orders = db()->prepare($orders_sql);
$orders_params = [$customer_id];
if ($from_date) $orders_params[] = $from_date;
if ($to_date) $orders_params[] = $to_date;
$stmt_orders->execute($orders_params);
$orders = $stmt_orders->fetchAll();
$stmt_payments = db()->prepare($payments_sql);
$payments_params = [$customer_id];
if ($from_date) $payments_params[] = $from_date;
if ($to_date) $payments_params[] = $to_date;
$stmt_payments->execute($payments_params);
$payments = $stmt_payments->fetchAll();
$transactions = [];
foreach ($orders as $o) { $transactions[] = ['date' => $o['created_at'], 'type' => 'order', 'ref' => $o['order_number'], 'debit' => $o['total_price'], 'credit' => 0, 'description' => __('order') . ' #' . $o['order_number']]; }
foreach ($payments as $p) { $transactions[] = ['date' => $p['created_at'], 'type' => 'payment', 'ref' => $p['order_number'], 'debit' => 0, 'credit' => $p['amount'], 'description' => __('payment') . ' (' . __($p['payment_method']) . ') - ' . __('order') . ' #' . $p['order_number']]; }
usort($transactions, function($a, $b) { return strtotime($a['date']) - strtotime($b['date']); });
$total_debit = 0; $total_credit = 0; $balance = 0;
foreach ($transactions as &$t) { $total_debit += $t['debit']; $total_credit += $t['credit']; $balance += ($t['debit'] - $t['credit']); $t['running_balance'] = $balance; }
// Loyalty Logic
$loyalty_sql = "SELECT lt.*, o.order_number FROM loyalty_transactions lt LEFT JOIN orders o ON lt.order_id = o.id WHERE lt.customer_id = ?";
if ($from_date) $loyalty_sql .= " AND DATE(lt.created_at) >= ?";
if ($to_date) $loyalty_sql .= " AND DATE(lt.created_at) <= ?";
$loyalty_sql .= " ORDER BY lt.created_at ASC";
$stmt_loyalty = db()->prepare($loyalty_sql);
$loyalty_params = [$customer_id];
if ($from_date) $loyalty_params[] = $from_date;
if ($to_date) $loyalty_params[] = $to_date;
$stmt_loyalty->execute($loyalty_params);
$loyalty_transactions = $stmt_loyalty->fetchAll();
$loyalty_balance = 0;
foreach ($loyalty_transactions as &$lt) {
$loyalty_balance += $lt['points'];
$lt['running_balance'] = $loyalty_balance;
}
$display_company_name = ($lang === 'ar' ? ($company_info['name_ar'] ?: $company_info['name_en']) : $company_info['name_en']) ?: 'Laundry Brand';
$display_address = ($lang === 'ar' ? ($company_info['address_ar'] ?: $company_info['address_en']) : $company_info['address_en']);
?>
<div class="d-print-none mb-4">
<nav aria-label="breadcrumb">
<ol class="breadcrumb"><li class="breadcrumb-item"><a href="customers.php"><?= __('customers') ?></a></li><li class="breadcrumb-item active"><?= __('customer_statement') ?></li></ol>
</nav>
</div>
<div class="card p-4 border-0 shadow-sm mb-4 d-print-none rounded-4">
<form action="" method="GET" class="row g-3 align-items-end">
<input type="hidden" name="id" value="<?= $customer_id ?>">
<input type="hidden" name="tab" value="<?= $active_tab ?>">
<div class="col-md-4"><label class="form-label small fw-bold"><?= __('from_date') ?></label><input type="date" name="from_date" class="form-control rounded-3" value="<?= $from_date ?>"></div>
<div class="col-md-4"><label class="form-label small fw-bold"><?= __('to_date') ?></label><input type="date" name="to_date" class="form-control rounded-3" value="<?= $to_date ?>"></div>
<div class="col-md-4 d-flex gap-2">
<button type="submit" class="btn btn-primary px-4 w-100 rounded-3"><i class="bi bi-filter me-1"></i> <?= __('filter') ?></button>
<button type="button" class="btn btn-outline-dark px-4 w-100 rounded-3" onclick="window.print()"><i class="bi bi-printer me-1"></i> <?= __('print') ?></button>
</div>
</form>
</div>
<!-- Tabs -->
<ul class="nav nav-tabs d-print-none mb-4 border-0 gap-2">
<li class="nav-item">
<a class="nav-link rounded-3 fw-bold <?= $active_tab === 'financial' ? 'active bg-primary text-white border-primary' : 'bg-white text-muted border' ?>" href="?id=<?= $customer_id ?>&tab=financial&from_date=<?= $from_date ?>&to_date=<?= $to_date ?>">
<i class="bi bi-cash-stack me-2"></i><?= __('financial_statement') ?? 'Financial Statement' ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link rounded-3 fw-bold <?= $active_tab === 'loyalty' ? 'active bg-primary text-white border-primary' : 'bg-white text-muted border' ?>" href="?id=<?= $customer_id ?>&tab=loyalty&from_date=<?= $from_date ?>&to_date=<?= $to_date ?>">
<i class="bi bi-star-fill me-2"></i><?= __('loyalty_statement') ?? 'Loyalty Statement' ?>
</a>
</li>
</ul>
<div id="printableStatement" class="card p-4 p-md-5 border-0 shadow-sm rounded-4">
<!-- Formal Header -->
<div class="d-none d-print-block mb-5">
<div class="row align-items-center mb-4">
<div class="col-7">
<?php if (!empty($company_info['logo'])): ?><img src="<?= $company_info['logo'] ?>" alt="Logo" style="max-height: 100px;" class="mb-3"><?php endif; ?>
<h2 class="fw-bold mb-1"><?= htmlspecialchars($display_company_name) ?></h2>
<div class="text-muted small"><?= htmlspecialchars($display_address) ?><br><?= __('phone') ?>: <?= htmlspecialchars($company_info['phone']) ?></div>
</div>
<div class="col-5 text-end">
<h1 class="fw-bold text-uppercase mb-2 text-primary"><?= $active_tab === 'loyalty' ? (__('loyalty_statement') ?? 'Loyalty Statement') : __('statement') ?></h1>
<div class="mt-3 small">
<p class="mb-0 fw-bold"><?= __('date') ?>: <?= date('d/m/Y') ?></p>
<?php if ($from_date || $to_date): ?><p class="mb-0 text-muted mt-1"><?= $from_date ? __('from_date').': '.date('d/m/Y',strtotime($from_date)) : '' ?> <?= $to_date ? __('to_date').': '.date('d/m/Y',strtotime($to_date)) : '' ?></p><?php endif; ?>
</div>
</div>
</div>
<div class="row p-3 bg-light rounded-3 mb-4 mx-0 border">
<div class="col-12"><h6 class="fw-bold text-muted text-uppercase mb-2 small"><?= __('customer_details') ?></h6><h4 class="fw-bold mb-1"><?= $lang === 'ar' ? ($customer['name_ar'] ?: $customer['name_en']) : $customer['name_en'] ?></h4><p class="mb-0 text-muted small"><?= $customer['phone'] ?> <?= $customer['email'] ? ' | '.$customer['email'] : '' ?></p></div>
</div>
</div>
<!-- Screen Header -->
<div class="d-print-none mb-4">
<h4 class="fw-bold mb-1"><?= $lang === 'ar' ? ($customer['name_ar'] ?: $customer['name_en']) : $customer['name_en'] ?></h4>
<p class="text-muted mb-0"><?= $customer['phone'] ?> <?= $customer['email'] ? ' | '.$customer['email'] : '' ?></p>
</div>
<?php if ($active_tab === 'financial'): ?>
<div class="table-responsive">
<table class="table table-striped align-middle mt-2">
<thead class="table-dark">
<tr><th><?= __('date') ?></th><th><?= __('description') ?></th><th class="text-center"><?= __('ref') ?></th><th class="text-end"><?= __('debit') ?></th><th class="text-end"><?= __('credit') ?></th><th class="text-end"><?= __('balance') ?></th></tr>
</thead>
<tbody>
<?php if (empty($transactions)): ?><tr><td colspan="6" class="text-center py-5 text-muted"><?= __('no_transactions_found') ?></td></tr>
<?php else: foreach ($transactions as $t): ?>
<tr><td class="small"><?= date('d/m/Y H:i', strtotime($t['date'])) ?></td><td><?= $t['description'] ?></td><td class="text-center"><span class="badge bg-light text-dark border"><?= $t['ref'] ?></span></td><td class="text-end"><?= $t['debit'] > 0 ? format_amount($t['debit']) : '-' ?></td><td class="text-end"><?= $t['credit'] > 0 ? format_amount($t['credit']) : '-' ?></td><td class="text-end fw-bold"><?= format_amount($t['running_balance']) ?></td></tr>
<?php endforeach; endif; ?>
</tbody>
<tfoot class="table-light fw-bold"><tr><td colspan="3" class="text-end"><?= __('total') ?></td><td class="text-end"><?= format_amount($total_debit) ?></td><td class="text-end"><?= format_amount($total_credit) ?></td><td class="text-end text-primary"><?= format_amount($balance) ?></td></tr></tfoot>
</table>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table table-striped align-middle mt-2">
<thead class="table-dark">
<tr><th><?= __('date') ?></th><th><?= __('description') ?></th><th class="text-center"><?= __('ref') ?></th><th class="text-end"><?= __('earned') ?? 'Earned' ?></th><th class="text-end"><?= __('redeemed') ?? 'Redeemed' ?></th><th class="text-end"><?= __('balance') ?></th></tr>
</thead>
<tbody>
<?php if (empty($loyalty_transactions)): ?><tr><td colspan="6" class="text-center py-5 text-muted"><?= __('no_transactions_found') ?></td></tr>
<?php else: foreach ($loyalty_transactions as $lt): ?>
<tr>
<td class="small"><?= date('d/m/Y H:i', strtotime($lt['created_at'])) ?></td>
<td><?= $lt['description'] ?> <span class="badge bg-soft-primary text-primary small ms-2"><?= __($lt['type']) ?></span></td>
<td class="text-center"><?= $lt['order_number'] ? '<span class="badge bg-light text-dark border">'.$lt['order_number'].'</span>' : '-' ?></td>
<td class="text-end text-success"><?= $lt['points'] > 0 ? '+'.number_format($lt['points'], 2) : '-' ?></td>
<td class="text-end text-danger"><?= $lt['points'] < 0 ? number_format($lt['points'], 2) : '-' ?></td>
<td class="text-end fw-bold"><?= number_format($lt['running_balance'], 2) ?></td>
</tr>
<?php endforeach; endif; ?>
</tbody>
<tfoot class="table-light fw-bold"><tr><td colspan="5" class="text-end"><?= __('current_balance') ?? 'Current Balance' ?></td><td class="text-end text-primary"><?= number_format($customer['loyalty_points'], 2) ?></td></tr></tfoot>
</table>
</div>
<?php endif; ?>
<!-- Summary Box -->
<div class="row mt-5 d-print-none">
<div class="col-md-5 ms-auto">
<div class="card bg-light border-0 p-4 rounded-4">
<h6 class="fw-bold mb-3 text-muted text-uppercase small"><?= __('summary') ?></h6>
<?php if ($active_tab === 'financial'): ?>
<div class="d-flex justify-content-between mb-2"><span><?= __('total_debit') ?>:</span><span class="fw-bold"><?= format_amount($total_debit) ?></span></div>
<div class="d-flex justify-content-between mb-2"><span><?= __('total_credit') ?>:</span><span class="fw-bold"><?= format_amount($total_credit) ?></span></div>
<div class="d-flex justify-content-between pt-2 mt-2 border-top"><span class="fw-bold"><?= __('closing_balance') ?>:</span><span class="fw-bold text-primary"><?= format_amount($balance) ?></span></div>
<?php else: ?>
<div class="d-flex justify-content-between pt-2 mt-2 border-top"><span class="fw-bold"><?= __('loyalty_points') ?>:</span><span class="fw-bold text-primary fs-4"><?= number_format($customer['loyalty_points'], 2) ?></span></div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<style>
.bg-soft-primary { background-color: rgba(13, 110, 253, 0.1); }
@media print {
body { background-color: white !important; font-size: 10px; }
.main-content { padding: 0 !important; margin: 0 !important; }
header, .sidebar, .d-print-none, .breadcrumb, nav { display: none !important; }
.container-fluid, .row, .col-lg-10 { padding: 0 !important; margin: 0 !important; width: 100% !important; max-width: 100% !important; flex: 0 0 100% !important; }
.card { box-shadow: none !important; border: none !important; padding: 0 !important; }
}
</style>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

353
customers.php Normal file
View File

@ -0,0 +1,353 @@
<?php
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/lang.php';
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
if (!has_permission('view')) {
header('Location: admin.php');
exit;
}
$branch_id = $_SESSION['branch_id'];
$loyalty_enabled = get_setting('loyalty_enabled', '0');
// Handle Actions
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$required_permission = 'view';
if ($action === 'add_customer') $required_permission = 'add';
if ($action === 'edit_customer') $required_permission = 'edit';
if ($action === 'delete_customer') $required_permission = 'delete';
if ($action === 'adjust_points') $required_permission = 'edit';
if (!has_permission($required_permission)) {
header('Location: customers.php?error=no_permission');
exit;
}
if ($action === 'add_customer') {
$phone = $_POST['phone'] ?? '';
$name_en = $_POST['name_en'] ?? '';
$name_ar = $_POST['name_ar'] ?? '';
$email = $_POST['email'] ?? '';
$address_en = $_POST['address_en'] ?? '';
$address_ar = $_POST['address_ar'] ?? '';
if ($phone && $name_en) {
$stmt = db()->prepare("INSERT INTO customers (phone, name_en, name_ar, email, address_en, address_ar) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$phone, $name_en, $name_ar, $email, $address_en, $address_ar]);
header('Location: customers.php?success=customer_added');
exit;
}
} elseif ($action === 'edit_customer') {
$id = $_POST['id'];
$phone = $_POST['phone'] ?? '';
$name_en = $_POST['name_en'] ?? '';
$name_ar = $_POST['name_ar'] ?? '';
$email = $_POST['email'] ?? '';
$address_en = $_POST['address_en'] ?? '';
$address_ar = $_POST['address_ar'] ?? '';
$stmt = db()->prepare("UPDATE customers SET phone = ?, name_en = ?, name_ar = ?, email = ?, address_en = ?, address_ar = ? WHERE id = ?");
$stmt->execute([$phone, $name_en, $name_ar, $email, $address_en, $address_ar, $id]);
header('Location: customers.php?success=customer_updated');
exit;
} elseif ($action === 'delete_customer') {
$id = $_POST['id'];
$stmt = db()->prepare("SELECT COUNT(*) FROM orders WHERE customer_id = ?");
$stmt->execute([$id]);
if ($stmt->fetchColumn() > 0) {
header('Location: customers.php?error=customer_has_orders');
exit;
}
$stmt = db()->prepare("DELETE FROM customers WHERE id = ?");
$stmt->execute([$id]);
header('Location: customers.php?success=customer_deleted');
exit;
} elseif ($action === 'adjust_points') {
$id = $_POST['id'];
$points = (float)$_POST['points'];
$type = $_POST['type']; // 'add' or 'subtract'
$desc = $_POST['description'] ?? 'Manual adjustment';
$pdo = db();
$pdo->beginTransaction();
try {
$val = ($type === 'add') ? $points : -$points;
$pdo->prepare("UPDATE customers SET loyalty_points = loyalty_points + ? WHERE id = ?")->execute([$val, $id]);
$pdo->prepare("INSERT INTO loyalty_transactions (customer_id, points, type, description) VALUES (?, ?, 'adjusted', ?)")
->execute([$id, $val, $desc]);
$pdo->commit();
header('Location: customers.php?success=points_adjusted');
exit;
} catch (Exception $e) {
$pdo->rollBack();
header('Location: customers.php?error=' . urlencode($e->getMessage()));
exit;
}
}
}
$title = 'customers';
require_once __DIR__ . '/includes/header.php';
$search = $_GET['search'] ?? '';
$sql = "SELECT * FROM customers WHERE 1=1";
$params = [];
if ($search) {
$sql .= " AND (name_en LIKE ? OR name_ar LIKE ? OR phone LIKE ?)";
$params[] = "%$search%"; $params[] = "%$search%"; $params[] = "%$search%";
}
$sql .= " ORDER BY created_at DESC";
$stmt = db()->prepare($sql);
$stmt->execute($params);
$customers = $stmt->fetchAll();
?>
<div class="card p-4 border-0 shadow-sm" style="border-radius: 20px;">
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4">
<h5 class="fw-bold mb-3 mb-md-0"><?= __('customers_list') ?? 'Customers List' ?></h5>
<div class="d-flex gap-2">
<form action="" method="GET" class="d-flex gap-2">
<div class="input-group shadow-sm rounded-4 overflow-hidden border">
<input type="text" name="search" class="form-control border-0 py-2 shadow-none" placeholder="<?= __('search') ?>" value="<?= htmlspecialchars($search) ?>">
<button class="btn btn-white border-0" type="submit"><i class="bi bi-search"></i></button>
</div>
</form>
<?php if (has_permission('add')): ?>
<button class="btn btn-primary px-4 shadow-sm rounded-4" onclick="openCustomerModal()">
<i class="bi bi-person-plus-fill me-1"></i> <?= __('add_new') ?>
</button>
<?php endif; ?>
</div>
</div>
<?php if(isset($_GET['success'])): ?>
<div class="alert alert-success alert-dismissible fade show rounded-4 shadow-sm mb-4 border-0" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> <?= __($_GET['success']) ?? 'Action completed successfully' ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<?php if(isset($_GET['error'])): ?>
<div class="alert alert-danger alert-dismissible fade show rounded-4 shadow-sm mb-4 border-0" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> <?= __($_GET['error']) ?? 'An error occurred' ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>#</th>
<th><?= __('name') ?></th>
<th><?= __('phone') ?></th>
<?php if ($loyalty_enabled == '1'): ?>
<th><?= __('loyalty_points') ?? 'Loyalty Points' ?></th>
<?php endif; ?>
<th><?= __('email') ?></th>
<th><?= __('date') ?></th>
<th class="text-end"><?= __('actions') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($customers as $c): ?>
<tr>
<td><?= $c['id'] ?></td>
<td><div class="fw-bold"><?= $lang === 'ar' ? ($c['name_ar'] ?: $c['name_en']) : $c['name_en'] ?></div></td>
<td><?= $c['phone'] ?></td>
<?php if ($loyalty_enabled == '1'): ?>
<td>
<span class="badge bg-primary rounded-pill px-3 py-2 pointer" onclick="openLoyaltyModal(<?= $c['id'] ?>, '<?= addslashes($lang === 'ar' ? ($c['name_ar'] ?: $c['name_en']) : $c['name_en']) ?>', <?= $c['loyalty_points'] ?>)">
<?= number_format($c['loyalty_points'], 2) ?>
</span>
</td>
<?php endif; ?>
<td><?= $c['email'] ?: '-' ?></td>
<td class="small text-muted"><?= date('d/m/Y', strtotime($c['created_at'])) ?></td>
<td class="text-end">
<div class="d-flex gap-1 justify-content-end">
<a href="customer_statement.php?id=<?= $c['id'] ?>" class="btn btn-sm btn-light border-0 p-2 text-info rounded-3" title="<?= __('statement') ?>">
<i class="bi bi-file-earmark-text-fill"></i>
</a>
<?php if (has_permission('edit')): ?>
<button class="btn btn-sm btn-light border-0 p-2 text-primary rounded-3" onclick="openCustomerModal(<?= htmlspecialchars(json_encode($c)) ?>)">
<i class="bi bi-pencil-fill"></i>
</button>
<?php endif; ?>
<?php if (has_permission('delete')): ?>
<button class="btn btn-sm btn-light border-0 p-2 text-danger rounded-3" onclick="confirmDelete('customer', <?= $c['id'] ?>)">
<i class="bi bi-trash-fill"></i>
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($customers)): ?>
<tr><td colspan="7" class="text-center py-5 text-muted"><i class="bi bi-people fs-1 d-block mb-3 opacity-25"></i><?= __('no_customers_found') ?? 'No customers found' ?></td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- Customer Modal (Add/Edit) -->
<div class="modal fade" id="customerModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header border-0 pb-0 p-4">
<h5 class="modal-title fw-bold" id="customerModalLabel"><?= __('add_new_customer') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<form id="customerForm" method="POST">
<input type="hidden" name="action" id="customerAction" value="add_customer">
<input type="hidden" name="id" id="customerId">
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('phone') ?></label>
<input type="text" name="phone" id="customerPhone" class="form-control rounded-3" required>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold"><?= __('name_en') ?? 'Name (English)' ?></label>
<div class="input-group">
<input type="text" name="name_en" id="customerNameEn" class="form-control rounded-start-3" required>
<button type="button" class="btn btn-outline-secondary rounded-end-3" onclick="translateField('customerNameEn', 'customerNameAr', 'en-ar')"><i class="bi bi-translate"></i></button>
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold"><?= __('name_ar') ?? 'Name (Arabic)' ?></label>
<div class="input-group">
<input type="text" name="name_ar" id="customerNameAr" class="form-control rounded-start-3">
<button type="button" class="btn btn-outline-secondary rounded-end-3" onclick="translateField('customerNameAr', 'customerNameEn', 'ar-en')"><i class="bi bi-translate"></i></button>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('email') ?></label>
<input type="email" name="email" id="customerEmail" class="form-control rounded-3">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold"><?= __('address_en') ?? 'Address (English)' ?></label>
<textarea name="address_en" id="customerAddressEn" class="form-control rounded-3" rows="2"></textarea>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold"><?= __('address_ar') ?? 'Address (Arabic)' ?></label>
<textarea name="address_ar" id="customerAddressAr" class="form-control rounded-3" rows="2"></textarea>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 py-3 mt-2 fw-bold shadow-sm rounded-4"><?= __('save') ?></button>
</form>
</div>
</div>
</div>
</div>
<!-- Loyalty Adjustment Modal -->
<div class="modal fade" id="loyaltyModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header border-0 pb-0 p-4">
<h5 class="modal-title fw-bold"><?= __('loyalty_points') ?? 'Loyalty Points' ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4 text-center">
<h6 id="loyaltyCustomerName" class="fw-bold mb-1"></h6>
<div class="display-6 fw-bold text-primary mb-4" id="loyaltyCurrentPoints">0.00</div>
<form method="POST">
<input type="hidden" name="action" value="adjust_points">
<input type="hidden" name="id" id="loyaltyCustomerId">
<div class="row g-2 mb-3">
<div class="col-6">
<input type="radio" class="btn-check" name="type" id="typeAdd" value="add" checked>
<label class="btn btn-outline-success w-100 py-2 rounded-3" for="typeAdd"><?= __('add') ?></label>
</div>
<div class="col-6">
<input type="radio" class="btn-check" name="type" id="typeSubtract" value="subtract">
<label class="btn btn-outline-danger w-100 py-2 rounded-3" for="typeSubtract"><?= __('subtract') ?></label>
</div>
</div>
<div class="mb-3">
<input type="number" name="points" class="form-control form-control-lg text-center rounded-3" placeholder="0.00" step="0.01" required>
</div>
<div class="mb-4">
<input type="text" name="description" class="form-control rounded-3" placeholder="<?= __('description') ?>">
</div>
<button type="submit" class="btn btn-primary w-100 py-3 fw-bold rounded-4 shadow-sm"><?= __('confirm') ?></button>
</form>
</div>
</div>
</div>
</div>
<form id="deleteForm" method="POST" style="display: none;">
<input type="hidden" name="action" id="deleteAction">
<input type="hidden" name="id" id="deleteId">
</form>
<script>
async function translateField(sourceId, targetId, direction) {
const sourceEl = document.getElementById(sourceId);
const targetEl = document.getElementById(targetId);
const text = sourceEl.value.trim();
if (!text) return;
const btn = event.currentTarget;
const originalHtml = btn.innerHTML;
btn.disabled = true; btn.innerHTML = "<span class=\"spinner-border spinner-border-sm\"></span>";
try {
const formData = new FormData(); formData.append("text", text); formData.append("direction", direction);
const resp = await fetch("api/translate.php", { method: "POST", body: formData });
const data = await resp.json(); if (data.success) targetEl.value = data.translation;
} catch (e) { console.error(e); }
finally { btn.disabled = false; btn.innerHTML = originalHtml; }
}
function openCustomerModal(customer = null) {
const modal = new bootstrap.Modal(document.getElementById('customerModal'));
const form = document.getElementById('customerForm');
const label = document.getElementById('customerModalLabel');
const idInput = document.getElementById('customerId');
if (customer) {
label.innerText = "<?= __('edit') ?> <?= __('customer') ?>";
document.getElementById('customerAction').value = 'edit_customer';
idInput.value = customer.id;
document.getElementById('customerPhone').value = customer.phone;
document.getElementById('customerNameEn').value = customer.name_en;
document.getElementById('customerNameAr').value = customer.name_ar;
document.getElementById('customerEmail').value = customer.email;
document.getElementById('customerAddressEn').value = customer.address_en || '';
document.getElementById('customerAddressAr').value = customer.address_ar || '';
} else {
label.innerText = "<?= __('add_new_customer') ?>";
document.getElementById('customerAction').value = 'add_customer';
form.reset(); idInput.value = '';
}
modal.show();
}
function openLoyaltyModal(id, name, points) {
const modal = new bootstrap.Modal(document.getElementById('loyaltyModal'));
document.getElementById('loyaltyCustomerId').value = id;
document.getElementById('loyaltyCustomerName').innerText = name;
document.getElementById('loyaltyCurrentPoints').innerText = parseFloat(points).toFixed(2);
modal.show();
}
function confirmDelete(type, id) {
if (confirm("<?= __('are_you_sure') ?>")) {
document.getElementById('deleteAction').value = 'delete_' + type;
document.getElementById('deleteId').value = id;
document.getElementById('deleteForm').submit();
}
}
</script>
<style>.pointer { cursor: pointer; }</style>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

View File

@ -1,17 +1,121 @@
<?php <?php
// Generated by setup_mariadb_project.sh — edit as needed. // Generated by setup_mariadb_project.sh — edit as needed.
define('DB_HOST', '127.0.0.1'); require_once __DIR__ . '/../includes/dotenv.php';
define('DB_NAME', 'app_38384');
define('DB_USER', 'app_38384'); define('DB_HOST', getenv('DB_HOST') ?: '127.0.0.1');
define('DB_PASS', '5561099f-23a3-43d8-b1a2-54739c50721b'); define('DB_NAME', getenv('DB_NAME') ?: 'app_38384');
define('DB_USER', getenv('DB_USER') ?: 'app_38384');
define('DB_PASS', getenv('DB_PASS') ?: '5561099f-23a3-43d8-b1a2-54739c50721b');
function db() { function db() {
static $pdo; static $pdo;
if (!$pdo) { if (!$pdo) {
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [ try {
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, $pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]); PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (PDOException $e) {
die("Database Connection Error: " . $e->getMessage());
}
} }
return $pdo; return $pdo;
} }
function check_permission($page = null, $user_id = null) {
// Safely access session
if (!$user_id) {
if (isset($_SESSION) && isset($_SESSION['user_id'])) {
$user_id = $_SESSION['user_id'];
} else {
$user_id = null;
}
}
if (!$page) $page = basename($_SERVER['PHP_SELF']);
if (!$user_id) return ['view' => 0, 'add' => 0, 'edit' => 0, 'delete' => 0];
static $permissions_cache = [];
$cache_key = $user_id . '_' . $page;
if (isset($permissions_cache[$cache_key])) return $permissions_cache[$cache_key];
try {
// First check if user is super_admin
$role = null;
if (isset($_SESSION) && isset($_SESSION['user_id']) && $_SESSION['user_id'] == $user_id && isset($_SESSION['role'])) {
$role = $_SESSION['role'];
} else {
$stmt = db()->prepare("SELECT role FROM users WHERE id = ?");
$stmt->execute([$user_id]);
$role = $stmt->fetchColumn();
}
if ($role === 'super_admin') {
$result = ['view' => 1, 'add' => 1, 'edit' => 1, 'delete' => 1];
$permissions_cache[$cache_key] = $result;
return $result;
}
$stmt = db()->prepare("SELECT can_view as view, can_add as `add`, can_edit as edit, can_delete as `delete` FROM user_permissions WHERE user_id = ? AND page = ?");
$stmt->execute([$user_id, $page]);
$perms = $stmt->fetch();
if (!$perms) {
return ['view' => 0, 'add' => 0, 'edit' => 0, 'delete' => 0];
}
$result = [
'view' => (int)$perms['view'],
'add' => (int)$perms['add'],
'edit' => (int)$perms['edit'],
'delete' => (int)$perms['delete']
];
$permissions_cache[$cache_key] = $result;
return $result;
} catch (Exception $e) {
return ['view' => 0, 'add' => 0, 'edit' => 0, 'delete' => 0];
}
}
function get_setting($key, $default = null) {
try {
$stmt = db()->prepare('SELECT setting_value FROM settings WHERE setting_key = ?');
$stmt->execute([$key]);
$val = $stmt->fetchColumn();
return ($val !== false) ? $val : $default;
} catch (Exception $e) {
return $default;
}
}
function set_setting($key, $value) {
$stmt = db()->prepare('INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = ?, updated_at = CURRENT_TIMESTAMP');
$stmt->execute([$key, $value, $value]);
}
function has_permission($action, $page = null, $user_id = null) {
$perms = check_permission($page, $user_id);
return !empty($perms[$action]);
}
if (!function_exists('getStatusColor')) {
function getStatusColor($status) {
return [
'received' => 'secondary',
'processing' => 'primary',
'ready' => 'success',
'delivered' => 'dark',
'cancelled' => 'danger',
][$status] ?? 'info';
}
}
if (!function_exists('getPaymentStatusColor')) {
function getPaymentStatusColor($status) {
return [
'unpaid' => 'danger',
'partially_paid' => 'warning',
'paid' => 'success',
][$status] ?? 'info';
}
}

View File

@ -0,0 +1,129 @@
CREATE TABLE IF NOT EXISTS companies (
id INT AUTO_INCREMENT PRIMARY KEY,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS branches (
id INT AUTO_INCREMENT PRIMARY KEY,
company_id INT NOT NULL,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
address_en TEXT,
address_ar TEXT,
phone VARCHAR(20),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
branch_id INT,
company_id INT,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
full_name_en VARCHAR(255),
full_name_ar VARCHAR(255),
role ENUM('super_admin', 'branch_manager', 'cashier') DEFAULT 'cashier',
email VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE SET NULL,
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS customers (
id INT AUTO_INCREMENT PRIMARY KEY,
branch_id INT,
name_en VARCHAR(255),
name_ar VARCHAR(255),
phone VARCHAR(20) NOT NULL,
email VARCHAR(100),
address_en TEXT,
address_ar TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS items (
id INT AUTO_INCREMENT PRIMARY KEY,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS services (
id INT AUTO_INCREMENT PRIMARY KEY,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS prices (
id INT AUTO_INCREMENT PRIMARY KEY,
branch_id INT NOT NULL,
item_id INT NOT NULL,
service_id INT NOT NULL,
price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY (branch_id, item_id, service_id),
FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE,
FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE,
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS orders (
id INT AUTO_INCREMENT PRIMARY KEY,
branch_id INT NOT NULL,
customer_id INT,
user_id INT,
order_number VARCHAR(50) UNIQUE,
status ENUM('received', 'processing', 'ready', 'delivered', 'cancelled') DEFAULT 'received',
total_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
payment_status ENUM('unpaid', 'partially_paid', 'paid') DEFAULT 'unpaid',
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (branch_id) REFERENCES branches(id),
FOREIGN KEY (customer_id) REFERENCES customers(id),
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS order_items (
id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL,
item_id INT NOT NULL,
service_id INT NOT NULL,
quantity INT DEFAULT 1,
unit_price DECIMAL(10, 2) NOT NULL,
subtotal DECIMAL(10, 2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY (item_id) REFERENCES items(id),
FOREIGN KEY (service_id) REFERENCES services(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS payments (
id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
payment_method ENUM('cash', 'card', 'transfer') DEFAULT 'cash',
transaction_id VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Initial data seed
INSERT IGNORE INTO companies (name_en, name_ar) VALUES ('Laundry Brand', 'علامة غسيل');
INSERT IGNORE INTO branches (company_id, name_en, name_ar) VALUES (1, 'Main Branch', 'الفرع الرئيسي');
INSERT IGNORE INTO users (branch_id, company_id, username, password_hash, full_name_en, full_name_ar, role)
VALUES (1, 1, 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin', 'مدير', 'super_admin');
-- password is 'password'
INSERT IGNORE INTO items (name_en, name_ar) VALUES ('Shirt', 'قميص'), ('Suit', 'بدلة'), ('T-Shirt', 'تيشيرت'), ('Pants', 'بنطال'), ('Dress', 'فستان');
INSERT IGNORE INTO services (name_en, name_ar) VALUES ('Wash Only', 'غسيل فقط'), ('Iron Only', 'كوي فقط'), ('Wash & Iron', 'غسيل وكوي'), ('Dry Clean', 'تنظيف جاف');
-- Default prices for Main Branch
INSERT IGNORE INTO prices (branch_id, item_id, service_id, price) VALUES
(1, 1, 1, 5.00), (1, 1, 2, 3.00), (1, 1, 3, 7.00),
(1, 2, 4, 30.00),
(1, 3, 3, 5.00),
(1, 4, 3, 6.00);

View File

@ -0,0 +1,37 @@
-- Create categories table
CREATE TABLE IF NOT EXISTS categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
image_url VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Add new columns to items table
ALTER TABLE items ADD COLUMN category_id INT AFTER id;
ALTER TABLE items ADD COLUMN image_url VARCHAR(255) AFTER name_ar;
ALTER TABLE items ADD COLUMN vat_percent DECIMAL(5, 2) DEFAULT 0.00 AFTER image_url;
-- Add foreign key for category
ALTER TABLE items ADD CONSTRAINT fk_item_category FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL;
-- Create item_variants table (interpreting "aspire table" as "a separate table for variants")
CREATE TABLE IF NOT EXISTS item_variants (
id INT AUTO_INCREMENT PRIMARY KEY,
item_id INT NOT NULL,
name_en VARCHAR(255) NOT NULL,
name_ar VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Update prices table to include variant_id (optional, making it more flexible)
ALTER TABLE prices ADD COLUMN variant_id INT AFTER item_id;
ALTER TABLE prices ADD CONSTRAINT fk_price_variant FOREIGN KEY (variant_id) REFERENCES item_variants(id) ON DELETE CASCADE;
-- Insert default categories
INSERT IGNORE INTO categories (name_en, name_ar) VALUES
('Men', 'رجال'),
('Women', 'نساء'),
('Kids', 'أطفال'),
('Bed Sheets', 'ملاءات السرير');

View File

@ -0,0 +1,7 @@
-- Update orders table for VAT
ALTER TABLE orders ADD COLUMN vat_total DECIMAL(10, 2) DEFAULT 0.00 AFTER total_price;
-- Update order_items table for variants and VAT
ALTER TABLE order_items ADD COLUMN variant_id INT AFTER item_id;
ALTER TABLE order_items ADD COLUMN vat_amount DECIMAL(10, 2) DEFAULT 0.00 AFTER unit_price;
ALTER TABLE order_items ADD CONSTRAINT fk_order_item_variant FOREIGN KEY (variant_id) REFERENCES item_variants(id) ON DELETE SET NULL;

View File

@ -0,0 +1,3 @@
-- Fix prices table unique key to include variant_id
ALTER TABLE prices DROP INDEX branch_id;
ALTER TABLE prices ADD UNIQUE KEY unique_price (branch_id, item_id, variant_id, service_id);

View File

@ -0,0 +1,5 @@
-- Drop the old index with variant_id
DROP INDEX unique_price ON prices;
-- Add a new unique index without variant_id
CREATE UNIQUE INDEX unique_price ON prices (branch_id, item_id, service_id);

View File

@ -0,0 +1,13 @@
-- Add logo, favicon and other details to companies table
ALTER TABLE companies
ADD COLUMN logo VARCHAR(255) DEFAULT NULL AFTER name_ar,
ADD COLUMN favicon VARCHAR(255) DEFAULT NULL AFTER logo,
ADD COLUMN email VARCHAR(100) DEFAULT NULL AFTER favicon,
ADD COLUMN phone VARCHAR(20) DEFAULT NULL AFTER email,
ADD COLUMN address_en TEXT DEFAULT NULL AFTER phone,
ADD COLUMN address_ar TEXT DEFAULT NULL AFTER address_en,
ADD COLUMN vat_number VARCHAR(50) DEFAULT NULL AFTER address_ar;
-- Add profile_picture to users table
ALTER TABLE users
ADD COLUMN profile_picture VARCHAR(255) DEFAULT NULL AFTER email;

View File

@ -0,0 +1,8 @@
-- Update decimal precision for all monetary columns to 3 decimals
ALTER TABLE prices MODIFY COLUMN price DECIMAL(12, 3) NOT NULL DEFAULT 0.000;
ALTER TABLE orders MODIFY COLUMN total_price DECIMAL(12, 3) NOT NULL DEFAULT 0.000;
ALTER TABLE orders MODIFY COLUMN vat_total DECIMAL(12, 3) DEFAULT 0.000;
ALTER TABLE order_items MODIFY COLUMN unit_price DECIMAL(12, 3) NOT NULL;
ALTER TABLE order_items MODIFY COLUMN vat_amount DECIMAL(12, 3) DEFAULT 0.000;
ALTER TABLE order_items MODIFY COLUMN subtotal DECIMAL(12, 3) NOT NULL;
ALTER TABLE payments MODIFY COLUMN amount DECIMAL(12, 3) NOT NULL;

View File

@ -0,0 +1,6 @@
-- Add CTR No and VAT No to companies table
ALTER TABLE companies ADD COLUMN ctr_no VARCHAR(50) DEFAULT NULL AFTER vat_number;
ALTER TABLE companies ADD COLUMN vat_no VARCHAR(50) DEFAULT NULL AFTER ctr_no;
-- Migrate existing vat_number to vat_no if any
UPDATE companies SET vat_no = vat_number WHERE vat_no IS NULL AND vat_number IS NOT NULL;

View File

@ -0,0 +1,2 @@
-- Migration 09: Add prefix column to branches table
ALTER TABLE branches ADD COLUMN prefix VARCHAR(3) DEFAULT NULL;

View File

@ -0,0 +1,22 @@
-- Migration to make items, services, categories and customers shared between branches.
-- Only sales (orders) remain branch-specific.
-- Step 0: Ensure branch_id is indexed separately so we can drop the unique index later.
ALTER TABLE prices ADD INDEX idx_branch_id (branch_id);
-- Step 1: Cleanup duplicate prices across different branches for the same item/service.
-- We keep the oldest record (lowest ID).
DELETE p1 FROM prices p1
INNER JOIN prices p2
ON p1.item_id = p2.item_id
AND p1.service_id = p2.service_id
AND p1.id > p2.id;
-- Step 2: Update unique index for prices to be global (item_id + service_id only).
-- Note: Dropping the index might require idx_branch_id to exist for the foreign key.
DROP INDEX unique_price ON prices;
CREATE UNIQUE INDEX unique_price ON prices (item_id, service_id);
-- Step 3: Make branch_id optional in prices and customers tables.
ALTER TABLE prices MODIFY branch_id INT NULL;
ALTER TABLE customers MODIFY branch_id INT NULL;

View File

@ -0,0 +1 @@
ALTER TABLE users MODIFY COLUMN role ENUM('super_admin', 'branch_manager', 'cashier', 'limited_viewer') DEFAULT 'cashier';

View File

@ -0,0 +1,55 @@
CREATE TABLE IF NOT EXISTS user_permissions (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
page VARCHAR(50) NOT NULL,
can_view TINYINT(1) DEFAULT 0,
can_add TINYINT(1) DEFAULT 0,
can_edit TINYINT(1) DEFAULT 0,
can_delete TINYINT(1) DEFAULT 0,
UNIQUE KEY (user_id, page),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Seed existing users with default permissions based on their current roles
-- For Super Admins: everything
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
SELECT id, p.page, 1, 1, 1, 1
FROM users
CROSS JOIN (
SELECT 'admin.php' as page UNION SELECT 'pos.php' UNION SELECT 'orders.php' UNION SELECT 'lab.php' UNION
SELECT 'customers.php' UNION SELECT 'reports.php' UNION SELECT 'items.php' UNION SELECT 'branches.php' UNION
SELECT 'users.php' UNION SELECT 'profile.php' UNION SELECT 'company_profile.php' UNION
SELECT 'order_details.php' UNION SELECT 'receipt.php'
) p
WHERE role = 'super_admin';
-- For Branch Managers: most things, but not company-wide settings or user management (usually)
-- Following the existing role logic from header.php
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
SELECT id, p.page, 1, 1, 1, 1
FROM users
CROSS JOIN (
SELECT 'admin.php' as page UNION SELECT 'pos.php' UNION SELECT 'orders.php' UNION SELECT 'lab.php' UNION
SELECT 'customers.php' UNION SELECT 'reports.php' UNION SELECT 'items.php' UNION SELECT 'branches.php' UNION
SELECT 'profile.php' UNION SELECT 'order_details.php' UNION SELECT 'receipt.php'
) p
WHERE role = 'branch_manager';
-- For Cashiers:
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
SELECT id, p.page, 1, 1, 1, 1
FROM users
CROSS JOIN (
SELECT 'admin.php' as page UNION SELECT 'pos.php' UNION SELECT 'orders.php' UNION SELECT 'lab.php' UNION
SELECT 'customers.php' UNION SELECT 'profile.php' UNION SELECT 'order_details.php' UNION SELECT 'receipt.php'
) p
WHERE role = 'cashier';
-- For Limited Viewers:
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
SELECT id, p.page, 1, 0, 0, 0
FROM users
CROSS JOIN (
SELECT 'admin.php' as page UNION SELECT 'profile.php'
) p
WHERE role = 'limited_viewer';

View File

@ -0,0 +1,14 @@
-- Create user_branches table
CREATE TABLE IF NOT EXISTS user_branches (
user_id INT NOT NULL,
branch_id INT NOT NULL,
PRIMARY KEY (user_id, branch_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Migrate existing data from users table
INSERT IGNORE INTO user_branches (user_id, branch_id)
SELECT id, branch_id FROM users WHERE branch_id IS NOT NULL;
-- Keep branch_id in users for now as fallback or default, but we should eventually rely on user_branches

View File

@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS settings (
setting_key VARCHAR(100) PRIMARY KEY,
setting_value TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Initial WhatsApp settings
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES
('whatsapp_enabled', '0'),
('wablas_token', ''),
('wablas_server', 'https://console.wablas.com'),
('msg_order_created_ar', 'عزيزي {customer_name}، تم استلام طلبك رقم {order_number}. التفاصيل: {order_details}. الإجمالي: {total_price}. شكراً لتعاملك معنا.'),
('msg_order_ready_ar', 'عزيزي {customer_name}، طلبك رقم {order_number} جاهز للاستلام. شكراً لتعاملك معنا.'),
('msg_payment_ar', 'عزيزي {customer_name}، تم استلام دفعة بمبلغ {amount} لطلبك رقم {order_number}. الرصيد المتبقي: {remaining_balance}. شكراً لتعاملك معنا.');

View File

@ -0,0 +1,6 @@
-- Grant permission to customer_statement.php for all users who have access to customers.php
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
SELECT user_id, 'customer_statement.php', can_view, can_add, can_edit, can_delete
FROM user_permissions
WHERE page = 'customers.php'
ON DUPLICATE KEY UPDATE can_view = VALUES(can_view);

View File

@ -0,0 +1,22 @@
-- Add loyalty points to customers table
ALTER TABLE customers ADD COLUMN IF NOT EXISTS loyalty_points DECIMAL(10, 2) DEFAULT 0.00;
-- Create loyalty transactions table
CREATE TABLE IF NOT EXISTS loyalty_transactions (
id INT AUTO_INCREMENT PRIMARY KEY,
customer_id INT NOT NULL,
order_id INT NULL,
points DECIMAL(10, 2) NOT NULL,
type ENUM('earned', 'redeemed', 'adjusted') NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (customer_id),
FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Initial settings for loyalty system
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES
('loyalty_enabled', '1'),
('loyalty_points_per_currency', '1'), -- 1 point per 1 unit spent
('loyalty_currency_per_point', '0.05'); -- 1 point = 0.05 unit discount (20 points = 1 unit)

View File

@ -0,0 +1,2 @@
-- Add loyalty_discount column to orders table
ALTER TABLE orders ADD COLUMN IF NOT EXISTS loyalty_discount DECIMAL(12, 3) DEFAULT 0.000;

View File

@ -0,0 +1,3 @@
ALTER TABLE `services` ADD COLUMN `is_deleted` TINYINT(1) DEFAULT 0;
ALTER TABLE `items` ADD COLUMN `is_deleted` TINYINT(1) DEFAULT 0;
ALTER TABLE `categories` ADD COLUMN `is_deleted` TINYINT(1) DEFAULT 0;

View File

@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS ratings (
id INT AUTO_INCREMENT PRIMARY KEY,
branch_id INT NULL,
company_id INT NULL,
rating_type ENUM('staff', 'service') NOT NULL,
rating_value INT NOT NULL,
comment TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE SET NULL,
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete)
SELECT id, 'ratings.php', 1, 1, 1, 1
FROM users WHERE role = 'super_admin';

280
defined_keys.txt Normal file
View File

@ -0,0 +1,280 @@
en
dashboard
pos
orders
customers
items
services
branches
users
settings
logout
login
username
password
confirm_password
change_password
submit
save
save_changes
cancel
add_new
edit
delete
actions
search
total
status
date
phone
name
name_en
name_ar
price
quantity
subtotal
vat
vat_total
payment_status
received
processing
ready
delivered
cancelled
unpaid
partially_paid
paid
cash
card
transfer
new_order
order_details
customer_name
select_customer
add_item
edit_item
delete_item
select_item
select_service
checkout
back_to_pos
language
arabic
english
branch
all_branches
category
categories
add_category
edit_category
delete_category
categories_management
add_new_category
select_category
all
variant
variants
add_variant
select_variant
variant_name_en
variant_name_ar
default
vat_percent
image_url
items_management
variants_prices
no_items_found
pricing_management
are_you_sure
company_profile
user_profile
logo
favicon
email
address_en
address_ar
vat_number
profile_picture
full_name_en
full_name_ar
update_profile
success_update
error_update
print
print_invoice
thermal_receipt
order
customer
qty
close
payments
no_payments
update_status
update
remaining_amount
add_payment
payment_method
customer_details
order_date
method
amount
currency
ctr_no
vat_no
add_new_user
role
add_user
lab
outlet
view_items
initial_letters
from_date
to_date
order_no
all_status
all_payments
search_placeholder
previous
next
page
of
ar
dashboard
pos
orders
customers
items
services
branches
users
settings
logout
login
username
password
confirm_password
change_password
submit
save
save_changes
cancel
add_new
edit
delete
actions
search
total
status
date
phone
name
name_en
name_ar
price
quantity
subtotal
vat
vat_total
payment_status
received
processing
ready
delivered
cancelled
unpaid
partially_paid
paid
cash
card
transfer
new_order
order_details
customer_name
select_customer
add_item
edit_item
delete_item
select_item
select_service
checkout
back_to_pos
language
arabic
english
branch
all_branches
category
categories
add_category
edit_category
delete_category
categories_management
add_new_category
select_category
all
variant
variants
add_variant
select_variant
variant_name_en
variant_name_ar
default
vat_percent
image_url
items_management
variants_prices
no_items_found
pricing_management
are_you_sure
company_profile
user_profile
logo
favicon
email
address_en
address_ar
vat_number
profile_picture
full_name_en
full_name_ar
update_profile
success_update
error_update
print
print_invoice
thermal_receipt
order
customer
qty
close
payments
no_payments
update_status
update
remaining_amount
add_payment
payment_method
customer_details
order_date
method
amount
currency
ctr_no
vat_no
add_new_user
role
add_user
lab
outlet
view_items
initial_letters
from_date
to_date
order_no
all_status
all_payments
search_placeholder
previous
next
page
of

39
includes/dotenv.php Normal file
View File

@ -0,0 +1,39 @@
<?php
/**
* Global .env loader for the Flatlogic LAMP VM.
* Loads environment variables from the parent directory's .env file.
*/
function load_dotenv(): void {
static $loaded = false;
if ($loaded) return;
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
if ($envPath && is_readable($envPath)) {
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines !== false) {
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') continue;
if (!str_contains($line, '=')) continue;
[$k, $v] = array_map('trim', explode('=', $line, 2));
if ($k === '') continue;
// Strip potential surrounding quotes (single or double)
$v = trim($v, "' ");
// Set env if not already set or if empty
if (getenv($k) === false || getenv($k) === '') {
putenv("{$k}={$v}");
$_ENV[$k] = $v;
$_SERVER[$k] = $v;
}
}
}
$loaded = true;
}
}
// Auto-load on include
load_dotenv();

81
includes/footer.php Normal file
View File

@ -0,0 +1,81 @@
</main>
</div>
</div>
<!-- Separate Popper and Bootstrap for maximum compatibility and debugging -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.8/umd/popper.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.min.js"></script>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
<script>
// Robust dropdown initialization and diagnostic
(function() {
function checkDependencies() {
console.log('--- Dependency Check ---');
console.log('Popper.js:', typeof Popper !== 'undefined' ? 'Loaded' : 'MISSING');
console.log('Bootstrap:', typeof bootstrap !== 'undefined' ? 'Loaded' : 'MISSING');
if (typeof bootstrap !== 'undefined' && bootstrap.Dropdown) {
console.log('Bootstrap Dropdown:', 'Available');
} else {
console.warn('Bootstrap Dropdown: NOT Available');
}
}
function initDropdowns() {
checkDependencies();
if (typeof bootstrap !== 'undefined' && bootstrap.Dropdown) {
console.log('Initializing Bootstrap Dropdowns...');
var dropdownElementList = [].slice.call(document.querySelectorAll('.dropdown-toggle'));
dropdownElementList.forEach(function (dropdownToggleEl) {
try {
// Clean up any existing instances first
var existing = bootstrap.Dropdown.getInstance(dropdownToggleEl);
if (existing) existing.dispose();
// New instance
new bootstrap.Dropdown(dropdownToggleEl);
} catch (err) {
console.error('Error initializing dropdown for:', dropdownToggleEl, err);
}
});
} else {
console.error('Bootstrap JS or Dropdown module not loaded!');
// Emergency Fallback: simple manual toggle if Bootstrap fails
document.querySelectorAll('.dropdown-toggle').forEach(function(btn) {
btn.style.cursor = 'pointer';
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('Emergency fallback toggle triggered');
var menu = this.nextElementSibling;
if (menu && menu.classList.contains('dropdown-menu')) {
var isShown = menu.classList.contains('show');
// Close others
document.querySelectorAll('.dropdown-menu.show').forEach(function(m) {
m.classList.remove('show');
});
if (!isShown) menu.classList.add('show');
}
});
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDropdowns);
} else {
initDropdowns();
}
// Global click listener to close dropdowns
document.addEventListener('click', function(e) {
if (!e.target.closest('.dropdown')) {
document.querySelectorAll('.dropdown-menu.show').forEach(function(menu) {
menu.classList.remove('show');
});
}
});
})();
</script>
</body>
</html>

435
includes/header.php Normal file
View File

@ -0,0 +1,435 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/lang.php';
// Authentication check (simple for now)
if (!isset($_SESSION['user_id']) && basename($_SERVER['PHP_SELF']) !== 'login.php') {
header('Location: login.php');
exit;
}
$current_user_id = $_SESSION['user_id'] ?? null;
$current_branch = $_SESSION['branch_id'] ?? null;
$current_role = $_SESSION['role'] ?? 'cashier';
// Branch switching logic
if (isset($_GET['switch_branch']) && $current_user_id) {
$new_branch_id = $_GET['switch_branch'];
if ($new_branch_id === 'all' && $current_role === 'super_admin') {
$_SESSION['branch_id'] = 'all';
$_SESSION['branch_name'] = __('all_branches');
// Redirect back to same page without the switch_branch param but keeping others
$params = $_GET;
unset($params['switch_branch']);
$query = count($params) > 0 ? '?' . http_build_query($params) : '';
$url = strtok($_SERVER["REQUEST_URI"], '?') . $query;
header("Location: $url");
exit;
}
$new_branch_id = (int)$new_branch_id;
// Verify user has access to this branch (or is super_admin)
if ($current_role === 'super_admin') {
$stmt = db()->prepare("SELECT * FROM branches WHERE id = ?");
$stmt->execute([$new_branch_id]);
} else {
$stmt = db()->prepare("SELECT b.* FROM user_branches ub JOIN branches b ON ub.branch_id = b.id WHERE ub.user_id = ? AND ub.branch_id = ?");
$stmt->execute([$current_user_id, $new_branch_id]);
}
$branch = $stmt->fetch();
if ($branch) {
$_SESSION['branch_id'] = $branch['id'];
$_SESSION['branch_name'] = $lang === 'ar' ? ($branch['name_ar'] ?: $branch['name_en']) : $branch['name_en'];
// Redirect back to same page without the switch_branch param but keeping others
$params = $_GET;
unset($params['switch_branch']);
$query = count($params) > 0 ? '?' . http_build_query($params) : '';
$url = strtok($_SERVER["REQUEST_URI"], '?') . $query;
header("Location: $url");
exit;
}
}
// Fetch user branches
$user_branches = [];
if ($current_user_id) {
if ($current_role === 'super_admin') {
$user_branches = db()->query("SELECT * FROM branches")->fetchAll();
} else {
$stmt = db()->prepare("SELECT b.* FROM user_branches ub JOIN branches b ON ub.branch_id = b.id WHERE ub.user_id = ?");
$stmt->execute([$current_user_id]);
$user_branches = $stmt->fetchAll();
}
}
// Ensure branch_name is set if not already
if (!isset($_SESSION['branch_name']) || empty($_SESSION['branch_name'])) {
if (isset($_SESSION['branch_id'])) {
if ($_SESSION['branch_id'] === 'all') {
$_SESSION['branch_name'] = __('all_branches');
} else {
$stmt = db()->prepare("SELECT * FROM branches WHERE id = ?");
$stmt->execute([$_SESSION['branch_id']]);
$b = $stmt->fetch();
if ($b) {
$_SESSION['branch_name'] = $lang === 'ar' ? ($b['name_ar'] ?: $b['name_en']) : $b['name_en'];
}
}
}
}
// Fetch Global Company Info
$stmt = db()->query("SELECT * FROM companies LIMIT 1");
$company_info = $stmt->fetch();
// Fetch Current User Info (for profile picture)
$current_user_data = null;
if ($current_user_id) {
$stmt = db()->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$current_user_id]);
$current_user_data = $stmt->fetch();
}
// Global Permission Check for current page
$current_page = basename($_SERVER['PHP_SELF']);
if ($current_user_id && $current_page !== 'login.php' && $current_page !== 'logout.php') {
if (!has_permission('view', $current_page)) {
if ($current_page !== 'admin.php' && has_permission('view', 'admin.php')) {
header('Location: admin.php');
exit;
} elseif ($current_page !== 'profile.php' && has_permission('view', 'profile.php')) {
header('Location: profile.php');
exit;
} elseif ($current_page !== 'admin.php' && $current_page !== 'profile.php') {
die('Access Denied. You do not have permission to view this page.');
}
}
}
?>
<!doctype html>
<html lang="<?= $lang ?>" dir="<?= is_rtl() ? 'rtl' : 'ltr' ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars(is_arabic() ? $company_info['name_ar'] : $company_info['name_en']) ?> - <?= __($title ?? 'dashboard') ?></title>
<?php if ($company_info['favicon']): ?>
<link rel="icon" type="image/x-icon" href="<?= $company_info['favicon'] ?>">
<?php endif; ?>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.<?= is_rtl() ? 'rtl.' : '' ?>min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Cairo:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', <?= is_rtl() ? "'Cairo'," : '' ?> sans-serif;
background-color: #f8f9fa;
}
.sidebar {
min-height: 100vh;
background: #212529;
color: #fff;
padding-top: 1rem;
}
.sidebar .nav-link {
color: rgba(255,255,255,.75);
margin-bottom: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 12px;
display: flex;
align-items: center;
}
.sidebar .nav-link i {
margin-right: 0.75rem;
<?= is_rtl() ? 'margin-left: 0.75rem; margin-right: 0;' : '' ?>
font-size: 1.25rem;
}
.sidebar .nav-link:hover, .sidebar .nav-link.active {
color: #fff;
background: rgba(255,255,255,.1);
}
.sidebar .nav-link.active {
background: var(--bs-primary);
}
.main-content {
padding: 2rem;
}
.card {
border: none;
border-radius: 15px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
}
.lang-switch {
font-size: 0.9rem;
}
.user-avatar-sm {
width: 32px;
height: 32px;
object-fit: cover;
border-radius: 50%;
}
.dropdown-item i {
margin-right: 0.5rem;
<?= is_rtl() ? 'margin-left: 0.5rem; margin-right: 0;' : '' ?>
}
.user-dropdown-btn {
background-color: white;
border: 1px solid #dee2e6;
border-radius: 50rem;
padding: 0.25rem 1rem;
display: flex;
align-items: center;
cursor: pointer;
transition: all 0.2s;
}
.user-dropdown-btn:hover {
background-color: #f8f9fa;
border-color: #ced4da;
}
/* Ensure dropdowns are above everything */
.dropdown-menu {
z-index: 9999 !important;
}
.dropdown-toggle {
cursor: pointer;
}
.whatsapp-toggle-btn {
transition: all 0.3s;
border: 2px solid #e9ecef;
background: #fff;
}
.whatsapp-toggle-btn.active {
border-color: #25D366;
color: #25D366;
background: rgba(37, 211, 102, 0.1);
}
.whatsapp-toggle-btn:not(.active) {
color: #6c757d;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- Sidebar -->
<nav class="col-md-3 col-lg-2 d-md-block sidebar collapse no-print">
<div class="position-sticky">
<div class="px-4 mb-4 mt-2 text-center">
<?php if ($company_info['logo']): ?>
<img src="<?= $company_info['logo'] ?>" alt="Logo" class="img-fluid mb-2" style="max-height: 60px;">
<?php else: ?>
<h5 class="fw-bold"><?= htmlspecialchars(is_arabic() ? $company_info['name_ar'] : $company_info['name_en']) ?></h5>
<?php endif; ?>
</div>
<ul class="nav flex-column px-3">
<?php if (has_permission('view', 'admin.php')): ?>
<li class="nav-item">
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'admin.php' ? 'active' : '' ?>" href="admin.php">
<i class="bi bi-speedometer2"></i>
<?= __('dashboard') ?>
</a>
</li>
<?php endif; ?>
<?php if (has_permission('view', 'pos.php')): ?>
<li class="nav-item">
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'pos.php' ? 'active' : '' ?>" href="pos.php">
<i class="bi bi-pc-display-horizontal"></i>
<?= __('pos') ?>
</a>
</li>
<?php endif; ?>
<?php if (has_permission('view', 'orders.php')): ?>
<li class="nav-item">
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'orders.php' ? 'active' : '' ?>" href="orders.php">
<i class="bi bi-cart"></i>
<?= __('orders') ?>
</a>
</li>
<?php endif; ?>
<?php if (has_permission('view', 'lab.php')): ?>
<li class="nav-item">
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'lab.php' ? 'active' : '' ?>" href="lab.php">
<i class="bi bi-box-seam"></i>
<?= __('lab') ?>
</a>
</li>
<?php endif; ?>
<?php if (has_permission('view', 'customers.php')): ?>
<li class="nav-item">
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'customers.php' ? 'active' : '' ?>" href="customers.php">
<i class="bi bi-people"></i>
<?= __('customers') ?>
</a>
</li>
<?php endif; ?>
<?php if (has_permission('view', 'reports.php')): ?>
<li class="nav-item">
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'reports.php' ? 'active' : '' ?>" href="reports.php">
<i class="bi bi-file-earmark-bar-graph"></i>
<?= __('reports') ?>
</a>
</li>
<?php endif; ?>
<?php if (has_permission('view', 'ratings.php')): ?>
<li class="nav-item">
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'ratings.php' ? 'active' : '' ?>" href="ratings.php">
<i class="bi bi-star"></i>
<?= __('ratings') ?>
</a>
</li>
<?php endif; ?>
<?php if (has_permission('view', 'items.php')): ?>
<li class="nav-item">
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'items.php' ? 'active' : '' ?>" href="items.php">
<i class="bi bi-tags"></i>
<?= __('items') ?>
</a>
</li>
<?php endif; ?>
<?php if (has_permission('view', 'branches.php')): ?>
<li class="nav-item">
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'branches.php' ? 'active' : '' ?>" href="branches.php">
<i class="bi bi-shop"></i>
<?= __('branches') ?>
</a>
</li>
<?php endif; ?>
<?php if (has_permission('view', 'users.php')): ?>
<li class="nav-item">
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'users.php' ? 'active' : '' ?>" href="users.php">
<i class="bi bi-person-badge"></i>
<?= __('users') ?>
</a>
</li>
<?php endif; ?>
<hr class="mx-3 my-2 text-secondary">
<?php if (has_permission('view', 'company_profile.php')): ?>
<li class="nav-item">
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'company_profile.php' ? 'active' : '' ?>" href="company_profile.php">
<i class="bi bi-gear"></i>
<?= __('company_profile') ?>
</a>
</li>
<?php endif; ?>
</ul>
<hr class="mx-3 my-4">
<div class="px-3">
<a href="logout.php" class="nav-link text-danger">
<i class="bi bi-box-arrow-right"></i>
<?= __('logout') ?>
</a>
</div>
</div>
</nav>
<!-- Main Content -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 main-content">
<header class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom no-print">
<h1 class="h2"><?= __($title ?? 'dashboard') ?></h1>
<div class="mb-2 mb-md-0">
<div class="d-flex align-items-center">
<?php if (count($user_branches) > 1 || $current_role === 'super_admin'): ?>
<div class="dropdown me-2">
<button class="btn btn-sm btn-outline-primary dropdown-toggle rounded-pill px-3" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-shop me-1"></i> <?= $_SESSION['branch_name'] ?? '' ?>
</button>
<ul class="dropdown-menu shadow border-0 mt-2">
<?php if ($current_role === 'super_admin'): ?>
<li>
<a class="dropdown-item <?= ($_SESSION['branch_id'] ?? '') === 'all' ? 'active' : '' ?>" href="<?= branch_url('all') ?>">
<?= __('all_branches') ?>
</a>
</li>
<li><hr class="dropdown-divider"></li>
<?php endif; ?>
<?php foreach ($user_branches as $ub): ?>
<li>
<a class="dropdown-item <?= $ub['id'] == ($_SESSION['branch_id'] ?? '') ? 'active' : '' ?>" href="<?= branch_url($ub['id']) ?>">
<?= $lang === 'ar' ? ($ub['name_ar'] ?: $ub['name_en']) : $ub['name_en'] ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php else: ?>
<span class="badge bg-primary px-3 py-2 me-2"><?= $_SESSION['branch_name'] ?? '' ?></span>
<?php endif; ?>
<!-- Language Switch -->
<div class="dropdown me-2">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle rounded-pill px-3" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-translate me-1"></i> <?= $lang === 'ar' ? 'العربية' : 'English' ?>
</button>
<ul class="dropdown-menu shadow border-0 mt-2">
<li>
<a class="dropdown-item <?= $lang === 'en' ? 'active' : '' ?>" href="<?= lang_url('en') ?>">
English
</a>
</li>
<li>
<a class="dropdown-item <?= $lang === 'ar' ? 'active' : '' ?>" href="<?= lang_url('ar') ?>">
العربية
</a>
</li>
</ul>
</div>
<div class="dropdown">
<button class="user-dropdown-btn dropdown-toggle" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<?php if ($current_user_data['profile_picture'] ?? null): ?>
<img src="<?= $current_user_data['profile_picture'] ?>" alt="Profile" class="user-avatar-sm me-2">
<?php else: ?>
<i class="bi bi-person-circle fs-5 me-2 text-secondary"></i>
<?php endif; ?>
<span class="small fw-bold d-none d-sm-inline"><?= $_SESSION['full_name'] ?? '' ?></span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow border-0 mt-2" aria-labelledby="userDropdown" style="border-radius: 12px;">
<?php if (has_permission('view', 'profile.php')): ?>
<li>
<a class="dropdown-item py-2" href="profile.php">
<i class="bi bi-person-circle"></i> <?= __('user_profile') ?>
</a>
</li>
<?php endif; ?>
<?php if (has_permission('view', 'company_profile.php')): ?>
<li>
<a class="dropdown-item py-2" href="company_profile.php">
<i class="bi bi-gear"></i> <?= __('company_profile') ?>
</a>
</li>
<?php endif; ?>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item py-2 text-danger" href="logout.php">
<i class="bi bi-box-arrow-right"></i> <?= __('logout') ?>
</a>
</li>
</ul>
</div>
</div>
</div>
</header>

613
includes/lang.php Normal file
View File

@ -0,0 +1,613 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
/**
* Global .env loader
*/
require_once __DIR__ . '/dotenv.php';
$lang = $_SESSION['lang'] ?? 'en';
if (isset($_GET['lang'])) {
$lang = $_GET['lang'] === 'ar' ? 'ar' : 'en';
$_SESSION['lang'] = $lang;
}
$translations = [
'en' => [
'how_rate_staff' => 'How would you rate our staff?',
'how_rate_service' => 'How would you rate our services?',
'comment_placeholder' => 'Tell us about your experience...',
'submit_another' => 'Submit Another Rating',
'provide_valid_rating' => 'Please provide a valid rating (1-5).',
'error_saving_rating' => 'Error saving rating. Please try again.',
'optional' => 'Optional',
'ratings' => 'Ratings',
'staff_rating' => 'Staff Rating',
'service_rating' => 'Service Rating',
'branch_links' => 'Branch Links',
'copy_link' => 'Copy Link',
'print_qr' => 'Print QR',
'link_copied' => 'Copied!',
'rating' => 'Rating',
'comment' => 'Comment',
'rating_type' => 'Type',
'staff' => 'Staff',
'rate_us' => 'Rate Us',
'rate_staff' => 'Rate Staff',
'rate_services' => 'Rate Our Services',
'clear_ratings' => 'Clear Ratings',
'confirm_clear_ratings' => 'Are you sure you want to clear all ratings?',
'ratings_cleared' => 'All ratings have been cleared successfully.',
'error_clearing_ratings' => 'Error clearing ratings.',
'service' => 'Service',
'average_rating' => 'Average Rating',
'total_ratings' => 'Total Ratings',
'submit_rating' => 'Submit Rating',
'rating_submitted' => 'Thank you! Your rating has been submitted.',
'dashboard' => 'Dashboard',
'pos' => 'POS',
'orders' => 'Orders',
'customers' => 'Customers',
'items' => 'Items',
'services' => 'Services',
'branches' => 'Branches',
'users' => 'Users',
'settings' => 'Settings',
'smtp_settings' => 'SMTP Settings',
'smtp_host' => 'SMTP Host',
'smtp_port' => 'SMTP Port',
'smtp_user' => 'SMTP Username',
'smtp_pass' => 'SMTP Password',
'smtp_secure' => 'SMTP Encryption (tls/ssl)',
'whatsapp_settings' => 'WhatsApp Settings',
'whatsapp_enabled' => 'WhatsApp Enabled',
'wablas_token' => 'Wablas API Token',
'wablas_server' => 'Wablas Server URL',
'wablas_security_key' => 'Wablas Security Key',
'msg_order_created_ar' => 'Order Created Message (Arabic)',
'msg_order_ready_ar' => 'Order Ready Message (Arabic)',
'msg_payment_ar' => 'Payment Received Message (Arabic)',
'messages_enabled' => 'Messages Enabled',
'messages_disabled' => 'Messages Disabled',
'logout' => 'Logout',
'login' => 'Login',
'username' => 'Username',
'password' => 'Password',
'confirm_password' => 'Confirm Password',
'change_password' => 'Change Password',
'submit' => 'Submit',
'save' => 'Save',
'save_changes' => 'Save Changes',
'cancel' => 'Cancel',
'add_new' => 'Add New',
'edit' => 'Edit',
'delete' => 'Delete',
'actions' => 'Actions',
'search' => 'Search',
'total' => 'Total',
'status' => 'Status',
'date' => 'Date',
'phone' => 'Phone',
'name' => 'Name',
'name_en' => 'Name (EN)',
'name_ar' => 'Name (AR)',
'price' => 'Price',
'quantity' => 'Quantity',
'subtotal' => 'Subtotal',
'vat' => 'VAT',
'vat_total' => 'VAT Total',
'payment_status' => 'Payment Status',
'received' => 'Received',
'processing' => 'Processing',
'ready' => 'Ready',
'delivered' => 'تم التسليم',
'cancelled' => 'Cancelled',
'unpaid' => 'Unpaid',
'partially_paid' => 'Partially Paid',
'paid' => 'Paid',
'cash' => 'Cash',
'card' => 'Card',
'transfer' => 'Transfer',
'new_order' => 'New Order',
'order_details' => 'Order Details',
'customer_name' => 'Customer Name',
'select_customer' => 'Select Customer',
'add_item' => 'Add Item',
'edit_item' => 'Edit Item',
'delete_item' => 'Delete Item',
'select_item' => 'Select Item',
'select_service' => 'Select Service',
'checkout' => 'Checkout',
'back_to_pos' => 'Back to POS',
'language' => 'Language',
'arabic' => 'Arabic',
'english' => 'English',
'branch' => 'Branch',
'all_branches' => 'All Branches',
'category' => 'Category',
'categories' => 'Categories',
'add_category' => 'Add Category',
'edit_category' => 'Edit Category',
'delete_category' => 'Delete Category',
'categories_management' => 'Categories Management',
'add_new_category' => 'Add New Category',
'select_category' => 'Select Category',
'all' => 'All',
'variant' => 'Variant',
'variants' => 'Variants',
'add_variant' => 'Add Variant',
'select_variant' => 'Select Variant',
'variant_name_en' => 'Variant Name (EN)',
'variant_name_ar' => 'Variant Name (AR)',
'default' => 'Default',
'vat_percent' => 'VAT %',
'image_url' => 'Image URL',
'items_management' => 'Items Management',
'variants_prices' => 'Variants & Prices',
'no_items_found' => 'No items found',
'pricing_management' => 'Pricing Management',
'are_you_sure' => 'Are you sure?',
'company_profile' => 'Settings',
'user_profile' => 'User Profile',
'logo' => 'Logo',
'favicon' => 'أيقونة الموقع',
'email' => 'Email',
'address_en' => 'Address (EN)',
'address_ar' => 'Address (AR)',
'vat_number' => 'VAT Number',
'profile_picture' => 'Profile Picture',
'full_name_en' => 'Full Name (EN)',
'full_name_ar' => 'Full Name (AR)',
'update_profile' => 'Update Profile',
'success_update' => 'Updated successfully!',
'error_update' => 'Error updating!',
'print' => 'Print',
'print_invoice' => 'Print Invoice',
'print_receipt' => 'Print Receipt',
'thermal_receipt' => 'Thermal Receipt',
'order' => 'Order',
'customer' => 'Customer',
'qty' => 'Qty',
'close' => 'Close',
'payments' => 'Payments',
'no_payments' => 'No payments',
'update_status' => 'Update Status',
'update' => 'Update',
'remaining_amount' => 'Remaining Amount',
'add_payment' => 'Add Payment',
'payment_method' => 'Payment Method',
'customer_details' => 'Customer Details',
'order_date' => 'Order Date',
'method' => 'Method',
'amount' => 'Amount',
'currency' => 'OMR',
'ctr_no' => 'CR Number',
'vat_no' => 'VAT Number',
'add_new_user' => 'Add New User',
'role' => 'Role',
'add_user' => 'Add User',
'lab' => 'Lab Module',
'outlet' => 'Outlet',
'view_items' => 'View Items',
'initial_letters' => 'Initial letters (3 letters)',
'from_date' => 'From Date',
'to_date' => 'To Date',
'order_no' => 'Order No',
'all_status' => 'All Status',
'all_payments' => 'All Payments',
'search_placeholder' => 'Search by Phone, Order No, Name...',
'previous' => 'Previous',
'next' => 'Next',
'page' => 'Page',
'of' => 'of',
'Access Denied' => 'تم رفض الوصول',
'active_orders' => 'Active Orders',
'add_branch' => 'Add Branch',
'add_new_branch' => 'Add New Branch',
'add_new_customer' => 'Add New Customer',
'add_new_service' => 'Add New Service',
'company' => 'Company',
'customers_list' => 'Customers List',
'edit_service' => 'Edit Service',
'exactly_3_letters' => 'Exactly 3 letters',
'image' => 'Image',
'item' => 'Item',
'manage_orders_across_outlets' => 'Manage orders across outlets',
'new_customers' => 'New Customers',
'no_orders_found' => 'No orders found',
'order_number' => 'Order Number',
'orders_list' => 'Orders List',
'pricing_services' => 'Pricing & Services',
'quick_actions' => 'Quick Actions',
'ready_orders' => 'Ready Orders',
'recent_orders' => 'Recent Orders',
'reports' => 'Reports',
'rate_us' => 'Rate Us',
'rate_staff' => 'Rate Staff',
'rate_services' => 'Rate Our Services',
'clear_ratings' => 'Clear Ratings',
'confirm_clear_ratings' => 'Are you sure you want to clear all ratings?',
'ratings_cleared' => 'All ratings have been cleared successfully.',
'error_clearing_ratings' => 'Error clearing ratings.',
'service' => 'Service',
'service_pricing' => 'Service Pricing',
'services_management' => 'Services Management',
'today_revenue' => 'Today Revenue',
'view' => 'View',
'generate_report' => 'Generate Report',
'total_revenue' => 'Total Revenue',
'total_orders' => 'Total Orders',
'average_order_value' => 'Average Order Value',
'revenue_by_outlet' => 'Revenue by Outlet',
'revenue_by_cashier' => 'Revenue by Cashier',
'orders_by_status' => 'Orders by Status',
'top_items' => 'Top Items',
'top_services' => 'Top Services',
'cashier' => 'Cashier',
'revenue_by_payment_method' => 'Revenue by Payment Method',
'print_report' => 'Print Report',
'revenue_last_7_days' => 'Revenue (Last 7 Days)',
'date_range' => 'Date Range',
'edit_user' => 'Edit User',
'confirm_delete_user' => 'Are you sure you want to delete this user?',
'leave_blank_to_keep_current' => 'leave blank to keep current',
'permissions' => 'Permissions',
'user_permissions' => 'User Permissions',
'orders_per_month' => 'Orders per Month',
'customer_statement' => 'Customer Statement',
'statement' => 'Statement',
'debit' => 'Debit',
'credit' => 'Credit',
'balance' => 'Balance',
'total_debit' => 'Total Debit',
'total_credit' => 'Total Credit',
'opening_balance' => 'Opening Balance',
'closing_balance' => 'Closing Balance',
'transaction_type' => 'Transaction Type',
'description' => 'Description',
'customer_added' => 'Customer added successfully',
'customer_updated' => 'Customer updated successfully',
'customer_deleted' => 'Customer deleted successfully',
'customer_has_orders' => 'Cannot delete customer because they have existing orders',
'loyalty_settings' => 'Loyalty System Settings',
'loyalty_enabled' => 'Loyalty System Enabled',
'loyalty_points_per_currency' => 'Points earned per 1 Omani Rial spent',
'loyalty_currency_per_point' => 'Value of 1 point (discount amount)',
'loyalty_points' => 'Loyalty Points',
'points_adjusted' => 'Points adjusted successfully',
'loyalty' => 'Loyalty Points',
'loyalty_discount' => 'Loyalty Discount',
'final_total' => 'Final Total',
'set_prices_for' => 'Set Prices for',
'prices_saved_automatically' => 'Prices are saved automatically',
'done' => 'Done',
'uncategorized' => 'Uncategorized',
'manage_items_categories_services' => 'Manage Items, Categories & Services',
'manage_categories' => 'Manage Categories',
'search_items' => 'Search Items',
'filter' => 'Filter',
'all_categories' => 'All Categories',
'upload_image' => 'Upload Image',
'confirm_delete' => 'Are you sure you want to delete this?',
],
'ar' => [
'how_rate_staff' => 'كيف تقيم موظفينا؟',
'how_rate_service' => 'كيف تقيم خدماتنا؟',
'comment_placeholder' => 'أخبرنا عن تجربتك...',
'submit_another' => 'إرسال تقييم آخر',
'provide_valid_rating' => 'يرجى تقديم تقييم صحيح (1-5).',
'error_saving_rating' => 'حدث خطأ أثناء حفظ التقييم. يرجى المحاولة مرة أخرى.',
'optional' => 'اختياري',
'ratings' => 'التقييمات',
'staff_rating' => 'تقييم الموظفين',
'service_rating' => 'تقييم الخدمة',
'branch_links' => 'روابط الفروع',
'copy_link' => 'نسخ الرابط',
'print_qr' => 'طباعة QR',
'link_copied' => 'تم النسخ!',
'rating' => 'التقييم',
'comment' => 'تعليق',
'rating_type' => 'النوع',
'staff' => 'موظف',
'rate_us' => 'قيمنا',
'rate_staff' => 'تقييم الموظفين',
'rate_services' => 'تقييم خدماتنا',
'clear_ratings' => 'مسح التقييمات',
'confirm_clear_ratings' => 'هل أنت متأكد من مسح جميع التقييمات؟',
'ratings_cleared' => 'تم مسح جميع التقييمات بنجاح.',
'error_clearing_ratings' => 'حدث خطأ أثناء مسح التقييمات.',
'service' => 'خدمة',
'average_rating' => 'متوسط التقييم',
'total_ratings' => 'إجمالي التقييمات',
'submit_rating' => 'إرسال التقييم',
'rating_submitted' => 'شكرًا لك! تم إرسال تقييمك.',
'dashboard' => 'لوحة القيادة',
'pos' => 'نقطة البيع',
'orders' => 'الطلبات',
'customers' => 'العملاء',
'items' => 'الأصناف',
'services' => 'الخدمات',
'branches' => 'الفروع',
'users' => 'المستخدمين',
'settings' => 'الإعدادات',
'smtp_settings' => 'إعدادات SMTP',
'smtp_host' => 'خادم SMTP',
'smtp_port' => 'منفذ SMTP',
'smtp_user' => 'اسم مستخدم SMTP',
'smtp_pass' => 'كلمة مرور SMTP',
'smtp_secure' => 'تشفير SMTP (tls/ssl)',
'whatsapp_settings' => 'إعدادات الواتساب',
'whatsapp_enabled' => 'تفعيل الواتساب',
'wablas_token' => 'رمز API Wablas',
'wablas_server' => 'رابط خادم Wablas',
'wablas_security_key' => 'مفتاح الأمان Wablas',
'msg_order_created_ar' => 'رسالة إنشاء الطلب (بالعربية)',
'msg_order_ready_ar' => 'رسالة الطلب جاهز (بالعربية)',
'msg_payment_ar' => 'رسالة استلام الدفعة (بالعربية)',
'messages_enabled' => 'الرسائل مفعلة',
'messages_disabled' => 'الرسائل معطلة',
'logout' => 'تسجيل الخروج',
'login' => 'تسجيل الدخول',
'username' => 'اسم المستخدم',
'password' => 'كلمة المرور',
'confirm_password' => 'تأكيد كلمة المرور',
'change_password' => 'تغيير كلمة المرور',
'submit' => 'إرسال',
'save' => 'حفظ',
'save_changes' => 'حفظ التغييرات',
'cancel' => 'إلغاء',
'add_new' => 'إضافة جديد',
'edit' => 'تعديل',
'delete' => 'حذف',
'actions' => 'إجراءات',
'search' => 'بحث',
'total' => 'الإجمالي',
'status' => 'الحالة',
'date' => 'التاريخ',
'phone' => 'الهاتف',
'name' => 'الاسم',
'name_en' => 'الاسم (EN)',
'name_ar' => 'الاسم (AR)',
'price' => 'السعر',
'quantity' => 'الكمية',
'subtotal' => 'المجموع الفرعي',
'vat' => 'ضريبة القيمة المضافة',
'vat_total' => 'إجمالي الضريبة',
'payment_status' => 'حالة الدفع',
'received' => 'تم الاستلام',
'processing' => 'قيد المعالجة',
'ready' => 'جاهز',
'delivered' => 'تم التسليم',
'cancelled' => 'ملغي',
'unpaid' => 'غير مدفوع',
'partially_paid' => 'مدفوع جزئياً',
'paid' => 'مدفوع',
'cash' => 'نقد',
'card' => 'بطاقة',
'transfer' => 'تحويل',
'new_order' => 'طلب جديد',
'order_details' => 'تفاصيل الطلب',
'customer_name' => 'اسم العميل',
'select_customer' => 'اختر العميل',
'add_item' => 'إضافة صنف',
'edit_item' => 'تعديل صنف',
'delete_item' => 'حذف صنف',
'select_item' => 'اختر الصنف',
'select_service' => 'اختر الخدمة',
'checkout' => 'إتمام الطلب',
'back_to_pos' => 'العودة لنقطة البيع',
'language' => 'اللغة',
'arabic' => 'العربية',
'english' => 'الإنجليزية',
'branch' => 'الفرع',
'all_branches' => 'جميع الفروع',
'category' => 'الفئة',
'categories' => 'الفئات',
'add_category' => 'إضافة فئة',
'edit_category' => 'تعديل فئة',
'delete_category' => 'حذف فئة',
'categories_management' => 'إدارة الفئات',
'add_new_category' => 'إضافة فئة جديدة',
'select_category' => 'اختر الفئة',
'all' => 'الكل',
'variant' => 'النوع',
'variants' => 'الأنواع',
'add_variant' => 'إضافة نوع',
'select_variant' => 'اختر النوع',
'variant_name_en' => 'اسم النوع (EN)',
'variant_name_ar' => 'اسم النوع (AR)',
'default' => 'افتراضي',
'vat_percent' => 'نسبة الضريبة %',
'image_url' => 'رابط الصورة',
'items_management' => 'إدارة الأصناف',
'variants_prices' => 'الأنواع والأسعار',
'no_items_found' => 'لا يوجد أصناف',
'pricing_management' => 'إدارة الأسعار',
'are_you_sure' => 'هل أنت متأكد؟',
'company_profile' => 'الإعدادات',
'user_profile' => 'ملف المستخدم',
'logo' => 'الشعار',
'favicon' => 'أيقونة الموقع',
'email' => 'البريد الإلكتروني',
'address_en' => 'العنوان (EN)',
'address_ar' => 'العنوان (AR)',
'vat_number' => 'الرقم الضريبي',
'profile_picture' => 'الصورة الشخصية',
'full_name_en' => 'الاسم الكامل (EN)',
'full_name_ar' => 'الاسم الكامل (AR)',
'update_profile' => 'تحديث الملف الشخصي',
'success_update' => 'تم التحديث بنجاح!',
'error_update' => 'خطأ في التحديث!',
'print' => 'طباعة',
'print_invoice' => 'طباعة الفاتورة',
'print_receipt' => 'طباعة الإيصال',
'thermal_receipt' => 'إيصال حراري',
'order' => 'الطلب',
'customer' => 'العميل',
'qty' => 'الكمية',
'close' => 'إإغلاق',
'payments' => 'المدفوعات',
'no_payments' => 'لا يوجد مدفوعات',
'update_status' => 'تحديث الحالة',
'update' => 'تحديث',
'remaining_amount' => 'المبلغ المتبقي',
'add_payment' => 'إإضافة دفعة',
'payment_method' => 'طريقة الدفع',
'customer_details' => 'تفاصيل العميل',
'order_date' => 'تاريخ الطلب',
'method' => 'الطريقة',
'amount' => 'المبلغ',
'currency' => 'ر.ع.',
'ctr_no' => 'رقم السجل التجاري',
'vat_no' => 'الرقم الضريبي',
'add_new_user' => 'إضافة مستخدم جديد',
'role' => 'الدور',
'add_user' => 'إضافة مستخدم',
'lab' => 'وحدة المختبر',
'outlet' => 'المنفذ',
'view_items' => 'عرض الأصناف',
'initial_letters' => 'الحروف الأولى (3 حروف)',
'from_date' => 'من تاريخ',
'to_date' => 'إلى تاريخ',
'order_no' => 'رقم الطلب',
'all_status' => 'جميع الحالات',
'all_payments' => 'جميع المدفوعات',
'search_placeholder' => 'بحث بالهاتف، رقم الطلب، الاسم...',
'previous' => 'السابق',
'next' => 'التالي',
'page' => 'صفحة',
'of' => 'من',
'Access Denied' => 'تم رفض الوصول',
'active_orders' => 'الطلبات النشطة',
'add_branch' => 'إضافة فرع',
'add_new_branch' => 'إضافة فرع جديد',
'add_new_customer' => 'إضافة عميل جديد',
'add_new_service' => 'إضافة خدمة جديدة',
'company' => 'الشركة',
'customers_list' => 'قائمة العملاء',
'edit_service' => 'تعديل خدمة',
'exactly_3_letters' => '3 أحرف بالضبط',
'image' => 'صورة',
'item' => 'صنف',
'manage_orders_across_outlets' => 'إدارة الطلبات عبر المنافذ',
'new_customers' => 'عملاء جدد',
'no_orders_found' => 'لا يوجد طلبات',
'order_number' => 'رقم الطلب',
'orders_list' => 'قائمة الطلبات',
'pricing_services' => 'الأسعار والخدمات',
'quick_actions' => 'إجراءات سريعة',
'ready_orders' => 'طلبات جاهزة',
'recent_orders' => 'آخر الطلبات',
'reports' => 'التقارير',
'rate_us' => 'قيمنا',
'rate_staff' => 'تقييم الموظفين',
'rate_services' => 'تقييم خدماتنا',
'clear_ratings' => 'مسح التقييمات',
'confirm_clear_ratings' => 'هل أنت متأكد من مسح جميع التقييمات؟',
'ratings_cleared' => 'تم مسح جميع التقييمات بنجاح.',
'error_clearing_ratings' => 'حدث خطأ أثناء مسح التقييمات.',
'service' => 'خدمة',
'service_pricing' => 'أسعار الخدمات',
'services_management' => 'إدارة الخدمات',
'today_revenue' => 'إيرادات اليوم',
'view' => 'عرض',
'generate_report' => 'إنشاء تقرير',
'total_revenue' => 'إجمالي الإيرادات',
'total_orders' => 'إجمالي الطلبات',
'average_order_value' => 'متوسط قيمة الطلب',
'revenue_by_outlet' => 'الإيرادات حسب المنفذ',
'revenue_by_cashier' => 'الإيرادات حسب الكاشير',
'orders_by_status' => 'الطلبات حسب الحالة',
'top_items' => 'أفضل الأصناف',
'top_services' => 'أفضل الخدمات',
'cashier' => 'الكاشير',
'revenue_by_payment_method' => 'الإيرادات حسب طريقة الدفع',
'print_report' => 'طباعة التقرير',
'revenue_last_7_days' => 'الإيرادات (آخر 7 أيام)',
'date_range' => 'نطاق التاريخ',
'edit_user' => 'تعديل مستخدم',
'confirm_delete_user' => 'هل أنت متأكد من حذف هذا المستخدم؟',
'leave_blank_to_keep_current' => 'اتركه فارغاً للاحتفاظ بكلمة المرور الحالية',
'permissions' => 'الصلاحيات',
'user_permissions' => 'صلاحيات المستخدم',
'orders_per_month' => 'الطلبات شهرياً',
'customer_statement' => 'كشف حساب عميل',
'statement' => 'كشف حساب',
'debit' => 'مدين',
'credit' => 'دائن',
'balance' => 'الرصيد',
'total_debit' => 'إجمالي المدين',
'total_credit' => 'إجمالي الدائن',
'opening_balance' => 'الرصيد الافتتاحي',
'closing_balance' => 'الرصيد الختامي',
'transaction_type' => 'نوع المعاملة',
'description' => 'الوصف',
'customer_added' => 'تم إضافة العميل بنجاح',
'customer_updated' => 'تم تحديث العميل بنجاح',
'customer_deleted' => 'تم حذف العميل بنجاح',
'customer_has_orders' => 'لا يمكن حذف العميل لوجود طلبات مرتبطة به',
'loyalty_settings' => 'إعدادات نظام الولاء',
'loyalty_enabled' => 'تفعيل نظام الولاء',
'loyalty_points_per_currency' => 'النقاط المكتسبة لكل ريال عماني يتم إنفاقه',
'loyalty_currency_per_point' => 'قيمة النقطة الواحدة (مبلغ الخصم)',
'loyalty_points' => 'نقاط الولاء',
'points_adjusted' => 'تم تعديل النقاط بنجاح',
'loyalty' => 'نقاط الولاء',
'loyalty_discount' => 'خصم الولاء',
'final_total' => 'الإجمالي النهائي',
'set_prices_for' => 'تحديد الأسعار لـ',
'prices_saved_automatically' => 'يتم حفظ الأسعار تلقائياً',
'done' => 'تم',
'uncategorized' => 'غير مصنف',
'manage_items_categories_services' => 'إدارة الأصناف والفئات والخدمات',
'manage_categories' => 'إدارة الفئات',
'search_items' => 'بحث عن أصناف',
'filter' => 'تصفية',
'all_categories' => 'جميع الفئات',
'upload_image' => 'رفع صورة',
'confirm_delete' => 'هل أنت متأكد من الحذف؟',
]
];
function __($key) {
global $translations, $lang;
return $translations[$lang][$key] ?? $key;
}
function is_rtl() {
global $lang;
return $lang === 'ar';
}
function is_arabic() {
global $lang;
return $lang === 'ar';
}
function currency() {
return __('currency');
}
function decimals() {
return 3;
}
function format_amount($amount) {
return number_format((float)$amount, decimals()) . ' ' . currency();
}
function lang_url($new_lang) {
$params = $_GET;
$params['lang'] = $new_lang;
return '?' . http_build_query($params);
}
function branch_url($new_branch_id) {
$params = $_GET;
$params['switch_branch'] = $new_branch_id;
return '?' . http_build_query($params);
}

54
includes/whatsapp.php Normal file
View File

@ -0,0 +1,54 @@
<?php
require_once __DIR__ . '/../db/config.php';
function send_whatsapp_message($phone, $message) {
if (get_setting('whatsapp_enabled') !== '1') {
return ['success' => false, 'error' => 'WhatsApp is disabled'];
}
$token = get_setting('wablas_token');
$server = get_setting('wablas_server', 'https://console.wablas.com');
$security_key = get_setting('wablas_security_key');
if (empty($token) || empty($server)) {
return ['success' => false, 'error' => 'Wablas configuration is incomplete'];
}
// Clean phone number
$phone = preg_replace('/[^0-9]/', '', $phone);
$curl = curl_init();
$data = [
'phone' => $phone,
'message' => $message,
];
if (!empty($security_key)) {
$data['security_key'] = $security_key;
}
curl_setopt($curl, CURLOPT_HTTPHEADER, [
"Authorization: $token",
]);
curl_setopt($curl, CURLOPT_URL, rtrim($server, '/') . "/api/send-message");
curl_setopt($curl, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_TIMEOUT, 10);
$result = curl_exec($curl);
$error = curl_error($curl);
curl_close($curl);
if ($error) {
return ['success' => false, 'error' => $error];
}
$response = json_decode($result, true);
if (isset($response['status']) && $response['status'] == true) {
return ['success' => true, 'response' => $response];
}
return ['success' => false, 'error' => $response['message'] ?? 'Unknown error from Wablas'];
}

View File

@ -1,52 +1,9 @@
<?php <?php
require_once __DIR__ . '/db/config.php'; require_once __DIR__ . '/includes/lang.php';
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Modern AI-ready Chat Assistant';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chat Assistant</title>
<?php if ($projectDescription): ?>
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>">
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>">
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>">
<?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
</head>
<body>
<div class="bg-animations">
<div class="blob blob-1"></div>
<div class="blob blob-2"></div>
<div class="blob blob-3"></div>
</div>
<div class="main-wrapper">
<div class="chat-container">
<div class="chat-header">
<span>Chat Assistant</span>
<a href="admin.php" class="admin-link">Admin</a>
</div>
<div class="chat-messages" id="chat-messages">
<div class="message bot">
Hello! I'm your assistant. How can I help you today?
</div>
</div>
<div class="chat-input-area">
<form id="chat-form">
<input type="text" id="chat-input" placeholder="Type your message..." autocomplete="off">
<button type="submit">Send</button>
</form>
</div>
</div>
</div>
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script> if (isset($_SESSION['user_id'])) {
</body> header('Location: admin.php');
</html> } else {
header('Location: login.php');
}
exit;

306
install.php Normal file
View File

@ -0,0 +1,306 @@
<?php
session_start();
$lockFile = __DIR__ . '/install.lock';
if (file_exists($lockFile)) {
die("Installation is locked. Remove 'install.lock' to re-run the installer.");
}
$step = isset($_GET['step']) ? (int)$_GET['step'] : 1;
$error = '';
$success = '';
// Helper to run SQL file
function runSqlFile($pdo, $filePath) {
if (!file_exists($filePath)) return false;
$sql = file_get_contents($filePath);
$queries = preg_split("/;+(?=[^']*'([^']*'[^']*')*[^']*$)/", $sql);
foreach ($queries as $query) {
$query = trim($query);
if ($query) {
try {
$pdo->exec($query);
} catch (PDOException $e) {
$msg = $e->getMessage();
if (
strpos($msg, 'already exists') !== false ||
strpos($msg, 'Duplicate column name') !== false ||
strpos($msg, 'Duplicate key name') !== false ||
strpos($msg, 'Duplicate entry') !== false ||
strpos($msg, "Can't DROP") !== false ||
strpos($msg, "Cannot drop index") !== false ||
strpos($msg, "needed in a foreign key constraint") !== false ||
strpos($msg, 'check that column/key exists') !== false ||
strpos($msg, 'Unknown table') !== false
) {
continue;
}
throw $e;
}
}
}
return true;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($step === 2) {
$host = $_POST['db_host'] ?? '127.0.0.1';
$user = $_POST['db_user'] ?? '';
$pass = $_POST['db_pass'] ?? '';
$name = $_POST['db_name'] ?? '';
try {
$pdo = new PDO("mysql:host=$host;charset=utf8mb4", $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("CREATE DATABASE IF NOT EXISTS `$name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
$pdo->exec("USE `$name` ");
// Read existing helpers from current config if it exists
$helpers = '';
if (file_exists(__DIR__ . '/db/config.php')) {
$currentConfig = file_get_contents(__DIR__ . '/db/config.php');
// Extract functions after the db() function or constants
if (preg_match('/function check_permission.*$/s', $currentConfig, $matches)) {
$helpers = $matches[0];
}
}
// If helpers weren't found, use a set of default ones
if (empty($helpers)) {
$helpers = "
function check_permission(\$page = null, \$user_id = null) {
if (!\$user_id) \$user_id = eg_SESSION['user_id'] ?? null;
if (!\$page) \$page = basename( eg_SERVER['PHP_SELF']);
if (!\$user_id) return ['view' => 0, 'add' => 0, 'edit' => 0, 'delete' => 0];
static \$permissions_cache = [];
\$cache_key = \$user_id . '_' . \$page;
if (isset( eg_permissions_cache[\$cache_key])) return \$permissions_cache[\$cache_key];
\$stmt = db()->prepare(\"SELECT can_view as view, can_add as `add`, can_edit as edit, can_delete as `delete` FROM user_permissions WHERE user_id = ? AND page = ?\");
\$stmt->execute([\$user_id, \$page]);
\$perms = \$stmt->fetch();
if (!\$perms) return ['view' => 0, 'add' => 0, 'edit' => 0, 'delete' => 0];
\$result = ['view' => (int) eg_perms['view'], 'add' => (int) eg_perms['add'], 'edit' => (int) eg_perms['edit'], 'delete' => (int) eg_perms['delete']];
\$permissions_cache[\$cache_key] = \$result;
return \$result;
}
function has_permission(\$action, \$page = null, \$user_id = null) {
\$perms = check_permission( eg_page, \$user_id);
return !empty( eg_perms[\$action]);
}";
}
$configContent = "<?php\n"
. "define('DB_HOST', '$host');\n"
. "define('DB_NAME', '$name');\n"
. "define('DB_USER', '$user');\n"
. "define('DB_PASS', '$pass');\n\n"
. "function db() {\n"
. " static \$pdo;
"
. " if (!\$pdo) {\n"
. " \$pdo = new PDO(\"mysql:host=\" . DB_HOST . \";dbname=\" . DB_NAME . \";charset=utf8mb4\", DB_USER, DB_PASS, [\n"
. " PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,\n"
. " PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,\n"
. " PDO::ATTR_EMULATE_PREPARES => false,\n"
. " ]);\n"
. " }\n"
. " return \$pdo;\n"
. "}\n\n"
. trim($helpers) . "\n";
if (!is_dir(__DIR__ . '/db')) mkdir(__DIR__ . '/db', 0755, true);
file_put_contents(__DIR__ . '/db/config.php', $configContent);
// Run initial schema
$pdo->exec("SET FOREIGN_KEY_CHECKS = 0;");
$migrations = glob(__DIR__ . "/db/migrations/*.sql"); sort($migrations); foreach ($migrations as $migration) { runSqlFile($pdo, $migration); }
$pdo->exec("SET FOREIGN_KEY_CHECKS = 1;");
$_SESSION['db_configured'] = true;
header("Location: install.php?step=3");
exit;
} catch (PDOException $e) {
$error = "Connection failed: " . $e->getMessage();
}
} elseif ($step === 3) {
require_once __DIR__ . '/db/config.php';
$user = $_POST['admin_user'] ?? '';
$pass = $_POST['admin_pass'] ?? '';
$email = $_POST['admin_email'] ?? '';
if (empty($user) || empty($pass)) {
$error = "Username and password are required.";
} else {
try {
$hash = password_hash($pass, PASSWORD_DEFAULT);
$db = db();
// Clear existing users
$db->exec("SET FOREIGN_KEY_CHECKS = 0;");
$db->exec("TRUNCATE users;");
$db->exec("SET FOREIGN_KEY_CHECKS = 1;");
// Ensure company and branch exist (schema usually seeds them, but we ensure)
$db->exec("INSERT IGNORE INTO companies (id, name_en, name_ar) VALUES (1, 'Laundry Brand', 'علامة غسيل')");
$db->exec("INSERT IGNORE INTO branches (id, company_id, name_en, name_ar) VALUES (1, 1, 'Main Branch', 'الفرع الرئيسي')");
$stmt = $db->prepare("INSERT INTO users (branch_id, company_id, username, password_hash, full_name_en, role, email) VALUES (1, 1, ?, ?, 'System Administrator', 'super_admin', ?)");
$stmt->execute([$user, $hash, $email]);
file_put_contents($lockFile, date('Y-m-d H:i:s'));
header("Location: install.php?step=4");
exit;
} catch (Exception $e) {
$error = "Admin setup failed: " . $e->getMessage();
}
}
}
}
// Environment Checks
$checks = [
'PHP Version >= 8.0' => version_compare(PHP_VERSION, '8.0.0', '>='),
'PDO Extension' => extension_loaded('pdo_mysql'),
'Config Writable' => is_writable(__DIR__ . '/db/config.php') || is_writable(__DIR__),
'Assets Writable' => (is_writable(__DIR__ . '/assets/images') || (is_dir(__DIR__ . '/assets/images') && is_writable(__DIR__ . '/assets/images')))
];
$envReady = !in_array(false, $checks, true);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fresh Installation</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background: #f0f2f5; font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; }
.install-card { width: 100%; max-width: 550px; padding: 2.5rem; border-radius: 20px; box-shadow: 0 15px 35px rgba(0,0,0,0.05); background: #fff; }
.step-indicator { display: flex; justify-content: space-between; margin-bottom: 2.5rem; position: relative; }
.step-indicator::before { content: ''; position: absolute; top: 15px; left: 0; right: 0; height: 2px; background: #e9ecef; z-index: 1; }
.step-dot { width: 32px; height: 32px; border-radius: 50%; background: #e9ecef; display: flex; align-items: center; justify-content: center; font-weight: 600; color: #6c757d; z-index: 2; position: relative; transition: all 0.3s ease; }
.step-dot.active { background: #0d6efd; color: #fff; transform: scale(1.1); box-shadow: 0 0 15px rgba(13, 110, 253, 0.3); }
.step-dot.completed { background: #198754; color: #fff; }
h3 { font-weight: 700; color: #1a1d20; letter-spacing: -0.5px; }
h5 { font-weight: 600; color: #495057; margin-bottom: 1.5rem; }
.form-label { font-weight: 500; color: #495057; }
.btn-primary { padding: 0.75rem; font-weight: 600; border-radius: 10px; transition: all 0.2s; }
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(13, 110, 253, 0.2); }
</style>
</head>
<body>
<div class="install-card">
<div class="text-center mb-4">
<h3 class="mb-1">Setup Wizard</h3>
<p class="text-muted small">Step-by-step application installation</p>
</div>
<div class="step-indicator">
<div class="step-dot <?php echo $step >= 1 ? ($step > 1 ? 'completed' : 'active') : ''; ?>">1</div>
<div class="step-dot <?php echo $step >= 2 ? ($step > 2 ? 'completed' : 'active') : ''; ?>">2</div>
<div class="step-dot <?php echo $step >= 3 ? ($step > 3 ? 'completed' : 'active') : ''; ?>">3</div>
<div class="step-dot <?php echo $step >= 4 ? ($step > 4 ? 'completed' : 'active') : ''; ?>">4</div>
</div>
<?php if ($error): ?>
<div class="alert alert-danger border-0 shadow-sm mb-4"><?php echo $error; ?></div>
<?php endif; ?>
<?php if ($step === 1): ?>
<h5>Checking Environment</h5>
<div class="list-group list-group-flush mb-4 border rounded-3 overflow-hidden">
<?php foreach ($checks as $label => $pass):
?>
<div class="list-group-item d-flex justify-content-between align-items-center py-3">
<span class="text-secondary"><?php echo $label; ?></span>
<?php if ($pass):
?>
<span class="badge bg-success-subtle text-success px-3 py-2 rounded-pill">Passed</span>
<?php else:
?>
<span class="badge bg-danger-subtle text-danger px-3 py-2 rounded-pill">Failed</span>
<?php endif;
?>
</div>
<?php endforeach;
?>
</div>
<div class="d-grid">
<a href="?step=2" class="btn btn-primary <?php echo !$envReady ? 'disabled' : ''; ?>">Continue to Database</a>
</div>
<?php elseif ($step === 2):
?>
<h5>Database Configuration</h5>
<form method="POST">
<div class="mb-3">
<label class="form-label">Database Host</label>
<input type="text" name="db_host" class="form-control form-control-lg bg-light border-0" value="127.0.0.1" required>
</div>
<div class="mb-3">
<label class="form-label">Database Name</label>
<input type="text" name="db_name" class="form-control form-control-lg bg-light border-0" placeholder="e.g. laundry_app" required>
</div>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label">Username</label>
<input type="text" name="db_user" class="form-control form-control-lg bg-light border-0" required>
</div>
<div class="col-md-6">
<label class="form-label">Password</label>
<input type="password" name="db_pass" class="form-control form-control-lg bg-light border-0">
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">Initialize Database</button>
</div>
</form>
<?php elseif ($step === 3):
?>
<h5>Administrator Account</h5>
<form method="POST">
<div class="mb-3">
<label class="form-label">Admin Username</label>
<input type="text" name="admin_user" class="form-control form-control-lg bg-light border-0" value="admin" required>
</div>
<div class="mb-3">
<label class="form-label">Email Address</label>
<input type="email" name="admin_email" class="form-control form-control-lg bg-light border-0" placeholder="admin@domain.com">
</div>
<div class="mb-4">
<label class="form-label">Password</label>
<input type="password" name="admin_pass" class="form-control form-control-lg bg-light border-0" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">Complete Setup</button>
</div>
</form>
<?php elseif ($step === 4):
?>
<div class="text-center">
<div class="mb-4">
<div class="bg-success-subtle text-success d-inline-flex align-items-center justify-content-center rounded-circle" style="width: 80px; height: 80px;">
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="currentColor" class="bi bi-check2-all" viewBox="0 0 16 16">
<path d="M12.354 4.354a.5.5 0 0 0-.708-.708L5 10.293 1.854 7.146a.5.5 0 1 0-.708.708l3.5 3.5a.5.5 0 0 0 .708 0l7-7zm-4.208 7-.896-.897.707-.707.543.543 6.646-6.647a.5.5 0 0 1 .708.708l-7 7a.5.5 0 0 1-.708 0z"/>
<path d="m5.354 7.146.896.897-.707.707-.897-.896a.5.5 0 1 1 .708-.708z"/>
</svg>
</div>
</div>
<h5>Installation Complete!</h5>
<p class="text-muted mb-4">The application has been configured and the administrator account is ready.</p>
<div class="d-grid gap-2">
<a href="login.php" class="btn btn-primary btn-lg">Login to System</a>
<p class="small text-danger mt-3">Warning: Delete install.php for production security.</p>
</div>
</div>
<?php endif;
?>
</div>
</body>
</html>

984
items.php Normal file
View File

@ -0,0 +1,984 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/lang.php';
$isAjax = isset($_POST['ajax']) || isset($_GET['ajax']) || (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest');
if (!has_permission('view')) {
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Access Denied']);
exit;
}
die('Access Denied');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
if ($action === 'add_item') {
$name_en = $_POST['name_en'];
$name_ar = $_POST['name_ar'];
$category_id = $_POST['category_id'] ?: null;
$vat_percent = (float)($_POST['vat_percent'] ?? 15.00);
$image_url = null;
// Handle Image Upload
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
$filename = 'item_' . uniqid('', true) . '.' . $ext;
$upload_dir = __DIR__ . '/assets/images/items/';
if (!is_dir($upload_dir)) mkdir($upload_dir, 0775, true);
if (move_uploaded_file($_FILES['image']['tmp_name'], $upload_dir . $filename)) {
$image_url = 'assets/images/items/' . $filename;
}
}
$stmt = db()->prepare("INSERT INTO items (name_en, name_ar, category_id, vat_percent, image_url) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$name_en, $name_ar, $category_id, $vat_percent, $image_url]);
header('Location: items.php?success=item_added');
exit;
} elseif ($action === 'edit_item') {
$id = $_POST['id'];
$name_en = $_POST['name_en'];
$name_ar = $_POST['name_ar'];
$category_id = $_POST['category_id'] ?: null;
$vat_percent = (float)($_POST['vat_percent'] ?? 15.00);
$image_url = $_POST['current_image_url'] ?? null;
// Handle Image Upload
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
$filename = 'item_' . uniqid('', true) . '.' . $ext;
$upload_dir = __DIR__ . '/assets/images/items/';
if (!is_dir($upload_dir)) mkdir($upload_dir, 0775, true);
if (move_uploaded_file($_FILES['image']['tmp_name'], $upload_dir . $filename)) {
$image_url = 'assets/images/items/' . $filename;
}
}
$stmt = db()->prepare("UPDATE items SET name_en = ?, name_ar = ?, category_id = ?, vat_percent = ?, image_url = ? WHERE id = ?");
$stmt->execute([$name_en, $name_ar, $category_id, $vat_percent, $image_url, $id]);
header('Location: items.php?success=item_updated');
exit;
} elseif ($action === 'delete_item') {
$id = $_POST['id'];
try {
$stmt = db()->prepare("DELETE FROM items WHERE id = ?");
$stmt->execute([$id]);
header('Location: items.php?success=item_deleted');
exit;
} catch (PDOException $e) {
header('Location: items.php?error=cannot_delete_item');
exit;
}
} elseif ($action === 'add_category') {
$name_en = $_POST['cat_name_en'];
$name_ar = $_POST['cat_name_ar'];
$stmt = db()->prepare("INSERT INTO categories (name_en, name_ar) VALUES (?, ?)");
$stmt->execute([$name_en, $name_ar]);
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => true, 'html' => renderCategoryList($lang)]);
exit;
}
header('Location: items.php?success=category_added');
exit;
} elseif ($action === 'edit_category') {
$id = $_POST['id'];
$name_en = $_POST['cat_name_en'];
$name_ar = $_POST['cat_name_ar'];
$stmt = db()->prepare("UPDATE categories SET name_en = ?, name_ar = ? WHERE id = ?");
$stmt->execute([$name_en, $name_ar, $id]);
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => true, 'html' => renderCategoryList($lang)]);
exit;
}
header('Location: items.php?success=category_updated');
exit;
} elseif ($action === 'delete_category') {
$id = $_POST['id'];
try {
// Before deleting category, set items to null category
$stmt = db()->prepare("UPDATE items SET category_id = NULL WHERE category_id = ?");
$stmt->execute([$id]);
$stmt = db()->prepare("DELETE FROM categories WHERE id = ?");
$stmt->execute([$id]);
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => true, 'html' => renderCategoryList($lang)]);
exit;
}
header('Location: items.php?success=category_deleted');
exit;
} catch (PDOException $e) {
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => 'Cannot delete category.']);
exit;
}
header('Location: items.php?error=cannot_delete_category');
exit;
}
} elseif ($action === 'add_service') {
$name_en = $_POST['svc_name_en'];
$name_ar = $_POST['svc_name_ar'];
$stmt = db()->prepare("INSERT INTO services (name_en, name_ar) VALUES (?, ?)");
$stmt->execute([$name_en, $name_ar]);
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => true, 'html' => renderServiceList($lang)]);
exit;
}
header('Location: items.php?success=service_added');
exit;
} elseif ($action === 'edit_service') {
$id = $_POST['id'];
$name_en = $_POST['svc_name_en'];
$name_ar = $_POST['svc_name_ar'];
$stmt = db()->prepare("UPDATE services SET name_en = ?, name_ar = ? WHERE id = ?");
$stmt->execute([$name_en, $name_ar, $id]);
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => true, 'html' => renderServiceList($lang)]);
exit;
}
header('Location: items.php?success=service_updated');
exit;
} elseif ($action === 'delete_service') {
$id = $_POST['id'];
try {
$stmt = db()->prepare("DELETE FROM services WHERE id = ?");
$stmt->execute([$id]);
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => true, 'html' => renderServiceList($lang)]);
exit;
}
header('Location: items.php?success=service_deleted');
exit;
} catch (PDOException $e) {
$stmt = db()->prepare("UPDATE services SET is_deleted = 1 WHERE id = ?");
$stmt->execute([$id]);
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => true, 'html' => renderServiceList($lang)]);
exit;
}
header('Location: items.php?success=service_deleted');
exit;
}
} elseif ($action === 'update_price') {
$item_id = $_POST['item_id'];
$service_id = $_POST['service_id'];
$price = $_POST['price'];
// Handle deletion if price is empty
if ($price === '') {
try {
// Delete ALL prices (both global and branch-specific) for this item and service.
// The user clicking "X" intends to remove this service from the item entirely.
$stmt = db()->prepare("DELETE FROM prices WHERE item_id = ? AND service_id = ?");
$stmt->execute([$item_id, $service_id]);
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => true]);
exit;
}
header('Location: items.php?success=price_deleted');
exit;
} catch (Exception $e) {
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
exit;
}
header('Location: items.php?error=delete_failed');
exit;
}
}
$price = str_replace(',', '.', $price); // Handle comma as decimal separator
$price = (float)$price;
try {
// Prices are globally shared
$chk = db()->prepare("SELECT id FROM prices WHERE item_id = ? AND service_id = ?");
$chk->execute([$item_id, $service_id]);
$existing = $chk->fetch();
if ($existing) {
$stmt = db()->prepare("UPDATE prices SET price = ?, branch_id = NULL WHERE id = ?");
$stmt->execute([$price, $existing['id']]);
} else {
$stmt = db()->prepare("INSERT INTO prices (item_id, service_id, price, branch_id) VALUES (?, ?, ?, NULL)");
$stmt->execute([$item_id, $service_id, $price]);
}
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => true]);
exit;
}
header('Location: items.php?success=price_updated');
exit;
} catch (Exception $e) {
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
exit;
}
header('Location: items.php?error=save_failed');
exit;
}
}
}
// NOW Include header
$title = 'items';
require_once __DIR__ . '/includes/header.php';
// Search and Category Filter
$search = $_GET['search'] ?? '';
$cat_id = $_GET['category_id'] ?? '';
$current_branch_id = $_SESSION['branch_id'] ?? null;
// Fetch categories
$categories = db()->query("SELECT * FROM categories WHERE is_deleted = 0 ORDER BY name_en ASC")->fetchAll();
// Fetch services
$services = db()->query("SELECT * FROM services WHERE is_deleted = 0 ORDER BY name_en ASC")->fetchAll();
// Fetch items with filters
$where = [];
$params = [];
if ($search) {
$where[] = "(name_en LIKE ? OR name_ar LIKE ?)";
$params[] = "%$search%";
$params[] = "%$search%";
}
if ($cat_id) {
$where[] = "category_id = ?";
$params[] = $cat_id;
}
$where[] = "is_deleted = 0";
$where_sql = $where ? "WHERE " . implode(" AND ", $where) : "";
$stmt = db()->prepare("SELECT * FROM items $where_sql ORDER BY name_en ASC");
$stmt->execute($params);
$items = $stmt->fetchAll();
// Fetch prices (globally shared)
$stmt = db()->prepare("SELECT * FROM prices");
$stmt->execute();
$prices_raw = $stmt->fetchAll();
$prices = [];
foreach ($prices_raw as $p) {
$prices[$p['item_id']][$p['service_id']] = $p['price'];
}
function renderCategoryList($lang) {
$categories = db()->query("SELECT * FROM categories WHERE is_deleted = 0 ORDER BY name_en ASC")->fetchAll();
ob_start();
foreach($categories as $cat): ?>
<tr>
<td><?= $cat['name_en'] ?></td>
<td><?= $cat['name_ar'] ?></td>
<td class="text-end">
<button class="btn btn-sm btn-light p-1 me-1" onclick='editCategory(<?= json_encode($cat) ?>)'><i class="bi bi-pencil text-primary"></i></button>
<button class="btn btn-sm btn-light p-1" onclick="confirmDeleteAjax('category', <?= $cat['id'] ?>)"><i class="bi bi-trash text-danger"></i></button>
</td>
</tr>
<?php endforeach;
return ob_get_clean();
}
function renderServiceList($lang) {
$services = db()->query("SELECT * FROM services WHERE is_deleted = 0 ORDER BY name_en ASC")->fetchAll();
ob_start();
foreach($services as $svc): ?>
<tr>
<td><?= $svc['name_en'] ?></td>
<td><?= $svc['name_ar'] ?></td>
<td class="text-end">
<button class="btn btn-sm btn-light p-1 me-1" onclick='editService(<?= json_encode($svc) ?>)'><i class="bi bi-pencil text-primary"></i></button>
<button class="btn btn-sm btn-light p-1" onclick="confirmDeleteAjax('service', <?= $svc['id'] ?>)"><i class="bi bi-trash text-danger"></i></button>
</td>
</tr>
<?php endforeach;
return ob_get_clean();
}
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1"><?= __('items') ?></h2>
<p class="text-muted small mb-0"><?= __('manage_items_categories_services') ?? 'Manage your items, categories and services' ?></p>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-light shadow-sm px-3" style="border-radius: 12px;" data-bs-toggle="modal" data-bs-target="#categoriesModal">
<i class="bi bi-tags me-2 text-primary"></i> <?= __('categories') ?>
</button>
<button type="button" class="btn btn-light shadow-sm px-3" style="border-radius: 12px;" data-bs-toggle="modal" data-bs-target="#servicesModal">
<i class="bi bi-gear me-2 text-primary"></i> <?= __('services') ?>
</button>
<?php if (has_permission('add')): ?>
<button type="button" class="btn btn-primary shadow-sm px-4" style="border-radius: 12px;" data-bs-toggle="modal" data-bs-target="#itemModal" onclick="resetForm()">
<i class="bi bi-plus-lg me-2"></i> <?= __('add_item') ?>
</button>
<?php endif; ?>
</div>
</div>
<!-- Filters -->
<div class="card p-3 mb-4 border-0 shadow-sm" style="border-radius: 20px;">
<form class="row g-3">
<div class="col-md-5">
<div class="input-group">
<span class="input-group-text bg-white border-0"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control border-0" placeholder="<?= __('search_items') ?>" value="<?= htmlspecialchars($search) ?>">
</div>
</div>
<div class="col-md-4">
<select name="category_id" class="form-select border-0">
<option value=""><?= __('all_categories') ?></option>
<?php foreach($categories as $cat): ?>
<option value="<?= $cat['id'] ?>" <?= $cat_id == $cat['id'] ? 'selected' : '' ?>>
<?= $lang === 'ar' ? $cat['name_ar'] : $cat['name_en'] ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary w-100 fw-bold rounded-4 h-100"><?= __('filter') ?></button>
</div>
</form>
</div>
<!-- Items List -->
<div class="card border-0 shadow-sm mb-4" style="border-radius: 20px; overflow: hidden;">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4 py-3 border-0" style="width: 80px;"><?= __('image') ?? 'Image' ?></th>
<th class="py-3 border-0"><?= __('name') ?? 'Name' ?></th>
<th class="py-3 border-0"><?= __('category') ?? 'Category' ?></th>
<th class="py-3 border-0"><?= __('services') ?? 'Services' ?></th>
<th class="pe-4 py-3 border-0 text-end"><?= __('actions') ?? 'Actions' ?></th>
</tr>
</thead>
<tbody>
<?php foreach($items as $item): ?>
<tr>
<td class="ps-4 py-3 border-bottom">
<?php if ($item['image_url']): ?>
<img src="<?= $item['image_url'] ?>" class="rounded-3 shadow-sm" style="width: 50px; height: 50px; object-fit: cover;">
<?php else: ?>
<div class="bg-light rounded-3 d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
<i class="bi bi-image text-muted"></i>
</div>
<?php endif; ?>
</td>
<td class="py-3 border-bottom fw-bold">
<?= $lang === 'ar' ? $item['name_ar'] : $item['name_en'] ?>
</td>
<td class="py-3 border-bottom text-muted small">
<?php
$cat_name = '';
foreach($categories as $cat) {
if ($cat['id'] == $item['category_id']) {
$cat_name = $lang === 'ar' ? $cat['name_ar'] : $cat['name_en'];
break;
}
}
echo $cat_name ?: __('uncategorized');
?>
</td>
<td class="py-3 border-bottom">
<button class="btn btn-sm btn-primary text-white fw-bold rounded-pill px-3 shadow-sm d-inline-flex align-items-center gap-1" data-bs-toggle="modal" data-bs-target="#pricingModal<?= $item['id'] ?>">
<i class="bi bi-tags"></i>
<?= count($prices[$item['id']] ?? []) ?> <?= __('services') ?>
</button>
</td>
<td class="pe-4 py-3 border-bottom text-end">
<div class="d-inline-flex gap-1">
<?php if (has_permission('edit')): ?>
<button class="btn btn-sm btn-light p-2 rounded-3 text-primary border border-primary border-opacity-25" onclick='editItem(<?= json_encode($item) ?>)' title="<?= __('edit') ?>">
<i class="bi bi-pencil px-1"></i>
</button>
<?php endif; ?>
<?php if (has_permission('delete')): ?>
<button class="btn btn-sm btn-light p-2 rounded-3 text-danger border border-danger border-opacity-25" onclick="confirmDeleteItem(<?= $item['id'] ?>)" title="<?= __('delete') ?>">
<i class="bi bi-trash px-1"></i>
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php if (empty($items)): ?>
<div class="text-center p-5">
<i class="bi bi-inbox text-muted" style="font-size: 4rem;"></i>
<p class="text-muted mt-3"><?= __('no_items_found') ?></p>
</div>
<?php endif; ?>
<!-- Item Modal -->
<div class="modal fade" id="itemModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="border-radius: 30px;">
<div class="modal-header border-0 p-4 pb-0">
<h5 class="modal-title fw-bold" id="itemModalTitle"><?= __('add_new_item') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="itemForm" method="POST" enctype="multipart/form-data">
<input type="hidden" name="action" id="itemAction" value="add_item">
<input type="hidden" name="id" id="itemId">
<input type="hidden" name="current_image_url" id="itemCurrentImageUrl">
<div class="modal-body p-4">
<div class="mb-4 text-center">
<div id="imagePreviewContainer" class="d-none mb-3">
<img id="imagePreview" src="" class="rounded-4 shadow-sm" style="max-height: 150px; max-width: 100%; object-fit: cover;">
</div>
<label for="itemImageFile" class="btn btn-light rounded-4 px-4 py-3 w-100 border-2 border-dashed" style="border-style: dashed !important;">
<i class="bi bi-cloud-upload me-2 fs-4"></i>
<div class="small fw-bold"><?= __('upload_image') ?></div>
<input type="file" name="image" id="itemImageFile" class="d-none" accept="image/*">
</label>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small fw-bold"><?= __('name_en') ?></label>
<div class="input-group">
<input type="text" name="name_en" id="itemNameEn" class="form-control" required style="border-radius: 12px 0 0 12px;">
<button type="button" class="btn btn-outline-secondary" onclick="translateField('itemNameEn', 'itemNameAr', 'en-ar')" style="border-radius: 0 12px 12px 0;">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold"><?= __('name_ar') ?></label>
<div class="input-group">
<input type="text" name="name_ar" id="itemNameAr" class="form-control text-end" required style="border-radius: 12px 0 0 12px;">
<button type="button" class="btn btn-outline-secondary" onclick="translateField('itemNameAr', 'itemNameEn', 'ar-en')" style="border-radius: 0 12px 12px 0;">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
<div class="col-md-8">
<label class="form-label small fw-bold"><?= __('category') ?></label>
<select name="category_id" id="itemCategoryId" class="form-select" style="border-radius: 12px;">
<option value=""><?= __('select_category') ?></option>
<?php foreach($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= $lang === 'ar' ? $cat['name_ar'] : $cat['name_en'] ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold"><?= __('vat_percent') ?></label>
<input type="number" step="0.01" name="vat_percent" id="itemVatPercent" class="form-control" value="15.00" style="border-radius: 12px;">
</div>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-4 px-4" data-bs-dismiss="modal"><?= __('cancel') ?></button>
<button type="submit" class="btn btn-primary rounded-4 px-4 fw-bold shadow-sm"><?= __('save') ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Categories Modal -->
<div class="modal fade" id="categoriesModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content" style="border-radius: 25px;">
<div class="modal-header border-0 p-4">
<h5 class="modal-title fw-bold"><?= __('categories_management') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4 pt-0">
<div class="row g-4">
<div class="col-md-5">
<form id="categoryForm" method="POST">
<input type="hidden" name="action" id="catAction" value="add_category">
<input type="hidden" name="id" id="catId">
<input type="hidden" name="ajax" value="1">
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('name_en') ?></label>
<div class="input-group">
<input type="text" name="cat_name_en" id="catNameEn" class="form-control" required>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="translateField('catNameEn', 'catNameAr', 'en-ar')">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('name_ar') ?></label>
<div class="input-group">
<input type="text" name="cat_name_ar" id="catNameAr" class="form-control text-end" required>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="translateField('catNameAr', 'catNameEn', 'ar-en')">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 fw-bold" style="border-radius: 10px;">
<span id="catSubmitBtn"><?= __('add_category') ?></span>
</button>
<button type="button" id="catCancelBtn" class="btn btn-light w-100 mt-2 d-none" onclick="resetCatForm()" style="border-radius: 10px;">
<?= __('cancel') ?>
</button>
</form>
</div>
<div class="col-md-7">
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
<table class="table table-hover small">
<thead>
<tr>
<th><?= __('name_en') ?></th>
<th><?= __('name_ar') ?></th>
<th class="text-end"><?= __('actions') ?></th>
</tr>
</thead>
<tbody id="categoryTableBody">
<?= renderCategoryList($lang) ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Services Modal -->
<div class="modal fade" id="servicesModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content" style="border-radius: 25px;">
<div class="modal-header border-0 p-4">
<h5 class="modal-title fw-bold"><?= __('services_management') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4 pt-0">
<div class="row g-4">
<div class="col-md-5">
<form id="serviceForm" method="POST">
<input type="hidden" name="action" id="svcAction" value="add_service">
<input type="hidden" name="id" id="svcId">
<input type="hidden" name="ajax" value="1">
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('name_en') ?></label>
<div class="input-group">
<input type="text" name="svc_name_en" id="svcNameEn" class="form-control" required>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="translateField('svcNameEn', 'svcNameAr', 'en-ar')">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('name_ar') ?></label>
<div class="input-group">
<input type="text" name="svc_name_ar" id="svcNameAr" class="form-control text-end" required>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="translateField('svcNameAr', 'svcNameEn', 'ar-en')">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 fw-bold" style="border-radius: 10px;">
<span id="svcSubmitBtn"><?= __('add_service') ?></span>
</button>
<button type="button" id="svcCancelBtn" class="btn btn-light w-100 mt-2 d-none" onclick="resetSvcForm()" style="border-radius: 10px;">
<?= __('cancel') ?>
</button>
</form>
</div>
<div class="col-md-7">
<div class="table-responsive" style="max-height: 400px; overflow-y: auto;">
<table class="table table-hover small">
<thead>
<tr>
<th><?= __('name_en') ?></th>
<th><?= __('name_ar') ?></th>
<th class="text-end"><?= __('actions') ?></th>
</tr>
</thead>
<tbody id="serviceTableBody">
<?= renderServiceList($lang) ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Pricing Modals -->
<?php foreach($items as $item): ?>
<div class="modal fade" id="pricingModal<?= $item['id'] ?>" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" style="border-radius: 25px;">
<div class="modal-header border-0 p-4">
<h5 class="modal-title fw-bold">
<?= __('set_prices_for') ?> <?= $lang === 'ar' ? $item['name_ar'] : $item['name_en'] ?>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4 pt-0">
<div class="alert alert-light small border-0 mb-4" style="border-radius: 15px;">
<i class="bi bi-info-circle me-2"></i><?= __('prices_saved_automatically') ?>
</div>
<?php
$assigned_services = [];
$unassigned_services = [];
foreach($services as $svc) {
if (isset($prices[$item['id']][$svc['id']])) {
$assigned_services[] = $svc;
} else {
$unassigned_services[] = $svc;
}
}
?>
<h6 class="fw-bold mb-3 small text-success"><?= __('assigned_services') ?? 'Assigned Services' ?></h6>
<div class="assigned-list">
<?php if (empty($assigned_services)): ?>
<div class="text-muted small mb-3 fst-italic no-services-msg"><?= __('no_services_assigned') ?? 'No services assigned to this item.' ?></div>
<?php endif; ?>
<?php foreach($assigned_services as $svc): ?>
<div class="mb-3 d-flex align-items-center justify-content-between p-3 bg-white border border-success border-opacity-25 shadow-sm service-row" style="border-radius: 15px;" id="row-<?= $item['id'] ?>-<?= $svc['id'] ?>">
<div class="fw-bold small">
<?= $lang === 'ar' ? $svc['name_ar'] : $svc['name_en'] ?>
</div>
<div class="d-flex align-items-center gap-2" style="width: 200px;">
<input type="number" step="0.001"
class="form-control form-control-sm border bg-light price-input"
style="border-radius: 10px;"
data-item-id="<?= $item['id'] ?>"
data-service-id="<?= $svc['id'] ?>"
value="<?= $prices[$item['id']][$svc['id']] ?>"
placeholder="0.000">
<button type="button" class="btn btn-sm btn-light p-1 remove-btn" style="border-radius: 8px;" onclick="removePrice(this, <?= $item['id'] ?>, <?= $svc['id'] ?>)">
<i class="bi bi-x-circle text-danger"></i>
</button>
<i class="bi bi-check-circle-fill text-success ms-1 success-indicator d-none"></i>
</div>
</div>
<?php endforeach; ?>
</div>
<h6 class="fw-bold mb-3 mt-4 small text-muted"><?= __('available_services') ?? 'Available Services' ?></h6>
<div class="unassigned-list">
<?php foreach($unassigned_services as $svc): ?>
<div class="mb-3 d-flex align-items-center justify-content-between p-3 bg-light service-row" style="border-radius: 15px;" id="row-<?= $item['id'] ?>-<?= $svc['id'] ?>">
<div class="fw-bold small text-muted">
<?= $lang === 'ar' ? $svc['name_ar'] : $svc['name_en'] ?>
</div>
<div class="d-flex align-items-center gap-2" style="width: 200px;">
<input type="number" step="0.001"
class="form-control form-control-sm border-0 bg-white price-input"
style="border-radius: 10px;"
data-item-id="<?= $item['id'] ?>"
data-service-id="<?= $svc['id'] ?>"
value=""
placeholder="<?= __('add') ?? 'Add...' ?>">
<button type="button" class="btn btn-sm btn-light p-1 remove-btn" style="border-radius: 8px; display: none;" onclick="removePrice(this, <?= $item['id'] ?>, <?= $svc['id'] ?>)">
<i class="bi bi-x-circle text-danger"></i>
</button>
<i class="bi bi-check-circle-fill text-success ms-1 success-indicator d-none"></i>
</div>
</div>
<?php endforeach; ?>
</div>
<button type="button" class="btn btn-dark w-100 py-3 fw-bold mt-3 shadow-sm" style="border-radius: 15px;" data-bs-dismiss="modal">
<?= __('done') ?>
</button>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
<script>
function translateField(sourceId, targetId, direction) {
const text = document.getElementById(sourceId).value;
if (!text) return;
const formData = new FormData();
formData.append("text", text);
formData.append("direction", direction);
fetch("api/translate.php", {
method: "POST",
body: formData
})
.then(res => res.json())
.then(data => {
if (data.success) {
document.getElementById(targetId).value = data.translation;
}
});
}
function resetForm() {
document.getElementById('itemForm').reset();
document.getElementById('itemAction').value = 'add_item';
document.getElementById('itemModalTitle').innerText = '<?= __('add_new_item') ?>';
document.getElementById('imagePreviewContainer').classList.add('d-none');
}
function editItem(item) {
resetForm();
document.getElementById('itemAction').value = 'edit_item';
document.getElementById('itemId').value = item.id;
document.getElementById('itemNameEn').value = item.name_en;
document.getElementById('itemNameAr').value = item.name_ar;
document.getElementById('itemCategoryId').value = item.category_id;
document.getElementById('itemVatPercent').value = item.vat_percent;
document.getElementById('itemCurrentImageUrl').value = item.image_url;
document.getElementById('itemModalTitle').innerText = '<?= __('edit_item') ?>';
if (item.image_url) {
document.getElementById('imagePreview').src = item.image_url;
document.getElementById('imagePreviewContainer').classList.remove('d-none');
}
new bootstrap.Modal(document.getElementById('itemModal')).show();
}
function confirmDeleteItem(id) {
if (confirm('<?= __('confirm_delete') ?>')) {
const form = document.createElement('form');
form.method = 'POST';
form.innerHTML = `<input type="hidden" name="action" value="delete_item"><input type="hidden" name="id" value="${id}">`;
document.body.appendChild(form);
form.submit();
}
}
function editCategory(cat) {
document.getElementById('catAction').value = 'edit_category';
document.getElementById('catId').value = cat.id;
document.getElementById('catNameEn').value = cat.name_en;
document.getElementById('catNameAr').value = cat.name_ar;
document.getElementById('catSubmitBtn').innerText = '<?= __('save') ?>';
document.getElementById('catCancelBtn').classList.remove('d-none');
}
function resetCatForm() {
document.getElementById('categoryForm').reset();
document.getElementById('catAction').value = 'add_category';
document.getElementById('catSubmitBtn').innerText = '<?= __('add_category') ?>';
document.getElementById('catCancelBtn').classList.add('d-none');
}
function editService(svc) {
document.getElementById('svcAction').value = 'edit_service';
document.getElementById('svcId').value = svc.id;
document.getElementById('svcNameEn').value = svc.name_en;
document.getElementById('svcNameAr').value = svc.name_ar;
document.getElementById('svcSubmitBtn').innerText = '<?= __('save') ?>';
document.getElementById('svcCancelBtn').classList.remove('d-none');
}
function resetSvcForm() {
document.getElementById('serviceForm').reset();
document.getElementById('svcAction').value = 'add_service';
document.getElementById('svcSubmitBtn').innerText = '<?= __('add_service') ?>';
document.getElementById('svcCancelBtn').classList.add('d-none');
}
function confirmDeleteAjax(type, id) {
if (confirm('<?= __('confirm_delete') ?>')) {
const formData = new FormData();
formData.append('action', 'delete_' + type);
formData.append('id', id);
formData.append('ajax', '1');
fetch('items.php', { method: 'POST', body: formData })
.then(res => res.json())
.then(data => {
if (data.success) {
if (type === 'category') document.getElementById('categoryTableBody').innerHTML = data.html;
if (type === 'service') document.getElementById('serviceTableBody').innerHTML = data.html;
} else {
alert(data.error || 'Failed to delete');
}
}).catch(err => {
alert('An error occurred while deleting.');
});
}
}
document.getElementById('categoryForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
const response = await fetch('items.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.success) {
document.getElementById('categoryTableBody').innerHTML = data.html;
resetCatForm();
}
} catch (error) {}
});
document.getElementById('serviceForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
const response = await fetch('items.php', { method: 'POST', body: formData });
const data = await response.json();
if (data.success) {
document.getElementById('serviceTableBody').innerHTML = data.html;
resetSvcForm();
} else {
alert(data.error || 'Failed to save service');
}
} catch (error) {
console.error('Error saving service:', error);
}
});
// AJAX logic for prices
document.querySelectorAll('.price-input').forEach(input => {
let timeout = null;
input.addEventListener('input', function() {
if (this.readOnly || this.disabled) return;
clearTimeout(timeout);
const self = this;
timeout = setTimeout(async () => {
const formData = new FormData();
formData.append('action', 'update_price');
formData.append('ajax', '1');
formData.append('item_id', self.dataset.itemId);
formData.append('service_id', self.dataset.serviceId);
formData.append('price', self.value);
try {
const response = await fetch('items.php', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
showIndicator(self);
const row = self.closest('.service-row');
const assignedList = row.closest('.modal-body').querySelector('.assigned-list');
if (row && assignedList && row.parentElement.classList.contains('unassigned-list')) {
row.classList.remove('bg-light');
row.classList.add('border', 'border-success', 'border-opacity-25', 'bg-white', 'shadow-sm');
const titleDiv = row.querySelector('.fw-bold.small');
if(titleDiv) titleDiv.classList.remove('text-muted');
const noMsg = assignedList.querySelector('.no-services-msg');
if(noMsg) noMsg.style.display = 'none';
assignedList.appendChild(row);
const btn = row.querySelector('.remove-btn');
if (btn) btn.style.display = 'inline-block';
}
} else {
alert(data.error || 'Failed to update price');
}
} catch (error) {
console.error('Error updating price:', error);
alert('Error updating price: ' + error.message);
}
}, 500);
});
});
async function removePrice(btn, itemId, serviceId) {
if (confirm('<?= __('confirm_delete') ?>')) {
const input = btn.parentElement.querySelector('.price-input');
const formData = new FormData();
formData.append('action', 'update_price');
formData.append('ajax', '1');
formData.append('item_id', itemId);
formData.append('service_id', serviceId);
formData.append('price', ''); // Empty price triggers deletion
try {
const response = await fetch('items.php', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
input.value = '';
showIndicator(input);
// Move visually to available services
const row = btn.closest('.service-row');
if (row) {
row.classList.remove('border', 'border-success', 'border-opacity-25', 'bg-white', 'shadow-sm');
row.classList.add('bg-light');
const titleDiv = row.querySelector('.fw-bold.small');
if(titleDiv) titleDiv.classList.add('text-muted');
const unassignedList = row.closest('.modal-body').querySelector('.unassigned-list');
if (unassignedList) {
unassignedList.appendChild(row);
btn.style.display = 'none'; // hide X button
}
}
} else {
alert(data.error || 'Failed to remove price');
}
} catch (error) {
console.error('Error removing price:', error);
}
}
}
function showIndicator(input) {
const indicator = input.parentElement.querySelector('.success-indicator');
if (indicator) {
indicator.classList.remove('d-none');
setTimeout(() => {
indicator.classList.add('d-none');
}, 2000);
}
}
// Image preview logic
document.getElementById('itemImageFile').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('imagePreview').src = e.target.result;
document.getElementById('imagePreviewContainer').classList.remove('d-none');
}
reader.readAsDataURL(file);
}
});
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

346
lab.php Normal file
View File

@ -0,0 +1,346 @@
<?php
$title = 'lab';
require_once __DIR__ . '/includes/header.php';
// View check is handled by header.php global check.
// Filters
$status_filter = $_GET['status'] ?? '';
$branch_filter = $_GET['branch_id'] ?? '';
$from_date = $_GET['from_date'] ?? '';
$to_date = $_GET['to_date'] ?? '';
$search = $_GET['search'] ?? '';
// Pagination
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($page < 1) $page = 1;
$limit = 10;
$offset = ($page - 1) * $limit;
$sql_base = " FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
LEFT JOIN branches b ON o.branch_id = b.id
WHERE 1=1";
$params = [];
if ($status_filter) {
$sql_base .= " AND o.status = ?";
$params[] = $status_filter;
}
if ($branch_filter) {
$sql_base .= " AND o.branch_id = ?";
$params[] = $branch_filter;
}
if ($from_date) {
$sql_base .= " AND DATE(o.created_at) >= ?";
$params[] = $from_date;
}
if ($to_date) {
$sql_base .= " AND DATE(o.created_at) <= ?";
$params[] = $to_date;
}
if ($search) {
$sql_base .= " AND (o.order_number LIKE ? OR c.phone LIKE ? OR c.name_en LIKE ? OR c.name_ar LIKE ?)";
$search_param = "%$search%";
$params[] = $search_param;
$params[] = $search_param;
$params[] = $search_param;
$params[] = $search_param;
}
// Total count for pagination
$count_sql = "SELECT COUNT(*) " . $sql_base;
$stmt_count = db()->prepare($count_sql);
$stmt_count->execute($params);
$total_items = $stmt_count->fetchColumn();
$total_pages = ceil($total_items / $limit);
// Final data query
$sql = "SELECT o.*, c.name_en as customer_name_en, c.name_ar as customer_name_ar, c.phone as customer_phone,
b.name_en as branch_name_en, b.name_ar as branch_name_ar " . $sql_base . " ORDER BY o.created_at DESC LIMIT $limit OFFSET $offset";
$stmt = db()->prepare($sql);
$stmt->execute($params);
$orders = $stmt->fetchAll();
$branches = db()->query("SELECT id, name_en, name_ar FROM branches")->fetchAll();
?>
<div class="card p-4 shadow-sm border-0" style="border-radius: 20px;">
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4">
<div>
<h5 class="fw-bold mb-0"><?= __('lab') ?? 'Lab Module' ?></h5>
<p class="text-muted small mb-0"><?= __('manage_orders_across_outlets') ?? 'Manage orders across all outlets' ?></p>
</div>
</div>
<!-- Filters -->
<form action="" method="GET" class="row g-3 mb-4">
<div class="col-md-3">
<input type="text" name="search" class="form-control" placeholder="<?= __('search_placeholder') ?>" value="<?= htmlspecialchars($search) ?>" style="border-radius: 12px;">
</div>
<div class="col-md-2">
<select name="branch_id" class="form-select border-radius-12" style="border-radius: 12px;" onchange="this.form.submit()">
<option value=""><?= __('all_branches') ?? 'All Branches' ?></option>
<?php foreach($branches as $b): ?>
<option value="<?= $b['id'] ?>" <?= $branch_filter == $b['id'] ? 'selected' : '' ?>>
<?= $lang === 'ar' ? ($b['name_ar'] ?: $b['name_en']) : $b['name_en'] ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<select name="status" class="form-select border-radius-12" style="border-radius: 12px;" onchange="this.form.submit()">
<option value=""><?= __('all_status') ?? 'All Status' ?></option>
<option value="received" <?= $status_filter == 'received' ? 'selected' : '' ?>><?= __('received') ?></option>
<option value="processing" <?= $status_filter == 'processing' ? 'selected' : '' ?>><?= __('processing') ?></option>
<option value="ready" <?= $status_filter == 'ready' ? 'selected' : '' ?>><?= __('ready') ?></option>
<option value="delivered" <?= $status_filter == 'delivered' ? 'selected' : '' ?>><?= __('delivered') ?></option>
<option value="cancelled" <?= $status_filter == 'cancelled' ? 'selected' : '' ?>><?= __('cancelled') ?></option>
</select>
</div>
<div class="col-md-5 d-flex gap-2">
<input type="date" name="from_date" class="form-control" value="<?= $from_date ?>" style="border-radius: 12px;" title="<?= __('from_date') ?>">
<input type="date" name="to_date" class="form-control" value="<?= $to_date ?>" style="border-radius: 12px;" title="<?= __('to_date') ?>">
<button type="submit" class="btn btn-light shadow-sm" style="border-radius: 12px;"><i class="bi bi-search"></i></button>
</div>
</form>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="bg-light">
<tr>
<th class="ps-4">#</th>
<th><?= __('order_number') ?? 'Order #' ?></th>
<th><?= __('outlet') ?? 'Outlet' ?></th>
<th><?= __('customer') ?></th>
<th><?= __('status') ?></th>
<th><?= __('date') ?></th>
<th class="text-end pe-4"><?= __('actions') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($orders as $order): ?>
<tr>
<td class="ps-4"><?= $order['id'] ?></td>
<td class="fw-bold"><?= $order['order_number'] ?></td>
<td>
<span class="badge bg-light text-dark border fw-normal">
<?= $lang === 'ar' ? ($order['branch_name_ar'] ?: $order['branch_name_en']) : $order['branch_name_en'] ?>
</span>
</td>
<td>
<div class="fw-bold"><?= $lang === 'ar' ? ($order['customer_name_ar'] ?: $order['customer_name_en']) : $order['customer_name_en'] ?></div>
<div class="small text-muted"><?= $order['customer_phone'] ?></div>
</td>
<td><span class="badge bg-<?= getStatusColor($order['status']) ?> rounded-pill px-3 py-2"><?= __($order['status']) ?></span></td>
<td class="small text-muted"><?= date('d/m/Y H:i', strtotime($order['created_at'])) ?></td>
<td class="text-end pe-4">
<div class="d-flex gap-1 justify-content-end">
<button class="btn btn-sm btn-light border-0 p-2 text-primary view-items-btn shadow-sm" data-id="<?= $order['id'] ?>" title="<?= __('view_items') ?>" style="border-radius: 8px;">
<i class="bi bi-eye-fill"></i>
</button>
<?php if (has_permission('edit')): ?>
<button class="btn btn-sm btn-light border-0 p-2 text-info status-change-btn shadow-sm" data-id="<?= $order['id'] ?>" data-status="<?= $order['status'] ?>" title="<?= __('update_status') ?>" style="border-radius: 8px;">
<i class="bi bi-arrow-repeat"></i>
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($orders)): ?>
<tr>
<td colspan="7" class="text-center py-5 text-muted">
<i class="bi bi-flask fs-1 d-block mb-3 opacity-25"></i>
<?= __('no_orders_found') ?? 'No orders found' ?>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($total_pages > 1): ?>
<nav class="mt-4">
<ul class="pagination justify-content-center">
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
<a class="page-link shadow-sm" href="?<?= http_build_query(array_merge($_GET, ['page' => $page - 1])) ?>" style="border-radius: 8px 0 0 8px;"><?= __('previous') ?></a>
</li>
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
<li class="page-item <?= $page == $i ? 'active' : '' ?>">
<a class="page-link shadow-sm" href="?<?= http_build_query(array_merge($_GET, ['page' => $i])) ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
<li class="page-item <?= $page >= $total_pages ? 'disabled' : '' ?>">
<a class="page-link shadow-sm" href="?<?= http_build_query(array_merge($_GET, ['page' => $page + 1])) ?>" style="border-radius: 0 8px 8px 0;"><?= __('next') ?></a>
</li>
</ul>
</nav>
<?php endif; ?>
</div>
<!-- View Items Modal -->
<div class="modal fade" id="itemsModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 rounded-4 shadow-lg">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold"><?= __('order') ?> #<span id="displayOrderNumber"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<div id="itemsContainer">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status"></div>
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-3 shadow-sm" data-bs-dismiss="modal"><?= __('close') ?></button>
</div>
</div>
</div>
</div>
<!-- Status Update Modal -->
<?php if (has_permission('edit')): ?>
<div class="modal fade" id="statusModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 rounded-4 shadow-lg">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold"><?= __('update_status') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<input type="hidden" id="modalOrderId">
<div class="row g-2">
<div class="col-6">
<button class="btn btn-outline-secondary w-100 py-3 rounded-4 status-opt shadow-sm" data-status="received"><?= __('received') ?></button>
</div>
<div class="col-6">
<button class="btn btn-outline-primary w-100 py-3 rounded-4 status-opt shadow-sm" data-status="processing"><?= __('processing') ?></button>
</div>
<div class="col-6">
<button class="btn btn-outline-success w-100 py-3 rounded-4 status-opt shadow-sm" data-status="ready"><?= __('ready') ?></button>
</div>
<div class="col-6">
<button class="btn btn-outline-dark w-100 py-3 rounded-4 status-opt shadow-sm" data-status="delivered"><?= __('delivered') ?></button>
</div>
<div class="col-12 mt-2">
<button class="btn btn-outline-danger w-100 py-3 rounded-4 status-opt shadow-sm" data-status="cancelled"><?= __('cancelled') ?></button>
</div>
</div>
</div>
</div>
</div>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', () => {
const statusModalEl = document.getElementById('statusModal');
const statusModal = statusModalEl ? new bootstrap.Modal(statusModalEl) : null;
const itemsModal = new bootstrap.Modal(document.getElementById('itemsModal'));
// View Items Logic
document.querySelectorAll('.view-items-btn').forEach(btn => {
btn.onclick = async () => {
const id = btn.dataset.id;
document.getElementById('displayOrderNumber').innerText = '...';
document.getElementById('itemsContainer').innerHTML = `
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status"></div>
</div>
`;
itemsModal.show();
try {
const res = await fetch(`api/get_order_items.php?id=${id}`);
const data = await res.json();
if (data.success) {
document.getElementById('displayOrderNumber').innerText = data.order_number;
let html = `<ul class="list-group list-group-flush">`;
data.items.forEach(item => {
const itemName = '<?= $lang ?>' === 'ar' ? (item.item_ar || item.item_en) : item.item_en;
html += `
<li class="list-group-item d-flex justify-content-between align-items-center py-3 border-0 border-bottom">
<div class="fw-bold text-dark">${itemName}</div>
<span class="badge bg-primary rounded-pill px-3 fs-6 shadow-sm">${item.quantity}</span>
</li>
`;
});
html += `</ul>`;
document.getElementById('itemsContainer').innerHTML = html;
} else {
document.getElementById('itemsContainer').innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
}
} catch (e) {
document.getElementById('itemsContainer').innerHTML = `<div class="alert alert-danger">Error loading items</div>`;
}
};
});
// Status Change Logic
document.querySelectorAll('.status-change-btn').forEach(btn => {
btn.onclick = () => {
if (!statusModal) return;
const id = btn.dataset.id;
const currentStatus = btn.dataset.status;
document.getElementById('modalOrderId').value = id;
document.querySelectorAll('.status-opt').forEach(opt => {
opt.classList.remove('active');
if (opt.dataset.status === currentStatus) opt.classList.add('active');
});
statusModal.show();
};
});
document.querySelectorAll('.status-opt').forEach(btn => {
btn.onclick = async () => {
const id = document.getElementById('modalOrderId').value;
const status = btn.dataset.status;
try {
const res = await fetch('api/update_order_status.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, status })
});
const data = await res.json();
if (data.success) {
location.reload();
} else {
alert(data.error);
}
} catch (e) {
alert('Error updating status');
}
};
});
});
</script>
<style>
.status-opt.active {
background-color: var(--bs-primary);
color: white;
border-color: var(--bs-primary);
}
.status-opt:hover {
transform: translateY(-2px);
transition: all 0.2s;
}
</style>
<?php
if (!function_exists('getStatusColor')) { function getStatusColor($status) {
return [
'received' => 'secondary',
'processing' => 'primary',
'ready' => 'success',
'delivered' => 'dark',
'cancelled' => 'danger',
][$status] ?? 'info';
}}
require_once __DIR__ . '/includes/footer.php';
?>

127
login.php Normal file
View File

@ -0,0 +1,127 @@
<?php
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/lang.php';
// Fetch Global Company Info
$stmt = db()->query("SELECT * FROM companies LIMIT 1");
$company_info = $stmt->fetch();
if (isset($_SESSION['user_id'])) {
header('Location: admin.php');
exit;
}
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$stmt = db()->prepare("SELECT u.*, b.name_en as branch_name_en, b.name_ar as branch_name_ar
FROM users u
LEFT JOIN branches b ON u.branch_id = b.id
WHERE u.username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['branch_id'] = $user['branch_id'];
$_SESSION['company_id'] = $user['company_id'];
$_SESSION['role'] = $user['role'];
$_SESSION['full_name'] = $lang === 'ar' ? ($user['full_name_ar'] ?: $user['full_name_en']) : $user['full_name_en'];
$_SESSION['branch_name'] = $lang === 'ar' ? ($user['branch_name_ar'] ?: $user['branch_name_en']) : $user['branch_name_en'];
header('Location: admin.php');
exit;
} else {
$error = 'Invalid username or password';
}
}
?>
<!doctype html>
<html lang="<?= $lang ?>" dir="<?= is_rtl() ? 'rtl' : 'ltr' ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars(is_arabic() ? $company_info['name_ar'] : $company_info['name_en']) ?> - <?= __('login') ?></title>
<?php if ($company_info['favicon']): ?>
<link rel="icon" type="image/x-icon" href="<?= $company_info['favicon'] ?>">
<?php endif; ?>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.<?= is_rtl() ? 'rtl.' : '' ?>min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Cairo:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: 'Inter', <?= is_rtl() ? "'Cairo'," : '' ?> sans-serif;
}
.login-card {
width: 100%;
max-width: 400px;
padding: 2.5rem;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
background: #fff;
}
.login-logo {
max-height: 80px;
width: auto;
margin-bottom: 1.5rem;
}
.btn-primary {
background-color: #3b82f6;
border-color: #3b82f6;
}
.btn-primary:hover {
background-color: #2563eb;
border-color: #2563eb;
}
.form-control:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 0.25rem rgba(59, 130, 246, 0.25);
}
</style>
</head>
<body>
<div class="login-card">
<div class="text-center">
<?php if ($company_info['logo']): ?>
<img src="<?= $company_info['logo'] ?>" alt="Logo" class="login-logo">
<?php endif; ?>
<h3 class="mb-4 fw-bold"><?= htmlspecialchars(is_arabic() ? $company_info['name_ar'] : $company_info['name_en']) ?></h3>
</div>
<?php if ($error): ?>
<div class="alert alert-danger py-2"><?= $error ?></div>
<?php endif; ?>
<form method="POST">
<div class="mb-3">
<label class="form-label"><?= __('username') ?></label>
<input type="text" name="username" class="form-control" placeholder="<?= __('username') ?>" required>
</div>
<div class="mb-4">
<label class="form-label"><?= __('password') ?></label>
<input type="password" name="password" class="form-control" placeholder="<?= __('password') ?>" required>
</div>
<button type="submit" class="btn btn-primary w-100 py-2 fw-bold mb-3" style="border-radius: 12px;"><?= __('login') ?></button>
</form>
<div class="text-center mt-3 pt-3 border-top">
<div class="nav-link text-muted small">
<a href="<?= lang_url('en') ?>" class="text-decoration-none <?= $lang === 'en' ? 'fw-bold text-primary' : '' ?>">English</a>
<span class="mx-2 text-secondary opacity-25">|</span>
<a href="<?= lang_url('ar') ?>" class="text-decoration-none <?= $lang === 'ar' ? 'fw-bold text-primary' : '' ?>">العربية</a>
</div>
</div>
</div>
</body>
</html>

5
logout.php Normal file
View File

@ -0,0 +1,5 @@
<?php
session_start();
session_destroy();
header('Location: login.php');
exit;

View File

@ -1,54 +1,24 @@
<?php <?php
// Mail configuration sourced from environment variables. // Mail configuration sourced from settings and environment variables.
// No secrets are stored here; the file just maps env -> config array for MailService.
require_once __DIR__ . '/../includes/dotenv.php';
require_once __DIR__ . '/../db/config.php';
function env_val(string $key, $default = null) { function env_val(string $key, $default = null) {
$v = getenv($key); $v = getenv($key);
return ($v === false || $v === null || $v === '') ? $default : $v; return ($v === false || $v === null || $v === '') ? $default : $v;
} }
// Fallback: if critical vars are missing from process env, try to parse executor/.env $transport = get_setting('mail_transport') ?: env_val('MAIL_TRANSPORT', 'smtp');
// This helps in web/Apache contexts where .env is not exported. $smtp_host = get_setting('smtp_host') ?: env_val('SMTP_HOST');
// Supports simple KEY=VALUE lines; ignores quotes and comments. $smtp_port = (int) (get_setting('smtp_port') ?: env_val('SMTP_PORT', 587));
function load_dotenv_if_needed(array $keys): void { $smtp_secure = get_setting('smtp_secure') ?: env_val('SMTP_SECURE', 'tls'); // tls | ssl | null
$missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === ''); $smtp_user = get_setting('smtp_user') ?: env_val('SMTP_USER');
if (empty($missing)) return; $smtp_pass = get_setting('smtp_pass') ?: env_val('SMTP_PASS');
static $loaded = false;
if ($loaded) return;
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
if ($envPath && is_readable($envPath)) {
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
foreach ($lines as $line) {
if ($line[0] === '#' || trim($line) === '') continue;
if (!str_contains($line, '=')) continue;
[$k, $v] = array_map('trim', explode('=', $line, 2));
// Strip potential surrounding quotes
$v = trim($v, "\"' ");
// Do not override existing env
if ($k !== '' && (getenv($k) === false || getenv($k) === '')) {
putenv("{$k}={$v}");
}
}
$loaded = true;
}
}
load_dotenv_if_needed([ $from_email = get_setting('mail_from') ?: env_val('MAIL_FROM', 'no-reply@localhost');
'MAIL_TRANSPORT','SMTP_HOST','SMTP_PORT','SMTP_SECURE','SMTP_USER','SMTP_PASS', $from_name = get_setting('mail_from_name') ?: env_val('MAIL_FROM_NAME', 'App');
'MAIL_FROM','MAIL_FROM_NAME','MAIL_REPLY_TO','MAIL_TO', $reply_to = get_setting('mail_reply_to') ?: env_val('MAIL_REPLY_TO');
'DKIM_DOMAIN','DKIM_SELECTOR','DKIM_PRIVATE_KEY_PATH'
]);
$transport = env_val('MAIL_TRANSPORT', 'smtp');
$smtp_host = env_val('SMTP_HOST');
$smtp_port = (int) env_val('SMTP_PORT', 587);
$smtp_secure = env_val('SMTP_SECURE', 'tls'); // tls | ssl | null
$smtp_user = env_val('SMTP_USER');
$smtp_pass = env_val('SMTP_PASS');
$from_email = env_val('MAIL_FROM', 'no-reply@localhost');
$from_name = env_val('MAIL_FROM_NAME', 'App');
$reply_to = env_val('MAIL_REPLY_TO');
$dkim_domain = env_val('DKIM_DOMAIN'); $dkim_domain = env_val('DKIM_DOMAIN');
$dkim_selector = env_val('DKIM_SELECTOR'); $dkim_selector = env_val('DKIM_SELECTOR');
@ -73,4 +43,4 @@ return [
'dkim_domain' => $dkim_domain, 'dkim_domain' => $dkim_domain,
'dkim_selector' => $dkim_selector, 'dkim_selector' => $dkim_selector,
'dkim_private_key_path' => $dkim_private_key_path, 'dkim_private_key_path' => $dkim_private_key_path,
]; ];

297
order_details.php Normal file
View File

@ -0,0 +1,297 @@
<?php
// ACTION HANDLING FIRST
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/lang.php';
require_once __DIR__ . '/includes/whatsapp.php';
$order_id = $_GET['id'] ?? null;
if (!$order_id) {
header('Location: orders.php');
exit;
}
$stmt = db()->prepare("SELECT o.*, c.name_en as customer_name_en, c.name_ar as customer_name_ar, c.phone as customer_phone, c.address_en as customer_address_en, c.address_ar as customer_address_ar
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
WHERE o.id = ?");
$stmt->execute([$order_id]);
$order = $stmt->fetch();
if (!$order) {
echo "Order not found";
exit;
}
// Handle status updates
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if ($_POST['action'] === 'update_status') {
$new_status = $_POST['status'];
$stmt = db()->prepare("UPDATE orders SET status = ? WHERE id = ?");
$stmt->execute([$new_status, $order_id]);
// WhatsApp Notification for Order Ready
try {
if ($new_status === 'ready' && get_setting('whatsapp_enabled') === '1' && !empty($order['customer_phone'])) {
$template = get_setting('msg_order_ready_ar');
if (!empty($template)) {
$message = str_replace(
['{customer_name}', '{order_number}'],
[$order['customer_name_ar'], $order['order_number']],
$template
);
send_whatsapp_message($order['customer_phone'], $message);
}
}
} catch (Exception $e) {}
header("Location: order_details.php?id=$order_id");
exit;
} elseif ($_POST['action'] === 'add_payment') {
$amount = (float)$_POST['amount'];
$method = $_POST['payment_method'];
$stmt = db()->prepare("INSERT INTO payments (order_id, amount, payment_method) VALUES (?, ?, ?)");
$stmt->execute([$order_id, $amount, $method]);
$stmt = db()->prepare("SELECT SUM(amount) as total_paid FROM payments WHERE order_id = ?");
$stmt->execute([$order_id]);
$total_paid = $stmt->fetch()['total_paid'] ?? 0;
$payment_status = $total_paid >= ($order['total_price'] - $order['loyalty_discount']) ? 'paid' : ($total_paid > 0 ? 'partially_paid' : 'unpaid');
$stmt = db()->prepare("UPDATE orders SET payment_status = ? WHERE id = ?");
$stmt->execute([$payment_status, $order_id]);
// WhatsApp Notification for Payment Received
try {
if (get_setting('whatsapp_enabled') === '1' && !empty($order['customer_phone'])) {
$remaining_now = ((float)$order['total_price'] - (float)$order['loyalty_discount']) - (float)$total_paid;
$template = get_setting('msg_payment_ar');
if (!empty($template)) {
$message = str_replace(
['{customer_name}', '{order_number}', '{amount}', '{remaining_balance}'],
[$order['customer_name_ar'], $order['order_number'], $amount, max(0, $remaining_now)],
$template
);
send_whatsapp_message($order['customer_phone'], $message);
}
}
} catch (Exception $e) {}
header("Location: order_details.php?id=$order_id");
exit;
}
}
// NOW Include header
$title = 'order_details';
require_once __DIR__ . '/includes/header.php';
if ($current_role === 'limited_viewer') { header('Location: admin.php'); exit; }
$stmt = db()->prepare("SELECT oi.*, i.name_en as item_en, i.name_ar as item_ar,
s.name_en as service_en, s.name_ar as service_ar
FROM order_items oi
JOIN items i ON oi.item_id = i.id
JOIN services s ON oi.service_id = s.id
WHERE oi.order_id = ?");
$stmt->execute([$order_id]);
$order_items = $stmt->fetchAll();
$stmt = db()->prepare("SELECT * FROM payments WHERE order_id = ?");
$stmt->execute([$order_id]);
$payments = $stmt->fetchAll();
?>
<div class="row">
<div class="col-md-8">
<div class="card p-4 mb-4" style="border-radius: 25px; border: none; shadow: 0 10px 30px rgba(0,0,0,0.05);">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="fw-bold mb-0"><?= __('order') ?> #<?= $order['order_number'] ?></h5>
<div class="d-flex gap-2">
<span class="badge bg-<?= getStatusColor($order['status']) ?> fs-6 px-3 py-2" style="border-radius: 12px;"><?= __($order['status']) ?></span>
<span class="badge badge-soft-<?= getPaymentStatusColor($order['payment_status']) ?> fs-6 px-3 py-2" style="border-radius: 12px;"><?= __($order['payment_status']) ?></span>
</div>
</div>
<div class="table-responsive mb-4">
<table class="table align-middle">
<thead>
<tr>
<th><?= __('item') ?></th>
<th><?= __('service') ?></th>
<th><?= __('quantity') ?></th>
<th class="text-end"><?= __('price') ?></th>
<th class="text-end"><?= __('vat') ?></th>
<th class="text-end"><?= __('subtotal') ?></th>
</tr>
</thead>
<tbody>
<?php
$subtotal_sum = 0;
foreach($order_items as $item):
$subtotal_sum += $item['subtotal'];
?>
<tr>
<td>
<strong><?= $lang === 'ar' ? ($item['item_ar'] ?: $item['item_en']) : $item['item_en'] ?></strong>
</td>
<td><?= $lang === 'ar' ? ($item['service_ar'] ?: $item['service_en']) : $item['service_en'] ?></td>
<td><?= $item['quantity'] ?></td>
<td class="text-end"><?= format_amount($item['unit_price']) ?></td>
<td class="text-end"><?= format_amount($item['vat_amount'] * $item['quantity']) ?></td>
<td class="text-end fw-bold"><?= format_amount($item['subtotal'] + ($item['vat_amount'] * $item['quantity'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr>
<th colspan="5" class="text-end text-muted small"><?= __('subtotal') ?></th>
<th class="text-end"><?= format_amount($subtotal_sum) ?></th>
</tr>
<tr>
<th colspan="5" class="text-end text-muted small"><?= __('vat_total') ?? 'VAT Total' ?></th>
<th class="text-end"><?= format_amount($order['vat_total']) ?></th>
</tr>
<?php if ($order['loyalty_discount'] > 0): ?>
<tr>
<th colspan="5" class="text-end text-success small"><?= __('loyalty_discount') ?? 'Loyalty Discount' ?></th>
<th class="text-end text-success fw-bold">-<?= format_amount($order['loyalty_discount']) ?></th>
</tr>
<?php endif; ?>
<tr>
<th colspan="5" class="text-end fs-5 fw-bold"><?= __('total') ?></th>
<th class="text-end fs-5 fw-bold text-primary"><?= format_amount($order['total_price'] - $order['loyalty_discount']) ?></th>
</tr>
</tfoot>
</table>
</div>
<div class="row mt-4 p-4 bg-light mx-1" style="border-radius: 20px;">
<div class="col-md-6">
<h6 class="fw-bold text-muted small mb-3"><?= __('customer_details') ?></h6>
<p class="mb-1 fw-bold"><?= $lang === 'ar' ? ($order['customer_name_ar'] ?: $order['customer_name_en']) : $order['customer_name_en'] ?></p>
<p class="mb-1"><?= $order['customer_phone'] ?></p>
<p class="mb-0 text-muted small"><?= $lang === 'ar' ? ($order['customer_address_ar'] ?: $order['customer_address_en']) : $order['customer_address_en'] ?></p>
</div>
<div class="col-md-6 text-md-end mt-4 mt-md-0">
<h6 class="fw-bold text-muted small mb-3"><?= __('order_date') ?></h6>
<p class="mb-0 fw-bold"><?= date('d M Y', strtotime($order['created_at'])) ?></p>
<p class="mb-0 text-muted"><?= date('h:i A', strtotime($order['created_at'])) ?></p>
</div>
</div>
</div>
<div class="card p-4 border-0 shadow-sm mb-4" style="border-radius: 25px;">
<h5 class="fw-bold mb-4"><?= __('payments') ?></h5>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr class="text-muted small">
<th><?= __('date') ?></th>
<th><?= __('method') ?></th>
<th class="text-end"><?= __('amount') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($payments as $p): ?>
<tr>
<td><?= date('d M Y, h:i A', strtotime($p['created_at'])) ?></td>
<td><span class="badge bg-light text-dark px-3 py-2 rounded-pill"><?= __($p['payment_method']) ?></span></td>
<td class="text-end fw-bold"><?= format_amount($p['amount']) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($payments)): ?>
<tr>
<td colspan="3" class="text-center text-muted py-4"><?= __('no_payments') ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card p-4 mb-4 border-0 shadow-sm" style="border-radius: 25px;">
<h5 class="fw-bold mb-4"><?= __('actions') ?></h5>
<form method="POST" class="mb-4">
<input type="hidden" name="action" value="update_status">
<label class="form-label small fw-bold text-muted"><?= __('update_status') ?></label>
<div class="input-group shadow-sm" style="border-radius: 12px; overflow: hidden;">
<select name="status" class="form-select border-0 bg-light">
<option value="received" <?= $order['status'] == 'received' ? 'selected' : '' ?>><?= __('received') ?></option>
<option value="processing" <?= $order['status'] == 'processing' ? 'selected' : '' ?>><?= __('processing') ?></option>
<option value="ready" <?= $order['status'] == 'ready' ? 'selected' : '' ?>><?= __('ready') ?></option>
<option value="delivered" <?= $order['status'] == 'delivered' ? 'selected' : '' ?>><?= __('delivered') ?></option>
<option value="cancelled" <?= $order['status'] == 'cancelled' ? 'selected' : '' ?>><?= __('cancelled') ?></option>
</select>
<button type="submit" class="btn btn-primary px-3 fw-bold"><?= __('update') ?></button>
</div>
</form>
<hr class="my-4 opacity-10">
<?php
$stmt = db()->prepare("SELECT SUM(amount) as total_paid FROM payments WHERE order_id = ?");
$stmt->execute([$order_id]);
$total_paid = $stmt->fetch()['total_paid'] ?? 0;
$remaining = ((float)$order['total_price'] - (float)$order['loyalty_discount']) - (float)$total_paid;
?>
<div class="d-flex justify-content-between align-items-center mb-3">
<span class="text-muted fw-bold small"><?= __('remaining_amount') ?></span>
<span class="fs-5 fw-bold <?= $remaining <= 0.001 ? 'text-success' : 'text-danger' ?>">
<?= format_amount(max(0, $remaining)) ?>
</span>
</div>
<?php if ($remaining > 0.001): ?>
<form method="POST">
<input type="hidden" name="action" value="add_payment">
<div class="mb-3">
<label class="form-label small fw-bold text-muted"><?= __('amount') ?></label>
<input type="number" step="0.001" name="amount" class="form-control bg-light border-0" value="<?= round($remaining, 3) ?>" required style="border-radius: 12px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted"><?= __('payment_method') ?></label>
<select name="payment_method" class="form-select bg-light border-0" style="border-radius: 12px;">
<option value="cash"><?= __('cash') ?></option>
<option value="card"><?= __('card') ?></option>
<option value="transfer"><?= __('transfer') ?></option>
</select>
</div>
<button type="submit" class="btn btn-success w-100 py-3 fw-bold shadow-sm" style="border-radius: 15px;"><?= __('add_payment') ?></button>
</form>
<?php endif; ?>
<div class="row mt-4">
<div class="col-6">
<button class="btn btn-outline-dark w-100 py-3 fw-bold shadow-sm" style="border-radius: 15px;" onclick="window.print()">
<i class="bi bi-printer me-2"></i> <?= __('print_invoice') ?>
</button>
</div>
<div class="col-6">
<a href="receipt.php?id=<?= $order_id ?>" target="_blank" class="btn btn-dark w-100 py-3 fw-bold shadow-sm" style="border-radius: 15px;">
<i class="bi bi-receipt me-2"></i> <?= __('thermal_receipt') ?? 'Thermal Receipt' ?>
</a>
</div>
</div>
</div>
</div>
</div>
<?php
if (!function_exists('getStatusColor')) { function getStatusColor($status) {
return [
'received' => 'secondary',
'processing' => 'primary',
'ready' => 'success',
'delivered' => 'dark',
'cancelled' => 'danger',
][$status] ?? 'info';
}}
function getPaymentStatusColor($status) {
return [
'unpaid' => 'danger',
'partially_paid' => 'warning',
'paid' => 'success',
][$status] ?? 'info';
}
require_once __DIR__ . '/includes/footer.php';
?>

339
orders.php Normal file
View File

@ -0,0 +1,339 @@
<?php
$title = 'orders';
require_once __DIR__ . '/includes/header.php';
$current_role = $_SESSION['role'] ?? 'cashier';
$session_branch_id = $_SESSION['branch_id'] ?? 'all';
// Filters
$status_filter = $_GET['status'] ?? '';
$payment_filter = $_GET['payment_status'] ?? '';
// Default to session branch if no explicit GET filter
$branch_filter = $_GET['branch_id'] ?? $session_branch_id;
$from_date = $_GET['from_date'] ?? '';
$to_date = $_GET['to_date'] ?? '';
$search = $_GET['search'] ?? '';
// Pagination
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($page < 1) $page = 1;
$limit = 10;
$offset = ($page - 1) * $limit;
$sql_base = " FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
LEFT JOIN branches b ON o.branch_id = b.id
WHERE 1=1";
$params = [];
// Branch restriction
if ($branch_filter !== 'all') {
$sql_base .= " AND o.branch_id = ?";
$params[] = $branch_filter;
}
if ($status_filter) {
$sql_base .= " AND o.status = ?";
$params[] = $status_filter;
}
if ($payment_filter) {
$sql_base .= " AND o.payment_status = ?";
$params[] = $payment_filter;
}
if ($from_date) {
$sql_base .= " AND DATE(o.created_at) >= ?";
$params[] = $from_date;
}
if ($to_date) {
$sql_base .= " AND DATE(o.created_at) <= ?";
$params[] = $to_date;
}
if ($search) {
$sql_base .= " AND (o.order_number LIKE ? OR c.phone LIKE ? OR c.name_en LIKE ? OR c.name_ar LIKE ?)";
$search_param = "%$search%";
$params[] = $search_param;
$params[] = $search_param;
$params[] = $search_param;
$params[] = $search_param;
}
// Total count for pagination
$count_sql = "SELECT COUNT(*) " . $sql_base;
$stmt_count = db()->prepare($count_sql);
$stmt_count->execute($params);
$total_items = $stmt_count->fetchColumn();
$total_pages = ceil($total_items / $limit);
// Final data query
$sql = "SELECT o.*, c.name_en as customer_name_en, c.name_ar as customer_name_ar, c.phone as customer_phone,
b.name_en as branch_name_en, b.name_ar as branch_name_ar " . $sql_base . " ORDER BY o.created_at DESC LIMIT $limit OFFSET $offset";
$stmt = db()->prepare($sql);
$stmt->execute($params);
$orders = $stmt->fetchAll();
$branches = [];
if ($current_role === 'super_admin') {
$branches = db()->query("SELECT id, name_en, name_ar FROM branches")->fetchAll();
}
?>
<div class="card p-4 border-0 shadow-sm" style="border-radius: 20px;">
<div class="d-flex flex-wrap justify-content-between align-items-center mb-4">
<h5 class="fw-bold mb-3 mb-md-0"><?= __('orders_list') ?? 'Orders List' ?></h5>
<?php if (has_permission('add')): ?>
<a href="pos.php" class="btn btn-primary px-4" style="border-radius: 12px;">
<i class="bi bi-plus-lg me-1"></i> <?= __('new_order') ?>
</a>
<?php endif; ?>
</div>
<!-- Filters -->
<form action="" method="GET" class="row g-3 mb-4">
<div class="col-md-3">
<input type="text" name="search" class="form-control" placeholder="<?= __('search_placeholder') ?>" value="<?= htmlspecialchars($search) ?>" style="border-radius: 12px;">
</div>
<?php if ($current_role === 'super_admin'): ?>
<div class="col-md-2">
<select name="branch_id" class="form-select" style="border-radius: 12px;" onchange="this.form.submit()">
<option value="all"><?= __('all_branches') ?></option>
<?php foreach($branches as $b): ?>
<option value="<?= $b['id'] ?>" <?= $branch_filter == $b['id'] ? 'selected' : '' ?>>
<?= $lang === 'ar' ? ($b['name_ar'] ?: $b['name_en']) : $b['name_en'] ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="col-md-2">
<select name="status" class="form-select" style="border-radius: 12px;" onchange="this.form.submit()">
<option value=""><?= __('all_status') ?></option>
<option value="received" <?= $status_filter === 'received' ? 'selected' : '' ?>><?= __('received') ?></option>
<option value="processing" <?= $status_filter === 'processing' ? 'selected' : '' ?>><?= __('processing') ?></option>
<option value="ready" <?= $status_filter === 'ready' ? 'selected' : '' ?>><?= __('ready') ?></option>
<option value="delivered" <?= $status_filter === 'delivered' ? 'selected' : '' ?>><?= __('delivered') ?></option>
<option value="cancelled" <?= $status_filter === 'cancelled' ? 'selected' : '' ?>><?= __('cancelled') ?></option>
</select>
</div>
<div class="col-md-2">
<select name="payment_status" class="form-select" style="border-radius: 12px;" onchange="this.form.submit()">
<option value=""><?= __('all_payments') ?></option>
<option value="unpaid" <?= $payment_filter === 'unpaid' ? 'selected' : '' ?>><?= __('unpaid') ?></option>
<option value="partially_paid" <?= $payment_filter === 'partially_paid' ? 'selected' : '' ?>><?= __('partially_paid') ?></option>
<option value="paid" <?= $payment_filter === 'paid' ? 'selected' : '' ?>><?= __('paid') ?></option>
</select>
</div>
<div class="col-md-3">
<div class="input-group">
<input type="date" name="from_date" class="form-control" value="<?= $from_date ?>" style="border-radius: 12px 0 0 12px;">
<input type="date" name="to_date" class="form-control" value="<?= $to_date ?>" style="border-radius: 0 12px 12px 0;">
</div>
</div>
</form>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>ID</th>
<th><?= __('order_number') ?></th>
<?php if ($current_role === 'super_admin'): ?>
<th><?= __('branch') ?></th>
<?php endif; ?>
<th><?= __('customer') ?></th>
<th><?= __('subtotal') ?></th>
<th><?= __('loyalty_discount') ?></th>
<th><?= __('total') ?></th>
<th><?= __('status') ?></th>
<th><?= __('payment_status') ?></th>
<th><?= __('date') ?></th>
<th class="text-end"><?= __('actions') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($orders as $order): ?>
<tr>
<td><?= $order['id'] ?></td>
<td class="fw-bold"><?= $order['order_number'] ?></td>
<?php if ($current_role === 'super_admin'): ?>
<td><span class="small text-muted"><?= $lang === 'ar' ? ($order['branch_name_ar'] ?: $order['branch_name_en']) : $order['branch_name_en'] ?></span></td>
<?php endif; ?>
<td>
<div class="fw-bold"><?= $lang === 'ar' ? ($order['customer_name_ar'] ?: $order['customer_name_en']) : $order['customer_name_en'] ?></div>
<div class="small text-muted"><?= $order['customer_phone'] ?></div>
</td>
<td class="small text-muted"><?= format_amount($order['total_price']) ?></td>
<td class="text-success small fw-bold"><?= $order['loyalty_discount'] > 0 ? '-' . format_amount($order['loyalty_discount']) : '-' ?></td>
<td class="fw-bold text-primary"><?= format_amount($order['total_price'] - $order['loyalty_discount']) ?></td>
<td><span class="badge bg-<?= getStatusColor($order['status']) ?> rounded-pill px-3 py-2"><?= __($order['status']) ?></span></td>
<td><span class="badge bg-<?= getPaymentStatusColor($order['payment_status']) ?> rounded-pill px-3 py-2 text-white"><?= __($order['payment_status']) ?></span></td>
<td class="small"><?= date('d/m/Y H:i', strtotime($order['created_at'])) ?></td>
<td class="text-end">
<div class="d-flex gap-1 justify-content-end">
<a href="order_details.php?id=<?= $order['id'] ?>" class="btn btn-sm btn-light border-0 p-2 text-primary" title="<?= __('view') ?? 'View' ?>" style="border-radius: 8px;">
<i class="bi bi-eye-fill"></i>
</a>
<?php if (has_permission('edit')): ?>
<button class="btn btn-sm btn-light border-0 p-2 text-info status-change-btn" data-id="<?= $order['id'] ?>" data-status="<?= $order['status'] ?>" title="<?= __('update_status') ?? 'Update Status' ?>" style="border-radius: 8px;">
<i class="bi bi-arrow-repeat"></i>
</button>
<a href="pos.php?edit_order_id=<?= $order['id'] ?>" class="btn btn-sm btn-light border-0 p-2 text-warning" title="<?= __('edit') ?>" style="border-radius: 8px;">
<i class="bi bi-pencil-fill"></i>
</a>
<?php endif; ?>
<?php if (has_permission('delete')): ?>
<button class="btn btn-sm btn-light border-0 p-2 text-danger delete-order-btn" data-id="<?= $order['id'] ?>" title="<?= __('delete') ?>" style="border-radius: 8px;">
<i class="bi bi-trash-fill"></i>
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($orders)): ?>
<tr>
<td colspan="<?= $current_role === 'super_admin' ? 11 : 10 ?>" class="text-center py-5 text-muted">
<i class="bi bi-inbox fs-1 d-block mb-3 opacity-25"></i>
<?= __('no_orders_found') ?? 'No orders found' ?>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($total_pages > 1): ?>
<nav class="mt-4">
<ul class="pagination justify-content-center">
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
<a class="page-link" href="?<?= http_build_query(array_merge($_GET, ['page' => $page - 1])) ?>" style="border-radius: 8px 0 0 8px;"><?= __('previous') ?></a>
</li>
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
<li class="page-item <?= $page == $i ? 'active' : '' ?>">
<a class="page-link" href="?<?= http_build_query(array_merge($_GET, ['page' => $i])) ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
<li class="page-item <?= $page >= $total_pages ? 'disabled' : '' ?>">
<a class="page-link" href="?<?= http_build_query(array_merge($_GET, ['page' => $page + 1])) ?>" style="border-radius: 0 8px 8px 0;"><?= __('next') ?></a>
</li>
</ul>
</nav>
<?php endif; ?>
</div>
<!-- Status Update Modal -->
<div class="modal fade" id="statusModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 rounded-4 shadow-lg">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold"><?= __('update_status') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<input type="hidden" id="modalOrderId">
<div class="row g-2">
<div class="col-6">
<button class="btn btn-outline-secondary w-100 py-3 rounded-4 status-opt" data-status="received"><?= __('received') ?></button>
</div>
<div class="col-6">
<button class="btn btn-outline-primary w-100 py-3 rounded-4 status-opt" data-status="processing"><?= __('processing') ?></button>
</div>
<div class="col-6">
<button class="btn btn-outline-success w-100 py-3 rounded-4 status-opt" data-status="ready"><?= __('ready') ?></button>
</div>
<div class="col-6">
<button class="btn btn-outline-dark w-100 py-3 rounded-4 status-opt" data-status="delivered"><?= __('delivered') ?></button>
</div>
<div class="col-12 mt-2">
<button class="btn btn-outline-danger w-100 py-3 rounded-4 status-opt" data-status="cancelled"><?= __('cancelled') ?></button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const statusModal = new bootstrap.Modal(document.getElementById('statusModal'));
// Status Change
document.querySelectorAll('.status-change-btn').forEach(btn => {
btn.onclick = () => {
const id = btn.dataset.id;
const currentStatus = btn.dataset.status;
document.getElementById('modalOrderId').value = id;
// Highlight current status
document.querySelectorAll('.status-opt').forEach(opt => {
opt.classList.remove('active');
if (opt.dataset.status === currentStatus) opt.classList.add('active');
});
statusModal.show();
};
});
document.querySelectorAll('.status-opt').forEach(btn => {
btn.onclick = async () => {
const id = document.getElementById('modalOrderId').value;
const status = btn.dataset.status;
try {
const res = await fetch('api/update_order_status.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, status })
});
const data = await res.json();
if (data.success) {
location.reload();
} else {
alert(data.error);
}
} catch (e) {
alert('Error updating status');
}
};
});
// Delete Order
document.querySelectorAll('.delete-order-btn').forEach(btn => {
btn.onclick = async () => {
if (confirm('<?= __('are_you_sure') ?>')) {
const id = btn.dataset.id;
try {
const res = await fetch('api/delete_order.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
const data = await res.json();
if (data.success) {
location.reload();
} else {
alert(data.error);
}
} catch (e) {
alert('Error deleting order');
}
}
};
});
});
</script>
<style>
.status-opt.active {
background-color: var(--bs-primary);
color: white;
border-color: var(--bs-primary);
}
.status-opt:hover {
transform: translateY(-2px);
transition: all 0.2s;
}
</style>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

420
pos.js Normal file
View File

@ -0,0 +1,420 @@
const itemsData = 1;
const lang = '1';
const currencyLabel = '1';
const decimalPrecision = 1;
const editOrderId = 1;
const loyaltyEnabled = 1;
const pointsPerCurrency = 1;
const currencyPerPoint = 1;
let cart = 1;
let selectionModal;
let paymentModal;
let customerLoyaltyPoints = 0;
let pointsToRedeem = 0;
// If not editing, try to load from local storage
if (!editOrderId) {
try {
const saved = localStorage.getItem('pos_cart');
if (saved) {
const parsed = JSON.parse(saved);
if (Array.isArray(parsed) && parsed.length > 0) cart = parsed;
}
} catch (e) { console.error('Cart parse error', e); }
}
document.addEventListener('DOMContentLoaded', () => {
if (typeof bootstrap !== 'undefined') {
const modalEl = document.getElementById('selectionModal');
if (modalEl) selectionModal = new bootstrap.Modal(modalEl);
const payModalEl = document.getElementById('paymentModal');
if (payModalEl) paymentModal = new bootstrap.Modal(payModalEl);
}
// Category filtering
document.querySelectorAll('.cat-filter').forEach(btn => {
btn.onclick = () => {
const cat = btn.getAttribute('data-cat');
document.querySelectorAll('.cat-filter').forEach(b => b.classList.remove('btn-primary'));
document.querySelectorAll('.cat-filter').forEach(b => b.classList.add('btn-white', 'border'));
btn.classList.add('btn-primary');
btn.classList.remove('btn-white', 'border');
document.querySelectorAll('.item-card-wrapper').forEach(card => {
if (cat === 'all' || card.getAttribute('data-cat') === cat) card.style.display = 'block';
else card.style.display = 'none';
});
};
});
// Search
const searchInput = document.getElementById('itemSearch');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
const q = e.target.value.toLowerCase();
document.querySelectorAll('.item-card-wrapper').forEach(card => {
const en = card.getAttribute('data-name-en') || '';
const ar = card.getAttribute('data-name-ar') || '';
if (en.includes(q) || ar.includes(q)) card.style.display = 'block';
else card.style.display = 'none';
});
});
}
// Persistent Customer Search
const custSearchInput = document.getElementById('customerSearchInput');
const custResults = document.getElementById('customerResults');
const clearCustBtn = document.getElementById('clearCustomerBtn');
const custIdInput = document.getElementById('customerId');
// Handle Edit Customer Pre-fill
<?php if ($edit_order && $edit_order['customer_id']): ?>
const initialCustId = "1";
const initialCustBtn = document.querySelector(`.customer-result-item[data-id="${initialCustId}"]`);
if (initialCustBtn) {
selectCustomer(initialCustId, initialCustBtn.getAttribute('data-name'), initialCustBtn.getAttribute('data-points'));
}
<?php endif; ?>
if (custSearchInput) {
custSearchInput.addEventListener('focus', () => {
custResults.classList.remove('d-none');
});
custSearchInput.addEventListener('input', function(e) {
const q = e.target.value.toLowerCase();
custResults.classList.remove('d-none');
document.querySelectorAll('.customer-result-item').forEach(item => {
const search = item.getAttribute('data-search') || '';
if (search.includes(q)) {
item.parentElement.style.display = 'block';
} else {
item.parentElement.style.display = 'none';
}
});
});
document.addEventListener('click', function(e) {
if (!document.getElementById('customerSearchWrapper').contains(e.target)) {
custResults.classList.add('d-none');
}
});
}
if (clearCustBtn) {
clearCustBtn.addEventListener('click', () => {
resetCustomerSelection();
});
}
custResults.addEventListener('click', function(e) {
const btn = e.target.closest('.customer-result-item');
if (btn) {
const id = btn.getAttribute('data-id');
const name = btn.getAttribute('data-name');
const points = btn.getAttribute('data-points') || 0;
selectCustomer(id, name, points);
custResults.classList.add('d-none');
}
});
updateCart();
});
function selectCustomer(id, name, points) {
const custIdInput = document.getElementById('customerId');
const custSearchInput = document.getElementById('customerSearchInput');
const clearCustBtn = document.getElementById('clearCustomerBtn');
const loyaltyDisplay = document.getElementById('loyaltyDisplay');
const loyaltyRedeemUI = document.getElementById('loyaltyRedeemUI');
const customerPointsEl = document.getElementById('customerPoints');
const redeemHint = document.getElementById('redeemHint');
custIdInput.value = id;
customerLoyaltyPoints = parseFloat(points);
if (id) {
custSearchInput.value = name;
clearCustBtn.classList.remove('d-none');
if (loyaltyEnabled) {
loyaltyDisplay.classList.remove('d-none');
loyaltyRedeemUI.classList.remove('d-none');
customerPointsEl.innerText = customerLoyaltyPoints.toFixed(2);
if (redeemHint) {
redeemHint.innerText = lang === 'en'
? `1 Point = ${currencyPerPoint} ${currencyLabel}`
: `1 نقطة = ${currencyPerPoint} ${currencyLabel}`;
}
}
} else {
resetCustomerSelection();
}
}
function resetCustomerSelection() {
const custIdInput = document.getElementById('customerId');
const custSearchInput = document.getElementById('customerSearchInput');
const clearCustBtn = document.getElementById('clearCustomerBtn');
const loyaltyDisplay = document.getElementById('loyaltyDisplay');
const loyaltyRedeemUI = document.getElementById('loyaltyRedeemUI');
custIdInput.value = '';
custSearchInput.value = '';
custSearchInput.placeholder = lang === 'en' ? 'Walk-in Customer' : 'عميل عابر';
clearCustBtn.classList.add('d-none');
loyaltyDisplay.classList.add('d-none');
loyaltyRedeemUI.classList.add('d-none');
customerLoyaltyPoints = 0;
pointsToRedeem = 0;
const ptsInput = document.getElementById('pointsToRedeem');
if (ptsInput) ptsInput.value = '';
updateCart();
}
function applyPoints() {
const ptsInput = document.getElementById('pointsToRedeem');
let val = parseFloat(ptsInput.value) || 0;
if (val > customerLoyaltyPoints) {
alert(lang === 'en' ? 'Insufficient points' : 'نقاط غير كافية');
val = customerLoyaltyPoints;
ptsInput.value = val;
}
pointsToRedeem = val;
updateCart();
}
function showOptions(itemId) {
const item = itemsData[itemId];
if (!item) return;
const itemNameEl = document.getElementById('selectionItemName');
if (itemNameEl) itemNameEl.innerText = lang === 'en' ? item.name_en : item.name_ar;
const list = document.getElementById('optionsList');
if (list) {
list.innerHTML = '';
item.services.forEach(s => {
const col = document.createElement('div');
col.className = 'col-6';
col.innerHTML = `
<button class="btn btn-outline-primary w-100 p-3 rounded-4 border-2 text-center h-100 transition-all" onclick="addToCart(${item.id}, ${s.id})">
<div class="fw-bold mb-1 small">${lang === 'en' ? s.name_en : s.name_ar}</div>
<div class="small opacity-75">${s.price.toFixed(decimalPrecision)} ${currencyLabel}</div>
</button>
`;
list.appendChild(col);
});
}
if (selectionModal) selectionModal.show();
}
function addToCart(itemId, serviceId) {
const item = itemsData[itemId];
if (!item) return;
const service = item.services.find(s => s.id === serviceId);
if (!service) return;
const existing = cart.find(i => i.item_id === itemId && i.service_id === serviceId);
if (existing) {
existing.qty++;
} else {
cart.push({
item_id: itemId,
service_id: serviceId,
name: lang === 'en' ? item.name_en : item.name_ar,
service_name: lang === 'en' ? service.name_en : service.name_ar,
price: service.price,
qty: 1,
vat_percent: item.vat_percent
});
}
if (selectionModal) selectionModal.hide();
updateCart();
}
function changeQty(index, delta) {
cart[index].qty += delta;
if (cart[index].qty <= 0) cart.splice(index, 1);
updateCart();
}
function updateCart() {
if (!editOrderId) {
localStorage.setItem('pos_cart', JSON.stringify(cart));
}
const cartList = document.getElementById('cartItems');
const emptyCart = document.getElementById('emptyCart');
if (!cartList || !emptyCart) return;
if (cart.length === 0) {
cartList.innerHTML = '';
emptyCart.style.display = 'block';
} else {
emptyCart.style.display = 'none';
cartList.innerHTML = cart.map((item, index) => `
<div class="d-flex align-items-center mb-3 bg-light p-2 rounded-3">
<div class="flex-grow-1">
<div class="fw-bold small text-dark">${item.name}</div>
<div class="text-muted" style="font-size: 0.75rem;">${item.service_name}</div>
<div class="fw-bold text-primary">${item.price.toFixed(decimalPrecision)} ${currencyLabel}</div>
</div>
<div class="d-flex align-items-center bg-white rounded-3 p-1">
<button class="btn btn-sm p-0 px-2" onclick="changeQty(${index}, -1)"><i class="bi bi-dash"></i></button>
<span class="mx-2 fw-bold">${item.qty}</span>
<button class="btn btn-sm p-0 px-2" onclick="changeQty(${index}, 1)"><i class="bi bi-plus"></i></button>
</div>
</div>
`).join('');
}
let subtotal = 0;
let totalVat = 0;
cart.forEach(item => {
const itemSubtotal = item.price * item.qty;
subtotal += itemSubtotal;
totalVat += itemSubtotal * ((item.vat_percent || 15) / 100);
});
const subtotalEl = document.getElementById('cartSubtotal');
const vatEl = document.getElementById('cartVat');
const totalEl = document.getElementById('cartTotal');
const discountRow = document.getElementById('loyaltyDiscountRow');
const discountEl = document.getElementById('cartLoyaltyDiscount');
let totalBeforeDiscount = subtotal + totalVat;
let loyaltyDiscount = pointsToRedeem * currencyPerPoint;
if (loyaltyDiscount > totalBeforeDiscount) {
loyaltyDiscount = totalBeforeDiscount;
pointsToRedeem = loyaltyDiscount / currencyPerPoint;
}
if (loyaltyDiscount > 0) {
discountRow.classList.remove('d-none');
discountEl.innerText = '-' + loyaltyDiscount.toFixed(decimalPrecision) + ' ' + currencyLabel;
} else {
discountRow.classList.add('d-none');
}
const finalTotal = totalBeforeDiscount - loyaltyDiscount;
if (subtotalEl) subtotalEl.innerText = subtotal.toFixed(decimalPrecision) + ' ' + currencyLabel;
if (vatEl) vatEl.innerText = totalVat.toFixed(decimalPrecision) + ' ' + currencyLabel;
if (totalEl) totalEl.innerText = finalTotal.toFixed(decimalPrecision) + ' ' + currencyLabel;
}
function clearCart() {
if (confirm(lang === 'en' ? 'Clear cart?' : 'مسح السلة؟')) {
cart = [];
pointsToRedeem = 0;
const ptsInput = document.getElementById('pointsToRedeem');
if (ptsInput) ptsInput.value = '';
updateCart();
}
}
function checkout() {
if (cart.length === 0) return;
let subtotal = 0;
let totalVat = 0;
cart.forEach(item => {
const itemSubtotal = item.price * item.qty;
subtotal += itemSubtotal;
totalVat += itemSubtotal * ((item.vat_percent || 15) / 100);
});
let finalTotal = (subtotal + totalVat) - (pointsToRedeem * currencyPerPoint);
if (finalTotal < 0) finalTotal = 0;
document.getElementById('paymentTotalAmount').innerText = finalTotal.toFixed(decimalPrecision) + ' ' + currencyLabel;
if (paymentModal) paymentModal.show();
}
async function completeCheckout(paymentMethod) {
const cid = document.getElementById('customerId').value;
let subtotal = 0;
let totalVat = 0;
const itemsToSubmit = cart.map(item => {
const itemSubtotal = item.price * item.qty;
const itemVat = itemSubtotal * ((item.vat_percent || 15) / 100);
subtotal += itemSubtotal;
totalVat += itemVat;
return {
itemId: item.item_id,
serviceId: item.service_id,
variantId: null,
quantity: item.qty,
price: item.price,
vatAmount: itemVat
};
});
const totalPrice = subtotal + totalVat;
try {
const response = await fetch('api/checkout.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
order_id: editOrderId,
customer_id: cid,
items: itemsToSubmit,
vat_total: totalVat,
total_price: totalPrice,
payment_method: paymentMethod,
points_to_redeem: pointsToRedeem
})
});
const res = await response.json();
if (res.success) {
const orderId = res.order_id || editOrderId;
if (!editOrderId) { cart = []; updateCart(); localStorage.removeItem("pos_cart"); } window.location.href = "receipt.php?id=" + orderId;
} else alert(res.error);
} catch (e) { alert('Error'); }
}
async function saveCustomer() {
const form = document.getElementById('addCustomerForm');
if (!form) return;
const data = new FormData(form);
try {
const response = await fetch('api/add_customer.php', { method: 'POST', body: data });
const res = await response.json();
if (res.success) {
const id = res.customer.id;
const nameEn = res.customer.name_en;
const nameAr = res.customer.name_ar || nameEn;
const phone = res.customer.phone;
const displayName = lang === 'en' ? nameEn : nameAr;
selectCustomer(id, displayName, 0);
const list = document.getElementById('customerResultsList');
if (list) {
const searchStr = `${nameEn} ${nameAr} ${phone}`.toLowerCase();
const div = document.createElement('div');
div.className = 'p-1';
div.innerHTML = `
<button class="btn btn-white btn-sm w-100 text-start rounded-2 customer-result-item p-2" type="button" data-id="${id}" data-name="${displayName}" data-phone="${phone}" data-search="${searchStr}" data-points="0">
<div class="fw-bold small text-dark">${displayName}</div>
<div class="text-muted small" style="font-size: 0.7rem;">${phone}</div>
</button>
`;
list.prepend(div);
}
if (typeof bootstrap !== 'undefined') {
const modalEl = document.getElementById('addCustomerModal');
if (modalEl) {
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
}
}
form.reset();
} else alert(res.error);
} catch (e) { alert('Error'); }
}

826
pos.php Normal file
View File

@ -0,0 +1,826 @@
<?php
$title = 'pos';
require_once __DIR__ . '/includes/header.php';
// Global check in header.php handles 'view' permission.
$edit_order_id = $_GET['edit_order_id'] ?? null;
if ($edit_order_id && !has_permission('edit')) {
header('Location: pos.php?error=no_edit_permission');
exit;
}
$branch_id = $_SESSION['branch_id'] ?? null;
if ($branch_id === 'all') $branch_id = null;
// Loyalty Settings
$loyalty_enabled = get_setting('loyalty_enabled', '0');
$loyalty_points_per_currency = get_setting('loyalty_points_per_currency', '1');
$loyalty_currency_per_point = get_setting('loyalty_currency_per_point', '0.05');
// Handle Edit Order
$edit_order = null;
$edit_items = [];
if ($edit_order_id) {
$stmt = db()->prepare("SELECT * FROM orders WHERE id = ?");
if ($branch_id) {
$stmt = db()->prepare("SELECT * FROM orders WHERE id = ? AND branch_id = ?");
$stmt->execute([$edit_order_id, $branch_id]);
} else {
$stmt->execute([$edit_order_id]);
}
$edit_order = $stmt->fetch();
if ($edit_order) {
$stmt = db()->prepare("SELECT oi.*, i.name_en, i.name_ar, i.vat_percent, s.name_en as service_en, s.name_ar as service_ar
FROM order_items oi
JOIN items i ON oi.item_id = i.id
JOIN services s ON oi.service_id = s.id
WHERE oi.order_id = ?");
$stmt->execute([$edit_order_id]);
$edit_items_raw = $stmt->fetchAll();
foreach ($edit_items_raw as $ei) {
$edit_items[] = [
'item_id' => $ei['item_id'],
'service_id' => $ei['service_id'],
'name' => $lang === 'en' ? $ei['name_en'] : $ei['name_ar'],
'service_name' => $lang === 'en' ? $ei['service_en'] : $ei['service_ar'],
'price' => (float)$ei['unit_price'],
'qty' => (int)$ei['quantity'],
'vat_percent' => (float)$ei['vat_percent']
];
}
}
}
// Get all categories
$categories = db()->query("SELECT * FROM categories WHERE is_deleted = 0 ORDER BY name_en ASC")->fetchAll();
// Get all items with details
$stmt = db()->prepare("SELECT i.*, c.name_en as cat_en, c.name_ar as cat_ar
FROM items i
LEFT JOIN categories c ON i.category_id = c.id
WHERE i.is_deleted = 0
ORDER BY i.name_en ASC");
$stmt->execute();
$items_raw = $stmt->fetchAll();
// Get all services
$services_raw = db()->query("SELECT * FROM services WHERE is_deleted = 0 ORDER BY name_en ASC")->fetchAll();
// Get all prices (globally shared)
$stmt = db()->prepare("SELECT * FROM prices");
$stmt->execute();
$prices_raw = $stmt->fetchAll();
$prices = [];
foreach ($prices_raw as $p) {
$prices[$p['item_id']][$p['service_id']] = (float)$p['price'];
}
$items = [];
foreach ($items_raw as $i) {
$item_services = [];
foreach ($services_raw as $s) {
if (isset($prices[$i['id']][$s['id']])) {
$item_services[] = [
'id' => $s['id'],
'name_en' => $s['name_en'],
'name_ar' => $s['name_ar'],
'price' => $prices[$i['id']][$s['id']]
];
}
}
if (!empty($item_services)) {
$items[$i['id']] = [
'id' => $i['id'],
'name_en' => $i['name_en'],
'name_ar' => $i['name_ar'],
'category_id' => $i['category_id'],
'image_url' => $i['image_url'],
'vat_percent' => (float)($i['vat_percent'] ?? 15),
'services' => $item_services
];
}
}
// Get all customers
$stmt = db()->prepare("SELECT id, name_en, name_ar, phone, loyalty_points FROM customers ORDER BY name_en ASC");
$stmt->execute();
$customers = $stmt->fetchAll();
$pageTitle = $edit_order ? ($lang == 'en' ? 'Edit Order #' . $edit_order['order_number'] : 'تعديل طلب رقم ' . $edit_order['order_number']) : ($lang == 'en' ? 'Point of Sale' : 'نقطة البيع');
?>
<div class="container-fluid py-4">
<div class="row g-4">
<!-- Left: Items & Categories -->
<div class="col-lg-8">
<div class="mb-4 d-flex align-items-center justify-content-between">
<h4 class="fw-bold mb-0"><?= $pageTitle ?></h4>
<div class="input-group w-50 shadow-sm rounded-4 overflow-hidden">
<span class="input-group-text bg-white border-0"><i class="bi bi-search"></i></span>
<input type="text" id="itemSearch" class="form-control border-0 py-2 shadow-none" placeholder="<?= $lang == 'en' ? 'Search items...' : 'بحث عن المنتجات...' ?>">
</div>
</div>
<!-- Categories Scroller -->
<div class="mb-4 overflow-auto d-flex pb-2 hide-scrollbar" style="white-space: nowrap;">
<button class="btn btn-primary rounded-pill px-4 cat-filter me-2 shadow-sm" data-cat="all">
<?= $lang == 'en' ? 'All' : 'الكل' ?>
</button>
<?php foreach($categories as $cat): ?>
<button class="btn btn-white border rounded-pill px-4 cat-filter me-2 shadow-sm" data-cat="<?= $cat['id'] ?>">
<?= $lang == 'en' ? $cat['name_en'] : $cat['name_ar'] ?>
</button>
<?php endforeach; ?>
</div>
<div class="row g-3" id="itemsList">
<?php foreach($items as $item): ?>
<div class="col-6 col-md-4 col-xl-3 item-card-wrapper"
data-cat="<?= $item['category_id'] ?>"
data-name-en="<?= strtolower($item['name_en']) ?>"
data-name-ar="<?= $item['name_ar'] ?>">
<div class="card h-100 border-0 shadow-sm rounded-4 item-card pointer overflow-hidden transition-all" onclick="showOptions(<?= $item['id'] ?>)">
<div class="position-relative bg-light" style="height: 160px; border-radius: 1rem 1rem 0 0; overflow: hidden;">
<?php if($item['image_url']): ?>
<img src="<?= $item['image_url'] ?>?v=<?= time() ?>" class="w-100 h-100" style="object-fit: contain; padding: 10px;">
<?php else: ?>
<div class="w-100 h-100 d-flex align-items-center justify-content-center opacity-25">
<i class="bi bi-box" style="font-size: 3rem;"></i>
</div>
<?php endif; ?>
<div class="position-absolute top-0 end-0 m-2">
<span class="badge bg-white text-dark shadow-sm rounded-pill px-2 py-1 small" style="font-size: 0.65rem;">
<?= $item['vat_percent'] ?>% VAT
</span>
</div>
</div>
<div class="card-body p-3 text-center">
<h6 class="card-title mb-1 text-truncate fw-bold small"><?= $lang == 'en' ? $item['name_en'] : $item['name_ar'] ?></h6>
<p class="small text-muted mb-0" style="font-size: 0.75rem;"><?= count($item['services']) ?> <?= $lang == 'en' ? 'Services' : 'خدمات' ?></p>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Right: Cart -->
<div class="col-lg-4">
<div class="card border-0 shadow-sm rounded-4 h-100 d-flex flex-column" style="min-height: 80vh;">
<div class="card-header bg-white py-3 border-0">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-bold"><?= $lang == 'en' ? 'Current Order' : 'الطلب الحالي' ?></h5>
<button class="btn btn-sm btn-outline-danger rounded-3 border-0" onclick="clearCart()">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div class="px-3 pb-3">
<div class="d-flex gap-2 mb-2">
<!-- Persistent Customer Search -->
<div class="position-relative flex-grow-1" id="customerSearchWrapper">
<div class="input-group shadow-sm rounded-3 overflow-hidden border">
<span class="input-group-text bg-white border-0"><i class="bi bi-person"></i></span>
<input type="text" class="form-control border-0 py-2 shadow-none" id="customerSearchInput" placeholder="<?= $lang == 'en' ? 'Walk-in Customer' : 'عميل عابر' ?>" autocomplete="off">
<button class="btn btn-white border-0 d-none" type="button" id="clearCustomerBtn">
<i class="bi bi-x-circle-fill text-muted"></i>
</button>
</div>
<div class="position-absolute w-100 shadow-lg bg-white rounded-3 mt-1 d-none" id="customerResults" style="z-index: 1050; max-height: 300px; overflow-y: auto; border: 1px solid #eee;">
<div class="p-2 border-bottom">
<button class="btn btn-light btn-sm w-100 text-start rounded-2 customer-result-item" data-id="" data-name="<?= $lang == 'en' ? 'Walk-in Customer' : 'عميل عابر' ?>" data-search="walk-in" data-points="0">
<?= $lang == 'en' ? 'Walk-in Customer' : 'عميل عابر' ?>
</button>
</div>
<div id="customerResultsList">
<?php foreach($customers as $c):
$cname = ($lang == 'en' ? $c['name_en'] : $c['name_ar']) ?: $c['name_en'];
$searchStr = strtolower($c['name_en'] . ' ' . ($c['name_ar'] ?? '') . ' ' . $c['phone']);
?>
<div class="p-1">
<button class="btn btn-white btn-sm w-100 text-start rounded-2 customer-result-item p-2" type="button" data-id="<?= $c['id'] ?>" data-name="<?= $cname ?>" data-phone="<?= $c['phone'] ?>" data-search="<?= $searchStr ?>" data-points="<?= $c['loyalty_points'] ?>">
<div class="fw-bold small text-dark"><?= $cname ?></div>
<div class="text-muted small" style="font-size: 0.7rem;"><?= $c['phone'] ?></div>
</button>
</div>
<?php endforeach; ?>
</div>
</div>
<input type="hidden" id="customerId" value="<?= $edit_order ? $edit_order['customer_id'] : '' ?>">
</div>
<?php if (has_permission('add', 'customers.php')): ?>
<button class="btn btn-primary rounded-3" data-bs-toggle="modal" data-bs-target="#addCustomerModal">
<i class="bi bi-plus-lg"></i>
</button>
<?php endif; ?>
</div>
<!-- Loyalty Info Display -->
<div id="loyaltyDisplay" class="d-none bg-primary bg-opacity-10 rounded-3 p-2 mb-2 d-flex align-items-center justify-content-between border border-primary border-opacity-25">
<div class="d-flex align-items-center">
<i class="bi bi-star-fill text-primary me-2"></i>
<span class="small fw-bold text-primary"><?= $lang == 'en' ? 'Loyalty Points' : 'نقاط الولاء' ?></span>
</div>
<span class="badge bg-primary rounded-pill" id="customerPoints">0</span>
</div>
</div>
<div class="card-body p-0 flex-grow-1 overflow-auto" id="cartContainer">
<div id="cartItems" class="p-3"></div>
<div id="emptyCart" class="text-center py-5 opacity-50">
<i class="bi bi-cart3 display-1 d-block mb-3"></i>
<p><?= $lang == 'en' ? 'Your cart is empty' : 'السلة فارغة' ?></p>
</div>
</div>
<div class="card-footer bg-white border-top-0 p-3">
<div class="bg-light rounded-4 p-3 mb-3">
<div class="d-flex justify-content-between mb-2 small text-muted">
<span><?= $lang == 'en' ? 'Subtotal' : 'المجموع الفرعي' ?></span>
<span id="cartSubtotal">0.000 <?= currency() ?></span>
</div>
<div class="d-flex justify-content-between mb-2 small text-muted">
<span><?= $lang == 'en' ? 'VAT' : 'الضريبة' ?></span>
<span id="cartVat">0.000 <?= currency() ?></span>
</div>
<!-- Loyalty Discount Display -->
<div id="loyaltyDiscountRow" class="d-none d-flex justify-content-between mb-2 small text-success fw-bold">
<span><?= $lang == 'en' ? 'Loyalty Discount' : 'خصم الولاء' ?></span>
<span id="cartLoyaltyDiscount">-0.000 <?= currency() ?></span>
</div>
<div class="d-flex justify-content-between pt-2 border-top border-secondary border-opacity-10 mt-2">
<h5 class="mb-0 fw-bold"><?= $lang == 'en' ? 'Total' : 'الإجمالي' ?></h5>
<h5 id="cartTotal" class="mb-0 fw-bold text-primary">0.000 <?= currency() ?></h5>
</div>
</div>
<!-- Loyalty Redemption UI -->
<?php if ($loyalty_enabled == '1'): ?>
<div id="loyaltyRedeemUI" class="d-none mb-3">
<div class="input-group shadow-sm rounded-3 overflow-hidden border">
<span class="input-group-text bg-white border-0 small"><i class="bi bi-star text-primary"></i></span>
<input type="number" id="pointsToRedeem" class="form-control border-0 py-2 shadow-none small" placeholder="<?= $lang == 'en' ? 'Redeem Points' : 'استبدال النقاط' ?>" min="0">
<button class="btn btn-primary btn-sm px-3" onclick="applyPoints()">
<?= $lang == 'en' ? 'Apply' : 'تطبيق' ?>
</button>
</div>
<div id="redeemHint" class="small text-muted mt-1" style="font-size: 0.7rem;"></div>
</div>
<?php endif; ?>
<?php if (($edit_order && has_permission('edit')) || (!$edit_order && has_permission('add'))): ?>
<button class="btn btn-primary w-100 py-3 rounded-4 fw-bold shadow-sm" onclick="checkout()">
<i class="bi bi-credit-card me-2"></i> <?= $edit_order ? ($lang == 'en' ? 'Update Order' : 'تحديث الطلب') : ($lang == 'en' ? 'Complete Order' : 'إتمام الطلب') ?>
</button>
<?php else: ?>
<button class="btn btn-secondary w-100 py-3 rounded-4 fw-bold shadow-sm" disabled>
<i class="bi bi-lock me-2"></i> <?= $lang == 'en' ? 'No Permission' : 'لا تملك صلاحية' ?>
</button>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<!-- Selection Modal -->
<div class="modal fade" id="selectionModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 rounded-4 shadow-lg">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold" id="selectionItemName"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<p class="text-muted small mb-3"><?= $lang == 'en' ? 'Select service type:' : 'اختر نوع الخدمة:' ?></p>
<div class="row g-2" id="optionsList"></div>
</div>
</div>
</div>
</div>
<!-- Add Customer Modal -->
<div class="modal fade" id="addCustomerModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 rounded-4 shadow-lg">
<div class="modal-header border-0 pb-0 p-4">
<h5 class="modal-title fw-bold"><?= $lang == 'en' ? 'Add New Customer' : 'إضافة عميل جديد' ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<form id="addCustomerForm">
<div class="mb-3">
<label class="form-label small"><?= $lang == 'en' ? 'Name (English)' : 'الاسم (إنجليزي)' ?></label>
<input type="text" name="name_en" class="form-control rounded-3" required>
</div>
<div class="mb-3">
<label class="form-label small"><?= $lang == 'en' ? 'Name (Arabic)' : 'الاسم (عربي)' ?></label>
<input type="text" name="name_ar" class="form-control rounded-3">
</div>
<div class="mb-3">
<label class="form-label small"><?= $lang == 'en' ? 'Phone Number' : 'رقم الجوال' ?></label>
<input type="tel" name="phone" class="form-control rounded-3" required>
</div>
<div class="mb-3">
<label class="form-label small"><?= $lang == 'en' ? 'Email (Optional)' : 'البريد (اختياري)' ?></label>
<input type="email" name="email" class="form-control rounded-3">
</div>
<button type="button" class="btn btn-primary w-100 py-2 rounded-3" onclick="saveCustomer()">
<?= $lang == 'en' ? 'Save Customer' : 'حفظ العميل' ?>
</button>
</form>
</div>
</div>
</div>
</div>
<!-- Payment Modal -->
<div class="modal fade" id="paymentModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 rounded-4 shadow-lg">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold"><?= $lang == 'en' ? 'Payment' : 'الدفع' ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<div class="text-center mb-4">
<h3 class="fw-bold text-primary mb-1" id="paymentTotalAmount">0.000</h3>
<div class="text-muted small"><?= $lang == 'en' ? 'Total Amount' : 'المبلغ الإجمالي' ?></div>
</div>
<div class="row g-3">
<div class="col-6">
<button class="btn btn-outline-primary w-100 py-3 rounded-3 d-flex flex-column align-items-center" onclick="completeCheckout('cash')">
<i class="bi bi-cash-stack mb-1 fs-4"></i>
<span class="small fw-bold"><?= $lang == 'en' ? 'Cash' : 'نقداً' ?></span>
</button>
</div>
<div class="col-6">
<button class="btn btn-outline-primary w-100 py-3 rounded-3 d-flex flex-column align-items-center" onclick="completeCheckout('card')">
<i class="bi bi-credit-card mb-1 fs-4"></i>
<span class="small fw-bold"><?= $lang == 'en' ? 'Card' : 'بطاقة' ?></span>
</button>
</div>
<div class="col-6">
<button class="btn btn-outline-primary w-100 py-3 rounded-3 d-flex flex-column align-items-center" onclick="completeCheckout('transfer')">
<i class="bi bi-arrow-left-right mb-1 fs-4"></i>
<span class="small fw-bold"><?= $lang == 'en' ? 'Transfer' : 'تحويل' ?></span>
</button>
</div>
<div class="col-6">
<button class="btn btn-outline-secondary w-100 py-3 rounded-3 d-flex flex-column align-items-center" onclick="completeCheckout('pay_later')">
<i class="bi bi-clock-history mb-1 fs-4"></i>
<span class="small fw-bold"><?= $lang == 'en' ? 'Pay Later' : 'الدفع لاحقاً' ?></span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Hidden Iframe for Printing -->
<script>
const itemsData = <?= json_encode((object)$items) ?>;
const lang = '<?= $lang ?>';
const currencyLabel = '<?= currency() ?>';
const decimalPrecision = <?= decimals() ?>;
const editOrderId = <?= $edit_order_id ?: 'null' ?>;
const loyaltyEnabled = <?= $loyalty_enabled ?>;
const pointsPerCurrency = <?= $loyalty_points_per_currency ?>;
const currencyPerPoint = <?= $loyalty_currency_per_point ?>;
let cart = <?= json_encode($edit_items) ?>;
let selectionModal;
let paymentModal;
let customerLoyaltyPoints = 0;
let pointsToRedeem = 0;
// If not editing, try to load from local storage
if (!editOrderId) {
try {
const saved = localStorage.getItem('pos_cart');
if (saved) {
const parsed = JSON.parse(saved);
if (Array.isArray(parsed) && parsed.length > 0) cart = parsed;
}
} catch (e) { console.error('Cart parse error', e); }
}
document.addEventListener('DOMContentLoaded', () => {
if (typeof bootstrap !== 'undefined') {
const modalEl = document.getElementById('selectionModal');
if (modalEl) selectionModal = new bootstrap.Modal(modalEl);
const payModalEl = document.getElementById('paymentModal');
if (payModalEl) paymentModal = new bootstrap.Modal(payModalEl);
}
// Category filtering
document.querySelectorAll('.cat-filter').forEach(btn => {
btn.onclick = () => {
const cat = btn.getAttribute('data-cat');
document.querySelectorAll('.cat-filter').forEach(b => b.classList.remove('btn-primary'));
document.querySelectorAll('.cat-filter').forEach(b => b.classList.add('btn-white', 'border'));
btn.classList.add('btn-primary');
btn.classList.remove('btn-white', 'border');
document.querySelectorAll('.item-card-wrapper').forEach(card => {
if (cat === 'all' || card.getAttribute('data-cat') === cat) card.style.display = 'block';
else card.style.display = 'none';
});
};
});
// Search
const searchInput = document.getElementById('itemSearch');
if (searchInput) {
searchInput.addEventListener('input', function(e) {
const q = e.target.value.toLowerCase();
document.querySelectorAll('.item-card-wrapper').forEach(card => {
const en = card.getAttribute('data-name-en') || '';
const ar = card.getAttribute('data-name-ar') || '';
if (en.includes(q) || ar.includes(q)) card.style.display = 'block';
else card.style.display = 'none';
});
});
}
// Persistent Customer Search
const custSearchInput = document.getElementById('customerSearchInput');
const custResults = document.getElementById('customerResults');
const clearCustBtn = document.getElementById('clearCustomerBtn');
const custIdInput = document.getElementById('customerId');
// Handle Edit Customer Pre-fill
<?php if ($edit_order && $edit_order['customer_id']): ?>
const initialCustId = "<?= $edit_order['customer_id'] ?>";
const initialCustBtn = document.querySelector(`.customer-result-item[data-id="${initialCustId}"]`);
if (initialCustBtn) {
selectCustomer(initialCustId, initialCustBtn.getAttribute('data-name'), initialCustBtn.getAttribute('data-points'));
}
<?php endif; ?>
if (custSearchInput) {
custSearchInput.addEventListener('focus', () => {
custResults.classList.remove('d-none');
});
custSearchInput.addEventListener('input', function(e) {
const q = e.target.value.toLowerCase();
custResults.classList.remove('d-none');
document.querySelectorAll('.customer-result-item').forEach(item => {
const search = item.getAttribute('data-search') || '';
if (search.includes(q)) {
item.parentElement.style.display = 'block';
} else {
item.parentElement.style.display = 'none';
}
});
});
document.addEventListener('click', function(e) {
if (!document.getElementById('customerSearchWrapper').contains(e.target)) {
custResults.classList.add('d-none');
}
});
}
if (clearCustBtn) {
clearCustBtn.addEventListener('click', () => {
resetCustomerSelection();
});
}
custResults.addEventListener('click', function(e) {
const btn = e.target.closest('.customer-result-item');
if (btn) {
const id = btn.getAttribute('data-id');
const name = btn.getAttribute('data-name');
const points = btn.getAttribute('data-points') || 0;
selectCustomer(id, name, points);
custResults.classList.add('d-none');
}
});
updateCart();
});
function selectCustomer(id, name, points) {
const custIdInput = document.getElementById('customerId');
const custSearchInput = document.getElementById('customerSearchInput');
const clearCustBtn = document.getElementById('clearCustomerBtn');
const loyaltyDisplay = document.getElementById('loyaltyDisplay');
const loyaltyRedeemUI = document.getElementById('loyaltyRedeemUI');
const customerPointsEl = document.getElementById('customerPoints');
const redeemHint = document.getElementById('redeemHint');
custIdInput.value = id;
customerLoyaltyPoints = parseFloat(points);
if (id) {
custSearchInput.value = name;
clearCustBtn.classList.remove('d-none');
if (loyaltyEnabled) {
loyaltyDisplay.classList.remove('d-none');
loyaltyRedeemUI.classList.remove('d-none');
customerPointsEl.innerText = customerLoyaltyPoints.toFixed(2);
if (redeemHint) {
redeemHint.innerText = lang === 'en'
? `1 Point = ${currencyPerPoint} ${currencyLabel}`
: `1 نقطة = ${currencyPerPoint} ${currencyLabel}`;
}
}
} else {
resetCustomerSelection();
}
}
function resetCustomerSelection() {
const custIdInput = document.getElementById('customerId');
const custSearchInput = document.getElementById('customerSearchInput');
const clearCustBtn = document.getElementById('clearCustomerBtn');
const loyaltyDisplay = document.getElementById('loyaltyDisplay');
const loyaltyRedeemUI = document.getElementById('loyaltyRedeemUI');
custIdInput.value = '';
custSearchInput.value = '';
custSearchInput.placeholder = lang === 'en' ? 'Walk-in Customer' : 'عميل عابر';
clearCustBtn.classList.add('d-none');
loyaltyDisplay.classList.add('d-none');
loyaltyRedeemUI.classList.add('d-none');
customerLoyaltyPoints = 0;
pointsToRedeem = 0;
const ptsInput = document.getElementById('pointsToRedeem');
if (ptsInput) ptsInput.value = '';
updateCart();
}
function applyPoints() {
const ptsInput = document.getElementById('pointsToRedeem');
let val = parseFloat(ptsInput.value) || 0;
if (val > customerLoyaltyPoints) {
alert(lang === 'en' ? 'Insufficient points' : 'نقاط غير كافية');
val = customerLoyaltyPoints;
ptsInput.value = val;
}
pointsToRedeem = val;
updateCart();
}
function showOptions(itemId) {
const item = itemsData[itemId];
if (!item) return;
const itemNameEl = document.getElementById('selectionItemName');
if (itemNameEl) itemNameEl.innerText = lang === 'en' ? item.name_en : item.name_ar;
const list = document.getElementById('optionsList');
if (list) {
list.innerHTML = '';
item.services.forEach(s => {
const col = document.createElement('div');
col.className = 'col-6';
col.innerHTML = `
<button class="btn btn-outline-primary w-100 p-3 rounded-4 border-2 text-center h-100 transition-all" onclick="addToCart(${item.id}, ${s.id})">
<div class="fw-bold mb-1 small">${lang === 'en' ? s.name_en : s.name_ar}</div>
<div class="small opacity-75">${s.price.toFixed(decimalPrecision)} ${currencyLabel}</div>
</button>
`;
list.appendChild(col);
});
}
if (selectionModal) selectionModal.show();
}
function addToCart(itemId, serviceId) {
const item = itemsData[itemId];
if (!item) return;
const service = item.services.find(s => s.id === serviceId);
if (!service) return;
const existing = cart.find(i => i.item_id === itemId && i.service_id === serviceId);
if (existing) {
existing.qty++;
} else {
cart.push({
item_id: itemId,
service_id: serviceId,
name: lang === 'en' ? item.name_en : item.name_ar,
service_name: lang === 'en' ? service.name_en : service.name_ar,
price: service.price,
qty: 1,
vat_percent: item.vat_percent
});
}
if (selectionModal) selectionModal.hide();
updateCart();
}
function changeQty(index, delta) {
cart[index].qty += delta;
if (cart[index].qty <= 0) cart.splice(index, 1);
updateCart();
}
function updateCart() {
if (!editOrderId) {
localStorage.setItem('pos_cart', JSON.stringify(cart));
}
const cartList = document.getElementById('cartItems');
const emptyCart = document.getElementById('emptyCart');
if (!cartList || !emptyCart) return;
if (cart.length === 0) {
cartList.innerHTML = '';
emptyCart.style.display = 'block';
} else {
emptyCart.style.display = 'none';
cartList.innerHTML = cart.map((item, index) => `
<div class="d-flex align-items-center mb-3 bg-light p-2 rounded-3">
<div class="flex-grow-1">
<div class="fw-bold small text-dark">${item.name}</div>
<div class="text-muted" style="font-size: 0.75rem;">${item.service_name}</div>
<div class="fw-bold text-primary">${item.price.toFixed(decimalPrecision)} ${currencyLabel}</div>
</div>
<div class="d-flex align-items-center bg-white rounded-3 p-1">
<button class="btn btn-sm p-0 px-2" onclick="changeQty(${index}, -1)"><i class="bi bi-dash"></i></button>
<span class="mx-2 fw-bold">${item.qty}</span>
<button class="btn btn-sm p-0 px-2" onclick="changeQty(${index}, 1)"><i class="bi bi-plus"></i></button>
</div>
</div>
`).join('');
}
let subtotal = 0;
let totalVat = 0;
cart.forEach(item => {
const itemSubtotal = item.price * item.qty;
subtotal += itemSubtotal;
totalVat += itemSubtotal * ((item.vat_percent || 15) / 100);
});
const subtotalEl = document.getElementById('cartSubtotal');
const vatEl = document.getElementById('cartVat');
const totalEl = document.getElementById('cartTotal');
const discountRow = document.getElementById('loyaltyDiscountRow');
const discountEl = document.getElementById('cartLoyaltyDiscount');
let totalBeforeDiscount = subtotal + totalVat;
let loyaltyDiscount = pointsToRedeem * currencyPerPoint;
if (loyaltyDiscount > totalBeforeDiscount) {
loyaltyDiscount = totalBeforeDiscount;
pointsToRedeem = loyaltyDiscount / currencyPerPoint;
}
if (loyaltyDiscount > 0) {
discountRow.classList.remove('d-none');
discountEl.innerText = '-' + loyaltyDiscount.toFixed(decimalPrecision) + ' ' + currencyLabel;
} else {
discountRow.classList.add('d-none');
}
const finalTotal = totalBeforeDiscount - loyaltyDiscount;
if (subtotalEl) subtotalEl.innerText = subtotal.toFixed(decimalPrecision) + ' ' + currencyLabel;
if (vatEl) vatEl.innerText = totalVat.toFixed(decimalPrecision) + ' ' + currencyLabel;
if (totalEl) totalEl.innerText = finalTotal.toFixed(decimalPrecision) + ' ' + currencyLabel;
}
function clearCart() {
if (confirm(lang === 'en' ? 'Clear cart?' : 'مسح السلة؟')) {
cart = [];
pointsToRedeem = 0;
const ptsInput = document.getElementById('pointsToRedeem');
if (ptsInput) ptsInput.value = '';
updateCart();
}
}
function checkout() {
if (cart.length === 0) return;
let subtotal = 0;
let totalVat = 0;
cart.forEach(item => {
const itemSubtotal = item.price * item.qty;
subtotal += itemSubtotal;
totalVat += itemSubtotal * ((item.vat_percent || 15) / 100);
});
let finalTotal = (subtotal + totalVat) - (pointsToRedeem * currencyPerPoint);
if (finalTotal < 0) finalTotal = 0;
document.getElementById('paymentTotalAmount').innerText = finalTotal.toFixed(decimalPrecision) + ' ' + currencyLabel;
if (paymentModal) paymentModal.show();
}
async function completeCheckout(paymentMethod) {
const cid = document.getElementById('customerId').value;
let subtotal = 0;
let totalVat = 0;
const itemsToSubmit = cart.map(item => {
const itemSubtotal = item.price * item.qty;
const itemVat = itemSubtotal * ((item.vat_percent || 15) / 100);
subtotal += itemSubtotal;
totalVat += itemVat;
return {
itemId: item.item_id,
serviceId: item.service_id,
variantId: null,
quantity: item.qty,
price: item.price,
vatAmount: itemVat
};
});
const totalPrice = subtotal + totalVat;
try {
const response = await fetch('api/checkout.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
order_id: editOrderId,
customer_id: cid,
items: itemsToSubmit,
vat_total: totalVat,
total_price: totalPrice,
payment_method: paymentMethod,
points_to_redeem: pointsToRedeem
})
});
const res = await response.json();
if (res.success) {
const orderId = res.order_id || editOrderId;
if (!editOrderId) { cart = []; updateCart(); localStorage.removeItem("pos_cart"); } window.location.href = "receipt.php?id=" + orderId;
} else alert(res.error);
} catch (e) { alert('Error'); }
}
async function saveCustomer() {
const form = document.getElementById('addCustomerForm');
if (!form) return;
const data = new FormData(form);
try {
const response = await fetch('api/add_customer.php', { method: 'POST', body: data });
const res = await response.json();
if (res.success) {
const id = res.customer.id;
const nameEn = res.customer.name_en;
const nameAr = res.customer.name_ar || nameEn;
const phone = res.customer.phone;
const displayName = lang === 'en' ? nameEn : nameAr;
selectCustomer(id, displayName, 0);
const list = document.getElementById('customerResultsList');
if (list) {
const searchStr = `${nameEn} ${nameAr} ${phone}`.toLowerCase();
const div = document.createElement('div');
div.className = 'p-1';
div.innerHTML = `
<button class="btn btn-white btn-sm w-100 text-start rounded-2 customer-result-item p-2" type="button" data-id="${id}" data-name="${displayName}" data-phone="${phone}" data-search="${searchStr}" data-points="0">
<div class="fw-bold small text-dark">${displayName}</div>
<div class="text-muted small" style="font-size: 0.7rem;">${phone}</div>
</button>
`;
list.prepend(div);
}
if (typeof bootstrap !== 'undefined') {
const modalEl = document.getElementById('addCustomerModal');
if (modalEl) {
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
}
}
form.reset();
} else alert(res.error);
} catch (e) { alert('Error'); }
}
</script>
<style>
.pointer { cursor: pointer; }
.item-card:hover { transform: translateY(-5px); }
.transition-all { transition: all 0.3s ease; }
.hide-scrollbar::-webkit-scrollbar { display: none; }
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.btn-white { background: white; }
.customer-result-item:hover { background-color: #f8f9fa; }
</style>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

242
profile.php Normal file
View File

@ -0,0 +1,242 @@
<?php
$title = 'user_profile';
require_once __DIR__ . '/includes/header.php';
$success = '';
$error = '';
// Get user data
$stmt = db()->prepare("SELECT u.*, b.name_en as branch_name_en, b.name_ar as branch_name_ar
FROM users u
LEFT JOIN branches b ON u.branch_id = b.id
WHERE u.id = ?");
$stmt->execute([$current_user_id]);
$user = $stmt->fetch();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$full_name_en = $_POST['full_name_en'] ?? '';
$full_name_ar = $_POST['full_name_ar'] ?? '';
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
// Handle Profile Picture Upload
$profile_picture = $user['profile_picture'];
if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] === UPLOAD_ERR_OK) {
$ext = pathinfo($_FILES['profile_picture']['name'], PATHINFO_EXTENSION);
$filename = 'user_' . $current_user_id . '_' . time() . '.' . $ext;
$target = 'assets/images/users/' . $filename;
if (!is_dir('assets/images/users/')) {
mkdir('assets/images/users/', 0775, true);
}
if (move_uploaded_file($_FILES['profile_picture']['tmp_name'], $target)) {
$profile_picture = $target;
}
}
if ($password !== '' && $password !== $confirm_password) {
$error = 'Passwords do not match';
} else {
try {
if ($password !== '') {
$password_hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = db()->prepare("UPDATE users SET full_name_en = ?, full_name_ar = ?, email = ?, profile_picture = ?, password_hash = ? WHERE id = ?");
$stmt->execute([$full_name_en, $full_name_ar, $email, $profile_picture, $password_hash, $current_user_id]);
} else {
$stmt = db()->prepare("UPDATE users SET full_name_en = ?, full_name_ar = ?, email = ?, profile_picture = ? WHERE id = ?");
$stmt->execute([$full_name_en, $full_name_ar, $email, $profile_picture, $current_user_id]);
}
// Update Session
$_SESSION['full_name'] = is_arabic() ? ($full_name_ar ?: $full_name_en) : $full_name_en;
$success = __('success_update');
// Refresh data
$stmt = db()->prepare("SELECT u.*, b.name_en as branch_name_en, b.name_ar as branch_name_ar
FROM users u
LEFT JOIN branches b ON u.branch_id = b.id
WHERE u.id = ?");
$stmt->execute([$current_user_id]);
$user = $stmt->fetch();
// Refresh header data
$current_user_data = $user;
} catch (Exception $e) {
$error = __('error_update') . ' ' . $e->getMessage();
}
}
}
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h3 mb-0"><?= __('user_profile') ?></h2>
<button type="button" class="btn btn-primary px-4 fw-bold shadow-sm" style="border-radius: 12px;" data-bs-toggle="modal" data-bs-target="#editProfileModal">
<i class="bi bi-pencil-square me-2"></i> <?= __('edit_profile') ?? 'Edit Profile' ?>
</button>
</div>
<?php if ($success): ?>
<div class="alert alert-success alert-dismissible fade show rounded-4 border-0 shadow-sm" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> <?= $success ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger alert-dismissible fade show rounded-4 border-0 shadow-sm" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> <?= $error ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="row">
<div class="col-lg-4 mb-4">
<div class="card text-center p-4 border-0 shadow-sm h-100" style="border-radius: 20px;">
<div class="mb-4">
<?php if ($user['profile_picture']): ?>
<img src="<?= $user['profile_picture'] ?>?v=<?= time() ?>" alt="Profile Picture" class="rounded-circle border p-2 shadow-sm" style="width: 180px; height: 180px; object-fit: cover; background-color: #fff;">
<?php else: ?>
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center mx-auto text-primary border shadow-sm" style="width: 180px; height: 180px; font-size: 4.5rem; background-color: #fff !important;">
<i class="bi bi-person"></i>
</div>
<?php endif; ?>
</div>
<h4 class="fw-bold mb-1"><?= htmlspecialchars($lang === 'ar' ? ($user['full_name_ar'] ?: $user['full_name_en']) : $user['full_name_en']) ?></h4>
<p class="text-muted mb-3"><?= htmlspecialchars($user['email']) ?></p>
<div class="d-flex justify-content-center gap-2">
<span class="badge bg-soft-primary text-primary px-3 py-2" style="border-radius: 8px;"><?= ucfirst(str_replace('_', ' ', $user['role'])) ?></span>
<?php if ($user['branch_name_en']): ?>
<span class="badge bg-soft-info text-info px-3 py-2" style="border-radius: 8px;">
<i class="bi bi-shop me-1"></i> <?= $lang === 'ar' ? ($user['branch_name_ar'] ?: $user['branch_name_en']) : $user['branch_name_en'] ?>
</span>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card p-4 border-0 shadow-sm h-100" style="border-radius: 20px;">
<h5 class="fw-bold mb-4 pb-2 border-bottom"><?= __('personal_information') ?? 'Personal Information' ?></h5>
<div class="row g-4">
<div class="col-md-6">
<label class="text-muted small d-block mb-1"><?= __('full_name_en') ?></label>
<div class="fw-bold fs-5"><?= htmlspecialchars($user['full_name_en'] ?? '-') ?></div>
</div>
<div class="col-md-6">
<label class="text-muted small d-block mb-1"><?= __('full_name_ar') ?></label>
<div class="fw-bold fs-5"><?= htmlspecialchars($user['full_name_ar'] ?? '-') ?></div>
</div>
<div class="col-md-6">
<label class="text-muted small d-block mb-1"><?= __('email') ?></label>
<div class="fw-bold fs-5"><?= htmlspecialchars($user['email'] ?? '-') ?></div>
</div>
<div class="col-md-6">
<label class="text-muted small d-block mb-1"><?= __('username') ?></label>
<div class="fw-bold fs-5"><?= htmlspecialchars($user['username'] ?? '-') ?></div>
</div>
<div class="col-md-6">
<label class="text-muted small d-block mb-1"><?= __('role') ?></label>
<div class="fw-bold fs-5"><?= ucfirst(str_replace('_', ' ', $user['role'])) ?></div>
</div>
<div class="col-md-6">
<label class="text-muted small d-block mb-1"><?= __('branch') ?></label>
<div class="fw-bold fs-5"><?= $lang === 'ar' ? ($user['branch_name_ar'] ?: $user['branch_name_en']) : ($user['branch_name_en'] ?: '-') ?></div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Profile Modal -->
<div class="modal fade" id="editProfileModal" tabindex="-1" aria-labelledby="editProfileModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0 shadow" style="border-radius: 25px;">
<form action="profile.php" method="POST" enctype="multipart/form-data">
<div class="modal-header border-0 p-4 pb-0">
<h5 class="modal-title fw-bold" id="editProfileModalLabel"><?= __('update_profile') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4">
<div class="row g-4">
<div class="col-md-12 text-center mb-2">
<div class="position-relative d-inline-block">
<?php if ($user['profile_picture']): ?>
<img id="modalPreview" src="<?= $user['profile_picture'] ?>?v=<?= time() ?>" alt="Profile Picture" class="rounded-circle border p-1 shadow-sm" style="width: 120px; height: 120px; object-fit: cover; background-color: #fff;">
<?php else: ?>
<div id="modalPlaceholder" class="rounded-circle bg-light d-flex align-items-center justify-content-center mx-auto text-primary border shadow-sm" style="width: 120px; height: 120px; font-size: 3rem; background-color: #fff !important;">
<i class="bi bi-person"></i>
</div>
<img id="modalPreview" src="" alt="Profile Picture" class="rounded-circle border p-1 shadow-sm d-none" style="width: 120px; height: 120px; object-fit: cover; background-color: #fff;">
<?php endif; ?>
<label class="btn btn-sm btn-primary rounded-circle position-absolute bottom-0 end-0 p-2 shadow" style="cursor: pointer;">
<i class="bi bi-camera-fill"></i>
<input type="file" name="profile_picture" class="d-none" accept="image/*" onchange="previewImage(this)">
</label>
</div>
<div class="mt-2 small text-muted"><?= __('profile_picture') ?></div>
</div>
<div class="col-md-6">
<label class="form-label fw-bold small"><?= __('full_name_en') ?></label>
<input type="text" name="full_name_en" class="form-control" value="<?= htmlspecialchars($user['full_name_en'] ?? '') ?>" required style="border-radius: 12px;">
</div>
<div class="col-md-6">
<label class="form-label fw-bold small"><?= __('full_name_ar') ?></label>
<input type="text" name="full_name_ar" class="form-control" value="<?= htmlspecialchars($user['full_name_ar'] ?? '') ?>" required style="border-radius: 12px;">
</div>
<div class="col-md-6">
<label class="form-label fw-bold small"><?= __('email') ?></label>
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($user['email'] ?? '') ?>" required style="border-radius: 12px;">
</div>
<div class="col-md-6">
<label class="form-label fw-bold small"><?= __('username') ?></label>
<input type="text" class="form-control bg-light" value="<?= htmlspecialchars($user['username']) ?>" disabled style="border-radius: 12px;">
</div>
<div class="col-md-12">
<hr class="my-2">
<h6 class="fw-bold mb-3"><?= __('change_password') ?></h6>
</div>
<div class="col-md-6">
<label class="form-label fw-bold small"><?= __('password') ?></label>
<input type="password" name="password" class="form-control" autocomplete="new-password" style="border-radius: 12px;" placeholder="<?= __('leave_blank_to_keep') ?? 'Leave blank to keep current' ?>">
</div>
<div class="col-md-6">
<label class="form-label fw-bold small"><?= __('confirm_password') ?></label>
<input type="password" name="confirm_password" class="form-control" autocomplete="new-password" style="border-radius: 12px;" placeholder="<?= __('confirm_new_password') ?? 'Confirm new password' ?>">
</div>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light px-4 fw-bold" data-bs-dismiss="modal" style="border-radius: 12px;"><?= __('cancel') ?></button>
<button type="submit" class="btn btn-primary px-5 fw-bold shadow-sm" style="border-radius: 12px;"><?= __('save_changes') ?></button>
</div>
</form>
</div>
</div>
</div>
<style>
.bg-soft-primary { background-color: rgba(13, 110, 253, 0.1); }
.bg-soft-info { background-color: rgba(13, 202, 240, 0.1); }
</style>
<script>
function previewImage(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
const preview = document.getElementById('modalPreview');
const placeholder = document.getElementById('modalPlaceholder');
preview.src = e.target.result;
preview.classList.remove('d-none');
if (placeholder) placeholder.classList.add('d-none');
}
reader.readAsDataURL(input.files[0]);
}
}
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

130
rate.php Normal file
View File

@ -0,0 +1,130 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION["lang"]) && !isset($_GET["lang"])) {
$_SESSION["lang"] = "ar";
}
require_once __DIR__ . "/db/config.php";
require_once __DIR__ . '/includes/lang.php';
$branch_id_get = isset($_GET['branch_id']) ? (int)$_GET['branch_id'] : null;
$info = null;
if ($branch_id_get) {
$stmt = db()->prepare("SELECT b.id AS branch_id, b.name_en AS branch_name_en, b.name_ar AS branch_name_ar, c.id AS company_id, c.name_en AS comp_name_en, c.name_ar AS comp_name_ar, c.logo FROM branches b JOIN companies c ON b.company_id = c.id WHERE b.id = ?");
$stmt->execute([$branch_id_get]);
$info = $stmt->fetch(PDO::FETCH_ASSOC);
}
if (empty($info)) {
$stmt = db()->query("SELECT b.id AS branch_id, b.name_en AS branch_name_en, b.name_ar AS branch_name_ar, c.id AS company_id, c.name_en AS comp_name_en, c.name_ar AS comp_name_ar, c.logo FROM branches b JOIN companies c ON b.company_id = c.id LIMIT 1");
$info = $stmt->fetch(PDO::FETCH_ASSOC);
}
$branch_id = $info['branch_id'] ?? null;
$company_name = $_SESSION['lang'] === 'ar' ? ($info['comp_name_ar'] ?? '') : ($info['comp_name_en'] ?? '');
$branch_name = $_SESSION['lang'] === 'ar' ? ($info['branch_name_ar'] ?? '') : ($info['branch_name_en'] ?? '');
$logo = $info['logo'] ?? null;
$branch_query = $branch_id ? '?branch_id=' . $branch_id : '';
?>
<!DOCTYPE html>
<html lang="<?= $_SESSION['lang'] ?>" dir="<?= $_SESSION['lang'] === 'ar' ? 'rtl' : 'ltr' ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('rate_us') ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<style>
body { background-color: #f8f9fc; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; flex-direction: column; min-height: 100vh; }
.rating-card { max-width: 600px; margin: 40px auto; border-radius: 15px; border: none; box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15); padding: 40px 30px; background: white; }
.main-content { flex: 1; }
.footer { background-color: white; border-top: 1px solid #eaecf4; padding: 1.5rem 0; color: #858796; text-align: center; }
.option-btn {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
border-radius: 15px;
padding: 30px 20px;
text-decoration: none;
color: #4e73df;
background-color: #f8f9fc;
border: 2px solid #eaecf4;
transition: all 0.3s ease;
height: 100%;
}
.option-btn:hover {
border-color: #4e73df;
background-color: #eaecf4;
color: #2e59d9;
transform: translateY(-5px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
}
.option-icon {
font-size: 3rem;
margin-bottom: 15px;
color: inherit;
}
.option-text {
font-size: 1.25rem;
font-weight: 600;
color: #5a5c69;
}
</style>
</head>
<body>
<div class="main-content">
<div class="container py-5">
<div class="text-end mb-3">
<?php if ($_SESSION['lang'] === 'ar'): ?>
<a href="?lang=en<?= $branch_id ? '&branch_id=' . $branch_id : '' ?>" class="text-decoration-none btn btn-sm btn-outline-secondary">English</a>
<?php else: ?>
<a href="?lang=ar<?= $branch_id ? '&branch_id=' . $branch_id : '' ?>" class="text-decoration-none btn btn-sm btn-outline-secondary">العربية</a>
<?php endif; ?>
</div>
<div class="rating-card text-center">
<?php if ($logo): ?>
<img src="<?= htmlspecialchars($logo) ?>" alt="<?= htmlspecialchars($company_name) ?>" class="mb-3" style="max-height: 90px; max-width: 100%; object-fit: contain;">
<?php endif; ?>
<h3 class="mb-1 fw-bold"><?= htmlspecialchars($company_name) ?></h3>
<?php if ($branch_name): ?>
<p class="text-muted mb-4 small"><i class="bi bi-geo-alt-fill text-danger me-1"></i><?= htmlspecialchars($branch_name) ?></p>
<?php endif; ?>
<h4 class="mb-5 text-primary border-top pt-4 mt-2"><?= __('rate_us') ?></h4>
<div class="row g-4 justify-content-center">
<div class="col-sm-6">
<a href="staff_rating.php<?= $branch_query ?>" class="option-btn">
<i class="bi bi-person-badge option-icon"></i>
<span class="option-text"><?= __('rate_staff') ?></span>
</a>
</div>
<div class="col-sm-6">
<a href="service_rating.php<?= $branch_query ?>" class="option-btn">
<i class="bi bi-stars option-icon"></i>
<span class="option-text"><?= __('rate_services') ?></span>
</a>
</div>
</div>
</div>
</div>
</div>
<footer class="footer">
<div class="container">
<span>&copy; <?= date('Y') ?> <?= htmlspecialchars($company_name) ?>. <?= __('all_rights_reserved') ?></span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

347
ratings.php Normal file
View File

@ -0,0 +1,347 @@
<?php
$title = 'ratings';
require_once __DIR__ . '/includes/header.php';
$success_msg = '';
$error_msg = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'clear_ratings') {
$stmt = db()->prepare("DELETE FROM ratings");
if ($stmt->execute()) {
$success_msg = __('ratings_cleared');
} else {
$error_msg = __('error_clearing_ratings');
}
}
$from_date = $_GET['from_date'] ?? date('Y-m-01');
$to_date = $_GET['to_date'] ?? date('Y-m-t');
$type_filter = $_GET['type'] ?? 'all';
$branch_filter = $_GET['branch_id'] ?? 'all';
$where = "WHERE DATE(ratings.created_at) BETWEEN ? AND ?";
$params = [$from_date, $to_date];
if ($type_filter !== 'all') {
$where .= " AND ratings.rating_type = ?";
$params[] = $type_filter;
}
if ($branch_filter !== 'all') {
$where .= " AND ratings.branch_id = ?";
$params[] = $branch_filter;
}
// Fetch branches for filter and modal
$branches_stmt = db()->query("SELECT id, name_en, name_ar FROM branches ORDER BY name_en");
$all_branches = $branches_stmt->fetchAll(PDO::FETCH_ASSOC);
// Summary Stats
$stmt = db()->prepare("
SELECT
COUNT(*) as total_ratings,
AVG(ratings.rating_value) as avg_rating
FROM ratings
LEFT JOIN branches b ON ratings.branch_id = b.id
$where
");
$stmt->execute($params);
$stats = $stmt->fetch(PDO::FETCH_ASSOC);
$total_ratings = $stats['total_ratings'] ?: 0;
$avg_rating = round($stats['avg_rating'] ?: 0, 1);
// Detail Records
$stmt = db()->prepare("
SELECT ratings.*, b.name_en AS branch_name_en, b.name_ar AS branch_name_ar
FROM ratings
LEFT JOIN branches b ON ratings.branch_id = b.id
$where
ORDER BY ratings.created_at DESC
");
$stmt->execute($params);
$ratings = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<div class="container-fluid py-4">
<?php if ($success_msg): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= htmlspecialchars($success_msg) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if ($error_msg): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?= htmlspecialchars($error_msg) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="row mb-4 align-items-center">
<div class="col">
<h1 class="h3 mb-0 text-gray-800"><?= __('ratings') ?></h1>
</div>
<div class="col-auto">
<button type="button" class="btn btn-sm btn-outline-info" data-bs-toggle="modal" data-bs-target="#branchLinksModal">
<i class="bi bi-link-45deg"></i> <?= __('branch_links') ?? 'Branch Links' ?>
</button>
<form method="POST" class="d-inline" onsubmit="return confirm('<?= __('confirm_clear_ratings') ?>');">
<input type="hidden" name="action" value="clear_ratings">
<button type="submit" class="btn btn-sm btn-danger ms-2"><i class="bi bi-trash"></i> <?= __('clear_ratings') ?></button>
</form>
</div>
</div>
<!-- Filter Card -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<form method="GET" class="row gx-3 gy-2 align-items-center">
<div class="col-sm-2">
<label class="visually-hidden"><?= __('from_date') ?></label>
<input type="date" class="form-control" name="from_date" value="<?= htmlspecialchars($from_date) ?>">
</div>
<div class="col-sm-2">
<label class="visually-hidden"><?= __('to_date') ?></label>
<input type="date" class="form-control" name="to_date" value="<?= htmlspecialchars($to_date) ?>">
</div>
<div class="col-sm-3">
<label class="visually-hidden"><?= __('branch') ?></label>
<select class="form-select" name="branch_id">
<option value="all" <?= $branch_filter == 'all' ? 'selected' : '' ?>><?= __('all_branches') ?></option>
<?php foreach($all_branches as $br): ?>
<option value="<?= $br['id'] ?>" <?= $branch_filter == $br['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars(is_arabic() ? $br['name_ar'] : $br['name_en']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-sm-3">
<label class="visually-hidden"><?= __('rating_type') ?></label>
<select class="form-select" name="type">
<option value="all" <?= $type_filter == 'all' ? 'selected' : '' ?>><?= __('all') ?></option>
<option value="staff" <?= $type_filter == 'staff' ? 'selected' : '' ?>><?= __('staff') ?></option>
<option value="service" <?= $type_filter == 'service' ? 'selected' : '' ?>><?= __('service') ?></option>
</select>
</div>
<div class="col-sm-2">
<button type="submit" class="btn btn-primary w-100"><i class="bi bi-search"></i> <?= __('filter') ?></button>
</div>
</form>
</div>
</div>
<!-- Stats Cards -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow-sm h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1"><?= __('average_rating') ?></div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
<?= $avg_rating ?> <i class="bi bi-star-fill text-warning"></i>
</div>
</div>
<div class="col-auto">
<i class="bi bi-star fa-2x text-gray-300" style="font-size: 2rem; color: #dddfeb;"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow-sm h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1"><?= __('total_ratings') ?></div>
<div class="h5 mb-0 font-weight-bold text-gray-800"><?= number_format($total_ratings) ?></div>
</div>
<div class="col-auto">
<i class="bi bi-chat-left-text fa-2x text-gray-300" style="font-size: 2rem; color: #dddfeb;"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Ratings Table -->
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="m-0 font-weight-bold text-primary"><?= __('ratings') ?></h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered table-hover w-100">
<thead class="table-light">
<tr>
<th><?= __('date') ?></th>
<th><?= __('branch') ?></th>
<th><?= __('rating_type') ?></th>
<th><?= __('rating') ?></th>
<th><?= __('comment') ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($ratings)): ?>
<tr>
<td colspan="5" class="text-center text-muted"><?= __('no_data_available') ?? 'No data available' ?></td>
</tr>
<?php else: ?>
<?php foreach ($ratings as $r): ?>
<tr>
<td><?= date('Y-m-d H:i', strtotime($r['created_at'])) ?></td>
<td><?= htmlspecialchars(is_arabic() ? ($r['branch_name_ar'] ?? '-') : ($r['branch_name_en'] ?? '-')) ?></td>
<td>
<?php if ($r['rating_type'] === 'staff'): ?>
<span class="badge bg-info text-dark"><?= __('staff') ?></span>
<?php else: ?>
<span class="badge bg-secondary"><?= __('service') ?></span>
<?php endif; ?>
</td>
<td>
<?php for($i=1; $i<=5; $i++): ?>
<i class="bi bi-star<?= $i <= $r['rating_value'] ? '-fill text-warning' : ' text-muted' ?>"></i>
<?php endfor; ?>
</td>
<td><?= htmlspecialchars($r['comment']) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Branch Links Modal -->
<div class="modal fade" id="branchLinksModal" tabindex="-1" aria-labelledby="branchLinksModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="branchLinksModalLabel"><i class="bi bi-link-45deg"></i> <?= __('branch_links') ?? 'Branch Links' ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="text-muted small mb-4"><?= is_arabic() ? 'انسخ الرابط الخاص بكل فرع لمشاركته مع العملاء. سيتم تسجيل التقييمات تلقائياً تحت الفرع الصحيح.' : 'Copy the unique link for each branch to share with customers. Ratings will automatically be recorded under the correct branch.' ?></p>
<div class="list-group">
<?php
$base_url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]" . dirname($_SERVER['PHP_SELF']);
if (empty($all_branches)): ?>
<div class="list-group-item text-muted text-center py-4">No branches found.</div>
<?php else: ?>
<?php foreach($all_branches as $br):
$br_name = is_arabic() ? $br['name_ar'] : $br['name_en'];
$br_link = $base_url . "/rate.php?branch_id=" . $br['id'];
?>
<div class="list-group-item d-flex justify-content-between align-items-center py-3">
<div class="me-3" style="word-break: break-all;">
<h6 class="mb-1 fw-bold"><?= htmlspecialchars($br_name) ?></h6>
<a href="<?= htmlspecialchars($br_link) ?>" target="_blank" class="text-decoration-none small text-primary"><?= htmlspecialchars($br_link) ?></a>
</div>
<div class="text-nowrap">
<button class="btn btn-sm btn-outline-secondary me-2" onclick="copyToClipboard('<?= $br_link ?>', this)">
<i class="bi bi-clipboard"></i> <span class="d-none d-sm-inline"><?= __('copy_link') ?? 'Copy Link' ?></span>
</button>
<button class="btn btn-sm btn-outline-primary" onclick="printQR('<?= $br_link ?>', '<?= addslashes(htmlspecialchars($br_name)) ?>')">
<i class="bi bi-qr-code"></i> <span class="d-none d-sm-inline"><?= __('print_qr') ?? 'Print QR' ?></span>
</button>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
<script>
function printQR(url, branchName) {
let printWindow = window.open('', '_blank', 'width=400,height=600');
printWindow.document.write(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Print QR - ${branchName}</title>
<style>
body { font-family: sans-serif; text-align: center; padding: 50px; background: #fff; color: #000; }
h2 { margin-bottom: 5px; font-size: 24px; }
#qrcode { display: flex; justify-content: center; margin-top: 30px; }
#qrcode img { margin: 0 auto; }
.rate-text { font-size: 20px; font-weight: bold; margin-top: 20px; }
</style>
</head>
<body>
<h2>${branchName}</h2>
<div id="qrcode"></div>
<div class="rate-text">Rate Us / قيمنا</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"><\/script>
<script>
window.onload = function() {
new QRCode(document.getElementById("qrcode"), {
text: "${url}",
width: 200,
height: 200,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.M
});
setTimeout(function() {
window.print();
window.close();
}, 500);
};
<\/script>
</body>
</html>
`);
printWindow.document.close();
}
function copyToClipboard(text, btn) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(function() {
let originalHtml = btn.innerHTML;
btn.innerHTML = '<i class="bi bi-check2"></i> <span class="d-none d-sm-inline"><?= __('link_copied') ?? 'Copied!' ?></span>';
btn.classList.remove('btn-outline-secondary');
btn.classList.add('btn-success', 'text-white');
setTimeout(function() {
btn.innerHTML = originalHtml;
btn.classList.remove('btn-success', 'text-white');
btn.classList.add('btn-outline-secondary');
}, 2000);
});
} else {
// Fallback for older browsers / http
let textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
let originalHtml = btn.innerHTML;
btn.innerHTML = '<i class="bi bi-check2"></i> <span class="d-none d-sm-inline"><?= __('link_copied') ?? 'Copied!' ?></span>';
btn.classList.remove('btn-outline-secondary');
btn.classList.add('btn-success', 'text-white');
setTimeout(function() {
btn.innerHTML = originalHtml;
btn.classList.remove('btn-success', 'text-white');
btn.classList.add('btn-outline-secondary');
}, 2000);
} catch (err) {
console.error('Unable to copy', err);
}
document.body.removeChild(textArea);
}
}
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

217
receipt.php Normal file
View File

@ -0,0 +1,217 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/lang.php';
if (($_SESSION['role'] ?? 'cashier') === 'limited_viewer') { die('Access Denied'); }
$order_id = $_GET['id'] ?? null;
if (!$order_id) {
die("Order ID is required");
}
// Fetch Global Company Info
$stmt = db()->query("SELECT * FROM companies LIMIT 1");
$company = $stmt->fetch();
// Fetch Order Details with Customer and Branch info
$stmt = db()->prepare("SELECT o.*,
c.name_en as customer_name_en, c.name_ar as customer_name_ar, c.phone as customer_phone,
b.name_en as branch_name_en, b.name_ar as branch_name_ar, b.address_en as branch_address_en, b.address_ar as branch_address_ar, b.phone as branch_phone
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
LEFT JOIN branches b ON o.branch_id = b.id
WHERE o.id = ?");
$stmt->execute([$order_id]);
$order = $stmt->fetch();
if (!$order) {
die("Order not found");
}
// Fetch items for this order
$stmt = db()->prepare("SELECT oi.*, i.name_en, i.name_ar, s.name_en as service_en, s.name_ar as service_ar
FROM order_items oi
JOIN items i ON oi.item_id = i.id
JOIN services s ON oi.service_id = s.id
WHERE oi.order_id = ?");
$stmt->execute([$order_id]);
$items = $stmt->fetchAll();
$lang = $_SESSION['lang'] ?? 'en';
function format_currency($amount) {
return number_format($amount, decimals()) . ' ' . currency();
}
?>
<!DOCTYPE html>
<html lang="<?= $lang ?>" dir="<?= is_arabic() ? 'rtl' : 'ltr' ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('thermal_receipt') ?> #<?= $order['order_number'] ?? $order['id'] ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;600&family=IBM+Plex+Sans+Arabic:wght@400;600&display=swap" rel="stylesheet">
<style>
body {
font-family: 'IBM Plex Sans', 'IBM Plex Sans Arabic', sans-serif;
background: #f8f9fa;
color: #333;
-webkit-print-color-adjust: exact;
}
.receipt-container {
width: 80mm;
background: white;
margin: 20px auto;
padding: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
@media print {
body { background: white; margin: 0; padding: 0; width: 80mm; }
.receipt-container { margin: 0; box-shadow: none; width: 80mm; padding: 0; }
.no-print { display: none; }
}
.header { text-align: center; border-bottom: 1px dashed #ccc; padding-bottom: 10px; margin-bottom: 10px; }
.item-row { display: flex; justify-content: space-between; margin-bottom: 5px; font-size: 0.9rem; }
.item-name { flex-grow: 1; }
.item-price { text-align: right; margin-left: 10px; }
.item-service { font-size: 0.75rem; color: #666; }
.totals { border-top: 1px dashed #ccc; padding-top: 10px; margin-top: 10px; }
.total-row { display: flex; justify-content: space-between; font-weight: 600; font-size: 1rem; }
.info-row { font-size: 0.8rem; margin-bottom: 3px; display: flex; justify-content: space-between; }
.barcode { text-align: center; margin-top: 15px; }
.arabic-text { font-family: 'IBM Plex Sans Arabic', sans-serif; }
</style>
</head>
<body>
<div class="no-print text-center my-3">
<button onclick="window.print()" class="btn btn-primary btn-sm px-4"><?= __('print_receipt') ?></button>
<a href="pos.php" class="btn btn-outline-secondary btn-sm ms-2"><?= __('back_to_pos') ?></a>
</div>
<div class="receipt-container">
<div class="header">
<?php if (!empty($company['logo'])): ?>
<img src="<?= htmlspecialchars($company['logo']) ?>" alt="Logo" style="max-height: 80px; max-width: 100%; margin-bottom: 10px;">
<?php endif; ?>
<h5 class="fw-bold m-0"><?= htmlspecialchars($company['name_en'] ?? 'Laundry POS') ?></h5>
<?php if (!empty($company['name_ar'])): ?>
<div class="arabic-text small"><?= htmlspecialchars($company['name_ar']) ?></div>
<?php endif; ?>
<div class="small mt-1"><?= htmlspecialchars($order['branch_name_en']) ?> / <span class="arabic-text"><?= htmlspecialchars($order['branch_name_ar']) ?></span></div>
<div class="small"><?= htmlspecialchars($order['branch_phone']) ?></div>
<?php if (!empty($company['ctr_no'])): ?>
<div class="small"><?= __('ctr_no') ?>: <?= htmlspecialchars($company['ctr_no']) ?></div>
<?php endif; ?>
<?php if (!empty($company['vat_no'])): ?>
<div class="small"><?= __('vat_no') ?>: <?= htmlspecialchars($company['vat_no']) ?></div>
<?php endif; ?>
</div>
<div class="info-row">
<span><?= __('date') ?>:</span>
<span><?= date('Y-m-d H:i', strtotime($order['created_at'])) ?></span>
</div>
<div class="info-row">
<span><?= __('order_no') ?>:</span>
<span class="fw-bold"><?= $order['order_number'] ?? $order['id'] ?></span>
</div>
<div class="info-row">
<span><?= __('customer') ?>:</span>
<span><?= $order['customer_name_en'] ?></span>
</div>
<div class="info-row">
<span><?= __('phone') ?>:</span>
<span><?= $order['customer_phone'] ?></span>
</div>
<div class="mt-3 border-top pt-2">
<?php foreach($items as $item):
?>
<div class="mb-2">
<div class="item-row">
<div class="item-name"><?= $item['name_en'] ?> / <span class="arabic-text"><?= $item['name_ar'] ?></span> x <?= $item['quantity'] ?></div>
<div class="item-price"><?= format_currency($item['subtotal']) ?></div>
</div>
<div class="item-service"><?= $item['service_en'] ?> / <span class="arabic-text"><?= $item['service_ar'] ?></span></div>
</div>
<?php endforeach;
?>
</div>
<?php
$calculated_subtotal = 0;
foreach($items as $item) {
$calculated_subtotal += $item['subtotal'];
}
?>
<div class="totals">
<div class="info-row">
<span><?= __('subtotal') ?>:</span>
<span><?= format_currency($calculated_subtotal) ?></span>
</div>
<?php if ($order['loyalty_discount'] > 0):
?>
<div class="info-row">
<span><?= __('loyalty_discount') ?>:</span>
<span>-<?= format_currency($order['loyalty_discount']) ?></span>
</div>
<?php endif;
?>
<div class="info-row">
<span><?= __('vat') ?>:</span>
<span><?= format_currency($order['vat_total'] ?? 0) ?></span>
</div>
<div class="total-row mt-2">
<span><?= __('total') ?>:</span>
<span><?= format_currency($order['total_price']) ?></span>
</div>
</div>
<div class="mt-4 text-center small">
<div>Thank you for choosing us!</div>
<div class="arabic-text">شكراً لاختياركم لنا!</div>
</div>
<div class="mt-3 text-center">
<div class="small fw-bold mb-1">Rate Us / <span class="arabic-text">قيمنا</span></div>
<div id="qrcode" class="d-flex justify-content-center"></div>
</div>
<div class="barcode">
<svg id="barcode"></svg>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.0/dist/JsBarcode.all.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script>
JsBarcode("#barcode", "<?= $order['order_number'] ?? $order['id'] ?>", {
format: "CODE128",
lineColor: "#000",
width: 1.5,
height: 40,
displayValue: false
});
<?php
$scheme = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') ? "https" : "http";
$host = $_SERVER['HTTP_HOST'];
$base_dir = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/');
$rate_url = $scheme . "://" . $host . $base_dir . "/rate.php?branch_id=" . ($order['branch_id'] ?? 1);
?>
new QRCode(document.getElementById("qrcode"), {
text: "<?= $rate_url ?>",
width: 80,
height: 80,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.M
});
</script>
<script>
window.onload = function() { setTimeout(() => window.print(), 500); };
</script>
</body>
</html>

513
reports.php Normal file
View File

@ -0,0 +1,513 @@
<?php
$title = 'reports';
require_once __DIR__ . '/includes/header.php';
// View check is already handled by header.php global check.
// If someone needs specific action permission on reports, we could check has_permission('add') etc.
$session_branch_id = $_SESSION['branch_id'] ?? 'all';
$from_date = $_GET['from_date'] ?? date('Y-m-01');
$to_date = $_GET['to_date'] ?? date('Y-m-t');
// Default to session branch if no explicit GET filter
$branch_filter = $_GET['branch_id'] ?? $session_branch_id;
$user_filter = $_GET['user_id'] ?? 'all';
// Base queries
$where = "WHERE DATE(o.created_at) BETWEEN ? AND ?";
$params = [$from_date, $to_date];
if ($branch_filter !== 'all') {
$where .= " AND o.branch_id = ?";
$params[] = $branch_filter;
}
if ($user_filter !== 'all') {
$where .= " AND o.user_id = ?";
$params[] = $user_filter;
}
// Summary Stats
$stmt = db()->prepare("SELECT
SUM(total_price) as total_revenue,
COUNT(*) as total_orders,
AVG(total_price) as avg_order_value
FROM orders o $where");
$stmt->execute($params);
$summary = $stmt->fetch();
// Revenue by Branch
$stmt = db()->prepare("SELECT
b.name_en, b.name_ar, SUM(o.total_price) as revenue
FROM orders o
JOIN branches b ON o.branch_id = b.id
$where
GROUP BY o.branch_id");
$stmt->execute($params);
$revenue_by_branch = $stmt->fetchAll();
// Revenue by Cashier
$stmt = db()->prepare("SELECT
u.full_name_en, u.full_name_ar, SUM(o.total_price) as revenue
FROM orders o
JOIN users u ON o.user_id = u.id
$where
GROUP BY o.user_id");
$stmt->execute($params);
$revenue_by_user = $stmt->fetchAll();
// Orders by Status
$stmt = db()->prepare("SELECT
status, COUNT(*) as count
FROM orders o
$where
GROUP BY status");
$stmt->execute($params);
$orders_by_status = $stmt->fetchAll();
// Revenue by Payment Method
$stmt = db()->prepare("SELECT
p.payment_method, SUM(p.amount) as revenue
FROM payments p
JOIN orders o ON p.order_id = o.id
$where
GROUP BY p.payment_method");
$stmt->execute($params);
$revenue_by_method = $stmt->fetchAll();
// Top Items
$stmt = db()->prepare("SELECT
i.name_en, i.name_ar, SUM(oi.quantity) as total_qty, SUM(oi.subtotal) as total_revenue
FROM order_items oi
JOIN orders o ON oi.order_id = o.id
JOIN items i ON oi.item_id = i.id
$where
GROUP BY oi.item_id
ORDER BY total_qty DESC LIMIT 10");
$stmt->execute($params);
$top_items = $stmt->fetchAll();
// Top Services
$stmt = db()->prepare("SELECT
s.name_en, s.name_ar, SUM(oi.quantity) as total_qty, SUM(oi.subtotal) as total_revenue
FROM order_items oi
JOIN orders o ON oi.order_id = o.id
JOIN services s ON oi.service_id = s.id
$where
GROUP BY oi.service_id
ORDER BY total_qty DESC LIMIT 10");
$stmt->execute($params);
$top_services = $stmt->fetchAll();
// Fetch branches and users for filters
$branches = db()->query("SELECT id, name_en, name_ar FROM branches")->fetchAll();
$users = db()->query("SELECT id, full_name_en, full_name_ar FROM users")->fetchAll();
// Filter display names
$branch_name = __('all_branches');
if ($branch_filter !== 'all') {
foreach($branches as $b) {
if ($b['id'] == $branch_filter) {
$branch_name = is_arabic() ? $b['name_ar'] : $b['name_en'];
break;
}
}
}
$user_name = __('all');
if ($user_filter !== 'all') {
foreach($users as $u) {
if ($u['id'] == $user_filter) {
$user_name = is_arabic() ? $u['full_name_ar'] : $u['full_name_en'];
break;
}
}
}
// Fallback values if database is empty
$display_company_name = ($lang === 'ar' ? ($company_info['name_ar'] ?: $company_info['name_en']) : $company_info['name_en']) ?: 'Laundry Brand';
$display_address = ($lang === 'ar' ? ($company_info['address_ar'] ?: $company_info['address_en']) : $company_info['address_en']);
$display_phone = $company_info['phone'];
$display_email = $company_info['email'];
$display_vat = $company_info['vat_no'] ?: $company_info['vat_number'];
$display_ctr = $company_info['ctr_no'];
?>
<div class="d-flex justify-content-between align-items-center mb-4 no-print">
<h4 class="fw-bold mb-0"><?= __('reports') ?></h4>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-primary shadow-sm" style="border-radius: 12px;" data-bs-toggle="modal" data-bs-target="#customerStatementModal">
<i class="bi bi-person-lines-fill me-2"></i> <?= __('customer_statement') ?>
</button>
<button onclick="window.print()" class="btn btn-primary shadow-sm" style="border-radius: 12px;">
<i class="bi bi-printer me-2"></i> <?= __('print_report') ?>
</button>
</div>
</div>
<!-- Customer Statement Search Modal -->
<div class="modal fade no-print" id="customerStatementModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg" style="border-radius: 20px;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold"><?= __('customer_statement') ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<form action="customer_statement.php" method="GET">
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('search_customer') ?? 'Search Customer' ?></label>
<div class="input-group">
<span class="input-group-text bg-white border-end-0" style="border-radius: 12px 0 0 12px;"><i class="bi bi-search"></i></span>
<input type="text" id="customerSearch" class="form-control border-start-0" placeholder="<?= __('type_name_or_phone') ?? 'Type name or phone...' ?>" style="border-radius: 0 12px 12px 0;">
</div>
<div id="customerList" class="list-group mt-2 overflow-auto" style="max-height: 200px;"></div>
<input type="hidden" name="id" id="selectedCustomerId" required>
</div>
<div id="selectedCustomerInfo" class="alert alert-light border d-none mb-3" style="border-radius: 12px;">
<div class="fw-bold" id="selectedCustomerName"></div>
<div class="small text-muted" id="selectedCustomerPhone"></div>
</div>
<button type="submit" id="generateStatementBtn" class="btn btn-primary w-100 py-3 fw-bold shadow-sm" style="border-radius: 15px;" disabled>
<?= __('generate_statement') ?? 'Generate Statement' ?>
</button>
</form>
</div>
</div>
</div>
</div>
<script>
document.getElementById('customerSearch').addEventListener('input', function() {
const query = this.value;
if (query.length < 2) {
document.getElementById('customerList').innerHTML = '';
return;
}
fetch('api/search_customers.php?query=' + encodeURIComponent(query))
.then(response => response.json())
.then(data => {
const list = document.getElementById('customerList');
list.innerHTML = '';
if (data.length === 0) {
list.innerHTML = '<div class="list-group-item small text-muted">No customers found</div>';
return;
}
data.forEach(c => {
const item = document.createElement('button');
item.className = 'list-group-item list-group-item-action small';
item.type = 'button';
item.innerHTML = `<strong>${c.name_en}</strong> (${c.phone})`;
item.onclick = () => {
document.getElementById('selectedCustomerId').value = c.id;
document.getElementById('selectedCustomerName').textContent = c.name_en;
document.getElementById('selectedCustomerPhone').textContent = c.phone;
document.getElementById('selectedCustomerInfo').classList.remove('d-none');
document.getElementById('generateStatementBtn').disabled = false;
list.innerHTML = '';
document.getElementById('customerSearch').value = '';
};
list.appendChild(item);
});
});
});
</script>
<!-- Print Header -->
<div class="d-none d-print-block mb-5 border-bottom pb-4">
<div class="row align-items-center">
<div class="col-7">
<?php if (!empty($company_info['logo'])): ?>
<img src="<?= $company_info['logo'] ?>" alt="Logo" style="max-height: 80px;" class="mb-3">
<?php else: ?>
<div class="mb-3 d-flex align-items-center text-primary">
<i class="bi bi-tsunami fs-1 me-2"></i>
<span class="fs-4 fw-bold">Laundry Management</span>
</div>
<?php endif; ?>
<h2 class="fw-bold mb-1"><?= htmlspecialchars($display_company_name) ?></h2>
<div class="text-muted small">
<?php if ($display_address): ?>
<?= htmlspecialchars($display_address) ?><br>
<?php endif; ?>
<?php if ($display_phone): ?>
<?= __('phone') ?>: <?= htmlspecialchars($display_phone) ?>
<?php endif; ?>
<?php if ($display_email): ?>
<?= $display_phone ? ' | ' : '' ?><?= __('email') ?>: <?= htmlspecialchars($display_email) ?><br>
<?php elseif($display_phone): ?>
<br>
<?php endif; ?>
<?php if ($display_vat): ?>
<?= __('vat_no') ?>: <?= htmlspecialchars($display_vat) ?>
<?php endif; ?>
<?php if ($display_ctr): ?>
<?= $display_vat ? ' | ' : '' ?><?= __('ctr_no') ?>: <?= htmlspecialchars($display_ctr) ?>
<?php endif; ?>
</div>
</div>
<div class="col-5 text-end">
<h1 class="fw-bold text-uppercase mb-2" style="color: #0d6efd;"><?= __('reports') ?></h1>
<div class="mt-3 small">
<p class="mb-0"><strong><?= __('date_range') ?>:</strong> <?= $from_date ?> - <?= $to_date ?></p>
<p class="mb-0"><strong><?= __('outlet') ?>:</strong> <?= $branch_name ?></p>
<p class="mb-0"><strong><?= __('cashier') ?>:</strong> <?= $user_name ?></p>
<p class="mb-0 text-muted mt-1"><?= __('print_date') ?? 'Print Date' ?>: <?= date('d/m/Y H:i') ?></p>
</div>
</div>
</div>
</div>
<div class="card p-4 mb-4 no-print border-0 shadow-sm" style="border-radius: 20px;">
<form method="GET" class="row g-3 align-items-end">
<div class="col-md-2">
<label class="form-label small fw-bold"><?= __('from_date') ?></label>
<input type="date" name="from_date" class="form-control" value="<?= $from_date ?>" style="border-radius: 12px;">
</div>
<div class="col-md-2">
<label class="form-label small fw-bold"><?= __('to_date') ?></label>
<input type="date" name="to_date" class="form-control" value="<?= $to_date ?>" style="border-radius: 12px;">
</div>
<?php if ($_SESSION['role'] === 'super_admin'): ?>
<div class="col-md-3">
<label class="form-label small fw-bold"><?= __('outlet') ?></label>
<select name="branch_id" class="form-select" style="border-radius: 12px;">
<option value="all"><?= __('all_branches') ?></option>
<?php foreach($branches as $b): ?>
<option value="<?= $b['id'] ?>" <?= $branch_filter == $b['id'] ? 'selected' : '' ?>>
<?= is_arabic() ? $b['name_ar'] : $b['name_en'] ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="col-md-3">
<label class="form-label small fw-bold"><?= __('cashier') ?></label>
<select name="user_id" class="form-select" style="border-radius: 12px;">
<option value="all"><?= __('all') ?></option>
<?php foreach($users as $u): ?>
<option value="<?= $u['id'] ?>" <?= $user_filter == $u['id'] ? 'selected' : '' ?>>
<?= is_arabic() ? $u['full_name_ar'] : $u['full_name_en'] ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100 shadow-sm fw-bold" style="border-radius: 12px;">
<i class="bi bi-filter me-2"></i> <?= __('generate_report') ?>
</button>
</div>
</form>
</div>
<div class="row g-4 mb-4">
<div class="col-md-4">
<div class="card p-4 text-center border-0 shadow-sm border-start border-primary border-5" style="border-radius: 15px;">
<div class="text-muted small mb-1"><?= __('total_revenue') ?></div>
<div class="fs-3 fw-bold text-primary"><?= format_amount($summary['total_revenue'] ?? 0) ?></div>
</div>
</div>
<div class="col-md-4">
<div class="card p-4 text-center border-0 shadow-sm border-start border-success border-5" style="border-radius: 15px;">
<div class="text-muted small mb-1"><?= __('total_orders') ?></div>
<div class="fs-3 fw-bold text-success"><?= $summary['total_orders'] ?? 0 ?></div>
</div>
</div>
<div class="col-md-4">
<div class="card p-4 text-center border-0 shadow-sm border-start border-info border-5" style="border-radius: 15px;">
<div class="text-muted small mb-1"><?= __('average_order_value') ?></div>
<div class="fs-3 fw-bold text-info"><?= format_amount($summary['avg_order_value'] ?? 0) ?></div>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-md-6">
<div class="card p-4 h-100 border-0 shadow-sm" style="border-radius: 20px;">
<h6 class="fw-bold mb-4"><?= __('revenue_by_outlet') ?></h6>
<div style="height: 300px; position: relative;">
<canvas id="revenueByBranchChart"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card p-4 h-100 border-0 shadow-sm" style="border-radius: 20px;">
<h6 class="fw-bold mb-4"><?= __('orders_by_status') ?></h6>
<div style="height: 300px; position: relative;">
<canvas id="ordersByStatusChart"></canvas>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-md-6">
<div class="card p-4 h-100 border-0 shadow-sm" style="border-radius: 20px;">
<h6 class="fw-bold mb-4"><?= __('revenue_by_cashier') ?></h6>
<div style="height: 300px; position: relative;">
<canvas id="revenueByCashierChart"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card p-4 h-100 border-0 shadow-sm" style="border-radius: 20px;">
<h6 class="fw-bold mb-4"><?= __('revenue_by_payment_method') ?></h6>
<div style="height: 300px; position: relative;">
<canvas id="revenueByMethodChart"></canvas>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-md-6">
<div class="card p-4 h-100 border-0 shadow-sm" style="border-radius: 20px;">
<h6 class="fw-bold mb-4"><?= __('top_items') ?></h6>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr>
<th><?= __('item') ?></th>
<th class="text-center"><?= __('qty') ?></th>
<th class="text-end"><?= __('revenue') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($top_items as $item): ?>
<tr>
<td><?= is_arabic() ? $item['name_ar'] : $item['name_en'] ?></td>
<td class="text-center"><?= $item['total_qty'] ?></td>
<td class="text-end"><?= format_amount($item['total_revenue']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card p-4 h-100 border-0 shadow-sm" style="border-radius: 20px;">
<h6 class="fw-bold mb-4"><?= __('top_services') ?></h6>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr>
<th><?= __('service') ?></th>
<th class="text-center"><?= __('qty') ?></th>
<th class="text-end"><?= __('revenue') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($top_services as $service): ?>
<tr>
<td><?= is_arabic() ? $service['name_ar'] : $service['name_en'] ?></td>
<td class="text-center"><?= $service['total_qty'] ?></td>
<td class="text-end"><?= format_amount($service['total_revenue']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Set default chart options
Chart.defaults.font.size = 12;
Chart.defaults.responsive = true;
Chart.defaults.maintainAspectRatio = false;
// Revenue by Branch Chart
new Chart(document.getElementById('revenueByBranchChart'), {
type: 'bar',
data: {
labels: <?= json_encode(array_map(function($b) use ($lang) {
return $lang === 'ar' ? $b['name_ar'] : $b['name_en'];
}, $revenue_by_branch), JSON_UNESCAPED_UNICODE) ?>,
datasets: [{
label: '<?= __('revenue') ?>',
data: <?= json_encode(array_column($revenue_by_branch, 'revenue')) ?>,
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
scales: {
y: { beginAtZero: true }
}
}
});
// Orders by Status Chart
new Chart(document.getElementById('ordersByStatusChart'), {
type: 'doughnut',
data: {
labels: <?= json_encode(array_map(function($s) {
return __($s['status']);
}, $orders_by_status), JSON_UNESCAPED_UNICODE) ?>,
datasets: [{
data: <?= json_encode(array_column($orders_by_status, 'count')) ?>,
backgroundColor: [
'#6c757d', '#0d6efd', '#198754', '#212529', '#dc3545'
]
}]
},
options: {
plugins: {
legend: { position: 'bottom' }
}
}
});
// Revenue by Cashier Chart
new Chart(document.getElementById('revenueByCashierChart'), {
type: 'bar',
data: {
labels: <?= json_encode(array_map(function($u) use ($lang) {
return $lang === 'ar' ? $u['full_name_ar'] : $u['full_name_en'];
}, $revenue_by_user), JSON_UNESCAPED_UNICODE) ?>,
datasets: [{
label: '<?= __('revenue') ?>',
data: <?= json_encode(array_column($revenue_by_user, 'revenue')) ?>,
backgroundColor: 'rgba(75, 192, 192, 0.5)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 1
}]
},
options: {
scales: {
y: { beginAtZero: true }
}
}
});
// Revenue by Method Chart
new Chart(document.getElementById('revenueByMethodChart'), {
type: 'pie',
data: {
labels: <?= json_encode(array_map(function($m) {
return __($m['payment_method']);
}, $revenue_by_method), JSON_UNESCAPED_UNICODE) ?>,
datasets: [{
data: <?= json_encode(array_column($revenue_by_method, 'revenue')) ?>,
backgroundColor: [
'#ffc107', '#0dcaf0', '#6610f2'
]
}]
},
options: {
plugins: {
legend: { position: 'bottom' }
}
}
});
</script>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

145
service_rating.php Normal file
View File

@ -0,0 +1,145 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION["lang"]) && !isset($_GET["lang"])) {
$_SESSION["lang"] = "ar";
}
require_once __DIR__ . "/db/config.php";
require_once __DIR__ . '/includes/lang.php';
$success = false;
$error = '';
$branch_id_get = isset($_GET['branch_id']) ? (int)$_GET['branch_id'] : null;
$info = null;
if ($branch_id_get) {
$stmt = db()->prepare("SELECT b.id AS branch_id, b.name_en AS branch_name_en, b.name_ar AS branch_name_ar, c.id AS company_id, c.name_en AS comp_name_en, c.name_ar AS comp_name_ar, c.logo FROM branches b JOIN companies c ON b.company_id = c.id WHERE b.id = ?");
$stmt->execute([$branch_id_get]);
$info = $stmt->fetch(PDO::FETCH_ASSOC);
}
if (empty($info)) {
$stmt = db()->query("SELECT b.id AS branch_id, b.name_en AS branch_name_en, b.name_ar AS branch_name_ar, c.id AS company_id, c.name_en AS comp_name_en, c.name_ar AS comp_name_ar, c.logo FROM branches b JOIN companies c ON b.company_id = c.id LIMIT 1");
$info = $stmt->fetch(PDO::FETCH_ASSOC);
}
$branch_id = $info['branch_id'] ?? null;
$company_id = $info['company_id'] ?? null;
$company_name = $_SESSION['lang'] === 'ar' ? ($info['comp_name_ar'] ?? '') : ($info['comp_name_en'] ?? '');
$branch_name = $_SESSION['lang'] === 'ar' ? ($info['branch_name_ar'] ?? '') : ($info['branch_name_en'] ?? '');
$logo = $info['logo'] ?? null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$rating = (int)($_POST['rating'] ?? 0);
$comment = $_POST['comment'] ?? '';
if ($rating < 1 || $rating > 5) {
$error = __('provide_valid_rating');
} else {
$stmt = db()->prepare("INSERT INTO ratings (branch_id, company_id, rating_type, rating_value, comment) VALUES (?, ?, 'service', ?, ?)");
if ($stmt->execute([$branch_id, $company_id, $rating, $comment])) {
$success = true;
} else {
$error = __('error_saving_rating');
}
}
}
?>
<!DOCTYPE html>
<html lang="<?= $_SESSION['lang'] ?>" dir="<?= $_SESSION['lang'] === 'ar' ? 'rtl' : 'ltr' ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('service_rating') ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<style>
body { background-color: #f8f9fc; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; flex-direction: column; min-height: 100vh; }
.rating-card { max-width: 500px; margin: 40px auto; border-radius: 15px; border: none; box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15); padding: 30px; background: white; }
.stars { display: flex; flex-direction: row-reverse; justify-content: center; margin-bottom: 20px; }
.stars input { display: none; }
.stars label { font-size: 2.5rem; color: #ddd; cursor: pointer; transition: color 0.2s; padding: 0 5px; }
.stars label:hover, .stars label:hover ~ label, .stars input:checked ~ label { color: #f6c23e; }
.btn-submit { border-radius: 30px; padding: 10px 30px; font-weight: 600; }
.main-content { flex: 1; }
.footer { background-color: white; border-top: 1px solid #eaecf4; padding: 1.5rem 0; color: #858796; text-align: center; }
<?php if ($_SESSION['lang'] === 'ar'): ?>
.stars { flex-direction: row; }
.stars label:hover ~ label, .stars input:checked ~ label { color: #ddd; }
.stars label:hover, .stars label:hover ~ label, .stars input:checked, .stars input:checked ~ label { color: #f6c23e !important; }
<?php endif; ?>
</style>
</head>
<body>
<div class="main-content">
<div class="container py-5">
<div class="text-end mb-3">
<?php if ($_SESSION['lang'] === 'ar'): ?>
<a href="?lang=en<?= isset($_GET['branch_id']) ? '&branch_id=' . (int)$_GET['branch_id'] : '' ?>" class="text-decoration-none btn btn-sm btn-outline-secondary">English</a>
<?php else: ?>
<a href="?lang=ar<?= isset($_GET['branch_id']) ? '&branch_id=' . (int)$_GET['branch_id'] : '' ?>" class="text-decoration-none btn btn-sm btn-outline-secondary">العربية</a>
<?php endif; ?>
</div>
<div class="rating-card text-center">
<?php if ($logo): ?>
<img src="<?= htmlspecialchars($logo) ?>" alt="<?= htmlspecialchars($company_name) ?>" class="mb-3" style="max-height: 80px; max-width: 100%; object-fit: contain;">
<?php endif; ?>
<h4 class="mb-1 fw-bold"><?= htmlspecialchars($company_name) ?></h4>
<?php if ($branch_name): ?>
<p class="text-muted mb-4 small"><i class="bi bi-geo-alt-fill text-danger me-1"></i><?= htmlspecialchars($branch_name) ?></p>
<?php endif; ?>
<h2 class="mb-4 text-primary border-top pt-4"><?= __('service_rating') ?></h2>
<?php if ($success): ?>
<div class="alert alert-success">
<i class="bi bi-check-circle-fill"></i> <?= __('rating_submitted') ?>
</div>
<a href="service_rating.php<?= isset($_GET['branch_id']) ? '?branch_id=' . (int)$_GET['branch_id'] : '' ?>" class="btn btn-outline-primary mt-3"><?= __('submit_another') ?></a>
<?php else: ?>
<?php if ($error): ?>
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="POST">
<p class="text-muted mb-2"><?= __('how_rate_service') ?></p>
<div class="stars">
<input type="radio" id="star5" name="rating" value="5" required />
<label for="star5" title="5 stars"><i class="bi bi-star-fill"></i></label>
<input type="radio" id="star4" name="rating" value="4" />
<label for="star4" title="4 stars"><i class="bi bi-star-fill"></i></label>
<input type="radio" id="star3" name="rating" value="3" />
<label for="star3" title="3 stars"><i class="bi bi-star-fill"></i></label>
<input type="radio" id="star2" name="rating" value="2" />
<label for="star2" title="2 stars"><i class="bi bi-star-fill"></i></label>
<input type="radio" id="star1" name="rating" value="1" />
<label for="star1" title="1 star"><i class="bi bi-star-fill"></i></label>
</div>
<div class="mb-4 text-start">
<label for="comment" class="form-label"><?= __('comment') ?> (<?= __('optional') ?>)</label>
<textarea class="form-control" id="comment" name="comment" rows="3" placeholder="<?= __('comment_placeholder') ?>"></textarea>
</div>
<button type="submit" class="btn btn-primary btn-submit w-100"><?= __('submit_rating') ?></button>
</form>
<?php endif; ?>
</div>
</div>
</div>
<footer class="footer">
<div class="container">
<span>&copy; <?= date('Y') ?> <?= htmlspecialchars($company_name) ?>. <?= __('all_rights_reserved') ?></span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

145
staff_rating.php Normal file
View File

@ -0,0 +1,145 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION["lang"]) && !isset($_GET["lang"])) {
$_SESSION["lang"] = "ar";
}
require_once __DIR__ . "/db/config.php";
require_once __DIR__ . '/includes/lang.php';
$success = false;
$error = '';
$branch_id_get = isset($_GET['branch_id']) ? (int)$_GET['branch_id'] : null;
$info = null;
if ($branch_id_get) {
$stmt = db()->prepare("SELECT b.id AS branch_id, b.name_en AS branch_name_en, b.name_ar AS branch_name_ar, c.id AS company_id, c.name_en AS comp_name_en, c.name_ar AS comp_name_ar, c.logo FROM branches b JOIN companies c ON b.company_id = c.id WHERE b.id = ?");
$stmt->execute([$branch_id_get]);
$info = $stmt->fetch(PDO::FETCH_ASSOC);
}
if (empty($info)) {
$stmt = db()->query("SELECT b.id AS branch_id, b.name_en AS branch_name_en, b.name_ar AS branch_name_ar, c.id AS company_id, c.name_en AS comp_name_en, c.name_ar AS comp_name_ar, c.logo FROM branches b JOIN companies c ON b.company_id = c.id LIMIT 1");
$info = $stmt->fetch(PDO::FETCH_ASSOC);
}
$branch_id = $info['branch_id'] ?? null;
$company_id = $info['company_id'] ?? null;
$company_name = $_SESSION['lang'] === 'ar' ? ($info['comp_name_ar'] ?? '') : ($info['comp_name_en'] ?? '');
$branch_name = $_SESSION['lang'] === 'ar' ? ($info['branch_name_ar'] ?? '') : ($info['branch_name_en'] ?? '');
$logo = $info['logo'] ?? null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$rating = (int)($_POST['rating'] ?? 0);
$comment = $_POST['comment'] ?? '';
if ($rating < 1 || $rating > 5) {
$error = __('provide_valid_rating');
} else {
$stmt = db()->prepare("INSERT INTO ratings (branch_id, company_id, rating_type, rating_value, comment) VALUES (?, ?, 'staff', ?, ?)");
if ($stmt->execute([$branch_id, $company_id, $rating, $comment])) {
$success = true;
} else {
$error = __('error_saving_rating');
}
}
}
?>
<!DOCTYPE html>
<html lang="<?= $_SESSION['lang'] ?>" dir="<?= $_SESSION['lang'] === 'ar' ? 'rtl' : 'ltr' ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('staff_rating') ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<style>
body { background-color: #f8f9fc; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; flex-direction: column; min-height: 100vh; }
.rating-card { max-width: 500px; margin: 40px auto; border-radius: 15px; border: none; box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15); padding: 30px; background: white; }
.stars { display: flex; flex-direction: row-reverse; justify-content: center; margin-bottom: 20px; }
.stars input { display: none; }
.stars label { font-size: 2.5rem; color: #ddd; cursor: pointer; transition: color 0.2s; padding: 0 5px; }
.stars label:hover, .stars label:hover ~ label, .stars input:checked ~ label { color: #f6c23e; }
.btn-submit { border-radius: 30px; padding: 10px 30px; font-weight: 600; }
.main-content { flex: 1; }
.footer { background-color: white; border-top: 1px solid #eaecf4; padding: 1.5rem 0; color: #858796; text-align: center; }
<?php if ($_SESSION['lang'] === 'ar'): ?>
.stars { flex-direction: row; } /* RTL fix for stars order */
.stars label:hover ~ label, .stars input:checked ~ label { color: #ddd; }
.stars label:hover, .stars label:hover ~ label, .stars input:checked, .stars input:checked ~ label { color: #f6c23e !important; }
<?php endif; ?>
</style>
</head>
<body>
<div class="main-content">
<div class="container py-5">
<div class="text-end mb-3">
<?php if ($_SESSION['lang'] === 'ar'): ?>
<a href="?lang=en<?= isset($_GET['branch_id']) ? '&branch_id=' . (int)$_GET['branch_id'] : '' ?>" class="text-decoration-none btn btn-sm btn-outline-secondary">English</a>
<?php else: ?>
<a href="?lang=ar<?= isset($_GET['branch_id']) ? '&branch_id=' . (int)$_GET['branch_id'] : '' ?>" class="text-decoration-none btn btn-sm btn-outline-secondary">العربية</a>
<?php endif; ?>
</div>
<div class="rating-card text-center">
<?php if ($logo): ?>
<img src="<?= htmlspecialchars($logo) ?>" alt="<?= htmlspecialchars($company_name) ?>" class="mb-3" style="max-height: 80px; max-width: 100%; object-fit: contain;">
<?php endif; ?>
<h4 class="mb-1 fw-bold"><?= htmlspecialchars($company_name) ?></h4>
<?php if ($branch_name): ?>
<p class="text-muted mb-4 small"><i class="bi bi-geo-alt-fill text-danger me-1"></i><?= htmlspecialchars($branch_name) ?></p>
<?php endif; ?>
<h2 class="mb-4 text-primary border-top pt-4"><?= __('staff_rating') ?></h2>
<?php if ($success): ?>
<div class="alert alert-success">
<i class="bi bi-check-circle-fill"></i> <?= __('rating_submitted') ?>
</div>
<a href="staff_rating.php<?= isset($_GET['branch_id']) ? '?branch_id=' . (int)$_GET['branch_id'] : '' ?>" class="btn btn-outline-primary mt-3"><?= __('submit_another') ?></a>
<?php else: ?>
<?php if ($error): ?>
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="POST">
<p class="text-muted mb-2"><?= __('how_rate_staff') ?></p>
<div class="stars">
<input type="radio" id="star5" name="rating" value="5" required />
<label for="star5" title="5 stars"><i class="bi bi-star-fill"></i></label>
<input type="radio" id="star4" name="rating" value="4" />
<label for="star4" title="4 stars"><i class="bi bi-star-fill"></i></label>
<input type="radio" id="star3" name="rating" value="3" />
<label for="star3" title="3 stars"><i class="bi bi-star-fill"></i></label>
<input type="radio" id="star2" name="rating" value="2" />
<label for="star2" title="2 stars"><i class="bi bi-star-fill"></i></label>
<input type="radio" id="star1" name="rating" value="1" />
<label for="star1" title="1 star"><i class="bi bi-star-fill"></i></label>
</div>
<div class="mb-4 text-start">
<label for="comment" class="form-label"><?= __('comment') ?> (<?= __('optional') ?>)</label>
<textarea class="form-control" id="comment" name="comment" rows="3" placeholder="<?= __('comment_placeholder') ?>"></textarea>
</div>
<button type="submit" class="btn btn-primary btn-submit w-100"><?= __('submit_rating') ?></button>
</form>
<?php endif; ?>
</div>
</div>
</div>
<footer class="footer">
<div class="container">
<span>&copy; <?= date('Y') ?> <?= htmlspecialchars($company_name) ?>. <?= __('all_rights_reserved') ?></span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

135
used_keys.txt Normal file
View File

@ -0,0 +1,135 @@
Access Denied
actions
active_orders
add_branch
add_item
add_new
add_new_branch
add_new_category
add_new_customer
add_new_service
add_new_user
add_payment
add_user
address_ar
address_en
all_branches
all_payments
all_status
amount
are_you_sure
branch
branches
cancel
cancelled
card
cash
categories
categories_management
category
change_password
close
company
company_profile
confirm_password
ctr_no
currency
customer
customer_details
customer_name
customers
customers_list
dashboard
date
delete
delivered
edit
edit_category
edit_item
edit_service
email
error_update
exactly_3_letters
favicon
from_date
full_name_ar
full_name_en
image
initial_letters
item
items
items_management
lab
language
login
logo
logout
manage_orders_across_outlets
method
name
name_ar
name_en
new_customers
new_order
next
no_orders_found
no_payments
order
order_date
order_number
orders
orders_list
outlet
paid
partially_paid
password
payment_method
payment_status
payments
phone
pos
previous
price
pricing_services
print_invoice
processing
profile_picture
quantity
quick_actions
ready
ready_orders
received
recent_orders
remaining_amount
reports
role
save
save_changes
search
search_placeholder
select_category
service
service_pricing
services
services_management
status
subtotal
success_update
thermal_receipt
to_date
today_revenue
total
transfer
unpaid
update
update_profile
update_status
user_profile
username
users
vat
vat_no
vat_percent
vat_total
view
view_items

465
users.php Normal file
View File

@ -0,0 +1,465 @@
<?php
// ACTION HANDLING FIRST
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/lang.php';
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
// Permission check for users.php
if (!has_permission('view', 'users.php')) {
header('Location: admin.php');
exit;
}
$manageable_pages = [
'admin.php' => 'dashboard',
'pos.php' => 'pos',
'orders.php' => 'orders',
'lab.php' => 'lab',
'customers.php' => 'customers',
'customer_statement.php' => 'customer_statement',
'reports.php' => 'reports',
'items.php' => 'items',
'branches.php' => 'branches',
'users.php' => 'users',
'profile.php' => 'user_profile',
'company_profile.php' => 'company_profile',
'order_details.php' => 'order_details',
'receipt.php' => 'receipt',
'ratings.php' => 'ratings'
];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if ($_POST['action'] === 'add_user' && has_permission('add', 'users.php')) {
$username = $_POST['username'];
$password = password_hash($_POST['password'], PASSWORD_DEFAULT);
$full_name_en = $_POST['full_name_en'];
$full_name_ar = $_POST['full_name_ar'];
$role = $_POST['role'];
$branch_ids = $_POST['branch_ids'] ?? [];
$company_id = 1; // Default for now
$primary_branch_id = !empty($branch_ids) ? $branch_ids[0] : null;
$stmt = db()->prepare("INSERT INTO users (username, password_hash, full_name_en, full_name_ar, role, branch_id, company_id) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$username, $password, $full_name_en, $full_name_ar, $role, $primary_branch_id, $company_id]);
$new_user_id = db()->lastInsertId();
// Sync branches
foreach ($branch_ids as $bid) {
$stmt = db()->prepare("INSERT INTO user_branches (user_id, branch_id) VALUES (?, ?)");
$stmt->execute([$new_user_id, $bid]);
}
// Seed default permissions based on role for the new user
$default_pages = [
'super_admin' => array_keys($manageable_pages),
'branch_manager' => ['admin.php', 'pos.php', 'orders.php', 'lab.php', 'customers.php', 'customer_statement.php', 'reports.php', 'items.php', 'branches.php', 'profile.php', 'order_details.php', 'receipt.php'],
'cashier' => ['admin.php', 'pos.php', 'orders.php', 'lab.php', 'customers.php', 'customer_statement.php', 'profile.php', 'order_details.php', 'receipt.php'],
'limited_viewer' => ['admin.php', 'profile.php']
];
$pages = $default_pages[$role] ?? [];
foreach ($pages as $p) {
$can_add_edit_del = ($role !== 'limited_viewer' ? 1 : 0);
$stmt = db()->prepare("INSERT INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete) VALUES (?, ?, 1, ?, ?, ?)");
$stmt->execute([$new_user_id, $p, $can_add_edit_del, $can_add_edit_del, $can_add_edit_del]);
}
header('Location: users.php');
exit;
}
if ($_POST['action'] === 'edit_user' && has_permission('edit', 'users.php')) {
$id = $_POST['id'];
$username = $_POST['username'];
$full_name_en = $_POST['full_name_en'];
$full_name_ar = $_POST['full_name_ar'];
$role = $_POST['role'];
$branch_ids = $_POST['branch_ids'] ?? [];
$primary_branch_id = !empty($branch_ids) ? $branch_ids[0] : null;
if (!empty($_POST['password'])) {
$password = password_hash($_POST['password'], PASSWORD_DEFAULT);
$stmt = db()->prepare("UPDATE users SET username = ?, password_hash = ?, full_name_en = ?, full_name_ar = ?, role = ?, branch_id = ? WHERE id = ?");
$stmt->execute([$username, $password, $full_name_en, $full_name_ar, $role, $primary_branch_id, $id]);
} else {
$stmt = db()->prepare("UPDATE users SET username = ?, full_name_en = ?, full_name_ar = ?, role = ?, branch_id = ? WHERE id = ?");
$stmt->execute([$username, $full_name_en, $full_name_ar, $role, $primary_branch_id, $id]);
}
// Sync branches
$stmt = db()->prepare("DELETE FROM user_branches WHERE user_id = ?");
$stmt->execute([$id]);
foreach ($branch_ids as $bid) {
$stmt = db()->prepare("INSERT INTO user_branches (user_id, branch_id) VALUES (?, ?)");
$stmt->execute([$id, $bid]);
}
header('Location: users.php?msg=user_updated');
exit;
}
if ($_POST['action'] === 'delete_user' && has_permission('delete', 'users.php')) {
$id = $_POST['id'];
// Prevent self-deletion
if ($id == $_SESSION['user_id']) {
header('Location: users.php?error=cannot_delete_self');
exit;
}
$stmt = db()->prepare("DELETE FROM users WHERE id = ?");
$stmt->execute([$id]);
header('Location: users.php?msg=user_deleted');
exit;
}
if ($_POST['action'] === 'update_permissions' && has_permission('edit', 'users.php')) {
$target_user_id = $_POST['user_id'];
$permissions = $_POST['perms'] ?? []; // format: [page][action] = 1
// Clear existing permissions for this user
$stmt = db()->prepare("DELETE FROM user_permissions WHERE user_id = ?");
$stmt->execute([$target_user_id]);
$all_pages_to_save = array_keys($manageable_pages);
foreach ($all_pages_to_save as $page) {
$can_view = isset($permissions[$page]['view']) ? 1 : 0;
$can_add = isset($permissions[$page]['add']) ? 1 : 0;
$can_edit = isset($permissions[$page]['edit']) ? 1 : 0;
$can_delete = isset($permissions[$page]['delete']) ? 1 : 0;
if ($can_view || $can_add || $can_edit || $can_delete) {
$stmt = db()->prepare("INSERT INTO user_permissions (user_id, page, can_view, can_add, can_edit, can_delete) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$target_user_id, $page, $can_view, $can_add, $can_edit, $can_delete]);
}
}
header('Location: users.php?msg=permissions_updated');
exit;
}
}
// NOW Include header
$title = 'users';
require_once __DIR__ . '/includes/header.php';
$users = db()->query("SELECT u.* FROM users u")->fetchAll();
$branches = db()->query("SELECT * FROM branches")->fetchAll();
// Fetch branches for all users in one go for efficiency
$user_branches_mapping = [];
$ub_stmt = db()->query("SELECT ub.user_id, b.name_en, b.name_ar FROM user_branches ub JOIN branches b ON ub.branch_id = b.id");
while ($row = $ub_stmt->fetch()) {
$user_branches_mapping[$row['user_id']][] = $lang === 'ar' ? $row['name_ar'] : $row['name_en'];
}
// Also need branch IDs for edit modal
$user_branch_ids_mapping = [];
$ub_id_stmt = db()->query("SELECT user_id, branch_id FROM user_branches");
while ($row = $ub_id_stmt->fetch()) {
$user_branch_ids_mapping[$row['user_id']][] = (int)$row['branch_id'];
}
?>
<div class="row">
<?php if (has_permission('add', 'users.php')): ?>
<div class="col-md-4">
<div class="card p-4 border-0 shadow-sm mb-4" style="border-radius: 20px;">
<h5 class="fw-bold mb-4"><?= __('add_new_user') ?? 'Add New User' ?></h5>
<form method="POST">
<input type="hidden" name="action" value="add_user">
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('username') ?></label>
<input type="text" name="username" class="form-control" required style="border-radius: 12px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('password') ?></label>
<input type="password" name="password" class="form-control" required style="border-radius: 12px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('full_name_en') ?></label>
<input type="text" name="full_name_en" class="form-control" required style="border-radius: 12px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('full_name_ar') ?></label>
<input type="text" name="full_name_ar" class="form-control" style="border-radius: 12px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('role') ?></label>
<select name="role" class="form-select" style="border-radius: 12px;">
<option value="super_admin">Super Admin</option>
<option value="branch_manager">Branch Manager</option>
<option value="cashier" selected>Cashier</option>
<option value="limited_viewer">Limited Viewer</option>
</select>
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('branches') ?? 'Branches' ?></label>
<div class="p-3 border rounded" style="max-height: 150px; overflow-y: auto; border-radius: 12px !important;">
<?php foreach($branches as $b): ?>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="branch_ids[]" value="<?= $b['id'] ?>" id="add_b_<?= $b['id'] ?>">
<label class="form-check-label" for="add_b_<?= $b['id'] ?>">
<?= $lang === 'ar' ? $b['name_ar'] : $b['name_en'] ?>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 py-3 fw-bold" style="border-radius: 15px;"><?= __('add_user') ?></button>
</form>
</div>
</div>
<div class="col-md-8">
<?php else: ?>
<div class="col-md-12">
<?php endif; ?>
<div class="card p-0 border-0 shadow-sm" style="border-radius: 20px; overflow: hidden;">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4 py-3">#</th>
<th class="py-3"><?= __('username') ?></th>
<th class="py-3"><?= __('name') ?></th>
<th class="py-3"><?= __('role') ?></th>
<th class="py-3"><?= __('branches') ?? 'Branches' ?></th>
<th class="pe-4 py-3 text-end"><?= __('actions') ?></th>
</tr>
</thead>
<tbody>
<?php foreach($users as $u): ?>
<tr>
<td class="ps-4"><?= $u['id'] ?></td>
<td><?= $u['username'] ?></td>
<td class="fw-bold"><?= $lang === 'ar' ? ($u['full_name_ar'] ?: $u['full_name_en']) : $u['full_name_en'] ?></td>
<td>
<span class="badge bg-soft-info text-info px-3 py-2" style="border-radius: 8px;">
<?= $u['role'] ?>
</span>
</td>
<td>
<?php if (isset($user_branches_mapping[$u['id']])): ?>
<?php foreach ($user_branches_mapping[$u['id']] as $bname): ?>
<span class="badge bg-light text-dark border me-1"><?= $bname ?></span>
<?php endforeach; ?>
<?php else: ?>
-
<?php endif; ?>
</td>
<td class="pe-4 text-end">
<div class="btn-group">
<?php if (has_permission('edit', 'users.php')): ?>
<button class="btn btn-sm btn-outline-primary px-3" onclick="openPermissions(<?= $u['id'] ?>, '<?= htmlspecialchars($u['username']) ?>')" style="border-radius: 8px 0 0 8px;" title="<?= __('permissions') ?>">
<i class="bi bi-shield-lock"></i>
</button>
<button class="btn btn-sm btn-outline-info px-3" onclick="editUser(<?= htmlspecialchars(json_encode($u)) ?>, <?= htmlspecialchars(json_encode($user_branch_ids_mapping[$u['id']] ?? [])) ?>)" style="border-radius: 0;" title="<?= __('edit') ?>">
<i class="bi bi-pencil-square"></i>
</button>
<?php endif; ?>
<?php if (has_permission('delete', 'users.php')): ?>
<button class="btn btn-sm btn-outline-danger px-3" onclick="deleteUser(<?= $u['id'] ?>)" style="border-radius: 0 8px 8px 0;" title="<?= __('delete') ?>" <?= $u['id'] == $_SESSION['user_id'] ? 'disabled' : '' ?>>
<i class="bi bi-trash"></i>
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Edit User Modal -->
<div class="modal fade" id="editUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content" style="border-radius: 20px;">
<div class="modal-header border-0 p-4 pb-0">
<h5 class="modal-title fw-bold"><?= __('edit_user') ?? 'Edit User' ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<input type="hidden" name="action" value="edit_user">
<input type="hidden" name="id" id="editUserId">
<div class="modal-body p-4">
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('username') ?></label>
<input type="text" name="username" id="editUsername" class="form-control" required style="border-radius: 12px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('password') ?> (<?= __('leave_blank_to_keep_current') ?? 'leave blank to keep current' ?>)</label>
<input type="password" name="password" class="form-control" style="border-radius: 12px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('full_name_en') ?></label>
<input type="text" name="full_name_en" id="editFullNameEn" class="form-control" required style="border-radius: 12px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('full_name_ar') ?></label>
<input type="text" name="full_name_ar" id="editFullNameAr" class="form-control" style="border-radius: 12px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('role') ?></label>
<select name="role" id="editRole" class="form-select" style="border-radius: 12px;">
<option value="super_admin">Super Admin</option>
<option value="branch_manager">Branch Manager</option>
<option value="cashier">Cashier</option>
<option value="limited_viewer">Limited Viewer</option>
</select>
</div>
<div class="mb-3">
<label class="form-label small fw-bold"><?= __('branches') ?? 'Branches' ?></label>
<div class="p-3 border rounded" style="max-height: 150px; overflow-y: auto; border-radius: 12px !important;">
<?php foreach($branches as $b): ?>
<div class="form-check">
<input class="form-check-input branch-checkbox" type="checkbox" name="branch_ids[]" value="<?= $b['id'] ?>" id="edit_b_<?= $b['id'] ?>">
<label class="form-check-label" for="edit_b_<?= $b['id'] ?>">
<?= $lang === 'ar' ? $b['name_ar'] : $b['name_en'] ?>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light px-4" data-bs-dismiss="modal" style="border-radius: 12px;"><?= __('cancel') ?></button>
<button type="submit" class="btn btn-primary px-4" style="border-radius: 12px;"><?= __('save_changes') ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirm Modal -->
<form id="deleteForm" method="POST" style="display:none;">
<input type="hidden" name="action" value="delete_user">
<input type="hidden" name="id" id="deleteUserId">
</form>
<!-- Permissions Modal -->
<div class="modal fade" id="permissionsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content" style="border-radius: 20px;">
<div class="modal-header border-0 p-4 pb-0">
<h5 class="modal-title fw-bold"><i class="bi bi-shield-lock me-2"></i> <?= __('user_permissions') ?? 'User Permissions' ?>: <span id="permsUsername"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<input type="hidden" name="action" value="update_permissions">
<input type="hidden" name="user_id" id="permsUserId">
<div class="modal-body p-4">
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr>
<th><?= __('page') ?></th>
<th class="text-center"><?= __('view') ?></th>
<th class="text-center"><?= __('add') ?></th>
<th class="text-center"><?= __('edit') ?></th>
<th class="text-center"><?= __('delete') ?></th>
</tr>
</thead>
<tbody id="permsTableBody">
<?php foreach($manageable_pages as $file => $label): ?>
<tr>
<td class="fw-bold small"><?= __($label) ?></td>
<td class="text-center">
<input class="form-check-input" type="checkbox" name="perms[<?= $file ?>][view]" id="v_<?= str_replace('.', '_', $file) ?>">
</td>
<td class="text-center">
<input class="form-check-input" type="checkbox" name="perms[<?= $file ?>][add]" id="a_<?= str_replace('.', '_', $file) ?>">
</td>
<td class="text-center">
<input class="form-check-input" type="checkbox" name="perms[<?= $file ?>][edit]" id="e_<?= str_replace('.', '_', $file) ?>">
</td>
<td class="text-center">
<input class="form-check-input" type="checkbox" name="perms[<?= $file ?>][delete]" id="d_<?= str_replace('.', '_', $file) ?>">
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light px-4" data-bs-dismiss="modal" style="border-radius: 12px;"><?= __('cancel') ?></button>
<button type="submit" class="btn btn-primary px-4" style="border-radius: 12px;"><?= __('save_changes') ?></button>
</div>
</form>
</div>
</div>
</div>
<script>
function openPermissions(userId, username) {
document.getElementById('permsUserId').value = userId;
document.getElementById('permsUsername').innerText = username;
// Reset all checkboxes
document.querySelectorAll('#permsTableBody input[type="checkbox"]').forEach(cb => cb.checked = false);
// Fetch current permissions via AJAX
fetch('api/get_user_permissions.php?user_id=' + userId)
.then(response => response.json())
.then(data => {
if (data && Array.isArray(data)) {
data.forEach(p => {
const pageId = p.page.replace('.', '_');
const v = document.getElementById('v_' + pageId);
const a = document.getElementById('a_' + pageId);
const e = document.getElementById('e_' + pageId);
const d = document.getElementById('d_' + pageId);
if (v && p.can_view == 1) v.checked = true;
if (a && p.can_add == 1) a.checked = true;
if (e && p.can_edit == 1) e.checked = true;
if (d && p.can_delete == 1) d.checked = true;
});
}
const modal = new bootstrap.Modal(document.getElementById('permissionsModal'));
modal.show();
})
.catch(err => {
console.error('Error fetching permissions:', err);
alert('Failed to load permissions. Please try again.');
});
}
function editUser(user, branchIds) {
document.getElementById('editUserId').value = user.id;
document.getElementById('editUsername').value = user.username;
document.getElementById('editFullNameEn').value = user.full_name_en;
document.getElementById('editFullNameAr').value = user.full_name_ar || '';
document.getElementById('editRole').value = user.role;
// Reset and set checkboxes
document.querySelectorAll('.branch-checkbox').forEach(cb => {
cb.checked = branchIds.includes(parseInt(cb.value));
});
const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
modal.show();
}
function deleteUser(id) {
if (confirm('<?= __('confirm_delete_user') ?? 'Are you sure you want to delete this user?' ?>')) {
document.getElementById('deleteUserId').value = id;
document.getElementById('deleteForm').submit();
}
}
</script>
<style>
.bg-soft-info { background-color: rgba(13, 202, 240, 0.1); }
</style>
<?php require_once __DIR__ . '/includes/footer.php'; ?>