Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ae728c351 | ||
|
|
ff9ab72c0b | ||
|
|
5b05c65a86 | ||
|
|
29a1863608 | ||
|
|
241682d60b | ||
|
|
07fa5eff07 | ||
|
|
f122b78692 | ||
|
|
cc7015afbe | ||
|
|
c5ebc795f7 | ||
|
|
0d0f7b2fd8 | ||
|
|
60d2f356c7 | ||
|
|
2130c9f23a | ||
|
|
9d29d48375 | ||
|
|
391d782761 | ||
|
|
22ba99cda2 | ||
|
|
6ff90fea7b | ||
|
|
c1cd5b183c | ||
|
|
030b415fea | ||
|
|
cbac17cdd4 | ||
|
|
9248543cfe | ||
|
|
6be2e6d02b | ||
|
|
4f4b85539c | ||
|
|
d229f476df | ||
|
|
1aec2c17d6 | ||
|
|
aa6cc744d0 | ||
|
|
2772970659 | ||
|
|
b0479e299c | ||
|
|
5deb44b9f8 | ||
|
|
26095df612 | ||
|
|
c451a24982 | ||
|
|
9f56d9b0a2 | ||
|
|
c24fdab770 | ||
|
|
d4f9a4496f | ||
|
|
4ea856ec88 | ||
|
|
16f05ef073 | ||
|
|
a7bd1b142a | ||
|
|
9b31c32aba | ||
|
|
fa60b1d6db | ||
|
|
f1e599bc52 | ||
|
|
71dcc986a2 | ||
|
|
252e6df9b2 | ||
|
|
c5bf0227e4 | ||
|
|
a70af4ac2a | ||
|
|
07ef77935c | ||
|
|
1e9abec636 | ||
|
|
130a0dc0a3 | ||
|
|
fb1aa34d97 | ||
|
|
e2cbec0383 | ||
|
|
bd73e23131 | ||
|
|
4268e51b35 | ||
|
|
8faf7adbb6 | ||
|
|
ac32148f05 | ||
|
|
13a3054fd1 | ||
|
|
7e5ce73bed | ||
|
|
3f43980699 | ||
|
|
7673b8e4dc | ||
|
|
600e86d0fa | ||
|
|
a1d6822c0a | ||
|
|
0c98d8d160 | ||
|
|
2bb3386b5d | ||
|
|
9eaaf40d0f | ||
|
|
c850a45169 | ||
|
|
46245138f0 | ||
|
|
9999efc72b | ||
|
|
ad06129a7a | ||
|
|
a6faa425c0 | ||
|
|
0c612d04a8 | ||
|
|
6e5310e4dc | ||
|
|
05a5289cfc | ||
|
|
68b1e34fe9 | ||
|
|
a8bbec67c9 | ||
|
|
32a53bae23 | ||
|
|
98141c9a34 | ||
|
|
90c77f9a44 | ||
|
|
2a7531af42 | ||
|
|
3245d00d29 | ||
|
|
e2fb4c84bf | ||
|
|
6cbe1c3306 | ||
|
|
291449ae16 | ||
|
|
110b26742e | ||
|
|
b26eab2ba0 | ||
|
|
0bec862e83 | ||
|
|
9561548a1e | ||
|
|
8cc026eb96 | ||
|
|
c20fbe80f7 | ||
|
|
8f996b6408 | ||
|
|
171664feb3 | ||
|
|
c6c300fdbb | ||
|
|
6d026cb61f | ||
|
|
4a8fa3dc33 | ||
|
|
3f3fe5a7d5 | ||
|
|
5914657321 | ||
|
|
54fe86501d | ||
|
|
4bd6115a47 | ||
|
|
b98ef1276a | ||
|
|
999d73eacf | ||
|
|
95541b059b | ||
|
|
4bbeb16cfc | ||
|
|
370ceb510e | ||
|
|
3595e1b23e | ||
|
|
9a50d0a34e | ||
|
|
7886680cd0 | ||
|
|
4451897e8d | ||
|
|
21321721fc | ||
|
|
37dfb898e7 | ||
|
|
e5617b6c15 | ||
|
|
6c8b522da6 | ||
|
|
f9d2ca374d | ||
|
|
a238062edb | ||
|
|
5d1e95ef4f | ||
|
|
2996ec35e3 | ||
|
|
5db529f225 | ||
|
|
3d24190863 | ||
|
|
3ce9ce30e6 |
2
.env
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
PROJECT_ID=38682
|
||||||
|
PROJECT_UUID=9d5f8cc1-ff96-4857-9598-039478d542f5
|
||||||
88
INSTALL.md
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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'; ?>
|
||||||
250
admin/expense_categories.php
Normal 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
@ -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
@ -0,0 +1,51 @@
|
|||||||
|
<footer class="mt-5 pt-4 border-top text-center text-muted small">
|
||||||
|
<p>© <?= 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
header('Location: ratings.php');
|
||||||
|
exit;
|
||||||
396
admin/ratings.php
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1,2 @@
|
|||||||
|
<?php
|
||||||
|
echo "Test Admin";
|
||||||
237
admin/user_group_edit.php
Normal 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
@ -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
@ -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
@ -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>
|
||||||
105
ai/config.php
@ -1,52 +1,93 @@
|
|||||||
<?php
|
<?php
|
||||||
// OpenAI proxy configuration (workspace scope).
|
// 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');
|
$projectUuid = getenv('PROJECT_UUID');
|
||||||
$projectId = getenv('PROJECT_ID');
|
$projectId = getenv('PROJECT_ID');
|
||||||
|
|
||||||
if (
|
// Try fallback locations if not in environment
|
||||||
($projectUuid === false || $projectUuid === null || $projectUuid === '') ||
|
if (empty($projectUuid) || empty($projectId)) {
|
||||||
($projectId === false || $projectId === null || $projectId === '')
|
$envPath = findEnvFile();
|
||||||
) {
|
if ($envPath) {
|
||||||
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
|
||||||
if ($envPath && is_readable($envPath)) {
|
|
||||||
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
$line = trim($line);
|
$line = trim($line);
|
||||||
if ($line === '' || $line[0] === '#') {
|
if ($line === '' || $line[0] === '#') continue;
|
||||||
continue;
|
if (strpos($line, '=') === false) continue;
|
||||||
}
|
|
||||||
if (!str_contains($line, '=')) {
|
$parts = explode('=', $line, 2);
|
||||||
continue;
|
$key = trim($parts[0]);
|
||||||
}
|
$value = isset($parts[1]) ? trim($parts[1]) : '';
|
||||||
[$key, $value] = array_map('trim', explode('=', $line, 2));
|
$value = trim($value, "' ");
|
||||||
if ($key === '') {
|
|
||||||
continue;
|
if ($key === 'PROJECT_UUID' && empty($projectUuid)) $projectUuid = $value;
|
||||||
}
|
if ($key === 'PROJECT_ID' && empty($projectId)) $projectId = $value;
|
||||||
$value = trim($value, "\"' ");
|
|
||||||
if (getenv($key) === false || getenv($key) === '') {
|
if (empty(getenv($key)) && !empty($value)) {
|
||||||
putenv("{$key}={$value}");
|
@putenv("{$key}={$value}");
|
||||||
|
$_ENV[$key] = $value;
|
||||||
|
$_SERVER[$key] = $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$projectUuid = getenv('PROJECT_UUID');
|
|
||||||
$projectId = getenv('PROJECT_ID');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$projectUuid = ($projectUuid === false) ? null : $projectUuid;
|
// Second fallback: Try to extract from db/config.php
|
||||||
$projectId = ($projectId === false) ? null : $projectId;
|
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';
|
// Final values
|
||||||
$responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null;
|
$projectUuid = !empty($projectUuid) ? $projectUuid : null;
|
||||||
|
$projectId = !empty($projectId) ? $projectId : null;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'base_url' => $baseUrl,
|
'base_url' => 'https://flatlogic.com',
|
||||||
'responses_path' => $responsesPath,
|
'responses_path' => $projectId ? "/projects/{$projectId}/ai-request" : null,
|
||||||
'project_id' => $projectId,
|
'project_id' => $projectId,
|
||||||
'project_uuid' => $projectUuid,
|
'project_uuid' => $projectUuid,
|
||||||
'project_header' => 'project-uuid',
|
'project_header' => 'Project-UUID',
|
||||||
'default_model' => 'gpt-5-mini',
|
'default_model' => 'gpt-4o-mini',
|
||||||
'timeout' => 30,
|
'timeout' => 60,
|
||||||
'verify_tls' => true,
|
'verify_tls' => true,
|
||||||
];
|
];
|
||||||
67
api/attendance_sync.php
Normal 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
@ -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
@ -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
@ -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']);
|
||||||
|
}
|
||||||
26
api/customer_loyalty_history.php
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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']);
|
||||||
|
}
|
||||||
|
|
||||||
@ -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 {
|
body {
|
||||||
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
background-size: 400% 400%;
|
background-color: var(--bg-body);
|
||||||
animation: gradient 15s ease infinite;
|
color: var(--text-primary);
|
||||||
color: #212529;
|
line-height: 1.5;
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
margin: 0;
|
||||||
font-size: 14px;
|
padding: 0;
|
||||||
margin: 0;
|
transition: background-color 0.3s, color 0.3s;
|
||||||
min-height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
display: flex;
|
||||||
align-items: center;
|
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;
|
cursor: pointer;
|
||||||
font-weight: 600;
|
user-select: none;
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-area button:hover {
|
/* Force group elements to always be black and ignore theme active/hover colors */
|
||||||
background: #000;
|
.sidebar .collapse .nav-link,
|
||||||
transform: translateY(-2px);
|
.sidebar .collapse .nav-link.active,
|
||||||
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
.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 */
|
.sidebar-heading i {
|
||||||
.bg-animations {
|
color: var(--accent-color);
|
||||||
position: fixed;
|
font-size: 1.1em;
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blob {
|
/* Chevron Rotation Logic */
|
||||||
position: absolute;
|
.sidebar-heading .chevron-icon {
|
||||||
width: 500px;
|
font-size: 0.85rem !important;
|
||||||
height: 500px;
|
transition: transform 0.3s ease;
|
||||||
background: rgba(255, 255, 255, 0.2);
|
margin-right: 0 !important; /* Override generic margin */
|
||||||
border-radius: 50%;
|
color: var(--text-heading) !important; /* Softer color for chevron */
|
||||||
filter: blur(80px);
|
|
||||||
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blob-1 {
|
.sidebar-heading[aria-expanded="true"] .chevron-icon {
|
||||||
top: -10%;
|
transform: rotate(180deg);
|
||||||
left: -10%;
|
|
||||||
background: rgba(238, 119, 82, 0.4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blob-2 {
|
.sidebar-heading.collapsed .chevron-icon {
|
||||||
bottom: -10%;
|
transform: rotate(0deg);
|
||||||
right: -10%;
|
|
||||||
background: rgba(35, 166, 213, 0.4);
|
|
||||||
animation-delay: -7s;
|
|
||||||
width: 600px;
|
|
||||||
height: 600px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blob-3 {
|
.brand-logo {
|
||||||
top: 40%;
|
font-weight: 700;
|
||||||
left: 30%;
|
font-size: 1.5rem;
|
||||||
background: rgba(231, 60, 126, 0.3);
|
color: var(--primary-color);
|
||||||
animation-delay: -14s;
|
text-decoration: none;
|
||||||
width: 450px;
|
letter-spacing: -0.5px;
|
||||||
height: 450px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes move {
|
.menu-category-title {
|
||||||
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
|
font-weight: 700;
|
||||||
33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
|
margin: 2rem 0 1rem;
|
||||||
66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
|
font-size: 1.25rem;
|
||||||
100% { transform: translate(0, 0) rotate(360deg) scale(1); }
|
border-bottom: 2px solid var(--accent-color);
|
||||||
|
display: inline-block;
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-link {
|
.product-card {
|
||||||
font-size: 14px;
|
background: var(--bg-card);
|
||||||
color: #fff;
|
border-radius: var(--border-radius);
|
||||||
text-decoration: none;
|
overflow: hidden;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
box-shadow: var(--shadow);
|
||||||
padding: 0.5rem 1rem;
|
transition: transform 0.2s ease;
|
||||||
border-radius: 8px;
|
border: 1px solid var(--border-color);
|
||||||
transition: all 0.3s ease;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-link:hover {
|
.product-card:hover {
|
||||||
background: rgba(0, 0, 0, 0.4);
|
transform: translateY(-4px);
|
||||||
text-decoration: none;
|
}
|
||||||
|
|
||||||
|
.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 Styles */
|
||||||
.admin-container {
|
.admin-table-container {
|
||||||
max-width: 900px;
|
background: var(--bg-card);
|
||||||
margin: 3rem auto;
|
border-radius: var(--border-radius);
|
||||||
padding: 2.5rem;
|
box-shadow: var(--shadow);
|
||||||
background: rgba(255, 255, 255, 0.85);
|
padding: 1.5rem;
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
border-radius: 24px;
|
|
||||||
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-container h1 {
|
.status-pending { background-color: #fff3cd !important; color: #856404 !important; border: 1px solid #ffeeba !important; font-weight: 600; padding: 0.35em 0.65em; }
|
||||||
margin-top: 0;
|
.status-preparing { background-color: #cce5ff !important; color: #004085 !important; border: 1px solid #b8daff !important; font-weight: 600; padding: 0.35em 0.65em; }
|
||||||
color: #212529;
|
.status-ready { background-color: #d1e7dd !important; color: #0f5132 !important; border: 1px solid #badbcc !important; font-weight: 600; padding: 0.35em 0.65em; }
|
||||||
font-weight: 800;
|
.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 {
|
/* Friendly Table & UI Enhancements */
|
||||||
width: 100%;
|
.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-collapse: separate;
|
||||||
border-spacing: 0 8px;
|
border-spacing: 0 12px;
|
||||||
margin-top: 1.5rem;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.friendly-table thead th {
|
||||||
.table th {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
border: none;
|
||||||
padding: 1rem;
|
|
||||||
color: #6c757d;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
letter-spacing: 1px;
|
color: var(--text-heading);
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
padding: 0 1.5rem 0.5rem 1.5rem;
|
||||||
}
|
}
|
||||||
|
.friendly-table tbody tr {
|
||||||
.table td {
|
background: var(--bg-card);
|
||||||
background: #fff;
|
box-shadow: var(--shadow);
|
||||||
padding: 1rem;
|
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;
|
border: none;
|
||||||
}
|
}
|
||||||
|
.btn-icon-soft:hover {
|
||||||
.table tr td:first-child { border-radius: 12px 0 0 12px; }
|
background: var(--accent-color);
|
||||||
.table tr td:last-child { border-radius: 0 12px 12px 0; }
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
.form-group {
|
.btn-icon-soft.delete:hover {
|
||||||
margin-bottom: 1.25rem;
|
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 {
|
/* Bootstrap Utility Overrides for Dark Mode */
|
||||||
display: block;
|
[data-theme="dark"] .bg-white {
|
||||||
margin-bottom: 0.5rem;
|
background-color: var(--bg-card) !important;
|
||||||
font-weight: 600;
|
color: var(--text-primary) !important;
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
}
|
||||||
|
[data-theme="dark"] .bg-light {
|
||||||
.form-control {
|
background-color: var(--bg-body) !important;
|
||||||
width: 100%;
|
color: var(--text-primary) !important;
|
||||||
padding: 0.75rem 1rem;
|
}
|
||||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
[data-theme="dark"] .text-muted {
|
||||||
border-radius: 12px;
|
color: var(--text-secondary) !important;
|
||||||
background: #fff;
|
}
|
||||||
transition: all 0.3s ease;
|
[data-theme="dark"] .text-dark {
|
||||||
box-sizing: border-box;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #23a6d5;
|
|
||||||
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
|
|
||||||
}
|
|
||||||
BIN
assets/images/ads/ad_699bf2574849b.jpeg
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
assets/images/ads/ad_699bf270e9a69.jpeg
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
assets/images/ads/ad_699bf288c457c.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
assets/images/categories/cat_699ad97b80a9f.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
assets/images/categories/cat_699ad9936c7f7.jpeg
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
assets/images/categories/cat_699ad9aca9529.jpeg
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
assets/images/categories/cat_699ad9c24cfdf.jpeg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/images/company/favicon_699ada16cc653.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
assets/images/company/favicon_699b1c9b2edeb.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
assets/images/company/favicon_699b1ccab91a1.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
assets/images/company/favicon_699d0d4e7a2f6.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
assets/images/company/logo_699ada16cc482.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
assets/images/company/logo_699d0d4e79490.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
assets/images/products/prod_699da7aa10ada.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
assets/images/products/prod_699dab0588305.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
assets/images/users/user_1_699c5922f2b0b.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/images/users/user_1_699d76ecd9c95.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
1030
assets/js/main.js
BIN
assets/pasted-20260224-165424-865370bd.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
143
customer_profile.php
Normal 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
@ -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
@ -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);
|
||||||
@ -15,3 +15,23 @@ function db() {
|
|||||||
}
|
}
|
||||||
return $pdo;
|
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
@ -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
@ -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";
|
||||||
|
|
||||||
20
db/migrations/001_add_payments.sql
Normal 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
|
||||||
44
db/migrations/002_add_customer_id_to_orders.sql
Normal 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;
|
||||||
15
db/migrations/003_add_payment_type_to_orders.sql
Normal 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;
|
||||||
11
db/migrations/004_seed_payment_types.sql
Normal 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;
|
||||||
28
db/migrations/005_loyalty_schema.sql
Normal 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);
|
||||||
8
db/migrations/006_integration_settings.sql
Normal 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)
|
||||||
|
);
|
||||||
3
db/migrations/007_add_loyalty_payment_type.sql
Normal 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');
|
||||||
28
db/migrations/008_user_system.sql
Normal 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.
|
||||||
7
db/migrations/009_user_outlets.sql
Normal 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
|
||||||
|
);
|
||||||
8
db/migrations/010_ads_images.sql
Normal 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
|
||||||
|
);
|
||||||
2
db/migrations/011_add_user_id_to_orders.sql
Normal 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;
|
||||||
18
db/migrations/012_expenses_schema.sql
Normal 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)
|
||||||
|
);
|
||||||
4
db/migrations/013_product_promotions.sql
Normal 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;
|
||||||
18
db/migrations/014_loyalty_toggle.sql
Normal 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;
|
||||||
11
db/migrations/015_add_redemptions_to_customers.sql
Normal 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'
|
||||||
|
);
|
||||||
2
db/migrations/016_add_profile_pic_to_users.sql
Normal 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;
|
||||||
15
db/migrations/017_attendance_system.sql
Normal 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
|
||||||
|
);
|
||||||
2
db/migrations/018_add_cost_price_to_products.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Add cost_price to products table
|
||||||
|
ALTER TABLE products ADD COLUMN cost_price DECIMAL(10, 2) DEFAULT 0.00;
|
||||||
28
db/migrations/019_purchase_module.sql
Normal 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
|
||||||
|
);
|
||||||
8
db/migrations/020_staff_ratings.sql
Normal 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
|
||||||
|
);
|
||||||