Compare commits

...

114 Commits

Author SHA1 Message Date
Flatlogic Bot
2ae728c351 update products 2026-03-28 18:27:57 +00:00
Flatlogic Bot
ff9ab72c0b adding timer to online order 2026-03-28 18:12:49 +00:00
Flatlogic Bot
5b05c65a86 Autosave: 20260328-025310 2026-03-28 02:53:10 +00:00
Flatlogic Bot
29a1863608 update message summery report 2026-03-27 02:41:38 +00:00
Flatlogic Bot
241682d60b update 0 summery result 2026-03-26 05:27:23 +00:00
Flatlogic Bot
07fa5eff07 update daily_report 2026-03-25 02:20:23 +00:00
Flatlogic Bot
f122b78692 Autosave: 20260325-020536 2026-03-25 02:05:36 +00:00
Flatlogic Bot
cc7015afbe updating summery report 2026-03-24 13:17:34 +00:00
Flatlogic Bot
c5ebc795f7 updating reports 2026-03-24 12:52:05 +00:00
Flatlogic Bot
0d0f7b2fd8 update message 2 2026-03-20 09:48:45 +00:00
Flatlogic Bot
60d2f356c7 update summery report 2026-03-20 06:38:22 +00:00
Flatlogic Bot
2130c9f23a updating sending summery more than one number 2026-03-19 09:46:40 +00:00
Flatlogic Bot
9d29d48375 update daily reports to insure all branches includ 2026-03-15 05:14:18 +00:00
Flatlogic Bot
391d782761 update logo 2026-03-15 03:42:34 +00:00
Flatlogic Bot
22ba99cda2 add logo to reciept 2026-03-15 03:18:03 +00:00
Flatlogic Bot
6ff90fea7b changing layout 2026-03-15 02:34:33 +00:00
Flatlogic Bot
c1cd5b183c adding tabs to settings 2026-03-14 19:07:03 +00:00
Flatlogic Bot
030b415fea add timezone 2026-03-14 18:51:26 +00:00
Flatlogic Bot
cbac17cdd4 editing admin/rating 2026-03-10 07:51:01 +00:00
Flatlogic Bot
9248543cfe fix pos layout 2026-03-09 05:25:57 +00:00
Flatlogic Bot
6be2e6d02b modifying pos 2026-03-07 10:21:00 +00:00
Flatlogic Bot
4f4b85539c adding customer messages 2026-03-06 18:22:23 +00:00
Flatlogic Bot
d229f476df adding cashiers sales report 2026-03-02 05:44:45 +00:00
Flatlogic Bot
1aec2c17d6 show products in qr order 2026-02-28 17:39:42 +00:00
Flatlogic Bot
aa6cc744d0 check for pos 2026-02-28 17:25:51 +00:00
Flatlogic Bot
2772970659 printing error 2026-02-28 13:37:08 +00:00
Flatlogic Bot
b0479e299c improving printing 2 2026-02-28 13:25:47 +00:00
Flatlogic Bot
5deb44b9f8 improving printing 2026-02-28 13:15:53 +00:00
Flatlogic Bot
26095df612 editing printing 2026-02-28 12:59:44 +00:00
Flatlogic Bot
c451a24982 enhancing printing 2026-02-27 13:17:35 +00:00
Flatlogic Bot
9f56d9b0a2 fixing report 2026-02-27 12:47:22 +00:00
Flatlogic Bot
c24fdab770 update reports 2026-02-27 11:51:59 +00:00
Flatlogic Bot
d4f9a4496f fix report 2026-02-27 10:25:36 +00:00
Flatlogic Bot
4ea856ec88 reports editing 2026-02-27 09:55:55 +00:00
Flatlogic Bot
16f05ef073 adding printers 2026-02-27 09:26:03 +00:00
Flatlogic Bot
a7bd1b142a 0 total for loyalty 2026-02-27 07:01:59 +00:00
Flatlogic Bot
9b31c32aba smtp config 2026-02-27 06:54:46 +00:00
Flatlogic Bot
fa60b1d6db Autosave: 20260227-062846 2026-02-27 06:28:46 +00:00
Flatlogic Bot
f1e599bc52 qrorder update 2026-02-27 04:36:00 +00:00
Flatlogic Bot
71dcc986a2 qorder update 2026-02-27 04:30:17 +00:00
Flatlogic Bot
252e6df9b2 change color 2026-02-27 04:23:22 +00:00
Flatlogic Bot
c5bf0227e4 qorder update 2026-02-27 04:20:26 +00:00
Flatlogic Bot
a70af4ac2a fix 10 2026-02-27 03:59:05 +00:00
Flatlogic Bot
07ef77935c fix 9 2026-02-27 03:37:33 +00:00
Flatlogic Bot
1e9abec636 fix 8 2026-02-27 03:25:14 +00:00
Flatlogic Bot
130a0dc0a3 fix 7 2026-02-27 03:16:35 +00:00
Flatlogic Bot
fb1aa34d97 fix 6 2026-02-27 03:10:56 +00:00
Flatlogic Bot
e2cbec0383 fix 5 2026-02-27 03:03:32 +00:00
Flatlogic Bot
bd73e23131 fix 4 2026-02-27 02:55:44 +00:00
Flatlogic Bot
4268e51b35 fixing tables and order 2026-02-27 02:35:58 +00:00
Flatlogic Bot
8faf7adbb6 missing migrations 2026-02-27 01:20:05 +00:00
Flatlogic Bot
ac32148f05 symbol added for currency 2026-02-26 17:57:35 +00:00
Flatlogic Bot
13a3054fd1 adding fat 2026-02-26 15:17:34 +00:00
Flatlogic Bot
7e5ce73bed Autosave: 20260226-135745 2026-02-26 13:57:45 +00:00
Flatlogic Bot
3f43980699 debugging access denied 2026-02-25 20:14:17 +00:00
Flatlogic Bot
7673b8e4dc update 3 2026-02-25 19:39:16 +00:00
Flatlogic Bot
600e86d0fa loyalty update 2026-02-25 19:30:09 +00:00
Flatlogic Bot
a1d6822c0a updating loyality 2026-02-25 19:06:52 +00:00
Flatlogic Bot
0c98d8d160 Autosave: 20260225-190532 2026-02-25 19:05:32 +00:00
Flatlogic Bot
2bb3386b5d change loyalty system 2026-02-25 18:31:31 +00:00
Flatlogic Bot
9eaaf40d0f rate editing 2026-02-25 18:22:13 +00:00
Flatlogic Bot
c850a45169 Autosave: 20260225-172428 2026-02-25 17:24:28 +00:00
Flatlogic Bot
46245138f0 login modifications 2026-02-25 02:50:54 +00:00
Flatlogic Bot
9999efc72b updating session and pos 2026-02-25 02:42:12 +00:00
Flatlogic Bot
ad06129a7a Autosave: 20260225-022606 2026-02-25 02:26:07 +00:00
Flatlogic Bot
a6faa425c0 last update for pos 2026-02-24 18:11:49 +00:00
Flatlogic Bot
0c612d04a8 Autosave: 20260224-174017 2026-02-24 17:40:17 +00:00
Flatlogic Bot
6e5310e4dc update pos 2026-02-24 17:07:31 +00:00
Flatlogic Bot
05a5289cfc updating outlets 2026-02-24 15:12:00 +00:00
Flatlogic Bot
68b1e34fe9 Autosave: 20260224-140900 2026-02-24 14:09:00 +00:00
Flatlogic Bot
a8bbec67c9 translation update22 2026-02-24 13:31:03 +00:00
Flatlogic Bot
32a53bae23 updated translations 2026-02-24 13:22:36 +00:00
Flatlogic Bot
98141c9a34 bug translate issue 2026-02-24 13:00:30 +00:00
Flatlogic Bot
90c77f9a44 Autosave: 20260224-125306 2026-02-24 12:53:06 +00:00
Flatlogic Bot
2a7531af42 fix pug in pos 2026-02-24 10:44:44 +00:00
Flatlogic Bot
3245d00d29 updating url 2026-02-24 10:22:29 +00:00
Flatlogic Bot
e2fb4c84bf purchase update 2026-02-24 10:05:25 +00:00
Flatlogic Bot
6cbe1c3306 arabic translate for qr order 2026-02-24 09:29:33 +00:00
Flatlogic Bot
291449ae16 Autosave: 20260224-082528 2026-02-24 08:25:28 +00:00
Flatlogic Bot
110b26742e new updates 2026-02-24 08:21:47 +00:00
Flatlogic Bot
b26eab2ba0 Autosave: 20260224-051916 2026-02-24 05:19:16 +00:00
Flatlogic Bot
0bec862e83 Autosave: 20260224-030706 2026-02-24 03:07:06 +00:00
Flatlogic Bot
9561548a1e Autosave: 20260224-022252 2026-02-24 02:22:52 +00:00
Flatlogic Bot
8cc026eb96 Revert to version 5914657 2026-02-24 02:11:46 +00:00
Flatlogic Bot
c20fbe80f7 Revert to version 3f3fe5a 2026-02-24 02:10:51 +00:00
Flatlogic Bot
8f996b6408 Revert to version c6c300f 2026-02-23 19:53:41 +00:00
Flatlogic Bot
171664feb3 updating schema 2026-02-23 18:50:55 +00:00
Flatlogic Bot
c6c300fdbb updating installation 2026-02-23 18:12:11 +00:00
Flatlogic Bot
6d026cb61f updating installation 2026-02-23 18:04:05 +00:00
Flatlogic Bot
4a8fa3dc33 adding installation 2026-02-23 17:57:45 +00:00
Flatlogic Bot
3f3fe5a7d5 Autosave: 20260223-175112 2026-02-23 17:51:12 +00:00
Flatlogic Bot
5914657321 adding rating system 2026-02-23 17:32:54 +00:00
Flatlogic Bot
54fe86501d adding purchase 2026-02-23 15:19:25 +00:00
Flatlogic Bot
4bd6115a47 Autosave: 20260223-150636 2026-02-23 15:06:36 +00:00
Flatlogic Bot
b98ef1276a adding attendence 2026-02-23 13:43:47 +00:00
Flatlogic Bot
999d73eacf user profile 2026-02-23 13:39:33 +00:00
Flatlogic Bot
95541b059b add a blank dashboard 2026-02-23 13:33:11 +00:00
Flatlogic Bot
4bbeb16cfc Autosave: 20260223-131232 2026-02-23 13:12:33 +00:00
Flatlogic Bot
370ceb510e modifying loyalty 2026-02-23 12:56:09 +00:00
Flatlogic Bot
3595e1b23e Autosave: 20260223-093758 2026-02-23 09:37:58 +00:00
Flatlogic Bot
9a50d0a34e settin qr orders 2026-02-23 08:23:40 +00:00
Flatlogic Bot
7886680cd0 adding qr ordering 2026-02-23 08:16:43 +00:00
Flatlogic Bot
4451897e8d adding ads.php 2026-02-23 07:31:09 +00:00
Flatlogic Bot
21321721fc Autosave: 20260223-072710 2026-02-23 07:27:10 +00:00
Flatlogic Bot
37dfb898e7 Autosave: 20260223-061939 2026-02-23 06:19:40 +00:00
Flatlogic Bot
e5617b6c15 Autosave: 20260223-034341 2026-02-23 03:43:41 +00:00
Flatlogic Bot
6c8b522da6 redeem and notifications 2026-02-23 03:07:51 +00:00
Flatlogic Bot
f9d2ca374d Autosave: 20260223-025012 2026-02-23 02:50:12 +00:00
Flatlogic Bot
a238062edb first draft 2026-02-23 02:38:46 +00:00
Flatlogic Bot
5d1e95ef4f Autosave: 20260222-182125 2026-02-22 18:21:25 +00:00
Flatlogic Bot
2996ec35e3 Autosave: 20260222-172008 2026-02-22 17:20:08 +00:00
Flatlogic Bot
5db529f225 Autosave: 20260222-135217 2026-02-22 13:52:18 +00:00
Flatlogic Bot
3d24190863 Autosave: 20260222-110909 2026-02-22 11:09:10 +00:00
Flatlogic Bot
3ce9ce30e6 Autosave: 20260222-101338 2026-02-22 10:13:38 +00:00
152 changed files with 43989 additions and 489 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
PROJECT_ID=38682
PROJECT_UUID=9d5f8cc1-ff96-4857-9598-039478d542f5

88
INSTALL.md Normal file
View File

@ -0,0 +1,88 @@
# Installation Guide
Follow these steps to set up the application on your server.
## 1. Requirements
Before you begin, ensure your server meets the following requirements:
* **Operating System:** Linux, Windows, or macOS.
* **Web Server:** Apache 2.4+ (with `mod_rewrite` enabled).
* **PHP:** Version 8.0 or higher.
* **Database:** MariaDB 10.4+ or MySQL 5.7+.
* **PHP Extensions:**
* `pdo_mysql`
* `curl`
* `gd` (for image processing)
* `mbstring`
* `json`
## 2. Database Setup
1. **Create a Database:** Create a new MySQL/MariaDB database (e.g., `pos_system`).
2. **Configure Connection:** Rename `db/config.php.example` to `db/config.php` (if not already present) and update the database credentials:
```php
define('DB_HOST', 'localhost');
define('DB_NAME', 'your_database_name');
define('DB_USER', 'your_username');
define('DB_PASS', 'your_password');
```
3. **Import Schema:** Import the base schema from `db/schema.sql` into your database.
4. **Run Migrations:** Import all SQL files located in `db/migrations/` in sequential order (001, 002, etc.).
5. **Initialize Data:** Run the initialization script by visiting `http://your-domain.com/db/init.php` in your browser. This will seed categories, products, and outlets.
## 3. Creating a Super Admin
Since the initial setup does not include a default user for security reasons, you must create the first Administrator account.
### Option A: Using the Setup Script (Recommended)
Create a temporary file named `setup_admin.php` in the root directory with the following content:
```php
<?php
require_once 'db/config.php';
require_once 'includes/functions.php';
$pdo = db();
$username = 'admin';
$password = password_hash('admin123', PASSWORD_DEFAULT);
$full_name = 'Super Admin';
$email = 'admin@example.com';
// Ensure the Administrator group exists
$stmt = $pdo->prepare("SELECT id FROM user_groups WHERE name = 'Administrator' LIMIT 1");
$stmt->execute();
$group_id = $stmt->fetchColumn();
if (!$group_id) {
$pdo->exec("INSERT INTO user_groups (name, permissions) VALUES ('Administrator', 'all')");
$group_id = $pdo->lastInsertId();
}
// Create the user
try {
$stmt = $pdo->prepare("INSERT INTO users (group_id, username, password, full_name, email, is_active) VALUES (?, ?, ?, ?, ?, 1)");
$stmt->execute([$group_id, $username, $password, $full_name, $email]);
echo "Super Admin created successfully!<br>Username: admin<br>Password: admin123<br><b>Please delete this file immediately!</b>";
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}
```
Visit `http://your-domain.com/setup_admin.php` and then **delete the file**.
### Option B: Manual SQL
If you prefer SQL, run the following (replace the password hash with one generated via `password_hash()` in PHP):
```sql
INSERT INTO users (group_id, username, password, full_name, email, is_active)
VALUES (1, 'admin', 'REPLACE_WITH_HASH', 'Super Admin', 'admin@example.com', 1);
```
## 4. Final Steps
1. **File Permissions:** Ensure the `assets/images/` directory is writable by the web server for uploading product and ad images.
2. **Login:** Go to `http://your-domain.com/login.php` and log in with your new credentials.
3. **Company Settings:** Navigate to **Admin -> Company Settings** to configure your restaurant name, address, and currency.
---
**Security Note:** Always ensure your `db/config.php` and `.env` files are not accessible from the public web.

290
admin/ads.php Normal file
View File

@ -0,0 +1,290 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../db/config.php";
require_permission("ads");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
// Handle Add/Edit Promo
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$title = trim($_POST['title']);
$sort_order = (int)$_POST['sort_order'];
$is_active = isset($_POST['is_active']) ? 1 : 0;
$display_layout = $_POST['display_layout'] ?? 'both';
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
$image_path = null;
if ($id) {
$stmt = $pdo->prepare("SELECT image_path FROM ads_images WHERE id = ?");
$stmt->execute([$id]);
$image_path = $stmt->fetchColumn();
}
if (isset($_FILES['image']) && $_FILES['image']['error'] !== UPLOAD_ERR_NO_FILE) {
if ($_FILES['image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../assets/images/ads/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$file_ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
if (in_array($file_ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
$new_file_name = uniqid('promo_') . '.' . $file_ext;
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadDir . $new_file_name)) {
$image_path = 'assets/images/ads/' . $new_file_name;
} else {
$message = '<div class="alert alert-danger">Failed to move uploaded file.</div>';
}
} else {
$message = '<div class="alert alert-danger">Invalid file type.</div>';
}
} else {
$message = '<div class="alert alert-danger">File upload error.</div>';
}
}
if (empty($image_path) && $action === 'add_promo' && empty($message)) {
$message = '<div class="alert alert-danger">Image is required for new advertisements.</div>';
}
if (empty($message)) {
try {
if ($action === 'edit_promo' && $id) {
if (!has_permission('ads')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("UPDATE ads_images SET title = ?, sort_order = ?, is_active = ?, display_layout = ?, image_path = ? WHERE id = ?");
$stmt->execute([$title, $sort_order, $is_active, $display_layout, $image_path, $id]);
$message = '<div class="alert alert-success">Advertisement updated successfully!</div>';
}
} elseif ($action === 'add_promo') {
if (!has_permission('ads')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO ads_images (title, sort_order, is_active, display_layout, image_path) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$title, $sort_order, $is_active, $display_layout, $image_path]);
$message = '<div class="alert alert-success">Advertisement created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
if (isset($_GET['delete'])) {
if (!has_permission('ads')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
try {
$id = $_GET['delete'];
$stmt = $pdo->prepare("SELECT image_path FROM ads_images WHERE id = ?");
$stmt->execute([$id]);
$promo = $stmt->fetch();
if ($promo) {
$fullPath = __DIR__ . '/../' . $promo['image_path'];
if (file_exists($fullPath) && is_file($fullPath)) unlink($fullPath);
$pdo->prepare("DELETE FROM ads_images WHERE id = ?")->execute([$id]);
}
header("Location: ads.php?deleted=1");
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Error deleting advertisement: ' . $e->getMessage() . '</div>';
}
}
}
if (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success">Advertisement deleted successfully!</div>';
}
$query = "SELECT * FROM ads_images ORDER BY sort_order ASC, created_at DESC";
$promos_pagination = paginate_query($pdo, $query);
$promos = $promos_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-0 text-dark">Advertisement Slider</h2>
<p class="text-muted mb-0">Manage pictures for the public display page.</p>
</div>
<?php if (has_permission('ads')): ?>
<button class="btn btn-primary btn-lg shadow-sm px-4" data-bs-toggle="modal" data-bs-target="#promoModal" onclick="preparePromoAddForm()" style="border-radius: 10px;">
<i class="bi bi-plus-lg me-1"></i> Add Image
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-body p-0">
<div class="p-3 border-bottom bg-light d-flex justify-content-between align-items-center">
<h6 class="mb-0 fw-bold">Items List</h6>
<?php render_pagination_controls($promos_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Order</th>
<th style="width: 150px;">Preview</th>
<th>Title / Caption</th>
<th>Layout</th>
<th>Status</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($promos as $promo): ?>
<tr>
<td class="ps-4 fw-medium text-dark"><?= $promo['sort_order'] ?></td>
<td>
<?php if (!empty($promo['image_path'])): ?>
<img src="../<?= htmlspecialchars($promo['image_path']) ?>"
class="rounded object-fit-cover border shadow-sm"
width="120" height="70">
<?php else: ?>
<div class="bg-light rounded border d-flex align-items-center justify-content-center text-muted" style="width: 120px; height: 70px;">No Image</div>
<?php endif; ?>
</td>
<td>
<div class="fw-bold text-dark"><?= htmlspecialchars($promo['title'] ?: 'No title') ?></div>
</td>
<td>
<?php
$layoutLabel = 'Both';
$layoutClass = 'bg-primary-subtle text-primary';
if (isset($promo['display_layout'])) {
if ($promo['display_layout'] === 'split') {
$layoutLabel = 'Split Only';
$layoutClass = 'bg-info-subtle text-info';
} elseif ($promo['display_layout'] === 'fullscreen') {
$layoutLabel = 'Fullscreen Only';
$layoutClass = 'bg-warning-subtle text-warning';
}
}
?>
<span class="badge <?= $layoutClass ?> px-3"><?= $layoutLabel ?></span>
</td>
<td>
<?php if (isset($promo['is_active']) && $promo['is_active']): ?>
<span class="badge bg-success-subtle text-success px-3">Active</span>
<?php else: ?>
<span class="badge bg-secondary-subtle text-secondary px-3">Inactive</span>
<?php endif; ?>
</td>
<td class="text-end pe-4">
<?php if (has_permission('ads')): ?>
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-3 me-1"
data-bs-toggle="modal" data-bs-target="#promoModal"
onclick='preparePromoEditForm(<?= htmlspecialchars(json_encode($promo), ENT_QUOTES, "UTF-8") ?>)'>
<i class="bi bi-pencil me-1"></i> Edit
</button>
<a href="?delete=<?= $promo['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('Are you sure?')"><i class="bi bi-trash me-1"></i> Delete</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($promos_pagination); ?>
</div>
</div>
</div>
<!-- Promo Modal -->
<?php if (has_permission('ads')): ?>
<div class="modal fade" id="promoModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header bg-primary text-white border-0">
<h5 class="modal-title fw-bold" id="promoModalTitle">Add New Item</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="promoForm" enctype="multipart/form-data">
<div class="modal-body p-4">
<input type="hidden" name="action" id="promoAction" value="add_promo">
<input type="hidden" name="id" id="promoId">
<div class="mb-4 text-center" id="promoImagePreviewContainer" style="display: none;">
<img src="" id="promoImagePreview" class="img-fluid rounded shadow-sm border" style="max-height: 200px;">
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">TITLE / CAPTION</label>
<input type="text" name="title" id="promoTitle" class="form-control" placeholder="e.g. Special Offer">
</div>
<div class="row g-3 mb-3">
<div class="col-6">
<label class="form-label small fw-bold text-muted">SORT ORDER</label>
<input type="number" name="sort_order" id="promoSortOrder" class="form-control" value="0">
</div>
<div class="col-6">
<label class="form-label small fw-bold text-muted">DISPLAY LAYOUT</label>
<select name="display_layout" id="promoDisplayLayout" class="form-select">
<option value="both">Both Layouts</option>
<option value="split">Split Only</option>
<option value="fullscreen">Fullscreen</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">IMAGE FILE</label>
<input type="file" name="image" id="promoImageFile" class="form-control" accept="image/*">
<small class="text-muted mt-1 d-block" id="promoImageHint">Required for new images.</small>
</div>
<div class="mb-0 form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" id="promoIsActive" value="1" checked>
<label class="form-check-label fw-medium text-dark" for="promoIsActive">Active (Show in Slider)</label>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
<script>
function preparePromoAddForm() {
document.getElementById('promoModalTitle').innerText = 'Add New Item';
document.getElementById('promoAction').value = 'add_promo';
document.getElementById('promoForm').reset();
document.getElementById('promoId').value = '';
document.getElementById('promoImagePreviewContainer').style.display = 'none';
document.getElementById('promoImageHint').innerText = 'Required for new images.';
document.getElementById('promoImageFile').required = true;
}
function preparePromoEditForm(data) {
if (!data) return;
document.getElementById('promoModalTitle').innerText = 'Edit Item';
document.getElementById('promoAction').value = 'edit_promo';
document.getElementById('promoId').value = data.id;
document.getElementById('promoTitle').value = data.title || '';
document.getElementById('promoSortOrder').value = data.sort_order || 0;
document.getElementById('promoDisplayLayout').value = data.display_layout || 'both';
document.getElementById('promoIsActive').checked = data.is_active == 1;
document.getElementById('promoImageHint').innerText = 'Leave empty to keep current image.';
document.getElementById('promoImageFile').required = false;
if (data.image_path) {
const preview = document.getElementById('promoImagePreview');
preview.src = '../' + data.image_path;
document.getElementById('promoImagePreviewContainer').style.display = 'block';
} else {
document.getElementById('promoImagePreviewContainer').style.display = 'none';
}
}
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

208
admin/areas.php Normal file
View File

@ -0,0 +1,208 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../db/config.php";
require_permission("areas_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
// Handle Add/Edit Area
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$name = trim($_POST['name']);
$outlet_id = (int)$_POST['outlet_id'];
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
if (empty($name)) {
$message = '<div class="alert alert-danger">Area name is required.</div>';
} else {
try {
if ($action === 'edit_area' && $id) {
if (!has_permission('areas_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("UPDATE areas SET name = ?, outlet_id = ? WHERE id = ?");
$stmt->execute([$name, $outlet_id, $id]);
$message = '<div class="alert alert-success">Area updated successfully!</div>';
}
} elseif ($action === 'add_area') {
if (!has_permission('areas_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO areas (name, outlet_id) VALUES (?, ?)");
$stmt->execute([$name, $outlet_id]);
$message = '<div class="alert alert-success">Area created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
// Handle Delete (Soft Delete)
if (isset($_GET['delete'])) {
if (!has_permission('areas_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete areas.</div>';
} else {
try {
$id = (int)$_GET['delete'];
// Soft delete to preserve relations with tables
$pdo->prepare("UPDATE areas SET is_deleted = 1 WHERE id = ?")->execute([$id]);
header("Location: areas.php?deleted=1");
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Error removing area: ' . $e->getMessage() . '</div>';
}
}
}
if (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success">Area removed successfully!</div>';
}
$outlets = $pdo->query("SELECT * FROM outlets WHERE is_deleted = 0 ORDER BY name ASC")->fetchAll();
$query = "SELECT a.*, o.name as outlet_name
FROM areas a
LEFT JOIN outlets o ON a.outlet_id = o.id
WHERE a.is_deleted = 0
ORDER BY a.id DESC";
$areas_pagination = paginate_query($pdo, $query);
$areas = $areas_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Areas</h2>
<?php if (has_permission('areas_add')): ?>
<button class="btn btn-primary" onclick="openAddModal()">
<i class="bi bi-plus-lg"></i> Add Area
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($areas_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">ID</th>
<th>Name</th>
<th>Outlet</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($areas as $area): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $area['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($area['name']) ?></td>
<td><span class="badge bg-light text-dark border"><?= htmlspecialchars($area['outlet_name'] ?: 'None') ?></span></td>
<td class="text-end pe-4">
<?php if (has_permission('areas_add')): ?>
<button type="button" class="btn btn-sm btn-outline-primary me-1"
onclick='openEditModal(<?= htmlspecialchars(json_encode($area), ENT_QUOTES, "UTF-8") ?>)' title="Edit"><i class="bi bi-pencil"></i></button>
<?php endif; ?>
<?php if (has_permission('areas_del')): ?>
<a href="?delete=<?= $area['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('<?= t('are_you_sure') ?>')"><i class="bi bi-trash"></i></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($areas)): ?>
<tr>
<td colspan="4" class="text-center py-4 text-muted">No areas found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($areas_pagination); ?>
</div>
</div>
</div>
<!-- Area Modal -->
<?php if (has_permission('areas_add')): ?>
<div class="modal fade" id="areaModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="areaModalTitle">Add New Area</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="areaForm">
<div class="modal-body">
<input type="hidden" name="action" id="areaAction" value="add_area">
<input type="hidden" name="id" id="areaId">
<div class="mb-3">
<label class="form-label">Name <span class="text-danger">*</span></label>
<input type="text" name="name" id="areaName" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Outlet <span class="text-danger">*</span></label>
<select name="outlet_id" id="areaOutletId" class="form-select" required>
<option value="">Select Outlet</option>
<?php foreach ($outlets as $outlet): ?>
<option value="<?= $outlet['id'] ?>"><?= htmlspecialchars($outlet['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Area</button>
</div>
</form>
</div>
</div>
</div>
<script>
function getAreaModal() {
if (typeof bootstrap === 'undefined') return null;
const el = document.getElementById('areaModal');
return el ? bootstrap.Modal.getOrCreateInstance(el) : null;
}
function openAddModal() {
const modal = getAreaModal();
if (!modal) return;
document.getElementById('areaModalTitle').innerText = 'Add New Area';
document.getElementById('areaAction').value = 'add_area';
document.getElementById('areaForm').reset();
document.getElementById('areaId').value = '';
modal.show();
}
function openEditModal(area) {
if (!area) return;
const modal = getAreaModal();
if (!modal) return;
document.getElementById('areaModalTitle').innerText = 'Edit Area';
document.getElementById('areaAction').value = 'edit_area';
document.getElementById('areaId').value = area.id;
document.getElementById('areaName').value = area.name || '';
document.getElementById('areaOutletId').value = area.outlet_id || '';
modal.show();
}
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

125
admin/attendance.php Normal file
View File

@ -0,0 +1,125 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('all');
$date_from = $_GET['date_from'] ?? date('Y-m-01');
$date_to = $_GET['date_to'] ?? date('Y-m-d');
$user_id = $_GET['user_id'] ?? '';
$query = "SELECT l.*, u.full_name, u.username
FROM attendance_logs l
LEFT JOIN users u ON l.user_id = u.id
WHERE DATE(l.log_timestamp) BETWEEN ? AND ?";
$params = [$date_from, $date_to];
if ($user_id) {
$query .= " AND l.user_id = ?";
$params[] = $user_id;
}
$query .= " ORDER BY l.log_timestamp DESC";
$stmt = $pdo->prepare($query);
$stmt->execute($params);
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
$users = $pdo->query("SELECT id, full_name, username FROM users ORDER BY full_name")->fetchAll();
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Attendance Sheet</h2>
<div class="d-flex gap-2">
<button class="btn btn-outline-primary rounded-pill px-3" onclick="window.print()">
<i class="bi bi-printer me-1"></i> Print
</button>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<form method="GET" class="row g-3">
<div class="col-md-3">
<label class="form-label small fw-bold text-muted">FROM DATE</label>
<input type="date" name="date_from" class="form-control" value="<?= $date_from ?>">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-muted">TO DATE</label>
<input type="date" name="date_to" class="form-control" value="<?= $date_to ?>">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-muted">USER</label>
<select name="user_id" class="form-select">
<option value="">All Users</option>
<?php foreach ($users as $u): ?>
<option value="<?= $u['id'] ?>" <?= $user_id == $u['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($u['full_name'] ?: $u['username']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100 rounded-pill fw-bold">Filter</button>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Timestamp</th>
<th>User</th>
<th>Employee ID</th>
<th>Type</th>
<th>Device</th>
<th class="pe-4">IP Address</th>
</tr>
</thead>
<tbody>
<?php if (empty($logs)): ?>
<tr>
<td colspan="6" class="text-center py-5 text-muted">No attendance logs found for the selected period.</td>
</tr>
<?php else: ?>
<?php foreach ($logs as $log): ?>
<tr>
<td class="ps-4">
<div class="fw-bold"><?= date('d M Y', strtotime($log['log_timestamp'])) ?></div>
<div class="small text-muted"><?= date('H:i:s', strtotime($log['log_timestamp'])) ?></div>
</td>
<td>
<?php if ($log['user_id']): ?>
<div class="fw-bold text-primary"><?= htmlspecialchars($log['full_name'] ?: $log['username']) ?></div>
<?php else: ?>
<span class="text-danger small">Unmapped Device ID: <?= htmlspecialchars($log['employee_id']) ?></span>
<?php endif; ?>
</td>
<td><code><?= htmlspecialchars($log['employee_id']) ?></code></td>
<td>
<?php if ($log['log_type'] === 'IN'): ?>
<span class="badge bg-success-subtle text-success border border-success-subtle rounded-pill">CHECK-IN</span>
<?php elseif ($log['log_type'] === 'OUT'): ?>
<span class="badge bg-danger-subtle text-danger border border-danger-subtle rounded-pill">CHECK-OUT</span>
<?php else: ?>
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle rounded-pill"><?= $log['log_type'] ?></span>
<?php endif; ?>
</td>
<td><span class="small"><?= htmlspecialchars($log['device_id']) ?></span></td>
<td class="pe-4 small text-muted"><?= htmlspecialchars($log['ip_address']) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

216
admin/backup.php Normal file
View File

@ -0,0 +1,216 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../db/config.php";
require_permission("settings_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$backupDir = __DIR__ . '/../storage/backups/';
if (!is_dir($backupDir)) {
mkdir($backupDir, 0777, true);
}
$message = '';
// Retention policy: keep 5 copies
function enforceRetention($dir) {
$files = glob($dir . '/*.sql');
if (count($files) > 5) {
usort($files, function($a, $b) {
return filemtime($a) - filemtime($b);
});
while (count($files) > 5) {
$oldest = array_shift($files);
unlink($oldest);
}
}
}
// Handle actions
$action = $_GET['action'] ?? '';
if ($action === 'backup') {
$filename = 'backup_' . date('Y-m-d_H-i-s') . '.sql';
$path = $backupDir . $filename;
$command = sprintf(
'mysqldump -h %s -u %s -p%s %s > %s',
escapeshellarg(DB_HOST),
escapeshellarg(DB_USER),
escapeshellarg(DB_PASS),
escapeshellarg(DB_NAME),
escapeshellarg($path)
);
exec($command, $output, $returnVar);
if ($returnVar === 0) {
enforceRetention($backupDir);
$message = '<div class="alert alert-success">Backup created successfully: ' . $filename . '</div>';
} else {
$message = '<div class="alert alert-danger">Error creating backup.</div>';
}
} elseif ($action === 'download' && isset($_GET['file'])) {
$file = basename($_GET['file']);
$path = $backupDir . $file;
if (file_exists($path)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $file . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($path));
readfile($path);
exit;
}
} elseif ($action === 'delete' && isset($_GET['file'])) {
$file = basename($_GET['file']);
$path = $backupDir . $file;
if (file_exists($path)) {
unlink($path);
$message = '<div class="alert alert-success">Backup deleted.</div>';
}
} elseif ($action === 'restore' && isset($_GET['file'])) {
$file = basename($_GET['file']);
$path = $backupDir . $file;
if (file_exists($path)) {
$command = sprintf(
'mysql -h %s -u %s -p%s %s < %s',
escapeshellarg(DB_HOST),
escapeshellarg(DB_USER),
escapeshellarg(DB_PASS),
escapeshellarg(DB_NAME),
escapeshellarg($path)
);
exec($command, $output, $returnVar);
if ($returnVar === 0) {
$message = '<div class="alert alert-success">Database restored successfully from ' . $file . '</div>';
} else {
$message = '<div class="alert alert-danger">Error restoring database.</div>';
}
}
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['toggle_auto'])) {
$status = $_POST['auto_backup_enabled'] ? 1 : 0;
$stmt = $pdo->prepare("UPDATE company_settings SET auto_backup_enabled = ?, updated_at = NOW() LIMIT 1");
$stmt->execute([$status]);
$message = '<div class="alert alert-success">Auto backup settings updated.</div>';
}
$settings = get_company_settings();
$backups = glob($backupDir . '*.sql');
usort($backups, function($a, $b) {
return filemtime($b) - filemtime($a);
});
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Backup & Restore</h2>
<a href="?action=backup" class="btn btn-primary">
<i class="bi bi-cloud-arrow-up"></i> Create Manual Backup
</a>
</div>
<?= $message ?>
<div class="row">
<div class="col-md-4 mb-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<h5 class="card-title fw-bold mb-3">Settings</h5>
<form method="POST">
<input type="hidden" name="toggle_auto" value="1">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" name="auto_backup_enabled" id="autoBackupSwitch" <?= ($settings['auto_backup_enabled'] ?? 0) ? 'checked' : '' ?> onchange="this.form.submit()">
<label class="form-check-label" for="autoBackupSwitch">Enable Auto Backup (Daily)</label>
</div>
<p class="text-muted small">
Auto backup runs once a day when you access the admin panel.
It keeps exactly 5 latest copies.
</p>
<?php if ($settings['last_auto_backup']): ?>
<div class="alert alert-info py-2 small mb-0">
Last auto backup: <?= date('Y-m-d H:i', strtotime($settings['last_auto_backup'])) ?>
</div>
<?php endif; ?>
</form>
</div>
</div>
</div>
<div class="col-md-8 mb-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h5 class="card-title fw-bold mb-3">Available Backups (Max 5)</h5>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Filename</th>
<th>Date</th>
<th>Size</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($backups)): ?>
<tr>
<td colspan="4" class="text-center py-4 text-muted">No backups found.</td>
</tr>
<?php else: ?>
<?php foreach ($backups as $path): ?>
<?php
$file = basename($path);
$size = round(filesize($path) / 1024, 2) . ' KB';
$date = date('Y-m-d H:i:s', filemtime($path));
?>
<tr>
<td><code class="text-primary"><?= $file ?></code></td>
<td><?= $date ?></td>
<td><?= $size ?></td>
<td class="text-end">
<div class="btn-group gap-1">
<a href="?action=download&file=<?= urlencode($file) ?>" class="btn btn-sm btn-outline-secondary rounded" title="Download">
<i class="bi bi-download"></i>
</a>
<button onclick="confirmRestore('<?= $file ?>')" class="btn btn-sm btn-outline-warning rounded" title="Restore">
<i class="bi bi-arrow-counterclockwise"></i>
</button>
<a href="?action=delete&file=<?= urlencode($file) ?>" class="btn btn-sm btn-outline-danger rounded" title="Delete" onclick="return confirm('Are you sure?')">
<i class="bi bi-trash"></i>
</a>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
function confirmRestore(file) {
Swal.fire({
title: 'Restore Database?',
text: "This will overwrite your current database with the backup from " + file + ". This action cannot be undone!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ffc107',
cancelButtonColor: '#6c757d',
confirmButtonText: 'Yes, Restore it!',
cancelButtonText: 'Cancel'
}).then((result) => {
if (result.isConfirmed) {
window.location.href = '?action=restore&file=' + encodeURIComponent(file);
}
});
}
</script>
<?php include 'includes/footer.php'; ?>

286
admin/categories.php Normal file
View File

@ -0,0 +1,286 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('categories_view');
$message = '';
// Handle Add/Edit Category
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
$name = $_POST['name'];
$name_ar = $_POST['name_ar'] ?? '';
$description = $_POST['description'];
$image_url = null;
if ($id) {
$stmt = $pdo->prepare("SELECT image_url FROM categories WHERE id = ?");
$stmt->execute([$id]);
$image_url = $stmt->fetchColumn();
} else {
$image_url = 'https://placehold.co/400x300?text=' . urlencode($name);
}
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../assets/images/categories/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$file_ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
if (in_array($file_ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
$fileName = uniqid('cat_') . '.' . $file_ext;
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadDir . $fileName)) {
$image_url = 'assets/images/categories/' . $fileName;
}
}
}
try {
if ($action === 'edit_category' && $id) {
if (!has_permission('categories_edit') && !has_permission('categories_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to edit categories.</div>';
} else {
$stmt = $pdo->prepare("UPDATE categories SET name = ?, name_ar = ?, description = ?, image_url = ? WHERE id = ?");
$stmt->execute([$name, $name_ar, $description, $image_url, $id]);
$message = '<div class="alert alert-success">Category updated successfully!</div>';
}
} elseif ($action === 'add_category') {
if (!has_permission('categories_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to add categories.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO categories (name, name_ar, description, image_url) VALUES (?, ?, ?, ?)");
$stmt->execute([$name, $name_ar, $description, $image_url]);
$message = '<div class="alert alert-success">Category created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
// Handle Delete (Soft Delete)
if (isset($_GET['delete'])) {
if (!has_permission('categories_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete categories.</div>';
} else {
try {
$id = (int)$_GET['delete'];
// Soft delete to avoid breaking product relations and historical order integrity
$pdo->prepare("UPDATE categories SET is_deleted = 1 WHERE id = ?")->execute([$id]);
header("Location: categories.php?deleted=1");
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Error removing category: ' . $e->getMessage() . '</div>';
}
}
}
if (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success">Category removed successfully!</div>';
}
$query = "SELECT * FROM categories WHERE is_deleted = 0 ORDER BY name ASC";
$categories_pagination = paginate_query($pdo, $query);
$categories = $categories_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1"><?= t('categories') ?></h2>
<p class="text-muted mb-0">Organize your menu and inventory</p>
</div>
<?php if (has_permission('categories_add')): ?>
<button class="btn btn-primary btn-lg shadow-sm" data-bs-toggle="modal" data-bs-target="#categoryModal" onclick="prepareAddForm()" style="border-radius: 12px;">
<i class="bi bi-plus-lg me-1"></i> <?= t('add') ?> <?= t('categories') ?>
</button>
<?php endif; ?>
</div>
<?= $message ?>
<?php if (empty($categories)): ?>
<div class="text-center py-5 bg-white rounded-4 shadow-sm">
<i class="bi bi-tags display-1 text-muted opacity-25 mb-3 d-block"></i>
<h4 class="text-dark"><?= t('none') ?></h4>
<p class="text-muted">Start by adding your first category.</p>
</div>
<?php else: ?>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4"><?= t('category') ?></th>
<th><?= t('arabic_name') ?></th>
<th><?= t('description') ?></th>
<th class="text-end pe-4"><?= t('actions') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($categories as $cat): ?>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center py-2">
<img src="<?= htmlspecialchars(strpos($cat['image_url'], 'http') === 0 ? $cat['image_url'] : '../' . $cat['image_url']) ?>" alt="" class="rounded-3 me-3 border shadow-sm" style="width: 50px; height: 50px; object-fit: cover;">
<div class="fw-bold text-dark fs-6"><?= htmlspecialchars($cat['name']) ?></div>
</div>
</td>
<td>
<div class="text-dark"><?= htmlspecialchars($cat['name_ar'] ?? '-') ?></div>
</td>
<td>
<div class="text-muted small text-truncate" style="max-width: 300px;"><?= htmlspecialchars($cat['description'] ?? t('none')) ?></div>
</td>
<td class="text-end pe-4">
<div class="d-inline-flex gap-2">
<?php if (has_permission('categories_edit') || has_permission('categories_add')): ?>
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-3"
data-bs-toggle="modal" data-bs-target="#categoryModal"
onclick='prepareEditForm(<?= htmlspecialchars(json_encode($cat), ENT_QUOTES, "UTF-8") ?>)'><?= t('edit') ?></button>
<?php endif; ?>
<?php if (has_permission('categories_del')): ?>
<a href="?delete=<?= $cat['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('<?= t('are_you_sure') ?>')"><?= t('delete') ?></a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($categories_pagination); ?>
</div>
</div>
<?php endif; ?>
<!-- Category Modal -->
<?php if (has_permission('categories_add') || has_permission('categories_edit')): ?>
<div class="modal fade" id="categoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header bg-primary text-white border-0 py-3">
<h5 class="modal-title fw-bold" id="categoryModalTitle"><?= t('add') ?> <?= t('categories') ?></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="categoryForm" enctype="multipart/form-data">
<div class="modal-body p-4">
<input type="hidden" name="action" id="categoryAction" value="add_category">
<input type="hidden" name="id" id="categoryId">
<div class="mb-3">
<label class="form-label small fw-bold text-muted d-flex justify-content-between">
<span><?= t('name') ?> (EN) <span class="text-danger">*</span></span>
<a href="javascript:void(0)" onclick="translateTo('English')" class="text-decoration-none small text-primary fw-bold" id="translateBtnEn">
<i class="bi bi-translate me-1"></i> Auto-translate
</a>
</label>
<input type="text" name="name" id="categoryName" class="form-control rounded-3 border-0 bg-light" required>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted d-flex justify-content-between">
<span><?= t('arabic_name') ?></span>
<a href="javascript:void(0)" onclick="translateTo('Arabic')" class="text-decoration-none small text-primary fw-bold" id="translateBtnAr">
<i class="bi bi-translate me-1"></i> Auto-translate
</a>
</label>
<input type="text" name="name_ar" id="categoryNameAr" class="form-control rounded-3 border-0 bg-light" dir="rtl">
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted"><?= t('description') ?></label>
<textarea name="description" id="categoryDescription" class="form-control rounded-3 border-0 bg-light" rows="3" placeholder="Optional category description..."></textarea>
</div>
<div class="mb-0">
<label class="form-label small fw-bold text-muted">IMAGE</label>
<div class="d-flex align-items-center gap-3 bg-light p-3 rounded-4 border border-dashed">
<img src="" id="categoryImagePreview" class="rounded-3 border shadow-sm" style="width: 60px; height: 60px; object-fit: cover; display: none;">
<div class="flex-grow-1">
<input type="file" name="image" class="form-control border-0 bg-transparent" accept="image/*">
</div>
</div>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal"><?= t('cancel') ?></button>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm"><?= t('save') ?></button>
</div>
</form>
</div>
</div>
</div>
<script>
function prepareAddForm() {
document.getElementById('categoryModalTitle').innerText = '<?= t('add') ?> <?= t('categories') ?>';
document.getElementById('categoryAction').value = 'add_category';
document.getElementById('categoryForm').reset();
document.getElementById('categoryId').value = '';
document.getElementById('categoryImagePreview').style.display = 'none';
}
function prepareEditForm(cat) {
if (!cat) return;
document.getElementById('categoryModalTitle').innerText = '<?= t('edit') ?> <?= t('categories') ?>: ' + cat.name;
document.getElementById('categoryAction').value = 'edit_category';
document.getElementById('categoryId').value = cat.id;
document.getElementById('categoryName').value = cat.name;
document.getElementById('categoryNameAr').value = cat.name_ar || '';
document.getElementById('categoryDescription').value = cat.description || '';
if (cat.image_url) {
const preview = document.getElementById('categoryImagePreview');
preview.src = cat.image_url.startsWith('http') ? cat.image_url : '../' + cat.image_url;
preview.style.display = 'block';
} else {
document.getElementById('categoryImagePreview').style.display = 'none';
}
}
async function translateTo(targetLang) {
const sourceId = targetLang === 'Arabic' ? 'categoryName' : 'categoryNameAr';
const targetId = targetLang === 'Arabic' ? 'categoryNameAr' : 'categoryName';
const btnId = targetLang === 'Arabic' ? 'translateBtnAr' : 'translateBtnEn';
const sourceText = document.getElementById(sourceId).value;
if (!sourceText) {
alert('Please enter text to translate first.');
return;
}
const btn = document.getElementById(btnId);
const originalContent = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Translating...';
btn.classList.add('disabled');
try {
const response = await fetch('../api/translate.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: sourceText, target_lang: targetLang })
});
const data = await response.json();
if (data.success) {
document.getElementById(targetId).value = data.translated_text;
} else {
alert('Translation error: ' + data.error);
}
} catch (error) {
console.error('Translation error:', error);
alert('An error occurred during translation.');
} finally {
btn.innerHTML = originalContent;
btn.classList.remove('disabled');
}
}
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

321
admin/company.php Normal file
View File

@ -0,0 +1,321 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../db/config.php";
require_permission("settings_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
$settings = get_company_settings();
// Handle Update
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!has_permission('settings_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to update settings.</div>';
} else {
$company_name = $_POST['company_name'] ?? '';
$address = $_POST['address'] ?? '';
$phone = $_POST['phone'] ?? '';
$email = $_POST['email'] ?? '';
$vat_rate = $_POST['vat_rate'] ?? 0;
$currency_symbol = $_POST['currency_symbol'] ?? '$';
$currency_decimals = $_POST['currency_decimals'] ?? 2;
$currency_position = $_POST['currency_position'] ?? 'before';
$ctr_number = $_POST['ctr_number'] ?? '';
$vat_number = $_POST['vat_number'] ?? '';
$commission_enabled = isset($_POST['commission_enabled']) ? 1 : 0;
$timezone = $_POST['timezone'] ?? 'UTC';
$whatsapp_report_number = $_POST['whatsapp_report_number'] ?? '';
$whatsapp_report_time = $_POST['whatsapp_report_time'] ?? '23:59:00';
$whatsapp_report_enabled = isset($_POST['whatsapp_report_enabled']) ? 1 : 0;
// Handle File Uploads
$uploadDir = __DIR__ . '/../assets/images/company/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$logo_url = $settings['logo_url'] ?? null;
$favicon_url = $settings['favicon_url'] ?? null;
// Logo Upload
if (isset($_FILES['logo']) && $_FILES['logo']['error'] === UPLOAD_ERR_OK) {
$fileInfo = pathinfo($_FILES['logo']['name']);
$fileExt = strtolower($fileInfo['extension']);
$allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
if (in_array($fileExt, $allowedExts)) {
$fileName = 'logo_' . uniqid() . '.' . $fileExt;
$targetFile = $uploadDir . $fileName;
if (move_uploaded_file($_FILES['logo']['tmp_name'], $targetFile)) {
$logo_url = 'assets/images/company/' . $fileName;
}
}
}
// Favicon Upload
if (isset($_FILES['favicon']) && $_FILES['favicon']['error'] === UPLOAD_ERR_OK) {
$fileInfo = pathinfo($_FILES['favicon']['name']);
$fileExt = strtolower($fileInfo['extension']);
$allowedExts = ['ico', 'png', 'svg']; // Favicons are usually ico/png/svg
if (in_array($fileExt, $allowedExts)) {
$fileName = 'favicon_' . uniqid() . '.' . $fileExt;
$targetFile = $uploadDir . $fileName;
if (move_uploaded_file($_FILES['favicon']['tmp_name'], $targetFile)) {
$favicon_url = 'assets/images/company/' . $fileName;
}
}
}
try {
// Check if row exists
$exists = $pdo->query("SELECT COUNT(*) FROM company_settings")->fetchColumn();
if ($exists) {
$stmt = $pdo->prepare("UPDATE company_settings SET company_name=?, address=?, phone=?, email=?, vat_rate=?, currency_symbol=?, currency_decimals=?, currency_position=?, ctr_number=?, vat_number=?, logo_url=?, favicon_url=?, commission_enabled=?, timezone=?, whatsapp_report_number=?, whatsapp_report_time=?, whatsapp_report_enabled=?, updated_at=NOW()");
$stmt->execute([$company_name, $address, $phone, $email, $vat_rate, $currency_symbol, $currency_decimals, $currency_position, $ctr_number, $vat_number, $logo_url, $favicon_url, $commission_enabled, $timezone, $whatsapp_report_number, $whatsapp_report_time, $whatsapp_report_enabled]);
} else {
$stmt = $pdo->prepare("INSERT INTO company_settings (company_name, address, phone, email, vat_rate, currency_symbol, currency_decimals, currency_position, ctr_number, vat_number, logo_url, favicon_url, commission_enabled, timezone, whatsapp_report_number, whatsapp_report_time, whatsapp_report_enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$company_name, $address, $phone, $email, $vat_rate, $currency_symbol, $currency_decimals, $currency_position, $ctr_number, $vat_number, $logo_url, $favicon_url, $commission_enabled, $timezone, $whatsapp_report_number, $whatsapp_report_time, $whatsapp_report_enabled]);
}
$message = '<div class="alert alert-success">Company settings updated successfully!</div>';
// Refresh settings
$settings = get_company_settings(); // Re-fetch to get updated values
// Manually update immediate values for display if fetch is cached/laggy (though re-fetch is better)
$settings['ctr_number'] = $ctr_number;
$settings['vat_number'] = $vat_number;
$settings['logo_url'] = $logo_url;
$settings['favicon_url'] = $favicon_url;
$settings['commission_enabled'] = $commission_enabled;
$settings['timezone'] = $timezone;
$settings['whatsapp_report_number'] = $whatsapp_report_number;
$settings['whatsapp_report_time'] = $whatsapp_report_time;
$settings['whatsapp_report_enabled'] = $whatsapp_report_enabled;
} catch (Exception $e) {
$message = '<div class="alert alert-danger">Error updating settings: ' . htmlspecialchars($e->getMessage()) . '</div>';
}
}
}
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Company Profile</h2>
</div>
<?= $message ?>
<form method="POST" enctype="multipart/form-data">
<div class="row">
<div class="col-12 col-lg-3 mb-4">
<div class="card border-0 shadow-sm sticky-top" style="top: 20px; z-index: 10;">
<div class="card-body p-0">
<div class="nav flex-column nav-pills" id="v-pills-tab" role="tablist" aria-orientation="vertical">
<button class="nav-link active rounded-0 text-start py-3 px-4 border-bottom" id="v-pills-general-tab" data-bs-toggle="pill" data-bs-target="#v-pills-general" type="button" role="tab" aria-controls="v-pills-general" aria-selected="true">
<i class="bi bi-info-circle me-2"></i> General Info
</button>
<button class="nav-link rounded-0 text-start py-3 px-4 border-bottom" id="v-pills-financial-tab" data-bs-toggle="pill" data-bs-target="#v-pills-financial" type="button" role="tab" aria-controls="v-pills-financial" aria-selected="false">
<i class="bi bi-currency-dollar me-2"></i> Financial & Taxes
</button>
<button class="nav-link rounded-0 text-start py-3 px-4 border-bottom" id="v-pills-system-tab" data-bs-toggle="pill" data-bs-target="#v-pills-system" type="button" role="tab" aria-controls="v-pills-system" aria-selected="false">
<i class="bi bi-gear me-2"></i> System Settings
</button>
<button class="nav-link rounded-0 text-start py-3 px-4" id="v-pills-branding-tab" data-bs-toggle="pill" data-bs-target="#v-pills-branding" type="button" role="tab" aria-controls="v-pills-branding" aria-selected="false">
<i class="bi bi-image me-2"></i> Branding
</button>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-9">
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<div class="tab-content" id="v-pills-tabContent">
<!-- General Info Tab -->
<div class="tab-pane fade show active" id="v-pills-general" role="tabpanel" aria-labelledby="v-pills-general-tab">
<h4 class="mb-4">General Information</h4>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Company Name</label>
<input type="text" name="company_name" class="form-control" value="<?= htmlspecialchars($settings['company_name'] ?? '') ?>" required <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($settings['email'] ?? '') ?>" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Phone</label>
<input type="text" name="phone" class="form-control" value="<?= htmlspecialchars($settings['phone'] ?? '') ?>" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
<div class="col-md-12 mb-3">
<label class="form-label">Address</label>
<textarea name="address" class="form-control" rows="3" <?= !has_permission('settings_add') ? 'readonly' : '' ?>><?= htmlspecialchars($settings['address'] ?? '') ?></textarea>
</div>
</div>
<hr class="my-4">
<h5 class="mb-3">Legal & Tax Information</h5>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">CTR No (Company Tax Registration)</label>
<input type="text" name="ctr_number" class="form-control" value="<?= htmlspecialchars($settings['ctr_number'] ?? '') ?>" placeholder="e.g. 123456789" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">VAT No (Value Added Tax Number)</label>
<input type="text" name="vat_number" class="form-control" value="<?= htmlspecialchars($settings['vat_number'] ?? '') ?>" placeholder="e.g. VAT-987654321" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
</div>
</div>
<!-- Financial & Taxes Tab -->
<div class="tab-pane fade" id="v-pills-financial" role="tabpanel" aria-labelledby="v-pills-financial-tab">
<h4 class="mb-4">Financial Settings</h4>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">VAT Rate (%)</label>
<div class="input-group">
<input type="number" step="0.01" name="vat_rate" class="form-control" value="<?= htmlspecialchars($settings['vat_rate'] ?? 0) ?>" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
<span class="input-group-text">%</span>
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Currency Symbol</label>
<input type="text" name="currency_symbol" class="form-control" value="<?= htmlspecialchars($settings['currency_symbol'] ?? '$') ?>" placeholder="e.g. $, €, £" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Currency Position</label>
<select name="currency_position" class="form-select" <?= !has_permission('settings_add') ? 'disabled' : '' ?>>
<option value="before" <?= ($settings['currency_position'] ?? 'before') === 'before' ? 'selected' : '' ?>>Before (e.g. $10.00)</option>
<option value="after" <?= ($settings['currency_position'] ?? 'before') === 'after' ? 'selected' : '' ?>>After (e.g. 10.00 OMR)</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Decimal Places</label>
<input type="number" name="currency_decimals" class="form-control" value="<?= htmlspecialchars($settings['currency_decimals'] ?? 2) ?>" min="0" max="4" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
</div>
<hr class="my-4">
<h5 class="mb-3">Commission System</h5>
<div class="row">
<div class="col-md-12 mb-3">
<div class="form-check form-switch mt-2">
<input class="form-check-input" type="checkbox" name="commission_enabled" id="commissionEnabled" <?= ($settings['commission_enabled'] ?? 0) ? 'checked' : '' ?> <?= !has_permission('settings_add') ? 'disabled' : '' ?>>
<label class="form-check-label fw-bold" for="commissionEnabled">Enable Commissions for Cashiers</label>
<div class="form-text">When enabled, commissions will be calculated for each order based on the cashier's commission rate.</div>
</div>
</div>
</div>
</div>
<!-- System Settings Tab -->
<div class="tab-pane fade" id="v-pills-system" role="tabpanel" aria-labelledby="v-pills-system-tab">
<h4 class="mb-4">System Settings</h4>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">System Timezone</label>
<select name="timezone" class="form-select" <?= !has_permission('settings_add') ? 'disabled' : '' ?> >
<?php
$timezones = timezone_identifiers_list();
$current_tz = $settings['timezone'] ?? 'UTC';
foreach ($timezones as $tz):
?>
<option value="<?= htmlspecialchars($tz) ?>" <?= $current_tz === $tz ? 'selected' : '' ?> >
<?= htmlspecialchars($tz) ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<hr class="my-4">
<h5 class="mb-3">Daily WhatsApp Summary Report</h5>
<div class="row">
<div class="col-md-12 mb-3">
<div class="form-check form-switch mt-2">
<input class="form-check-input" type="checkbox" name="whatsapp_report_enabled" id="whatsapp_report_enabled" <?= ($settings['whatsapp_report_enabled'] ?? 0) ? 'checked' : '' ?> <?= !has_permission('settings_add') ? 'disabled' : '' ?> />
<label class="form-check-label fw-bold" for="whatsapp_report_enabled">Enable Daily WhatsApp Report</label>
<div class="form-text">Sends a daily summary of orders per outlet (cash, card, bank, etc.) via Wablas.</div>
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">WhatsApp Number(s)</label>
<input type="text" name="whatsapp_report_number" class="form-control" value="<?= htmlspecialchars($settings['whatsapp_report_number'] ?? '') ?>" placeholder="e.g. 9689XXXXXXX, 9689XXXXXXY" <?= !has_permission('settings_add') ? 'readonly' : '' ?> />
<div class="form-text">Numbers with country code to receive the daily summary. Separate multiple numbers with commas.</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Report Time</label>
<input type="time" name="whatsapp_report_time" class="form-control" value="<?= htmlspecialchars($settings['whatsapp_report_time'] ?? '23:59:00') ?>" <?= !has_permission('settings_add') ? 'readonly' : '' ?> />
<div class="form-text">System time when the report should be generated and sent.</div>
</div>
</div>
</div>
<!-- Branding Tab -->
<div class="tab-pane fade" id="v-pills-branding" role="tabpanel" aria-labelledby="v-pills-branding-tab">
<h4 class="mb-4">Branding</h4>
<div class="row align-items-center">
<div class="col-md-12 mb-4">
<label class="form-label">Company Logo</label>
<div class="d-flex align-items-center gap-3">
<?php if (!empty($settings['logo_url'])): ?>
<div class="border rounded p-2 bg-light d-flex align-items-center justify-content-center" style="width: 120px; height: 80px;">
<img src="<?= htmlspecialchars('../' . $settings['logo_url']) ?>" alt="Logo" style="max-height: 100%; max-width: 100%; object-fit: contain;">
</div>
<?php endif; ?>
<div class="flex-grow-1">
<?php if (has_permission('settings_add')): ?>
<input type="file" name="logo" class="form-control" accept="image/*">
<div class="form-text mt-2">Recommended: PNG or SVG with transparent background.</div>
<?php else: ?>
<span class="text-muted">Logo file</span>
<?php endif; ?>
</div>
</div>
</div>
<hr class="my-2">
<div class="col-md-12 mb-3 mt-3">
<label class="form-label">Favicon</label>
<div class="d-flex align-items-center gap-3">
<?php if (!empty($settings['favicon_url'])): ?>
<div class="border rounded p-2 bg-light d-flex align-items-center justify-content-center" style="width: 60px; height: 60px;">
<img src="<?= htmlspecialchars('../' . $settings['favicon_url']) ?>" alt="Favicon" style="max-height: 100%; max-width: 100%; object-fit: contain;">
</div>
<?php endif; ?>
<div class="flex-grow-1">
<?php if (has_permission('settings_add')): ?>
<input type="file" name="favicon" class="form-control" accept=".ico,.png,.svg">
<div class="form-text mt-2">Recommended: 32x32 ICO or PNG.</div>
<?php else: ?>
<span class="text-muted">Favicon file</span>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<?php if (has_permission('settings_add')): ?>
<div class="d-flex justify-content-end mb-5">
<button type="submit" class="btn btn-primary btn-lg px-5 shadow-sm">
<i class="bi bi-save me-2"></i> Save All Changes
</button>
</div>
<?php endif; ?>
</div>
</div>
</form>
<?php include 'includes/footer.php'; ?>

279
admin/customers.php Normal file
View File

@ -0,0 +1,279 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('customers_view');
$message = '';
// Handle Add/Edit Customer
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
$name = trim($_POST['name']);
$email = trim($_POST['email']);
$phone = trim($_POST['phone']);
$address = trim($_POST['address'] ?? '');
if (empty($name)) {
$message = '<div class="alert alert-danger">Customer name is required.</div>';
} else {
try {
if ($action === 'edit_customer' && $id) {
if (!has_permission('customers_edit') && !has_permission('customers_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to edit customers.</div>';
} else {
$stmt = $pdo->prepare("UPDATE customers SET name = ?, email = ?, phone = ?, address = ? WHERE id = ?");
$stmt->execute([$name, $email, $phone, $address, $id]);
$message = '<div class="alert alert-success">Customer updated successfully!</div>';
}
} elseif ($action === 'add_customer') {
if (!has_permission('customers_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to add customers.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO customers (name, email, phone, address) VALUES (?, ?, ?, ?)");
$stmt->execute([$name, $email, $phone, $address]);
// --- Send Welcome WhatsApp Message via Wablas ---
if (!empty($phone)) {
try {
require_once __DIR__ . '/../includes/WablasService.php';
$wablas = new WablasService($pdo);
$companyStmt = $pdo->query("SELECT company_name FROM company_settings LIMIT 1");
$companyName = $companyStmt->fetchColumn() ?: 'Our Restaurant';
$settingsStmt = $pdo->query("SELECT points_for_free_meal FROM loyalty_settings WHERE id = 1");
$settings = $settingsStmt->fetch(PDO::FETCH_ASSOC);
$threshold = $settings ? intval($settings['points_for_free_meal']) : 70;
$welcomeMsg = "Welcome *{$name}* to *{$companyName}*! 🎉\n\nThank you for registering. You can now earn loyalty points with every order!\n\nYou currently have 0 points. Collect {$threshold} points to earn a free meal!";
$wablas->sendMessage($phone, $welcomeMsg);
} catch (Exception $w) {
error_log("Wablas Admin Welcome Exception: " . $w->getMessage());
}
}
$message = '<div class="alert alert-success">Customer created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
// Handle Delete
if (isset($_GET['delete'])) {
if (!has_permission('customers_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete customers.</div>';
} else {
try {
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM customers WHERE id = ?")->execute([$id]);
header("Location: customers.php?deleted=1");
exit;
} catch (PDOException $e) {
if ($e->getCode() == '23000') {
$message = '<div class="alert alert-danger">Cannot delete this customer because they are linked to other records (e.g., orders).</div>';
} else {
$message = '<div class="alert alert-danger">Error deleting customer: ' . $e->getMessage() . '</div>';
}
}
}
}
if (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success">Customer deleted successfully!</div>';
}
$search = $_GET['search'] ?? '';
$params = [];
$query = "SELECT * FROM customers";
if ($search) {
$query .= " WHERE name LIKE ? OR phone LIKE ? OR email LIKE ?";
$params = ["%$search%", "%$search%", "%$search%"];
}
$query .= " ORDER BY id DESC";
$customers_pagination = paginate_query($pdo, $query, $params);
$customers = $customers_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1">Customer Relationship</h2>
<p class="text-muted mb-0">Manage your customer database and contact info</p>
</div>
<?php if (has_permission('customers_add')):
?>
<button class="btn btn-primary btn-lg shadow-sm" data-bs-toggle="modal" data-bs-target="#customerModal" onclick="prepareAddForm()" style="border-radius: 12px;">
<i class="bi bi-person-plus me-1"></i> Add Customer
</button>
<?php endif;
?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm mb-4 rounded-4">
<div class="card-body p-4">
<form method="GET" class="row g-3 align-items-center">
<div class="col-md-9">
<div class="input-group">
<span class="input-group-text bg-light border-0 text-muted"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control border-0 bg-light rounded-3" placeholder="Search by name, phone or email..." value="<?= htmlspecialchars($search) ?>" style="border-radius: 0 10px 10px 0;">
</div>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary px-4 w-100 rounded-pill fw-bold shadow-sm">Search Records</button>
</div>
</form>
</div>
</div>
<?php if (empty($customers)):
?>
<div class="text-center py-5 bg-white rounded-4 shadow-sm">
<i class="bi bi-people display-1 text-muted opacity-25 mb-3 d-block"></i>
<h4 class="text-dark">No customers found</h4>
<p class="text-muted">No results matching your search criteria.</p>
</div>
<?php else:
?>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Customer</th>
<th>Contact Info</th>
<th>Address</th>
<th>Points</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($customers as $customer):
?>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center py-2">
<div class="bg-primary-subtle text-primary rounded-circle d-flex align-items-center justify-content-center fw-bold me-3" style="width: 42px; height: 42px;">
<?= strtoupper(substr($customer['name'], 0, 1)) ?>
</div>
<div class="fw-bold text-dark fs-6"><?= htmlspecialchars($customer['name']) ?></div>
</div>
</td>
<td>
<div class="small fw-bold text-dark mb-1"><i class="bi bi-phone me-1 text-muted"></i><?= htmlspecialchars($customer['phone'] ?: '-') ?></div>
<div class="small text-muted"><i class="bi bi-envelope me-1"></i><?= htmlspecialchars($customer['email'] ?: '-') ?></div>
</td>
<td>
<div class="small text-muted text-truncate" style="max-width: 200px;"><?= htmlspecialchars($customer['address'] ?: '-') ?></div>
</td>
<td>
<span class="badge bg-info-subtle text-info border border-info rounded-pill px-3"><?= number_format($customer['points'] ?? 0) ?> pts</span>
</td>
<td class="text-end pe-4">
<div class="d-inline-flex gap-2">
<?php if (has_permission('customers_edit') || has_permission('customers_add')):
?>
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-3"
data-bs-toggle="modal" data-bs-target="#customerModal"
onclick='prepareEditForm(<?= htmlspecialchars(json_encode($customer), ENT_QUOTES, "UTF-8") ?>)'>Edit</button>
<?php endif;
?>
<?php if (has_permission('customers_del')):
?>
<a href="?delete=<?= $customer['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('Delete customer? This will remove their loyalty history.')">Delete</a>
<?php endif;
?>
</div>
</td>
</tr>
<?php endforeach;
?>
</tbody>
</table>
</div>
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($customers_pagination); ?>
</div>
</div>
<?php endif;
?>
<!-- Customer Modal -->
<?php if (has_permission('customers_add') || has_permission('customers_edit')):
?>
<div class="modal fade" id="customerModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header bg-primary text-white border-0 py-3">
<h5 class="modal-title fw-bold" id="customerModalTitle">Add New Customer</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="customerForm">
<div class="modal-body p-4">
<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 text-muted">FULL NAME <span class="text-danger">*</span></label>
<input type="text" name="name" id="customerName" class="form-control rounded-3 border-0 bg-light" required>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">PHONE NUMBER</label>
<input type="text" name="phone" id="customerPhone" class="form-control rounded-3 border-0 bg-light">
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">EMAIL ADDRESS</label>
<input type="email" name="email" id="customerEmail" class="form-control rounded-3 border-0 bg-light">
</div>
</div>
<div class="mb-0">
<label class="form-label small fw-bold text-muted">ADDRESS</label>
<textarea name="address" id="customerAddress" class="form-control rounded-3 border-0 bg-light" rows="3" placeholder="Street, City, State..."></textarea>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-modal="modal" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm">Save Customer Profile</button>
</div>
</form>
</div>
</div>
</div>
<script>
function prepareAddForm() {
document.getElementById('customerModalTitle').innerText = 'Add New Customer';
document.getElementById('customerAction').value = 'add_customer';
document.getElementById('customerForm').reset();
document.getElementById('customerId').value = '';
}
function prepareEditForm(customer) {
if (!customer) return;
document.getElementById('customerModalTitle').innerText = 'Edit Customer Profile';
document.getElementById('customerAction').value = 'edit_customer';
document.getElementById('customerId').value = customer.id;
document.getElementById('customerName').value = customer.name || '';
document.getElementById('customerPhone').value = customer.phone || '';
document.getElementById('customerEmail').value = customer.email || '';
document.getElementById('customerAddress').value = customer.address || '';
}
</script>
<?php endif;
?>
<?php include 'includes/footer.php'; ?>

View File

@ -0,0 +1,250 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../db/config.php";
require_permission("expense_categories_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
// Handle Add/Edit Expense Category
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$name = trim($_POST['name']);
$name_ar = trim($_POST['name_ar'] ?? '');
$description = trim($_POST['description']);
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
if (empty($name)) {
$message = '<div class="alert alert-danger">Category name is required.</div>';
} else {
try {
if ($action === 'edit_expense_category' && $id) {
if (!has_permission('expense_categories_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("UPDATE expense_categories SET name = ?, name_ar = ?, description = ? WHERE id = ?");
$stmt->execute([$name, $name_ar, $description, $id]);
$message = '<div class="alert alert-success">Expense category updated successfully!</div>';
}
} elseif ($action === 'add_expense_category') {
if (!has_permission('expense_categories_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO expense_categories (name, name_ar, description) VALUES (?, ?, ?)");
$stmt->execute([$name, $name_ar, $description]);
$message = '<div class="alert alert-success">Expense category created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
// Handle Delete (Soft Delete)
if (isset($_GET['delete'])) {
if (!has_permission('expense_categories_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete expense categories.</div>';
} else {
try {
$id = (int)$_GET['delete'];
// Soft delete to preserve relations with expenses
$pdo->prepare("UPDATE expense_categories SET is_deleted = 1 WHERE id = ?")->execute([$id]);
header("Location: expense_categories.php?deleted=1");
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Error removing category: ' . $e->getMessage() . '</div>';
}
}
}
if (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success">Expense category removed successfully!</div>';
}
$query = "SELECT * FROM expense_categories WHERE is_deleted = 0 ORDER BY name ASC";
$expense_categories_pagination = paginate_query($pdo, $query);
$expense_categories = $expense_categories_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Expense Categories</h2>
<?php if (has_permission('expense_categories_add')): ?>
<button class="btn btn-primary" onclick="openAddModal()">
<i class="bi bi-plus-lg"></i> Add Category
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($expense_categories_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">ID</th>
<th>Name</th>
<th>Arabic Name</th>
<th>Description</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($expense_categories as $cat): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $cat['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($cat['name']) ?></td>
<td><?= htmlspecialchars($cat['name_ar'] ?: '-') ?></td>
<td><small class="text-muted"><?= htmlspecialchars($cat['description'] ?: '-') ?></small></td>
<td class="text-end pe-4">
<?php if (has_permission('expense_categories_add')): ?>
<button type="button" class="btn btn-sm btn-outline-primary me-1"
onclick='openEditModal(<?= htmlspecialchars(json_encode($cat), ENT_QUOTES, "UTF-8") ?>)' title="Edit"><i class="bi bi-pencil"></i></button>
<?php endif; ?>
<?php if (has_permission('expense_categories_del')): ?>
<a href="?delete=<?= $cat['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('<?= t('are_you_sure') ?>')"><i class="bi bi-trash"></i></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($expense_categories)): ?>
<tr>
<td colspan="5" class="text-center py-4 text-muted">No expense categories found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($expense_categories_pagination); ?>
</div>
</div>
</div>
<!-- Expense Category Modal -->
<?php if (has_permission('expense_categories_add')): ?>
<div class="modal fade" id="expenseCategoryModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="expenseCategoryModalTitle">Add New Category</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="expenseCategoryForm">
<div class="modal-body">
<input type="hidden" name="action" id="expenseCategoryAction" value="add_expense_category">
<input type="hidden" name="id" id="expenseCategoryId">
<div class="mb-3">
<label class="form-label">Category Name <span class="text-danger">*</span></label>
<div class="input-group">
<input type="text" name="name" id="expenseCategoryName" class="form-control" required>
<button class="btn btn-outline-secondary" type="button" id="btnTranslate">
<i class="bi bi-translate text-primary"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">Arabic Name</label>
<input type="text" name="name_ar" id="expenseCategoryNameAr" class="form-control" dir="rtl">
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="description" id="expenseCategoryDescription" class="form-control" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Category</button>
</div>
</form>
</div>
</div>
</div>
<script>
function getExpenseCategoryModal() {
if (typeof bootstrap === 'undefined') return null;
const el = document.getElementById('expenseCategoryModal');
return el ? bootstrap.Modal.getOrCreateInstance(el) : null;
}
function openAddModal() {
const modal = getExpenseCategoryModal();
if (!modal) return;
document.getElementById('expenseCategoryModalTitle').innerText = 'Add New Category';
document.getElementById('expenseCategoryAction').value = 'add_expense_category';
document.getElementById('expenseCategoryForm').reset();
document.getElementById('expenseCategoryId').value = '';
modal.show();
}
function openEditModal(cat) {
if (!cat) return;
const modal = getExpenseCategoryModal();
if (!modal) return;
document.getElementById('expenseCategoryModalTitle').innerText = 'Edit Category';
document.getElementById('expenseCategoryAction').value = 'edit_expense_category';
document.getElementById('expenseCategoryId').value = cat.id;
document.getElementById('expenseCategoryName').value = cat.name || '';
document.getElementById('expenseCategoryNameAr').value = cat.name_ar || '';
document.getElementById('expenseCategoryDescription').value = cat.description || '';
modal.show();
}
document.getElementById('btnTranslate').addEventListener('click', function() {
const text = document.getElementById('expenseCategoryName').value;
if (!text) {
alert('Please enter a category name first.');
return;
}
const btn = this;
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm text-primary" role="status" aria-hidden="true"></span>';
fetch('../api/translate.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: text,
target_lang: 'Arabic'
}),
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('expenseCategoryNameAr').value = data.translated_text;
} else {
alert('Translation failed: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred during translation.');
})
.finally(() => {
btn.disabled = false;
btn.innerHTML = originalHtml;
});
});
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

295
admin/expenses.php Normal file
View File

@ -0,0 +1,295 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("expenses_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
// Handle Create/Update
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if ($_POST['action'] === 'save_expense') {
if (!has_permission('expenses_add') && !has_permission('expenses_edit')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission.</div>';
} else {
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
$category_id = $_POST['category_id'];
$outlet_id = $_POST['outlet_id'];
$amount = $_POST['amount'];
$description = trim($_POST['description']);
$expense_date = $_POST['expense_date'];
if (empty($category_id) || empty($amount) || empty($expense_date)) {
$message = '<div class="alert alert-danger">Category, amount, and date are required.</div>';
} else {
try {
if ($id) {
$stmt = $pdo->prepare("UPDATE expenses SET category_id = ?, outlet_id = ?, amount = ?, description = ?, expense_date = ? WHERE id = ?");
$stmt->execute([$category_id, $outlet_id, $amount, $description, $expense_date, $id]);
$message = '<div class="alert alert-success">Expense updated successfully!</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO expenses (category_id, outlet_id, amount, description, expense_date) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$category_id, $outlet_id, $amount, $description, $expense_date]);
$message = '<div class="alert alert-success">Expense recorded successfully!</div>';
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
}
}
// Handle Delete
if (isset($_GET['delete'])) {
if (!has_permission('expenses_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete expenses.</div>';
} else {
try {
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM expenses WHERE id = ?")->execute([$id]);
header("Location: expenses.php?deleted=1");
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Error deleting expense: ' . $e->getMessage() . '</div>';
}
}
}
if (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success">Expense deleted successfully!</div>';
}
$expense_categories = $pdo->query("SELECT * FROM expense_categories ORDER BY name")->fetchAll();
$outlets = $pdo->query("SELECT * FROM outlets ORDER BY name")->fetchAll();
$search = $_GET['search'] ?? '';
$category_filter = $_GET['category_filter'] ?? '';
$outlet_filter = $_GET['outlet_filter'] ?? '';
$params = [];
$where = [];
$query = "SELECT e.*, ec.name as category_name, o.name as outlet_name
FROM expenses e
LEFT JOIN expense_categories ec ON e.category_id = ec.id
LEFT JOIN outlets o ON e.outlet_id = o.id";
if ($search) {
$where[] = "e.description LIKE ?";
$params[] = "%$search%";
}
if ($category_filter) {
$where[] = "e.category_id = ?";
$params[] = $category_filter;
}
if ($outlet_filter) {
$where[] = "e.outlet_id = ?";
$params[] = $outlet_filter;
}
if (!empty($where)) {
$query .= " WHERE " . implode(" AND ", $where);
}
$query .= " ORDER BY e.expense_date DESC, e.id DESC";
$expenses_pagination = paginate_query($pdo, $query, $params);
$expenses = $expenses_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1">Expenses</h2>
<p class="text-muted mb-0">Track and manage business expenditures</p>
</div>
<?php if (has_permission('expenses_add')): ?>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#expenseModal" onclick="resetExpenseModal()">
<i class="bi bi-plus-lg"></i> Add Expense
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-4">
<input type="text" name="search" class="form-control" placeholder="Search description..." value="<?= htmlspecialchars($search) ?>">
</div>
<div class="col-md-3">
<select name="category_filter" class="form-select">
<option value="">All Categories</option>
<?php foreach ($expense_categories as $cat): ?>
<option value="<?= $cat['id'] ?>" <?= $category_filter == $cat['id'] ? 'selected' : '' ?>><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<select name="outlet_filter" class="form-select">
<option value="">All Outlets</option>
<?php foreach ($outlets as $outlet): ?>
<option value="<?= $outlet['id'] ?>" <?= $outlet_filter == $outlet['id'] ? 'selected' : '' ?>><?= htmlspecialchars($outlet['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2 d-flex gap-2">
<button type="submit" class="btn btn-outline-primary w-100">Filter</button>
<?php if ($search || $category_filter || $outlet_filter): ?>
<a href="expenses.php" class="btn btn-outline-secondary"><i class="bi bi-x"></i></a>
<?php endif; ?>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($expenses_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Date</th>
<th>Category</th>
<th>Outlet</th>
<th>Description</th>
<th>Amount</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($expenses as $exp): ?>
<tr>
<td class="ps-4 fw-medium"><?= date('M d, Y', strtotime($exp['expense_date'])) ?></td>
<td><span class="badge bg-info bg-opacity-10 text-info"><?= htmlspecialchars($exp['category_name']) ?></span></td>
<td><?= htmlspecialchars($exp['outlet_name']) ?></td>
<td><?= htmlspecialchars($exp['description']) ?></td>
<td class="fw-bold"><?= format_currency($exp['amount']) ?></td>
<td class="text-end pe-4">
<?php if (has_permission('expenses_edit')): ?>
<button type="button" class="btn btn-sm btn-outline-primary me-1"
data-bs-toggle="modal"
data-bs-target="#expenseModal"
onclick='editExpense(<?= htmlspecialchars(json_encode($exp), ENT_QUOTES, 'UTF-8') ?>)'>
<i class="bi bi-pencil"></i>
</button>
<?php endif; ?>
<?php if (has_permission('expenses_del')): ?>
<a href="?delete=<?= $exp['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure?')"><i class="bi bi-trash"></i></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($expenses)): ?>
<tr>
<td colspan="6" class="text-center py-4 text-muted">No expenses found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($expenses_pagination); ?>
</div>
</div>
</div>
<!-- Expense Modal -->
<div class="modal fade" id="expenseModal" tabindex="-1" aria-labelledby="expenseModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form method="POST">
<input type="hidden" name="action" value="save_expense">
<input type="hidden" name="id" id="expense_id">
<div class="modal-header">
<h5 class="modal-title" id="expenseModalLabel">Add New Expense</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Category <span class="text-danger">*</span></label>
<select name="category_id" id="expense_category_id" class="form-select" required>
<option value="">Select Category</option>
<?php foreach ($expense_categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Outlet <span class="text-danger">*</span></label>
<select name="outlet_id" id="expense_outlet_id" class="form-select" required>
<?php foreach ($outlets as $outlet): ?>
<option value="<?= $outlet['id'] ?>"><?= htmlspecialchars($outlet['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Amount <span class="text-danger">*</span></label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" step="0.01" name="amount" id="expense_amount" class="form-control" required>
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Date <span class="text-danger">*</span></label>
<input type="date" name="expense_date" id="expense_date" class="form-control" value="<?= date('Y-m-d') ?>" required>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea name="description" id="expense_description" class="form-control" rows="3" placeholder="What was this expense for?"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary" id="expenseSubmitBtn">Record Expense</button>
</div>
</form>
</div>
</div>
</div>
<script>
function resetExpenseModal() {
document.getElementById('expenseModalLabel').innerText = 'Add New Expense';
document.getElementById('expenseSubmitBtn').innerText = 'Record Expense';
document.getElementById('expense_id').value = '';
document.getElementById('expense_category_id').value = '';
document.getElementById('expense_outlet_id').value = '<?= !empty($outlets) ? $outlets[0]['id'] : '' ?>';
document.getElementById('expense_amount').value = '';
document.getElementById('expense_date').value = '<?= date('Y-m-d') ?>';
document.getElementById('expense_description').value = '';
}
function editExpense(exp) {
document.getElementById('expenseModalLabel').innerText = 'Edit Expense';
document.getElementById('expenseSubmitBtn').innerText = 'Save Changes';
document.getElementById('expense_id').value = exp.id;
document.getElementById('expense_category_id').value = exp.category_id;
document.getElementById('expense_outlet_id').value = exp.outlet_id;
document.getElementById('expense_amount').value = exp.amount;
document.getElementById('expense_date').value = exp.expense_date;
document.getElementById('expense_description').value = exp.description;
}
</script>
<?php include 'includes/footer.php'; ?>

51
admin/includes/footer.php Normal file
View File

@ -0,0 +1,51 @@
<footer class="mt-5 pt-4 border-top text-center text-muted small">
<p>&copy; <?= date('Y') ?> <?= htmlspecialchars($companyName ?? 'Foody') ?>. All rights reserved.</p>
<p>Powered By Abidarcafe @2026</p>
</footer>
</div> <!-- End Main Content -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize all dropdowns manually to ensure they work
var dropdownElementList = [].slice.call(document.querySelectorAll('.dropdown-toggle'))
var dropdownList = dropdownElementList.map(function (dropdownToggleEl) {
return new bootstrap.Dropdown(dropdownToggleEl)
});
// Global SweetAlert2 replacement for confirm()
const confirmLinks = document.querySelectorAll('a[onclick^="return confirm"]');
confirmLinks.forEach(link => {
const originalOnClick = link.getAttribute('onclick');
// Extract the message from confirm('...')
const match = originalOnClick.match(/confirm\(['"](.+?)['"]\)/);
const message = match ? match[1] : 'Are you sure?';
// Remove the original onclick
link.removeAttribute('onclick');
// Add new click listener
link.addEventListener('click', function(e) {
e.preventDefault(); // Prevent default navigation
Swal.fire({
title: 'Are you sure?',
text: message,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Yes, proceed!'
}).then((result) => {
if (result.isConfirmed) {
window.location.href = this.href;
}
});
});
});
});
</script>
</body>
</html>

647
admin/includes/header.php Normal file
View File

@ -0,0 +1,647 @@
<?php
// Ensure functions are available
if (file_exists(__DIR__ . '/../../db/config.php')) {
require_once __DIR__ . '/../../db/config.php';
}
if (file_exists(__DIR__ . '/../../includes/functions.php')) {
require_once __DIR__ . '/../../includes/functions.php';
}
if (function_exists('init_session')) {
init_session();
} elseif (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (file_exists(__DIR__ . '/../../includes/lang.php')) {
require_once __DIR__ . '/../../includes/lang.php';
}
// Handle language switching
if (isset($_GET['lang'])) {
$allowed_langs = ['en', 'ar'];
if (in_array($_GET['lang'], $allowed_langs)) {
$_SESSION['lang'] = $_GET['lang'];
}
// Remove lang from URL to prevent infinite redirect or messy URLs
$current_url = strtok($_SERVER["REQUEST_URI"], '?');
$query = $_GET;
unset($query['lang']);
if (count($query) > 0) {
$current_url .= '?' . http_build_query($query);
}
header("Location: $current_url");
exit;
}
$currentLang = "en";
$isRTL = false;
// Require login for all admin pages
if (function_exists('require_login')) {
require_login();
}
// Trigger auto backup if needed - REMOVED from synchronous flow, moved to footer AJAX
$currentUser = function_exists('get_logged_user') ? get_logged_user() : null;
$userName = $currentUser['full_name'] ?? ($currentUser['username'] ?? 'Admin');
$userGroup = $currentUser['group_name'] ?? 'System';
$userInitial = strtoupper(substr($userName, 0, 1));
$userPic = $currentUser['profile_pic'] ?? null;
$companySettings = function_exists('get_company_settings') ? get_company_settings() : [];
$companyName = $companySettings['company_name'] ?? 'Foody';
$logoUrl = $companySettings['logo_url'] ?? '';
$faviconUrl = $companySettings['favicon_url'] ?? '';
// Simple active link helper
function isActive($page) {
return basename($_SERVER['PHP_SELF']) === $page ? 'active' : '';
}
// Group active helper
function isGroupActive($pages) {
return in_array(basename($_SERVER['PHP_SELF']), $pages) ? 'show' : '';
}
function isGroupExpanded($pages) {
return in_array(basename($_SERVER['PHP_SELF']), $pages) ? 'true' : 'false';
}
function getGroupToggleClass($pages) {
return in_array(basename($_SERVER['PHP_SELF']), $pages) ? '' : 'collapsed';
}
// Permission helper for sidebar
function can_view($module) {
if (!function_exists('has_permission')) return true;
return has_permission($module . '_view') || has_permission($module);
}
?>
<!doctype html>
<html lang="<?= $currentLang ?>" dir="<?= $isRTL ? 'rtl' : 'ltr' ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($companyName) ?> Admin Panel</title>
<?php if ($faviconUrl): ?>
<link rel="icon" href="../<?= htmlspecialchars($faviconUrl) ?>">
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<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;500;600;700&family=Noto+Sans+Arabic:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/custom.css?v=<?= time() ?>">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
/* Base styles using CSS variables for theming */
body {
font-family: 'Inter', 'Noto Sans Arabic', sans-serif;
background-color: var(--bg-body);
color: var(--text-primary);
}
.sidebar {
height: 100vh;
position: fixed;
top: 0;
<?= $isRTL ? 'right: 0; border-left: 1px solid var(--border-color); border-right: none;' : 'left: 0; border-right: 1px solid var(--border-color); border-left: none;' ?>
width: 250px;
background: var(--bg-sidebar);
padding-top: 0;
z-index: 1060;
overflow-y: auto;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.sidebar-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
background: var(--bg-sidebar);
z-index: 10;
transition: background-color 0.3s ease, border-color 0.3s ease;
}
.sidebar .nav-link {
color: var(--text-secondary);
padding: 0.6rem 1.5rem;
font-weight: 500;
font-size: 0.95rem;
transition: all 0.2s ease;
<?= $isRTL ? 'text-align: right;' : '' ?>
}
.sidebar .nav-link:hover, .sidebar .nav-link.active {
color: var(--sidebar-active-color);
background: var(--sidebar-active-bg);
<?= $isRTL ? 'border-left: 3px solid var(--sidebar-active-border); border-right: none;' : 'border-right: 3px solid var(--sidebar-active-border); border-left: none;' ?>
}
.sidebar-heading {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-heading);
padding: 1.25rem 1.5rem 0.5rem;
font-weight: 700;
display: flex;
align-items: center;
cursor: pointer; /* Clickable */
width: 100%;
background: none;
border: none;
<?= $isRTL ? 'text-align: right; flex-direction: row-reverse;' : 'text-align: left;' ?>
}
.sidebar-heading:hover {
color: var(--accent-color);
}
.sidebar-heading i:first-child {
font-size: 1rem;
<?= $isRTL ? 'margin-left: 0.5rem; margin-right: 0;' : 'margin-right: 0.5rem; margin-left: 0;' ?>
color: var(--accent-color);
}
.main-content { <?= $isRTL ? 'margin-right: 250px; margin-left: 0;' : 'margin-left: 250px; margin-right: 0;' ?> padding: 2rem; }
@media (max-width: 768px) {
.sidebar { transform: <?= $isRTL ? 'translateX(100%)' : 'translateX(-100%)' ?>; transition: transform 0.3s ease; top: 0; height: 100vh; }
.sidebar.show { transform: translateX(0); }
.main-content { margin-left: 0; margin-right: 0; }
.sidebar-header { display: none !important; } /* Hide duplicate logo on mobile */
}
.stat-card {
border: none;
border-radius: 12px;
background-color: var(--bg-card);
box-shadow: var(--shadow);
transition: transform 0.2s, background-color 0.3s;
}
.stat-card:hover { transform: translateY(-3px); }
.icon-box {
width: 48px;
height: 48px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
background-color: var(--bg-body); /* subtle background for icons */
color: var(--accent-color);
}
/* Dropdown theming override */
.dropdown-menu {
background-color: var(--bg-card);
border-color: var(--border-color);
<?= $isRTL ? 'text-align: right;' : '' ?>
}
.dropdown-item {
color: var(--text-primary);
<?= $isRTL ? 'text-align: right;' : '' ?>
}
.dropdown-item i {
<?= $isRTL ? 'margin-left: 0.5rem; margin-right: 0;' : 'margin-right: 0.5rem; margin-left: 0;' ?>
}
.dropdown-item:hover {
background-color: var(--bg-body);
color: var(--accent-color);
}
.dropdown-header {
color: var(--text-secondary);
<?= $isRTL ? 'text-align: right;' : '' ?>
}
.offcanvas-start {
<?= $isRTL ? 'right: 0; left: auto; transform: translateX(100%);' : '' ?>
}
.offcanvas-start.show {
<?= $isRTL ? 'transform: translateX(0);' : '' ?>
}
</style>
<script>
// Theme Switcher Logic
function setTheme(themeName) {
if (themeName === 'default') {
document.documentElement.removeAttribute('data-theme');
localStorage.removeItem('theme');
} else {
document.documentElement.setAttribute('data-theme', themeName);
localStorage.setItem('theme', themeName);
}
}
// Apply theme immediately on load to prevent flash
(function() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme);
}
})();
</script>
</head>
<body>
<!-- Mobile Header -->
<nav class="navbar navbar-light border-bottom d-md-none fixed-top" style="background-color: var(--bg-card);">
<div class="container-fluid">
<a class="navbar-brand fw-bold" href="index.php" style="color: var(--text-primary);">
<?php if ($logoUrl): ?>
<img src="../<?= htmlspecialchars($logoUrl) ?>" alt="Logo" style="height: 30px;">
<?php else: ?>
<?= htmlspecialchars($companyName) ?><span style="color: var(--accent-color);">Admin</span>
<?php endif; ?>
</a>
<button class="navbar-toggler border-0" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<!-- Sidebar -->
<div class="offcanvas-md offcanvas-<?= $isRTL ? 'end' : 'start' ?> sidebar" tabindex="-1" id="sidebarMenu">
<div class="d-flex align-items-center justify-content-center sidebar-header d-none d-md-flex">
<a href="index.php" class="text-decoration-none">
<?php if ($logoUrl): ?>
<img src="../<?= htmlspecialchars($logoUrl) ?>" alt="Logo" style="max-height: 50px; max-width: 100%;">
<?php else: ?>
<h4 class="fw-bold m-0" style="color: var(--text-primary);"><?= htmlspecialchars($companyName) ?><span style="color: var(--accent-color);">.</span></h4>
<?php endif; ?>
</a>
</div>
<div class="px-4 py-3 d-md-none d-flex justify-content-between align-items-center">
<h5 class="fw-bold m-0" style="color: var(--text-primary);"><?= t('menu_management') ?></h5><button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#sidebarMenu" aria-label="Close"></button>
</div>
<div class="sidebar-content accordion" id="sidebarAccordion">
<ul class="nav flex-column">
<?php if (can_view('dashboard')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('index.php') ?>" href="index.php">
<i class="bi bi-grid me-2"></i> <?= t('dashboard') ?>
</a>
</li>
<?php endif; ?>
</ul>
<?php
$posGroup = ['orders.php', 'ads.php', 'ad_edit.php'];
$canViewPosGroup = can_view('pos') || can_view('orders') || can_view('kitchen') || can_view('ads');
if ($canViewPosGroup):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($posGroup) ?>"
data-bs-toggle="collapse" href="#collapsePos" role="button" aria-expanded="<?= isGroupExpanded($posGroup) ?>" aria-controls="collapsePos">
<span><i class="bi bi-shop"></i> <?= t('pos_operations') ?></span>
<i class="bi bi-chevron-down chevron-icon"></i>
</a>
<div class="collapse <?= isGroupActive($posGroup) ?>" id="collapsePos" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column">
<?php if (can_view('pos')): ?>
<li class="nav-item">
<a class="nav-link" href="../pos.php" target="_blank">
<i class="bi bi-display me-2"></i> <?= t('pos_terminal') ?>
</a>
</li>
<?php endif; ?>
<?php if (can_view('orders')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('orders.php') ?>" href="orders.php">
<i class="bi bi-receipt me-2"></i> <?= t('orders_pos') ?>
</a>
</li>
<?php endif; ?>
<?php if (can_view('kitchen')): ?>
<li class="nav-item">
<a class="nav-link" href="../kitchen.php" target="_blank">
<i class="bi bi-fire me-2"></i> <?= t('kitchen_view') ?>
</a>
</li>
<?php endif; ?>
<?php if (can_view('ads')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('ads.php') || isActive('ad_edit.php') ? 'active' : '' ?>" href="ads.php">
<i class="bi bi-megaphone me-2"></i> <?= t('ads_management') ?>
</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
<?php endif; ?>
<?php
$menuGroup = ['products.php', 'product_edit.php', 'categories.php', 'category_edit.php', 'product_variants.php'];
$canViewMenuGroup = can_view('products') || can_view('categories');
if ($canViewMenuGroup):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($menuGroup) ?>"
data-bs-toggle="collapse" href="#collapseMenu" role="button" aria-expanded="<?= isGroupExpanded($menuGroup) ?>" aria-controls="collapseMenu">
<span><i class="bi bi-menu-button-wide"></i> <?= t('menu_management') ?></span>
<i class="bi bi-chevron-down chevron-icon"></i>
</a>
<div class="collapse <?= isGroupActive($menuGroup) ?>" id="collapseMenu" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column">
<?php if (can_view('products')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('products.php') ?>" href="products.php">
<i class="bi bi-box-seam me-2"></i> <?= t('products') ?>
</a>
</li>
<?php endif; ?>
<?php if (can_view('categories')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('categories.php') ?>" href="categories.php">
<i class="bi bi-tags me-2"></i> <?= t('categories') ?>
</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
<?php endif; ?>
<?php
$setupGroup = ['outlets.php', 'outlet_edit.php', 'areas.php', 'area_edit.php', 'tables.php', 'table_edit.php'];
$canViewSetupGroup = can_view('outlets') || can_view('areas') || can_view('tables');
if ($canViewSetupGroup):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($setupGroup) ?>"
data-bs-toggle="collapse" href="#collapseSetup" role="button" aria-expanded="<?= isGroupExpanded($setupGroup) ?>" aria-controls="collapseSetup">
<span><i class="bi bi-gear-wide-connected"></i> <?= t('restaurant_setup') ?></span>
<i class="bi bi-chevron-down chevron-icon"></i>
</a>
<div class="collapse <?= isGroupActive($setupGroup) ?>" id="collapseSetup" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column">
<?php if (can_view('outlets')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('outlets.php') ?>" href="outlets.php">
<i class="bi bi-shop me-2"></i> <?= t('outlets') ?>
</a>
</li>
<?php endif; ?>
<?php if (can_view('areas')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('areas.php') ?>" href="areas.php">
<i class="bi bi-geo-alt me-2"></i> <?= t('areas') ?>
</a>
</li>
<?php endif; ?>
<?php if (can_view('tables')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('tables.php') ?>" href="tables.php">
<i class="bi bi-ui-checks-grid me-2"></i> <?= t('tables') ?>
</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
<?php endif; ?>
<?php
$peopleGroup = ['customers.php', 'suppliers.php', 'loyalty.php'];
$canViewPeopleGroup = can_view('customers') || can_view('suppliers') || can_view('loyalty');
if ($canViewPeopleGroup):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($peopleGroup) ?>"
data-bs-toggle="collapse" href="#collapsePeople" role="button" aria-expanded="<?= isGroupExpanded($peopleGroup) ?>" aria-controls="collapsePeople">
<span><i class="bi bi-people"></i> <?= t('people_partners') ?></span>
<i class="bi bi-chevron-down chevron-icon"></i>
</a>
<div class="collapse <?= isGroupActive($peopleGroup) ?>" id="collapsePeople" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column">
<?php if (can_view('customers')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('customers.php') ?>" href="customers.php">
<i class="bi bi-people-fill me-2"></i> <?= t('customers') ?>
</a>
</li>
<?php endif; ?>
<?php if (can_view('suppliers')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('suppliers.php') ?>" href="suppliers.php">
<i class="bi bi-truck me-2"></i> <?= t('suppliers') ?>
</a>
</li>
<?php endif; ?>
<?php if (can_view('loyalty')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('loyalty.php') ?>" href="loyalty.php">
<i class="bi bi-award me-2"></i> <?= t('loyalty') ?>
</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
<?php endif; ?>
<?php
$financialsGroup = ['expenses.php', 'expense_categories.php', 'expense_category_edit.php', 'purchases.php'];
$canViewFinancialsGroup = can_view('expenses') || can_view('expense_categories') || can_view('purchases');
if ($canViewFinancialsGroup):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($financialsGroup) ?>"
data-bs-toggle="collapse" href="#collapseFinancials" role="button" aria-expanded="<?= isGroupExpanded($financialsGroup) ?>" aria-controls="collapseFinancials">
<span><i class="bi bi-wallet2"></i> <?= t('financials') ?></span>
<i class="bi bi-chevron-down chevron-icon"></i>
</a>
<div class="collapse <?= isGroupActive($financialsGroup) ?>" id="collapseFinancials" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column">
<?php if (can_view('purchases')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('purchases.php') ? 'active' : '' ?>" href="purchases.php">
<i class="bi bi-cart-plus me-2"></i> <?= t('purchases') ?>
</a>
</li>
<?php endif; ?>
<?php if (can_view('expenses')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('expenses.php') ? 'active' : '' ?>" href="expenses.php">
<i class="bi bi-cash-stack me-2"></i> <?= t('expenses') ?>
</a>
</li>
<?php endif; ?>
<?php if (can_view('expense_categories')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('expense_categories.php') || isActive('expense_category_edit.php') ? 'active' : '' ?>" href="expense_categories.php">
<i class="bi bi-tags me-2"></i> <?= t('expense_categories') ?>
</a>
</li>
<?php endif; ?>
</ul>
</div>
</div>
<?php endif; ?>
<?php
$reportsGroup = ['reports.php', 'report_products.php', 'report_staff.php', 'report_cashiers.php'];
if (can_view('reports')):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($reportsGroup) ?>"
data-bs-toggle="collapse" href="#collapseReports" role="button" aria-expanded="<?= isGroupExpanded($reportsGroup) ?>" aria-controls="collapseReports">
<span><i class="bi bi-bar-chart-line"></i> <?= t('reports_analytics') ?></span>
<i class="bi bi-chevron-down chevron-icon"></i>
</a>
<div class="collapse <?= isGroupActive($reportsGroup) ?>" id="collapseReports" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link <?= isActive('reports.php') ?>" href="reports.php">
<i class="bi bi-graph-up me-2"></i> <?= t('daily_reports') ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('report_products.php') ?>" href="report_products.php">
<i class="bi bi-box-seam me-2"></i> Product Sales
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('report_staff.php') ?>" href="report_staff.php">
<i class="bi bi-person-badge me-2"></i> Staff Sales
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('report_cashiers.php') ?>" href="report_cashiers.php">
<i class="bi bi-wallet2 me-2"></i> Cashier Sales
</a>
</li>
</ul>
</div>
</div>
<?php endif; ?>
<?php
$userGroupPages = [ 'ratings.php','users.php', 'user_edit.php', 'user_groups.php', 'user_group_edit.php', 'attendance.php'];
$canViewUserGroup = can_view('users') || can_view('user_groups');
if ($canViewUserGroup):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($userGroupPages) ?>"
data-bs-toggle="collapse" href="#collapseUsers" role="button" aria-expanded="<?= isGroupExpanded($userGroupPages) ?>" aria-controls="collapseUsers">
<span><i class="bi bi-person-badge"></i> <?= t('user_management') ?></span>
<i class="bi bi-chevron-down chevron-icon"></i>
</a>
<div class="collapse <?= isGroupActive($userGroupPages) ?>" id="collapseUsers" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column">
<?php if (can_view('users')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('users.php') ?>" href="users.php">
<i class="bi bi-people me-2"></i> <?= t('users') ?>
</a>
</li>
<?php endif; ?>
<?php if (can_view('user_groups')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('user_groups.php') ?>" href="user_groups.php">
<i class="bi bi-shield-lock me-2"></i> <?= t('roles_groups') ?>
</a>
</li>
<?php endif; ?>
<li class="nav-item">
<a class="nav-link <?= isActive('attendance.php') ?>" href="attendance.php">
<i class="bi bi-calendar-check me-2"></i> <?= t('attendance') ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('ratings.php') ?>" href="ratings.php">
<i class="bi bi-star me-2"></i> <?= t('staff_ratings') ?>
</a>
</li>
</ul>
</div>
</div>
<?php endif; ?>
<?php
$settingsGroup = ['payment_types.php', 'payment_type_edit.php', 'integrations.php', 'company.php', 'backup.php'];
$canViewSettingsGroup = can_view('payment_types') || can_view('settings');
if ($canViewSettingsGroup):
?>
<div class="nav-group">
<a class="sidebar-heading d-flex justify-content-between align-items-center text-decoration-none <?= getGroupToggleClass($settingsGroup) ?>"
data-bs-toggle="collapse" href="#collapseSettings" role="button" aria-expanded="<?= isGroupExpanded($settingsGroup) ?>" aria-controls="collapseSettings">
<span><i class="bi bi-sliders"></i> <?= t('settings') ?></span>
<i class="bi bi-chevron-down chevron-icon"></i>
</a>
<div class="collapse <?= isGroupActive($settingsGroup) ?>" id="collapseSettings" data-bs-parent="#sidebarAccordion">
<ul class="nav flex-column mb-5">
<?php if (can_view('payment_types')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('payment_types.php') ?>" href="payment_types.php">
<i class="bi bi-credit-card me-2"></i> <?= t('payment_types') ?>
</a>
</li>
<?php endif; ?>
<?php if (can_view('settings')): ?>
<li class="nav-item">
<a class="nav-link <?= isActive('integrations.php') ?>" href="integrations.php">
<i class="bi bi-plugin me-2"></i> <?= t('integrations') ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('company.php') ?>" href="company.php">
<i class="bi bi-building me-2"></i> <?= t('company') ?>
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('backup.php') ?>" href="backup.php">
<i class="bi bi-cloud-arrow-down me-2"></i> <?= t('backup_restore') ?>
</a>
</li>
<?php endif; ?>
<li class="nav-item border-top mt-2 pt-2">
<a class="nav-link text-muted" href="../index.php" target="_blank">
<i class="bi bi-box-arrow-up-right me-2"></i> <?= t('view_site') ?>
</a>
</li>
</ul>
</div>
</div>
<?php endif; ?>
</div>
</div>
<div class="main-content pt-5 pt-md-4">
<!-- Top Header -->
<header class="d-flex justify-content-end align-items-center mb-4 pb-3 border-bottom">
<div class="d-flex align-items-center gap-3">
<!-- Theme Switcher -->
<div class="dropdown">
<button class="btn btn-link text-decoration-none dropdown-toggle d-flex align-items-center gap-2" type="button" id="topThemeDropdown" data-bs-toggle="dropdown" aria-expanded="false" style="color: var(--text-primary);">
<i class="bi bi-palette"></i>
<span class="d-none d-sm-inline"><?= t('theme') ?></span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow border-0" aria-labelledby="topThemeDropdown">
<li><h6 class="dropdown-header"><?= t('select_theme') ?></h6></li>
<li><button class="dropdown-item d-flex align-items-center gap-2" onclick="setTheme('default')"><span class="d-inline-block rounded-circle" style="width:12px;height:12px;background:#eee;border:1px solid #ddd"></span> <?= t('default') ?></button></li>
<li><button class="dropdown-item d-flex align-items-center gap-2" onclick="setTheme('dark')"><span class="d-inline-block rounded-circle" style="width:12px;height:12px;background:#333"></span> <?= t('dark') ?></button></li>
<li><button class="dropdown-item d-flex align-items-center gap-2" onclick="setTheme('ocean')"><span class="d-inline-block rounded-circle" style="width:12px;height:12px;background:#0077B6"></span> <?= t('ocean') ?></button></li>
<li><button class="dropdown-item d-flex align-items-center gap-2" onclick="setTheme('forest')"><span class="d-inline-block rounded-circle" style="width:12px;height:12px;background:#2D6A4F"></span> <?= t('forest') ?></button></li>
<li><button class="dropdown-item d-flex align-items-center gap-2" onclick="setTheme('grape')"><span class="d-inline-block rounded-circle" style="width:12px;height:12px;background:#7B1FA2"></span> <?= t('grape') ?></button></li>
</ul>
</div>
<!-- User Profile -->
<div class="dropdown">
<a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false" style="color: var(--text-primary);">
<?php if ($userPic): ?>
<img src="../<?= htmlspecialchars($userPic) ?>?v=<?= time() ?>" alt="Profile" class="rounded-circle me-2 shadow-sm" style="width:38px;height:38px; object-fit: cover;">
<?php else: ?>
<div class="bg-primary bg-gradient rounded-circle d-flex align-items-center justify-content-center text-white me-2 shadow-sm" style="width:38px;height:38px; font-weight:600;"><?= $userInitial ?></div>
<?php endif; ?>
<span class="d-none d-sm-inline fw-medium"><?= htmlspecialchars($userName) ?></span>
</a>
<ul class="dropdown-menu dropdown-menu-end shadow border-0" aria-labelledby="userDropdown">
<li><span class="dropdown-item-text text-muted small"><?= t('signed_in_as') ?> <?= htmlspecialchars($userGroup) ?></span></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="profile.php"><i class="bi bi-person me-2"></i> <?= t('my_profile') ?></a></li>
<li><a class="dropdown-item" href="company.php"><i class="bi bi-building me-2"></i> <?= t('company_settings') ?></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="<?= get_base_url() ?>logout.php"><i class="bi bi-box-arrow-right me-2"></i> <?= t('logout') ?></a></li>
</ul>
</div>
</div>
</header>

443
admin/index.php Normal file
View File

@ -0,0 +1,443 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
// Ensure user is logged in first
require_login();
$pdo = db();
require_permission('dashboard_view');
// Check if user should see the detailed dashboard or the simplified one
// We'll use 'dashboard_add' as a proxy for 'detailed' access, or Super Admin (all)
$isDetailed = has_permission('dashboard_add') || has_permission('all');
if ($isDetailed) {
// Fetch Dashboard Stats
$today = date('Y-m-d');
$thisMonth = date('Y-m');
// Total Revenue Today
$stmt = $pdo->prepare("SELECT SUM(total_amount) FROM orders WHERE DATE(created_at) = ? AND status != 'cancelled'");
$stmt->execute([$today]);
$revenueToday = $stmt->fetchColumn() ?: 0;
// Total Orders Today
$stmt = $pdo->prepare("SELECT COUNT(*) FROM orders WHERE DATE(created_at) = ? AND status != 'cancelled'");
$stmt->execute([$today]);
$ordersToday = $stmt->fetchColumn();
// Total Revenue This Month
$stmt = $pdo->prepare("SELECT SUM(total_amount) FROM orders WHERE DATE_FORMAT(created_at, '%Y-%m') = ? AND status != 'cancelled'");
$stmt->execute([$thisMonth]);
$revenueThisMonth = $stmt->fetchColumn() ?: 0;
// Total Expenses This Month
$stmt = $pdo->prepare("SELECT SUM(amount) FROM expenses WHERE DATE_FORMAT(expense_date, '%Y-%m') = ?");
$stmt->execute([$thisMonth]);
$expensesThisMonth = $stmt->fetchColumn() ?: 0;
// Estimated Net Profit This Month
$netProfitThisMonth = $revenueThisMonth - $expensesThisMonth;
// Active Outlets
$outletsCount = $pdo->query("SELECT COUNT(*) FROM outlets")->fetchColumn();
// Total Products
$productsCount = $pdo->query("SELECT COUNT(*) FROM products")->fetchColumn();
// 1. Sales Trend (Last 12 Months)
$salesTrendQuery = "
SELECT DATE_FORMAT(created_at, '%b %Y') as month_label, SUM(total_amount) as total, DATE_FORMAT(created_at, '%Y-%m') as sort_key
FROM orders
WHERE status != 'cancelled'
AND created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
GROUP BY month_label, sort_key
ORDER BY sort_key ASC";
$salesTrend = $pdo->query($salesTrendQuery)->fetchAll();
// 2. Sales by Category (Pie Chart)
$salesByCategoryQuery = "
SELECT c.name as category_name, SUM(oi.quantity * oi.unit_price) as total_sales
FROM order_items oi
JOIN products p ON oi.product_id = p.id
JOIN categories c ON p.category_id = c.id
JOIN orders o ON oi.order_id = o.id
WHERE o.status != 'cancelled'
GROUP BY c.name
ORDER BY total_sales DESC";
$salesByCategory = $pdo->query($salesByCategoryQuery)->fetchAll();
// 3. Sales by Order Type (Pie Chart)
$salesByTypeQuery = "
SELECT order_type, SUM(total_amount) as total
FROM orders
WHERE status != 'cancelled'
GROUP BY order_type";
$salesByType = $pdo->query($salesByTypeQuery)->fetchAll();
// 4. Top 5 Items Sold
$topItemsQuery = "
SELECT p.name, SUM(oi.quantity) as total_qty
FROM order_items oi
JOIN products p ON oi.product_id = p.id
JOIN orders o ON oi.order_id = o.id
WHERE o.status != 'cancelled'
GROUP BY p.name
ORDER BY total_qty DESC
LIMIT 5";
$topItems = $pdo->query($topItemsQuery)->fetchAll();
// 5. Value Add: Average Order Value
$stmt = $pdo->query("SELECT AVG(total_amount) FROM orders WHERE status != 'cancelled'");
$avgOrderValue = $stmt->fetchColumn() ?: 0;
// Recent Orders
$recentOrders = $pdo->query("SELECT o.*,
(SELECT GROUP_CONCAT(p.name SEPARATOR ', ') FROM order_items oi JOIN products p ON oi.product_id = p.id WHERE oi.order_id = o.id) as items
FROM orders o ORDER BY created_at DESC LIMIT 5")->fetchAll();
}
include 'includes/header.php';
?>
<?php if ($isDetailed): ?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1">Dashboard</h2>
<p class="text-muted">Welcome back, <?= htmlspecialchars($userName) ?>!</p>
</div>
<div class="d-flex gap-2">
<?php if (has_permission('orders_view')): ?>
<a href="reports.php" class="btn btn-outline-primary"><i class="bi bi-file-earmark-bar-graph me-1"></i> Reports</a>
<?php endif; ?>
<?php if (has_permission('orders_add')): ?>
<a href="../pos.php" class="btn btn-primary shadow-sm"><i class="bi bi-plus-lg me-1"></i> New Order</a>
<?php endif; ?>
</div>
</div>
<div class="row g-4 mb-4">
<!-- Revenue Card -->
<div class="col-md-3">
<div class="card stat-card h-100 p-3">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center">
<div class="icon-box bg-success bg-opacity-10 text-success me-3">
<i class="bi bi-currency-dollar"></i>
</div>
<div>
<h6 class="text-muted mb-0">Today's Revenue</h6>
<h3 class="fw-bold mb-0"><?= format_currency($revenueToday) ?></h3>
</div>
</div>
</div>
</div>
</div>
<!-- Net Profit Card (Value Add) -->
<div class="col-md-3">
<div class="card stat-card h-100 p-3">
<div class="d-flex align-items-center">
<div class="icon-box bg-info bg-opacity-10 text-info me-3">
<i class="bi bi-bank"></i>
</div>
<div>
<h6 class="text-muted mb-0">Profit (This Month)</h6>
<h3 class="fw-bold mb-0 <?= $netProfitThisMonth >= 0 ? 'text-success' : 'text-danger' ?>"><?= format_currency($netProfitThisMonth) ?></h3>
</div>
</div>
</div>
</div>
<!-- Orders Card -->
<div class="col-md-3">
<div class="card stat-card h-100 p-3">
<div class="d-flex align-items-center">
<div class="icon-box bg-primary bg-opacity-10 text-primary me-3">
<i class="bi bi-receipt"></i>
</div>
<div>
<h6 class="text-muted mb-0">Orders Today</h6>
<h3 class="fw-bold mb-0"><?= $ordersToday ?></h3>
</div>
</div>
</div>
</div>
<!-- Average Order Value -->
<div class="col-md-3">
<div class="card stat-card h-100 p-3">
<div class="d-flex align-items-center">
<div class="icon-box bg-warning bg-opacity-10 text-warning me-3">
<i class="bi bi-calculator"></i>
</div>
<div>
<h6 class="text-muted mb-0">Avg. Order Value</h6>
<h3 class="fw-bold mb-0"><?= format_currency($avgOrderValue) ?></h3>
</div>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<!-- Sales Trend Graph -->
<div class="col-lg-8">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-bold">Sales Trend</h5>
<span class="badge bg-light text-dark border">Last 12 Months</span>
</div>
<div class="card-body">
<canvas id="salesTrendChart" style="min-height: 350px;"></canvas>
</div>
</div>
</div>
<!-- Sales Distribution (Two Pie Charts) -->
<div class="col-lg-4">
<div class="row g-4 h-100">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 fw-bold">Category Distribution</h5>
</div>
<div class="card-body">
<?php if (empty($salesByCategory)): ?>
<p class="text-muted text-center py-4">No data available</p>
<?php else: ?>
<canvas id="categoryPieChart" style="max-height: 180px;"></canvas>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 fw-bold">Order Type Distribution</h5>
</div>
<div class="card-body">
<?php if (empty($salesByType)): ?>
<p class="text-muted text-center py-4">No data available</p>
<?php else: ?>
<canvas id="typePieChart" style="max-height: 180px;"></canvas>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<!-- Top 5 Items Sold -->
<div class="col-lg-5">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white border-bottom py-3">
<h5 class="mb-0 fw-bold">Top 5 Items Sold (Qty)</h5>
</div>
<div class="card-body p-0">
<ul class="list-group list-group-flush">
<?php foreach ($topItems as $index => $item): ?>
<li class="list-group-item d-flex justify-content-between align-items-center py-3 border-0 border-bottom">
<div class="d-flex align-items-center">
<span class="badge bg-primary bg-opacity-10 text-primary rounded-circle me-3 d-flex align-items-center justify-content-center" style="width: 30px; height: 30px;"><?= $index + 1 ?></span>
<span class="fw-medium text-dark"><?= htmlspecialchars($item['name']) ?></span>
</div>
<span class="badge bg-light text-dark border fw-bold"><?= $item['total_qty'] ?> Sold</span>
</li>
<?php endforeach; ?>
<?php if (empty($topItems)): ?>
<li class="list-group-item text-center py-4 text-muted">No sales data yet.</li>
<?php endif; ?>
</ul>
</div>
</div>
</div>
<!-- Recent Orders Table -->
<div class="col-lg-7">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-bold">Recent Orders</h5>
<?php if (has_permission('orders_view')): ?>
<a href="orders.php" class="btn btn-sm btn-link text-decoration-none p-0">View All</a>
<?php endif; ?>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">ID</th>
<th>Total</th>
<th>Status</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentOrders as $order): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $order['id'] ?></td>
<td class="fw-bold"><?= format_currency($order['total_amount']) ?></td>
<td>
<span class="status-badge status-<?= $order['status'] ?> badge rounded-pill">
<?= ucfirst($order['status']) ?>
</span>
</td>
<td class="text-muted small"><?= date('M d, H:i', strtotime($order['created_at'])) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($recentOrders)): ?>
<tr><td colspan="4" class="text-center py-4 text-muted">No recent orders found.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Chart.js and Graph Logic -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const colors = ['#0d6efd', '#198754', '#ffc107', '#0dcaf0', '#6610f2', '#fd7e14', '#20c997', '#d63384', '#6f42c1', '#adb5bd'];
// 1. Sales Trend Chart
const salesTrendCtx = document.getElementById('salesTrendChart').getContext('2d');
new Chart(salesTrendCtx, {
type: 'line',
data: {
labels: <?= json_encode(array_column($salesTrend, 'month_label')) ?>,
datasets: [{
label: 'Monthly Revenue',
data: <?= json_encode(array_column($salesTrend, 'total')) ?>,
borderColor: '#0d6efd',
backgroundColor: 'rgba(13, 110, 253, 0.1)',
fill: true,
tension: 0.4,
borderWidth: 3,
pointBackgroundColor: '#fff',
pointBorderColor: '#0d6efd',
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
return value.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumSignificantDigits: 3 });
}
}
}
}
}
});
// 2. Category Pie Chart
<?php if (!empty($salesByCategory)): ?>
const categoryPieCtx = document.getElementById('categoryPieChart').getContext('2d');
new Chart(categoryPieCtx, {
type: 'doughnut',
data: {
labels: <?= json_encode(array_column($salesByCategory, 'category_name')) ?>,
datasets: [{
data: <?= json_encode(array_column($salesByCategory, 'total_sales')) ?>,
backgroundColor: colors,
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '70%',
plugins: {
legend: {
position: 'right',
labels: {
usePointStyle: true,
padding: 15,
font: { size: 10 }
}
}
}
}
});
<?php endif; ?>
// 3. Order Type Pie Chart
<?php if (!empty($salesByType)): ?>
const typePieCtx = document.getElementById('typePieChart').getContext('2d');
new Chart(typePieCtx, {
type: 'doughnut',
data: {
labels: <?= json_encode(array_map('ucfirst', array_column($salesByType, 'order_type'))) ?>,
datasets: [{
data: <?= json_encode(array_column($salesByType, 'total')) ?>,
backgroundColor: ['#6610f2', '#198754', '#fd7e14', '#ffc107'],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '70%',
plugins: {
legend: {
position: 'right',
labels: {
usePointStyle: true,
padding: 15,
font: { size: 10 }
}
}
}
}
});
<?php endif; ?>
});
</script>
<?php else: ?>
<!-- Simplified Dashboard -->
<div class="d-flex flex-column align-items-center justify-content-center py-5 mt-5">
<div class="mb-4">
<?php if ($logoUrl): ?>
<img src="../<?= htmlspecialchars($logoUrl) ?>" alt="<?= htmlspecialchars($companyName) ?>" style="max-height: 120px; max-width: 100%; filter: drop-shadow(0 10px 15px rgba(0,0,0,0.1));">
<?php else: ?>
<div class="bg-primary bg-opacity-10 text-primary p-4 rounded-circle mb-3 shadow-sm" style="width: 120px; height: 120px; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-shop fs-1"></i>
</div>
<?php endif; ?>
</div>
<h1 class="fw-bold text-center mb-2"><?= htmlspecialchars($companyName) ?></h1>
<p class="text-muted text-center fs-5 mb-4">Welcome to the Admin Panel, <?= htmlspecialchars($userName) ?>!</p>
<div class="d-flex gap-3 mt-4">
<?php if (has_permission('pos_view')): ?>
<a href="../pos.php" class="btn btn-primary btn-lg rounded-pill px-5 shadow-sm">
<i class="bi bi-display me-2"></i> POS Terminal
</a>
<?php endif; ?>
<?php if (has_permission('kitchen_view')): ?>
<a href="../kitchen.php" class="btn btn-outline-primary btn-lg rounded-pill px-5">
<i class="bi bi-fire me-2"></i> Kitchen View
</a>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

343
admin/integrations.php Normal file
View File

@ -0,0 +1,343 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../db/config.php";
require_permission("settings_view");
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/WablasService.php';
require_once __DIR__ . '/../mail/MailService.php';
$pdo = db();
$wablasTestResult = null;
$smtpTestResult = null;
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!has_permission('settings_add')) {
header("Location: integrations.php?error=permission_denied");
exit;
}
$provider = $_POST['provider'] ?? '';
$action = $_POST['action'] ?? 'save';
// Thawani
if ($provider === 'thawani') {
$keys = ['public_key', 'secret_key', 'environment'];
foreach ($keys as $k) {
$val = $_POST[$k] ?? '';
$stmt = $pdo->prepare("INSERT INTO integration_settings (provider, setting_key, setting_value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
$stmt->execute(['thawani', $k, $val]);
}
$message = 'saved';
if ($action === 'save') {
header("Location: integrations.php?msg=saved");
exit;
}
}
// Wablas
if ($provider === 'wablas') {
$keys = ['domain', 'token', 'secret_key', 'order_template', 'welcome_template', 'is_enabled'];
foreach ($keys as $k) {
$val = $_POST[$k] ?? '0';
if ($k === 'is_enabled' && !isset($_POST[$k])) {
$val = '0';
}
$stmt = $pdo->prepare("INSERT INTO integration_settings (provider, setting_key, setting_value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
$stmt->execute(['wablas', $k, $val]);
}
if ($action === 'save') {
header("Location: integrations.php?msg=saved");
exit;
} elseif ($action === 'test') {
// Instantiate service (loads from DB, which we just updated)
$wablasService = new WablasService($pdo);
$testPhone = $_POST['test_phone'] ?? '';
if (!empty($testPhone)) {
$wablasTestResult = $wablasService->sendMessage($testPhone, "Test message from Flatlogic POS. Connection verified!");
} else {
$wablasTestResult = $wablasService->testConnection();
}
}
}
// SMTP
if ($provider === 'smtp') {
$keys = ['host', 'port', 'secure', 'username', 'password', 'from_email', 'from_name'];
foreach ($keys as $k) {
$val = $_POST[$k] ?? '';
$stmt = $pdo->prepare("INSERT INTO integration_settings (provider, setting_key, setting_value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
$stmt->execute(['smtp', $k, $val]);
}
if ($action === 'save') {
header("Location: integrations.php?msg=saved");
exit;
} elseif ($action === 'test') {
$testEmail = $_POST['test_email'] ?? '';
if (!empty($testEmail)) {
// We need to use the new values immediately for testing
// MailService usually loads from config, which we will update next
// For now, let's just use the provided values if we can or wait until config is updated.
// Actually, let's update config first then test.
$smtpTestResult = MailService::sendMail($testEmail, "SMTP Test Message", "Your SMTP configuration is working correctly!", "Your SMTP configuration is working correctly!");
}
}
}
}
// Fetch current settings
$stmt = $pdo->query("SELECT provider, setting_key, setting_value FROM integration_settings");
$allSettings = $stmt->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC);
function getSetting($settings, $provider, $key) {
if (isset($settings[$provider])) {
foreach ($settings[$provider] as $s) {
if ($s['setting_key'] === $key) return $s['setting_value'];
}
}
return '';
}
$thawaniEnv = getSetting($allSettings, 'thawani', 'environment');
$thawaniPub = getSetting($allSettings, 'thawani', 'public_key');
$thawaniSec = getSetting($allSettings, 'thawani', 'secret_key');
$wablasDom = getSetting($allSettings, 'wablas', 'domain');
$wablasTok = getSetting($allSettings, 'wablas', 'token');
$wablasSecKey = getSetting($allSettings, 'wablas', 'secret_key');
$wablasTemplate = getSetting($allSettings, 'wablas', 'order_template');
$wablasWelcomeTemplate = getSetting($allSettings, 'wablas', 'welcome_template');
$wablasEnabled = getSetting($allSettings, 'wablas', 'is_enabled');
// SMTP Settings
$smtpHost = getSetting($allSettings, 'smtp', 'host');
$smtpPort = getSetting($allSettings, 'smtp', 'port') ?: '587';
$smtpSecure = getSetting($allSettings, 'smtp', 'secure') ?: 'tls';
$smtpUser = getSetting($allSettings, 'smtp', 'username');
$smtpPass = getSetting($allSettings, 'smtp', 'password');
$smtpFromEmail = getSetting($allSettings, 'smtp', 'from_email');
$smtpFromName = getSetting($allSettings, 'smtp', 'from_name');
// Default template if empty
if (empty($wablasTemplate)) {
$wablasTemplate = "Dear *{customer_name}*,
Thank you for dining with *{company_name}*! 🍽️
*Order Details:*
{order_details}
Total: *{total_amount}*
You've earned *{points_earned} points* with this order.
💰 *Current Balance: {new_balance} points*";
}
if (empty($wablasWelcomeTemplate)) {
$wablasWelcomeTemplate = "Welcome *{customer_name}* to *{company_name}*! 🎉
Thank you for registering. You can now earn loyalty points with every order!
You currently have 0 points. Collect {points_threshold} points to earn a free meal!";
}
require_once __DIR__ . '/includes/header.php';
?>
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="h3 mb-0 text-gray-800">Integrations</h2>
</div>
<?php if (isset($_GET['error']) && $_GET['error'] == 'permission_denied'): ?>
<div class="alert alert-danger border-0 shadow-sm rounded-3">Access Denied: You do not have permission to perform this action.</div>
<?php endif; ?>
<?php if (isset($_GET['msg']) && $_GET['msg'] == 'saved'): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
Settings saved successfully.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if ($wablasTestResult): ?>
<div class="alert alert-<?= $wablasTestResult['success'] ? 'success' : 'danger' ?> alert-dismissible fade show" role="alert">
<strong>Wablas Test Result:</strong> <?= htmlspecialchars($wablasTestResult['message']) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<?php if ($smtpTestResult): ?>
<div class="alert alert-<?= $smtpTestResult['success'] ? 'success' : 'danger' ?> alert-dismissible fade show" role="alert">
<strong>SMTP Test Result:</strong> <?= $smtpTestResult['success'] ? 'Success! Test email sent.' : 'Error: ' . htmlspecialchars($smtpTestResult['error']) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="row">
<!-- SMTP Settings -->
<div class="col-md-12 mb-4">
<div class="card shadow">
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 fw-bold text-primary">SMTP Configuration</h6>
</div>
<div class="card-body">
<form method="POST">
<input type="hidden" name="provider" value="smtp">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">SMTP Host</label>
<input type="text" class="form-control" name="host" placeholder="smtp.gmail.com" value="<?= htmlspecialchars($smtpHost) ?>" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
<div class="col-md-3 mb-3">
<label class="form-label">SMTP Port</label>
<input type="number" class="form-control" name="port" value="<?= htmlspecialchars($smtpPort) ?>" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Encryption</label>
<select class="form-select" name="secure" <?= !has_permission('settings_add') ? 'disabled' : '' ?>>
<option value="tls" <?= $smtpSecure == 'tls' ? 'selected' : '' ?>>TLS</option>
<option value="ssl" <?= $smtpSecure == 'ssl' ? 'selected' : '' ?>>SSL</option>
<option value="none" <?= $smtpSecure == 'none' ? 'selected' : '' ?>>None</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">SMTP Username</label>
<input type="text" class="form-control" name="username" value="<?= htmlspecialchars($smtpUser) ?>" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">SMTP Password</label>
<input type="password" class="form-control" name="password" value="<?= htmlspecialchars($smtpPass) ?>" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">From Email</label>
<input type="email" class="form-control" name="from_email" placeholder="noreply@yourdomain.com" value="<?= htmlspecialchars($smtpFromEmail) ?>" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">From Name</label>
<input type="text" class="form-control" name="from_name" placeholder="Business Name" value="<?= htmlspecialchars($smtpFromName) ?>" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
</div>
<?php if (has_permission('settings_add')): ?>
<div class="mb-3 border-top pt-3">
<label class="form-label text-muted small">Test SMTP Connection</label>
<div class="input-group">
<input type="email" class="form-control" name="test_email" placeholder="receiver@example.com" value="<?= htmlspecialchars($_POST['test_email'] ?? '') ?>">
<button type="submit" name="action" value="test" class="btn btn-info text-white">Save & Send Test Email</button>
</div>
</div>
<div class="d-flex justify-content-end">
<button type="submit" name="action" value="save" class="btn btn-primary">Save SMTP Settings</button>
</div>
<?php endif; ?>
</form>
</div>
</div>
</div>
<!-- Thawani -->
<div class="col-md-6 mb-4">
<div class="card shadow h-100">
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 fw-bold text-primary">Thawani Payments</h6>
</div>
<div class="card-body">
<form method="POST">
<input type="hidden" name="provider" value="thawani">
<div class="mb-3">
<label class="form-label">Environment</label>
<select class="form-select" name="environment" <?= !has_permission('settings_add') ? 'disabled' : '' ?>>
<option value="sandbox" <?= $thawaniEnv == 'sandbox' ? 'selected' : '' ?>>Sandbox</option>
<option value="production" <?= $thawaniEnv == 'production' ? 'selected' : '' ?>>Production</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Public Key</label>
<input type="text" class="form-control" name="public_key" value="<?= htmlspecialchars($thawaniPub) ?>" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
<div class="mb-3">
<label class="form-label">Secret Key</label>
<input type="password" class="form-control" name="secret_key" value="<?= htmlspecialchars($thawaniSec) ?>" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
<?php if (has_permission('settings_add')): ?>
<button type="submit" name="action" value="save" class="btn btn-primary">Save Thawani Settings</button>
<?php endif; ?>
</form>
</div>
</div>
</div>
<!-- Wablas -->
<div class="col-md-6 mb-4">
<div class="card shadow h-100">
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 fw-bold text-success">Wablas WhatsApp</h6>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_enabled" id="is_enabled_switch" form="wablas_form" value="1" <?= $wablasEnabled === '1' ? 'checked' : '' ?> <?= !has_permission('settings_add') ? 'disabled' : '' ?>>
<label class="form-check-label" for="is_enabled_switch">Enabled</label>
</div>
</div>
<div class="card-body">
<form method="POST" id="wablas_form">
<input type="hidden" name="provider" value="wablas">
<div class="mb-3">
<label class="form-label">Domain</label>
<input type="text" class="form-control" name="domain" placeholder="https://..." value="<?= htmlspecialchars($wablasDom) ?>" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
<div class="mb-3">
<label class="form-label">Token</label>
<input type="password" class="form-control" name="token" value="<?= htmlspecialchars($wablasTok) ?>" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
<div class="mb-3">
<label class="form-label">Secret Key</label>
<input type="password" class="form-control" name="secret_key" value="<?= htmlspecialchars($wablasSecKey) ?>" <?= !has_permission('settings_add') ? 'readonly' : '' ?>>
</div>
<div class="mb-3">
<label class="form-label">Order Notification Template</label>
<textarea class="form-control font-monospace" name="order_template" rows="8" <?= !has_permission('settings_add') ? 'readonly' : '' ?>><?= htmlspecialchars($wablasTemplate) ?></textarea>
<div class="form-text mt-2">
<strong>Available Variables:</strong><br>
<code>{customer_name}</code>, <code>{company_name}</code>, <code>{order_id}</code>,
<code>{order_details}</code> (list of items), <code>{total_amount}</code>, <code>{currency_symbol}</code>,
<code>{points_earned}</code>, <code>{points_redeemed}</code>, <code>{new_balance}</code>.
</div>
</div>
<div class="mb-3">
<label class="form-label">Welcome Message Template</label>
<textarea class="form-control font-monospace" name="welcome_template" rows="6" <?= !has_permission('settings_add') ? 'readonly' : '' ?>><?= htmlspecialchars($wablasWelcomeTemplate) ?></textarea>
<div class="form-text mt-2">
<strong>Available Variables:</strong><br>
<code>{customer_name}</code>, <code>{company_name}</code>, <code>{points_threshold}</code>.
</div>
</div>
<?php if (has_permission('settings_add')): ?>
<div class="mb-3 border-top pt-3">
<label class="form-label text-muted small">Test Configuration</label>
<div class="input-group">
<input type="text" class="form-control" name="test_phone" placeholder="e.g. 62812345678" value="<?= htmlspecialchars($_POST['test_phone'] ?? '') ?>">
<button type="submit" name="action" value="test" class="btn btn-info text-white">Test & Send Message</button>
</div>
</div>
<div class="d-flex justify-content-end">
<button type="submit" name="action" value="save" class="btn btn-success">Save Settings</button>
</div>
<?php endif; ?>
</form>
</div>
</div>
</div>
</div>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

213
admin/loyalty.php Normal file
View File

@ -0,0 +1,213 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../db/config.php";
require_permission("loyalty_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
// Handle Settings Update
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_settings'])) {
if (!has_permission('loyalty_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to update loyalty settings.</div>';
} else {
$points_per_order = intval($_POST['points_per_order']);
$points_for_free_meal = intval($_POST['points_for_free_meal']);
$is_enabled = isset($_POST['is_enabled']) ? 1 : 0;
$stmt = $pdo->prepare("UPDATE loyalty_settings SET points_per_order = ?, points_for_free_meal = ?, is_enabled = ? WHERE id = 1");
$stmt->execute([$points_per_order, $points_for_free_meal, $is_enabled]);
$message = '<div class="alert alert-success">Loyalty settings updated successfully!</div>';
}
}
// Fetch Settings
$stmt = $pdo->query("SELECT * FROM loyalty_settings WHERE id = 1");
$settings = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$settings) {
// Default fallback if migration failed or empty
$settings = ['points_per_order' => 10, 'points_for_free_meal' => 70, 'is_enabled' => 1];
}
// Fetch Customers with Points
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$limit = 20;
$offset = ($page - 1) * $limit;
$count_stmt = $pdo->query("SELECT COUNT(*) FROM customers WHERE points > 0");
$total_customers = $count_stmt->fetchColumn();
$total_pages = ceil($total_customers / $limit);
$query = "SELECT * FROM customers WHERE points > 0 ORDER BY points DESC LIMIT $limit OFFSET $offset";
$stmt = $pdo->query($query);
$customers = $stmt->fetchAll(PDO::FETCH_ASSOC);
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="d-flex align-items-center gap-3">
<h2 class="fw-bold mb-0">Loyalty Program</h2>
<?php if ($settings['is_enabled']): ?>
<span class="badge bg-success-subtle text-success border border-success-subtle px-3">Active</span>
<?php else: ?>
<span class="badge bg-danger-subtle text-danger border border-danger-subtle px-3">Disabled</span>
<?php endif; ?>
</div>
<?php if (has_permission('loyalty_add')): ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#settingsModal">
<i class="bi bi-gear-fill me-2"></i> Configure Settings
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="row mb-4">
<div class="col-md-6 col-lg-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-muted text-uppercase small">Current Configuration</h6>
<div class="d-flex justify-content-between align-items-center mt-3">
<div>
<span class="d-block text-muted small">Points per Loyalty Product</span>
<span class="fs-4 fw-bold text-primary"><?= $settings['points_per_order'] ?> pts</span>
</div>
<div class="border-start ps-3">
<span class="d-block text-muted small">Free Product Threshold</span>
<span class="fs-4 fw-bold text-success"><?= $settings['points_for_free_meal'] ?> pts</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h5 class="mb-0">Loyalty Members</h5>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Customer</th>
<th>Contact</th>
<th>Points Balance</th>
<th>Status</th>
<th>Joined</th>
</tr>
</thead>
<tbody>
<?php foreach ($customers as $customer): ?>
<?php
$progress = min(100, ($customer['points'] / $settings['points_for_free_meal']) * 100);
$eligible = $customer['points'] >= $settings['points_for_free_meal'];
?>
<tr>
<td class="ps-4">
<div class="fw-bold"><?= htmlspecialchars($customer['name']) ?></div>
</td>
<td>
<div class="small"><?= htmlspecialchars($customer['phone'] ?? '-') ?></div>
<div class="small text-muted"><?= htmlspecialchars($customer['email'] ?? '') ?></div>
</td>
<td>
<div class="d-flex align-items-center">
<span class="badge bg-warning text-dark border border-warning me-2" style="width: 60px;">
<?= $customer['points'] ?> pts
</span>
<div class="progress flex-grow-1" style="height: 6px; max-width: 100px;">
<div class="progress-bar bg-success" role="progressbar" style="width: <?= $progress ?>%"></div>
</div>
</div>
</td>
<td>
<?php if ($eligible): ?>
<span class="badge bg-success-subtle text-success border border-success-subtle">
<i class="bi bi-gift-fill me-1"></i> Eligible for Free Product
</span>
<?php else: ?>
<span class="text-muted small">
<?= $settings['points_for_free_meal'] - $customer['points'] ?> pts to go
</span>
<?php endif; ?>
</td>
<td class="text-muted small"><?= date('M d, Y', strtotime($customer['created_at'])) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($customers)): ?>
<tr>
<td colspan="5" class="text-center py-5 text-muted">No active loyalty members found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($total_pages > 1): ?>
<div class="card-footer bg-white py-3">
<nav>
<ul class="pagination justify-content-center mb-0">
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
<a class="page-link" href="?page=<?= $page - 1 ?>">Previous</a>
</li>
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
<li class="page-item <?= $page == $i ? 'active' : '' ?>">
<a class="page-link" href="?page=<?= $i ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
<li class="page-item <?= $page >= $total_pages ? 'disabled' : '' ?>">
<a class="page-link" href="?page=<?= $page + 1 ?>">Next</a>
</li>
</ul>
</nav>
</div>
<?php endif; ?>
</div>
<!-- Settings Modal -->
<?php if (has_permission('loyalty_add')): ?>
<div class="modal fade" id="settingsModal" tabindex="-1">
<div class="modal-dialog">
<form method="POST" class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Configure Loyalty Program</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" name="update_settings" value="1">
<div class="mb-4">
<div class="form-check form-switch p-0 d-flex justify-content-between align-items-center">
<label class="form-check-label fw-bold" for="is_enabled">Enable Loyalty Program</label>
<input class="form-check-input ms-0" type="checkbox" id="is_enabled" name="is_enabled" <?= $settings['is_enabled'] ? 'checked' : '' ?> style="width: 3em; height: 1.5em; cursor: pointer;">
</div>
<div class="form-text mt-1">When disabled, loyalty features will be hidden from the POS.</div>
</div>
<div class="mb-3">
<label class="form-label">Points per Loyalty Product</label>
<input type="number" name="points_per_order" class="form-control" value="<?= $settings['points_per_order'] ?>" min="0" required>
<div class="form-text">Points awarded for every loyalty-participating product in the order.</div>
</div>
<div class="mb-3">
<label class="form-label">Points for Free Product</label>
<input type="number" name="points_for_free_meal" class="form-control" value="<?= $settings['points_for_free_meal'] ?>" min="0" required>
<div class="form-text">Threshold points required to redeem a free product.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

166
admin/order_edit.php Normal file
View File

@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('orders_add');
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if (!$id) {
header("Location: orders.php");
exit;
}
$message = '';
// Handle Form Submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$status = $_POST['status'];
$outlet_id = (int)$_POST['outlet_id'];
$customer_id = !empty($_POST['customer_id']) ? (int)$_POST['customer_id'] : null;
$order_type = $_POST['order_type'];
$table_number = $_POST['table_number'];
$notes = $_POST['notes'];
$stmt = $pdo->prepare("UPDATE orders SET
status = ?,
outlet_id = ?,
customer_id = ?,
order_type = ?,
table_number = ?,
notes = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?");
if ($stmt->execute([$status, $outlet_id, $customer_id, $order_type, $table_number, $notes, $id])) {
$message = '<div class="alert alert-success border-0 shadow-sm rounded-3"><i class="bi bi-check-circle-fill me-2"></i>Order updated successfully!</div>';
// Redirect back after short delay or via header
header("Refresh: 2; url=order_view.php?id=$id");
} else {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3"><i class="bi bi-exclamation-triangle-fill me-2"></i>Error updating order.</div>';
}
}
// Fetch Order Details
$stmt = $pdo->prepare("SELECT * FROM orders WHERE id = ?");
$stmt->execute([$id]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$order) {
die("Order not found.");
}
// Fetch Outlets
$outlets = $pdo->query("SELECT id, name FROM outlets ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
// Fetch Customers
$customers = $pdo->query("SELECT id, name FROM customers ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Edit Order #<?= $order['id'] ?></h2>
<div class="d-flex gap-2">
<a href="order_view.php?id=<?= $id ?>" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Cancel
</a>
<a href="orders.php" class="btn btn-light border">
<i class="bi bi-list"></i> Back to List
</a>
</div>
</div>
<?= $message ?>
<div class="row">
<div class="col-md-8">
<div class="card border-0 shadow-sm">
<div class="card-body">
<form method="POST">
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label small fw-bold text-muted text-uppercase">Order Status</label>
<select name="status" class="form-select form-select-lg" required>
<option value="pending" <?= $order['status'] === 'pending' ? 'selected' : '' ?>>Pending</option>
<option value="preparing" <?= $order['status'] === 'preparing' ? 'selected' : '' ?>>Preparing</option>
<option value="ready" <?= $order['status'] === 'ready' ? 'selected' : '' ?>>Ready</option>
<option value="completed" <?= $order['status'] === 'completed' ? 'selected' : '' ?>>Completed</option>
<option value="cancelled" <?= $order['status'] === 'cancelled' ? 'selected' : '' ?>>Cancelled</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-muted text-uppercase">Outlet</label>
<select name="outlet_id" class="form-select form-select-lg" required>
<?php foreach ($outlets as $outlet): ?>
<option value="<?= $outlet['id'] ?>" <?= $order['outlet_id'] == $outlet['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($outlet['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label small fw-bold text-muted text-uppercase">Order Type</label>
<select name="order_type" class="form-select" required>
<option value="dine-in" <?= $order['order_type'] === 'dine-in' ? 'selected' : '' ?>>Dine-In</option>
<option value="takeaway" <?= $order['order_type'] === 'takeaway' ? 'selected' : '' ?>>Takeaway</option>
<option value="delivery" <?= $order['order_type'] === 'delivery' ? 'selected' : '' ?>>Delivery</option>
<option value="drive-thru" <?= $order['order_type'] === 'drive-thru' ? 'selected' : '' ?>>Drive-Thru</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-muted text-uppercase">Table Number</label>
<input type="text" name="table_number" class="form-control" value="<?= htmlspecialchars((string)($order['table_number'] ?? '')) ?>" placeholder="e.g. 5">
</div>
</div>
<div class="mb-4">
<label class="form-label small fw-bold text-muted text-uppercase">Customer</label>
<select name="customer_id" class="form-select">
<option value="">Guest (None)</option>
<?php foreach ($customers as $customer): ?>
<option value="<?= $customer['id'] ?>" <?= $order['customer_id'] == $customer['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($customer['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-4">
<label class="form-label small fw-bold text-muted text-uppercase">Order Notes</label>
<textarea name="notes" class="form-control" rows="4" placeholder="Add any special instructions or notes..."><?= htmlspecialchars((string)($order['notes'] ?? '')) ?></textarea>
</div>
<div class="d-flex justify-content-end gap-2 border-top pt-4 mt-4">
<a href="order_view.php?id=<?= $id ?>" class="btn btn-light rounded-pill px-4">Discard Changes</a>
<button type="submit" class="btn btn-primary rounded-pill px-4">Update Order Details</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm bg-light">
<div class="card-body">
<h6 class="fw-bold mb-3"><i class="bi bi-info-circle me-2"></i>Editing Order Information</h6>
<p class="small text-muted mb-3">Updating the status here will immediately reflect across all systems (Kitchen, POS, Admin).</p>
<div class="alert alert-warning border-0 small py-2 px-3">
<i class="bi bi-exclamation-triangle-fill me-1"></i> Changes to items should be handled via the POS system or directly in the database.
</div>
<div class="mt-4 pt-4 border-top">
<p class="small text-muted mb-1 text-uppercase fw-bold">Order Created</p>
<p class="mb-3"><?= date('M d, Y H:i:s', strtotime($order['created_at'])) ?></p>
<p class="small text-muted mb-1 text-uppercase fw-bold">Last Updated</p>
<p class="mb-0"><?= $order['updated_at'] ? date('M d, Y H:i:s', strtotime($order['updated_at'])) : 'Never' ?></p>
</div>
</div>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

591
admin/order_view.php Normal file
View File

@ -0,0 +1,591 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('orders_view');
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if (!$id) {
header("Location: orders.php");
exit;
}
// Fetch Order Details
$stmt = $pdo->prepare("SELECT o.*, ot.name as outlet_name, pt.name as payment_type_name,
c.name as customer_name, c.phone as customer_phone, c.email as customer_email, c.address as customer_address, o.car_plate,
u.username as created_by_username
FROM orders o
LEFT JOIN outlets ot ON o.outlet_id = ot.id
LEFT JOIN payment_types pt ON o.payment_type_id = pt.id
LEFT JOIN customers c ON o.customer_id = c.id
LEFT JOIN users u ON o.user_id = u.id
WHERE o.id = ?");
$stmt->execute([$id]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$order) {
die("Order not found.");
}
// Fetch Order Items
$stmt = $pdo->prepare("SELECT oi.*, COALESCE(p.name, oi.product_name) as product_name, COALESCE(pv.name, oi.variant_name) as variant_name
FROM order_items oi
LEFT JOIN products p ON oi.product_id = p.id
LEFT JOIN product_variants pv ON oi.variant_id = pv.id
WHERE oi.order_id = ?");
$stmt->execute([$id]);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
include 'includes/header.php';
// Calculate subtotal from items to be sure
$subtotal = 0;
foreach ($items as $item) {
$subtotal += $item['unit_price'] * $item['quantity'];
}
$vat_amount = (float)($order['vat'] ?? 0);
$discount_amount = (float)($order['discount'] ?? 0);
$company_settings = get_company_settings();
$vat_rate = (float)($company_settings['vat_rate'] ?? 0);
?>
<style>
/* Print Styles */
@media print {
.sidebar, .navbar, header, .no-print, .btn, .breadcrumb, .no-print * {
display: none !important;
}
.main-content {
margin-left: 0 !important;
padding: 0 !important;
margin-top: 0 !important;
}
body {
background-color: #fff !important;
color: #000 !important;
}
.card {
border: none !important;
box-shadow: none !important;
margin-bottom: 0 !important;
}
.container-fluid {
padding: 0 !important;
}
.print-only {
display: block !important;
}
.table {
width: 100% !important;
border-collapse: collapse !important;
}
.table th, .table td {
border: 1px solid #dee2e6 !important;
padding: 8px !important;
}
.status-badge {
border: 1px solid #000 !important;
color: #000 !important;
background: none !important;
}
.card-body {
padding: 0 !important;
}
}
.print-only {
display: none;
}
</style>
<!-- Formal Print Header -->
<div class="print-only mb-4">
<div class="row align-items-center">
<div class="col-6">
<?php if (!empty($company_settings['logo_url'])): ?>
<img src="../<?= htmlspecialchars($company_settings['logo_url']) ?>" alt="Logo" style="max-height: 80px;">
<?php else: ?>
<h2 class="fw-bold"><?= htmlspecialchars($company_settings['company_name']) ?></h2>
<?php endif; ?>
</div>
<div class="col-6 text-end">
<h1 class="fw-bold mb-0">INVOICE</h1>
<p class="mb-0">#<?= $order['id'] ?></p>
</div>
</div>
<hr>
<div class="row mb-4">
<div class="col-6">
<h6 class="fw-bold text-uppercase text-muted small mb-2">Company Details</h6>
<div class="fw-bold"><?= htmlspecialchars($company_settings['company_name']) ?></div>
<div><?= nl2br(htmlspecialchars($company_settings['address'] ?? '')) ?></div>
<div>Phone: <?= htmlspecialchars($company_settings['phone'] ?? '') ?></div>
<?php if (!empty($company_settings['vat_number'])): ?>
<div>VAT No: <?= htmlspecialchars($company_settings['vat_number']) ?></div>
<?php endif; ?>
</div>
<div class="col-6 text-end">
<h6 class="fw-bold text-uppercase text-muted small mb-2">Order Info</h6>
<div><strong>Date:</strong> <?= date('M d, Y H:i', strtotime($order['created_at'])) ?></div>
<div><strong>Status:</strong> <?= ucfirst($order['status']) ?></div>
<div><strong>Payment:</strong> <?= htmlspecialchars($order['payment_type_name'] ?? 'N/A') ?></div>
<div><strong>Type:</strong> <?= ucfirst($order['order_type']) ?></div>
</div>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-4 no-print">
<div>
<h2 class="fw-bold mb-0">Order #<?= $order['id'] ?></h2>
<p class="text-muted mb-0">Placed on <?= date('M d, Y H:i', strtotime($order['created_at'])) ?></p>
</div>
<div class="d-flex gap-2">
<a href="orders.php" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to List
</a>
<button onclick="printThermalReceipt()" class="btn btn-warning border-warning"><i class="bi bi-printer-fill"></i> Thermal Receipt</button>
<button onclick="window.print()" class="btn btn-light border">
<i class="bi bi-printer"></i> Print Receipt
</button>
<?php if (has_permission('orders_add')): ?>
<a href="order_edit.php?id=<?= $order['id'] ?>" class="btn btn-primary">
<i class="bi bi-pencil"></i> Edit Order
</a>
<?php endif; ?>
</div>
</div>
<div class="row">
<div class="col-md-8">
<!-- Order Items -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white py-3 no-print">
<h5 class="card-title mb-0 fw-bold">Order Items</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light text-muted small text-uppercase">
<tr>
<th class="ps-4">Product</th>
<th class="text-center">Price</th>
<th class="text-center">Qty</th>
<th class="text-end pe-4">Total</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $item): ?>
<tr>
<td class="ps-4">
<div class="fw-bold text-dark"><?= htmlspecialchars($item['product_name']) ?></div>
<?php if ($item['variant_name']): ?>
<small class="text-muted">Variant: <?= htmlspecialchars($item['variant_name']) ?></small>
<?php endif; ?>
</td>
<td class="text-center"><?= format_currency($item['unit_price']) ?></td>
<td class="text-center"><?= $item['quantity'] ?></td>
<td class="text-end pe-4 fw-bold"><?= format_currency($item['unit_price'] * $item['quantity']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot class="bg-light">
<tr>
<td colspan="3" class="text-end py-3 ps-4 border-0">
<div class="text-muted mb-1 small">Subtotal</div>
<?php if ($vat_amount > 0): ?>
<div class="text-muted mb-1 small">VAT</div>
<?php endif; ?>
<?php if ($discount_amount > 0): ?>
<div class="text-muted mb-1 small">Discount</div>
<?php endif; ?>
<h5 class="fw-bold mb-0 text-dark">Total Amount</h5>
</td>
<td class="text-end py-3 pe-4 border-0">
<div class="mb-1 small"><?= format_currency($subtotal) ?></div>
<?php if ($vat_amount > 0): ?>
<div class="mb-1 small text-primary">+<?= format_currency($vat_amount) ?></div>
<?php endif; ?>
<?php if ($discount_amount > 0): ?>
<div class="mb-1 small text-danger">-<?= format_currency($discount_amount) ?></div>
<?php endif; ?>
<h5 class="fw-bold mb-0 text-primary"><?= format_currency($order['total_amount']) ?></h5>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<!-- Additional Info -->
<div class="card border-0 shadow-sm mb-4 no-print">
<div class="card-body">
<h5 class="fw-bold mb-3">Internal Notes</h5>
<p class="text-muted mb-0"><?= htmlspecialchars($order['notes'] ?? 'No notes provided for this order.') ?></p>
</div>
</div>
</div>
<div class="col-md-4">
<!-- Status & Payment -->
<div class="card border-0 shadow-sm mb-4 no-print">
<div class="card-body">
<h6 class="text-muted small text-uppercase fw-bold mb-3">Order Status</h6>
<div class="d-flex align-items-center mb-4">
<span class="badge rounded-pill fs-6 px-3 py-2 status-<?= $order['status'] ?>">
<?= ucfirst($order['status']) ?>
</span>
<span class="ms-3 text-muted small">Last updated: <?= date('M d, H:i', strtotime($order['updated_at'] ?? $order['created_at'])) ?></span>
</div>
<hr>
<h6 class="text-muted small text-uppercase fw-bold mb-3 mt-4">Payment Information</h6>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Method:</span>
<span class="fw-bold text-dark"><?= htmlspecialchars($order['payment_type_name'] ?? 'Unpaid') ?></span>
</div>
<div class="d-flex justify-content-between">
<span class="text-muted">Status:</span>
<span class="badge bg-success bg-opacity-10 text-success border border-success">Paid</span>
</div>
</div>
</div>
<!-- Order Details -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<h6 class="text-muted small text-uppercase fw-bold mb-3">Order Details</h6>
<div class="mb-3">
<label class="text-muted small d-block">Outlet</label>
<div class="fw-bold"><?= htmlspecialchars($order['outlet_name'] ?? 'N/A') ?></div>
</div>
<div class="mb-3">
<label class="text-muted small d-block">Order Type</label>
<div class="fw-bold"><?= ucfirst($order['order_type']) ?></div>
</div>
<?php if ($order['order_type'] === 'dine-in'): ?>
<div class="mb-3">
<label class="text-muted small d-block">Table Number</label>
<div class="fw-bold">Table <?= htmlspecialchars((string)$order['table_number']) ?></div>
</div>
<?php endif; ?>
<div class="mb-0">
<label class="text-muted small d-block">Processed By</label>
<div class="fw-bold"><?= htmlspecialchars($order['created_by_username'] ?? 'System') ?></div>
</div>
</div>
</div>
<!-- Customer Info -->
<div class="card border-0 shadow-sm">
<div class="card-body">
<h6 class="text-muted small text-uppercase fw-bold mb-3">Customer Information</h6>
<?php if ($order['customer_name']): ?>
<div class="d-flex align-items-center mb-3">
<div class="bg-primary bg-opacity-10 text-primary p-2 rounded-circle me-3 no-print">
<i class="bi bi-person fs-4"></i>
</div>
<div>
<div class="fw-bold"><?= htmlspecialchars($order['customer_name']) ?></div>
<small class="text-muted">Customer ID: #<?= $order['customer_id'] ?></small>
</div>
</div>
<?php if ($order['car_plate']): ?>
<div class="mb-2">
<i class="bi bi-car-front text-muted me-2 no-print"></i>
<span class="print-only">Car Plate: </span>
<span class="text-dark"><?= htmlspecialchars($order['car_plate']) ?></span>
</div>
<?php endif; ?>
<?php if ($order['customer_phone']): ?>
<div class="mb-2">
<i class="bi bi-telephone text-muted me-2 no-print"></i>
<span class="print-only">Phone: </span>
<a href="tel:<?= $order['customer_phone'] ?>" class="text-decoration-none text-dark"><?= htmlspecialchars($order['customer_phone'] ?? '') ?></a>
</div>
<?php endif; ?>
<?php if ($order['customer_email']): ?>
<div class="mb-2">
<i class="bi bi-envelope text-muted me-2 no-print"></i>
<span class="print-only">Email: </span>
<a href="mailto:<?= $order['customer_email'] ?>" class="text-decoration-none text-dark"><?= htmlspecialchars($order['customer_email'] ?? '') ?></a>
</div>
<?php endif; ?>
<?php if (!empty($order['customer_address'])): ?>
<div class="mb-0">
<i class="bi bi-geo-alt text-muted me-2 no-print"></i>
<span class="print-only">Address: </span>
<span class="text-dark"><?= htmlspecialchars($order['customer_address']) ?></span>
</div>
<?php endif; ?>
<?php else: ?>
<div class="text-center py-3">
<i class="bi bi-person-x fs-1 text-muted opacity-25 no-print"></i>
<p class="text-muted small mb-0 mt-2">No customer attached to this order (Guest)</p>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>
<script>
const COMPANY_SETTINGS = <?= json_encode(get_company_settings()) ?>;
const CURRENT_USER = { name: '<?= addslashes($order['created_by_username'] ?? 'System') ?>' };
const CURRENT_OUTLET = { name: '<?= addslashes($order['outlet_name'] ?? 'N/A') ?>' };
const BASE_URL = '<?= get_base_url() ?>';
function formatCurrency(amount) {
const symbol = COMPANY_SETTINGS.currency_symbol || '$';
const decimals = parseInt(COMPANY_SETTINGS.currency_decimals || 2);
return symbol + parseFloat(Math.abs(amount)).toFixed(decimals);
}
function printThermalReceipt() {
const data = {
orderId: '<?= $order['id'] ?>',
customer: <?= $order['customer_name'] ? json_encode(['name' => $order['customer_name'], 'phone' => $order['customer_phone'], 'address' => $order['customer_address'] ?? '']) : 'null' ?>,
items: <?= json_encode(array_map(function($i) {
return [
'name' => $i['product_name'],
'variant_name' => $i['variant_name'],
'quantity' => $i['quantity'],
'price' => $i['unit_price']
];
}, $items)) ?>,
total: <?= (float)$order['total_amount'] ?>,
vat: <?= (float)$order['vat'] ?>,
discount: <?= (float)$order['discount'] ?>,
orderType: '<?= $order['order_type'] ?>',
tableNumber: '<?= $order['table_number'] ?>',
date: '<?= date('M d, Y H:i', strtotime($order['created_at'])) ?>',
paymentMethod: '<?= $order['payment_type_name'] ?? 'Unpaid' ?>',
loyaltyRedeemed: <?= ($order['payment_type_name'] === 'Loyalty Redeem' || (float)$order['total_amount'] <= 0) ? 'true' : 'false' ?>
};
const width = 400;
const height = 800;
const left = (screen.width - width) / 2;
const top = (screen.height - height) / 2;
const win = window.open('', '_blank', `width=${width},height=${height},top=${top},left=${left}`);
if (!win) {
alert('Please allow popups for this website to print thermal receipts.');
return;
}
const tr = {
'Order': 'الطلب',
'Type': 'النوع',
'Date': 'التاريخ',
'Staff': 'الموظف',
'Table': 'طاولة',
'Payment': 'الدفع',
'ITEM': 'الصنف',
'TOTAL': 'المجموع',
'Subtotal': 'المجموع الفرعي',
'VAT': 'ضريبة القيمة المضافة',
'Tax Included': 'شامل الضريبة',
'THANK YOU FOR YOUR VISIT!': 'شكراً لزيارتكم!',
'Please come again.': 'يرجى زيارتنا مرة أخرى.',
'Customer Details': 'تفاصيل العميل',
'Tel': 'هاتف',
'takeaway': 'سفري',
'dine-in': 'محلي',
'delivery': 'توصيل',
'VAT No': 'الرقم الضريبي',
'CTR No': 'رقم السجل التجاري'
};
const itemsHtml = data.items.map(item => `
<tr>
<td style="padding: 5px 0; border-bottom: 1px solid #eee;">
<div style="font-weight: bold;">${item.name}</div>
${item.variant_name ? `<div style="font-size: 10px; color: #555;">(${item.variant_name})</div>` : ''}
<div style="font-size: 11px;">${item.quantity} x ${formatCurrency(item.price)}</div>
</td>
<td style="text-align: right; vertical-align: middle; font-weight: bold;">${formatCurrency(item.quantity * item.price)}</td>
</tr>
`).join('');
const customerHtml = data.customer ? `
<div style="margin-bottom: 10px; border: 1px solid #eee; padding: 8px; border-radius: 4px;">
<div style="font-weight: bold; text-transform: uppercase; font-size: 10px; color: #666; margin-bottom: 3px;">
<span style="float: left;">Customer Details</span>
<span style="float: right;">${tr['Customer Details']}</span>
<div style="clear: both;"></div>
</div>
<div style="font-weight: bold;">${data.customer.name}</div>
${data.customer.phone ? `<div>Tel: ${data.customer.phone}</div>` : ''}
${data.customer.address ? `<div style="font-size: 11px;">${data.customer.address}</div>` : ''}
</div>
` : '';
const tableHtml = data.tableNumber && data.orderType === 'dine-in' ? `
<div style="display: flex; justify-content: space-between;">
<span><strong>Table:</strong> ${data.tableNumber}</span>
<span><strong>${tr['Table']}:</strong> ${data.tableNumber}</span>
</div>` : '';
const paymentHtml = data.paymentMethod ? `
<div style="display: flex; justify-content: space-between;">
<span><strong>Payment:</strong> ${data.paymentMethod}</span>
<span><strong>${tr['Payment']}:</strong> ${data.paymentMethod}</span>
</div>` : '';
const loyaltyHtml = data.loyaltyRedeemed ? `<div style="color: #d63384; font-weight: bold; margin: 5px 0; text-align: center;">* Loyalty Reward Applied *</div>` : '';
const logoHtml = COMPANY_SETTINGS.logo_url ? `<div style="text-align: center; margin-bottom: 10px;"><img src="${BASE_URL}${COMPANY_SETTINGS.logo_url}" style="max-height: 80px; max-width: 150px; filter: grayscale(100%);" alt="Logo"></div>` : '';
const vatRate = COMPANY_SETTINGS.vat_rate || 0;
const html = `
<html dir="ltr">
<head>
<title>Receipt #${data.orderId}</title>
<style>
body {
font-family: 'Courier New', Courier, monospace;
font-size: 13px;
margin: 0;
padding: 15px;
color: #000;
line-height: 1.4;
}
.header { text-align: center; margin-bottom: 15px; }
.header h2 { margin: 0 0 5px 0; font-size: 20px; font-weight: bold; }
.header div { font-size: 12px; }
table { width: 100%; border-collapse: collapse; }
.divider { border-bottom: 2px dashed #000; margin: 10px 0; }
.thick-divider { border-bottom: 2px solid #000; margin: 10px 0; }
.totals td { padding: 3px 0; }
.footer { text-align: center; margin-top: 25px; font-size: 12px; }
.order-info { font-size: 11px; margin-bottom: 10px; }
.order-info-row { display: flex; justify-content: space-between; margin-bottom: 2px; }
.rtl { direction: rtl; unicode-bidi: embed; }
@media print {
body { width: 80mm; padding: 5mm; }
@page { size: 80mm auto; margin: 0; }
}
</style>
</head>
<body>
<div class="header">
${logoHtml}
<h2>${COMPANY_SETTINGS.company_name}</h2>
<div style="font-weight: bold;">${CURRENT_OUTLET.name}</div>
<div>${COMPANY_SETTINGS.address}</div>
<div>Tel: ${COMPANY_SETTINGS.phone}</div>
${COMPANY_SETTINGS.vat_number ? `<div style="margin-top: 4px;">VAT No / الرقم الضريبي: ${COMPANY_SETTINGS.vat_number}</div>` : ''}
${COMPANY_SETTINGS.ctr_number ? `<div>CTR No / رقم السجل: ${COMPANY_SETTINGS.ctr_number}</div>` : ''}
</div>
<div class="divider"></div>
<div class="order-info">
<div class="order-info-row">
<span><strong>Order:</strong> #${data.orderId}</span>
<span><strong>${tr['Order']}:</strong> #${data.orderId}</span>
</div>
<div class="order-info-row">
<span><strong>Type:</strong> ${data.orderType.toUpperCase()}</span>
<span><strong>${tr['Type']}:</strong> ${tr[data.orderType] || data.orderType}</span>
</div>
<div class="order-info-row">
<span><strong>Date:</strong> ${data.date}</span>
<span><strong>${tr['Date']}:</strong> ${data.date}</span>
</div>
<div class="order-info-row">
<span><strong>Staff:</strong> ${CURRENT_USER.name}</span>
<span><strong>${tr['Staff']}:</strong> ${CURRENT_USER.name}</span>
</div>
</div>
${tableHtml}
${paymentHtml}
${loyaltyHtml}
<div class="thick-divider"></div>
${customerHtml}
<table>
<thead>
<tr>
<th style="text-align: left; padding-bottom: 5px;">
ITEM / الصنف
</th>
<th style="text-align: right; padding-bottom: 5px;">
TOTAL / المجموع
</th>
</tr>
</thead>
<tbody>
${itemsHtml}
</tbody>
</table>
<div class="divider"></div>
<div class="totals">
<table style="width: 100%">
<tr>
<td>Subtotal / ${tr['Subtotal']}</td>
<td style="text-align: right">${formatCurrency(data.items.reduce((acc, i) => acc + (i.price * i.quantity), 0))}</td>
</tr>
${data.vat > 0 ? `
<tr>
<td>VAT (${vatRate}%) / ${tr['VAT']}</td>
<td style="text-align: right">+${formatCurrency(data.vat)}</td>
</tr>` : ''}
${data.discount > 0 ? `
<tr>
<td>Discount / الخصم</td>
<td style="text-align: right">-${formatCurrency(data.discount)}</td>
</tr>` : ''}
<tr style="font-weight: bold; font-size: 18px;">
<td style="padding-top: 10px;">TOTAL / ${tr['TOTAL']}</td>
<td style="text-align: right; padding-top: 10px;">${formatCurrency(data.total)}</td>
</tr>
</table>
</div>
<div class="thick-divider"></div>
<div class="footer">
<div style="font-weight: bold; font-size: 14px; margin-bottom: 2px;">THANK YOU FOR YOUR VISIT!</div>
<div style="font-weight: bold; font-size: 14px; margin-bottom: 5px;" class="rtl">${tr['THANK YOU FOR YOUR VISIT!']}</div>
<div>Please come again.</div>
<div class="rtl">${tr['Please come again.']}</div>
${COMPANY_SETTINGS.email ? `<div style="margin-top: 5px; font-size: 10px;">${COMPANY_SETTINGS.email}</div>` : ''}
<div style="margin-top: 15px; font-size: 10px; color: #888;">Powered by Abidarcafe</div>
</div>
<script>
let printed = false;
function doPrint() {
if (printed) return;
printed = true;
window.print();
setTimeout(function() { window.close(); }, 500);
}
var img = document.querySelector('img');
if (img && !img.complete) {
img.onload = doPrint;
img.onerror = doPrint;
setTimeout(doPrint, 1500); // Fallback
} else {
doPrint();
}
</script>
</body>
</html>
`;
win.document.write(html);
win.document.close();
}
</script>

434
admin/orders.php Normal file
View File

@ -0,0 +1,434 @@
<?php
declare(strict_types=1);
error_reporting(E_ALL);
ini_set("display_errors", "1");
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('orders_view');
// Handle status updates
if (isset($_POST['action']) && $_POST['action'] === 'update_status') {
if (!has_permission('orders_add')) {
header("Location: orders.php?error=permission_denied");
exit;
}
$order_id = $_POST['order_id'];
$new_status = $_POST['status'];
$stmt = $pdo->prepare("UPDATE orders SET status = ? WHERE id = ?");
$stmt->execute([$new_status, $order_id]);
header("Location: orders.php?" . http_build_query($_GET)); // Keep filters
exit;
}
// Handle stopping all promotions
if (isset($_POST['action']) && $_POST['action'] === 'stop_promotions') {
if (!has_permission('manage_products')) {
header("Location: orders.php?error=permission_denied");
exit;
}
// Set promo_date_to to yesterday for all currently active promotions
$stmt = $pdo->prepare("UPDATE products SET promo_date_to = DATE_SUB(CURDATE(), INTERVAL 1 DAY) WHERE (promo_date_to >= CURDATE() OR promo_date_to IS NULL) AND promo_discount_percent IS NOT NULL");
$stmt->execute();
header("Location: orders.php?success=promotions_stopped");
exit;
}
// Handle Delete Order
if (isset($_GET['delete'])) {
if (!has_permission('manage_orders')) {
header("Location: orders.php?error=permission_denied");
exit;
}
$id = (int)$_GET['delete'];
$pdo->beginTransaction();
try {
$pdo->prepare("DELETE FROM order_items WHERE order_id = ?")->execute([$id]);
$pdo->prepare("DELETE FROM orders WHERE id = ?")->execute([$id]);
$pdo->commit();
header("Location: orders.php?success=order_deleted");
} catch (Exception $e) {
$pdo->rollBack();
header("Location: orders.php?error=delete_failed");
}
exit;
}
// Fetch Outlets for Filter
$outlets = $pdo->query("SELECT id, name FROM outlets WHERE is_deleted = 0 ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
// Build Query with Filters
$params = [];
$where = [];
// Filter: Outlet
if (!empty($_GET['outlet_id'])) {
$where[] = "o.outlet_id = :outlet_id";
$params[':outlet_id'] = $_GET['outlet_id'];
}
// Filter: Date Range
if (!empty($_GET['start_date'])) {
$where[] = "DATE(o.created_at) >= :start_date";
$params[':start_date'] = $_GET['start_date'];
}
if (!empty($_GET['end_date'])) {
$where[] = "DATE(o.created_at) <= :end_date";
$params[':end_date'] = $_GET['end_date'];
}
// Filter: Search (Order No / Customer Name)
if (!empty($_GET['search'])) {
$searchTerm = $_GET['search'];
if (is_numeric($searchTerm)) {
$where[] = "(o.id = :search_exact OR o.customer_name LIKE :search_like)";
$params[':search_exact'] = $searchTerm;
$params[':search_like'] = "%$searchTerm%";
} else {
$where[] = "o.customer_name LIKE :search";
$params[':search'] = "%$searchTerm%";
}
}
$where_clause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
// Calculate Total Sum and Total Commission and Total VAT for filtered orders
$sum_query = "SELECT SUM(total_amount) as total_sum, SUM(commission_amount) as total_commission, SUM(vat) as total_vat FROM orders o $where_clause";
$stmt_sum = $pdo->prepare($sum_query);
$stmt_sum->execute($params);
$sum_data = $stmt_sum->fetch(PDO::FETCH_ASSOC);
$total_sum = (float)($sum_data['total_sum'] ?? 0);
$total_commission = (float)($sum_data['total_commission'] ?? 0);
$total_vat_sum = (float)($sum_data['total_vat'] ?? 0);
// Main Query
$query = "SELECT o.*, ot.name as outlet_name, pt.name as payment_type_name, u.username as cashier_name,
(SELECT GROUP_CONCAT(CONCAT(p.name, ' x', oi.quantity) SEPARATOR ', ') FROM order_items oi JOIN products p ON oi.product_id = p.id WHERE oi.order_id = o.id) as items_summary
FROM orders o
LEFT JOIN outlets ot ON o.outlet_id = ot.id
LEFT JOIN payment_types pt ON o.payment_type_id = pt.id
LEFT JOIN users u ON o.user_id = u.id
$where_clause
ORDER BY o.created_at DESC";
$orders_pagination = paginate_query($pdo, $query, $params);
$orders = $orders_pagination['data'];
$settings = get_company_settings();
$commission_enabled = !empty($settings['commission_enabled']);
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Order Management</h2>
<div class="d-flex gap-2">
<?php if (has_permission('manage_products')): ?>
<form method="POST" onsubmit="return confirm('Are you sure you want to stop all running promotions? This will end all active promotions by setting their end date to yesterday.');">
<input type="hidden" name="action" value="stop_promotions">
<button type="submit" class="btn btn-danger shadow-sm">
<i class="bi bi-stop-circle me-1"></i> Stop All Promotions
</button>
</form>
<?php endif; ?>
<span class="badge bg-success bg-opacity-10 text-success border border-success px-3 py-2 rounded-pill d-flex align-items-center">
<i class="bi bi-circle-fill small me-1"></i> Live
</span>
</div>
</div>
<?php if (isset($_GET['error'])): ?>
<?php if ($_GET['error'] === 'permission_denied'): ?>
<div class="alert alert-danger border-0 shadow-sm rounded-3">Access Denied: You do not have permission to perform this action.</div>
<?php elseif ($_GET['error'] === 'delete_failed'): ?>
<div class="alert alert-danger border-0 shadow-sm rounded-3">Error: Failed to delete order.</div>
<?php endif; ?>
<?php endif; ?>
<?php if (isset($_GET['success'])): ?>
<?php if ($_GET['success'] === 'promotions_stopped'): ?>
<div class="alert alert-success border-0 shadow-sm rounded-3">
<i class="bi bi-check-circle-fill me-2"></i> All running promotions have been stopped successfully.
</div>
<?php elseif ($_GET['success'] === 'order_deleted'): ?>
<div class="alert alert-success border-0 shadow-sm rounded-3">
<i class="bi bi-check-circle-fill me-2"></i> Order has been deleted successfully.
</div>
<?php endif; ?>
<?php endif; ?>
<!-- Summary Stats -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0 bg-primary bg-opacity-10 text-primary p-3 rounded">
<i class="bi bi-currency-dollar fs-4"></i>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="text-muted mb-0 small text-uppercase fw-bold">Total Revenue</h6>
<div class="fs-4 fw-bold text-primary"><?= format_currency($total_sum) ?></div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0 bg-warning bg-opacity-10 text-warning p-3 rounded">
<i class="bi bi-percent fs-4"></i>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="text-muted mb-0 small text-uppercase fw-bold">Total VAT</h6>
<div class="fs-4 fw-bold text-warning"><?= format_currency($total_vat_sum) ?></div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0 bg-success bg-opacity-10 text-success p-3 rounded">
<i class="bi bi-receipt fs-4"></i>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="text-muted mb-0 small text-uppercase fw-bold">Total Orders</h6>
<div class="fs-4 fw-bold text-success"><?= $orders_pagination['total_rows'] ?></div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0 bg-info bg-opacity-10 text-info p-3 rounded">
<i class="bi bi-calendar-event fs-4"></i>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="text-muted mb-0 small text-uppercase fw-bold">Date Range</h6>
<div class="small fw-bold">
<?= !empty($_GET['start_date']) ? date('M d, Y', strtotime($_GET['start_date'])) : 'Start' ?>
-
<?= !empty($_GET['end_date']) ? date('M d, Y', strtotime($_GET['end_date'])) : 'Today' ?>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body bg-light">
<form method="GET" class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label small fw-bold text-muted">Outlet</label>
<select name="outlet_id" class="form-select">
<option value="">All Outlets</option>
<?php foreach ($outlets as $outlet): ?>
<option value="<?= $outlet['id'] ?>" <?= (isset($_GET['outlet_id']) && $_GET['outlet_id'] == $outlet['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($outlet['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-muted">Date Range</label>
<div class="input-group">
<input type="date" name="start_date" class="form-control" value="<?= $_GET['start_date'] ?? '' ?>" placeholder="Start">
<span class="input-group-text bg-white border-start-0 border-end-0">-</span>
<input type="date" name="end_date" class="form-control" value="<?= $_GET['end_date'] ?? '' ?>" placeholder="End">
</div>
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-muted">Search</label>
<input type="text" name="search" class="form-control" placeholder="Order No / Customer" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>">
</div>
<div class="col-md-3">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-filter"></i> Filter
</button>
<a href="orders.php" class="btn btn-outline-secondary">
<i class="bi bi-x-lg"></i>
</a>
</div>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($orders_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">ID</th>
<th>Outlet</th>
<th>Cashier</th>
<th>Customer</th>
<th>Type</th>
<th>Car Plate</th>
<th>VAT</th>
<th>Total</th>
<?php if ($commission_enabled): ?>
<th>Commission</th>
<?php endif; ?>
<th>Payment</th>
<th>Status</th>
<th>Time</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>
<span class="badge bg-white text-dark border">
<i class="bi bi-shop me-1"></i>
<?= htmlspecialchars($order['outlet_name'] ?? 'Unknown') ?>
</span>
</td>
<td>
<small class="fw-bold">@<?= htmlspecialchars((string)($order['cashier_name'] ?? 'Guest')) ?></small>
</td>
<td>
<?php if (!empty($order['customer_name'])): ?>
<div><?= htmlspecialchars((string)($order['customer_name'] ?? '')) ?></div>
<?php if (!empty($order['car_plate'])): ?>
<div class="small text-muted"><i class="bi bi-car-front me-1"></i><?= htmlspecialchars((string)($order['car_plate'] ?? '')) ?></div>
<?php endif; ?>
<?php if (!empty($order['customer_phone'])): ?>
<small class="text-muted"><i class="bi bi-telephone me-1"></i><?= htmlspecialchars((string)($order['customer_phone'] ?? '')) ?></small>
<?php endif; ?>
<?php else: ?>
<span class="text-muted small">Guest</span>
<?php endif; ?>
</td>
<td>
<?php
$badge = match($order['order_type']) {
'dine-in' => 'bg-info',
'takeaway' => 'bg-success',
'delivery' => 'bg-warning',
'drive-thru' => 'bg-primary',
default => 'bg-secondary'
};
?>
<span class="badge <?= $badge ?> text-dark bg-opacity-25 border border-<?= str_replace('bg-', '', $badge) ?>"><?= ucfirst($order['order_type']) ?></span>
</td>
<td>
<span class="text-muted small"><?= format_currency($order['vat']) ?></span>
</td>
<td class="fw-bold"><?= format_currency($order['total_amount']) ?></td>
<?php if ($commission_enabled): ?>
<td>
<span class="text-warning fw-bold"><?= format_currency($order['commission_amount']) ?></span>
</td>
<?php endif; ?>
<td>
<?php
$payment_name = $order['payment_type_name'] ?? 'Unpaid';
$payment_badge = match(strtolower($payment_name)) {
'cash' => 'bg-success',
'credit card' => 'bg-primary',
'loyalty redeem' => 'bg-warning',
'bank transfer' => 'bg-info',
'unpaid' => 'bg-secondary',
default => 'bg-secondary'
};
?>
<span class="badge <?= $payment_badge ?> text-dark bg-opacity-25 border border-<?= str_replace('bg-', '', $payment_badge) ?>">
<?= htmlspecialchars((string)($payment_name ?? '')) ?>
</span>
</td>
<td>
<span class="badge rounded-pill status-<?= $order['status'] ?>">
<?= ucfirst($order['status']) ?>
</span>
</td>
<td class="text-muted small">
<div><?= date('M d', strtotime($order['created_at'])) ?></div>
<div><?= date('H:i', strtotime($order['created_at'])) ?></div>
</td>
<td class="text-end pe-4">
<div class="d-flex gap-2 justify-content-end align-items-center">
<!-- Status Workflow Buttons -->
<?php if (has_permission('orders_add')): ?>
<form method="POST" class="d-flex gap-1 me-2 border-end pe-2">
<input type="hidden" name="order_id" value="<?= $order['id'] ?>">
<input type="hidden" name="action" value="update_status">
<?php if ($order['status'] === 'pending'): ?>
<button type="submit" name="status" value="preparing" class="btn btn-sm btn-primary py-0 px-1" title="Start Preparing">
<i class="bi bi-play-fill"></i>
</button>
<button type="submit" name="status" value="cancelled" class="btn btn-sm btn-outline-danger py-0 px-1" title="Cancel Order">
<i class="bi bi-x"></i>
</button>
<?php elseif ($order['status'] === 'preparing'): ?>
<button type="submit" name="status" value="ready" class="btn btn-sm btn-warning text-dark py-0 px-1" title="Mark Ready">
<i class="bi bi-check-circle"></i>
</button>
<?php elseif ($order['status'] === 'ready'): ?>
<button type="submit" name="status" value="completed" class="btn btn-sm btn-success py-0 px-1" title="Complete Order">
<i class="bi bi-check-all"></i>
</button>
<?php endif; ?>
</form>
<?php endif; ?>
<!-- Standard Actions -->
<a href="order_view.php?id=<?= $order['id'] ?>" class="btn-icon-soft" title="View Order">
<i class="bi bi-eye-fill"></i>
</a>
<?php if (has_permission('orders_add')): ?>
<a href="order_edit.php?id=<?= $order['id'] ?>" class="btn-icon-soft edit" title="Edit Order">
<i class="bi bi-pencil-fill"></i>
</a>
<?php endif; ?>
<?php if (has_permission('manage_orders')): ?>
<a href="?delete=<?= $order['id'] ?>" class="btn-icon-soft delete" onclick="return confirm('Are you sure you want to delete this order? This action cannot be undone.')" title="Delete Order">
<i class="bi bi-trash-fill"></i>
</a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($orders)): ?>
<tr>
<td colspan="12" class="text-center py-5 text-muted">
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
No active orders found matching your criteria.
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($orders_pagination); ?>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

314
admin/outlets.php Normal file
View File

@ -0,0 +1,314 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../db/config.php";
require_permission("outlets_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
// Handle Add/Edit Outlet
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$name = trim($_POST['name']);
$name_ar = trim($_POST['name_ar'] ?? '');
$address = trim($_POST['address']);
$cashier_printer_ip = trim($_POST['cashier_printer_ip'] ?? '');
$kitchen_printer_ip = trim($_POST['kitchen_printer_ip'] ?? '');
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
if (empty($name)) {
$message = '<div class="alert alert-danger">Outlet name is required.</div>';
} else {
try {
if ($action === 'edit_outlet' && $id) {
if (!has_permission('outlets_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("UPDATE outlets SET name = ?, name_ar = ?, address = ?, cashier_printer_ip = ?, kitchen_printer_ip = ? WHERE id = ?");
$stmt->execute([$name, $name_ar, $address, $cashier_printer_ip, $kitchen_printer_ip, $id]);
$message = '<div class="alert alert-success">Outlet updated successfully!</div>';
}
} elseif ($action === 'add_outlet') {
if (!has_permission('outlets_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO outlets (name, name_ar, address, cashier_printer_ip, kitchen_printer_ip) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$name, $name_ar, $address, $cashier_printer_ip, $kitchen_printer_ip]);
$message = '<div class="alert alert-success">Outlet created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
// Handle Delete (Soft Delete)
if (isset($_GET['delete'])) {
if (!has_permission('outlets_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete outlets.</div>';
} else {
try {
$id = (int)$_GET['delete'];
// Soft delete to preserve relations with users, expenses, and orders
$pdo->prepare("UPDATE outlets SET is_deleted = 1 WHERE id = ?")->execute([$id]);
header("Location: outlets.php?deleted=1");
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Error removing outlet: ' . $e->getMessage() . '</div>';
}
}
}
if (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success">Outlet removed successfully!</div>';
}
$query = "SELECT * FROM outlets WHERE is_deleted = 0 ORDER BY id DESC";
$outlets_pagination = paginate_query($pdo, $query);
$outlets = $outlets_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Outlets</h2>
<?php if (has_permission('outlets_add')):
?><button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#outletModal" onclick="prepareAddForm()">
<i class="bi bi-plus-lg"></i> Add Outlet
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($outlets_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">ID</th>
<th>Name</th>
<th>Address</th>
<th>Printers (Cashier / Kitchen)</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($outlets as $outlet):
?><tr>
<td class="ps-4 fw-medium">#<?= $outlet['id'] ?></td>
<td>
<div class="fw-bold"><?= htmlspecialchars($outlet['name']) ?></div>
<small class="text-muted"><?= htmlspecialchars($outlet['name_ar'] ?: '-') ?></small>
</td>
<td><small class="text-muted"><?= htmlspecialchars($outlet['address']) ?></small></td>
<td>
<div class="small">
<i class="bi bi-printer text-primary"></i> <?= htmlspecialchars($outlet['cashier_printer_ip'] ?: 'Not set') ?>
</div>
<div class="small mt-1">
<i class="bi bi-printer text-warning"></i> <?= htmlspecialchars($outlet['kitchen_printer_ip'] ?: 'Not set') ?>
</div>
</td>
<td class="text-end pe-4">
<?php if (has_permission('outlets_add')):
?><button type="button" class="btn btn-sm btn-outline-primary me-1"
data-bs-toggle="modal" data-bs-target="#outletModal"
onclick='prepareEditForm(<?= htmlspecialchars(json_encode($outlet), ENT_QUOTES, "UTF-8") ?>)'><i class="bi bi-pencil"></i></button>
<?php endif; ?>
<?php if (has_permission('outlets_del')):
?><a href="?delete=<?= $outlet['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('<?= t('are_you_sure') ?>')"><i class="bi bi-trash"></i></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($outlets)):
?><tr>
<td colspan="5" class="text-center py-4 text-muted">No outlets found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($outlets_pagination); ?>
</div>
</div>
</div>
<!-- Outlet Modal -->
<?php if (has_permission('outlets_add')):
?><div class="modal fade" id="outletModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="outletModalTitle">Add New Outlet</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="outletForm">
<div class="modal-body">
<input type="hidden" name="action" id="outletAction" value="add_outlet">
<input type="hidden" name="id" id="outletId">
<div class="mb-3">
<label class="form-label">Name <span class="text-danger">*</span></label>
<div class="input-group">
<input type="text" name="name" id="outletName" class="form-control" required>
<button class="btn btn-outline-secondary" type="button" id="btnTranslate">
<i class="bi bi-translate text-primary"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">Arabic Name</label>
<input type="text" name="name_ar" id="outletNameAr" class="form-control" dir="rtl">
</div>
<div class="mb-3">
<label class="form-label">Address</label>
<textarea name="address" id="outletAddress" class="form-control" rows="3"></textarea>
</div>
<hr>
<div class="alert alert-info small py-2">
<i class="bi bi-info-circle me-1"></i>
Note: Direct IP printing only works if the printer is reachable from the server. For local printers, use browser printing instead.
</div>
<h6 class="fw-bold mb-3">IP/TCP Printers (ESC/POS)</h6>
<div class="mb-3">
<label class="form-label small">Cashier Printer IP</label>
<div class="input-group">
<input type="text" name="cashier_printer_ip" id="outletCashierIp" class="form-control" placeholder="e.g. 192.168.1.100">
<button class="btn btn-outline-secondary" type="button" onclick="testPrinter('outletCashierIp')">
Test
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label small">Kitchen Printer IP</label>
<div class="input-group">
<input type="text" name="kitchen_printer_ip" id="outletKitchenIp" class="form-control" placeholder="e.g. 192.168.1.101">
<button class="btn btn-outline-secondary" type="button" onclick="testPrinter('outletKitchenIp')">
Test
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Outlet</button>
</div>
</form>
</div>
</div>
</div>
<script>
function prepareAddForm() {
document.getElementById('outletModalTitle').innerText = 'Add New Outlet';
document.getElementById('outletAction').value = 'add_outlet';
document.getElementById('outletForm').reset();
document.getElementById('outletId').value = '';
}
function prepareEditForm(outlet) {
if (!outlet) return;
document.getElementById('outletModalTitle').innerText = 'Edit Outlet';
document.getElementById('outletAction').value = 'edit_outlet';
document.getElementById('outletId').value = outlet.id;
document.getElementById('outletName').value = outlet.name || '';
document.getElementById('outletNameAr').value = outlet.name_ar || '';
document.getElementById('outletAddress').value = outlet.address || '';
document.getElementById('outletCashierIp').value = outlet.cashier_printer_ip || '';
document.getElementById('outletKitchenIp').value = outlet.kitchen_printer_ip || '';
}
function testPrinter(inputId) {
const ip = document.getElementById(inputId).value;
if (!ip) {
alert('Please enter an IP address first.');
return;
}
const btn = event.currentTarget;
const originalText = btn.innerText;
btn.disabled = true;
btn.innerText = '...';
fetch('../api/print.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'test',
ip: ip
}),
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Printer test command sent successfully!');
} else {
alert('Printer connection failed: ' + (data.error || 'Unknown error') + '\n\nNote: Local IPs (like 192.168.x.x) are not reachable from the cloud server.');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred during the test.');
})
.finally(() => {
btn.disabled = false;
btn.innerText = originalText;
});
}
document.getElementById('btnTranslate').addEventListener('click', function() {
const text = document.getElementById('outletName').value;
if (!text) {
alert('Please enter an outlet name first.');
return;
}
const btn = this;
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm text-primary" role="status" aria-hidden="true"></span>';
fetch('../api/translate.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: text,
target_lang: 'Arabic'
}),
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('outletNameAr').value = data.translated_text;
} else {
alert('Translation failed: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred during translation.');
})
.finally(() => {
btn.disabled = false;
btn.innerHTML = originalHtml;
});
});
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

218
admin/payment_types.php Normal file
View File

@ -0,0 +1,218 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../db/config.php";
require_permission("payment_types_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
// Handle Add/Edit Payment Type
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$name = trim($_POST['name']);
$code = trim($_POST['code']);
$is_active = isset($_POST['is_active']) ? 1 : 0;
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
if (empty($name) || empty($code)) {
$message = '<div class="alert alert-danger">Name and code are required.</div>';
} else {
try {
if ($action === 'edit_payment_type' && $id) {
if (!has_permission('payment_types_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("UPDATE payment_types SET name = ?, code = ?, is_active = ? WHERE id = ?");
$stmt->execute([$name, $code, $is_active, $id]);
$message = '<div class="alert alert-success">Payment type updated successfully!</div>';
}
} elseif ($action === 'add_payment_type') {
if (!has_permission('payment_types_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO payment_types (name, code, is_active) VALUES (?, ?, ?)");
$stmt->execute([$name, $code, $is_active]);
$message = '<div class="alert alert-success">Payment type created successfully!</div>';
}
}
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
$message = '<div class="alert alert-danger">Code already exists.</div>';
} else {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
}
// Handle Delete
if (isset($_GET['delete'])) {
if (!has_permission('payment_types_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete payment types.</div>';
} else {
try {
$id = $_GET['delete'];
$pdo->prepare("DELETE FROM payment_types WHERE id = ?")->execute([$id]);
header("Location: payment_types.php?deleted=1");
exit;
} catch (PDOException $e) {
if ($e->getCode() == '23000') {
$message = '<div class="alert alert-danger">Cannot delete this payment type because it is linked to other records (e.g., orders).</div>';
} else {
$message = '<div class="alert alert-danger">Error deleting payment type: ' . $e->getMessage() . '</div>';
}
}
}
}
if (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success">Payment type deleted successfully!</div>';
}
$query = "SELECT * FROM payment_types ORDER BY id ASC";
$payments_pagination = paginate_query($pdo, $query);
$payment_types = $payments_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Payment Types</h2>
<?php if (has_permission('payment_types_add')): ?>
<button class="btn btn-primary" onclick="openAddModal()">
<i class="bi bi-plus-lg"></i> Add Payment Type
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($payments_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">ID</th>
<th>Name</th>
<th>Code</th>
<th>Status</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($payment_types as $type): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $type['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($type['name']) ?></td>
<td><code><?= htmlspecialchars($type['code']) ?></code></td>
<td>
<?php if ($type['is_active']): ?>
<span class="badge bg-success-subtle text-success px-3">Active</span>
<?php else: ?>
<span class="badge bg-secondary-subtle text-secondary px-3">Inactive</span>
<?php endif; ?>
</td>
<td class="text-end pe-4">
<?php if (has_permission('payment_types_add')): ?>
<button type="button" class="btn btn-sm btn-outline-primary me-1"
onclick='openEditModal(<?= htmlspecialchars(json_encode($type), ENT_QUOTES, "UTF-8") ?>)' title="Edit"><i class="bi bi-pencil"></i></button>
<?php endif; ?>
<?php if (has_permission('payment_types_del')): ?>
<a href="?delete=<?= $type['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this payment type?')"><i class="bi bi-trash"></i></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($payment_types)): ?>
<tr>
<td colspan="5" class="text-center py-4 text-muted">No payment types found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($payments_pagination); ?>
</div>
</div>
</div>
<!-- Payment Type Modal -->
<?php if (has_permission('payment_types_add')): ?>
<div class="modal fade" id="paymentTypeModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="paymentTypeModalTitle">Add New Payment Type</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="paymentTypeForm">
<div class="modal-body">
<input type="hidden" name="action" id="paymentTypeAction" value="add_payment_type">
<input type="hidden" name="id" id="paymentTypeId">
<div class="mb-3">
<label class="form-label">Name <span class="text-danger">*</span></label>
<input type="text" name="name" id="paymentTypeName" class="form-control" required placeholder="e.g. Cash, Credit Card">
</div>
<div class="mb-3">
<label class="form-label">Code <span class="text-danger">*</span></label>
<input type="text" name="code" id="paymentTypeCode" class="form-control" required placeholder="e.g. cash, card, loyalty">
</div>
<div class="mb-3 form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" id="paymentTypeIsActive" checked>
<label class="form-check-label" for="paymentTypeIsActive">Is Active</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Payment Type</button>
</div>
</form>
</div>
</div>
</div>
<script>
function getPaymentTypeModal() {
if (typeof bootstrap === 'undefined') return null;
const el = document.getElementById('paymentTypeModal');
return el ? bootstrap.Modal.getOrCreateInstance(el) : null;
}
function openAddModal() {
const modal = getPaymentTypeModal();
if (!modal) return;
document.getElementById('paymentTypeModalTitle').innerText = 'Add New Payment Type';
document.getElementById('paymentTypeAction').value = 'add_payment_type';
document.getElementById('paymentTypeForm').reset();
document.getElementById('paymentTypeId').value = '';
modal.show();
}
function openEditModal(type) {
if (!type) return;
const modal = getPaymentTypeModal();
if (!modal) return;
document.getElementById('paymentTypeModalTitle').innerText = 'Edit Payment Type';
document.getElementById('paymentTypeAction').value = 'edit_payment_type';
document.getElementById('paymentTypeId').value = type.id;
document.getElementById('paymentTypeName').value = type.name || '';
document.getElementById('paymentTypeCode').value = type.code || '';
document.getElementById('paymentTypeIsActive').checked = type.is_active == 1;
modal.show();
}
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

295
admin/product_variants.php Normal file
View File

@ -0,0 +1,295 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
if (!isset($_GET['product_id'])) {
header("Location: products.php");
exit;
}
$product_id = $_GET['product_id'];
// Fetch Product Details
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$product_id]);
$product = $stmt->fetch();
if (!$product) {
header("Location: products.php");
exit;
}
$message = '';
// Handle Add Variant
if (isset($_POST['action']) && $_POST['action'] === 'add_variant') {
$name = $_POST['name'];
$name_ar = $_POST['name_ar'] ?? '';
$price_adj = $_POST['price_adjustment'];
try {
$stmt = $pdo->prepare("INSERT INTO product_variants (product_id, name, name_ar, price_adjustment) VALUES (?, ?, ?, ?)");
$stmt->execute([$product_id, $name, $name_ar, $price_adj]);
header("Location: product_variants.php?product_id=$product_id&added=1");
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Error adding variant: ' . $e->getMessage() . '</div>';
}
}
// Handle Edit Variant
if (isset($_POST['action']) && $_POST['action'] === 'edit_variant') {
$id = $_POST['variant_id'];
$name = $_POST['name'];
$name_ar = $_POST['name_ar'] ?? '';
$price_adj = $_POST['price_adjustment'];
try {
$stmt = $pdo->prepare("UPDATE product_variants SET name = ?, name_ar = ?, price_adjustment = ? WHERE id = ?");
$stmt->execute([$name, $name_ar, $price_adj, $id]);
header("Location: product_variants.php?product_id=$product_id&updated=1");
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Error updating variant: ' . $e->getMessage() . '</div>';
}
}
// Handle Delete (Soft Delete)
if (isset($_GET['delete'])) {
try {
$id = (int)$_GET['delete'];
$pdo->prepare("UPDATE product_variants SET is_deleted = 1 WHERE id = ?")->execute([$id]);
header("Location: product_variants.php?product_id=$product_id&deleted=1");
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Error deleting variant: ' . $e->getMessage() . '</div>';
}
}
if (isset($_GET['added'])) {
$message = '<div class="alert alert-success">Variant added successfully!</div>';
} elseif (isset($_GET['updated'])) {
$message = '<div class="alert alert-success">Variant updated successfully!</div>';
} elseif (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success">Variant removed successfully!</div>';
}
$query = "SELECT * FROM product_variants WHERE product_id = ? AND is_deleted = 0 ORDER BY price_adjustment ASC";
$variants_pagination = paginate_query($pdo, $query, [$product_id]);
$variants = $variants_pagination['data'];
include 'includes/header.php';
$effective_base_price = get_product_price($product);
?>
<div class="mb-4">
<a href="products.php" class="text-decoration-none text-muted mb-2 d-inline-block"><i class="bi bi-arrow-left"></i> Back to Products</a>
<div class="d-flex justify-content-between align-items-center">
<div>
<h2 class="fw-bold mb-0">Variants: <?= htmlspecialchars($product['name']) ?></h2>
<p class="text-muted mb-0">Manage sizes, extras, or options.</p>
</div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addVariantModal">
<i class="bi bi-plus-lg"></i> Add Variant
</button>
</div>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($variants_pagination, ['product_id' => $product_id]); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Variant Name</th>
<th>Arabic Name</th>
<th>Price Adjustment</th>
<th>Final Price (Est.)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($variants as $variant): ?>
<tr>
<td class="ps-4 fw-medium"><?= htmlspecialchars($variant['name']) ?></td>
<td><div class="text-primary small fw-semibold" dir="rtl"><?= htmlspecialchars($variant['name_ar'] ?? '-') ?></div></td>
<td>
<?php if ($variant['price_adjustment'] > 0): ?>
<span class="text-danger">+ <?= format_currency($variant['price_adjustment']) ?></span>
<?php elseif ($variant['price_adjustment'] < 0): ?>
<span class="text-success">- <?= format_currency(abs((float)$variant['price_adjustment'])) ?></span>
<?php else: ?>
<span class="text-muted">No change</span>
<?php endif; ?>
</td>
<td>
<div class="fw-bold"><?= format_currency($effective_base_price + $variant['price_adjustment']) ?></div>
<?php if ($effective_base_price < $product['price']): ?>
<small class="text-muted text-decoration-line-through"><?= format_currency($product['price'] + $variant['price_adjustment']) ?></small>
<?php endif; ?>
</td>
<td>
<button class="btn btn-sm btn-outline-primary me-1"
data-bs-toggle="modal"
data-bs-target="#editVariantModal"
data-id="<?= $variant['id'] ?>"
data-name="<?= htmlspecialchars($variant['name']) ?>"
data-name-ar="<?= htmlspecialchars($variant['name_ar'] ?? '') ?>"
data-price="<?= $variant['price_adjustment'] ?>">
<i class="bi bi-pencil"></i>
</button>
<a href="?product_id=<?= $product_id ?>&delete=<?= $variant['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this variant?')"><i class="bi bi-trash"></i></a>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($variants)): ?>
<tr>
<td colspan="5" class="text-center py-5 text-muted">No variants defined (e.g., Small, Large, Spicy).</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($variants_pagination, ['product_id' => $product_id]); ?>
</div>
</div>
</div>
<!-- Add Variant Modal -->
<div class="modal fade" id="addVariantModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Variant</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="add_variant">
<div class="mb-3">
<label class="form-label">Variant Name (EN)</label>
<div class="input-group">
<input type="text" name="name" id="add_name" class="form-control" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateField('add_name', 'add_name_ar')">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">Variant Name (AR)</label>
<input type="text" name="name_ar" id="add_name_ar" class="form-control text-end" dir="rtl">
</div>
<div class="mb-3">
<label class="form-label">Price Adjustment (+/-)</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" step="0.01" name="price_adjustment" class="form-control" value="0.00" required>
</div>
<div class="form-text">Enter positive value for extra cost, negative for discount.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Variant</button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Variant Modal -->
<div class="modal fade" id="editVariantModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Variant</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST">
<div class="modal-body">
<input type="hidden" name="action" value="edit_variant">
<input type="hidden" name="variant_id" id="edit_variant_id">
<div class="mb-3">
<label class="form-label">Variant Name (EN)</label>
<div class="input-group">
<input type="text" name="name" id="edit_variant_name" class="form-control" required>
<button class="btn btn-outline-secondary" type="button" onclick="translateField('edit_variant_name', 'edit_variant_name_ar')">
<i class="bi bi-translate"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">Variant Name (AR)</label>
<input type="text" name="name_ar" id="edit_variant_name_ar" class="form-control text-end" dir="rtl">
</div>
<div class="mb-3">
<label class="form-label">Price Adjustment (+/-)</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="number" step="0.01" name="price_adjustment" id="edit_variant_price" class="form-control" required>
</div>
<div class="form-text">Enter positive value for extra cost, negative for discount.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Update Variant</button>
</div>
</form>
</div>
</div>
</div>
<script>
var editVariantModal = document.getElementById('editVariantModal');
if (editVariantModal) {
editVariantModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
var id = button.getAttribute('data-id');
var name = button.getAttribute('data-name');
var nameAr = button.getAttribute('data-name-ar');
var price = button.getAttribute('data-price');
var modalIdInput = editVariantModal.querySelector('#edit_variant_id');
var modalNameInput = editVariantModal.querySelector('#edit_variant_name');
var modalNameArInput = editVariantModal.querySelector('#edit_variant_name_ar');
var modalPriceInput = editVariantModal.querySelector('#edit_variant_price');
modalIdInput.value = id;
modalNameInput.value = name;
modalNameArInput.value = nameAr;
modalPriceInput.value = price;
});
}
async function translateField(sourceId, targetId) {
const text = document.getElementById(sourceId).value;
if (!text) return;
try {
const response = await fetch('../api/translate.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: text, target_lang: 'Arabic' })
});
const data = await response.json();
if (data.success) {
document.getElementById(targetId).value = data.translated_text;
}
} catch (e) {
console.error(e);
}
}
</script>
<?php include 'includes/footer.php'; ?>

505
admin/products.php Normal file
View File

@ -0,0 +1,505 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('products_view');
$message = '';
// Handle Add/Edit Product
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
$name = $_POST['name'];
$name_ar = $_POST['name_ar'] ?? '';
$category_id = (int)$_POST['category_id'];
$price = (float)$_POST['price'];
$vat_percent = (float)($_POST['vat_percent'] ?? 0);
$cost_price = (float)($_POST['cost_price'] ?? 0);
$stock_quantity = (int)($_POST['stock_quantity'] ?? 0);
$description = $_POST['description'] ?? '';
$promo_discount_percent = !empty($_POST['promo_discount_percent']) ? (float)$_POST['promo_discount_percent'] : null;
$promo_date_from = !empty($_POST['promo_date_from']) ? $_POST['promo_date_from'] : null;
$promo_date_to = !empty($_POST['promo_date_to']) ? $_POST['promo_date_to'] : null;
$is_loyalty = isset($_POST['is_loyalty']) ? 1 : 0;
$show_in_qorder = isset($_POST['show_in_qorder']) ? 1 : 0;
$image_url = null;
if ($id) {
$stmt = $pdo->prepare("SELECT image_url FROM products WHERE id = ?");
$stmt->execute([$id]);
$image_url = $stmt->fetchColumn();
}
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../assets/images/products/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$file_ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
if (in_array($file_ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
$fileName = uniqid('prod_') . '.' . $file_ext;
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadDir . $fileName)) {
$image_url = 'assets/images/products/' . $fileName;
}
}
}
try {
if ($action === 'edit_product' && $id) {
if (!has_permission('products_edit')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to edit products.</div>';
} else {
$stmt = $pdo->prepare("UPDATE products SET name = ?, name_ar = ?, category_id = ?, price = ?, vat_percent = ?, cost_price = ?, stock_quantity = ?, description = ?, image_url = ?, promo_discount_percent = ?, promo_date_from = ?, promo_date_to = ?, is_loyalty = ?, show_in_qorder = ?, show_in_online_order = ? WHERE id = ?");
$stmt->execute([$name, $name_ar, $category_id, $price, $vat_percent, $cost_price, $stock_quantity, $description, $image_url, $promo_discount_percent, $promo_date_from, $promo_date_to, $is_loyalty, $show_in_qorder, $show_in_online_order, $id]);
$message = '<div class="alert alert-success">Product updated successfully!</div>';
}
} elseif ($action === 'add_product') {
if (!has_permission('products_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to add products.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO products (name, name_ar, category_id, price, vat_percent, cost_price, stock_quantity, description, image_url, promo_discount_percent, promo_date_from, promo_date_to, is_loyalty, show_in_qorder, show_in_online_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$name, $name_ar, $category_id, $price, $vat_percent, $cost_price, $stock_quantity, $description, $image_url, $promo_discount_percent, $promo_date_from, $promo_date_to, $is_loyalty, $show_in_qorder, $show_in_online_order]);
$message = '<div class="alert alert-success">Product created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
// Handle Delete (Soft Delete)
if (isset($_GET['delete'])) {
if (!has_permission('products_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete products.</div>';
} else {
try {
$id = (int)$_GET['delete'];
// Use Soft Delete to preserve data integrity for orders
$pdo->prepare("UPDATE products SET is_deleted = 1 WHERE id = ?")->execute([$id]);
$pdo->prepare("UPDATE product_variants SET is_deleted = 1 WHERE product_id = ?")->execute([$id]);
header("Location: products.php?deleted=1");
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Error deleting product: ' . $e->getMessage() . '</div>';
}
}
}
if (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success">Product removed successfully!</div>';
}
$categories = $pdo->query("SELECT * FROM categories WHERE is_deleted = 0 ORDER BY name ASC")->fetchAll();
// Build Query with Filters
$params = [];
$where = ["p.is_deleted = 0"]; // Base filter for soft delete
$search = $_GET['search'] ?? '';
$category_filter = $_GET['category_filter'] ?? '';
if ($search) {
$where[] = "(p.name LIKE :search OR p.name_ar LIKE :search OR p.description LIKE :search)";
$params[':search'] = "%$search%";
}
if ($category_filter) {
$where[] = "p.category_id = :category_id";
$params[':category_id'] = $category_filter;
}
$where_clause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
$query = "SELECT p.*, c.name as category_name
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
$where_clause
ORDER BY p.name ASC";
$products_pagination = paginate_query($pdo, $query, $params);
$products = $products_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1"><?= t('products') ?></h2>
<p class="text-muted mb-0">Manage your menu items and stock</p>
</div>
<?php if (has_permission('products_add')): ?>
<button class="btn btn-primary btn-lg shadow-sm" data-bs-toggle="modal" data-bs-target="#productModal" onclick="prepareAddForm()" style="border-radius: 12px;">
<i class="bi bi-plus-lg me-1"></i> <?= t('add') ?> <?= t('products') ?>
</button>
<?php endif; ?>
</div>
<?= $message ?>
<!-- Filters -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body bg-light">
<form method="GET" class="row g-3 align-items-end">
<div class="col-md-5">
<label class="form-label small fw-bold text-muted"><?= t('search') ?></label>
<div class="input-group">
<span class="input-group-text bg-white border-end-0"><i class="bi bi-search text-muted"></i></span>
<input type="text" name="search" class="form-control border-start-0" placeholder="Search by name or description..." value="<?= htmlspecialchars($search) ?>">
</div>
</div>
<div class="col-md-4">
<label class="form-label small fw-bold text-muted"><?= t('category') ?></label>
<select name="category_filter" class="form-select">
<option value=""><?= t('all') ?> <?= t('categories') ?></option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>" <?= $category_filter == $cat['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($cat['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-filter"></i> <?= t('filter') ?>
</button>
<a href="products.php" class="btn btn-outline-secondary" title="Clear Filters">
<i class="bi bi-arrow-counterclockwise"></i>
</a>
</div>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($products_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4"><?= t('product') ?></th>
<th><?= t('category') ?></th>
<th><?= t('price') ?></th>
<th><?= t('VAT') ?></th>
<th><?= t('stock') ?></th>
<th><?= t('promotion') ?></th>
<th class="text-end pe-4"><?= t('actions') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($products as $product): ?>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center py-2">
<?php if ($product['image_url']): ?>
<img src="<?= htmlspecialchars(strpos($product['image_url'], 'http') === 0 ? $product['image_url'] : '../' . $product['image_url']) ?>" alt="" class="rounded-3 me-3 border 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 me-3 border" style="width: 50px; height: 50px;">
<i class="bi bi-image text-muted opacity-50"></i>
</div>
<?php endif; ?>
<div>
<div class="fw-bold text-dark fs-6">
<?= htmlspecialchars($product['name']) ?>
<?php if ($product['is_loyalty']): ?>
<span class="badge bg-info bg-opacity-10 text-info border border-info rounded-pill ms-1" style="font-size: 0.7rem;">
<i class="bi bi-star-fill"></i> Loyalty
</span>
<?php endif; ?>
<?php if ($product["show_in_qorder"]): ?>
<span class="badge bg-primary bg-opacity-10 text-primary border border-primary rounded-pill ms-1" style="font-size: 0.7rem;">
<i class="bi bi-qr-code"></i> QR Menu
</span>
<?php endif; ?>
<?php if ($product["show_in_online_order"] ?? false): ?>
<span class="badge bg-success bg-opacity-10 text-success border border-success rounded-pill ms-1" style="font-size: 0.7rem;">
<i class="bi bi-globe"></i> Online
</span>
<?php endif; ?>
</div>
<small class="text-muted"><?= htmlspecialchars($product['name_ar'] ?? '') ?></small>
</div>
</div>
</td>
<td>
<span class="badge bg-light text-dark border"><?= htmlspecialchars($product['category_name'] ?? t('none')) ?></span>
</td>
<td>
<div class="fw-bold text-dark"><?= format_currency($product['price']) ?></div>
<?php if ($product['cost_price'] > 0): ?>
<small class="text-muted"><?= t('cost') ?>: <?= format_currency($product['cost_price']) ?></small>
<?php endif; ?>
</td>
<td>
<span class="badge bg-light text-dark border"><?= (float)$product['vat_percent'] ?>%</span>
</td>
<td>
<?php if ($product['stock_quantity'] <= 5): ?>
<span class="badge bg-danger bg-opacity-10 text-danger border border-danger rounded-pill px-3"><?= $product['stock_quantity'] ?></span>
<?php else: ?>
<span class="badge bg-success bg-opacity-10 text-success border border-success rounded-pill px-3"><?= $product['stock_quantity'] ?></span>
<?php endif; ?>
</td>
<td>
<?php if ($product['promo_discount_percent'] > 0): ?>
<span class="badge bg-warning bg-opacity-10 text-warning border border-warning rounded-pill px-2">
<i class="bi bi-megaphone-fill me-1"></i><?= $product['promo_discount_percent'] ?>% Off
</span>
<?php else: ?>
<span class="text-muted small">-</span>
<?php endif; ?>
</td>
<td class="text-end pe-4">
<div class="d-inline-flex gap-2">
<?php if (has_permission('products_edit')): ?>
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-3"
data-bs-toggle="modal" data-bs-target="#productModal"
onclick='prepareEditForm(<?= htmlspecialchars(json_encode($product), ENT_QUOTES, "UTF-8") ?>)'><?= t('edit') ?></button>
<?php endif; ?>
<?php if (has_permission('products_del')): ?>
<a href="?delete=<?= $product['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('<?= t('are_you_sure') ?>')"><?= t('delete') ?></a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($products)): ?>
<tr>
<td colspan="7" class="text-center py-5 text-muted">
<i class="bi bi-inbox fs-1 d-block mb-2 opacity-25"></i>
<?= t('none') ?>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($products_pagination); ?>
</div>
</div>
</div>
<!-- Product Modal -->
<?php if (has_permission('products_add') || has_permission('products_edit')): ?>
<div class="modal fade" id="productModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header bg-primary text-white border-0 py-3">
<h5 class="modal-title fw-bold" id="productModalTitle"><?= t('add') ?> <?= t('products') ?></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="productForm" enctype="multipart/form-data">
<div class="modal-body p-4">
<input type="hidden" name="action" id="productAction" value="add_product">
<input type="hidden" name="id" id="productId">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small fw-bold text-muted d-flex justify-content-between">
<span><?= t('name') ?> (EN) <span class="text-danger">*</span></span>
<a href="javascript:void(0)" onclick="translateTo('English')" class="text-decoration-none small text-primary fw-bold" id="translateBtnEn">
<i class="bi bi-translate me-1"></i> Auto-translate
</a>
</label>
<input type="text" name="name" id="productName" class="form-control rounded-3 border-0 bg-light" required>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-muted d-flex justify-content-between">
<span><?= t('arabic_name') ?></span>
<a href="javascript:void(0)" onclick="translateTo('Arabic')" class="text-decoration-none small text-primary fw-bold" id="translateBtnAr">
<i class="bi bi-translate me-1"></i> Auto-translate
</a>
</label>
<input type="text" name="name_ar" id="productNameAr" class="form-control rounded-3 border-0 bg-light" dir="rtl">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold text-muted"><?= t('category') ?> <span class="text-danger">*</span></label>
<select name="category_id" id="productCategoryId" class="form-select rounded-3 border-0 bg-light" required>
<option value=""><?= t('select') ?> <?= t('category') ?></option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= htmlspecialchars($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold text-muted"><?= t('price') ?> <span class="text-danger">*</span></label>
<input type="number" step="0.01" name="price" id="productPrice" class="form-control rounded-3 border-0 bg-light" required>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold text-muted"><?= t('VAT') ?> (%)</label>
<select name="vat_percent" id="productVatPercent" class="form-select rounded-3 border-0 bg-light">
<option value="0">0%</option>
<option value="5">5%</option>
<option value="10">10%</option>
<option value="15">15%</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label small fw-bold text-muted"><?= t('cost') ?> <?= t('price') ?></label>
<input type="number" step="0.01" name="cost_price" id="productCostPrice" class="form-control rounded-3 border-0 bg-light">
</div>
<div class="col-md-2">
<label class="form-label small fw-bold text-muted"><?= t('stock') ?> <?= t('quantity') ?></label>
<input type="number" name="stock_quantity" id="productStockQuantity" class="form-control rounded-3 border-0 bg-light">
</div>
<div class="col-12">
<label class="form-label small fw-bold text-muted"><?= t('description') ?></label>
<input type="text" name="description" id="productDescription" class="form-control rounded-3 border-0 bg-light">
</div>
<div class="col-12">
<label class="form-label small fw-bold text-muted"><?= t('image') ?></label>
<div class="d-flex align-items-center gap-3 bg-light p-3 rounded-4 border border-dashed">
<img src="" id="productImagePreview" class="rounded-3 border shadow-sm" style="width: 60px; height: 60px; object-fit: cover; display: none;">
<div class="flex-grow-1">
<input type="file" name="image" class="form-control border-0 bg-transparent" accept="image/*">
</div>
</div>
</div>
<div class="col-12 mt-4">
<div class="form-check form-switch p-3 bg-light rounded-3 border d-flex justify-content-between align-items-center mb-3">
<div class="ms-1">
<label class="form-check-label fw-bold text-dark mb-0" for="productIsLoyalty">Loyalty System Participation</label>
<div class="small text-muted">Include this product in earning and redeeming loyalty points</div>
</div>
<input class="form-check-input ms-0" type="checkbox" name="is_loyalty" id="productIsLoyalty" style="width: 2.5rem; height: 1.25rem;">
</div>
<div class="form-check form-switch p-3 bg-light rounded-3 border d-flex justify-content-between align-items-center mb-3">
<div class="ms-1">
<label class="form-check-label fw-bold text-dark mb-0" for="productShowInQorder">Show in QR Order</label>
<div class="small text-muted">Make this product visible in the customer QR menu</div>
</div>
<input class="form-check-input ms-0" type="checkbox" name="show_in_qorder" id="productShowInQorder" style="width: 2.5rem; height: 1.25rem;" checked>
</div>
<div class="form-check form-switch p-3 bg-light rounded-3 border d-flex justify-content-between align-items-center mb-3">
<div class="ms-1">
<label class="form-check-label fw-bold text-dark mb-0" for="productShowInOnlineOrder">Show in Online Order</label>
<div class="small text-muted">Make this product visible in the Online Ordering page</div>
</div>
<input class="form-check-input ms-0" type="checkbox" name="show_in_online_order" id="productShowInOnlineOrder" style="width: 2.5rem; height: 1.25rem;" checked>
</div>
<h6 class="fw-bold border-bottom pb-2 mb-3"><i class="bi bi-percent me-1"></i> Promotion Settings</h6>
<div class="row g-3">
<div class="col-md-4">
<label class="form-label small fw-bold text-muted">Discount (%)</label>
<input type="number" step="0.1" name="promo_discount_percent" id="productPromoDiscount" class="form-control rounded-3 border-0 bg-light" min="0" max="100">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold text-muted">From Date</label>
<input type="date" name="promo_date_from" id="productPromoFrom" class="form-control rounded-3 border-0 bg-light">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold text-muted">To Date</label>
<input type="date" name="promo_date_to" id="productPromoTo" class="form-control rounded-3 border-0 bg-light">
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal"><?= t('cancel') ?></button>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm"><?= t('save') ?></button>
</div>
</form>
</div>
</div>
</div>
<script>
function prepareAddForm() {
document.getElementById('productModalTitle').innerText = '<?= t('add') ?> <?= t('products') ?>';
document.getElementById('productAction').value = 'add_product';
document.getElementById('productForm').reset();
document.getElementById('productId').value = '';
document.getElementById('productImagePreview').style.display = 'none';
document.getElementById('productShowInQorder').checked = true;
document.getElementById('productShowInOnlineOrder').checked = true;
}
function prepareEditForm(p) {
if (!p) return;
document.getElementById('productModalTitle').innerText = '<?= t('edit') ?>: ' + p.name;
document.getElementById('productAction').value = 'edit_product';
document.getElementById('productId').value = p.id;
document.getElementById('productName').value = p.name;
document.getElementById('productNameAr').value = p.name_ar || '';
document.getElementById('productCategoryId').value = p.category_id;
document.getElementById('productPrice').value = p.price;
document.getElementById('productVatPercent').value = parseFloat(p.vat_percent || 0);
document.getElementById('productCostPrice').value = p.cost_price || '';
document.getElementById('productStockQuantity').value = p.stock_quantity || '0';
document.getElementById('productDescription').value = p.description || '';
document.getElementById('productPromoDiscount').value = p.promo_discount_percent || '';
document.getElementById('productPromoFrom').value = p.promo_date_from || '';
document.getElementById('productPromoTo').value = p.promo_date_to || '';
document.getElementById('productIsLoyalty').checked = p.is_loyalty == 1;
document.getElementById('productShowInQorder').checked = p.show_in_qorder == 1;
document.getElementById('productShowInOnlineOrder').checked = (p.show_in_online_order === undefined ? true : p.show_in_online_order == 1);
if (p.image_url) {
const preview = document.getElementById('productImagePreview');
preview.src = p.image_url.startsWith('http') ? p.image_url : '../' + p.image_url;
preview.style.display = 'block';
} else {
document.getElementById('productImagePreview').style.display = 'none';
}
}
async function translateTo(targetLang) {
const sourceId = targetLang === 'Arabic' ? 'productName' : 'productNameAr';
const targetId = targetLang === 'Arabic' ? 'productNameAr' : 'productName';
const btnId = targetLang === 'Arabic' ? 'translateBtnAr' : 'translateBtnEn';
const sourceText = document.getElementById(sourceId).value;
if (!sourceText) {
alert('Please enter text to translate first.');
return;
}
const btn = document.getElementById(btnId);
const originalContent = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Translating...';
btn.classList.add('disabled');
try {
const response = await fetch('../api/translate.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: sourceText, target_lang: targetLang })
});
const data = await response.json();
if (data.success) {
document.getElementById(targetId).value = data.translated_text;
} else {
alert('Translation error: ' + data.error);
}
} catch (error) {
console.error('Translation error:', error);
alert('An error occurred during translation.');
} finally {
btn.innerHTML = originalContent;
btn.classList.remove('disabled');
}
}
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

258
admin/profile.php Normal file
View File

@ -0,0 +1,258 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
require_login();
$pdo = db();
$currentUser = get_logged_user();
$id = $currentUser['id'];
// Helper for fresh data
function fetch_user_data($pdo, $id) {
$stmt = $pdo->prepare("SELECT u.*, g.name as group_name, g.permissions
FROM users u
LEFT JOIN user_groups g ON u.group_id = g.id
WHERE u.id = ?");
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
$user = fetch_user_data($pdo, $id);
if (!$user) {
logout_user();
header('Location: ' . get_base_url() . 'login.php');
exit;
}
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$full_name = $_POST['full_name'];
$full_name_ar = $_POST['full_name_ar'] ?? '';
$email = $_POST['email'];
$pdo->beginTransaction();
try {
$sql = "UPDATE users SET full_name = ?, full_name_ar = ?, email = ? WHERE id = ?";
$params = [$full_name, $full_name_ar, $email, $id];
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
// Update password if provided
if (!empty($_POST['password'])) {
$password = password_hash($_POST['password'], PASSWORD_DEFAULT);
$pdo->prepare("UPDATE users SET password = ? WHERE id = ?")->execute([$password, $id]);
}
// Handle Profile Picture Upload
if (isset($_FILES['profile_pic']) && $_FILES['profile_pic']['error'] === UPLOAD_ERR_OK) {
$upload_dir = __DIR__ . '/../assets/images/users/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0775, true);
}
$file_tmp = $_FILES['profile_pic']['tmp_name'];
$file_name = $_FILES['profile_pic']['name'];
$file_ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
$allowed_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (in_array($file_ext, $allowed_exts)) {
$new_file_name = 'user_' . $id . '_' . uniqid() . '.' . $file_ext;
$upload_path = $upload_dir . $new_file_name;
if (move_uploaded_file($file_tmp, $upload_path)) {
// Delete old profile pic if exists
if ($user['profile_pic'] && file_exists(__DIR__ . '/../' . $user['profile_pic'])) {
unlink(__DIR__ . '/../' . $user['profile_pic']);
}
$profile_pic_path = 'assets/images/users/' . $new_file_name;
$pdo->prepare("UPDATE users SET profile_pic = ? WHERE id = ?")->execute([$profile_pic_path, $id]);
}
}
}
$pdo->commit();
$message = '<div class="alert alert-success">Profile updated successfully!</div>';
// Refresh user data and update session
$user = fetch_user_data($pdo, $id);
$_SESSION['user'] = $user;
unset($_SESSION['user']['password']);
} catch (Exception $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
$message = '<div class="alert alert-danger">Error updating profile: ' . $e->getMessage() . '</div>';
}
}
include 'includes/header.php';
?>
<div class="mb-4">
<h2 class="fw-bold"><?= t('my_profile') ?></h2>
<p class="text-muted">Manage your personal information and account settings.</p>
</div>
<?= $message ?>
<div class="row">
<div class="col-md-8">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<form method="POST" enctype="multipart/form-data">
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label small fw-bold text-muted d-flex justify-content-between">
<span><?= t('name') ?> (EN) <span class="text-danger">*</span></span>
<a href="javascript:void(0)" onclick="translateTo('English')" class="text-decoration-none small text-primary fw-bold" id="translateBtnEn">
<i class="bi bi-translate me-1"></i> Auto-translate
</a>
</label>
<input type="text" name="full_name" id="full_name" class="form-control rounded-3" value="<?= htmlspecialchars($user['full_name'] ?? '') ?>" required>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-muted d-flex justify-content-between">
<span><?= t('arabic_name') ?></span>
<a href="javascript:void(0)" onclick="translateTo('Arabic')" class="text-decoration-none small text-primary fw-bold" id="translateBtnAr">
<i class="bi bi-translate me-1"></i> Auto-translate
</a>
</label>
<input type="text" name="full_name_ar" id="full_name_ar" class="form-control rounded-3 text-end" value="<?= htmlspecialchars($user['full_name_ar'] ?? '') ?>" dir="rtl">
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">USERNAME (READ-ONLY)</label>
<input type="text" class="form-control bg-light" value="<?= htmlspecialchars($user['username'] ?? '') ?>" readonly>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-muted"><?= t('email') ?></label>
<input type="email" name="email" class="form-control" value="<?= htmlspecialchars($user['email'] ?? '') ?>" required>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">ROLE / GROUP</label>
<input type="text" class="form-control bg-light" value="<?= htmlspecialchars($user['group_name'] ?? '') ?>" readonly>
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">EMPLOYEE / BIOMETRIC ID</label>
<input type="text" class="form-control bg-light" value="<?= htmlspecialchars($user['employee_id'] ?? 'Not assigned') ?>" readonly>
</div>
</div>
<div class="row mb-4">
<div class="col-md-12">
<label class="form-label small fw-bold text-muted">PROFILE PICTURE</label>
<div class="d-flex align-items-center gap-3">
<?php if ($user['profile_pic']): ?>
<img src="../<?= htmlspecialchars($user['profile_pic']) ?>?v=<?= time() ?>" alt="Profile Picture" class="rounded-circle shadow-sm" style="width: 100px; height: 100px; object-fit: cover;">
<?php else: ?>
<div class="bg-primary bg-gradient text-white rounded-circle d-flex align-items-center justify-content-center shadow-sm" style="width: 100px; height: 100px; font-weight: 700; font-size: 2rem;">
<?= strtoupper(substr(($user['full_name'] ?? $user['username'] ?? 'U'), 0, 1)) ?>
</div>
<?php endif; ?>
<div class="flex-grow-1">
<input type="file" name="profile_pic" class="form-control" accept="image/*">
<div class="form-text mt-1">Allowed: JPG, PNG, GIF, WebP. Recommended: Square image.</div>
</div>
</div>
</div>
</div>
<div class="mb-4">
<label class="form-label small fw-bold text-muted">NEW PASSWORD (LEAVE BLANK TO KEEP CURRENT)</label>
<input type="password" name="password" class="form-control" placeholder="******">
</div>
<hr class="my-4">
<div class="d-flex justify-content-end gap-2">
<button type="submit" class="btn btn-primary rounded-pill px-5 fw-bold"><?= t('save') ?></button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card border-0 shadow-sm rounded-4 bg-primary bg-gradient text-white shadow mb-4">
<div class="card-body p-4 text-center">
<div class="mb-3">
<?php if ($user['profile_pic']): ?>
<img src="../<?= htmlspecialchars($user['profile_pic']) ?>?v=<?= time() ?>" alt="Profile Picture" class="rounded-circle shadow-sm border border-3 border-white mx-auto" style="width: 120px; height: 120px; object-fit: cover;">
<?php else: ?>
<div class="bg-white text-primary rounded-circle d-flex align-items-center justify-content-center mx-auto shadow-sm" style="width:120px;height:120px; font-weight:700; font-size:3rem;">
<?= strtoupper(substr(($user['full_name'] ?? $user['username'] ?? 'U'), 0, 1)) ?>
</div>
<?php endif; ?>
</div>
<h4 class="fw-bold mb-1"><?= htmlspecialchars($user['full_name'] ?? '') ?></h4>
<div class="small opacity-75 mb-3">@<?= htmlspecialchars($user['username'] ?? '') ?> • <?= htmlspecialchars($user['group_name'] ?? '') ?></div>
<div class="badge bg-white text-primary rounded-pill px-3 py-2 mb-3">
Active Account
</div>
<div class="small opacity-75">
Member since <?= date('F d, Y', strtotime($user['created_at'] ?? 'now')) ?>
</div>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-4">
<h6 class="fw-bold mb-3"><i class="bi bi-shield-check me-2 text-primary"></i> Account Security</h6>
<ul class="list-unstyled small mb-0">
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i> Password is encrypted</li>
<li class="mb-2"><i class="bi bi-check-circle-fill text-success me-2"></i> Role-based access control</li>
<li><i class="bi bi-check-circle-fill text-success me-2"></i> Session-based authentication</li>
</ul>
</div>
</div>
</div>
</div>
<script>
async function translateTo(targetLang) {
const sourceId = targetLang === 'Arabic' ? 'full_name' : 'full_name_ar';
const targetId = targetLang === 'Arabic' ? 'full_name_ar' : 'full_name';
const btnId = targetLang === 'Arabic' ? 'translateBtnAr' : 'translateBtnEn';
const sourceText = document.getElementById(sourceId).value;
if (!sourceText) {
alert('Please enter text to translate first.');
return;
}
const btn = document.getElementById(btnId);
// Note: since the buttons are different elements here, we might need a better way to find them or just use alert
// Actually I'll just use the target lang to identify
try {
const response = await fetch('../api/translate.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: sourceText, target_lang: targetLang })
});
const data = await response.json();
if (data.success) {
document.getElementById(targetId).value = data.translated_text;
} else {
alert('Translation error: ' + data.error);
}
} catch (error) {
console.error('Translation error:', error);
alert('An error occurred during translation.');
}
}
</script>
<?php include 'includes/footer.php'; ?>

549
admin/purchases.php Normal file
View File

@ -0,0 +1,549 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../db/config.php";
require_permission("purchases_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
// Handle SAVE (Add/Edit) Purchase via POST
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'save_purchase') {
$id = $_POST['id'] ?: null;
$supplier_id = $_POST['supplier_id'] ?: null;
$purchase_date = $_POST['purchase_date'];
$status = $_POST['status'];
$notes = $_POST['notes'];
$product_ids = $_POST['product_id'] ?? [];
$quantities = $_POST['quantity'] ?? [];
$cost_prices = $_POST['cost_price'] ?? [];
try {
$pdo->beginTransaction();
$total_amount = 0;
foreach ($product_ids as $index => $pid) {
$total_amount += $quantities[$index] * $cost_prices[$index];
}
$purchase = null;
if ($id) {
$stmt = $pdo->prepare("SELECT * FROM purchases WHERE id = ?");
$stmt->execute([$id]);
$purchase = $stmt->fetch();
if ($purchase) {
$old_status = $purchase['status'];
$stmt = $pdo->prepare("UPDATE purchases SET supplier_id = ?, purchase_date = ?, status = ?, notes = ?, total_amount = ? WHERE id = ?");
$stmt->execute([$supplier_id, $purchase_date, $status, $notes, $total_amount, $id]);
$stmt = $pdo->prepare("SELECT * FROM purchase_items WHERE purchase_id = ?");
$stmt->execute([$id]);
$old_items = $stmt->fetchAll();
if ($old_status === 'completed') {
foreach ($old_items as $oi) {
$pdo->prepare("UPDATE products SET stock_quantity = stock_quantity - ? WHERE id = ?")
->execute([$oi['quantity'], $oi['product_id']]);
}
}
$pdo->prepare("DELETE FROM purchase_items WHERE purchase_id = ?")->execute([$id]);
}
} else {
$stmt = $pdo->prepare("INSERT INTO purchases (supplier_id, purchase_date, status, notes, total_amount) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$supplier_id, $purchase_date, $status, $notes, $total_amount]);
$id = $pdo->lastInsertId();
}
foreach ($product_ids as $index => $pid) {
$qty = $quantities[$index];
$cost = $cost_prices[$index];
$total_item_price = $qty * $cost;
$stmt = $pdo->prepare("INSERT INTO purchase_items (purchase_id, product_id, quantity, cost_price, total_price) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$id, $pid, $qty, $cost, $total_item_price]);
if ($status === 'completed') {
$pdo->prepare("UPDATE products SET stock_quantity = stock_quantity + ?, cost_price = ? WHERE id = ?")
->execute([$qty, $cost, $pid]);
}
}
$pdo->commit();
$message = '<div class="alert alert-success alert-dismissible fade show" role="alert">Purchase saved successfully!<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
} catch (Exception $e) {
$pdo->rollBack();
$message = '<div class="alert alert-danger">Error: ' . $e->getMessage() . '</div>';
}
}
// Handle Delete
if (isset($_GET['delete'])) {
if (!has_permission('purchases_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete purchases.</div>';
} else {
try {
$id = $_GET['delete'];
$pdo->beginTransaction();
$pdo->prepare("DELETE FROM purchase_items WHERE purchase_id = ?")->execute([$id]);
$pdo->prepare("DELETE FROM purchases WHERE id = ?")->execute([$id]);
$pdo->commit();
header("Location: purchases.php?deleted=1");
exit;
} catch (PDOException $e) {
$pdo->rollBack();
$message = '<div class="alert alert-danger">Error deleting purchase: ' . $e->getMessage() . '</div>';
}
}
}
if (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success alert-dismissible fade show" role="alert">Purchase record deleted successfully!<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
}
$suppliers = $pdo->query("SELECT * FROM suppliers ORDER BY name")->fetchAll();
$products = $pdo->query("SELECT id, name, cost_price FROM products ORDER BY name")->fetchAll();
$products_json = json_encode($products);
$search = $_GET['search'] ?? '';
$supplier_filter = $_GET['supplier_filter'] ?? '';
$status_filter = $_GET['status_filter'] ?? '';
$params = [];
$where = [];
$query = "SELECT p.*, s.name as supplier_name
FROM purchases p
LEFT JOIN suppliers s ON p.supplier_id = s.id";
if ($search) {
$where[] = "p.notes LIKE ?";
$params[] = "%$search%";
}
if ($supplier_filter) {
$where[] = "p.supplier_id = ?";
$params[] = $supplier_filter;
}
if ($status_filter) {
$where[] = "p.status = ?";
$params[] = $status_filter;
}
if (!empty($where)) {
$query .= " WHERE " . implode(" AND ", $where);
}
$query .= " ORDER BY p.purchase_date DESC, p.id DESC";
$purchases_pagination = paginate_query($pdo, $query, $params);
$purchases = $purchases_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1 text-dark">Purchases Inventory</h2>
<p class="text-muted mb-0">Manage restocks, supplier invoices and inventory tracking</p>
</div>
<?php if (has_permission('purchases_add')): ?>
<button type="button" class="btn btn-primary btn-lg shadow-sm" style="border-radius: 12px;" data-bs-toggle="modal" data-bs-target="#purchaseModal" onclick="prepareAddPurchase()">
<i class="bi bi-plus-lg me-1"></i> New Purchase Order
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm mb-4 rounded-4">
<div class="card-body p-4">
<form method="GET" class="row g-3 align-items-center">
<div class="col-md-4">
<div class="input-group">
<span class="input-group-text bg-light border-0 text-muted"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control border-0 bg-light" placeholder="Search by notes..." value="<?= htmlspecialchars($search) ?>" style="border-radius: 0 10px 10px 0;">
</div>
</div>
<div class="col-md-3">
<select name="supplier_filter" class="form-select border-0 bg-light rounded-3">
<option value="">All Suppliers</option>
<?php foreach ($suppliers as $s): ?>
<option value="<?= $s['id'] ?>" <?= $supplier_filter == $s['id'] ? 'selected' : '' ?>><?= htmlspecialchars($s['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<select name="status_filter" class="form-select border-0 bg-light rounded-3">
<option value="">All Status</option>
<option value="pending" <?= $status_filter == 'pending' ? 'selected' : '' ?>>Pending</option>
<option value="completed" <?= $status_filter == 'completed' ? 'selected' : '' ?>>Completed</option>
<option value="cancelled" <?= $status_filter == 'cancelled' ? 'selected' : '' ?>>Cancelled</option>
</select>
</div>
<div class="col-md-3 d-flex gap-2">
<button type="submit" class="btn btn-primary px-4 w-100 rounded-pill fw-bold">Filter Results</button>
<?php if ($search || $supplier_filter || $status_filter): ?>
<a href="purchases.php" class="btn btn-light text-muted px-3 rounded-circle d-flex align-items-center justify-content-center"><i class="bi bi-x-lg"></i></a>
<?php endif; ?>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4 py-3">Purchase Details</th>
<th>Supplier</th>
<th>Status</th>
<th>Total Amount</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($purchases as $p):
$status_badge = 'bg-secondary';
if ($p['status'] === 'completed') $status_badge = 'bg-success-subtle text-success border border-success';
if ($p['status'] === 'pending') $status_badge = 'bg-warning-subtle text-warning border border-warning';
if ($p['status'] === 'cancelled') $status_badge = 'bg-danger-subtle text-danger border border-danger';
?>
<tr>
<td class="ps-4">
<div class="fw-bold text-dark"><?= date('M d, Y', strtotime($p['purchase_date'])) ?></div>
<div class="small text-muted fw-medium">Ref: #INV-<?= str_pad($p['id'], 5, '0', STR_PAD_LEFT) ?></div>
</td>
<td>
<div class="fw-bold text-dark"><?= htmlspecialchars($p['supplier_name'] ?? 'Direct Purchase') ?></div>
</td>
<td>
<span class="badge <?= $status_badge ?> rounded-pill px-3 py-2 small fw-bold text-uppercase" style="font-size: 0.7rem;"><?= ucfirst($p['status']) ?></span>
</td>
<td>
<div class="fw-bold text-primary fs-5"><?= format_currency($p['total_amount']) ?></div>
</td>
<td class="text-end pe-4">
<div class="d-inline-flex gap-2">
<?php if (has_permission('purchases_edit') || has_permission('purchases_add')): ?>
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-3" onclick="editPurchase(<?= $p['id'] ?>)" title="Edit/View">
<i class="bi bi-pencil-square me-1"></i> Edit
</button>
<?php endif; ?>
<?php if (has_permission('purchases_del')): ?>
<a href="?delete=<?= $p['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('Are you sure you want to delete this purchase record?')" title="Delete">
<i class="bi bi-trash me-1"></i> Delete
</a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($purchases)): ?>
<tr>
<td colspan="5" class="text-center py-5 text-muted">
<div class="mb-2 display-6 opacity-25"><i class="bi bi-cart-x"></i></div>
<h5 class="fw-bold">No purchase records found.</h5>
<p class="small">Try different search terms or start by adding a new purchase.</p>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if (!empty($purchases)): ?>
<div class="p-4 border-top bg-light">
<?php render_pagination_controls($purchases_pagination); ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Purchase Modal -->
<div class="modal fade" id="purchaseModal" tabindex="-1" aria-labelledby="purchaseModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header bg-primary text-white border-0 py-3">
<h5 class="modal-title fw-bold" id="purchaseModalLabel">New Purchase Order</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<form method="POST" id="purchaseForm" class="p-4">
<input type="hidden" name="action" value="save_purchase">
<input type="hidden" name="id" id="purchaseId">
<div class="row g-4">
<div class="col-12">
<div class="card border-0 bg-light mb-4 rounded-3">
<div class="card-body p-4">
<h6 class="fw-bold mb-3">General Information</h6>
<div class="row g-3">
<div class="col-md-4">
<label class="form-label text-muted small fw-bold">SUPPLIER</label>
<select name="supplier_id" id="modal_supplier_id" class="form-select border-0 shadow-sm">
<option value="">Direct Purchase / None</option>
<?php foreach ($suppliers as $s): ?>
<option value="<?= $s['id'] ?>"><?= htmlspecialchars($s['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<label class="form-label text-muted small fw-bold">PURCHASE DATE</label>
<input type="date" name="purchase_date" id="modal_purchase_date" class="form-control border-0 shadow-sm" value="<?= date('Y-m-d') ?>" required>
</div>
<div class="col-md-4">
<label class="form-label text-muted small fw-bold">STATUS</label>
<select name="status" id="modal_status" class="form-select border-0 shadow-sm">
<option value="pending">Pending</option>
<option value="completed">Completed (Updates Stock)</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div class="col-12">
<label class="form-label text-muted small fw-bold">NOTES</label>
<textarea name="notes" id="modal_notes" class="form-control border-0 shadow-sm" rows="2" placeholder="Reference No, delivery details..."></textarea>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm rounded-3">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h6 class="fw-bold mb-0">Products to Purchase</h6>
<div class="position-relative" style="width: 350px;">
<div class="input-group shadow-sm rounded-pill overflow-hidden">
<span class="input-group-text bg-white border-0 text-muted ps-3"><i class="bi bi-search"></i></span>
<input type="text" id="productSearch" class="form-control border-0" placeholder="Search products to add...">
</div>
<div id="searchResults" class="dropdown-menu w-100 shadow-sm mt-1" style="max-height: 300px; overflow-y: auto;"></div>
</div>
</div>
<div class="table-responsive">
<table class="table align-middle" id="itemsTable">
<thead class="table-light">
<tr>
<th style="width: 40%;">PRODUCT</th>
<th style="width: 20%;">QTY</th>
<th style="width: 20%;">COST</th>
<th style="width: 15%;" class="text-end">TOTAL</th>
<th style="width: 50px;"></th>
</tr>
</thead>
<tbody id="itemsBody">
<!-- Items will be added here -->
</tbody>
<tfoot id="tableFooter" class="d-none">
<tr>
<td colspan="3" class="text-end fw-bold pt-4">Grand Total:</td>
<td class="text-end fw-bold pt-4 text-primary fs-5" id="grandTotal"><?= format_currency(0) ?></td>
<td></td>
</tr>
</tfoot>
</table>
<div id="noItemsMessage" class="text-center py-5 text-muted">
<div class="mb-2 display-6 opacity-25"><i class="bi bi-cart-plus"></i></div>
<div>Use the search bar above to add products to this purchase.</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="purchaseForm" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm">
<i class="bi bi-check-lg me-1"></i> Save Purchase
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const products = <?= $products_json ?>;
const productSearch = document.getElementById('productSearch');
const searchResults = document.getElementById('searchResults');
const itemsBody = document.getElementById('itemsBody');
const grandTotalElement = document.getElementById('grandTotal');
const noItemsMessage = document.getElementById('noItemsMessage');
const tableFooter = document.getElementById('tableFooter');
function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);
}
function calculateTotals() {
let grandTotal = 0;
const rows = document.querySelectorAll('.item-row');
rows.forEach(row => {
const qty = parseFloat(row.querySelector('.qty-input').value) || 0;
const cost = parseFloat(row.querySelector('.cost-input').value) || 0;
const total = qty * cost;
row.querySelector('.row-total').textContent = formatCurrency(total);
grandTotal += total;
});
grandTotalElement.textContent = formatCurrency(grandTotal);
if (rows.length > 0) {
noItemsMessage.classList.add('d-none');
tableFooter.classList.remove('d-none');
} else {
noItemsMessage.classList.remove('d-none');
tableFooter.classList.add('d-none');
}
}
productSearch.addEventListener('input', function() {
const query = this.value.toLowerCase().trim();
searchResults.innerHTML = '';
if (query.length < 1) {
searchResults.classList.remove('show');
return;
}
const filtered = products.filter(p => p.name.toLowerCase().includes(query));
if (filtered.length > 0) {
filtered.forEach(p => {
const item = document.createElement('a');
item.className = 'dropdown-item py-2 border-bottom';
item.href = '#';
item.innerHTML = `
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="fw-bold">${p.name}</div>
<small class="text-muted">Cost: ${formatCurrency(p.cost_price)}</small>
</div>
<i class="bi bi-plus-circle text-primary"></i>
</div>
`;
item.addEventListener('click', function(e) {
e.preventDefault();
addProductToTable(p);
productSearch.value = '';
searchResults.classList.remove('show');
});
searchResults.appendChild(item);
});
searchResults.classList.add('show');
} else {
const item = document.createElement('div');
item.className = 'dropdown-item disabled text-center py-3';
item.textContent = 'No products found';
searchResults.appendChild(item);
searchResults.classList.add('show');
}
});
document.addEventListener('click', function(e) {
if (!productSearch.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.classList.remove('show');
}
});
function addProductToTable(product, quantity = 1, costPrice = null) {
const existingRow = Array.from(document.querySelectorAll('.item-row')).find(row => row.dataset.productId == product.id);
if (existingRow && costPrice === null) {
const qtyInput = existingRow.querySelector('.qty-input');
qtyInput.value = parseInt(qtyInput.value) + 1;
calculateTotals();
return;
}
const cost = costPrice !== null ? costPrice : product.cost_price;
const row = document.createElement('tr');
row.className = 'item-row';
row.dataset.productId = product.id;
row.innerHTML = `
<td>
<div class="fw-bold">${product.name}</div>
<input type="hidden" name="product_id[]" value="${product.id}">
</td>
<td><input type="number" name="quantity[]" class="form-control qty-input border-0 bg-light" min="1" value="${quantity}" required></td>
<td><input type="number" step="0.01" name="cost_price[]" class="form-control cost-input border-0 bg-light" value="${cost}" required></td>
<td class="text-end fw-bold row-total">${formatCurrency(quantity * cost)}</td>
<td class="text-end"><button type="button" class="btn btn-link text-danger remove-item p-0 shadow-none"><i class="bi bi-trash"></i></button></td>
`;
itemsBody.appendChild(row);
calculateTotals();
}
itemsBody.addEventListener('input', function(e) {
if (e.target.classList.contains('qty-input') || e.target.classList.contains('cost-input')) {
calculateTotals();
}
});
itemsBody.addEventListener('click', function(e) {
if (e.target.closest('.remove-item')) {
e.target.closest('.item-row').remove();
calculateTotals();
}
});
window.prepareAddPurchase = function() {
document.getElementById('purchaseModalLabel').innerText = 'New Purchase Order';
document.getElementById('purchaseId').value = '';
document.getElementById('purchaseForm').reset();
document.getElementById('modal_purchase_date').value = new Date().toISOString().split('T')[0];
itemsBody.innerHTML = '';
calculateTotals();
};
window.editPurchase = function(id) {
document.getElementById('purchaseModalLabel').innerText = 'Edit Purchase #' + id;
document.getElementById('purchaseId').value = id;
itemsBody.innerHTML = '';
const modal = new bootstrap.Modal(document.getElementById('purchaseModal'));
modal.show();
fetch('../api/purchase_details.php?id=' + id)
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('modal_supplier_id').value = data.purchase.supplier_id || '';
document.getElementById('modal_purchase_date').value = data.purchase.purchase_date;
document.getElementById('modal_status').value = data.purchase.status;
document.getElementById('modal_notes').value = data.purchase.notes || '';
data.items.forEach(item => {
addProductToTable({
id: item.product_id,
name: item.product_name,
cost_price: item.cost_price
}, item.quantity, item.cost_price);
});
} else {
alert('Error: ' + data.error);
modal.hide();
}
});
};
});
</script>
<style>
.dropdown-menu.show {
display: block;
}
.dropdown-item:hover {
background-color: #f8f9fa;
}
.modal-xl {
max-width: 1140px;
}
</style>
<?php include 'includes/footer.php'; ?>

3
admin/rating.php Normal file
View File

@ -0,0 +1,3 @@
<?php
header('Location: ratings.php');
exit;

396
admin/ratings.php Normal file
View File

@ -0,0 +1,396 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../db/config.php";
require_permission("ratings_view");
$pdo = db();
$message = '';
$tab = $_GET['tab'] ?? 'staff';
// Handle Add Rating (Manual)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_rating') {
$rating = (int)$_POST['rating'];
$comment = trim($_POST['comment']);
$order_id = !empty($_POST['order_id']) ? (int)$_POST['order_id'] : null;
if ($tab === 'service') {
try {
$stmt = $pdo->prepare("INSERT INTO service_ratings (rating, comment) VALUES (?, ?)");
$stmt->execute([$rating, $comment]);
$message = '<div class="alert alert-success">Service rating added successfully!</div>';
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
} else {
$user_id = (int)$_POST['user_id'];
try {
$stmt = $pdo->prepare("INSERT INTO staff_ratings (user_id, order_id, rating, comment) VALUES (?, ?, ?, ?)");
$stmt->execute([$user_id, $order_id, $rating, $comment]);
$message = '<div class="alert alert-success">Rating added successfully!</div>';
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
// Handle Delete
if (isset($_GET['delete'])) {
if (!has_permission('settings')) { // Use settings permission for deletion
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
try {
$id = $_GET['delete'];
$table = ($tab === 'service') ? 'service_ratings' : 'staff_ratings';
$pdo->prepare("DELETE FROM $table WHERE id = ?")->execute([$id]);
header("Location: ratings.php?tab=" . urlencode($tab) . "&deleted=1");
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Error deleting rating: ' . $e->getMessage() . '</div>';
}
}
}
if (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success">Rating deleted successfully!</div>';
}
$staff = $pdo->query("SELECT id, full_name, username FROM users WHERE is_ratable = 1 ORDER BY full_name ASC")->fetchAll();
// Fetch summaries
if ($tab === 'staff') {
$whereParams = [];
$whereSql = "";
if ($filter_staff) {
$whereSql .= " AND r.user_id = ?";
$whereParams[] = $filter_staff;
}
if ($filter_start) {
$whereSql .= " AND DATE(r.created_at) >= ?";
$whereParams[] = $filter_start;
}
if ($filter_end) {
$whereSql .= " AND DATE(r.created_at) <= ?";
$whereParams[] = $filter_end;
}
$summaryQuery = "
SELECT u.id, u.full_name, u.username, u.profile_pic,
AVG(r.rating) as avg_rating, COUNT(r.id) as total_ratings
FROM users u
JOIN staff_ratings r ON u.id = r.user_id
WHERE 1=1 $whereSql
GROUP BY u.id
ORDER BY avg_rating DESC
";
$stmt = $pdo->prepare($summaryQuery);
$stmt->execute($whereParams);
$summaries = $stmt->fetchAll(PDO::FETCH_ASSOC);
$query = "SELECT r.*, u.full_name as staff_name, u.username as staff_username, u.profile_pic
FROM staff_ratings r
JOIN users u ON r.user_id = u.id
WHERE 1=1 $whereSql
ORDER BY r.created_at DESC";
$ratings_pagination = paginate_query($pdo, $query, $whereParams);
$ratings = $ratings_pagination['data'];
} else {
$whereParams = [];
$whereSql = "";
if ($filter_start) {
$whereSql .= " AND DATE(created_at) >= ?";
$whereParams[] = $filter_start;
}
if ($filter_end) {
$whereSql .= " AND DATE(created_at) <= ?";
$whereParams[] = $filter_end;
}
$serviceSummaryQuery = "SELECT AVG(rating) as avg_rating, COUNT(id) as total_ratings FROM service_ratings WHERE 1=1 $whereSql";
$stmt = $pdo->prepare($serviceSummaryQuery);
$stmt->execute($whereParams);
$serviceSummary = $stmt->fetch(PDO::FETCH_ASSOC);
$query = "SELECT * FROM service_ratings WHERE 1=1 $whereSql ORDER BY created_at DESC";
$ratings_pagination = paginate_query($pdo, $query, $whereParams);
$ratings = $ratings_pagination['data'];
}
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Ratings & Feedback</h2>
<div class="d-flex gap-2">
<a href="../rate.php" target="_blank" class="btn btn-outline-primary">
<i class="bi bi-box-arrow-up-right"></i> Open Public Rating Page
</a>
<button class="btn btn-primary" onclick="openAddModal()">
<i class="bi bi-star"></i> Add Manual Rating
</button>
</div>
</div>
<?= $message ?>
<ul class="nav nav-pills mb-4 bg-white p-2 rounded-4 shadow-sm d-inline-flex">
<li class="nav-item">
<a class="nav-link rounded-pill <?= $tab === 'staff' ? 'active' : '' ?>" href="?tab=staff">
<i class="bi bi-people me-1"></i> Staff Performance
</a>
</li>
<li class="nav-item">
<a class="nav-link rounded-pill <?= $tab === 'service' ? 'active' : '' ?>" href="?tab=service">
<i class="bi bi-shop me-1"></i> Restaurant Service
</a>
</li>
</ul>
<!-- Filters -->
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body">
<form method="GET" class="row g-3 align-items-end">
<input type="hidden" name="tab" value="<?= htmlspecialchars($tab) ?>">
<?php if ($tab === 'staff'): ?>
<div class="col-md-3">
<label class="form-label small fw-bold text-muted">Staff Member</label>
<select name="staff_id" class="form-select">
<option value="">All Staff</option>
<?php foreach ($staff as $s): ?>
<option value="<?= $s['id'] ?>" <?= $filter_staff == $s['id'] ? 'selected' : '' ?> >
<?= htmlspecialchars($s['full_name'] ?: $s['username']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="col-md-3">
<label class="form-label small fw-bold text-muted">Start Date</label>
<input type="date" name="start_date" class="form-control" value="<?= htmlspecialchars($filter_start) ?>">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-muted">End Date</label>
<input type="date" name="end_date" class="form-control" value="<?= htmlspecialchars($filter_end) ?>">
</div>
<div class="col-md-3">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-funnel"></i> Filter
</button>
<?php if ($filter_staff || $filter_start || $filter_end): ?>
<a href="?tab=<?= urlencode($tab) ?>" class="btn btn-outline-secondary px-3" title="Clear Filters">
<i class="bi bi-x-circle"></i>
</a>
<?php endif; ?>
</div>
</div>
</form>
</div>
</div>
<?php if ($tab === 'staff'): ?>
<?php if (empty($summaries)): ?>
<div class="alert alert-info border-0 shadow-sm rounded-4 text-center py-4 mb-5">
<i class="bi bi-info-circle fs-2 mb-2"></i>
<p class="mb-0">No staff members have been rated yet.</p>
</div>
<?php else: ?>
<div class="row g-4 mb-5">
<?php foreach ($summaries as $summary): ?>
<div class="col-md-3">
<div class="card border-0 shadow-sm rounded-4 h-100">
<div class="card-body text-center">
<?php if (!empty($summary['profile_pic'])): ?>
<img src="../<?= htmlspecialchars($summary['profile_pic']) ?>" alt="Staff" class="rounded-circle mb-3 shadow-sm" style="width: 56px; height: 56px; object-fit: cover;">
<?php else: ?>
<div class="rounded-circle bg-light d-inline-flex align-items-center justify-content-center mb-3 shadow-sm" style="width: 56px; height: 56px;">
<i class="bi bi-person fs-3 text-primary"></i>
</div>
<?php endif; ?>
<h5 class="card-title mb-1 small fw-bold text-truncate" title="<?= htmlspecialchars($summary['full_name'] ?: $summary['username']) ?>">
<?= htmlspecialchars($summary['full_name'] ?: $summary['username']) ?>
</h5>
<div class="text-warning mb-2" style="font-size: 0.8rem;">
<?php
$avg = round($summary['avg_rating']);
for ($i = 1; $i <= 5; $i++) {
echo $i <= $avg ? '<i class="bi bi-star-fill"></i>' : '<i class="bi bi-star"></i>';
}
?>
<span class="text-muted ms-1 small">(<?= number_format($summary['avg_rating'], 1) ?>)</span>
</div>
<p class="text-muted small mb-0"><?= $summary['total_ratings'] ?> ratings</p>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="row mb-5">
<div class="col-md-4">
<div class="card border-0 shadow-sm rounded-4 bg-primary text-white">
<div class="card-body text-center py-4">
<i class="bi bi-shop fs-1 mb-3"></i>
<h4 class="fw-bold mb-1">Overall Service</h4>
<div class="fs-2 fw-bold mb-2">
<?php if ($serviceSummary['total_ratings'] > 0): ?>
<?= number_format($serviceSummary['avg_rating'], 1) ?> / 5.0
<?php else: ?>
N/A
<?php endif; ?>
</div>
<div class="text-white-50 small">
Based on <?= $serviceSummary['total_ratings'] ?> reviews
</div>
</div>
</div>
</div>
</div>
<?php endif; ?>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-header bg-white py-3 border-bottom-0">
<h5 class="card-title mb-0 small fw-bold"><?= $tab === 'service' ? 'Service Feedback' : 'Staff Feedback' ?></h5>
</div>
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($ratings_pagination, ['tab' => $tab]); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Date</th>
<?php if ($tab === 'staff'): ?><th>Staff Member</th><?php endif; ?>
<th>Rating</th>
<th>Comment</th>
<?php if ($tab === 'staff'): ?><th>Order ID</th><?php endif; ?>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($ratings as $r): ?>
<tr>
<td class="ps-4">
<div class="fw-bold"><?= date('M d, Y', strtotime($r['created_at'])) ?></div>
<small class="text-muted"><?= date('H:i', strtotime($r['created_at'])) ?></small>
</td>
<?php if ($tab === 'staff'): ?>
<td>
<div class="d-flex align-items-center">
<?php if (!empty($r['profile_pic'])): ?>
<img src="../<?= htmlspecialchars($r['profile_pic']) ?>" class="rounded-circle me-2 shadow-sm" style="width: 24px; height: 24px; object-fit: cover;">
<?php else: ?>
<div class="rounded-circle bg-light d-flex align-items-center justify-content-center me-2 shadow-sm" style="width: 24px; height: 24px;">
<i class="bi bi-person text-primary" style="font-size: 0.7rem;"></i>
</div>
<?php endif; ?>
<span class="fw-medium text-dark"><?= htmlspecialchars($r['staff_name'] ?: $r['staff_username']) ?></span>
</div>
</td>
<?php endif; ?>
<td>
<div class="text-warning">
<?php for($i=1; $i<=5; $i++): ?>
<i class="bi bi-star<?= $i <= $r['rating'] ? '-fill' : '' ?>"></i>
<?php endfor; ?>
</div>
</td>
<td><small><?= htmlspecialchars($r['comment'] ?: '-') ?></small></td>
<?php if ($tab === 'staff'): ?>
<td><?= !empty($r['order_id']) ? '#' . $r['order_id'] : '-' ?></td>
<?php endif; ?>
<td class="text-end pe-4">
<?php if (has_permission('settings')): ?>
<a href="?tab=<?= urlencode($tab) ?>&delete=<?= $r['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this rating?')"><i class="bi bi-trash"></i></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($ratings)): ?>
<tr>
<td colspan="<?= $tab === 'staff' ? 6 : 4 ?>" class="text-center py-4 text-muted">No ratings found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($ratings_pagination, ['tab' => $tab]); ?>
</div>
</div>
</div>
<!-- Rating Modal -->
<div class="modal fade" id="ratingModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">Add Manual Rating</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="ratingForm">
<div class="modal-body">
<input type="hidden" name="action" value="add_rating">
<?php if ($tab === 'staff'): ?>
<div class="mb-3">
<label class="form-label">Staff Member <span class="text-danger">*</span></label>
<select name="user_id" class="form-select" required>
<option value="">Select Staff</option>
<?php foreach ($staff as $s): ?>
<option value="<?= $s['id'] ?>"><?= htmlspecialchars($s['full_name'] ?: $s['username']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Order ID (Optional)</label>
<input type="number" name="order_id" class="form-control" placeholder="e.g. 1024">
</div>
<?php endif; ?>
<div class="mb-3">
<label class="form-label">Rating <span class="text-danger">*</span></label>
<select name="rating" class="form-select" required>
<option value="5">5 Stars - Excellent</option>
<option value="4">4 Stars - Very Good</option>
<option value="3" selected>3 Stars - Good</option>
<option value="2">2 Stars - Fair</option>
<option value="1">1 Star - Poor</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Comment</label>
<textarea name="comment" class="form-control" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Rating</button>
</div>
</form>
</div>
</div>
</div>
<script>
function getRatingModal() {
if (typeof bootstrap === 'undefined') return null;
const el = document.getElementById('ratingModal');
return el ? bootstrap.Modal.getOrCreateInstance(el) : null;
}
function openAddModal() {
const modal = getRatingModal();
if (!modal) return;
document.getElementById('ratingForm').reset();
modal.show();
}
</script>
<?php include 'includes/footer.php'; ?>

344
admin/report_cashiers.php Normal file
View File

@ -0,0 +1,344 @@
<?php
require_once __DIR__ . '/includes/header.php';
// Check permission
if (function_exists('require_permission')) {
require_permission('reports_view');
}
$pdo = db();
// Filters
$startDate = $_GET['start_date'] ?? date('Y-m-d');
$endDate = $_GET['end_date'] ?? date('Y-m-d');
$outletId = $_GET['outlet_id'] ?? '';
$cashierId = $_GET['cashier_id'] ?? '';
// Fetch Outlets
$outletsStmt = $pdo->query("SELECT id, name, name_ar FROM outlets WHERE is_deleted = 0 ORDER BY name ASC");
$allOutlets = $outletsStmt->fetchAll();
// Fetch Cashiers (Users)
$cashiersStmt = $pdo->query("SELECT id, full_name, full_name_ar FROM users WHERE is_deleted = 0 AND is_active = 1 ORDER BY full_name ASC");
$allCashiers = $cashiersStmt->fetchAll();
// Fetch all payment types
$allPaymentTypesStmt = $pdo->query("SELECT name FROM payment_types WHERE is_deleted = 0 ORDER BY id ASC");
$allPaymentTypes = $allPaymentTypesStmt->fetchAll(PDO::FETCH_COLUMN);
// Define a set of Bootstrap colors to cycle through for payment types
$paymentColors = [
'text-success',
'text-info',
'text-warning',
'text-danger',
'text-secondary',
'text-dark',
'text-primary'
];
$paymentBadgeColors = [
'bg-success',
'bg-info text-dark',
'bg-warning text-dark',
'bg-danger',
'bg-secondary',
'bg-dark',
'bg-primary'
];
// Base query additions
$conditions = ["DATE(o.created_at) BETWEEN ? AND ? AND o.status != 'cancelled'"];
$queryParams = [$startDate, $endDate];
if (!empty($outletId)) {
$conditions[] = "o.outlet_id = ?";
$queryParams[] = $outletId;
}
if (!empty($cashierId)) {
$conditions[] = "o.user_id = ?";
$queryParams[] = $cashierId;
}
$whereClause = implode(' AND ', $conditions);
// Fetch total amounts grouped by cashier, outlet and payment type
$salesStmt = $pdo->prepare("
SELECT
u.id as cashier_id,
u.full_name as cashier_name,
u.full_name_ar as cashier_name_ar,
outl.id as outlet_id,
outl.name as outlet_name,
outl.name_ar as outlet_name_ar,
pt.name as payment_name,
SUM(o.total_amount) as total_amount,
COUNT(o.id) as orders_count
FROM orders o
JOIN users u ON o.user_id = u.id
LEFT JOIN outlets outl ON o.outlet_id = outl.id
LEFT JOIN payment_types pt ON o.payment_type_id = pt.id
WHERE $whereClause
GROUP BY o.user_id, o.outlet_id, o.payment_type_id
ORDER BY u.full_name ASC, outl.name ASC
");
$salesStmt->execute($queryParams);
$salesData = $salesStmt->fetchAll();
// Group by Cashier + Outlet
$cashierSales = [];
foreach ($salesData as $row) {
$key = $row['cashier_id'] . '_' . $row['outlet_id'];
if (!isset($cashierSales[$key])) {
$cashierSales[$key] = [
'name' => $row['cashier_name'],
'name_ar' => $row['cashier_name_ar'],
'outlet_name' => $row['outlet_name'] ?? 'Unknown Outlet',
'outlet_name_ar' => $row['outlet_name_ar'] ?? '',
'payments' => [],
'total' => 0,
'orders_count' => 0
];
}
$pName = $row['payment_name'] ?? 'Unknown';
$amount = (float)$row['total_amount'];
$count = (int)$row['orders_count'];
$cashierSales[$key]['payments'][$pName] = $amount;
$cashierSales[$key]['total'] += $amount;
$cashierSales[$key]['orders_count'] += $count;
}
?>
<style>
@media print {
body {
background-color: #fff !important;
font-family: Arial, sans-serif !important;
}
.d-print-none,
.navbar,
header,
.main-content > header,
.sidebar,
aside,
nav {
display: none !important;
}
.main-content {
margin: 0 !important;
padding: 0 !important;
}
.container-fluid {
padding: 0 !important;
width: 100% !important;
max-width: 100% !important;
}
.card {
border: none !important;
box-shadow: none !important;
}
.card-header {
background: transparent !important;
border-bottom: 2px solid #000 !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin-bottom: 20px;
}
.table-responsive {
overflow: visible !important;
}
.table {
width: 100% !important;
border-collapse: collapse !important;
}
.table th,
.table td {
border: 1px solid #ddd !important;
padding: 8px !important;
}
.table thead th {
background-color: #f8f9fa !important;
color: #000 !important;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.badge {
border: 1px solid #ccc !important;
color: #000 !important;
background-color: transparent !important;
border-radius: 4px !important;
}
h2, h5 {
margin: 0 !important;
padding: 0 !important;
}
.print-header {
display: block !important;
text-align: center;
margin-bottom: 30px;
}
.print-header h2 {
font-size: 24px;
font-weight: bold;
}
.print-header p {
font-size: 14px;
color: #555;
margin-top: 5px;
}
@page {
margin: 1cm;
size: landscape;
}
}
.print-header { display: none; }
</style>
<div class="container-fluid p-0">
<div class="print-header">
<h2>Cashier Sales Report</h2>
<p>Date Range: <?= htmlspecialchars($startDate) ?> to <?= htmlspecialchars($endDate) ?></p>
</div>
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-3 d-print-none">
<div>
<h2 class="fw-bold mb-1">Cashier Sales Report</h2>
<p class="text-muted mb-0">Sales grouped by cashier and payment methods</p>
</div>
<form class="row g-2 align-items-center" method="GET">
<div class="col-auto">
<select name="cashier_id" class="form-select" style="min-width: 150px;">
<option value="">All Cashiers</option>
<?php foreach ($allCashiers as $c): ?>
<option value="<?= $c['id'] ?>" <?= $cashierId == $c['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($c['full_name']) ?> <?= $c['full_name_ar'] ? '('.$c['full_name_ar'].')' : '' ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-auto">
<select name="outlet_id" class="form-select" style="min-width: 150px;">
<option value="">All Outlets</option>
<?php foreach ($allOutlets as $o): ?>
<option value="<?= $o['id'] ?>" <?= $outletId == $o['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($o['name']) ?> <?= $o['name_ar'] ? '('.$o['name_ar'].')' : '' ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-auto">
<div class="input-group">
<span class="input-group-text bg-white border-end-0 text-muted"><i class="bi bi-calendar"></i></span>
<input type="date" name="start_date" class="form-control border-start-0" value="<?= htmlspecialchars($startDate) ?>">
</div>
</div>
<div class="col-auto">
<div class="input-group">
<span class="input-group-text bg-white border-end-0 text-muted"><i class="bi bi-calendar"></i></span>
<input type="date" name="end_date" class="form-control border-start-0" value="<?= htmlspecialchars($endDate) ?>">
</div>
</div>
<div class="col-auto">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary px-4 text-nowrap">Filter</button>
<a href="report_cashiers.php" class="btn btn-light text-nowrap">Reset</a>
</div>
</div>
</form>
</div>
<div class="row g-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center">
<h5 class="fw-bold mb-0"><i class="bi bi-wallet2 me-2 text-primary d-print-none"></i> <span class="d-print-none">Cashier Breakdown</span></h5>
<button onclick="window.print()" class="btn btn-sm btn-outline-secondary d-print-none"><i class="bi bi-printer me-1"></i> Print</button>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-3">Cashier Name</th>
<th>Outlet</th>
<?php foreach ($allPaymentTypes as $index => $ptName): ?>
<th class="text-end">
<span class="badge <?= $paymentBadgeColors[$index % count($paymentBadgeColors)] ?> rounded-pill px-3 py-2">
<?= htmlspecialchars($ptName) ?>
</span>
</th>
<?php endforeach; ?>
<th class="text-end">Orders</th>
<th class="text-end pe-3">Total Sales</th>
</tr>
</thead>
<tbody>
<?php if (empty($cashierSales)): ?>
<tr><td colspan="<?= count($allPaymentTypes) + 4 ?>" class="text-center py-5 text-muted">No sales found for the selected criteria</td></tr>
<?php else: ?>
<?php foreach ($cashierSales as $key => $data): ?>
<tr>
<td class="ps-3 fw-bold">
<?= htmlspecialchars($data['name']) ?>
<?php if ($data['name_ar']): ?>
<small class="text-muted d-block fw-normal"><?= htmlspecialchars($data['name_ar']) ?></small>
<?php endif; ?>
</td>
<td>
<?= htmlspecialchars($data['outlet_name']) ?>
<?php if ($data['outlet_name_ar']): ?>
<small class="text-muted d-block fw-normal"><?= htmlspecialchars($data['outlet_name_ar']) ?></small>
<?php endif; ?>
</td>
<?php foreach ($allPaymentTypes as $index => $ptName): ?>
<td class="text-end fw-semibold <?= $paymentColors[$index % count($paymentColors)] ?>">
<?= format_currency($data['payments'][$ptName] ?? 0) ?>
</td>
<?php endforeach; ?>
<td class="text-end fw-bold text-secondary"><?= $data['orders_count'] ?></td>
<td class="text-end pe-3 fw-bold text-primary"><?= format_currency($data['total']) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
<tfoot class="bg-light fw-bold">
<?php if (!empty($cashierSales)):
$totals = [];
$grandTotal = 0;
$grandTotalOrders = 0;
foreach ($allPaymentTypes as $pt) {
$totals[$pt] = 0;
}
foreach ($cashierSales as $data) {
foreach ($allPaymentTypes as $pt) {
$totals[$pt] += $data['payments'][$pt] ?? 0;
}
$grandTotal += $data['total'];
$grandTotalOrders += $data['orders_count'];
}
?>
<tr>
<td class="ps-3">Totals</td>
<td></td>
<?php foreach ($allPaymentTypes as $index => $pt): ?>
<td class="text-end <?= $paymentColors[$index % count($paymentColors)] ?>">
<?= format_currency($totals[$pt]) ?>
</td>
<?php endforeach; ?>
<td class="text-end fw-bold text-secondary fs-6"><?= $grandTotalOrders ?></td>
<td class="text-end pe-3 text-primary fs-6"><?= format_currency($grandTotal) ?></td>
</tr>
<?php endif; ?>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

148
admin/report_products.php Normal file
View File

@ -0,0 +1,148 @@
<?php
require_once __DIR__ . '/includes/header.php';
// Check permission
if (function_exists('require_permission')) {
require_permission('reports_view');
}
$pdo = db();
// Date and Outlet Filter
$startDate = $_GET['start_date'] ?? date('Y-m-d', strtotime('-7 days'));
$endDate = $_GET['end_date'] ?? date('Y-m-d');
$outletId = $_GET['outlet_id'] ?? '';
// Fetch Outlets for filter
$outletsStmt = $pdo->query("SELECT id, name, name_ar FROM outlets WHERE is_deleted = 0 ORDER BY name ASC");
$allOutlets = $outletsStmt->fetchAll();
// Base query additions
$outletCondition = "";
$queryParams = [$startDate, $endDate];
if (!empty($outletId)) {
$outletCondition = " AND o.outlet_id = ? ";
$queryParams[] = $outletId;
}
// Product Sales & Profit
$productSalesStmt = $pdo->prepare("
SELECT
p.name as product_name,
p.name_ar as product_name_ar,
SUM(oi.quantity) as qty_sold,
SUM(oi.quantity * oi.unit_price) as total_amount,
SUM(oi.quantity * (oi.unit_price - IFNULL(p.cost_price, 0))) as total_profit
FROM order_items oi
JOIN orders o ON oi.order_id = o.id
JOIN products p ON oi.product_id = p.id
WHERE DATE(o.created_at) BETWEEN ? AND ?
AND o.status != 'cancelled'
$outletCondition
GROUP BY p.id
ORDER BY qty_sold DESC
");
$productSalesStmt->execute($queryParams);
$productSales = $productSalesStmt->fetchAll();
// Totals for the footer
$totalQty = 0;
$totalAmount = 0;
$totalProfit = 0;
foreach ($productSales as $p) {
$totalQty += $p['qty_sold'];
$totalAmount += $p['total_amount'];
$totalProfit += $p['total_profit'];
}
?>
<div class="container-fluid p-0">
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-3">
<div>
<h2 class="fw-bold mb-1">Product Sales Report</h2>
<p class="text-muted mb-0">Detailed breakdown of product performance and profitability</p>
</div>
<form class="row g-2 align-items-center" method="GET">
<div class="col-auto">
<select name="outlet_id" class="form-select" style="min-width: 180px;">
<option value="">All Outlets</option>
<?php foreach ($allOutlets as $o): ?>
<option value="<?= $o['id'] ?>" <?= $outletId == $o['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($o['name']) ?> <?= $o['name_ar'] ? '('.$o['name_ar'].')' : '' ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-auto">
<div class="input-group">
<span class="input-group-text bg-white border-end-0 text-muted"><i class="bi bi-calendar"></i></span>
<input type="date" name="start_date" class="form-control border-start-0" value="<?= htmlspecialchars($startDate) ?>">
</div>
</div>
<div class="col-auto">
<div class="input-group">
<span class="input-group-text bg-white border-end-0 text-muted"><i class="bi bi-calendar"></i></span>
<input type="date" name="end_date" class="form-control border-start-0" value="<?= htmlspecialchars($endDate) ?>">
</div>
</div>
<div class="col-auto">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary px-4 text-nowrap">Filter</button>
<a href="report_products.php" class="btn btn-light text-nowrap">Reset</a>
</div>
</div>
</form>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center">
<h5 class="fw-bold mb-0"><i class="bi bi-box-seam me-2 text-dark"></i> Products</h5>
<button onclick="window.print()" class="btn btn-sm btn-outline-secondary"><i class="bi bi-printer me-1"></i> Print</button>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-3">Product Name</th>
<th class="text-center">Qty Sold</th>
<th class="text-end">Total Amount</th>
<th class="text-end pe-3">Estimated Profit</th>
</tr>
</thead>
<tbody>
<?php if (empty($productSales)): ?>
<tr><td colspan="4" class="text-center py-5 text-muted">No sales found for the selected criteria</td></tr>
<?php else: ?>
<?php foreach ($productSales as $prod): ?>
<tr>
<td class="ps-3">
<div class="fw-bold text-dark"><?= htmlspecialchars($prod['product_name']) ?></div>
<?php if ($prod['product_name_ar']): ?>
<small class="text-muted d-block"><?= htmlspecialchars($prod['product_name_ar']) ?></small>
<?php endif; ?>
</td>
<td class="text-center"><?= number_format($prod['qty_sold']) ?></td>
<td class="text-end fw-bold"><?= format_currency($prod['total_amount']) ?></td>
<td class="text-end pe-3 fw-bold text-success"><?= format_currency($prod['total_profit']) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
<?php if (!empty($productSales)): ?>
<tfoot class="bg-light fw-bold">
<tr>
<td class="ps-3 text-uppercase">Total</td>
<td class="text-center"><?= number_format($totalQty) ?></td>
<td class="text-end"><?= format_currency($totalAmount) ?></td>
<td class="text-end pe-3 text-success"><?= format_currency($totalProfit) ?></td>
</tr>
</tfoot>
<?php endif; ?>
</table>
</div>
</div>
</div>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

172
admin/report_staff.php Normal file
View File

@ -0,0 +1,172 @@
<?php
require_once __DIR__ . '/includes/header.php';
// Check permission
if (function_exists('require_permission')) {
require_permission('reports_view');
}
$pdo = db();
// Date and Outlet Filter
$startDate = $_GET['start_date'] ?? date('Y-m-d', strtotime('-7 days'));
$endDate = $_GET['end_date'] ?? date('Y-m-d');
$outletId = $_GET['outlet_id'] ?? '';
// Fetch Outlets for filter
$outletsStmt = $pdo->query("SELECT id, name, name_ar FROM outlets WHERE is_deleted = 0 ORDER BY name ASC");
$allOutlets = $outletsStmt->fetchAll();
// Base query additions
$outletCondition = "";
$queryParams = [$startDate, $endDate];
if (!empty($outletId)) {
$outletCondition = " AND o.outlet_id = ? ";
$queryParams[] = $outletId;
}
// Staff Sales by Payment Type
$staffPaymentStmt = $pdo->prepare("
SELECT
u.full_name as staff_name,
u.full_name_ar as staff_name_ar,
pt.name as payment_name,
SUM(o.total_amount) as total_amount
FROM orders o
JOIN users u ON o.user_id = u.id
LEFT JOIN payment_types pt ON o.payment_type_id = pt.id
WHERE DATE(o.created_at) BETWEEN ? AND ?
AND o.status != 'cancelled'
$outletCondition
GROUP BY o.user_id, o.payment_type_id
ORDER BY u.full_name ASC, total_amount DESC
");
$staffPaymentStmt->execute($queryParams);
$staffPaymentSales = $staffPaymentStmt->fetchAll();
// Summary by payment type
$paymentSummary = [];
$grandTotal = 0;
foreach ($staffPaymentSales as $row) {
$pName = $row['payment_name'] ?? 'N/A';
if (!isset($paymentSummary[$pName])) {
$paymentSummary[$pName] = 0;
}
$paymentSummary[$pName] += $row['total_amount'];
$grandTotal += $row['total_amount'];
}
?>
<div class="container-fluid p-0">
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-3">
<div>
<h2 class="fw-bold mb-1">Staff Sales Report</h2>
<p class="text-muted mb-0">Sales breakdown by staff and payment method</p>
</div>
<form class="row g-2 align-items-center" method="GET">
<div class="col-auto">
<select name="outlet_id" class="form-select" style="min-width: 180px;">
<option value="">All Outlets</option>
<?php foreach ($allOutlets as $o): ?>
<option value="<?= $o['id'] ?>" <?= $outletId == $o['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($o['name']) ?> <?= $o['name_ar'] ? '('.$o['name_ar'].')' : '' ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-auto">
<div class="input-group">
<span class="input-group-text bg-white border-end-0 text-muted"><i class="bi bi-calendar"></i></span>
<input type="date" name="start_date" class="form-control border-start-0" value="<?= htmlspecialchars($startDate) ?>">
</div>
</div>
<div class="col-auto">
<div class="input-group">
<span class="input-group-text bg-white border-end-0 text-muted"><i class="bi bi-calendar"></i></span>
<input type="date" name="end_date" class="form-control border-start-0" value="<?= htmlspecialchars($endDate) ?>">
</div>
</div>
<div class="col-auto">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary px-4 text-nowrap">Filter</button>
<a href="report_staff.php" class="btn btn-light text-nowrap">Reset</a>
</div>
</div>
</form>
</div>
<div class="row g-4">
<div class="col-lg-8">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center">
<h5 class="fw-bold mb-0"><i class="bi bi-person-badge me-2 text-primary"></i> Detailed Staff Sales</h5>
<button onclick="window.print()" class="btn btn-sm btn-outline-secondary"><i class="bi bi-printer me-1"></i> Print</button>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-3">Staff Name</th>
<th>Payment Type</th>
<th class="text-end pe-3">Total Amount</th>
</tr>
</thead>
<tbody>
<?php if (empty($staffPaymentSales)): ?>
<tr><td colspan="3" class="text-center py-5 text-muted">No sales found for the selected criteria</td></tr>
<?php else: ?>
<?php
$currentStaff = '';
foreach ($staffPaymentSales as $row):
$showStaff = ($currentStaff != $row['staff_name']);
$currentStaff = $row['staff_name'];
?>
<tr class="<?= $showStaff ? 'border-top' : '' ?>">
<td class="ps-3">
<?php if ($showStaff): ?>
<div class="fw-bold text-dark"><?= htmlspecialchars($row['staff_name']) ?></div>
<?php if ($row['staff_name_ar']): ?>
<small class="text-muted"><?= htmlspecialchars($row['staff_name_ar']) ?></small>
<?php endif; ?>
<?php endif; ?>
</td>
<td>
<span class="badge bg-light text-dark border"><?= htmlspecialchars($row['payment_name'] ?? 'N/A') ?></span>
</td>
<td class="text-end pe-3 fw-bold"><?= format_currency($row['total_amount']) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white py-3 border-0">
<h5 class="fw-bold mb-0"><i class="bi bi-pie-chart me-2 text-success"></i> Payment Summary</h5>
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
<?php foreach ($paymentSummary as $pName => $amount): ?>
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
<span><?= htmlspecialchars($pName) ?></span>
<span class="fw-bold"><?= format_currency($amount) ?></span>
</li>
<?php endforeach; ?>
<li class="list-group-item d-flex justify-content-between align-items-center px-0 bg-light mt-2 p-2 rounded">
<span class="fw-bold">Grand Total</span>
<span class="fw-bold text-primary fs-5"><?= format_currency($grandTotal) ?></span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>

310
admin/reports.php Normal file
View File

@ -0,0 +1,310 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/lang.php';
require_login();
require_permission('reports_view');
$title = t('reports_analytics');
include __DIR__ . '/includes/header.php';
$pdo = db();
$date_from = $_GET['date_from'] ?? date('Y-m-d', strtotime('-30 days'));
$date_to = $_GET['date_to'] ?? date('Y-m-d');
$outlet_id = $_GET['outlet_id'] ?? 'all';
$params = [':from' => $date_from . ' 00:00:00', ':to' => $date_to . ' 23:59:59'];
$outlet_query = "";
if ($outlet_id !== 'all') {
$outlet_query = " AND o.outlet_id = :outlet_id";
$params[':outlet_id'] = $outlet_id;
}
try {
// Outlets for filter
$outlets = $pdo->query("SELECT id, name FROM outlets WHERE is_deleted = 0")->fetchAll();
// Daily Sales
$daily_sales = $pdo->prepare("SELECT DATE(o.created_at) as date, SUM(o.total_amount) as total
FROM orders o
WHERE o.created_at BETWEEN :from AND :to $outlet_query
AND o.status != 'cancelled'
GROUP BY DATE(o.created_at)
ORDER BY date ASC");
$daily_sales->execute($params);
$daily_sales_data = $daily_sales->fetchAll();
// Staff Sales
$staff_sales = $pdo->prepare("SELECT u.username, SUM(o.total_amount) as total
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.created_at BETWEEN :from AND :to $outlet_query
AND o.status != 'cancelled'
GROUP BY u.id
ORDER BY total DESC");
$staff_sales->execute($params);
$staff_sales_data = $staff_sales->fetchAll();
// Outlet Sales
$outlet_sales = $pdo->prepare("SELECT ou.name, SUM(o.total_amount) as total
FROM orders o
JOIN outlets ou ON o.outlet_id = ou.id
WHERE o.created_at BETWEEN :from AND :to $outlet_query
AND o.status != 'cancelled'
GROUP BY ou.id
ORDER BY total DESC");
$outlet_sales->execute($params);
$outlet_sales_data = $outlet_sales->fetchAll();
// Category Sales
$cat_sales = $pdo->prepare("SELECT c.name, SUM(oi.quantity * oi.unit_price) as total
FROM order_items oi
JOIN orders o ON oi.order_id = o.id
JOIN products p ON oi.product_id = p.id
JOIN categories c ON p.category_id = c.id
WHERE o.created_at BETWEEN :from AND :to $outlet_query
AND o.status != 'cancelled'
GROUP BY c.id
ORDER BY total DESC");
$cat_sales->execute($params);
$cat_sales_data = $cat_sales->fetchAll();
// Expenses
$expenses = $pdo->prepare("SELECT ec.name, SUM(e.amount) as total
FROM expenses e
JOIN expense_categories ec ON e.category_id = ec.id
WHERE e.expense_date BETWEEN :from AND :to
AND (:outlet_id_all = 'all' OR e.outlet_id = :outlet_id_exp)
GROUP BY ec.id");
$exp_params = [':from' => $date_from, ':to' => $date_to, ':outlet_id_all' => $outlet_id, ':outlet_id_exp' => $outlet_id];
$expenses->execute($exp_params);
$expense_data = $expenses->fetchAll();
// Summary
$total_sales = array_sum(array_column($daily_sales_data, 'total'));
$total_expenses = array_sum(array_column($expense_data, 'total'));
$net_profit = $total_sales - $total_expenses;
} catch (PDOException $e) {
echo '<div class="alert alert-danger">Database error: ' . htmlspecialchars($e->getMessage()) . '</div>';
include __DIR__ . '/includes/footer.php';
exit;
}
?>
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-3">
<h1 class="h3 mb-0 text-gray-800"><?php echo t('daily_reports'); ?></h1>
<form class="row g-2 align-items-center">
<div class="col-auto">
<input type="date" name="date_from" class="form-control form-control-sm" value="<?php echo $date_from; ?>">
</div>
<div class="col-auto">
<input type="date" name="date_to" class="form-control form-control-sm" value="<?php echo $date_to; ?>">
</div>
<div class="col-auto">
<select name="outlet_id" class="form-select form-select-sm">
<option value="all"><?php echo t('all'); ?></option>
<?php foreach ($outlets as $o): ?>
<option value="<?php echo $o['id']; ?>" <?php echo $outlet_id == $o['id'] ? 'selected' : ''; ?>>
<?php echo $o['name']; ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary btn-sm"><?php echo t('filter'); ?></button>
</div>
</form>
</div>
<div class="row">
<div class="col-md-4 mb-4">
<div class="card border-0 shadow-sm h-100 py-2" style="border-left: 4px solid #4e73df !important;">
<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"><?php echo t('total'); ?> <?php echo t('orders'); ?></div>
<div class="h5 mb-0 font-weight-bold text-gray-800"><?php echo number_format($total_sales, 2); ?></div>
</div>
<div class="col-auto">
<i class="bi bi-cart-check fs-2 text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card border-0 shadow-sm h-100 py-2" style="border-left: 4px solid #e74a3b !important;">
<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-danger text-uppercase mb-1"><?php echo t('expenses'); ?></div>
<div class="h5 mb-0 font-weight-bold text-gray-800"><?php echo number_format($total_expenses, 2); ?></div>
</div>
<div class="col-auto">
<i class="bi bi-wallet2 fs-2 text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card border-0 shadow-sm h-100 py-2" style="border-left: 4px solid #1cc88a !important;">
<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"><?php echo t('total'); ?></div>
<div class="h5 mb-0 font-weight-bold text-gray-800"><?php echo number_format($net_profit, 2); ?></div>
</div>
<div class="col-auto">
<i class="bi bi-cash-stack fs-2 text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xl-8 col-lg-7">
<div class="card shadow-sm mb-4">
<div class="card-header py-3 bg-white border-bottom-0">
<h6 class="m-0 font-weight-bold text-primary"><?php echo t('daily_reports'); ?></h6>
</div>
<div class="card-body">
<div class="chart-area" style="height: 320px;">
<canvas id="dailySalesChart"></canvas>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-lg-5">
<div class="card shadow-sm mb-4">
<div class="card-header py-3 bg-white border-bottom-0">
<h6 class="m-0 font-weight-bold text-primary"><?php echo t('categories'); ?></h6>
</div>
<div class="card-body">
<div class="chart-pie pt-4 pb-2" style="height: 320px;">
<canvas id="categorySalesChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6 mb-4">
<div class="card shadow-sm mb-4">
<div class="card-header py-3 bg-white border-bottom-0">
<h6 class="m-0 font-weight-bold text-primary"><?php echo t('users'); ?></h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th><?php echo t('name'); ?></th>
<th class="text-end"><?php echo t('total'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($staff_sales_data)): ?>
<tr><td colspan="2" class="text-center text-muted"><?php echo t('none'); ?></td></tr>
<?php endif; ?>
<?php foreach ($staff_sales_data as $s): ?>
<tr>
<td><?php echo htmlspecialchars($s['username']); ?></td>
<td class="text-end"><?php echo number_format($s['total'], 2); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<div class="card shadow-sm mb-4">
<div class="card-header py-3 bg-white border-bottom-0">
<h6 class="m-0 font-weight-bold text-primary"><?php echo t('outlets'); ?></h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th><?php echo t('outlet'); ?></th>
<th class="text-end"><?php echo t('total'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($outlet_sales_data)): ?>
<tr><td colspan="2" class="text-center text-muted"><?php echo t('none'); ?></td></tr>
<?php endif; ?>
<?php foreach ($outlet_sales_data as $o): ?>
<tr>
<td><?php echo htmlspecialchars($o['name']); ?></td>
<td class="text-end"><?php echo number_format($o['total'], 2); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const dailyCtx = document.getElementById('dailySalesChart').getContext('2d');
new Chart(dailyCtx, {
type: 'line',
data: {
labels: <?php echo json_encode(array_column($daily_sales_data, 'date')); ?>,
datasets: [{
label: '<?php echo t('total'); ?>',
data: <?php echo json_encode(array_column($daily_sales_data, 'total')); ?>,
borderColor: '#4e73df',
backgroundColor: 'rgba(78, 115, 223, 0.05)',
fill: true,
tension: 0.3
}]
},
options: {
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
y: { beginAtZero: true }
}
}
});
const catCtx = document.getElementById('categorySalesChart').getContext('2d');
new Chart(catCtx, {
type: 'doughnut',
data: {
labels: <?php echo json_encode(array_column($cat_sales_data, 'name')); ?>,
datasets: [{
data: <?php echo json_encode(array_column($cat_sales_data, 'total')); ?>,
backgroundColor: ['#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b'],
hoverBackgroundColor: ['#2e59d9', '#17a673', '#2c9faf', '#dda20a', '#be2617'],
hoverBorderColor: "rgba(234, 236, 244, 1)",
}]
},
options: {
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom' }
},
cutout: '70%'
}
});
</script>
<?php include __DIR__ . '/includes/footer.php'; ?>

222
admin/suppliers.php Normal file
View File

@ -0,0 +1,222 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../db/config.php";
require_permission("suppliers_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
// Handle Add/Edit Supplier
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$name = trim($_POST['name']);
$contact_person = trim($_POST['contact_person']);
$email = trim($_POST['email']);
$phone = trim($_POST['phone']);
$address = trim($_POST['address']);
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
if (empty($name)) {
$message = '<div class="alert alert-danger">Supplier name is required.</div>';
} else {
try {
if ($action === 'edit_supplier' && $id) {
if (!has_permission('suppliers_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("UPDATE suppliers SET name = ?, contact_person = ?, email = ?, phone = ?, address = ? WHERE id = ?");
$stmt->execute([$name, $contact_person, $email, $phone, $address, $id]);
$message = '<div class="alert alert-success">Supplier updated successfully!</div>';
}
} elseif ($action === 'add_supplier') {
if (!has_permission('suppliers_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO suppliers (name, contact_person, email, phone, address) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$name, $contact_person, $email, $phone, $address]);
$message = '<div class="alert alert-success">Supplier created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
// Handle Delete (Soft Delete)
if (isset($_GET['delete'])) {
if (!has_permission('suppliers_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete suppliers.</div>';
} else {
try {
$id = (int)$_GET['delete'];
// Soft delete to preserve data integrity for purchases
$pdo->prepare("UPDATE suppliers SET is_deleted = 1 WHERE id = ?")->execute([$id]);
header("Location: suppliers.php?deleted=1");
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Error removing supplier: ' . $e->getMessage() . '</div>';
}
}
}
if (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success">Supplier removed successfully!</div>';
}
$query = "SELECT * FROM suppliers WHERE is_deleted = 0 ORDER BY name ASC";
$suppliers_pagination = paginate_query($pdo, $query);
$suppliers = $suppliers_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Suppliers</h2>
<?php if (has_permission('suppliers_add')): ?>
<button class="btn btn-primary" onclick="openAddModal()">
<i class="bi bi-plus-lg"></i> Add Supplier
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($suppliers_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">ID</th>
<th>Name</th>
<th>Contact Person</th>
<th>Email / Phone</th>
<th>Address</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($suppliers as $supplier): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $supplier['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($supplier['name']) ?></td>
<td><?= htmlspecialchars($supplier['contact_person'] ?: '-') ?></td>
<td>
<div><?= htmlspecialchars($supplier['email'] ?: '-') ?></div>
<small class="text-muted"><?= htmlspecialchars($supplier['phone'] ?: '-') ?></small>
</td>
<td><small class="text-muted"><?= htmlspecialchars($supplier['address'] ?: '-') ?></small></td>
<td class="text-end pe-4">
<?php if (has_permission('suppliers_add')): ?>
<button type="button" class="btn btn-sm btn-outline-primary me-1"
onclick='openEditModal(<?= htmlspecialchars(json_encode($supplier), ENT_QUOTES, "UTF-8") ?>)' title="Edit"><i class="bi bi-pencil"></i></button>
<?php endif; ?>
<?php if (has_permission('suppliers_del')): ?>
<a href="?delete=<?= $supplier['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('<?= t('are_you_sure') ?>')"><i class="bi bi-trash"></i></a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($suppliers)): ?>
<tr>
<td colspan="6" class="text-center py-4 text-muted">No suppliers found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($suppliers_pagination); ?>
</div>
</div>
</div>
<!-- Supplier Modal -->
<?php if (has_permission('suppliers_add')): ?>
<div class="modal fade" id="supplierModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title" id="supplierModalTitle">Add New Supplier</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="supplierForm">
<div class="modal-body">
<input type="hidden" name="action" id="supplierAction" value="add_supplier">
<input type="hidden" name="id" id="supplierId">
<div class="mb-3">
<label class="form-label">Supplier Name <span class="text-danger">*</span></label>
<input type="text" name="name" id="supplierName" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Contact Person</label>
<input type="text" name="contact_person" id="supplierContactPerson" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" name="email" id="supplierEmail" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">Phone</label>
<input type="text" name="phone" id="supplierPhone" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">Address</label>
<textarea name="address" id="supplierAddress" class="form-control" rows="2"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary">Save Supplier</button>
</div>
</form>
</div>
</div>
</div>
<script>
function getSupplierModal() {
if (typeof bootstrap === 'undefined') return null;
const el = document.getElementById('supplierModal');
return el ? bootstrap.Modal.getOrCreateInstance(el) : null;
}
function openAddModal() {
const modal = getSupplierModal();
if (!modal) return;
document.getElementById('supplierModalTitle').innerText = 'Add New Supplier';
document.getElementById('supplierAction').value = 'add_supplier';
document.getElementById('supplierForm').reset();
document.getElementById('supplierId').value = '';
modal.show();
}
function openEditModal(supplier) {
if (!supplier) return;
const modal = getSupplierModal();
if (!modal) return;
document.getElementById('supplierModalTitle').innerText = 'Edit Supplier';
document.getElementById('supplierAction').value = 'edit_supplier';
document.getElementById('supplierId').value = supplier.id;
document.getElementById('supplierName').value = supplier.name || '';
document.getElementById('supplierContactPerson').value = supplier.contact_person || '';
document.getElementById('supplierEmail').value = supplier.email || '';
document.getElementById('supplierPhone').value = supplier.phone || '';
document.getElementById('supplierAddress').value = supplier.address || '';
modal.show();
}
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

265
admin/tables.php Normal file
View File

@ -0,0 +1,265 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . "/../includes/functions.php";
require_permission("tables_view");
$pdo = db();
$message = '';
// Handle Add/Edit Table
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$table_number = trim($_POST['table_number']);
$capacity = (int)$_POST['capacity'];
$area_id = (int)$_POST['area_id'];
$status = $_POST['status'];
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
if (empty($table_number)) {
$message = '<div class="alert alert-danger">Table number is required.</div>';
} else {
try {
if ($action === 'edit_table' && $id) {
if (!has_permission('tables_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("UPDATE `tables` SET table_number = ?, capacity = ?, area_id = ?, status = ? WHERE id = ?");
$stmt->execute([$table_number, $capacity, $area_id, $status, $id]);
$message = '<div class="alert alert-success">Table updated successfully!</div>';
}
} elseif ($action === 'add_table') {
if (!has_permission('tables_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$stmt = $pdo->prepare("INSERT INTO `tables` (table_number, capacity, area_id, status) VALUES (?, ?, ?, ?)");
$stmt->execute([$table_number, $capacity, $area_id, $status]);
$message = '<div class="alert alert-success">Table created successfully!</div>';
}
}
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
// Handle Delete (Soft Delete)
if (isset($_GET['delete'])) {
if (!has_permission('tables_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete tables.</div>';
} else {
try {
$id = (int)$_GET['delete'];
// Soft delete to avoid breaking historical order integrity
$pdo->prepare("UPDATE `tables` SET is_deleted = 1 WHERE id = ?")->execute([$id]);
header("Location: tables.php?deleted=1");
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Error removing table: ' . $e->getMessage() . '</div>';
}
}
}
if (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success">Table removed successfully!</div>';
}
$areas = $pdo->query("SELECT * FROM areas WHERE is_deleted = 0 ORDER BY name ASC")->fetchAll();
// Use a very standard query without backticks on aliases to maximize compatibility
$query = "SELECT t.id, t.table_number, t.capacity, t.status, t.area_id, a.name AS area_name
FROM `tables` t
LEFT JOIN areas a ON t.area_id = a.id
WHERE t.is_deleted = 0
ORDER BY a.name ASC, t.table_number ASC";
$tables_pagination = paginate_query($pdo, $query);
$tables = $tables_pagination['data'];
include 'includes/header.php';
// Base URL for QR codes
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') ? "https://" : "http://";
$host = $_SERVER['HTTP_HOST'];
// Calculate project root
$current_dir = dirname($_SERVER['PHP_SELF']); // /admin
$project_root = dirname($current_dir); // /
if ($project_root === DIRECTORY_SEPARATOR) $project_root = '';
$baseUrl = $protocol . $host . $project_root;
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Tables</h2>
<?php if (has_permission('tables_add')): ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#tableModal" onclick="prepareAddForm()">
<i class="bi bi-plus-lg"></i> Add Table
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm rounded-3">
<div class="card-body p-0">
<?php render_pagination_controls($tables_pagination); ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">QR Code</th>
<th>Table #</th>
<th>Area</th>
<th>Capacity</th>
<th>Status</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($tables)): ?>
<tr>
<td colspan="6" class="text-center py-5">
<div class="text-muted mb-3">
<i class="bi bi-info-circle fs-1"></i>
</div>
<h6>No tables found</h6>
<p class="small">Add your first table to get started.</p>
</td>
</tr>
<?php endif; ?>
<?php foreach ($tables as $table): ?>
<tr>
<td class="ps-4">
<?php
$qr_url = $baseUrl . "/qorder.php?table=" . $table['id'];
$qr_api = "https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=" . urlencode($qr_url);
?>
<div class="d-flex align-items-center gap-3">
<a href="<?= $qr_url ?>" target="_blank" class="d-inline-block border rounded p-1">
<img src="<?= $qr_api ?>" alt="QR" width="50" height="50">
</a>
<div class="small text-muted">
<a href="<?= $qr_api ?>&size=500x500" download="table_<?= $table['table_number'] ?>.png" class="text-decoration-none">
<i class="bi bi-download me-1"></i>Download
</a>
</div>
</div>
</td>
<td class="fw-bold"><?= htmlspecialchars($table['table_number']) ?></td>
<td>
<span class="badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-25">
<?= htmlspecialchars($table['area_name'] ?? 'No Area') ?>
</span>
</td>
<td>
<i class="bi bi-people me-1 text-muted"></i>
<?= htmlspecialchars((string)$table['capacity']) ?>
</td>
<td>
<?php
$status_class = 'bg-success';
if ($table['status'] === 'occupied') $status_class = 'bg-danger';
if ($table['status'] === 'reserved') $status_class = 'bg-warning';
if ($table['status'] === 'inactive') $status_class = 'bg-secondary';
?>
<span class="badge <?= $status_class ?> bg-opacity-10 text-<?= str_replace('bg-', '', $status_class) ?> border border-<?= str_replace('bg-', '', $status_class) ?> border-opacity-25">
<?= ucfirst($table['status']) ?>
</span>
</td>
<td class="text-end pe-4">
<div class="btn-group btn-group-sm">
<?php if (has_permission('tables_add')): ?>
<button class="btn btn-outline-primary" onclick='editTable(<?= json_encode($table) ?>)'>
<i class="bi bi-pencil"></i>
</button>
<?php endif; ?>
<?php if (has_permission('tables_del')): ?>
<a href="?delete=<?= $table['id'] ?>" class="btn btn-outline-danger" onclick="return confirm('Are you sure you want to remove this table?')">
<i class="bi bi-trash"></i>
</a>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php render_pagination_controls($tables_pagination); ?>
</div>
</div>
<!-- Add/Edit Table Modal -->
<div class="modal fade" id="tableModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<div class="modal-header">
<h5 class="modal-title fw-bold" id="modalTitle">Add New Table</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form action="tables.php" method="POST">
<input type="hidden" name="action" id="formAction" value="add_table">
<input type="hidden" name="id" id="tableId">
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-bold small text-uppercase">Table Number/Name</label>
<input type="text" name="table_number" id="tableNumber" class="form-control" required placeholder="e.g. Table 1, Booth A">
</div>
<div class="mb-3">
<label class="form-label fw-bold small text-uppercase">Area / Section</label>
<select name="area_id" id="areaId" class="form-select" required>
<option value="">Select Area</option>
<?php foreach ($areas as $area): ?>
<option value="<?= $area['id'] ?>"><?= htmlspecialchars($area['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label fw-bold small text-uppercase">Capacity</label>
<input type="number" name="capacity" id="tableCapacity" class="form-control" min="1" value="4" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label fw-bold small text-uppercase">Status</label>
<select name="status" id="tableStatus" class="form-select">
<option value="available">Available</option>
<option value="occupied">Occupied</option>
<option value="reserved">Reserved</option>
<option value="inactive">Inactive</option>
</select>
</div>
</div>
</div>
<div class="modal-footer bg-light border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary px-4">Save Table</button>
</div>
</form>
</div>
</div>
</div>
<script>
function prepareAddForm() {
document.getElementById('modalTitle').innerText = 'Add New Table';
document.getElementById('formAction').value = 'add_table';
document.getElementById('tableId').value = '';
document.getElementById('tableNumber').value = '';
document.getElementById('areaId').value = '';
document.getElementById('tableCapacity').value = '4';
document.getElementById('tableStatus').value = 'available';
}
function editTable(table) {
document.getElementById('modalTitle').innerText = 'Edit Table';
document.getElementById('formAction').value = 'edit_table';
document.getElementById('tableId').value = table.id;
document.getElementById('tableNumber').value = table.table_number;
document.getElementById('areaId').value = table.area_id;
document.getElementById('tableCapacity').value = table.capacity;
document.getElementById('tableStatus').value = table.status;
new bootstrap.Modal(document.getElementById('tableModal')).show();
}
</script>
<?php include 'includes/footer.php'; ?>

2
admin/test.php Normal file
View File

@ -0,0 +1,2 @@
<?php
echo "Test Admin";

237
admin/user_group_edit.php Normal file
View File

@ -0,0 +1,237 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$pdo = db();
require_permission('user_groups_view');
$id = $_GET['id'] ?? null;
$group = null;
// Handle New Group Creation via POST from user_groups.php
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$id && isset($_POST['name'])) {
if (!has_permission('user_groups_add')) {
header('Location: user_groups.php?error=permission_denied');
exit;
}
$stmt = $pdo->prepare("INSERT INTO user_groups (name, permissions) VALUES (?, '')");
$stmt->execute([$_POST['name']]);
$id = $pdo->lastInsertId();
header("Location: user_group_edit.php?id=" . $id);
exit;
}
if ($id) {
$stmt = $pdo->prepare("SELECT * FROM user_groups WHERE id = ?");
$stmt->execute([$id]);
$group = $stmt->fetch(PDO::FETCH_ASSOC);
}
if (!$group) {
header('Location: user_groups.php');
exit;
}
$message = '';
$modules = [
'dashboard' => 'Dashboard',
'pos' => 'POS Terminal',
'kitchen' => 'Kitchen View',
'orders' => 'Orders',
'products' => 'Products',
'categories' => 'Categories',
'customers' => 'Customers',
'outlets' => 'Outlets',
'areas' => 'Areas',
'tables' => 'Tables',
'suppliers' => 'Suppliers',
'purchases' => 'Purchases',
'expenses' => 'Expenses',
'expense_categories' => 'Expense Categories',
'payment_types' => 'Payment Types',
'loyalty' => 'Loyalty',
'ads' => 'Ads',
'reports' => 'Reports',
'users' => 'Users',
'user_groups' => 'User Groups',
'settings' => 'Settings',
'attendance' => 'Attendance',
'ratings' => 'Staff Ratings'
];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'update_group') {
if (!has_permission('user_groups_add')) {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3">Access Denied: You do not have permission to edit groups.</div>';
} else {
$name = $_POST['name'];
$permissions = isset($_POST['perms']) ? implode(',', $_POST['perms']) : '';
// Check if name is not empty
if (empty($name)) {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3"><i class="bi bi-exclamation-triangle-fill me-2"></i>Group name cannot be empty.</div>';
} else {
$stmt = $pdo->prepare("UPDATE user_groups SET name = ?, permissions = ? WHERE id = ?");
if ($stmt->execute([$name, $permissions, $id])) {
$message = '<div class="alert alert-success border-0 shadow-sm rounded-3"><i class="bi bi-check-circle-fill me-2"></i>Group updated successfully!</div>';
// Refresh group data
$stmt = $pdo->prepare("SELECT * FROM user_groups WHERE id = ?");
$stmt->execute([$id]);
$group = $stmt->fetch(PDO::FETCH_ASSOC);
} else {
$message = '<div class="alert alert-danger border-0 shadow-sm rounded-3"><i class="bi bi-exclamation-triangle-fill me-2"></i>Error updating group.</div>';
}
}
}
}
$current_perms = explode(',', $group['permissions']);
include 'includes/header.php';
?>
<div class="mb-4">
<a href="user_groups.php" class="text-decoration-none text-muted small"><i class="bi bi-arrow-left"></i> Back to Groups</a>
<div class="d-flex justify-content-between align-items-center mt-2">
<h2 class="fw-bold">Edit Group Permissions: <?= htmlspecialchars($group['name']) ?></h2>
<div>
<button type="button" class="btn btn-outline-primary btn-sm rounded-pill" onclick="toggleAll(true)">Select All</button>
<button type="button" class="btn btn-outline-secondary btn-sm rounded-pill" onclick="toggleAll(false)">Deselect All</button>
</div>
</div>
</div>
<?= $message ?>
<form method="POST">
<input type="hidden" name="action" value="update_group">
<div class="row">
<div class="col-md-12">
<div class="card border-0 shadow-sm rounded-4 mb-4">
<div class="card-body p-4">
<div class="mb-4 col-md-4">
<label class="form-label small fw-bold text-muted">GROUP NAME</label>
<input type="text" name="name" class="form-control form-control-lg border-0 bg-light" value="<?= htmlspecialchars($group['name']) ?>" required style="border-radius: 12px;" <?= !has_permission('user_groups_add') ? 'readonly' : '' ?>>
</div>
<label class="form-label small fw-bold text-muted mb-3">MODULE PERMISSIONS</label>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="bg-light">
<tr>
<th class="ps-4 py-3">Module</th>
<th class="text-center py-3">View</th>
<th class="text-center py-3">Add</th>
<th class="text-center py-3">Edit</th>
<th class="text-center py-3">Delete</th>
<th class="text-center py-3">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($modules as $key => $label): ?>
<tr>
<td class="ps-4 fw-medium text-dark"><?= $label ?></td>
<td class="text-center">
<div class="form-check form-check-inline m-0">
<input class="form-check-input perm-checkbox" type="checkbox" name="perms[]" value="<?= $key ?>_view" id="perm_<?= $key ?>_view" <?= (in_array($key . '_view', $current_perms) || in_array('all', $current_perms)) ? 'checked' : '' ?> <?= !has_permission('user_groups_add') ? 'disabled' : '' ?>>
</div>
</td>
<td class="text-center">
<div class="form-check form-check-inline m-0">
<input class="form-check-input perm-checkbox" type="checkbox" name="perms[]" value="<?= $key ?>_add" id="perm_<?= $key ?>_add" <?= (in_array($key . '_add', $current_perms) || in_array('all', $current_perms)) ? 'checked' : '' ?> <?= !has_permission('user_groups_add') ? 'disabled' : '' ?>>
</div>
</td>
<td class="text-center">
<div class="form-check form-check-inline m-0">
<input class="form-check-input perm-checkbox" type="checkbox" name="perms[]" value="<?= $key ?>_edit" id="perm_<?= $key ?>_edit" <?= (in_array($key . '_edit', $current_perms) || in_array('all', $current_perms)) ? 'checked' : '' ?> <?= !has_permission('user_groups_add') ? 'disabled' : '' ?>>
</div>
</td>
<td class="text-center">
<div class="form-check form-check-inline m-0">
<input class="form-check-input perm-checkbox" type="checkbox" name="perms[]" value="<?= $key ?>_del" id="perm_<?= $key ?>_del" <?= (in_array($key . '_del', $current_perms) || in_array('all', $current_perms)) ? 'checked' : '' ?> <?= !has_permission('user_groups_add') ? 'disabled' : '' ?>>
</div>
</td>
<td class="text-center">
<?php if (has_permission('user_groups_add')): ?>
<button type="button" class="btn btn-sm btn-outline-secondary rounded-pill px-3" onclick="toggleRow('<?= $key ?>', true)">All</button>
<button type="button" class="btn btn-sm btn-link text-muted text-decoration-none" onclick="toggleRow('<?= $key ?>', false)">None</button>
<?php else: ?>
<span class="text-muted small">-</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<tr class="table-info bg-opacity-10">
<td class="ps-4 fw-bold">ADMINISTRATIVE</td>
<td colspan="4" class="text-center small text-muted">Grants full access to everything in the system.</td>
<td class="text-center">
<div class="form-check form-switch d-inline-block">
<input class="form-check-input" type="checkbox" name="perms[]" value="all" id="perm_all" <?= in_array('all', $current_perms) ? 'checked' : '' ?> <?= !has_permission('user_groups_add') ? 'disabled' : '' ?>>
<label class="form-check-label fw-bold" for="perm_all">Super Admin</label>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-4 pt-3 border-top d-flex justify-content-end gap-2">
<a href="user_groups.php" class="btn btn-light rounded-pill px-4">Cancel</a>
<?php if (has_permission('user_groups_add')): ?>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm">Save Permissions</button>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
</form>
<script>
function toggleRow(key, state) {
const view = document.getElementById('perm_' + key + '_view');
const add = document.getElementById('perm_' + key + '_add');
const edit = document.getElementById('perm_' + key + '_edit');
const del = document.getElementById('perm_' + key + '_del');
if(view) view.checked = state;
if(add) add.checked = state;
if(edit) edit.checked = state;
if(del) del.checked = state;
}
function toggleAll(state) {
document.querySelectorAll('.perm-checkbox').forEach(cb => {
if (!cb.disabled) cb.checked = state;
});
}
// If Super Admin is checked, maybe disable others? Or just let them be.
const permAll = document.getElementById('perm_all');
if (permAll) {
permAll.addEventListener('change', function() {
if (this.checked) {
document.querySelectorAll('.perm-checkbox').forEach(cb => {
cb.checked = true;
cb.disabled = true;
});
} else {
document.querySelectorAll('.perm-checkbox').forEach(cb => {
cb.disabled = false;
});
}
});
// Initial state for Super Admin
if (permAll.checked) {
document.querySelectorAll('.perm-checkbox').forEach(cb => {
cb.disabled = true;
});
}
}
</script>
<?php include 'includes/footer.php'; ?>

219
admin/user_groups.php Normal file
View File

@ -0,0 +1,219 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../db/config.php";
require_permission("user_groups_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
// Handle New Group Creation
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_user_group') {
if (!has_permission('user_groups_add')) {
$message = '<div class="alert alert-danger">Access Denied.</div>';
} else {
$name = trim($_POST['name']);
if (empty($name)) {
$message = '<div class="alert alert-danger">Group name is required.</div>';
} else {
try {
$stmt = $pdo->prepare("INSERT INTO user_groups (name, permissions) VALUES (?, '')");
$stmt->execute([$name]);
$newId = $pdo->lastInsertId();
header("Location: user_group_edit.php?id=" . $newId);
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
}
// Handle Delete
if (isset($_GET['delete'])) {
if (!has_permission('user_groups_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete user groups.</div>';
} else {
try {
$id = $_GET['delete'];
// Don't allow deleting Administrator group
$stmt = $pdo->prepare("SELECT name FROM user_groups WHERE id = ?");
$stmt->execute([$id]);
$groupName = $stmt->fetchColumn();
if ($groupName === 'Administrator') {
$message = '<div class="alert alert-danger">The Administrator group cannot be deleted.</div>';
} else {
$pdo->prepare("DELETE FROM user_groups WHERE id = ?")->execute([$id]);
header("Location: user_groups.php?deleted=1");
exit;
}
} catch (PDOException $e) {
if ($e->getCode() == '23000') {
$message = '<div class="alert alert-danger">Cannot delete this group because it is linked to users.</div>';
} else {
$message = '<div class="alert alert-danger">Error deleting group: ' . $e->getMessage() . '</div>';
}
}
}
}
if (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success">User group deleted successfully!</div>';
}
$availablePermissions = [
'dashboard' => 'Dashboard',
'pos' => 'POS Terminal',
'orders' => 'Orders',
'kitchen' => 'Kitchen View',
'products' => 'Products',
'categories' => 'Categories',
'customers' => 'Customers',
'outlets' => 'Outlets',
'areas' => 'Areas',
'tables' => 'Tables',
'suppliers' => 'Suppliers',
'purchases' => 'Purchases',
'expenses' => 'Expenses',
'expense_categories' => 'Expense Categories',
'payment_types' => 'Payment Types',
'loyalty' => 'Loyalty',
'ads' => 'Ads',
'reports' => 'Reports',
'users' => 'Users',
'user_groups' => 'User Groups',
'settings' => 'Settings',
'attendance' => 'Attendance',
'ratings' => 'Staff Ratings'
];
$query = "SELECT * FROM user_groups ORDER BY id ASC";
$groups_pagination = paginate_query($pdo, $query);
$groups = $groups_pagination['data'];
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-0">User Roles & Groups</h2>
<p class="text-muted mb-0">Manage permissions and access levels</p>
</div>
<?php if (has_permission('user_groups_add')): ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addGroupModal">
<i class="bi bi-plus-lg"></i> Add Group
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($groups_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">ID</th>
<th>Group Name</th>
<th>Permissions Summary</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($groups as $group): ?>
<tr>
<td class="ps-4 fw-medium text-muted">#<?= $group['id'] ?></td>
<td class="fw-bold text-dark"><?= htmlspecialchars($group['name']) ?></td>
<td>
<?php
if ($group['permissions'] === 'all') {
echo '<span class="badge bg-danger-subtle text-danger border border-danger">Super Admin (All)</span>';
} else {
$perms = explode(',', $group['permissions']);
$modules_found = [];
foreach ($perms as $p) {
$mod = explode('_', $p)[0];
if (isset($availablePermissions[$mod]) && !in_array($availablePermissions[$mod], $modules_found)) {
$modules_found[] = $availablePermissions[$mod];
}
}
if (count($modules_found) > 0) {
echo '<div class="d-flex flex-wrap gap-1">';
$i = 0;
foreach ($modules_found as $m) {
if ($i < 5) {
echo '<span class="badge bg-light text-dark border small">' . $m . '</span>';
}
$i++;
}
if (count($modules_found) > 5) {
echo '<span class="badge bg-light text-muted border small">+' . (count($modules_found) - 5) . ' more</span>';
}
echo '</div>';
} elseif (!empty($group['permissions'])) {
echo '<small class="text-muted">' . htmlspecialchars(substr($group['permissions'], 0, 30)) . '...</small>';
} else {
echo '<small class="text-muted">No permissions defined</small>';
}
}
?>
</td>
<td class="text-end pe-4">
<?php if (has_permission('user_groups_add')): ?>
<a href="user_group_edit.php?id=<?= $group['id'] ?>" class="btn btn-sm btn-outline-primary rounded-pill px-3 me-1" title="Manage Permissions">
<i class="bi bi-shield-lock me-1"></i> Permissions
</a>
<?php endif; ?>
<?php if (has_permission('user_groups_del') && $group['name'] !== 'Administrator'): ?>
<a href="?delete=<?= $group['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('Delete this user group?')">
<i class="bi bi-trash me-1"></i> Delete
</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($groups_pagination); ?>
</div>
</div>
</div>
<!-- Add Group Modal -->
<div class="modal fade" id="addGroupModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-0 shadow rounded-4">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">Create New Group</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST">
<div class="modal-body py-4">
<input type="hidden" name="action" value="add_user_group">
<div class="mb-3">
<label class="form-label small fw-bold text-muted">GROUP NAME</label>
<input type="text" name="name" class="form-control form-control-lg border-0 bg-light rounded-3" required placeholder="e.g. Supervisor" autofocus>
</div>
<p class="text-muted small mb-0">After creating, you will be redirected to the permissions page to configure access levels.</p>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm">Create & Configure</button>
</div>
</form>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

447
admin/users.php Normal file
View File

@ -0,0 +1,447 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_once __DIR__ . "/../db/config.php";
require_permission("users_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$message = '';
// Handle Add/Edit User
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$action = $_POST['action'];
$username = trim($_POST['username']);
$full_name = trim($_POST['full_name']);
$full_name_ar = trim($_POST['full_name_ar'] ?? '');
$email = trim($_POST['email']);
$group_id = (int)$_POST['group_id'];
$is_active = isset($_POST['is_active']) ? 1 : 0;
$is_ratable = isset($_POST['is_ratable']) ? 1 : 0;
$commission_rate = (float)($_POST['commission_rate'] ?? 0);
$id = isset($_POST['id']) ? (int)$_POST['id'] : null;
$selected_outlets = $_POST['outlets'] ?? [];
$profile_pic = null;
if ($id) {
$stmt = $pdo->prepare("SELECT profile_pic FROM users WHERE id = ?");
$stmt->execute([$id]);
$profile_pic = $stmt->fetchColumn();
}
if (isset($_FILES['profile_pic']) && $_FILES['profile_pic']['error'] === UPLOAD_ERR_OK) {
$uploadDir = __DIR__ . '/../assets/images/users/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
$file_ext = strtolower(pathinfo($_FILES['profile_pic']['name'], PATHINFO_EXTENSION));
if (in_array($file_ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
$new_file_name = 'user_' . ($id ?: uniqid()) . '_' . uniqid() . '.' . $file_ext;
if (move_uploaded_file($_FILES['profile_pic']['tmp_name'], $uploadDir . $new_file_name)) {
$profile_pic = 'assets/images/users/' . $new_file_name;
}
}
}
if (empty($username)) {
$message = '<div class="alert alert-danger">Username is required.</div>';
} else {
try {
$pdo->beginTransaction();
if ($action === 'edit_user' && $id) {
if (!has_permission('users_edit') && !has_permission('users_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to edit users.</div>';
} else {
$sql = "UPDATE users SET username = ?, full_name = ?, full_name_ar = ?, email = ?, group_id = ?, is_active = ?, is_ratable = ?, profile_pic = ?, commission_rate = ? WHERE id = ?";
$params = [$username, $full_name, $full_name_ar, $email, $group_id, $is_active, $is_ratable, $profile_pic, $commission_rate, $id];
if (!empty($_POST['password'])) {
$password = password_hash($_POST['password'], PASSWORD_DEFAULT);
$sql = "UPDATE users SET username = ?, full_name = ?, full_name_ar = ?, email = ?, group_id = ?, is_active = ?, is_ratable = ?, profile_pic = ?, commission_rate = ?, password = ? WHERE id = ?";
$params = [$username, $full_name, $full_name_ar, $email, $group_id, $is_active, $is_ratable, $profile_pic, $commission_rate, $password, $id];
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
// Update outlets
$pdo->prepare("DELETE FROM user_outlets WHERE user_id = ?")->execute([$id]);
if (!empty($selected_outlets)) {
$stmt = $pdo->prepare("INSERT INTO user_outlets (user_id, outlet_id) VALUES (?, ?)");
foreach ($selected_outlets as $outlet_id) {
$stmt->execute([$id, (int)$outlet_id]);
}
}
$message = '<div class="alert alert-success">User updated successfully!</div>';
}
} elseif ($action === 'add_user') {
if (!has_permission('users_add')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to add users.</div>';
} else {
$password = password_hash($_POST['password'] ?: '123456', PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT INTO users (username, password, full_name, full_name_ar, email, group_id, is_active, is_ratable, profile_pic, commission_rate) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$username, $password, $full_name, $full_name_ar, $email, $group_id, $is_active, $is_ratable, $profile_pic, $commission_rate]);
$new_user_id = $pdo->lastInsertId();
// Update outlets
if (!empty($selected_outlets)) {
$stmt = $pdo->prepare("INSERT INTO user_outlets (user_id, outlet_id) VALUES (?, ?)");
foreach ($selected_outlets as $outlet_id) {
$stmt->execute([$new_user_id, (int)$outlet_id]);
}
}
$message = '<div class="alert alert-success">User created successfully!</div>';
}
}
$pdo->commit();
} catch (PDOException $e) {
$pdo->rollBack();
if ($e->getCode() == 23000) {
$message = '<div class="alert alert-danger">Username or Email already exists.</div>';
} else {
$message = '<div class="alert alert-danger">Database error: ' . $e->getMessage() . '</div>';
}
}
}
}
// Handle Delete (Soft Delete)
if (isset($_GET['delete'])) {
if (!has_permission('users_del')) {
$message = '<div class="alert alert-danger">Access Denied: You do not have permission to delete users.</div>';
} else {
$id = (int)$_GET['delete'];
// Don't allow deleting current user
if ($id == $_SESSION['user']['id']) {
$message = '<div class="alert alert-danger text-center">You cannot remove your own account.</div>';
} else {
try {
// Use Soft Delete to preserve data integrity for orders and staff ratings
$pdo->prepare("UPDATE users SET is_deleted = 1, is_active = 0 WHERE id = ?")->execute([$id]);
header("Location: users.php?deleted=1");
exit;
} catch (PDOException $e) {
$message = '<div class="alert alert-danger">Error removing user: ' . $e->getMessage() . '</div>';
}
}
}
}
if (isset($_GET['deleted'])) {
$message = '<div class="alert alert-success">User removed successfully!</div>';
}
$groups = $pdo->query("SELECT * FROM user_groups WHERE is_deleted = 0 ORDER BY name ASC")->fetchAll();
$all_outlets = $pdo->query("SELECT * FROM outlets WHERE is_deleted = 0 ORDER BY name ASC")->fetchAll();
$query = "SELECT u.*, g.name as group_name
FROM users u
LEFT JOIN user_groups g ON u.group_id = g.id
WHERE u.is_deleted = 0
ORDER BY u.id DESC";
$users_pagination = paginate_query($pdo, $query);
$users = $users_pagination['data'];
// Fetch outlets for each user
foreach ($users as &$user) {
$stmt = $pdo->prepare("SELECT outlet_id FROM user_outlets WHERE user_id = ?");
$stmt->execute([$user['id']]);
$user['outlets'] = $stmt->fetchAll(PDO::FETCH_COLUMN);
}
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1">User Management</h2>
<p class="text-muted mb-0">Manage staff accounts, roles and access permissions</p>
</div>
<?php if (has_permission('users_add')): ?>
<button class="btn btn-primary btn-lg shadow-sm" data-bs-toggle="modal" data-bs-target="#userModal" onclick="prepareAddForm()" style="border-radius: 12px;">
<i class="bi bi-plus-lg"></i> Add New User
</button>
<?php endif; ?>
</div>
<?= $message ?>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($users_pagination); ?>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">User</th>
<th>Arabic Name</th>
<th>Email</th>
<th>Role / Group</th>
<th>Status</th>
<th>Commission</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<?php if ($user['profile_pic']): ?>
<img src="../<?= htmlspecialchars($user['profile_pic']) ?>" class="rounded-circle me-3 border border-2 border-white shadow-sm" width="48" height="48" style="object-fit: cover;">
<?php else: ?>
<div class="bg-primary bg-gradient rounded-circle d-flex align-items-center justify-content-center text-white me-3 shadow-sm" style="width:48px;height:48px; font-weight:700; font-size: 1.2rem;"><?= strtoupper(substr($user['username'],0,1)) ?></div>
<?php endif; ?>
<div>
<div class="fw-bold text-dark fs-6"><?= htmlspecialchars($user['full_name'] ?: $user['username']) ?></div>
<small class="text-muted fw-medium">@<?= htmlspecialchars($user['username']) ?></small>
</div>
</div>
</td>
<td><?= htmlspecialchars($user['full_name_ar'] ?: '-') ?></td>
<td><?= htmlspecialchars($user['email'] ?: '-') ?></td>
<td><span class="badge bg-light text-dark border px-2 py-1"><?= htmlspecialchars($user['group_name'] ?: 'None') ?></span></td>
<td>
<?php if ($user['is_active']): ?>
<span class="badge bg-success-subtle text-success px-3 py-1 rounded-pill">Active</span>
<?php else: ?>
<span class="badge bg-danger-subtle text-danger px-3 py-1 rounded-pill">Inactive</span>
<?php endif; ?>
</td>
<td>
<span class="fw-bold text-primary"><?= number_format($user['commission_rate'], 1) ?>%</span>
</td>
<td class="text-end pe-4">
<?php if (has_permission('users_edit') || has_permission('users_add')): ?>
<button type="button" class="btn btn-sm btn-outline-primary rounded-pill px-3 me-1"
data-bs-toggle="modal" data-bs-target="#userModal"
onclick='prepareEditForm(<?= htmlspecialchars(json_encode($user), ENT_QUOTES, "UTF-8") ?>)' title="Edit Profile">
<i class="bi bi-pencil me-1"></i> Edit</button>
<?php endif; ?>
<?php if (has_permission('users_del')): ?>
<a href="?delete=<?= $user['id'] ?>" class="btn btn-sm btn-outline-danger rounded-pill px-3" onclick="return confirm('<?= t('are_you_sure') ?>')">
<i class="bi bi-trash"></i>
</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($users)): ?>
<tr>
<td colspan="7" class="text-center py-5 text-muted">No users found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($users_pagination); ?>
</div>
</div>
</div>
<!-- User Modal -->
<?php if (has_permission('users_add') || has_permission('users_edit')): ?>
<div class="modal fade" id="userModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg rounded-4">
<div class="modal-header bg-primary text-white border-0 py-3">
<h5 class="modal-title fw-bold" id="userModalTitle">Add New User</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="POST" id="userForm" enctype="multipart/form-data">
<div class="modal-body p-4">
<input type="hidden" name="action" id="userAction" value="add_user">
<input type="hidden" name="id" id="userId">
<div class="mb-4 text-center" id="userImagePreviewContainer" style="display: none;">
<img src="" id="userImagePreview" class="img-fluid rounded-circle border border-4 border-white shadow mb-2" style="width: 100px; height: 100px; object-fit: cover;">
<p class="small text-muted mb-0">Current Profile Picture</p>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">FULL NAME</label>
<div class="input-group">
<input type="text" name="full_name" id="userFullName" class="form-control rounded-start-3 border-0 bg-light">
<button class="btn btn-outline-secondary border-0 bg-light" type="button" id="btnTranslate">
<i class="bi bi-translate text-primary"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">ARABIC FULL NAME</label>
<input type="text" name="full_name_ar" id="userFullNameAr" class="form-control rounded-3 border-0 bg-light" dir="rtl">
</div>
<div class="row g-3">
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold text-muted">USERNAME <span class="text-danger">*</span></label>
<input type="text" name="username" id="userUsername" class="form-control rounded-3 border-0 bg-light" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold text-muted">EMAIL</label>
<input type="email" name="email" id="userEmail" class="form-control rounded-3 border-0 bg-light">
</div>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">PASSWORD <span id="pwdLabel" class="text-danger">*</span></label>
<input type="password" name="password" id="userPassword" class="form-control rounded-3 border-0 bg-light" placeholder="Min. 6 characters">
<small class="text-muted" id="pwdHint" style="display:none;">Leave blank to keep the current password.</small>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">ROLE / USER GROUP <span class="text-danger">*</span></label>
<select name="group_id" id="userGroupId" class="form-select rounded-3 border-0 bg-light" required>
<option value="">Select Group</option>
<?php foreach ($groups as $group): ?>
<option value="<?= $group['id'] ?>"><?= htmlspecialchars($group['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label small fw-bold text-muted">ASSIGNED OUTLETS</label>
<div class="bg-light p-3 rounded-3 border-0" style="max-height: 150px; overflow-y: auto;">
<?php foreach ($all_outlets as $outlet): ?>
<div class="form-check">
<input class="form-check-input outlet-checkbox" type="checkbox" name="outlets[]" value="<?= $outlet['id'] ?>" id="outlet_<?= $outlet['id'] ?>">
<label class="form-check-label" for="outlet_<?= $outlet['id'] ?>">
<?= htmlspecialchars($outlet['name']) ?>
</label>
</div>
<?php endforeach; ?>
</div>
<small class="text-muted">Select outlets this user has access to.</small>
</div>
<div class="row g-3 mb-3">
<div class="col-md-12">
<label class="form-label small fw-bold text-muted">COMMISSION RATE (%)</label>
<div class="input-group">
<input type="number" step="0.1" name="commission_rate" id="userCommissionRate" class="form-control rounded-3 border-0 bg-light" placeholder="0.0">
<span class="input-group-text border-0 bg-light">%</span>
</div>
<small class="text-muted">Percentage earned per sale processed by this user.</small>
</div>
</div>
<div class="mb-4">
<label class="form-label small fw-bold text-muted">PROFILE PICTURE</label>
<input type="file" name="profile_pic" id="userProfilePicFile" class="form-control rounded-3 border-0 bg-light" accept="image/*">
</div>
<div class="row bg-light p-3 rounded-4 g-2">
<div class="col-6">
<div class="form-check form-switch m-0">
<input class="form-check-input" type="checkbox" name="is_active" id="userIsActive" checked>
<label class="form-check-label fw-bold small" for="userIsActive">Active Account</label>
</div>
</div>
<div class="col-6">
<div class="form-check form-switch m-0">
<input class="form-check-input" type="checkbox" name="is_ratable" id="userIsRatable" checked>
<label class="form-check-label fw-bold small" for="userIsRatable">Ratable Staff</label>
</div>
</div>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-light rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary rounded-pill px-4 fw-bold shadow-sm">Save User Profile</button>
</div>
</form>
</div>
</div>
</div>
<script>
function prepareAddForm() {
document.getElementById('userModalTitle').innerText = 'Add New User';
document.getElementById('userAction').value = 'add_user';
document.getElementById('userForm').reset();
document.getElementById('userId').value = '';
document.getElementById('pwdLabel').style.display = 'inline';
document.getElementById('pwdHint').style.display = 'none';
document.getElementById('userPassword').required = true;
document.getElementById('userImagePreviewContainer').style.display = 'none';
document.getElementById('userCommissionRate').value = '0.0';
// Uncheck all outlets
document.querySelectorAll('.outlet-checkbox').forEach(cb => cb.checked = false);
}
function prepareEditForm(user) {
if (!user) return;
document.getElementById('userModalTitle').innerText = 'Edit User: ' + (user.full_name || user.username);
document.getElementById('userAction').value = 'edit_user';
document.getElementById('userId').value = user.id;
document.getElementById('userFullName').value = user.full_name || '';
document.getElementById('userFullNameAr').value = user.full_name_ar || '';
document.getElementById('userUsername').value = user.username || '';
document.getElementById('userEmail').value = user.email || '';
document.getElementById('userGroupId').value = user.group_id || '';
document.getElementById('userIsActive').checked = user.is_active == 1;
document.getElementById('userIsRatable').checked = user.is_ratable == 1;
document.getElementById('userCommissionRate').value = user.commission_rate || '0.0';
document.getElementById('userPassword').required = false;
document.getElementById('pwdLabel').style.display = 'none';
document.getElementById('pwdHint').style.display = 'block';
// Set outlets
document.querySelectorAll('.outlet-checkbox').forEach(cb => {
cb.checked = user.outlets && user.outlets.includes(cb.value);
});
if (user.profile_pic) {
const preview = document.getElementById('userImagePreview');
preview.src = '../' + user.profile_pic;
document.getElementById('userImagePreviewContainer').style.display = 'block';
} else {
document.getElementById('userImagePreviewContainer').style.display = 'none';
}
}
document.getElementById('btnTranslate').addEventListener('click', function() {
const text = document.getElementById('userFullName').value;
if (!text) {
alert('Please enter a full name first.');
return;
}
const btn = this;
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm text-primary" role="status" aria-hidden="true"></span>';
fetch('../api/translate.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: text,
target_lang: 'Arabic'
}),
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('userFullNameAr').value = data.translated_text;
} else {
alert('Translation failed: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred during translation.');
})
.finally(() => {
btn.disabled = false;
btn.innerHTML = originalHtml;
});
});
</script>
<?php endif; ?>
<?php include 'includes/footer.php'; ?>

301
ads.php Normal file
View File

@ -0,0 +1,301 @@
<?php
require_once __DIR__ . '/db/config.php';
$pdo = db();
// Fetch active advertisement images
$stmt = $pdo->prepare("SELECT * FROM ads_images WHERE is_active = 1 ORDER BY sort_order ASC");
$stmt->execute();
$ads = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get current outlet (default to 1 if not specified)
$outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : 1;
// Fetch company settings for branding
$stmt = $pdo->query("SELECT * FROM company_settings LIMIT 1");
$company = $stmt->fetch(PDO::FETCH_ASSOC);
$companyName = $company['company_name'] ?? 'Foody';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Now Serving & Promo - <?= htmlspecialchars($companyName) ?></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.0/font/bootstrap-icons.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;700;800&display=swap" rel="stylesheet">
<style>
:root {
--primary-color: #0d6efd;
--ready-color: #198754;
--preparing-color: #0dcaf0;
--bg-dark: #121212;
--card-bg: #1a1a1a;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-dark);
color: white;
overflow: hidden;
height: 100vh;
width: 100vw;
margin: 0;
padding: 0;
}
.main-container {
display: flex;
height: 100vh;
width: 100vw;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
box-sizing: border-box;
}
.serving-board {
flex: 0 0 35%;
background-color: var(--card-bg);
border-right: 2px solid #333;
display: flex;
flex-direction: column;
padding: 2.5rem;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 20;
overflow: hidden;
}
.promo-slider-container {
flex: 1;
position: relative;
background-color: #000;
overflow: hidden;
height: 100vh;
z-index: 10;
box-sizing: border-box;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
#promoCarousel, .carousel-inner, .carousel-item {
height: 100% !important;
width: 100% !important;
position: relative;
margin: 0 !important;
padding: 0 !important;
box-sizing: border-box;
}
.promo-image-element {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
background-color: #000;
}
/* Layout filtering */
body.split-view .carousel-item.layout-fullscreen { display: none !important; }
body.fullscreen-promo .carousel-item.layout-split { display: none !important; }
/* Fullscreen Promo View */
.fullscreen-promo .serving-board {
flex: 0 0 0% !important;
width: 0 !important;
padding: 0 !important;
margin: 0 !important;
border: none !important;
opacity: 0 !important;
pointer-events: none;
}
.fullscreen-promo .promo-slider-container {
flex: 0 0 100% !important;
width: 100% !important;
}
/* Order Status Styling */
.board-header { margin-bottom: 2.5rem; }
.board-title { font-weight: 800; font-size: 2.5rem; margin-bottom: 0.5rem; }
.section-title { font-weight: 700; font-size: 1.5rem; margin-bottom: 1.5rem; display: flex; align-items: center; gap: 10px; }
.order-grid { display: flex; flex-wrap: wrap; gap: 12px; }
.order-number {
font-size: 2.5rem;
font-weight: 800;
padding: 12px 24px;
border-radius: 12px;
min-width: 100px;
text-align: center;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
.empty-msg { font-style: italic; color: #555; }
</style>
</head>
<body class="split-view">
<div class="main-container" id="main-view">
<div class="serving-board">
<div class="board-header">
<h1 class="board-title">Order Status</h1>
<p class="text-muted"><?= htmlspecialchars($companyName) ?> • Outlet #<?= $outlet_id ?></p>
</div>
<div class="mb-5">
<h2 class="section-title text-success"><i class="bi bi-check-circle-fill"></i> Ready</h2>
<div id="ready-orders" class="order-grid"><div class="empty-msg">Waiting for orders...</div></div>
</div>
<div>
<h2 class="section-title text-info"><i class="bi bi-clock-history"></i> Preparing</h2>
<div id="preparing-orders" class="order-grid"><div class="empty-msg">No active orders</div></div>
</div>
<div class="mt-auto pt-4 border-top border-dark text-center text-muted">
<h3 id="clock" class="mb-0 fw-bold"></h3>
</div>
</div>
<div class="promo-slider-container">
<?php if (!empty($ads)): ?>
<div id="promoCarousel" class="carousel slide" data-bs-interval="8000">
<div class="carousel-inner">
<?php
$foundFirst = false;
foreach ($ads as $index => $ad):
$layout = $ad['display_layout'] ?? 'both';
$layoutClass = 'layout-' . $layout;
// Initial visibility in split-view
$isVisibleNow = ($layout === 'both' || $layout === 'split');
$activeClass = '';
if (!$foundFirst && $isVisibleNow) {
$activeClass = 'active';
$foundFirst = true;
}
?>
<div class="carousel-item <?= $activeClass ?> <?= $layoutClass ?>" data-layout="<?= $layout ?>">
<img src="<?= htmlspecialchars($ad['image_path']) ?>"
class="promo-image-element"
alt="Promotion">
</div>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<div class="h-100 d-flex align-items-center justify-content-center text-muted">
<div class="text-center">
<i class="bi bi-image-fill display-1 opacity-25"></i>
<p class="mt-3">No active promotions</p>
</div>
</div>
<?php endif; ?>
</div>
</div>
<button id="toggle-view" class="btn btn-dark btn-lg rounded-circle shadow" style="position:fixed; bottom:30px; right:30px; z-index:10000; opacity:0.6; width:60px; height:60px; display:flex; align-items:center; justify-content:center;">
<i class="bi bi-arrows-fullscreen"></i>
</button>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
let carouselInstance = null;
const el = document.querySelector('#promoCarousel');
function initCarousel() {
if (!el) return;
// Find first visible item and make it active
const isFullscreen = document.body.classList.contains('fullscreen-promo');
const items = el.querySelectorAll('.carousel-item');
let foundActive = false;
items.forEach(item => {
item.classList.remove('active');
const layout = item.getAttribute('data-layout');
const isVisible = (layout === 'both') ||
(isFullscreen && layout === 'fullscreen') ||
(!isFullscreen && layout === 'split');
if (isVisible && !foundActive) {
item.classList.add('active');
foundActive = true;
}
});
if (carouselInstance) {
carouselInstance.dispose();
carouselInstance = null;
}
carouselInstance = new bootstrap.Carousel(el, {
interval: 8000,
pause: false
});
carouselInstance.cycle();
}
initCarousel();
// Toggle View Functionality
const toggleBtn = document.getElementById('toggle-view');
const toggleIcon = toggleBtn.querySelector('i');
toggleBtn.addEventListener('click', () => {
const isFullscreenNow = document.body.classList.toggle('fullscreen-promo');
document.body.classList.toggle('split-view');
if (isFullscreenNow) {
toggleIcon.className = 'bi bi-fullscreen-exit';
} else {
toggleIcon.className = 'bi bi-arrows-fullscreen';
}
// Re-init carousel to handle hidden items
setTimeout(initCarousel, 100);
});
// Fetch Orders Logic
async function fetchOrders() {
try {
const res = await fetch('api/kitchen.php?outlet_id=<?= $outlet_id ?>');
if (!res.ok) throw new Error('API Error');
const orders = await res.json();
const rContainer = document.getElementById('ready-orders');
const pContainer = document.getElementById('preparing-orders');
if (!rContainer || !pContainer) return;
const ready = orders.filter(o => o.status === 'ready');
const prep = orders.filter(o => o.status === 'preparing');
rContainer.innerHTML = ready.length ? ready.map(o =>
`<div class="order-number" style="background:var(--ready-color); color:white;">${o.id}</div>`
).join('') : '<div class="empty-msg">Waiting for orders...</div>';
pContainer.innerHTML = prep.length ? prep.map(o =>
`<div class="order-number" style="background:var(--preparing-color); color:black;">${o.id}</div>`
).join('') : '<div class="empty-msg">No active orders</div>';
} catch(e) {
console.error('Fetch Orders Failed:', e);
}
}
setInterval(fetchOrders, 5000);
fetchOrders();
// Clock Logic
setInterval(() => {
const clock = document.getElementById('clock');
if (clock) {
clock.textContent = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
}, 1000);
});
</script>
</body>
</html>

View File

@ -1,52 +1,93 @@
<?php
// OpenAI proxy configuration (workspace scope).
// Reads values from environment variables or executor/.env.
// Reads values from environment variables or .env files.
function findEnvFile() {
$locations = [
__DIR__ . '/../../.env',
__DIR__ . '/../.env',
__DIR__ . '/.env',
'./.env',
'../.env',
];
if (isset($_SERVER['SCRIPT_FILENAME'])) {
$locations[] = dirname($_SERVER['SCRIPT_FILENAME']) . '/.env';
$locations[] = dirname($_SERVER['SCRIPT_FILENAME']) . '/../.env';
$locations[] = dirname($_SERVER['SCRIPT_FILENAME']) . '/../../.env';
}
foreach ($locations as $loc) {
if (file_exists($loc) && is_readable($loc)) {
return $loc;
}
}
return null;
}
$projectUuid = getenv('PROJECT_UUID');
$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)) {
// Try fallback locations if not in environment
if (empty($projectUuid) || empty($projectId)) {
$envPath = findEnvFile();
if ($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}");
if ($line === '' || $line[0] === '#') continue;
if (strpos($line, '=') === false) continue;
$parts = explode('=', $line, 2);
$key = trim($parts[0]);
$value = isset($parts[1]) ? trim($parts[1]) : '';
$value = trim($value, "' ");
if ($key === 'PROJECT_UUID' && empty($projectUuid)) $projectUuid = $value;
if ($key === 'PROJECT_ID' && empty($projectId)) $projectId = $value;
if (empty(getenv($key)) && !empty($value)) {
@putenv("{$key}={$value}");
$_ENV[$key] = $value;
$_SERVER[$key] = $value;
}
}
$projectUuid = getenv('PROJECT_UUID');
$projectId = getenv('PROJECT_ID');
}
}
$projectUuid = ($projectUuid === false) ? null : $projectUuid;
$projectId = ($projectId === false) ? null : $projectId;
// Second fallback: Try to extract from db/config.php
if (empty($projectUuid) || empty($projectId)) {
$possibleDbPaths = [
__DIR__ . '/../db/config.php',
__DIR__ . '/db/config.php',
'./db/config.php',
'../db/config.php'
];
foreach ($possibleDbPaths as $dbPath) {
if (file_exists($dbPath) && is_readable($dbPath)) {
$content = file_get_contents($dbPath);
if (empty($projectUuid) && preg_match('/define\s*\(\s*[\'"]DB_PASS[\'"]\s*,\s*[\'"]([^\'"]+)[\'"]\s*\)/', $content, $m)) {
$projectUuid = $m[1];
}
if (empty($projectId) && preg_match('/define\s*\(\s*[\'"]DB_NAME[\'"]\s*,\s*[\'"]app_(\d+)[\'"]\s*\)/', $content, $m)) {
$projectId = $m[1];
}
if (!empty($projectUuid)) break;
}
}
}
$baseUrl = 'https://flatlogic.com';
$responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null;
// Final values
$projectUuid = !empty($projectUuid) ? $projectUuid : null;
$projectId = !empty($projectId) ? $projectId : null;
return [
'base_url' => $baseUrl,
'responses_path' => $responsesPath,
'base_url' => 'https://flatlogic.com',
'responses_path' => $projectId ? "/projects/{$projectId}/ai-request" : null,
'project_id' => $projectId,
'project_uuid' => $projectUuid,
'project_header' => 'project-uuid',
'default_model' => 'gpt-5-mini',
'timeout' => 30,
'project_header' => 'Project-UUID',
'default_model' => 'gpt-4o-mini',
'timeout' => 60,
'verify_tls' => true,
];

67
api/attendance_sync.php Normal file
View File

@ -0,0 +1,67 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
// Simple API Key check (Optional but recommended)
// In a real scenario, you'd want a more secure way to authenticate the device
$api_key = $_GET['api_key'] ?? '';
$expected_key = getenv('ATTENDANCE_API_KEY') ?: 'secret_device_key';
if ($api_key !== $expected_key && !empty($expected_key)) {
// http_response_code(401);
// echo json_encode(['success' => false, 'error' => 'Unauthorized']);
// exit;
}
$input = file_get_contents('php://input');
$data = json_decode($input, true);
if (!$data) {
echo json_encode(['success' => false, 'error' => 'Invalid JSON input']);
exit;
}
// Normalize to array of logs
if (!isset($data[0])) {
$logs = [$data];
} else {
$logs = $data;
}
$pdo = db();
$inserted = 0;
$errors = [];
foreach ($logs as $log) {
$emp_id = $log['employee_id'] ?? null;
$timestamp = $log['timestamp'] ?? date('Y-m-d H:i:s');
$type = strtoupper($log['type'] ?? 'IN');
$device_id = $log['device_id'] ?? 'Biometric Device';
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
if (!$emp_id) {
$errors[] = "Missing employee_id for a log entry";
continue;
}
try {
// Find user by employee_id
$stmt = $pdo->prepare("SELECT id FROM users WHERE employee_id = ?");
$stmt->execute([$emp_id]);
$user = $stmt->fetch();
$user_id = $user ? $user['id'] : null;
// Insert log
$stmt = $pdo->prepare("INSERT INTO attendance_logs (user_id, employee_id, log_timestamp, log_type, device_id, ip_address) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$user_id, $emp_id, $timestamp, $type, $device_id, $ip]);
$inserted++;
} catch (Exception $e) {
$errors[] = "Error inserting log for $emp_id: " . $e->getMessage();
}
}
echo json_encode([
'success' => true,
'inserted' => $inserted,
'errors' => $errors
]);

17
api/auto_backup.php Normal file
View File

@ -0,0 +1,17 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
// This script triggers the auto backup check.
// It should be called asynchronously from the frontend to avoid blocking page load.
// We don't necessarily need to be logged in to trigger the check,
// as the function itself checks if 24h passed.
// But for security, let's at least check if it's a local or authorized request if possible.
// For now, trigger_auto_backup() is safe because it only runs once every 24h.
session_write_close();
trigger_auto_backup();
header('Content-Type: application/json');
echo json_encode(['success' => true]);

22
api/check_settings.php Normal file
View File

@ -0,0 +1,22 @@
<?php
require_once 'db/config.php';
try {
$db = db();
$stmt = $db->query("SELECT * FROM company_settings LIMIT 1");
$settings = $stmt->fetch(PDO::FETCH_ASSOC);
echo "<h1>Debug Settings</h1>";
echo "<pre>";
print_r($settings);
echo "</pre>";
if ($settings) {
echo "whatsapp_report_enabled: " . var_export($settings['whatsapp_report_enabled'], true) . "<br>";
echo "whatsapp_report_number: " . var_export($settings['whatsapp_report_number'], true) . "<br>";
} else {
echo "No settings found in database.";
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}

93
api/create_customer.php Normal file
View File

@ -0,0 +1,93 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$name = trim($input['name'] ?? '');
$phone = trim($input['phone'] ?? '');
if (empty($name)) {
echo json_encode(['error' => 'Name is required']);
exit;
}
// Relaxed phone validation: 8 to 15 digits
if (!preg_match('/^\d{8,15}$/', $phone)) {
echo json_encode(['error' => 'Phone number must be between 8 and 15 digits']);
exit;
}
try {
$pdo = db();
// Check if phone already exists
$stmt = $pdo->prepare("SELECT id FROM customers WHERE phone = ?");
$stmt->execute([$phone]);
if ($stmt->fetch()) {
echo json_encode(['error' => 'Customer with this phone number already exists']);
exit;
}
$stmt = $pdo->prepare("INSERT INTO customers (name, phone, points) VALUES (?, ?, 0)");
if ($stmt->execute([$name, $phone])) {
$id = $pdo->lastInsertId();
// Fetch settings for consistency (though new customer is 0 points)
$settingsStmt = $pdo->query("SELECT points_for_free_meal FROM loyalty_settings WHERE id = 1");
$settings = $settingsStmt->fetch(PDO::FETCH_ASSOC);
$threshold = $settings ? intval($settings['points_for_free_meal']) : 70;
// --- Send Welcome WhatsApp Message via Wablas ---
try {
require_once __DIR__ . '/../includes/WablasService.php';
$wablas = new WablasService($pdo);
$companyStmt = $pdo->query("SELECT company_name FROM company_settings LIMIT 1");
$companyName = $companyStmt->fetchColumn() ?: 'Our Restaurant';
// Fetch welcome template
$templateStmt = $pdo->query("SELECT setting_value FROM integration_settings WHERE provider='wablas' AND setting_key='welcome_template'");
$welcomeTemplate = $templateStmt->fetchColumn();
if (empty($welcomeTemplate)) {
$welcomeTemplate = "Welcome *{customer_name}* to *{company_name}*! 🎉\n\nThank you for registering. You can now earn loyalty points with every order!\n\nYou currently have 0 points. Collect {points_threshold} points to earn a free meal!";
}
$welcomeMsg = str_replace(
['{customer_name}', '{company_name}', '{points_threshold}'],
[$name, $companyName, $threshold],
$welcomeTemplate
);
$wablas->sendMessage($phone, $welcomeMsg);
} catch (Exception $w) {
error_log("Wablas Welcome Msg Exception: " . $w->getMessage());
}
echo json_encode([
'success' => true,
'customer' => [
'id' => $id,
'name' => $name,
'phone' => $phone,
'email' => '',
'points' => 0,
'eligible_for_free_meal' => false,
'points_needed' => $threshold
]
]);
} else {
echo json_encode(['error' => 'Failed to create customer']);
}
} catch (Exception $e) {
error_log("Create Customer Error: " . $e->getMessage());
echo json_encode(['error' => 'Database error']);
}

View File

@ -0,0 +1,26 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
$customer_id = isset($_GET['customer_id']) ? intval($_GET['customer_id']) : null;
if (!$customer_id) {
echo json_encode(['success' => false, 'error' => 'Customer ID required']);
exit;
}
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT id, points_change, reason, order_id, created_at
FROM loyalty_points_history
WHERE customer_id = ?
ORDER BY created_at DESC
LIMIT 50");
$stmt->execute([$customer_id]);
$history = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['success' => true, 'history' => $history]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

198
api/daily_report_cron.php Normal file
View File

@ -0,0 +1,198 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/WablasService.php';
// Enable error logging for cron
ini_set('log_errors', 1);
$logFile = __DIR__ . '/../storage/cron_debug.log';
ini_set('error_log', $logFile);
// Helper to log with timestamp
function cron_log($msg) {
global $logFile;
file_put_contents($logFile, "[" . date('Y-m-d H:i:s') . "] " . $msg . "\n", FILE_APPEND);
}
// Helper to format currency
function format_currency_custom($amount, $settings) {
$symbol = $settings['currency_symbol'] ?? '$';
$decimals = (int)($settings['currency_decimals'] ?? 3);
$position = $settings['currency_position'] ?? 'after';
$formatted = number_format((float)$amount, $decimals);
return ($position === 'after') ? "$formatted $symbol" : "$symbol $formatted";
}
try {
$settings = get_company_settings();
if (empty($settings['whatsapp_report_enabled']) || empty($settings['whatsapp_report_number']) || empty($settings['whatsapp_report_time'])) {
exit;
}
$timezone = !empty($settings['timezone']) ? $settings['timezone'] : 'UTC';
date_default_timezone_set($timezone);
$pdo = db();
// Use UTC strategy for consistent querying
// 1. Calculate Local Start/End of Today
$nowDt = new DateTime('now', new DateTimeZone($timezone));
$startLocal = clone $nowDt;
$startLocal->setTime(0, 0, 0);
$endLocal = clone $nowDt;
$endLocal->setTime(23, 59, 59);
// 2. Convert to UTC strings for DB query
$startUtc = clone $startLocal;
$startUtc->setTimezone(new DateTimeZone('UTC'));
$strStartUtc = $startUtc->format('Y-m-d H:i:s');
$endUtc = clone $endLocal;
$endUtc->setTimezone(new DateTimeZone('UTC'));
$strEndUtc = $endUtc->format('Y-m-d H:i:s');
// 3. Set DB Session to UTC so it interprets our UTC strings correctly
try {
$pdo->exec("SET time_zone = '+00:00'");
} catch (Exception $e) {
cron_log("Warning: Could not set MySQL timezone to UTC: " . $e->getMessage());
}
$reportTime = $settings['whatsapp_report_time'];
$lastReportFile = __DIR__ . '/../storage/last_daily_report.txt';
$lastReportDate = file_exists($lastReportFile) ? trim(file_get_contents($lastReportFile)) : '';
// Check schedule relative to Local Time
$targetTodayDt = clone $nowDt;
$timeParts = explode(':', $reportTime);
$targetTodayDt->setTime((int)$timeParts[0], (int)($timeParts[1] ?? 0), 0);
$diffToday = $nowDt->getTimestamp() - $targetTodayDt->getTimestamp();
// Run if within 15 mins of schedule AND not already run for this local date
if ($diffToday >= 0 && $diffToday <= 900 && $lastReportDate !== $nowDt->format('Y-m-d')) {
cron_log("Condition met: sending daily report for " . $nowDt->format('Y-m-d'));
cron_log("Querying UTC Range: $strStartUtc to $strEndUtc");
$todayDisplay = $nowDt->format('Y-m-d');
$companyName = $settings['company_name'] ?? 'Company';
// --- 1. Global Stats ---
// Total Revenue
$stmt = $pdo->prepare("SELECT SUM(total_amount) FROM orders WHERE created_at >= ? AND created_at <= ? AND status != 'cancelled'");
$stmt->execute([$strStartUtc, $strEndUtc]);
$totalRevenue = $stmt->fetchColumn() ?: 0;
// Total Orders
$stmt = $pdo->prepare("SELECT COUNT(*) FROM orders WHERE created_at >= ? AND created_at <= ? AND status != 'cancelled'");
$stmt->execute([$strStartUtc, $strEndUtc]);
$totalOrders = $stmt->fetchColumn() ?: 0;
// --- 2. Build Message Header ---
$message = "📊 *Daily Summary Report* 📊\n";
$message .= "🏢 *" . $companyName . "*\n";
$message .= "📅 Date: " . $todayDisplay . "\n";
$message .= "--------------------------------\n";
$message .= "🛒 *Total Orders:* " . $totalOrders . "\n";
$message .= "💰 *Total Revenue:* " . format_currency_custom($totalRevenue, $settings) . "\n";
// --- 3. Per-Outlet Stats ---
// Fetch outlets dynamically from DB
$stmtOutlets = $pdo->query("SELECT id, name, name_ar FROM outlets WHERE is_deleted = 0 ORDER BY id ASC");
$outlets = $stmtOutlets->fetchAll();
cron_log("Fetched " . count($outlets) . " outlets from database.");
foreach ($outlets as $outlet) {
$outletId = $outlet['id'];
$outletName = !empty($outlet['name_ar']) ? $outlet['name_ar'] : $outlet['name'];
// Outlet Revenue
$stmtRev = $pdo->prepare("SELECT SUM(total_amount) FROM orders WHERE outlet_id = ? AND created_at >= ? AND created_at <= ? AND status != 'cancelled'");
$stmtRev->execute([$outletId, $strStartUtc, $strEndUtc]);
$outletRevenue = $stmtRev->fetchColumn() ?: 0;
$message .= "\n🏪 *" . $outletName . " Total:* " . format_currency_custom($outletRevenue, $settings) . "\n";
// Staff Breakdown
$message .= "🧑‍🍳 *Staff Breakdown:*
";
$stmtStaff = $pdo->prepare("
SELECT u.username, u.full_name, COUNT(o.id) as order_count, SUM(o.total_amount) as revenue
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.outlet_id = ? AND o.created_at >= ? AND o.created_at <= ? AND o.status != 'cancelled'
GROUP BY o.user_id
");
$stmtStaff->execute([$outletId, $strStartUtc, $strEndUtc]);
$staffStats = $stmtStaff->fetchAll();
if (empty($staffStats)) {
$message .= "- No staff sales\n";
} else {
foreach ($staffStats as $stat) {
$staffName = !empty($stat['full_name']) ? $stat['full_name'] : ($stat['username'] ?? 'Unknown');
$message .= "- " . $staffName . ": " . format_currency_custom($stat['revenue'], $settings) . " (" . $stat['order_count'] . " orders)\n";
}
}
// Payment Breakdown
$message .= "💳 *Payment Breakdown:*
";
$stmtPayment = $pdo->prepare("
SELECT pt.name, SUM(o.total_amount) as revenue
FROM orders o
LEFT JOIN payment_types pt ON o.payment_type_id = pt.id
WHERE o.outlet_id = ? AND o.created_at >= ? AND o.created_at <= ? AND o.status != 'cancelled'
GROUP BY o.payment_type_id
");
$stmtPayment->execute([$outletId, $strStartUtc, $strEndUtc]);
$paymentStats = $stmtPayment->fetchAll();
if (empty($paymentStats)) {
$message .= "- No payments\n";
} else {
foreach ($paymentStats as $stat) {
$paymentName = $stat['name'] ?? 'Unknown';
$message .= "- " . $paymentName . ": " . format_currency_custom($stat['revenue'], $settings) . "\n";
}
}
}
$message .= "--------------------------------\n";
$message .= "Generated automatically at " . $nowDt->format('H:i:s') . "\n";
$message .= "Source: " . ($settings['company_name'] ?? 'System') . " Database";
// --- 4. Send Message ---
$wablas = new WablasService($pdo);
$recipients = explode(',', $settings['whatsapp_report_number']);
$anySuccess = false;
foreach ($recipients as $recipient) {
$recipient = trim($recipient);
if (empty($recipient)) continue;
cron_log("Sending detailed report to: " . $recipient);
$result = $wablas->sendMessage($recipient, $message);
if (!empty($result['success']) && $result['success'] == true) {
cron_log("Report sent successfully to " . $recipient);
$anySuccess = true;
} else {
cron_log("Failed to send report to " . $recipient . ": " . ($result['message'] ?? 'Unknown error'));
}
}
if ($anySuccess) {
file_put_contents($lastReportFile, $todayDisplay);
}
}
} catch (Exception $e) {
cron_log("CRITICAL ERROR: " . $e->getMessage());
}

111
api/kitchen.php Normal file
View File

@ -0,0 +1,111 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$pdo = db();
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// Fetch active kitchen orders
try {
$outlet_id = isset($_GET['outlet_id']) ? intval($_GET['outlet_id']) : 1;
// We want orders that are NOT completed or cancelled
// Status flow: pending -> preparing -> ready -> completed
// Kitchen sees: pending, preparing, ready
$stmt = $pdo->prepare("
SELECT
o.id, o.table_number, o.order_type, o.status, o.created_at, o.ready_time, o.customer_name, o.car_plate, o.user_id,
oi.quantity, COALESCE(p.name, oi.product_name) as product_name, COALESCE(v.name, oi.variant_name) as variant_name
FROM orders o
JOIN order_items oi ON o.id = oi.order_id
LEFT JOIN products p ON oi.product_id = p.id
LEFT JOIN product_variants v ON oi.variant_id = v.id
WHERE o.status IN ('pending', 'preparing', 'ready')
AND o.outlet_id = :outlet_id
ORDER BY o.created_at ASC
");
$stmt->execute(['outlet_id' => $outlet_id]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$orders = [];
foreach ($rows as $row) {
$id = $row['id'];
if (!isset($orders[$id])) {
$orders[$id] = [
'id' => $row['id'],
'table_number' => $row['table_number'],
'order_type' => $row['order_type'],
'status' => $row['status'],
'created_at' => $row['created_at'],
'customer_name' => $row['customer_name'],
'car_plate' => $row['car_plate'],
'ready_time' => $row['ready_time'],
'user_id' => $row['user_id'], 'is_online_order' => ($row['user_id'] === null && $row['order_type'] !== 'dine-in'), 'is_table_order' => ($row['user_id'] === null && $row['order_type'] === 'dine-in'),
'items' => []
];
}
$orders[$id]['items'][] = [
'quantity' => $row['quantity'],
'product_name' => $row['product_name'],
'variant_name' => $row['variant_name']
];
}
echo json_encode(array_values($orders));
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Update order status or bulk action
$data = json_decode(file_get_contents('php://input'), true);
$action = $data['action'] ?? null;
$outletId = $data['outlet_id'] ?? null;
if ($action === 'serve_all') {
if (!$outletId) {
http_response_code(400);
echo json_encode(['error' => 'Missing outlet_id for serve_all']);
exit;
}
try {
$stmt = $pdo->prepare("UPDATE orders SET status = 'completed' WHERE outlet_id = ? AND status IN ('pending', 'preparing', 'ready')");
$stmt->execute([$outletId]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
exit;
}
$orderId = $data['order_id'] ?? null;
$newStatus = $data['status'] ?? null;
if (!$orderId || !$newStatus) {
http_response_code(400);
echo json_encode(['error' => 'Missing order_id or status']);
exit;
}
$allowedStatuses = ['pending', 'preparing', 'ready', 'completed', 'cancelled'];
if (!in_array($newStatus, $allowedStatuses)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid status']);
exit;
}
try {
$stmt = $pdo->prepare("UPDATE orders SET status = ? WHERE id = ?");
$stmt->execute([$newStatus, $orderId]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
exit;
}

12
api/last_msg.txt Normal file
View File

@ -0,0 +1,12 @@
Dear *Moosa Ali Al-Abri*,
Thank you for dining with us! 🍽️
*Order Details:*
2 x Signature Burger
1 x Veggie Delight
Total: *37.480* OMR
You've earned *10 points* with this order.
💰 *Current Balance: 20 points*
You need *50 more points* to unlock a free meal.

467
api/order.php Normal file
View File

@ -0,0 +1,467 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/WablasService.php';
$input = file_get_contents('php://input');
$data = json_decode($input, true);
if (!$data) {
echo json_encode(['success' => false, 'error' => 'No data provided']);
exit;
}
try {
$pdo = db();
$pdo->beginTransaction();
// Validate order_type against allowed ENUM values
$allowed_types = ['dine-in', 'takeaway', 'delivery', 'drive-thru'];
$order_type = isset($data['order_type']) && in_array($data['order_type'], $allowed_types)
? $data['order_type']
: 'dine-in';
// Get outlet_id from input, default to 1 if missing
$outlet_id = !empty($data['outlet_id']) ? intval($data['outlet_id']) : 1;
$table_id = null;
$table_number = null;
if ($order_type === 'dine-in') {
$tid = $data['table_id'] ?? ($data['table_number'] ?? null); // Support both table_id and table_number as numeric ID
if ($tid) {
// Validate table exists AND belongs to the correct outlet
// Using standard aliases without backticks for better compatibility
$stmt = $pdo->prepare(
"SELECT t.id, t.table_number
FROM tables t
JOIN areas a ON t.area_id = a.id
WHERE t.id = ? AND a.outlet_id = ?"
);
$stmt->execute([$tid, $outlet_id]);
$table = $stmt->fetch(PDO::FETCH_ASSOC);
if ($table) {
$table_id = $table['id'];
$table_number = $table['table_number'];
}
}
// If not found or not provided, leave null (Walk-in/Counter) or try to find a default table for this outlet
if (!$table_id) {
// Optional: try to find the first available table for this outlet
$stmt = $pdo->prepare(
"SELECT t.id, t.table_number
FROM tables t
JOIN areas a ON t.area_id = a.id
WHERE a.outlet_id = ?
LIMIT 1"
);
$stmt->execute([$outlet_id]);
$table = $stmt->fetch(PDO::FETCH_ASSOC);
if ($table) {
$table_id = $table['id'];
$table_number = $table['table_number'];
}
}
}
// Customer Handling
$register_customer = !empty($data['register_customer']);
$customer_id = !empty($data['customer_id']) ? intval($data['customer_id']) : null;
$customer_name = $data['customer_name'] ?? null;
$customer_phone = $data['customer_phone'] ?? null;
$car_plate = $data['car_plate'] ?? null;
$prep_time_minutes = isset($data['prep_time_minutes']) ? intval($data['prep_time_minutes']) : null;
$ready_time = null;
if ($prep_time_minutes > 0) {
$ready_time = date("Y-m-d H:i:s", strtotime("+$prep_time_minutes minutes"));
}
$register_customer = !empty($data['register_customer']);
if (!$customer_id && $customer_phone && $register_customer) {
$stmt = $pdo->prepare("SELECT id FROM customers WHERE phone = ?");
$stmt->execute([$customer_phone]);
$existing = $stmt->fetch();
if ($existing) {
$customer_id = $existing['id'];
} else {
$stmt = $pdo->prepare("INSERT INTO customers (name, phone) VALUES (?, ?)");
$stmt->execute([$customer_name, $customer_phone]);
$customer_id = $pdo->lastInsertId();
}
} else if (!$customer_id && $customer_phone && !$register_customer) {
$stmt = $pdo->prepare("SELECT id FROM customers WHERE phone = ?");
$stmt->execute([$customer_phone]);
$existing = $stmt->fetch();
if ($existing) {
$customer_id = $existing['id'];
}
}
// Fetch Loyalty Settings
$settingsStmt = $pdo->query("SELECT is_enabled, points_per_order, points_for_free_meal FROM loyalty_settings WHERE id = 1");
$loyaltySettings = $settingsStmt->fetch(PDO::FETCH_ASSOC);
$loyalty_enabled = $loyaltySettings ? (bool)$loyaltySettings['is_enabled'] : true;
$points_threshold = $loyaltySettings ? intval($loyaltySettings['points_for_free_meal']) : 70;
$points_per_product = $loyaltySettings ? intval($loyaltySettings['points_per_order']) : 10;
$current_points = 0;
$points_deducted = 0;
$points_awarded = 0;
$award_history_id = null;
$redeem_history_id = null;
if ($customer_id) {
$stmt = $pdo->prepare("SELECT name, phone, points FROM customers WHERE id = ?");
$stmt->execute([$customer_id]);
$cust = $stmt->fetch(PDO::FETCH_ASSOC);
if ($cust) {
$customer_name = $cust['name'];
$customer_phone = $cust['phone'];
$current_points = intval($cust['points']);
} else {
$customer_id = null;
}
}
// Loyalty Redemption Logic (initial check)
$redeem_loyalty = !empty($data['redeem_loyalty']) && $loyalty_enabled;
if ($redeem_loyalty && $customer_id) {
if ($current_points < $points_threshold) {
throw new Exception("Insufficient loyalty points for redemption.");
}
}
// Total amount will be recalculated on server to be safe
$calculated_subtotal = 0;
$calculated_vat = 0;
// First, process items to calculate real total and handle loyalty
$processed_items = [];
$loyalty_items_indices = [];
if (!empty($data['items']) && is_array($data['items'])) {
foreach ($data['items'] as $item) {
$pid = $item['product_id'] ?? ($item['id'] ?? null);
if (!$pid) continue;
$qty = intval($item['quantity'] ?? 1);
$vid = $item['variant_id'] ?? null;
// Fetch Product Price (Promo aware)
$pStmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$pStmt->execute([$pid]);
$product = $pStmt->fetch(PDO::FETCH_ASSOC);
if (!$product) continue;
$unit_price = get_product_price($product);
$vat_percent = floatval($product['vat_percent'] ?? 0);
$variant_name = null;
// Add variant adjustment
if ($vid) {
$vStmt = $pdo->prepare("SELECT name, price_adjustment FROM product_variants WHERE id = ? AND product_id = ?");
$vStmt->execute([$vid, $pid]);
$variant = $vStmt->fetch(PDO::FETCH_ASSOC);
if ($variant) {
$unit_price += floatval($variant['price_adjustment']);
$variant_name = $variant['name'];
}
}
if ($product['is_loyalty']) {
$loyalty_items_indices[] = count($processed_items);
}
$processed_items[] = [
'product_id' => $pid,
'product_name' => $product['name'],
'variant_id' => $vid,
'variant_name' => $variant_name,
'quantity' => $qty,
'unit_price' => $unit_price,
'vat_percent' => $vat_percent,
'is_loyalty' => (bool)$product['is_loyalty']
];
}
}
// Handle Loyalty Redemption (Make loyalty items free up to points limit)
if ($redeem_loyalty && $customer_id) {
if (empty($loyalty_items_indices)) {
throw new Exception("No loyalty-eligible products in the order to redeem.");
}
$total_loyalty_qty = 0;
foreach ($processed_items as $item) {
// Safety check: Redemption orders should only contain loyalty items as per JS logic
if (!$item['is_loyalty']) {
throw new Exception("Loyalty redemption orders can only contain loyalty-eligible products. Please remove non-eligible items.");
}
$total_loyalty_qty += $item['quantity'];
}
$possible_redemptions = floor($current_points / $points_threshold);
if ($total_loyalty_qty > $possible_redemptions) {
throw new Exception("You are only eligible for $possible_redemptions free product(s). Please reduce quantity in cart.");
}
$redemptions_done = 0;
while ($redemptions_done < $possible_redemptions) {
// Find the most expensive loyalty item that is still paid
$max_price = -1;
$max_index = -1;
foreach ($processed_items as $idx => $item) {
if ($item['is_loyalty'] && $item['unit_price'] > 0 && $item['quantity'] > 0) {
if ($item['unit_price'] > $max_price) {
$max_price = $item['unit_price'];
$max_index = $idx;
}
}
}
if ($max_index === -1) break; // No more loyalty items to redeem
// Deduct points for ONE redemption
$points_deducted += $points_threshold;
$redemptions_done++;
// Make ONE unit of the found product free
if ($processed_items[$max_index]['quantity'] > 1) {
$processed_items[$max_index]['quantity']--;
$free_item = $processed_items[$max_index];
$free_item['quantity'] = 1;
$free_item['unit_price'] = 0;
$processed_items[] = $free_item;
} else {
$processed_items[$max_index]['unit_price'] = 0;
}
}
if ($redemptions_done > 0) {
// Update customer points and count
$deductStmt = $pdo->prepare("UPDATE customers SET points = points - ? WHERE id = ?");
$deductStmt->execute([$points_deducted, $customer_id]);
$pdo->prepare("UPDATE customers SET loyalty_redemptions_count = loyalty_redemptions_count + ? WHERE id = ?")
->execute([$redemptions_done, $customer_id]);
// Record Loyalty History (Deduction)
$historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Redeemed Free Product(s)')");
$historyStmt->execute([$customer_id, -$points_deducted]);
$redeem_history_id = $pdo->lastInsertId();
// --- OVERRIDE PAYMENT TYPE ---
$ptStmt = $pdo->prepare("SELECT id FROM payment_types WHERE name = 'Loyalty Redeem' LIMIT 1");
$ptStmt->execute();
$loyaltyPt = $ptStmt->fetchColumn();
if ($loyaltyPt) {
$data['payment_type_id'] = $loyaltyPt;
}
} else {
throw new Exception("No loyalty-eligible products in the order to redeem.");
}
}
// Recalculate Subtotal, VAT and Earned Points
foreach ($processed_items as &$pi) {
$item_subtotal = $pi['unit_price'] * $pi['quantity'];
$item_vat = $item_subtotal * ($pi['vat_percent'] / 100);
$pi['vat_amount'] = $item_vat;
$calculated_subtotal += $item_subtotal;
$calculated_vat += $item_vat;
// Award points for PAID loyalty items
if ($pi['is_loyalty'] && $pi['unit_price'] > 0 && $pi['quantity'] > 0) {
$points_awarded += $pi['quantity'] * $points_per_product;
}
}
unset($pi);
// Award Points
if ($customer_id && $loyalty_enabled && $points_awarded > 0) {
$awardStmt = $pdo->prepare("UPDATE customers SET points = points + ? WHERE id = ?");
$awardStmt->execute([$points_awarded, $customer_id]);
// Record Loyalty History (Award)
$historyStmt = $pdo->prepare("INSERT INTO loyalty_points_history (customer_id, points_change, reason) VALUES (?, ?, 'Earned from Products')");
$historyStmt->execute([$customer_id, $points_awarded]);
$award_history_id = $pdo->lastInsertId();
}
$final_total = max(0, $calculated_subtotal + $calculated_vat);
// Explicitly ensure loyalty redemption orders have 0 total
if ($redeem_loyalty) {
$final_total = 0;
$calculated_vat = 0;
}
// User/Payment info
$user = get_logged_user();
$user_id = $user ? $user['id'] : null;
$payment_type_id = !empty($data['payment_type_id']) ? intval($data['payment_type_id']) : null;
// Commission Calculation
$companySettings = get_company_settings();
$commission_amount = 0;
if (!empty($companySettings['commission_enabled']) && $user_id) {
$userStmt = $pdo->prepare("SELECT commission_rate FROM users WHERE id = ?");
$userStmt->execute([$user_id]);
$commission_rate = (float)$userStmt->fetchColumn();
if ($commission_rate > 0) {
$commission_amount = $calculated_subtotal * ($commission_rate / 100);
}
}
// Check for Existing Order ID (Update Mode)
$order_id = isset($data['order_id']) ? intval($data['order_id']) : null;
$is_update = false;
if ($order_id) {
$checkStmt = $pdo->prepare("SELECT id FROM orders WHERE id = ?");
$checkStmt->execute([$order_id]);
if ($checkStmt->fetch()) {
$is_update = true;
} else {
$order_id = null;
}
}
if ($is_update) {
$stmt = $pdo->prepare("UPDATE orders SET
outlet_id = ?, table_id = ?, table_number = ?, order_type = ?,
customer_id = ?, customer_name = ?, customer_phone = ?, car_plate = ?, ready_time = ?,
payment_type_id = ?, total_amount = ?, discount = ?, vat = ?, user_id = ?,
commission_amount = ?, status = 'pending'
WHERE id = ?");
$stmt->execute([
$outlet_id, $table_id, $table_number, $order_type,
$customer_id, $customer_name, $customer_phone, $car_plate, $ready_time,
$payment_type_id, $final_total, 0, $calculated_vat, $user_id,
$commission_amount, $order_id
]);
$delStmt = $pdo->prepare("DELETE FROM order_items WHERE order_id = ?");
$delStmt->execute([$order_id]);
} else {
$stmt = $pdo->prepare("INSERT INTO orders (outlet_id, table_id, table_number, order_type, customer_id, customer_name, customer_phone, car_plate, ready_time, payment_type_id, total_amount, discount, vat, user_id, commission_amount, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')");
$stmt->execute([$outlet_id, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $car_plate, $ready_time, $payment_type_id, $final_total, 0, $calculated_vat, $user_id, $commission_amount]);
$order_id = $pdo->lastInsertId();
}
// Update loyalty history with order_id
if ($order_id) {
if ($award_history_id) {
$pdo->prepare("UPDATE loyalty_points_history SET order_id = ? WHERE id = ?")->execute([$order_id, $award_history_id]);
}
if ($redeem_history_id) {
$pdo->prepare("UPDATE loyalty_points_history SET order_id = ? WHERE id = ?")->execute([$order_id, $redeem_history_id]);
}
}
// Insert Items and Update Stock
$item_stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, product_name, variant_id, variant_name, quantity, unit_price, vat_percent, vat_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stock_stmt = $pdo->prepare("UPDATE products SET stock_quantity = stock_quantity - ? WHERE id = ?");
$order_items_list = [];
foreach ($processed_items as $pi) {
$item_stmt->execute([$order_id, $pi['product_id'], $pi['product_name'], $pi['variant_id'], $pi['variant_name'], $pi['quantity'], $pi['unit_price'], $pi['vat_percent'], $pi['vat_amount']]);
// Decrement Stock
$stock_stmt->execute([$pi['quantity'], $pi['product_id']]);
$pName = $pi['product_name'];
if ($pi['variant_name']) {
$pName .= " ({$pi['variant_name']})";
}
$order_items_list[] = "{$pi['quantity']} x $pName";
}
$pdo->commit();
// --- Post-Transaction Actions (WhatsApp) ---
if ($customer_id && $customer_phone) {
try {
$final_points = $current_points - $points_deducted + ($loyalty_enabled ? $points_awarded : 0);
$wablas = new WablasService($pdo);
$company_name = $companySettings['company_name'] ?? 'Flatlogic POS';
$currency_symbol = $companySettings['currency_symbol'] ?? 'OMR';
$currency_position = $companySettings['currency_position'] ?? 'after';
$stmt = $pdo->prepare("SELECT setting_value FROM integration_settings WHERE provider = 'wablas' AND setting_key = 'order_template'");
$stmt->execute();
$template = $stmt->fetchColumn();
if (!$template) {
$template = "Dear *{customer_name}*,
Thank you for dining with *{company_name}*! 🍽️
*Order Details:*
{order_details}
Total: *{total_amount}*
You've earned *{points_earned} points* with this order.
💰 *Current Balance: {new_balance} points*
{loyalty_status}";
}
$loyalty_status = "";
if ($loyalty_enabled) {
$loyalty_status = ($final_points >= $points_threshold)
? "🎉 Congratulations! You have enough points for a *FREE MEAL* on your next visit!"
: "You need *" . ($points_threshold - $final_points) . " more points* to unlock a free meal.";
}
$formatted_total = number_format($final_total, (int)($companySettings['currency_decimals'] ?? 2));
if ($currency_position === 'after') {
$total_with_currency = $formatted_total . " " . $currency_symbol;
} else {
$total_with_currency = $currency_symbol . $formatted_total;
}
$replacements = [
'{customer_name}' => $customer_name,
'{company_name}' => $company_name,
'{order_id}' => $order_id,
'{order_details}' => implode("\n", $order_items_list),
'{total_amount}' => $total_with_currency,
'{currency_symbol}' => $currency_symbol,
'{points_earned}' => $loyalty_enabled ? $points_awarded : 0,
'{points_redeemed}' => $points_deducted,
'{new_balance}' => $final_points,
'{loyalty_status}' => $loyalty_status
];
$msg = str_replace(array_keys($replacements), array_values($replacements), $template);
$customer_phone = trim((string)$customer_phone);
$res = $wablas->sendMessage($customer_phone, $msg);
if (empty($res['success'])) {
error_log("Wablas Order Send Failed for {$customer_phone}: " . ($res['message'] ?? 'Unknown'));
}
} catch (Exception $w) {
error_log("Wablas Exception: " . $w->getMessage());
}
}
echo json_encode(['success' => true, 'order_id' => $order_id]);
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
error_log("Order Error: " . $e->getMessage());
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

82
api/print.php Normal file
View File

@ -0,0 +1,82 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/PrinterService.php';
$pdo = db();
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? 'print';
if ($action === 'test') {
$ip = $input['ip'] ?? null;
if (!$ip) {
echo json_encode(['success' => false, 'error' => 'IP is required']);
exit;
}
// Send a simple test line
$testData = "\x1b" . "@"; // Initialize printer
$testData .= "Printer Test Successful\n";
$testData .= "IP: $ip\n";
$testData .= "Date: " . date('Y-m-d H:i:s') . "\n";
$testData .= "\n\n\n\n\n";
$testData .= "\x1b" . "m"; // Cut
$result = PrinterService::sendToNetworkPrinter($ip, $testData);
echo json_encode($result);
exit;
}
$order_id = $input['order_id'] ?? null;
$type = $input['type'] ?? 'cashier'; // 'cashier' or 'kitchen'
if (!$order_id) {
echo json_encode(['success' => false, 'error' => 'Order ID is required']);
exit;
}
try {
// Fetch order details with outlet info
$stmt = $pdo->prepare(
"SELECT o.*, out.cashier_printer_ip, out.kitchen_printer_ip "
. "FROM orders o "
. "JOIN outlets out ON o.outlet_id = out.id "
. "WHERE o.id = ?"
);
$stmt->execute([$order_id]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$order) {
echo json_encode(['success' => false, 'error' => 'Order not found']);
exit;
}
// Fetch items
$stmt = $pdo->prepare("SELECT * FROM order_items WHERE order_id = ?");
$stmt->execute([$order_id]);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Fetch company settings
$company = get_company_settings();
// Determine target IP
$printer_ip = ($type === 'kitchen') ? $order['kitchen_printer_ip'] : $order['cashier_printer_ip'];
if (empty($printer_ip)) {
echo json_encode(['success' => false, 'error' => "No $type printer IP configured for this outlet"]);
exit;
}
// Generate ESC/POS data
$data = PrinterService::formatReceipt($order, $items, $company);
// Send to printer
$result = PrinterService::sendToNetworkPrinter($printer_ip, $data);
echo json_encode($result);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

31
api/purchase_details.php Normal file
View File

@ -0,0 +1,31 @@
<?php
require_once __DIR__ . "/../includes/functions.php";
require_permission("purchases_view");
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$id = $_GET['id'] ?? null;
if (!$id) {
echo json_encode(['success' => false, 'error' => 'Missing ID']);
exit;
}
$stmt = $pdo->prepare("SELECT * FROM purchases WHERE id = ?");
$stmt->execute([$id]);
$purchase = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$purchase) {
echo json_encode(['success' => false, 'error' => 'Purchase not found']);
exit;
}
$stmt = $pdo->prepare("SELECT pi.*, p.name as product_name FROM purchase_items pi JOIN products p ON pi.product_id = p.id WHERE pi.purchase_id = ?");
$stmt->execute([$id]);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'purchase' => $purchase,
'items' => $items
]);

107
api/recall_orders.php Normal file
View File

@ -0,0 +1,107 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$action = $_GET['action'] ?? 'list';
try {
if ($action === 'list') {
$outlet_id = $_GET['outlet_id'] ?? 1;
$stmt = $pdo->prepare("
SELECT o.id, o.customer_name, o.customer_phone, o.total_amount, o.created_at, o.table_number, o.order_type,
(SELECT COUNT(*) FROM order_items WHERE order_id = o.id) as item_count
FROM orders o
WHERE o.outlet_id = ?
AND o.status = 'pending'
AND (o.payment_type_id IS NULL OR o.payment_type_id = 0)
ORDER BY o.created_at DESC
");
$stmt->execute([$outlet_id]);
$orders = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Format date for JS
foreach ($orders as &$o) {
$o['time_formatted'] = date('H:i', strtotime($o['created_at']));
}
echo json_encode(['success' => true, 'orders' => $orders]);
exit;
}
if ($action === 'details') {
$order_id = $_GET['id'] ?? null;
if (!$order_id) {
echo json_encode(['success' => false, 'error' => 'Missing ID']);
exit;
}
// Fetch Order
$stmt = $pdo->prepare("SELECT * FROM orders WHERE id = ?");
$stmt->execute([$order_id]);
$order = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$order) {
echo json_encode(['success' => false, 'error' => 'Order not found']);
exit;
}
// Fetch Items with is_loyalty
$stmtItems = $pdo->prepare("
SELECT oi.*, p.name as product_name, p.price as base_price, p.is_loyalty, v.name as variant_name, v.price_adjustment
FROM order_items oi
JOIN products p ON oi.product_id = p.id
LEFT JOIN product_variants v ON oi.variant_id = v.id
WHERE oi.order_id = ?
");
$stmtItems->execute([$order_id]);
$items = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
// Format items for JS cart
$cartItems = [];
foreach ($items as $item) {
$cartItems[] = [
'id' => $item['product_id'],
'name' => $item['product_name'],
'price' => floatval($item['unit_price']),
'base_price' => floatval($item['base_price']),
'quantity' => intval($item['quantity']),
'variant_id' => $item['variant_id'],
'variant_name' => $item['variant_name'],
'hasVariants' => !empty($item['variant_id']),
'is_loyalty' => intval($item['is_loyalty']) === 1
];
}
// Fetch Customer
$customer = null;
if ($order['customer_id']) {
$cStmt = $pdo->prepare("SELECT * FROM customers WHERE id = ?");
$cStmt->execute([$order['customer_id']]);
$customer = $cStmt->fetch(PDO::FETCH_ASSOC);
if ($customer) {
// Fetch Loyalty Threshold for consistent frontend logic
$settingsStmt = $pdo->query("SELECT points_for_free_meal FROM loyalty_settings WHERE id = 1");
$loyaltySettings = $settingsStmt->fetch(PDO::FETCH_ASSOC);
$threshold = $loyaltySettings ? intval($loyaltySettings['points_for_free_meal']) : 70;
$customer['points'] = intval($customer['points']);
$customer['eligible_for_free_meal'] = $customer['points'] >= $threshold;
$customer['eligible_count'] = floor($customer['points'] / $threshold);
$customer['points_needed'] = max(0, $threshold - ($customer['points'] % $threshold));
}
}
echo json_encode([
'success' => true,
'order' => $order,
'items' => $cartItems,
'customer' => $customer
]);
exit;
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

37
api/search_customers.php Normal file
View File

@ -0,0 +1,37 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$q = $_GET['q'] ?? '';
if (strlen($q) < 2) {
echo json_encode([]);
exit;
}
try {
$pdo = db();
// Fetch Loyalty Settings
$settingsStmt = $pdo->query("SELECT points_for_free_meal FROM loyalty_settings WHERE id = 1");
$settings = $settingsStmt->fetch(PDO::FETCH_ASSOC);
$threshold = $settings ? intval($settings['points_for_free_meal']) : 70;
$stmt = $pdo->prepare("SELECT id, name, phone, email, points FROM customers WHERE name LIKE ? OR phone LIKE ? LIMIT 10");
$searchTerm = "%$q%";
$stmt->execute([$searchTerm, $searchTerm]);
$customers = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($customers as &$customer) {
$customer['points'] = intval($customer['points']);
$customer['eligible_for_free_meal'] = $customer['points'] >= $threshold;
$customer['eligible_count'] = floor($customer['points'] / $threshold);
$customer['points_needed'] = max(0, $threshold - ($customer['points'] % $threshold));
$customer['threshold'] = $threshold;
}
echo json_encode($customers);
} catch (Exception $e) {
error_log("Customer Search Error: " . $e->getMessage());
echo json_encode(['error' => 'Database error']);
}

48
api/tables.php Normal file
View File

@ -0,0 +1,48 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
try {
$pdo = db();
$outlet_id = isset($_GET['outlet_id']) ? intval($_GET['outlet_id']) : 1;
// Fetch all tables with their area names, filtered by outlet_id
// Using standard aliases without backticks for better compatibility
$sql = "
SELECT t.id, t.table_number AS name, t.capacity, a.name AS area_name, t.status
FROM `tables` t
LEFT JOIN areas a ON t.area_id = a.id
WHERE a.outlet_id = :outlet_id AND t.is_deleted = 0
ORDER BY a.name ASC, t.table_number ASC
";
$stmt = $pdo->prepare($sql);
$stmt->execute(['outlet_id' => $outlet_id]);
$tables = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Fetch currently occupied table IDs
// Orders that are NOT completed and NOT cancelled are considered active
$occupiedSql = "
SELECT DISTINCT table_id
FROM orders
WHERE status NOT IN ('completed', 'cancelled')
AND table_id IS NOT NULL
AND outlet_id = :outlet_id
";
$occupiedStmt = $pdo->prepare($occupiedSql);
$occupiedStmt->execute(['outlet_id' => $outlet_id]);
$occupiedTableIds = $occupiedStmt->fetchAll(PDO::FETCH_COLUMN);
// Mark tables as occupied
foreach ($tables as &$table) {
$table['is_occupied'] = in_array($table['id'], $occupiedTableIds) || $table['status'] === 'occupied';
}
echo json_encode(['success' => true, 'tables' => $tables]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

29
api/translate.php Normal file
View File

@ -0,0 +1,29 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../ai/LocalAIApi.php';
$data = json_decode(file_get_contents('php://input'), true);
$text = $data['text'] ?? '';
$target_lang = $data['target_lang'] ?? 'Arabic';
if (empty($text)) {
echo json_encode(['success' => false, 'error' => 'No text provided']);
exit;
}
$prompt = "Translate the following product name or description to $target_lang. Return ONLY the translated text, nothing else.\n\nText: $text";
$resp = LocalAIApi::createResponse([
'input' => [
['role' => 'system', 'content' => 'You are a helpful translation assistant.'],
['role' => 'user', 'content' => $prompt],
],
]);
if (!empty($resp['success'])) {
$translatedText = LocalAIApi::extractText($resp);
echo json_encode(['success' => true, 'translated_text' => trim($translatedText)]);
} else {
echo json_encode(['success' => false, 'error' => $resp['error'] ?? 'AI error']);
}

View File

@ -1,302 +1,434 @@
:root {
/* Default Theme (Light/Minimal) */
--primary-color: #1A1A1A;
--accent-color: #E63946;
--bg-body: #F5F5F5;
--bg-card: #FFFFFF;
--bg-sidebar: #FFFFFF;
--text-primary: #1A1A1A;
--text-secondary: #666666;
--text-heading: #999999;
--border-color: #EEEEEE;
--border-radius: 8px;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
--sidebar-active-bg: #FFF0F0;
--sidebar-active-color: #FF6B6B;
--sidebar-active-border: #FF6B6B;
}
/* Dark Theme */
[data-theme="dark"] {
--primary-color: #E0E0E0;
--accent-color: #FF6B6B;
--bg-body: #121212;
--bg-card: #1E1E1E;
--bg-sidebar: #1E1E1E;
--text-primary: #E0E0E0;
--text-secondary: #A0A0A0;
--text-heading: #888888;
--border-color: #333333;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.5);
--sidebar-active-bg: #2C2C2C;
--sidebar-active-color: #FF6B6B;
--sidebar-active-border: #FF6B6B;
}
/* Ocean Theme */
[data-theme="ocean"] {
--primary-color: #003049;
--accent-color: #0077B6;
--bg-body: #E0F7FA;
--bg-card: #FFFFFF;
--bg-sidebar: #FFFFFF;
--text-primary: #003049;
--text-secondary: #546E7A;
--text-heading: #0288D1;
--border-color: #B2EBF2;
--sidebar-active-bg: #E1F5FE;
--sidebar-active-color: #0288D1;
--sidebar-active-border: #0288D1;
}
/* Forest Theme */
[data-theme="forest"] {
--primary-color: #1B4332;
--accent-color: #2D6A4F;
--bg-body: #F1F8E9;
--bg-card: #FFFFFF;
--bg-sidebar: #FFFFFF;
--text-primary: #1B4332;
--text-secondary: #558B2F;
--text-heading: #33691E;
--border-color: #C8E6C9;
--sidebar-active-bg: #E8F5E9;
--sidebar-active-color: #2E7D32;
--sidebar-active-border: #2E7D32;
}
/* Grape Theme */
[data-theme="grape"] {
--primary-color: #4A148C;
--accent-color: #7B1FA2;
--bg-body: #F3E5F5;
--bg-card: #FFFFFF;
--bg-sidebar: #FFFFFF;
--text-primary: #4A148C;
--text-secondary: #7B1FA2;
--text-heading: #8E24AA;
--border-color: #E1BEE7;
--sidebar-active-bg: #F3E5F5;
--sidebar-active-color: #8E24AA;
--sidebar-active-border: #8E24AA;
}
body {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
color: #212529;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
margin: 0;
min-height: 100vh;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-body);
color: var(--text-primary);
line-height: 1.5;
margin: 0;
padding: 0;
transition: background-color 0.3s, color 0.3s;
}
.main-wrapper {
.navbar {
background-color: var(--bg-card);
border-bottom: 1px solid var(--border-color);
padding: 1rem 0;
}
.sidebar {
background: var(--bg-sidebar) !important;
border-right: 1px solid var(--border-color) !important;
}
.sidebar-header {
background: var(--bg-sidebar) !important;
border-bottom: 1px solid var(--border-color) !important;
}
.sidebar .nav-link {
color: var(--text-secondary) !important;
}
.sidebar .nav-link:hover, .sidebar .nav-link.active {
color: var(--sidebar-active-color) !important;
background: var(--sidebar-active-bg) !important;
border-right: 3px solid var(--sidebar-active-border) !important;
}
.sidebar-heading {
color: var(--text-heading) !important;
display: flex;
align-items: center;
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;
padding: 0.75rem 1.5rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
user-select: none;
}
.chat-input-area button:hover {
background: #000;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
/* Force group elements to always be black and ignore theme active/hover colors */
.sidebar .collapse .nav-link,
.sidebar .collapse .nav-link.active,
.sidebar .collapse .nav-link:hover,
.sidebar .collapse .nav-link:focus {
color: #000000 !important;
}
.sidebar .collapse .nav-link i,
.sidebar .collapse .nav-link.active i,
.sidebar .collapse .nav-link:hover i,
.sidebar .collapse .nav-link:focus i {
color: #000000 !important;
}
/* Background Animations */
.bg-animations {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
pointer-events: none;
.sidebar-heading i {
color: var(--accent-color);
font-size: 1.1em;
}
.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);
/* Chevron Rotation Logic */
.sidebar-heading .chevron-icon {
font-size: 0.85rem !important;
transition: transform 0.3s ease;
margin-right: 0 !important; /* Override generic margin */
color: var(--text-heading) !important; /* Softer color for chevron */
}
.blob-1 {
top: -10%;
left: -10%;
background: rgba(238, 119, 82, 0.4);
.sidebar-heading[aria-expanded="true"] .chevron-icon {
transform: rotate(180deg);
}
.blob-2 {
bottom: -10%;
right: -10%;
background: rgba(35, 166, 213, 0.4);
animation-delay: -7s;
width: 600px;
height: 600px;
.sidebar-heading.collapsed .chevron-icon {
transform: rotate(0deg);
}
.blob-3 {
top: 40%;
left: 30%;
background: rgba(231, 60, 126, 0.3);
animation-delay: -14s;
width: 450px;
height: 450px;
.brand-logo {
font-weight: 700;
font-size: 1.5rem;
color: var(--primary-color);
text-decoration: none;
letter-spacing: -0.5px;
}
@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); }
.menu-category-title {
font-weight: 700;
margin: 2rem 0 1rem;
font-size: 1.25rem;
border-bottom: 2px solid var(--accent-color);
display: inline-block;
color: var(--text-primary);
}
.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;
.product-card {
background: var(--bg-card);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--shadow);
transition: transform 0.2s ease;
border: 1px solid var(--border-color);
height: 100%;
}
.admin-link:hover {
background: rgba(0, 0, 0, 0.4);
text-decoration: none;
.product-card:hover {
transform: translateY(-4px);
}
.product-image {
width: 100%;
height: 180px;
background-color: var(--border-color);
object-fit: cover;
}
.product-info {
padding: 1.25rem;
}
.product-name {
font-weight: 600;
font-size: 1.1rem;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.product-desc {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 1rem;
min-height: 3rem;
}
.product-price {
font-weight: 700;
color: var(--accent-color);
font-size: 1.1rem;
}
.btn-add {
background-color: var(--primary-color);
color: #FFFFFF;
border-radius: var(--border-radius);
padding: 0.5rem 1rem;
font-weight: 600;
border: none;
}
.btn-add:hover {
opacity: 0.9;
}
.cart-sidebar {
background: var(--bg-card);
height: 100vh;
position: sticky;
top: 0;
border-left: 1px solid var(--border-color);
padding: 2rem;
display: flex;
flex-direction: column;
}
.cart-item {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
}
.cart-item-name {
font-weight: 600;
font-size: 0.9rem;
color: var(--text-primary);
}
.cart-item-price {
font-weight: 700;
font-size: 0.9rem;
color: var(--text-primary);
}
.cart-total {
margin-top: auto;
padding-top: 1rem;
border-top: 2px solid var(--primary-color);
font-weight: 700;
font-size: 1.25rem;
display: flex;
justify-content: space-between;
color: var(--text-primary);
}
.order-badge {
background: var(--accent-color);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 700;
}
/* 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-table-container {
background: var(--bg-card);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
padding: 1.5rem;
}
.admin-container h1 {
margin-top: 0;
color: #212529;
font-weight: 800;
.status-pending { background-color: #fff3cd !important; color: #856404 !important; border: 1px solid #ffeeba !important; font-weight: 600; padding: 0.35em 0.65em; }
.status-preparing { background-color: #cce5ff !important; color: #004085 !important; border: 1px solid #b8daff !important; font-weight: 600; padding: 0.35em 0.65em; }
.status-ready { background-color: #d1e7dd !important; color: #0f5132 !important; border: 1px solid #badbcc !important; font-weight: 600; padding: 0.35em 0.65em; }
.status-completed { background-color: #d1e7dd !important; color: #0f5132 !important; border: 1px solid #badbcc !important; font-weight: 600; padding: 0.35em 0.65em; }
.status-cancelled { background-color: #f8d7da !important; color: #721c24 !important; border: 1px solid #f5c6cb !important; font-weight: 600; padding: 0.35em 0.65em; }
.toast-container {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 1050;
}
.table {
width: 100%;
/* Friendly Table & UI Enhancements */
.filter-bar {
background: var(--bg-card);
border-radius: 12px;
padding: 1rem 1.5rem;
box-shadow: var(--shadow);
margin-bottom: 1.5rem;
}
.friendly-table {
border-collapse: separate;
border-spacing: 0 8px;
margin-top: 1.5rem;
border-spacing: 0 12px;
width: 100%;
}
.table th {
background: transparent;
.friendly-table thead th {
border: none;
padding: 1rem;
color: #6c757d;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
color: var(--text-heading);
letter-spacing: 0.8px;
padding: 0 1.5rem 0.5rem 1.5rem;
}
.table td {
background: #fff;
padding: 1rem;
.friendly-table tbody tr {
background: var(--bg-card);
box-shadow: var(--shadow);
border-radius: 12px;
transition: all 0.2s ease;
}
.friendly-table tbody tr:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0,0,0,0.06);
}
.friendly-table td {
border: none;
padding: 1rem 1.5rem;
vertical-align: middle;
background-color: var(--bg-card);
color: var(--text-primary);
}
.friendly-table td:first-child {
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
}
.friendly-table td:last-child {
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
}
.img-thumb-lg {
width: 64px;
height: 64px;
border-radius: 12px;
object-fit: cover;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.badge-soft {
background-color: var(--border-color);
color: var(--text-secondary);
font-weight: 500;
padding: 0.4em 0.8em;
border-radius: 6px;
}
.btn-icon-soft {
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
transition: all 0.2s;
background: var(--border-color);
color: var(--text-secondary);
border: none;
}
.table tr td:first-child { border-radius: 12px 0 0 12px; }
.table tr td:last-child { border-radius: 0 12px 12px 0; }
.form-group {
margin-bottom: 1.25rem;
.btn-icon-soft:hover {
background: var(--accent-color);
color: #FFFFFF;
}
.btn-icon-soft.delete:hover {
background: #fee2e2;
color: #ef4444;
}
.btn-icon-soft.edit:hover {
background: #dbeafe;
color: #3b82f6;
}
.text-price {
font-family: 'Inter', sans-serif;
font-weight: 700;
color: var(--text-primary);
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
/* Bootstrap Utility Overrides for Dark Mode */
[data-theme="dark"] .bg-white {
background-color: var(--bg-card) !important;
color: var(--text-primary) !important;
}
.form-control {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
background: #fff;
transition: all 0.3s ease;
box-sizing: border-box;
[data-theme="dark"] .bg-light {
background-color: var(--bg-body) !important;
color: var(--text-primary) !important;
}
.form-control:focus {
outline: none;
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
[data-theme="dark"] .text-muted {
color: var(--text-secondary) !important;
}
[data-theme="dark"] .text-dark {
color: var(--text-primary) !important;
}
[data-theme="dark"] .border-bottom,
[data-theme="dark"] .border-top,
[data-theme="dark"] .border-end,
[data-theme="dark"] .border-start,
[data-theme="dark"] .border {
border-color: var(--border-color) !important;
}
[data-theme="dark"] .card {
background-color: var(--bg-card);
border-color: var(--border-color);
}
[data-theme="dark"] .table {
color: var(--text-primary);
}
/* Ensure Dropdowns are always on top */
.dropdown-menu {
z-index: 1050 !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

143
customer_profile.php Normal file
View File

@ -0,0 +1,143 @@
<?php
require_once 'db/config.php';
$pdo = db();
$settings = get_company_settings();
$customer = null;
$orders = [];
$points_history = [];
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['phone'])) {
$phone = trim($_POST['phone']);
$stmt = $pdo->prepare("SELECT * FROM customers WHERE phone = ?");
$stmt->execute([$phone]);
$customer = $stmt->fetch(PDO::FETCH_ASSOC);
if ($customer) {
$stmt_orders = $pdo->prepare("SELECT o.id, o.created_at, o.total_amount, o.status, o.car_plate, o.ready_time,
(SELECT GROUP_CONCAT(CONCAT(oi.quantity, 'x ', oi.product_name) SEPARATOR ', ') FROM order_items oi WHERE oi.order_id = o.id) as items_summary
FROM orders o WHERE o.customer_id = ? ORDER BY o.created_at DESC");
$stmt_orders->execute([$customer['id']]);
$orders = $stmt_orders->fetchAll(PDO::FETCH_ASSOC);
$stmt_points = $pdo->prepare("SELECT * FROM loyalty_points_history WHERE customer_id = ? ORDER BY created_at DESC");
$stmt_points->execute([$customer['id']]);
$points_history = $stmt_points->fetchAll(PDO::FETCH_ASSOC);
} else {
$error = "Customer not found with this phone number.";
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>My Profile - <?= htmlspecialchars($settings['company_name'] ?? 'Order Online') ?></title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root { --primary-color: #2563eb; --bg-light: #f8fafc; --card-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --primary-font: 'Plus Jakarta Sans', sans-serif; }
body { background-color: var(--bg-light); font-family: var(--primary-font); color: #1e293b; }
.hero-section { background: linear-gradient(135deg, #1e293b 0%, #334155 100%); color: white; padding: 2rem 1rem 4rem; border-bottom-left-radius: 2.5rem; border-bottom-right-radius: 2.5rem; margin-bottom: -2rem; text-align: center; }
.back-btn { position: absolute; top: 1.5rem; left: 1.5rem; color: white; font-size: 1.5rem; }
.card-custom { background: white; border-radius: 1.25rem; box-shadow: var(--card-shadow); border: none; margin-bottom: 1.5rem; padding: 1.5rem; }
.points-display { font-size: 2.5rem; font-weight: 700; color: var(--primary-color); }
</style>
</head>
<body>
<header class="hero-section position-relative">
<a href="online_order.php" class="back-btn"><i class="bi bi-arrow-left"></i></a>
<h2 class="fw-bold mb-1 mt-2">My Profile</h2>
<p class="opacity-75 mb-0 small">View orders & loyalty points</p>
</header>
<div class="container py-4 position-relative" style="z-index: 10;">
<?php if (!$customer): ?>
<div class="card-custom">
<h4 class="fw-bold mb-3">Login to view profile</h4>
<?php if ($error): ?>
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="POST">
<div class="mb-3">
<label class="form-label">Phone Number</label>
<input type="tel" name="phone" class="form-control form-control-lg" placeholder="Enter your registered phone" required>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100 rounded-pill fw-bold">View Profile</button>
</form>
</div>
<?php else: ?>
<div class="card-custom text-center">
<div class="mb-2 text-muted small">Welcome back,</div>
<h3 class="fw-bold mb-3"><?= htmlspecialchars($customer['name']) ?></h3>
<div class="d-flex justify-content-center align-items-center gap-3 bg-light rounded-4 py-3 px-4 d-inline-flex mx-auto">
<div><div class="text-muted small">Available Points</div><div class="points-display"><?= (int)$customer['points'] ?></div></div>
<i class="bi bi-star-fill text-warning fs-1"></i>
</div>
</div>
<h5 class="fw-bold mb-3 ms-2">Order History</h5>
<?php if (empty($orders)): ?>
<p class="text-muted ms-2">No orders found.</p>
<?php else: ?>
<?php foreach ($orders as $order): ?>
<div class="card-custom p-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="fw-bold">Order #<?= $order['id'] ?></span>
<span class="badge bg-<?= $order['status'] === 'completed' ? 'success' : ($order['status'] === 'cancelled' ? 'danger' : 'warning') ?>"><?= ucfirst($order['status']) ?></span>
</div>
<div class="text-muted small mb-2"><i class="bi bi-calendar3 me-1"></i> <?= date('M d, Y h:i A', strtotime($order['created_at'])) ?></div>
<div class="small mb-2"><?= htmlspecialchars($order['items_summary']) ?></div>
<?php if(!empty($order['car_plate'])): ?><div class="small text-muted mb-2"><i class="bi bi-car-front me-1"></i> <?= htmlspecialchars($order['car_plate']) ?></div><?php endif; ?>
<?php if(!empty($order['ready_time']) && !in_array($order['status'], ['completed', 'cancelled'])): ?>
<div class="small mb-2 text-danger fw-bold countdown-timer" data-ready-time="<?= strtotime($order['ready_time']) * 1000 ?>"><i class="bi bi-clock-history me-1"></i> <span></span></div>
<?php endif; ?>
<div class="fw-bold text-primary text-end"><?= number_format($order['total_amount'], 3) ?> OMR</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
<h5 class="fw-bold mb-3 ms-2 mt-4">Points History</h5>
<?php if (empty($points_history)): ?>
<p class="text-muted ms-2">No points history found.</p>
<?php else: ?>
<div class="card-custom p-0 overflow-hidden">
<div class="list-group list-group-flush">
<?php foreach ($points_history as $hist): ?>
<div class="list-group-item p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="fw-bold small"><?= htmlspecialchars($hist['reason']) ?></div>
<div class="text-muted" style="font-size: 0.75rem;"><?= date('M d, Y h:i A', strtotime($hist['created_at'])) ?></div>
</div>
<div class="fw-bold <?= $hist['points_change'] > 0 ? 'text-success' : 'text-danger' ?>">
<?= $hist['points_change'] > 0 ? '+' : '' ?><?= $hist['points_change'] ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<div class="text-center mt-4">
<a href="customer_profile.php" class="btn btn-outline-secondary rounded-pill px-4">Logout</a>
</div>
<?php endif; ?>
</div>
<script>
function updateTimers() {
document.querySelectorAll('.countdown-timer').forEach(el => {
const readyTime = parseInt(el.getAttribute('data-ready-time'));
const now = new Date().getTime();
const diff = readyTime - now;
if (diff > 0) {
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
el.querySelector('span').innerText = 'Ready in: ' + minutes + 'm ' + seconds + 's';
} else {
el.querySelector('span').innerText = 'Should be ready soon!';
}
});
}
setInterval(updateTimers, 1000);
updateTimers();
</script>
</body>
</html>

41
db/cleanup_demo_data.php Normal file
View File

@ -0,0 +1,41 @@
<?php
require_once __DIR__ . '/config.php';
try {
$pdo = db();
$pdo->beginTransaction();
echo "Cleaning up demo data...\n";
// 1. Delete Order Items (Child of Orders)
$pdo->exec("DELETE FROM order_items");
echo "- Deleted order items.\n";
// 2. Delete Orders (Child of Outlets, Users, Tables)
$pdo->exec("DELETE FROM orders");
echo "- Deleted orders.\n";
// 3. Delete Tables (Child of Areas)
$pdo->exec("DELETE FROM tables");
echo "- Deleted tables.\n";
// 4. Delete Areas (Child of Outlets)
$pdo->exec("DELETE FROM areas");
echo "- Deleted areas.\n";
// 5. Delete Outlets
$pdo->exec("DELETE FROM outlets");
echo "- Deleted outlets.\n";
// Optional: Reset auto-increment (MySQL specific)
$pdo->exec("ALTER TABLE outlets AUTO_INCREMENT = 1");
$pdo->exec("ALTER TABLE orders AUTO_INCREMENT = 1");
$pdo->commit();
echo "✅ Demo data cleared successfully.\n";
} catch (Exception $e) {
$pdo->rollBack();
echo "❌ Error clearing data: " . $e->getMessage() . "\n";
}

16
db/company_settings.sql Normal file
View File

@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS company_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
company_name VARCHAR(255) NOT NULL DEFAULT 'My Restaurant',
address TEXT,
phone VARCHAR(50),
email VARCHAR(255),
vat_rate DECIMAL(5, 2) DEFAULT 0.00,
currency_symbol VARCHAR(10) DEFAULT '$',
currency_decimals INT DEFAULT 2,
timezone VARCHAR(100) DEFAULT 'UTC',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
INSERT INTO company_settings (company_name, address, phone, vat_rate, currency_symbol, currency_decimals)
SELECT 'My Restaurant', '123 Food Street', '555-0199', 10.00, '$', 2
WHERE NOT EXISTS (SELECT 1 FROM company_settings);

View File

@ -15,3 +15,23 @@ function db() {
}
return $pdo;
}
require_once __DIR__ . '/../includes/functions.php';
try {
$settings = get_company_settings();
if (!empty($settings['timezone'])) {
date_default_timezone_set($settings['timezone']);
} else {
date_default_timezone_set('UTC');
}
// Synchronize MySQL connection timezone offset with PHP
try {
db()->exec("SET time_zone = '" . date('P') . "'");
} catch (Exception $e) {
// Silently ignore if MySQL doesn't accept the timezone setting
}
} catch (Exception $e) {
date_default_timezone_set('UTC');
}

39
db/init.php Normal file
View File

@ -0,0 +1,39 @@
<?php
require_once __DIR__ . '/config.php';
try {
$pdo = db();
// Execute schema
$sql = file_get_contents(__DIR__ . '/schema.sql');
$pdo->exec($sql);
// Check if data exists
$stmt = $pdo->query("SELECT COUNT(*) FROM outlets");
if ($stmt->fetchColumn() == 0) {
// Seed Outlets
$pdo->exec("INSERT INTO outlets (name, address) VALUES ('Main Downtown', '123 Main St'), ('Westside Hub', '456 West Blvd')");
// Seed Categories
$pdo->exec("INSERT INTO categories (name, sort_order) VALUES ('Burgers', 1), ('Sides', 2), ('Drinks', 3)");
// Seed Products
$pdo->exec("INSERT INTO products (category_id, name, description, price) VALUES
(1, 'Signature Burger', 'Juicy beef patty with special sauce', 12.99),
(1, 'Veggie Delight', 'Plant-based patty with fresh avocado', 11.50),
(2, 'Truffle Fries', 'Crispy fries with truffle oil and parmesan', 5.99),
(3, 'Craft Cola', 'House-made sparkling cola', 3.50)");
// Seed Variants
$pdo->exec("INSERT INTO product_variants (product_id, name, price_adjustment) VALUES
(1, 'Double Patty', 4.00),
(1, 'Extra Cheese', 1.00),
(3, 'Large Portion', 2.00)");
echo "Database initialized and seeded successfully.";
} else {
echo "Database already initialized.";
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}

45
db/migrate.php Normal file
View File

@ -0,0 +1,45 @@
<?php
require_once __DIR__ . '/config.php';
$pdo = db();
// Ensure migrations table exists
$pdo->exec("CREATE TABLE IF NOT EXISTS migrations (
id INT AUTO_INCREMENT PRIMARY KEY,
migration_name VARCHAR(255) NOT NULL UNIQUE,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
$migrations_folder = array_diff(scandir(__DIR__ . '/migrations'), ['.', '..']);
sort($migrations_folder);
$applied_migrations = $pdo->query("SELECT migration_name FROM migrations")->fetchAll(PDO::FETCH_COLUMN);
foreach ($migrations_folder as $migration) {
if (!in_array($migration, $applied_migrations)) {
echo "Applying migration: $migration\n";
$sql = file_get_contents(__DIR__ . '/migrations/' . $migration);
try {
if (!empty(trim($sql))) {
$pdo->exec($sql);
}
$stmt = $pdo->prepare("INSERT INTO migrations (migration_name) VALUES (?)");
$stmt->execute([$migration]);
echo "Successfully applied $migration\n";
} catch (Exception $e) {
echo "Error applying $migration: " . $e->getMessage() . "\n";
// If it's a "Duplicate column name" error, we can assume it was applied manually and just record it
if (strpos($e->getMessage(), 'Duplicate column name') !== false || strpos($e->getMessage(), 'already exists') !== false) {
echo "Column/Table already exists, recording as applied.\n";
$stmt = $pdo->prepare("INSERT INTO migrations (migration_name) VALUES (?)");
$stmt->execute([$migration]);
} else {
// For other errors, we might want to stop or continue
echo "Stopping migrations due to error.\n";
break;
}
}
}
}
echo "Migration process finished.\n";

View File

@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS payment_types (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type ENUM('cash', 'card', 'api') DEFAULT 'cash',
api_provider VARCHAR(50) DEFAULT NULL,
is_active BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS integration_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
provider VARCHAR(50) NOT NULL,
setting_key VARCHAR(100) NOT NULL,
setting_value TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY unique_setting (provider, setting_key)
);
-- Attempt to add column, ignore if exists (standard SQL doesn't support IF NOT EXISTS for columns easily without procedures, but for this env we'll try)
-- We will run this via PHP and handle errors if column exists

View File

@ -0,0 +1,44 @@
-- Create customers table if it doesn't exist
CREATE TABLE IF NOT EXISTS customers (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
phone VARCHAR(20) UNIQUE,
email VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Add customer_id to orders table
SET @dbname = DATABASE();
SET @tablename = "orders";
SET @columnname = "customer_id";
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(table_name = @tablename)
AND (table_schema = @dbname)
AND (column_name = @columnname)
) > 0,
"SELECT 1",
"ALTER TABLE orders ADD COLUMN customer_id INT DEFAULT NULL AFTER order_type;"
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;
-- Add foreign key if it doesn't exist
SET @constraintname = "fk_orders_customer";
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE
(table_name = @tablename)
AND (table_schema = @dbname)
AND (constraint_name = @constraintname)
) > 0,
"SELECT 1",
"ALTER TABLE orders ADD CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE SET NULL;"
));
PREPARE addConstraint FROM @preparedStatement;
EXECUTE addConstraint;
DEALLOCATE PREPARE addConstraint;

View File

@ -0,0 +1,15 @@
-- Add payment_type_id to orders table
ALTER TABLE orders ADD COLUMN payment_type_id INT DEFAULT NULL;
-- Seed default payment types if table is empty
INSERT INTO payment_types (name, type, is_active)
SELECT * FROM (SELECT 'Cash', 'cash', 1) AS tmp
WHERE NOT EXISTS (
SELECT name FROM payment_types WHERE name = 'Cash'
) LIMIT 1;
INSERT INTO payment_types (name, type, is_active)
SELECT * FROM (SELECT 'Credit Card', 'card', 1) AS tmp
WHERE NOT EXISTS (
SELECT name FROM payment_types WHERE name = 'Credit Card'
) LIMIT 1;

View File

@ -0,0 +1,11 @@
INSERT INTO payment_types (name, type, is_active)
SELECT * FROM (SELECT 'Cash' as n, 'cash' as t, 1 as a) AS tmp
WHERE NOT EXISTS (
SELECT name FROM payment_types WHERE name = 'Cash'
) LIMIT 1;
INSERT INTO payment_types (name, type, is_active)
SELECT * FROM (SELECT 'Credit Card' as n, 'card' as t, 1 as a) AS tmp
WHERE NOT EXISTS (
SELECT name FROM payment_types WHERE name = 'Credit Card'
) LIMIT 1;

View File

@ -0,0 +1,28 @@
-- Add points column to customers table if it doesn't exist
SET @dbname = DATABASE();
SET @tablename = "customers";
SET @columnname = "points";
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(table_name = @tablename)
AND (table_schema = @dbname)
AND (column_name = @columnname)
) > 0,
"SELECT 1",
"ALTER TABLE customers ADD COLUMN points INT DEFAULT 0;"
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;
-- Create loyalty_settings table
CREATE TABLE IF NOT EXISTS loyalty_settings (
id INT PRIMARY KEY,
points_per_order INT DEFAULT 10,
points_for_free_meal INT DEFAULT 70
);
-- Seed default settings
INSERT IGNORE INTO loyalty_settings (id, points_per_order, points_for_free_meal) VALUES (1, 10, 70);

View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS integration_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
provider VARCHAR(50) NOT NULL,
setting_key VARCHAR(100) NOT NULL,
setting_value TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_provider_key (provider, setting_key)
);

View File

@ -0,0 +1,3 @@
INSERT INTO payment_types (name, type, is_active)
SELECT 'Loyalty Redeem', 'cash', 0
WHERE NOT EXISTS (SELECT 1 FROM payment_types WHERE name = 'Loyalty Redeem');

View File

@ -0,0 +1,28 @@
CREATE TABLE IF NOT EXISTS user_groups (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
permissions TEXT, -- JSON or comma-separated list of capabilities
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
group_id INT,
username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
full_name VARCHAR(255),
email VARCHAR(255) UNIQUE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE SET NULL
);
-- Seed default groups
INSERT INTO user_groups (name, permissions) VALUES ('Administrator', 'all');
INSERT INTO user_groups (name, permissions) VALUES ('Manager', 'manage_orders,manage_products,manage_reports');
INSERT INTO user_groups (name, permissions) VALUES ('Cashier', 'pos,manage_orders');
INSERT INTO user_groups (name, permissions) VALUES ('Waiter', 'pos');
-- Seed default admin user (password: admin123)
-- Using PHP to hash the password properly would be better, but for initial seeding we can use a placeholder if we have a setup script.
-- Let's just create the tables for now.

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS user_outlets (
user_id INT(11) NOT NULL,
outlet_id INT(11) NOT NULL,
PRIMARY KEY (user_id, outlet_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (outlet_id) REFERENCES outlets(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS ads_images (
id INT AUTO_INCREMENT PRIMARY KEY,
image_path VARCHAR(255) NOT NULL,
title VARCHAR(255) DEFAULT NULL,
sort_order INT DEFAULT 0,
is_active TINYINT(1) DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1,2 @@
ALTER TABLE orders ADD COLUMN user_id INT(11) NULL AFTER outlet_id;
ALTER TABLE orders ADD CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;

View File

@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS expense_categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS expenses (
id INT AUTO_INCREMENT PRIMARY KEY,
category_id INT NOT NULL,
outlet_id INT NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
description TEXT,
expense_date DATE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES expense_categories(id),
FOREIGN KEY (outlet_id) REFERENCES outlets(id)
);

View File

@ -0,0 +1,4 @@
ALTER TABLE products
ADD COLUMN promo_discount_percent DECIMAL(5, 2) DEFAULT NULL,
ADD COLUMN promo_date_from DATE DEFAULT NULL,
ADD COLUMN promo_date_to DATE DEFAULT NULL;

View File

@ -0,0 +1,18 @@
-- Add is_enabled column to loyalty_settings table
SET @dbname = DATABASE();
SET @tablename = "loyalty_settings";
SET @columnname = "is_enabled";
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(table_name = @tablename)
AND (table_schema = @dbname)
AND (column_name = @columnname)
) > 0,
"SELECT 1",
"ALTER TABLE loyalty_settings ADD COLUMN is_enabled TINYINT(1) DEFAULT 1;"
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

View File

@ -0,0 +1,11 @@
-- Add loyalty_redemptions_count to customers table
ALTER TABLE customers ADD COLUMN loyalty_redemptions_count INT DEFAULT 0;
-- Optional: Initialize count from existing orders
UPDATE customers c
SET c.loyalty_redemptions_count = (
SELECT COUNT(*)
FROM orders o
JOIN payment_types pt ON o.payment_type_id = pt.id
WHERE o.customer_id = c.id AND pt.name = 'Loyalty Redeem'
);

View File

@ -0,0 +1,2 @@
-- Add profile_pic column to users table
ALTER TABLE users ADD COLUMN profile_pic VARCHAR(255) DEFAULT NULL AFTER email;

View File

@ -0,0 +1,15 @@
-- Add employee_id to users to map biometric device IDs
ALTER TABLE users ADD COLUMN employee_id VARCHAR(50) UNIQUE AFTER full_name;
-- Table to store raw attendance logs from biometric device
CREATE TABLE IF NOT EXISTS attendance_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
employee_id VARCHAR(50), -- Mapping from the device
log_timestamp DATETIME,
log_type ENUM('IN', 'OUT', 'OTHER') DEFAULT 'IN',
device_id VARCHAR(100),
ip_address VARCHAR(45),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
);

View File

@ -0,0 +1,2 @@
-- Add cost_price to products table
ALTER TABLE products ADD COLUMN cost_price DECIMAL(10, 2) DEFAULT 0.00;

View File

@ -0,0 +1,28 @@
-- Migration: Add stock quantity to products and create purchase module tables
-- Add stock_quantity to products
ALTER TABLE products ADD COLUMN stock_quantity INT DEFAULT 0;
-- Create purchases table
CREATE TABLE IF NOT EXISTS purchases (
id INT AUTO_INCREMENT PRIMARY KEY,
supplier_id INT NULL,
purchase_date DATE NOT NULL,
total_amount DECIMAL(10, 2) DEFAULT 0.00,
status ENUM('pending', 'completed', 'cancelled') DEFAULT 'pending',
notes TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL
);
-- Create purchase_items table
CREATE TABLE IF NOT EXISTS purchase_items (
id INT AUTO_INCREMENT PRIMARY KEY,
purchase_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
cost_price DECIMAL(10, 2) NOT NULL,
total_price DECIMAL(10, 2) NOT NULL,
FOREIGN KEY (purchase_id) REFERENCES purchases(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS staff_ratings (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
rating INT NOT NULL CHECK (rating >= 1 AND rating <= 5),
comment TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

Some files were not shown because too many files have changed in this diff Show More