Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f040f6eca | ||
|
|
c441d54a7a | ||
|
|
c8388831c1 | ||
|
|
c6d83786d4 | ||
|
|
df53811d22 | ||
|
|
b1bb8c3b26 | ||
|
|
d76a592792 | ||
|
|
595c9b31cf | ||
|
|
52be768699 | ||
|
|
9f43a69a1d | ||
|
|
5f8d3b1aed | ||
|
|
57a6e7ed99 | ||
|
|
84add80342 | ||
|
|
f8ccd73ba6 | ||
|
|
f08a6b75bb | ||
|
|
9060a6d586 | ||
|
|
35d8fe23f8 | ||
|
|
a7d442be5d | ||
|
|
c34a6ddf37 | ||
|
|
a2fc645f85 | ||
|
|
0b22cc2f02 | ||
|
|
a251e37178 | ||
|
|
f49527d7c4 | ||
|
|
9a5d45c4c5 | ||
|
|
e135bdd9e0 | ||
|
|
538704f9d0 | ||
|
|
1de3f1d7d1 | ||
|
|
ec6f16b511 | ||
|
|
2a16f1f83e | ||
|
|
d2a2461bda | ||
|
|
e9ceb4546d | ||
|
|
78166012df | ||
|
|
2a38c2ffdb | ||
|
|
275baf63cd | ||
|
|
3f1e18552b | ||
|
|
2807096c56 | ||
|
|
268b4cc1cf | ||
|
|
62b332799d | ||
|
|
ab23c4540e | ||
|
|
874d750960 |
402
admin.php
402
admin.php
@ -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';
|
||||||
|
?>
|
||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
37
api/add_customer.php
Normal file
37
api/add_customer.php
Normal 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()]);
|
||||||
|
}
|
||||||
24
api/add_customer_redirect.php
Normal file
24
api/add_customer_redirect.php
Normal 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
170
api/checkout.php
Normal 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
31
api/delete_order.php
Normal 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
35
api/get_order_items.php
Normal 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()]);
|
||||||
|
}
|
||||||
21
api/get_user_permissions.php
Normal file
21
api/get_user_permissions.php
Normal 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
25
api/search_customers.php
Normal 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
42
api/translate.php
Normal 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()]);
|
||||||
|
}
|
||||||
|
|
||||||
64
api/update_order_status.php
Normal file
64
api/update_order_status.php
Normal 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
39
api/update_setting.php
Normal 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()]);
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BIN
assets/images/items/item_69a567f77a8364.03372918.jpeg
Normal file
BIN
assets/images/items/item_69a567f77a8364.03372918.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
assets/images/items/item_69a5695c9ca064.49414552.jpg
Normal file
BIN
assets/images/items/item_69a5695c9ca064.49414552.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
BIN
assets/images/items/item_69a5697ac55c21.80460234.jpg
Normal file
BIN
assets/images/items/item_69a5697ac55c21.80460234.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
assets/images/items/item_69a569a491f0e1.37331888.jpg
Normal file
BIN
assets/images/items/item_69a569a491f0e1.37331888.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
BIN
assets/images/users/user_1_1772520504.jfif
Normal file
BIN
assets/images/users/user_1_1772520504.jfif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
@ -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
255
branches.php
Normal 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
307
company_profile.php
Normal 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
5
cookies.txt
Normal 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
194
customer_statement.php
Normal 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
353
customers.php
Normal 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'; ?>
|
||||||
120
db/config.php
120
db/config.php
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
129
db/migrations/01_initial_schema.sql
Normal file
129
db/migrations/01_initial_schema.sql
Normal 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);
|
||||||
37
db/migrations/02_update_items.sql
Normal file
37
db/migrations/02_update_items.sql
Normal 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', 'ملاءات السرير');
|
||||||
7
db/migrations/03_update_orders.sql
Normal file
7
db/migrations/03_update_orders.sql
Normal 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;
|
||||||
3
db/migrations/04_fix_prices_key.sql
Normal file
3
db/migrations/04_fix_prices_key.sql
Normal 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);
|
||||||
5
db/migrations/05_simplify_prices_index.sql
Normal file
5
db/migrations/05_simplify_prices_index.sql
Normal 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);
|
||||||
13
db/migrations/06_profile_additions.sql
Normal file
13
db/migrations/06_profile_additions.sql
Normal 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;
|
||||||
8
db/migrations/07_update_currency_precision.sql
Normal file
8
db/migrations/07_update_currency_precision.sql
Normal 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;
|
||||||
6
db/migrations/08_add_ctr_vat_no.sql
Normal file
6
db/migrations/08_add_ctr_vat_no.sql
Normal 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;
|
||||||
2
db/migrations/09_add_branch_prefix.sql
Normal file
2
db/migrations/09_add_branch_prefix.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Migration 09: Add prefix column to branches table
|
||||||
|
ALTER TABLE branches ADD COLUMN prefix VARCHAR(3) DEFAULT NULL;
|
||||||
22
db/migrations/10_make_shared_entities.sql
Normal file
22
db/migrations/10_make_shared_entities.sql
Normal 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;
|
||||||
1
db/migrations/11_add_limited_viewer_role.sql
Normal file
1
db/migrations/11_add_limited_viewer_role.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users MODIFY COLUMN role ENUM('super_admin', 'branch_manager', 'cashier', 'limited_viewer') DEFAULT 'cashier';
|
||||||
55
db/migrations/12_page_based_permissions.sql
Normal file
55
db/migrations/12_page_based_permissions.sql
Normal 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';
|
||||||
14
db/migrations/13_multiple_user_branches.sql
Normal file
14
db/migrations/13_multiple_user_branches.sql
Normal 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
|
||||||
14
db/migrations/14_add_whatsapp_settings.sql
Normal file
14
db/migrations/14_add_whatsapp_settings.sql
Normal 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}. شكراً لتعاملك معنا.');
|
||||||
6
db/migrations/15_add_customer_statement_permission.sql
Normal file
6
db/migrations/15_add_customer_statement_permission.sql
Normal 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);
|
||||||
22
db/migrations/16_add_loyalty_system.sql
Normal file
22
db/migrations/16_add_loyalty_system.sql
Normal 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)
|
||||||
2
db/migrations/17_add_loyalty_discount_to_orders.sql
Normal file
2
db/migrations/17_add_loyalty_discount_to_orders.sql
Normal 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;
|
||||||
3
db/migrations/18_add_is_deleted_to_items.sql
Normal file
3
db/migrations/18_add_is_deleted_to_items.sql
Normal 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;
|
||||||
15
db/migrations/19_add_ratings.sql
Normal file
15
db/migrations/19_add_ratings.sql
Normal 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
280
defined_keys.txt
Normal 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
39
includes/dotenv.php
Normal 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
81
includes/footer.php
Normal 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
435
includes/header.php
Normal 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
613
includes/lang.php
Normal 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
54
includes/whatsapp.php
Normal 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'];
|
||||||
|
}
|
||||||
57
index.php
57
index.php
@ -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
306
install.php
Normal 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
984
items.php
Normal 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
346
lab.php
Normal 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
127
login.php
Normal 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
5
logout.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
session_destroy();
|
||||||
|
header('Location: login.php');
|
||||||
|
exit;
|
||||||
@ -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');
|
||||||
|
|||||||
297
order_details.php
Normal file
297
order_details.php
Normal 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
339
orders.php
Normal 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
420
pos.js
Normal 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
826
pos.php
Normal 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
242
profile.php
Normal 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
130
rate.php
Normal 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>© <?= 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
347
ratings.php
Normal 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
217
receipt.php
Normal 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
513
reports.php
Normal 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
145
service_rating.php
Normal 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>© <?= 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
145
staff_rating.php
Normal 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>© <?= 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
135
used_keys.txt
Normal 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
465
users.php
Normal 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'; ?>
|
||||||
Loading…
x
Reference in New Issue
Block a user