Autosave: 20260304-030410
This commit is contained in:
parent
f08a6b75bb
commit
f8ccd73ba6
22
admin.php
22
admin.php
@ -2,28 +2,6 @@
|
||||
$title = 'dashboard';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Helper functions for status colors
|
||||
if (!function_exists('getStatusColor')) {
|
||||
function getStatusColor($status) {
|
||||
return [
|
||||
'received' => 'secondary',
|
||||
'processing' => 'primary',
|
||||
'ready' => 'success',
|
||||
'delivered' => 'dark',
|
||||
'cancelled' => 'danger',
|
||||
][$status] ?? 'info';
|
||||
}
|
||||
}
|
||||
if (!function_exists('getPaymentStatusColor')) {
|
||||
function getPaymentStatusColor($status) {
|
||||
return [
|
||||
'unpaid' => 'danger',
|
||||
'partially_paid' => 'warning',
|
||||
'paid' => 'success',
|
||||
][$status] ?? 'info';
|
||||
}
|
||||
}
|
||||
|
||||
// Stats logic
|
||||
$branch_id = $_SESSION['branch_id'];
|
||||
$is_super = ($current_role === 'super_admin');
|
||||
|
||||
@ -2,4 +2,4 @@
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
localhost FALSE / FALSE 0 PHPSESSID 7ksl899v5vo08i9all8u0pma2i
|
||||
127.0.0.1 FALSE / FALSE 0 PHPSESSID nfojd941ukr507ug78gdo5ccik
|
||||
|
||||
@ -80,4 +80,26 @@ function set_setting($key, $value) {
|
||||
function has_permission($action, $page = null, $user_id = null) {
|
||||
$perms = check_permission($page, $user_id);
|
||||
return !empty($perms[$action]);
|
||||
}
|
||||
|
||||
if (!function_exists('getStatusColor')) {
|
||||
function getStatusColor($status) {
|
||||
return [
|
||||
'received' => 'secondary',
|
||||
'processing' => 'primary',
|
||||
'ready' => 'success',
|
||||
'delivered' => 'dark',
|
||||
'cancelled' => 'danger',
|
||||
][$status] ?? 'info';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('getPaymentStatusColor')) {
|
||||
function getPaymentStatusColor($status) {
|
||||
return [
|
||||
'unpaid' => 'danger',
|
||||
'partially_paid' => 'warning',
|
||||
'paid' => 'success',
|
||||
][$status] ?? 'info';
|
||||
}
|
||||
}
|
||||
3
db/migrations/18_add_is_deleted_to_items.sql
Normal file
3
db/migrations/18_add_is_deleted_to_items.sql
Normal file
@ -0,0 +1,3 @@
|
||||
ALTER TABLE `services` ADD COLUMN `is_deleted` TINYINT(1) DEFAULT 0;
|
||||
ALTER TABLE `items` ADD COLUMN `is_deleted` TINYINT(1) DEFAULT 0;
|
||||
ALTER TABLE `categories` ADD COLUMN `is_deleted` TINYINT(1) DEFAULT 0;
|
||||
816
items.php
816
items.php
@ -1,17 +1,81 @@
|
||||
<?php
|
||||
ob_start();
|
||||
session_start();
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
require_once __DIR__ . '/includes/lang.php';
|
||||
|
||||
$action = $_POST['action'] ?? $_GET['action'] ?? '';
|
||||
$isAjax = isset($_POST['ajax']) || isset($_GET['ajax']) || (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest');
|
||||
|
||||
require_once __DIR__ . '/db/config.php';
|
||||
if (!has_permission('view')) {
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Access Denied']);
|
||||
exit;
|
||||
}
|
||||
die('Access Denied');
|
||||
}
|
||||
|
||||
// Action handling
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
$action = $_POST['action'];
|
||||
|
||||
if ($action === 'add_category') {
|
||||
if ($action === 'add_item') {
|
||||
$name_en = $_POST['name_en'];
|
||||
$name_ar = $_POST['name_ar'];
|
||||
$category_id = $_POST['category_id'] ?: null;
|
||||
$vat_percent = (float)($_POST['vat_percent'] ?? 15.00);
|
||||
$image_url = null;
|
||||
|
||||
// Handle Image Upload
|
||||
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
||||
$ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
|
||||
$filename = 'item_' . uniqid('', true) . '.' . $ext;
|
||||
$upload_dir = __DIR__ . '/assets/images/items/';
|
||||
if (!is_dir($upload_dir)) mkdir($upload_dir, 0775, true);
|
||||
if (move_uploaded_file($_FILES['image']['tmp_name'], $upload_dir . $filename)) {
|
||||
$image_url = 'assets/images/items/' . $filename;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = db()->prepare("INSERT INTO items (name_en, name_ar, category_id, vat_percent, image_url) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$name_en, $name_ar, $category_id, $vat_percent, $image_url]);
|
||||
|
||||
header('Location: items.php?success=item_added');
|
||||
exit;
|
||||
} elseif ($action === 'edit_item') {
|
||||
$id = $_POST['id'];
|
||||
$name_en = $_POST['name_en'];
|
||||
$name_ar = $_POST['name_ar'];
|
||||
$category_id = $_POST['category_id'] ?: null;
|
||||
$vat_percent = (float)($_POST['vat_percent'] ?? 15.00);
|
||||
$image_url = $_POST['current_image_url'] ?? null;
|
||||
|
||||
// Handle Image Upload
|
||||
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
||||
$ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
|
||||
$filename = 'item_' . uniqid('', true) . '.' . $ext;
|
||||
$upload_dir = __DIR__ . '/assets/images/items/';
|
||||
if (!is_dir($upload_dir)) mkdir($upload_dir, 0775, true);
|
||||
if (move_uploaded_file($_FILES['image']['tmp_name'], $upload_dir . $filename)) {
|
||||
$image_url = 'assets/images/items/' . $filename;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = db()->prepare("UPDATE items SET name_en = ?, name_ar = ?, category_id = ?, vat_percent = ?, image_url = ? WHERE id = ?");
|
||||
$stmt->execute([$name_en, $name_ar, $category_id, $vat_percent, $image_url, $id]);
|
||||
|
||||
header('Location: items.php?success=item_updated');
|
||||
exit;
|
||||
} elseif ($action === 'delete_item') {
|
||||
$id = $_POST['id'];
|
||||
try {
|
||||
$stmt = db()->prepare("DELETE FROM items WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
header('Location: items.php?success=item_deleted');
|
||||
exit;
|
||||
} catch (PDOException $e) {
|
||||
header('Location: items.php?error=cannot_delete_item');
|
||||
exit;
|
||||
}
|
||||
} elseif ($action === 'add_category') {
|
||||
$name_en = $_POST['cat_name_en'];
|
||||
$name_ar = $_POST['cat_name_ar'];
|
||||
$stmt = db()->prepare("INSERT INTO categories (name_en, name_ar) VALUES (?, ?)");
|
||||
@ -19,20 +83,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json');
|
||||
$categories = db()->query("SELECT * FROM categories ORDER BY name_en ASC")->fetchAll();
|
||||
ob_start();
|
||||
foreach($categories as $cat): ?>
|
||||
<tr>
|
||||
<td><?= $cat['name_en'] ?></td>
|
||||
<td><?= $cat['name_ar'] ?></td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-light p-1 me-1" onclick='editCategory(<?= json_encode($cat) ?>)'><i class="bi bi-pencil text-primary"></i></button>
|
||||
<button class="btn btn-sm btn-light p-1" onclick="confirmDeleteAjax('category', <?= $cat['id'] ?>)"><i class="bi bi-trash text-danger"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach;
|
||||
$html = ob_get_clean();
|
||||
echo json_encode(['success' => true, 'html' => $html]);
|
||||
echo json_encode(['success' => true, 'html' => renderCategoryList($lang)]);
|
||||
exit;
|
||||
}
|
||||
header('Location: items.php?success=category_added');
|
||||
@ -46,103 +97,36 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json');
|
||||
$categories = db()->query("SELECT * FROM categories ORDER BY name_en ASC")->fetchAll();
|
||||
ob_start();
|
||||
foreach($categories as $cat): ?>
|
||||
<tr>
|
||||
<td><?= $cat['name_en'] ?></td>
|
||||
<td><?= $cat['name_ar'] ?></td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-light p-1 me-1" onclick='editCategory(<?= json_encode($cat) ?>)'><i class="bi bi-pencil text-primary"></i></button>
|
||||
<button class="btn btn-sm btn-light p-1" onclick="confirmDeleteAjax('category', <?= $cat['id'] ?>)"><i class="bi bi-trash text-danger"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach;
|
||||
$html = ob_get_clean();
|
||||
echo json_encode(['success' => true, 'html' => $html]);
|
||||
echo json_encode(['success' => true, 'html' => renderCategoryList($lang)]);
|
||||
exit;
|
||||
}
|
||||
header('Location: items.php?success=category_updated');
|
||||
exit;
|
||||
} elseif ($action === 'delete_category') {
|
||||
$id = $_POST['id'];
|
||||
$stmt = db()->prepare("DELETE FROM categories WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
try {
|
||||
// Before deleting category, set items to null category
|
||||
$stmt = db()->prepare("UPDATE items SET category_id = NULL WHERE category_id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$stmt = db()->prepare("DELETE FROM categories WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json');
|
||||
$categories = db()->query("SELECT * FROM categories ORDER BY name_en ASC")->fetchAll();
|
||||
ob_start();
|
||||
foreach($categories as $cat): ?>
|
||||
<tr>
|
||||
<td><?= $cat['name_en'] ?></td>
|
||||
<td><?= $cat['name_ar'] ?></td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-light p-1 me-1" onclick='editCategory(<?= json_encode($cat) ?>)'><i class="bi bi-pencil text-primary"></i></button>
|
||||
<button class="btn btn-sm btn-light p-1" onclick="confirmDeleteAjax('category', <?= $cat['id'] ?>)"><i class="bi bi-trash text-danger"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach;
|
||||
$html = ob_get_clean();
|
||||
echo json_encode(['success' => true, 'html' => $html]);
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => true, 'html' => renderCategoryList($lang)]);
|
||||
exit;
|
||||
}
|
||||
header('Location: items.php?success=category_deleted');
|
||||
exit;
|
||||
} catch (PDOException $e) {
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'error' => 'Cannot delete category.']);
|
||||
exit;
|
||||
}
|
||||
header('Location: items.php?error=cannot_delete_category');
|
||||
exit;
|
||||
}
|
||||
header('Location: items.php?success=category_deleted');
|
||||
exit;
|
||||
} elseif ($action === 'add_item') {
|
||||
$category_id = $_POST['category_id'] ?: null;
|
||||
$name_en = $_POST['name_en'];
|
||||
$name_ar = $_POST['name_ar'];
|
||||
$vat_percent = $_POST['vat_percent'] ?: 0;
|
||||
|
||||
$image_url = null;
|
||||
if (isset($_FILES['image']) && $_FILES['image']['error'] === 0) {
|
||||
$ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
|
||||
$filename = 'item_' . uniqid('', true) . '.' . $ext;
|
||||
$upload_dir = __DIR__ . '/assets/images/items/';
|
||||
if (!is_dir($upload_dir)) mkdir($upload_dir, 0777, true);
|
||||
if (move_uploaded_file($_FILES['image']['tmp_name'], $upload_dir . $filename)) {
|
||||
$image_url = 'assets/images/items/' . $filename;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = db()->prepare("INSERT INTO items (category_id, name_en, name_ar, vat_percent, image_url) VALUES (?, ?, ?, ?, ?)");
|
||||
$stmt->execute([$category_id, $name_en, $name_ar, $vat_percent, $image_url]);
|
||||
header('Location: items.php?success=item_added');
|
||||
exit;
|
||||
} elseif ($action === 'edit_item') {
|
||||
$id = $_POST['id'];
|
||||
$category_id = $_POST['category_id'] ?: null;
|
||||
$name_en = $_POST['name_en'];
|
||||
$name_ar = $_POST['name_ar'];
|
||||
$vat_percent = $_POST['vat_percent'] ?: 0;
|
||||
|
||||
$image_url = null;
|
||||
if (isset($_FILES['image']) && $_FILES['image']['error'] === 0) {
|
||||
$ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
|
||||
$filename = 'item_' . uniqid('', true) . '.' . $ext;
|
||||
$upload_dir = __DIR__ . '/assets/images/items/';
|
||||
if (!is_dir($upload_dir)) mkdir($upload_dir, 0777, true);
|
||||
if (move_uploaded_file($_FILES['image']['tmp_name'], $upload_dir . $filename)) {
|
||||
$image_url = 'assets/images/items/' . $filename;
|
||||
}
|
||||
}
|
||||
|
||||
if ($image_url) {
|
||||
$stmt = db()->prepare("UPDATE items SET category_id = ?, name_en = ?, name_ar = ?, vat_percent = ?, image_url = ? WHERE id = ?");
|
||||
$stmt->execute([$category_id, $name_en, $name_ar, $vat_percent, $image_url, $id]);
|
||||
} else {
|
||||
$stmt = db()->prepare("UPDATE items SET category_id = ?, name_en = ?, name_ar = ?, vat_percent = ? WHERE id = ?");
|
||||
$stmt->execute([$category_id, $name_en, $name_ar, $vat_percent, $id]);
|
||||
}
|
||||
header('Location: items.php?success=item_updated');
|
||||
exit;
|
||||
} elseif ($action === 'delete_item') {
|
||||
$id = $_POST['id'];
|
||||
$stmt = db()->prepare("DELETE FROM items WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
header('Location: items.php?success=item_deleted');
|
||||
exit;
|
||||
} elseif ($action === 'add_service') {
|
||||
$name_en = $_POST['svc_name_en'];
|
||||
$name_ar = $_POST['svc_name_ar'];
|
||||
@ -172,16 +156,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
exit;
|
||||
} elseif ($action === 'delete_service') {
|
||||
$id = $_POST['id'];
|
||||
$stmt = db()->prepare("DELETE FROM services WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
try {
|
||||
$stmt = db()->prepare("DELETE FROM services WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => true, 'html' => renderServiceList($lang)]);
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => true, 'html' => renderServiceList($lang)]);
|
||||
exit;
|
||||
}
|
||||
header('Location: items.php?success=service_deleted');
|
||||
exit;
|
||||
} catch (PDOException $e) {
|
||||
$stmt = db()->prepare("UPDATE services SET is_deleted = 1 WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => true, 'html' => renderServiceList($lang)]);
|
||||
exit;
|
||||
}
|
||||
header('Location: items.php?success=service_deleted');
|
||||
exit;
|
||||
}
|
||||
header('Location: items.php?success=service_deleted');
|
||||
exit;
|
||||
} elseif ($action === 'update_price') {
|
||||
$item_id = $_POST['item_id'];
|
||||
$service_id = $_POST['service_id'];
|
||||
@ -201,19 +197,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
// Handle deletion if price is empty
|
||||
if ($price === '') {
|
||||
try {
|
||||
if ($branch_id) {
|
||||
// Delete branch-specific price
|
||||
$stmt = db()->prepare("DELETE FROM prices WHERE item_id = ? AND service_id = ? AND branch_id = ?");
|
||||
$stmt->execute([$item_id, $service_id, $branch_id]);
|
||||
|
||||
// Also try to delete global price if it's the one currently showing or if we want it completely gone
|
||||
// Most users expect that clicking "Delete" removes the item/service association they see.
|
||||
$stmtGlobal = db()->prepare("DELETE FROM prices WHERE item_id = ? AND service_id = ? AND branch_id IS NULL");
|
||||
$stmtGlobal->execute([$item_id, $service_id]);
|
||||
} else {
|
||||
$stmt = db()->prepare("DELETE FROM prices WHERE item_id = ? AND service_id = ? AND branch_id IS NULL");
|
||||
$stmt->execute([$item_id, $service_id]);
|
||||
}
|
||||
// Delete ALL prices (both global and branch-specific) for this item and service.
|
||||
// The user clicking "X" intends to remove this service from the item entirely.
|
||||
$stmt = db()->prepare("DELETE FROM prices WHERE item_id = ? AND service_id = ?");
|
||||
$stmt->execute([$item_id, $service_id]);
|
||||
|
||||
if ($isAjax) {
|
||||
header('Content-Type: application/json');
|
||||
@ -284,46 +271,40 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
||||
$title = 'items';
|
||||
require_once __DIR__ . '/includes/header.php';
|
||||
|
||||
// Filter logic
|
||||
// Search and Category Filter
|
||||
$search = $_GET['search'] ?? '';
|
||||
$category_filter = $_GET['category_id'] ?? '';
|
||||
$cat_id = $_GET['category_id'] ?? '';
|
||||
|
||||
$categories = db()->query("SELECT * FROM categories ORDER BY name_en ASC")->fetchAll();
|
||||
$current_branch_id = $_SESSION['branch_id'] ?? null;
|
||||
|
||||
$query = "SELECT i.*, c.name_en as cat_en, c.name_ar as cat_ar
|
||||
FROM items i
|
||||
LEFT JOIN categories c ON i.category_id = c.id";
|
||||
$params = [];
|
||||
// Fetch categories
|
||||
$categories = db()->query("SELECT * FROM categories WHERE is_deleted = 0 ORDER BY name_en ASC")->fetchAll();
|
||||
|
||||
// Fetch services
|
||||
$services = db()->query("SELECT * FROM services WHERE is_deleted = 0 ORDER BY name_en ASC")->fetchAll();
|
||||
|
||||
// Fetch items with filters
|
||||
$where = [];
|
||||
|
||||
$params = [];
|
||||
if ($search) {
|
||||
$where[] = "(i.name_en LIKE ? OR i.name_ar LIKE ?)";
|
||||
$where[] = "(name_en LIKE ? OR name_ar LIKE ?)";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
|
||||
if ($category_filter) {
|
||||
$where[] = "i.category_id = ?";
|
||||
$params[] = $category_filter;
|
||||
if ($cat_id) {
|
||||
$where[] = "category_id = ?";
|
||||
$params[] = $cat_id;
|
||||
}
|
||||
|
||||
if ($where) {
|
||||
$query .= " WHERE " . implode(" AND ", $where);
|
||||
}
|
||||
|
||||
$query .= " ORDER BY i.name_en ASC";
|
||||
$stmt = db()->prepare($query);
|
||||
$where[] = "is_deleted = 0";
|
||||
$where_sql = $where ? "WHERE " . implode(" AND ", $where) : "";
|
||||
$stmt = db()->prepare("SELECT * FROM items $where_sql ORDER BY name_en ASC");
|
||||
$stmt->execute($params);
|
||||
$items = $stmt->fetchAll();
|
||||
|
||||
$services = db()->query("SELECT * FROM services ORDER BY name_en ASC")->fetchAll();
|
||||
|
||||
// Fetch prices based on current branch
|
||||
$current_branch_id = $_SESSION['branch_id'] ?? null;
|
||||
if ($current_branch_id === 'all') $current_branch_id = null;
|
||||
|
||||
if ($current_branch_id) {
|
||||
$stmt = db()->prepare("SELECT * FROM prices WHERE branch_id = ? OR branch_id IS NULL ORDER BY branch_id DESC"); // Branch-specific first
|
||||
// Fetch prices
|
||||
if ($current_branch_id && $current_branch_id !== 'all') {
|
||||
$stmt = db()->prepare("SELECT * FROM prices WHERE branch_id = ? OR branch_id IS NULL ORDER BY branch_id DESC");
|
||||
$stmt->execute([$current_branch_id]);
|
||||
} else {
|
||||
$stmt = db()->prepare("SELECT * FROM prices WHERE branch_id IS NULL");
|
||||
@ -338,8 +319,24 @@ foreach ($prices_raw as $p) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderCategoryList($lang) {
|
||||
$categories = db()->query("SELECT * FROM categories WHERE is_deleted = 0 ORDER BY name_en ASC")->fetchAll();
|
||||
ob_start();
|
||||
foreach($categories as $cat): ?>
|
||||
<tr>
|
||||
<td><?= $cat['name_en'] ?></td>
|
||||
<td><?= $cat['name_ar'] ?></td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-light p-1 me-1" onclick='editCategory(<?= json_encode($cat) ?>)'><i class="bi bi-pencil text-primary"></i></button>
|
||||
<button class="btn btn-sm btn-light p-1" onclick="confirmDeleteAjax('category', <?= $cat['id'] ?>)"><i class="bi bi-trash text-danger"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach;
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
function renderServiceList($lang) {
|
||||
$services = db()->query("SELECT * FROM services ORDER BY name_en ASC")->fetchAll();
|
||||
$services = db()->query("SELECT * FROM services WHERE is_deleted = 0 ORDER BY name_en ASC")->fetchAll();
|
||||
ob_start();
|
||||
foreach($services as $svc): ?>
|
||||
<tr>
|
||||
@ -375,154 +372,178 @@ function renderServiceList($lang) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm mb-4" style="border-radius: 20px;">
|
||||
<div class="card-body p-4">
|
||||
<form method="GET" class="row g-3">
|
||||
<div class="col-md-5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-light border-0"><i class="bi bi-search"></i></span>
|
||||
<input type="text" name="search" class="form-control bg-light border-0" placeholder="<?= __('search_items') ?>" value="<?= htmlspecialchars($search) ?>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<select name="category_id" class="form-select bg-light border-0">
|
||||
<option value=""><?= __('all_categories') ?></option>
|
||||
<?php foreach($categories as $cat): ?>
|
||||
<option value="<?= $cat['id'] ?>" <?= $category_filter == $cat['id'] ? 'selected' : '' ?> >
|
||||
<?= $lang === 'ar' ? $cat['name_ar'] : $cat['name_en'] ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="submit" class="btn btn-dark w-100" style="border-radius: 12px;"><?= __('filter') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<?php foreach($items as $item):
|
||||
?><div class="col-md-6 col-lg-4 col-xl-3">
|
||||
<div class="card h-100 border-0 shadow-sm item-card" style="border-radius: 20px; overflow: hidden;">
|
||||
<div style="height: 180px; overflow: hidden; position: relative;">
|
||||
<?php if ($item['image_url']): ?>
|
||||
<img src="<?= htmlspecialchars($item['image_url']) ?>" class="card-img-top h-100 w-100 object-fit-cover" alt="<?= htmlspecialchars($item['name_en']) ?>">
|
||||
<?php else:
|
||||
?><div class="bg-light h-100 w-100 d-flex align-items-center justify-content-center">
|
||||
<i class="bi bi-image text-muted opacity-25" style="font-size: 3rem;"></i>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="position-absolute top-0 end-0 p-3">
|
||||
<span class="badge bg-white text-dark shadow-sm py-2 px-3" style="border-radius: 10px;">
|
||||
<?= htmlspecialchars($item['cat_en'] ?? __('uncategorized')) ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<h5 class="fw-bold mb-1"><?= htmlspecialchars($item['name_en']) ?></h5>
|
||||
<p class="text-muted small mb-3"><?= htmlspecialchars($item['name_ar']) ?></p>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mt-auto">
|
||||
<div class="text-primary small fw-bold">
|
||||
<?= count($services) ?> <?= __('services') ?>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-sm btn-light p-2" style="border-radius: 8px;" data-bs-toggle="modal" data-bs-target="#pricingModal<?= $item['id'] ?>">
|
||||
<i class="bi bi-tag-fill text-primary"></i>
|
||||
</button>
|
||||
<?php if (has_permission('edit')): ?>
|
||||
<button class="btn btn-sm btn-light p-2 mx-1" style="border-radius: 8px;" onclick="editItem(<?= htmlspecialchars(json_encode($item), ENT_QUOTES) ?>)">
|
||||
<i class="bi bi-pencil text-primary"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (has_permission('delete')): ?>
|
||||
<button class="btn btn-sm btn-light p-2" style="border-radius: 8px;" onclick="confirmDeleteAjax('item', <?= $item['id'] ?>)">
|
||||
<i class="bi bi-trash text-danger"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Filters -->
|
||||
<div class="card p-3 mb-4 border-0 shadow-sm" style="border-radius: 20px;">
|
||||
<form class="row g-3">
|
||||
<div class="col-md-5">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text bg-white border-0"><i class="bi bi-search"></i></span>
|
||||
<input type="text" name="search" class="form-control border-0" placeholder="<?= __('search_items') ?>" value="<?= htmlspecialchars($search) ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<div class="col-md-4">
|
||||
<select name="category_id" class="form-select border-0">
|
||||
<option value=""><?= __('all_categories') ?></option>
|
||||
<?php foreach($categories as $cat): ?>
|
||||
<option value="<?= $cat['id'] ?>" <?= $cat_id == $cat['id'] ? 'selected' : '' ?>>
|
||||
<?= $lang === 'ar' ? $cat['name_ar'] : $cat['name_en'] ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button type="submit" class="btn btn-primary w-100 fw-bold rounded-4 h-100"><?= __('filter') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modals -->
|
||||
<!-- Items List -->
|
||||
<div class="card border-0 shadow-sm mb-4" style="border-radius: 20px; overflow: hidden;">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4 py-3 border-0" style="width: 80px;"><?= __('image') ?? 'Image' ?></th>
|
||||
<th class="py-3 border-0"><?= __('name') ?? 'Name' ?></th>
|
||||
<th class="py-3 border-0"><?= __('category') ?? 'Category' ?></th>
|
||||
<th class="py-3 border-0"><?= __('services') ?? 'Services' ?></th>
|
||||
<th class="pe-4 py-3 border-0 text-end"><?= __('actions') ?? 'Actions' ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach($items as $item): ?>
|
||||
<tr>
|
||||
<td class="ps-4 py-3 border-bottom">
|
||||
<?php if ($item['image_url']): ?>
|
||||
<img src="<?= $item['image_url'] ?>" class="rounded-3 shadow-sm" style="width: 50px; height: 50px; object-fit: cover;">
|
||||
<?php else: ?>
|
||||
<div class="bg-light rounded-3 d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
||||
<i class="bi bi-image text-muted"></i>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="py-3 border-bottom fw-bold">
|
||||
<?= $lang === 'ar' ? $item['name_ar'] : $item['name_en'] ?>
|
||||
</td>
|
||||
<td class="py-3 border-bottom text-muted small">
|
||||
<?php
|
||||
$cat_name = '';
|
||||
foreach($categories as $cat) {
|
||||
if ($cat['id'] == $item['category_id']) {
|
||||
$cat_name = $lang === 'ar' ? $cat['name_ar'] : $cat['name_en'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
echo $cat_name ?: __('uncategorized');
|
||||
?>
|
||||
</td>
|
||||
<td class="py-3 border-bottom">
|
||||
<button class="btn btn-sm btn-primary text-white fw-bold rounded-pill px-3 shadow-sm d-inline-flex align-items-center gap-1" data-bs-toggle="modal" data-bs-target="#pricingModal<?= $item['id'] ?>">
|
||||
<i class="bi bi-tags"></i>
|
||||
<?= count($prices[$item['id']] ?? []) ?> <?= __('services') ?>
|
||||
</button>
|
||||
</td>
|
||||
<td class="pe-4 py-3 border-bottom text-end">
|
||||
<div class="d-inline-flex gap-1">
|
||||
<?php if (has_permission('edit')): ?>
|
||||
<button class="btn btn-sm btn-light p-2 rounded-3 text-primary border border-primary border-opacity-25" onclick='editItem(<?= json_encode($item) ?>)' title="<?= __('edit') ?>">
|
||||
<i class="bi bi-pencil px-1"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if (has_permission('delete')): ?>
|
||||
<button class="btn btn-sm btn-light p-2 rounded-3 text-danger border border-danger border-opacity-25" onclick="confirmDeleteItem(<?= $item['id'] ?>)" title="<?= __('delete') ?>">
|
||||
<i class="bi bi-trash px-1"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (empty($items)): ?>
|
||||
<div class="text-center p-5">
|
||||
<i class="bi bi-inbox text-muted" style="font-size: 4rem;"></i>
|
||||
<p class="text-muted mt-3"><?= __('no_items_found') ?></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Item Modal -->
|
||||
<div class="modal fade" id="itemModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content" style="border-radius: 25px;">
|
||||
<div class="modal-header border-0 p-4">
|
||||
<h5 class="modal-title fw-bold" id="itemModalLabel"><?= __('add_item') ?></h5>
|
||||
<div class="modal-content" style="border-radius: 30px;">
|
||||
<div class="modal-header border-0 p-4 pb-0">
|
||||
<h5 class="modal-title fw-bold" id="itemModalTitle"><?= __('add_new_item') ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4 pt-0">
|
||||
<form id="itemForm" method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="action" id="itemAction" value="add_item">
|
||||
<input type="hidden" name="id" id="itemId">
|
||||
|
||||
<div class="text-center mb-4">
|
||||
<form id="itemForm" method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="action" id="itemAction" value="add_item">
|
||||
<input type="hidden" name="id" id="itemId">
|
||||
<input type="hidden" name="current_image_url" id="itemCurrentImageUrl">
|
||||
<div class="modal-body p-4">
|
||||
<div class="mb-4 text-center">
|
||||
<div id="imagePreviewContainer" class="d-none mb-3">
|
||||
<img id="imagePreview" src="" class="rounded-4 shadow-sm" style="max-height: 150px; width: auto;">
|
||||
<img id="imagePreview" src="" class="rounded-4 shadow-sm" style="max-height: 150px; max-width: 100%; object-fit: cover;">
|
||||
</div>
|
||||
<label class="btn btn-light w-100 py-3 mb-0" style="border-radius: 15px; border: 2px dashed #dee2e6;">
|
||||
<i class="bi bi-cloud-arrow-up fs-4 d-block mb-1"></i>
|
||||
<span class="small text-muted"><?= __('upload_image') ?></span>
|
||||
<label for="itemImageFile" class="btn btn-light rounded-4 px-4 py-3 w-100 border-2 border-dashed" style="border-style: dashed !important;">
|
||||
<i class="bi bi-cloud-upload me-2 fs-4"></i>
|
||||
<div class="small fw-bold"><?= __('upload_image') ?></div>
|
||||
<input type="file" name="image" id="itemImageFile" class="d-none" accept="image/*">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold"><?= __('name_en') ?></label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="name_en" id="itemNameEn" class="form-control" placeholder="Item Name (English)" required>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="translateField('itemNameEn', 'itemNameAr', 'en-ar')">
|
||||
<i class="bi bi-translate"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold"><?= __('name_ar') ?></label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="name_ar" id="itemNameAr" class="form-control text-end" placeholder="اسم الصنف (بالعربي)" required>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="translateField('itemNameAr', 'itemNameEn', 'ar-en')">
|
||||
<i class="bi bi-translate"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-bold"><?= __('category') ?></label>
|
||||
<select name="category_id" id="itemCategory" class="form-select" required>
|
||||
<option value=""><?= __('select_category') ?></option>
|
||||
<?php foreach($categories as $cat):
|
||||
?><option value="<?= $cat['id'] ?>"><?= $lang === 'ar' ? $cat['name_ar'] : $cat['name_en'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label small fw-bold"><?= __('vat_percent') ?> (%)</label>
|
||||
<input type="number" step="0.01" name="vat_percent" id="itemVat" class="form-control" placeholder="0.00">
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-light w-100 py-2 fw-bold" style="border-radius: 12px;" data-bs-dismiss="modal"><?= __('cancel') ?></button>
|
||||
<button type="submit" class="btn btn-primary w-100 py-2 fw-bold" style="border-radius: 12px;"><?= __('save') ?></button>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold"><?= __('name_en') ?></label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="name_en" id="itemNameEn" class="form-control" required style="border-radius: 12px 0 0 12px;">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="translateField('itemNameEn', 'itemNameAr', 'en-ar')" style="border-radius: 0 12px 12px 0;">
|
||||
<i class="bi bi-translate"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small fw-bold"><?= __('name_ar') ?></label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="name_ar" id="itemNameAr" class="form-control text-end" required style="border-radius: 12px 0 0 12px;">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="translateField('itemNameAr', 'itemNameEn', 'ar-en')" style="border-radius: 0 12px 12px 0;">
|
||||
<i class="bi bi-translate"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label small fw-bold"><?= __('category') ?></label>
|
||||
<select name="category_id" id="itemCategoryId" class="form-select" style="border-radius: 12px;">
|
||||
<option value=""><?= __('select_category') ?></option>
|
||||
<?php foreach($categories as $cat): ?>
|
||||
<option value="<?= $cat['id'] ?>"><?= $lang === 'ar' ? $cat['name_ar'] : $cat['name_en'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small fw-bold"><?= __('vat_percent') ?></label>
|
||||
<input type="number" step="0.01" name="vat_percent" id="itemVatPercent" class="form-control" value="15.00" style="border-radius: 12px;">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-0">
|
||||
<button type="button" class="btn btn-light rounded-4 px-4" data-bs-dismiss="modal"><?= __('cancel') ?></button>
|
||||
<button type="submit" class="btn btn-primary rounded-4 px-4 fw-bold shadow-sm"><?= __('save') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Categories Modal -->
|
||||
<div class="modal fade" id="categoriesModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content" style="border-radius: 25px;">
|
||||
<div class="modal-header border-0 p-4">
|
||||
<h5 class="modal-title fw-bold"><?= __('manage_categories') ?></h5>
|
||||
<h5 class="modal-title fw-bold"><?= __('categories_management') ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4 pt-0">
|
||||
@ -570,16 +591,7 @@ function renderServiceList($lang) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="categoryTableBody">
|
||||
<?php foreach($categories as $cat): ?>
|
||||
<tr>
|
||||
<td><?= $cat['name_en'] ?></td>
|
||||
<td><?= $cat['name_ar'] ?></td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-light p-1 me-1" onclick='editCategory(<?= json_encode($cat) ?>)'><i class="bi bi-pencil text-primary"></i></button>
|
||||
<button class="btn btn-sm btn-light p-1" onclick="confirmDeleteAjax('category', <?= $cat['id'] ?>)"><i class="bi bi-trash text-danger"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?= renderCategoryList($lang) ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -592,10 +604,10 @@ function renderServiceList($lang) {
|
||||
|
||||
<!-- Services Modal -->
|
||||
<div class="modal fade" id="servicesModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content" style="border-radius: 25px;">
|
||||
<div class="modal-header border-0 p-4">
|
||||
<h5 class="modal-title fw-bold"><?= __('manage_services') ?></h5>
|
||||
<h5 class="modal-title fw-bold"><?= __('services_management') ?></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4 pt-0">
|
||||
@ -670,26 +682,68 @@ function renderServiceList($lang) {
|
||||
<i class="bi bi-info-circle me-2"></i><?= __('prices_saved_automatically') ?>
|
||||
</div>
|
||||
|
||||
<?php foreach($services as $svc): ?>
|
||||
<div class="mb-3 d-flex align-items-center justify-content-between p-3 bg-light" style="border-radius: 15px;">
|
||||
<div class="fw-bold small">
|
||||
<?= $lang === 'ar' ? $svc['name_ar'] : $svc['name_en'] ?>
|
||||
<?php
|
||||
$assigned_services = [];
|
||||
$unassigned_services = [];
|
||||
foreach($services as $svc) {
|
||||
if (isset($prices[$item['id']][$svc['id']])) {
|
||||
$assigned_services[] = $svc;
|
||||
} else {
|
||||
$unassigned_services[] = $svc;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<h6 class="fw-bold mb-3 small text-success"><?= __('assigned_services') ?? 'Assigned Services' ?></h6>
|
||||
<div class="assigned-list">
|
||||
<?php if (empty($assigned_services)): ?>
|
||||
<div class="text-muted small mb-3 fst-italic no-services-msg"><?= __('no_services_assigned') ?? 'No services assigned to this item.' ?></div>
|
||||
<?php endif; ?>
|
||||
<?php foreach($assigned_services as $svc): ?>
|
||||
<div class="mb-3 d-flex align-items-center justify-content-between p-3 bg-white border border-success border-opacity-25 shadow-sm service-row" style="border-radius: 15px;" id="row-<?= $item['id'] ?>-<?= $svc['id'] ?>">
|
||||
<div class="fw-bold small">
|
||||
<?= $lang === 'ar' ? $svc['name_ar'] : $svc['name_en'] ?>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2" style="width: 200px;">
|
||||
<input type="number" step="0.001"
|
||||
class="form-control form-control-sm border bg-light price-input"
|
||||
style="border-radius: 10px;"
|
||||
data-item-id="<?= $item['id'] ?>"
|
||||
data-service-id="<?= $svc['id'] ?>"
|
||||
value="<?= $prices[$item['id']][$svc['id']] ?>"
|
||||
placeholder="0.000">
|
||||
<button type="button" class="btn btn-sm btn-light p-1 remove-btn" style="border-radius: 8px;" onclick="removePrice(this, <?= $item['id'] ?>, <?= $svc['id'] ?>)">
|
||||
<i class="bi bi-x-circle text-danger"></i>
|
||||
</button>
|
||||
<i class="bi bi-check-circle-fill text-success ms-1 success-indicator d-none"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2" style="width: 200px;">
|
||||
<input type="number" step="0.001"
|
||||
class="form-control form-control-sm border-0 bg-white price-input"
|
||||
style="border-radius: 10px;"
|
||||
data-item-id="<?= $item['id'] ?>"
|
||||
data-service-id="<?= $svc['id'] ?>"
|
||||
value="<?= $prices[$item['id']][$svc['id']] ?? '' ?>"
|
||||
placeholder="0.000">
|
||||
<button class="btn btn-sm btn-light p-1" style="border-radius: 8px;" onclick="removePrice(this, <?= $item['id'] ?>, <?= $svc['id'] ?>)">
|
||||
<i class="bi bi-x-circle text-danger"></i>
|
||||
</button>
|
||||
<i class="bi bi-check-circle-fill text-success ms-1 success-indicator d-none"></i>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<h6 class="fw-bold mb-3 mt-4 small text-muted"><?= __('available_services') ?? 'Available Services' ?></h6>
|
||||
<div class="unassigned-list">
|
||||
<?php foreach($unassigned_services as $svc): ?>
|
||||
<div class="mb-3 d-flex align-items-center justify-content-between p-3 bg-light service-row" style="border-radius: 15px;" id="row-<?= $item['id'] ?>-<?= $svc['id'] ?>">
|
||||
<div class="fw-bold small text-muted">
|
||||
<?= $lang === 'ar' ? $svc['name_ar'] : $svc['name_en'] ?>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2" style="width: 200px;">
|
||||
<input type="number" step="0.001"
|
||||
class="form-control form-control-sm border-0 bg-white price-input"
|
||||
style="border-radius: 10px;"
|
||||
data-item-id="<?= $item['id'] ?>"
|
||||
data-service-id="<?= $svc['id'] ?>"
|
||||
value=""
|
||||
placeholder="<?= __('add') ?? 'Add...' ?>">
|
||||
<button type="button" class="btn btn-sm btn-light p-1 remove-btn" style="border-radius: 8px; display: none;" onclick="removePrice(this, <?= $item['id'] ?>, <?= $svc['id'] ?>)">
|
||||
<i class="bi bi-x-circle text-danger"></i>
|
||||
</button>
|
||||
<i class="bi bi-check-circle-fill text-success ms-1 success-indicator d-none"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-dark w-100 py-3 fw-bold mt-3 shadow-sm" style="border-radius: 15px;" data-bs-dismiss="modal">
|
||||
<?= __('done') ?>
|
||||
@ -700,37 +754,28 @@ function renderServiceList($lang) {
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<form id="deleteForm" method="POST" class="d-none">
|
||||
<input type="hidden" name="action" id="deleteAction">
|
||||
<input type="hidden" name="id" id="deleteId">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
async function translateField(sourceId, targetId, direction) {
|
||||
const sourceEl = document.getElementById(sourceId);
|
||||
const targetEl = document.getElementById(targetId);
|
||||
const text = sourceEl.value;
|
||||
if (!text) return;
|
||||
const btn = event.currentTarget;
|
||||
const originalHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
||||
try {
|
||||
const response = await fetch('api/translate.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text, direction })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) { targetEl.value = data.translated; }
|
||||
} catch (e) { console.error(e); }
|
||||
finally { btn.disabled = false; btn.innerHTML = originalHtml; }
|
||||
function translateField(sourceId, targetId, direction) {
|
||||
const text = document.getElementById(sourceId).value;
|
||||
if (!text) return;
|
||||
|
||||
fetch('api/translate.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text, direction })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById(targetId).value = data.translated;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
document.getElementById('itemForm').reset();
|
||||
document.getElementById('itemAction').value = 'add_item';
|
||||
document.getElementById('itemModalLabel').innerText = '<?= __('add_item') ?>';
|
||||
document.getElementById('itemModalTitle').innerText = '<?= __('add_new_item') ?>';
|
||||
document.getElementById('imagePreviewContainer').classList.add('d-none');
|
||||
}
|
||||
|
||||
@ -740,9 +785,10 @@ function editItem(item) {
|
||||
document.getElementById('itemId').value = item.id;
|
||||
document.getElementById('itemNameEn').value = item.name_en;
|
||||
document.getElementById('itemNameAr').value = item.name_ar;
|
||||
document.getElementById('itemCategory').value = item.category_id || '';
|
||||
document.getElementById('itemVat').value = item.vat_percent || '';
|
||||
document.getElementById('itemModalLabel').innerText = '<?= __('edit_item') ?>';
|
||||
document.getElementById('itemCategoryId').value = item.category_id;
|
||||
document.getElementById('itemVatPercent').value = item.vat_percent;
|
||||
document.getElementById('itemCurrentImageUrl').value = item.image_url;
|
||||
document.getElementById('itemModalTitle').innerText = '<?= __('edit_item') ?>';
|
||||
|
||||
if (item.image_url) {
|
||||
document.getElementById('imagePreview').src = item.image_url;
|
||||
@ -752,15 +798,16 @@ function editItem(item) {
|
||||
new bootstrap.Modal(document.getElementById('itemModal')).show();
|
||||
}
|
||||
|
||||
function confirmDeleteAjax(type, id) {
|
||||
function confirmDeleteItem(id) {
|
||||
if (confirm('<?= __('confirm_delete') ?>')) {
|
||||
document.getElementById('deleteAction').value = 'delete_' + type;
|
||||
document.getElementById('deleteId').value = id;
|
||||
document.getElementById('deleteForm').submit();
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.innerHTML = `<input type="hidden" name="action" value="delete_item"><input type="hidden" name="id" value="${id}">`;
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
|
||||
// Category AJAX
|
||||
function editCategory(cat) {
|
||||
document.getElementById('catAction').value = 'edit_category';
|
||||
document.getElementById('catId').value = cat.id;
|
||||
@ -777,24 +824,6 @@ function resetCatForm() {
|
||||
document.getElementById('catCancelBtn').classList.add('d-none');
|
||||
}
|
||||
|
||||
document.getElementById('categoryForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(this);
|
||||
try {
|
||||
const response = await fetch('items.php', { method: 'POST', body: formData });
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
document.getElementById('categoryTableBody').innerHTML = data.html;
|
||||
resetCatForm();
|
||||
} else {
|
||||
alert(data.error || 'Failed to save category');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving category:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Service AJAX
|
||||
function editService(svc) {
|
||||
document.getElementById('svcAction').value = 'edit_service';
|
||||
document.getElementById('svcId').value = svc.id;
|
||||
@ -811,10 +840,44 @@ function resetSvcForm() {
|
||||
document.getElementById('svcCancelBtn').classList.add('d-none');
|
||||
}
|
||||
|
||||
// Service Form AJAX
|
||||
document.getElementById('serviceForm').addEventListener('submit', async function(e) {
|
||||
function confirmDeleteAjax(type, id) {
|
||||
if (confirm('<?= __('confirm_delete') ?>')) {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'delete_' + type);
|
||||
formData.append('id', id);
|
||||
formData.append('ajax', '1');
|
||||
|
||||
fetch('items.php', { method: 'POST', body: formData })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
if (type === 'category') document.getElementById('categoryTableBody').innerHTML = data.html;
|
||||
if (type === 'service') document.getElementById('serviceTableBody').innerHTML = data.html;
|
||||
} else {
|
||||
alert(data.error || 'Failed to delete');
|
||||
}
|
||||
}).catch(err => {
|
||||
alert('An error occurred while deleting.');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('categoryForm')?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(this);
|
||||
const formData = new FormData(e.target);
|
||||
try {
|
||||
const response = await fetch('items.php', { method: 'POST', body: formData });
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
document.getElementById('categoryTableBody').innerHTML = data.html;
|
||||
resetCatForm();
|
||||
}
|
||||
} catch (error) {}
|
||||
});
|
||||
|
||||
document.getElementById('serviceForm')?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
try {
|
||||
const response = await fetch('items.php', { method: 'POST', body: formData });
|
||||
const data = await response.json();
|
||||
@ -852,6 +915,19 @@ document.querySelectorAll('.price-input').forEach(input => {
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showIndicator(self);
|
||||
const row = self.closest('.service-row');
|
||||
const assignedList = row.closest('.modal-body').querySelector('.assigned-list');
|
||||
if (row && assignedList && row.parentElement.classList.contains('unassigned-list')) {
|
||||
row.classList.remove('bg-light');
|
||||
row.classList.add('border', 'border-success', 'border-opacity-25', 'bg-white', 'shadow-sm');
|
||||
const titleDiv = row.querySelector('.fw-bold.small');
|
||||
if(titleDiv) titleDiv.classList.remove('text-muted');
|
||||
const noMsg = assignedList.querySelector('.no-services-msg');
|
||||
if(noMsg) noMsg.style.display = 'none';
|
||||
assignedList.appendChild(row);
|
||||
const btn = row.querySelector('.remove-btn');
|
||||
if (btn) btn.style.display = 'inline-block';
|
||||
}
|
||||
} else {
|
||||
alert(data.error || 'Failed to update price');
|
||||
}
|
||||
@ -882,6 +958,20 @@ async function removePrice(btn, itemId, serviceId) {
|
||||
if (data.success) {
|
||||
input.value = '';
|
||||
showIndicator(input);
|
||||
// Move visually to available services
|
||||
const row = btn.closest('.service-row');
|
||||
if (row) {
|
||||
row.classList.remove('border', 'border-success', 'border-opacity-25', 'bg-white', 'shadow-sm');
|
||||
row.classList.add('bg-light');
|
||||
const titleDiv = row.querySelector('.fw-bold.small');
|
||||
if(titleDiv) titleDiv.classList.add('text-muted');
|
||||
|
||||
const unassignedList = row.closest('.modal-body').querySelector('.unassigned-list');
|
||||
if (unassignedList) {
|
||||
unassignedList.appendChild(row);
|
||||
btn.style.display = 'none'; // hide X button
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert(data.error || 'Failed to remove price');
|
||||
}
|
||||
@ -915,4 +1005,4 @@ document.getElementById('itemImageFile').addEventListener('change', function(e)
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
57
orders.php
57
orders.php
@ -108,25 +108,26 @@ if ($current_role === 'super_admin') {
|
||||
<div class="col-md-2">
|
||||
<select name="status" class="form-select" style="border-radius: 12px;" onchange="this.form.submit()">
|
||||
<option value=""><?= __('all_status') ?></option>
|
||||
<option value="received" <?= $status_filter == 'received' ? 'selected' : '' ?>><?= __('received') ?></option>
|
||||
<option value="processing" <?= $status_filter == 'processing' ? 'selected' : '' ?>><?= __('processing') ?></option>
|
||||
<option value="ready" <?= $status_filter == 'ready' ? 'selected' : '' ?>><?= __('ready') ?></option>
|
||||
<option value="delivered" <?= $status_filter == 'delivered' ? 'selected' : '' ?>><?= __('delivered') ?></option>
|
||||
<option value="cancelled" <?= $status_filter == 'cancelled' ? 'selected' : '' ?>><?= __('cancelled') ?></option>
|
||||
<option value="received" <?= $status_filter === 'received' ? 'selected' : '' ?>><?= __('received') ?></option>
|
||||
<option value="processing" <?= $status_filter === 'processing' ? 'selected' : '' ?>><?= __('processing') ?></option>
|
||||
<option value="ready" <?= $status_filter === 'ready' ? 'selected' : '' ?>><?= __('ready') ?></option>
|
||||
<option value="delivered" <?= $status_filter === 'delivered' ? 'selected' : '' ?>><?= __('delivered') ?></option>
|
||||
<option value="cancelled" <?= $status_filter === 'cancelled' ? 'selected' : '' ?>><?= __('cancelled') ?></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select name="payment_status" class="form-select" style="border-radius: 12px;" onchange="this.form.submit()">
|
||||
<option value=""><?= __('all_payments') ?></option>
|
||||
<option value="unpaid" <?= $payment_filter == 'unpaid' ? 'selected' : '' ?>><?= __('unpaid') ?></option>
|
||||
<option value="partially_paid" <?= $payment_filter == 'partially_paid' ? 'selected' : '' ?>><?= __('partially_paid') ?></option>
|
||||
<option value="paid" <?= $payment_filter == 'paid' ? 'selected' : '' ?>><?= __('paid') ?></option>
|
||||
<option value="unpaid" <?= $payment_filter === 'unpaid' ? 'selected' : '' ?>><?= __('unpaid') ?></option>
|
||||
<option value="partially_paid" <?= $payment_filter === 'partially_paid' ? 'selected' : '' ?>><?= __('partially_paid') ?></option>
|
||||
<option value="paid" <?= $payment_filter === 'paid' ? 'selected' : '' ?>><?= __('paid') ?></option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex gap-2">
|
||||
<input type="date" name="from_date" class="form-control" value="<?= $from_date ?>" style="border-radius: 12px;" title="<?= __('from_date') ?>">
|
||||
<input type="date" name="to_date" class="form-control" value="<?= $to_date ?>" style="border-radius: 12px;" title="<?= __('to_date') ?>">
|
||||
<button type="submit" class="btn btn-light" style="border-radius: 12px;"><i class="bi bi-search"></i></button>
|
||||
<div class="col-md-3">
|
||||
<div class="input-group">
|
||||
<input type="date" name="from_date" class="form-control" value="<?= $from_date ?>" style="border-radius: 12px 0 0 12px;">
|
||||
<input type="date" name="to_date" class="form-control" value="<?= $to_date ?>" style="border-radius: 0 12px 12px 0;">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -134,15 +135,15 @@ if ($current_role === 'super_admin') {
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th><?= __('order_number') ?? 'Order #' ?></th>
|
||||
<th>ID</th>
|
||||
<th><?= __('order_number') ?></th>
|
||||
<?php if ($current_role === 'super_admin'): ?>
|
||||
<th><?= __('branch') ?></th>
|
||||
<?php endif; ?>
|
||||
<th><?= __('customer') ?></th>
|
||||
<th><?= __('subtotal') ?></th>
|
||||
<th><?= __('loyalty_discount') ?></th>
|
||||
<th><?= __('total') ?></th>
|
||||
<th><?= __('loyalty_discount') ?? 'Loyalty Discount' ?></th>
|
||||
<th><?= __('final_total') ?? 'Final Total' ?></th>
|
||||
<th><?= __('status') ?></th>
|
||||
<th><?= __('payment_status') ?></th>
|
||||
<th><?= __('date') ?></th>
|
||||
@ -165,7 +166,7 @@ if ($current_role === 'super_admin') {
|
||||
<td class="text-success small fw-bold"><?= $order['loyalty_discount'] > 0 ? '-' . format_amount($order['loyalty_discount']) : '-' ?></td>
|
||||
<td class="fw-bold text-primary"><?= format_amount($order['total_price'] - $order['loyalty_discount']) ?></td>
|
||||
<td><span class="badge bg-<?= getStatusColor($order['status']) ?> rounded-pill px-3 py-2"><?= __($order['status']) ?></span></td>
|
||||
<td><span class="badge badge-soft-<?= getPaymentStatusColor($order['payment_status']) ?> rounded-pill px-3 py-2"><?= __($order['payment_status']) ?></span></td>
|
||||
<td><span class="badge bg-<?= getPaymentStatusColor($order['payment_status']) ?> rounded-pill px-3 py-2 text-white"><?= __($order['payment_status']) ?></span></td>
|
||||
<td class="small"><?= date('d/m/Y H:i', strtotime($order['created_at'])) ?></td>
|
||||
<td class="text-end">
|
||||
<div class="d-flex gap-1 justify-content-end">
|
||||
@ -254,7 +255,7 @@ if ($current_role === 'super_admin') {
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const statusModal = new bootstrap.Modal(document.getElementById('statusModal'));
|
||||
|
||||
// Status Change
|
||||
@ -335,22 +336,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php
|
||||
if (!function_exists('getStatusColor')) { function getStatusColor($status) {
|
||||
return [
|
||||
'received' => 'secondary',
|
||||
'processing' => 'primary',
|
||||
'ready' => 'success',
|
||||
'delivered' => 'dark',
|
||||
'cancelled' => 'danger',
|
||||
][$status] ?? 'info';
|
||||
}}
|
||||
function getPaymentStatusColor($status) {
|
||||
return [
|
||||
'unpaid' => 'danger',
|
||||
'partially_paid' => 'warning',
|
||||
'paid' => 'success',
|
||||
][$status] ?? 'info';
|
||||
}
|
||||
require_once __DIR__ . '/includes/footer.php';
|
||||
?>
|
||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||
420
pos.js
Normal file
420
pos.js
Normal file
@ -0,0 +1,420 @@
|
||||
const itemsData = 1;
|
||||
const lang = '1';
|
||||
const currencyLabel = '1';
|
||||
const decimalPrecision = 1;
|
||||
const editOrderId = 1;
|
||||
const loyaltyEnabled = 1;
|
||||
const pointsPerCurrency = 1;
|
||||
const currencyPerPoint = 1;
|
||||
|
||||
let cart = 1;
|
||||
let selectionModal;
|
||||
let paymentModal;
|
||||
let customerLoyaltyPoints = 0;
|
||||
let pointsToRedeem = 0;
|
||||
|
||||
// If not editing, try to load from local storage
|
||||
if (!editOrderId) {
|
||||
try {
|
||||
const saved = localStorage.getItem('pos_cart');
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) cart = parsed;
|
||||
}
|
||||
} catch (e) { console.error('Cart parse error', e); }
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof bootstrap !== 'undefined') {
|
||||
const modalEl = document.getElementById('selectionModal');
|
||||
if (modalEl) selectionModal = new bootstrap.Modal(modalEl);
|
||||
|
||||
const payModalEl = document.getElementById('paymentModal');
|
||||
if (payModalEl) paymentModal = new bootstrap.Modal(payModalEl);
|
||||
}
|
||||
|
||||
// Category filtering
|
||||
document.querySelectorAll('.cat-filter').forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
const cat = btn.getAttribute('data-cat');
|
||||
document.querySelectorAll('.cat-filter').forEach(b => b.classList.remove('btn-primary'));
|
||||
document.querySelectorAll('.cat-filter').forEach(b => b.classList.add('btn-white', 'border'));
|
||||
btn.classList.add('btn-primary');
|
||||
btn.classList.remove('btn-white', 'border');
|
||||
|
||||
document.querySelectorAll('.item-card-wrapper').forEach(card => {
|
||||
if (cat === 'all' || card.getAttribute('data-cat') === cat) card.style.display = 'block';
|
||||
else card.style.display = 'none';
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// Search
|
||||
const searchInput = document.getElementById('itemSearch');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
const q = e.target.value.toLowerCase();
|
||||
document.querySelectorAll('.item-card-wrapper').forEach(card => {
|
||||
const en = card.getAttribute('data-name-en') || '';
|
||||
const ar = card.getAttribute('data-name-ar') || '';
|
||||
if (en.includes(q) || ar.includes(q)) card.style.display = 'block';
|
||||
else card.style.display = 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Persistent Customer Search
|
||||
const custSearchInput = document.getElementById('customerSearchInput');
|
||||
const custResults = document.getElementById('customerResults');
|
||||
const clearCustBtn = document.getElementById('clearCustomerBtn');
|
||||
const custIdInput = document.getElementById('customerId');
|
||||
|
||||
// Handle Edit Customer Pre-fill
|
||||
<?php if ($edit_order && $edit_order['customer_id']): ?>
|
||||
const initialCustId = "1";
|
||||
const initialCustBtn = document.querySelector(`.customer-result-item[data-id="${initialCustId}"]`);
|
||||
if (initialCustBtn) {
|
||||
selectCustomer(initialCustId, initialCustBtn.getAttribute('data-name'), initialCustBtn.getAttribute('data-points'));
|
||||
}
|
||||
<?php endif; ?>
|
||||
|
||||
if (custSearchInput) {
|
||||
custSearchInput.addEventListener('focus', () => {
|
||||
custResults.classList.remove('d-none');
|
||||
});
|
||||
|
||||
custSearchInput.addEventListener('input', function(e) {
|
||||
const q = e.target.value.toLowerCase();
|
||||
custResults.classList.remove('d-none');
|
||||
document.querySelectorAll('.customer-result-item').forEach(item => {
|
||||
const search = item.getAttribute('data-search') || '';
|
||||
if (search.includes(q)) {
|
||||
item.parentElement.style.display = 'block';
|
||||
} else {
|
||||
item.parentElement.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!document.getElementById('customerSearchWrapper').contains(e.target)) {
|
||||
custResults.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (clearCustBtn) {
|
||||
clearCustBtn.addEventListener('click', () => {
|
||||
resetCustomerSelection();
|
||||
});
|
||||
}
|
||||
|
||||
custResults.addEventListener('click', function(e) {
|
||||
const btn = e.target.closest('.customer-result-item');
|
||||
if (btn) {
|
||||
const id = btn.getAttribute('data-id');
|
||||
const name = btn.getAttribute('data-name');
|
||||
const points = btn.getAttribute('data-points') || 0;
|
||||
selectCustomer(id, name, points);
|
||||
custResults.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
updateCart();
|
||||
});
|
||||
|
||||
function selectCustomer(id, name, points) {
|
||||
const custIdInput = document.getElementById('customerId');
|
||||
const custSearchInput = document.getElementById('customerSearchInput');
|
||||
const clearCustBtn = document.getElementById('clearCustomerBtn');
|
||||
const loyaltyDisplay = document.getElementById('loyaltyDisplay');
|
||||
const loyaltyRedeemUI = document.getElementById('loyaltyRedeemUI');
|
||||
const customerPointsEl = document.getElementById('customerPoints');
|
||||
const redeemHint = document.getElementById('redeemHint');
|
||||
|
||||
custIdInput.value = id;
|
||||
customerLoyaltyPoints = parseFloat(points);
|
||||
|
||||
if (id) {
|
||||
custSearchInput.value = name;
|
||||
clearCustBtn.classList.remove('d-none');
|
||||
if (loyaltyEnabled) {
|
||||
loyaltyDisplay.classList.remove('d-none');
|
||||
loyaltyRedeemUI.classList.remove('d-none');
|
||||
customerPointsEl.innerText = customerLoyaltyPoints.toFixed(2);
|
||||
if (redeemHint) {
|
||||
redeemHint.innerText = lang === 'en'
|
||||
? `1 Point = ${currencyPerPoint} ${currencyLabel}`
|
||||
: `1 نقطة = ${currencyPerPoint} ${currencyLabel}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resetCustomerSelection();
|
||||
}
|
||||
}
|
||||
|
||||
function resetCustomerSelection() {
|
||||
const custIdInput = document.getElementById('customerId');
|
||||
const custSearchInput = document.getElementById('customerSearchInput');
|
||||
const clearCustBtn = document.getElementById('clearCustomerBtn');
|
||||
const loyaltyDisplay = document.getElementById('loyaltyDisplay');
|
||||
const loyaltyRedeemUI = document.getElementById('loyaltyRedeemUI');
|
||||
|
||||
custIdInput.value = '';
|
||||
custSearchInput.value = '';
|
||||
custSearchInput.placeholder = lang === 'en' ? 'Walk-in Customer' : 'عميل عابر';
|
||||
clearCustBtn.classList.add('d-none');
|
||||
loyaltyDisplay.classList.add('d-none');
|
||||
loyaltyRedeemUI.classList.add('d-none');
|
||||
customerLoyaltyPoints = 0;
|
||||
pointsToRedeem = 0;
|
||||
const ptsInput = document.getElementById('pointsToRedeem');
|
||||
if (ptsInput) ptsInput.value = '';
|
||||
updateCart();
|
||||
}
|
||||
|
||||
function applyPoints() {
|
||||
const ptsInput = document.getElementById('pointsToRedeem');
|
||||
let val = parseFloat(ptsInput.value) || 0;
|
||||
if (val > customerLoyaltyPoints) {
|
||||
alert(lang === 'en' ? 'Insufficient points' : 'نقاط غير كافية');
|
||||
val = customerLoyaltyPoints;
|
||||
ptsInput.value = val;
|
||||
}
|
||||
pointsToRedeem = val;
|
||||
updateCart();
|
||||
}
|
||||
|
||||
function showOptions(itemId) {
|
||||
const item = itemsData[itemId];
|
||||
if (!item) return;
|
||||
const itemNameEl = document.getElementById('selectionItemName');
|
||||
if (itemNameEl) itemNameEl.innerText = lang === 'en' ? item.name_en : item.name_ar;
|
||||
const list = document.getElementById('optionsList');
|
||||
if (list) {
|
||||
list.innerHTML = '';
|
||||
item.services.forEach(s => {
|
||||
const col = document.createElement('div');
|
||||
col.className = 'col-6';
|
||||
col.innerHTML = `
|
||||
<button class="btn btn-outline-primary w-100 p-3 rounded-4 border-2 text-center h-100 transition-all" onclick="addToCart(${item.id}, ${s.id})">
|
||||
<div class="fw-bold mb-1 small">${lang === 'en' ? s.name_en : s.name_ar}</div>
|
||||
<div class="small opacity-75">${s.price.toFixed(decimalPrecision)} ${currencyLabel}</div>
|
||||
</button>
|
||||
`;
|
||||
list.appendChild(col);
|
||||
});
|
||||
}
|
||||
if (selectionModal) selectionModal.show();
|
||||
}
|
||||
|
||||
function addToCart(itemId, serviceId) {
|
||||
const item = itemsData[itemId];
|
||||
if (!item) return;
|
||||
const service = item.services.find(s => s.id === serviceId);
|
||||
if (!service) return;
|
||||
|
||||
const existing = cart.find(i => i.item_id === itemId && i.service_id === serviceId);
|
||||
if (existing) {
|
||||
existing.qty++;
|
||||
} else {
|
||||
cart.push({
|
||||
item_id: itemId,
|
||||
service_id: serviceId,
|
||||
name: lang === 'en' ? item.name_en : item.name_ar,
|
||||
service_name: lang === 'en' ? service.name_en : service.name_ar,
|
||||
price: service.price,
|
||||
qty: 1,
|
||||
vat_percent: item.vat_percent
|
||||
});
|
||||
}
|
||||
if (selectionModal) selectionModal.hide();
|
||||
updateCart();
|
||||
}
|
||||
|
||||
function changeQty(index, delta) {
|
||||
cart[index].qty += delta;
|
||||
if (cart[index].qty <= 0) cart.splice(index, 1);
|
||||
updateCart();
|
||||
}
|
||||
|
||||
function updateCart() {
|
||||
if (!editOrderId) {
|
||||
localStorage.setItem('pos_cart', JSON.stringify(cart));
|
||||
}
|
||||
const cartList = document.getElementById('cartItems');
|
||||
const emptyCart = document.getElementById('emptyCart');
|
||||
if (!cartList || !emptyCart) return;
|
||||
|
||||
if (cart.length === 0) {
|
||||
cartList.innerHTML = '';
|
||||
emptyCart.style.display = 'block';
|
||||
} else {
|
||||
emptyCart.style.display = 'none';
|
||||
cartList.innerHTML = cart.map((item, index) => `
|
||||
<div class="d-flex align-items-center mb-3 bg-light p-2 rounded-3">
|
||||
<div class="flex-grow-1">
|
||||
<div class="fw-bold small text-dark">${item.name}</div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;">${item.service_name}</div>
|
||||
<div class="fw-bold text-primary">${item.price.toFixed(decimalPrecision)} ${currencyLabel}</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center bg-white rounded-3 p-1">
|
||||
<button class="btn btn-sm p-0 px-2" onclick="changeQty(${index}, -1)"><i class="bi bi-dash"></i></button>
|
||||
<span class="mx-2 fw-bold">${item.qty}</span>
|
||||
<button class="btn btn-sm p-0 px-2" onclick="changeQty(${index}, 1)"><i class="bi bi-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
let subtotal = 0;
|
||||
let totalVat = 0;
|
||||
|
||||
cart.forEach(item => {
|
||||
const itemSubtotal = item.price * item.qty;
|
||||
subtotal += itemSubtotal;
|
||||
totalVat += itemSubtotal * ((item.vat_percent || 15) / 100);
|
||||
});
|
||||
|
||||
const subtotalEl = document.getElementById('cartSubtotal');
|
||||
const vatEl = document.getElementById('cartVat');
|
||||
const totalEl = document.getElementById('cartTotal');
|
||||
const discountRow = document.getElementById('loyaltyDiscountRow');
|
||||
const discountEl = document.getElementById('cartLoyaltyDiscount');
|
||||
|
||||
let totalBeforeDiscount = subtotal + totalVat;
|
||||
let loyaltyDiscount = pointsToRedeem * currencyPerPoint;
|
||||
|
||||
if (loyaltyDiscount > totalBeforeDiscount) {
|
||||
loyaltyDiscount = totalBeforeDiscount;
|
||||
pointsToRedeem = loyaltyDiscount / currencyPerPoint;
|
||||
}
|
||||
|
||||
if (loyaltyDiscount > 0) {
|
||||
discountRow.classList.remove('d-none');
|
||||
discountEl.innerText = '-' + loyaltyDiscount.toFixed(decimalPrecision) + ' ' + currencyLabel;
|
||||
} else {
|
||||
discountRow.classList.add('d-none');
|
||||
}
|
||||
|
||||
const finalTotal = totalBeforeDiscount - loyaltyDiscount;
|
||||
|
||||
if (subtotalEl) subtotalEl.innerText = subtotal.toFixed(decimalPrecision) + ' ' + currencyLabel;
|
||||
if (vatEl) vatEl.innerText = totalVat.toFixed(decimalPrecision) + ' ' + currencyLabel;
|
||||
if (totalEl) totalEl.innerText = finalTotal.toFixed(decimalPrecision) + ' ' + currencyLabel;
|
||||
}
|
||||
|
||||
function clearCart() {
|
||||
if (confirm(lang === 'en' ? 'Clear cart?' : 'مسح السلة؟')) {
|
||||
cart = [];
|
||||
pointsToRedeem = 0;
|
||||
const ptsInput = document.getElementById('pointsToRedeem');
|
||||
if (ptsInput) ptsInput.value = '';
|
||||
updateCart();
|
||||
}
|
||||
}
|
||||
|
||||
function checkout() {
|
||||
if (cart.length === 0) return;
|
||||
|
||||
let subtotal = 0;
|
||||
let totalVat = 0;
|
||||
cart.forEach(item => {
|
||||
const itemSubtotal = item.price * item.qty;
|
||||
subtotal += itemSubtotal;
|
||||
totalVat += itemSubtotal * ((item.vat_percent || 15) / 100);
|
||||
});
|
||||
|
||||
let finalTotal = (subtotal + totalVat) - (pointsToRedeem * currencyPerPoint);
|
||||
if (finalTotal < 0) finalTotal = 0;
|
||||
|
||||
document.getElementById('paymentTotalAmount').innerText = finalTotal.toFixed(decimalPrecision) + ' ' + currencyLabel;
|
||||
if (paymentModal) paymentModal.show();
|
||||
}
|
||||
|
||||
async function completeCheckout(paymentMethod) {
|
||||
const cid = document.getElementById('customerId').value;
|
||||
|
||||
let subtotal = 0;
|
||||
let totalVat = 0;
|
||||
const itemsToSubmit = cart.map(item => {
|
||||
const itemSubtotal = item.price * item.qty;
|
||||
const itemVat = itemSubtotal * ((item.vat_percent || 15) / 100);
|
||||
subtotal += itemSubtotal;
|
||||
totalVat += itemVat;
|
||||
return {
|
||||
itemId: item.item_id,
|
||||
serviceId: item.service_id,
|
||||
variantId: null,
|
||||
quantity: item.qty,
|
||||
price: item.price,
|
||||
vatAmount: itemVat
|
||||
};
|
||||
});
|
||||
|
||||
const totalPrice = subtotal + totalVat;
|
||||
|
||||
try {
|
||||
const response = await fetch('api/checkout.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
order_id: editOrderId,
|
||||
customer_id: cid,
|
||||
items: itemsToSubmit,
|
||||
vat_total: totalVat,
|
||||
total_price: totalPrice,
|
||||
payment_method: paymentMethod,
|
||||
points_to_redeem: pointsToRedeem
|
||||
})
|
||||
});
|
||||
const res = await response.json();
|
||||
if (res.success) {
|
||||
const orderId = res.order_id || editOrderId;
|
||||
if (!editOrderId) { cart = []; updateCart(); localStorage.removeItem("pos_cart"); } window.location.href = "receipt.php?id=" + orderId;
|
||||
} else alert(res.error);
|
||||
} catch (e) { alert('Error'); }
|
||||
}
|
||||
|
||||
|
||||
async function saveCustomer() {
|
||||
const form = document.getElementById('addCustomerForm');
|
||||
if (!form) return;
|
||||
const data = new FormData(form);
|
||||
try {
|
||||
const response = await fetch('api/add_customer.php', { method: 'POST', body: data });
|
||||
const res = await response.json();
|
||||
if (res.success) {
|
||||
const id = res.customer.id;
|
||||
const nameEn = res.customer.name_en;
|
||||
const nameAr = res.customer.name_ar || nameEn;
|
||||
const phone = res.customer.phone;
|
||||
const displayName = lang === 'en' ? nameEn : nameAr;
|
||||
|
||||
selectCustomer(id, displayName, 0);
|
||||
|
||||
const list = document.getElementById('customerResultsList');
|
||||
if (list) {
|
||||
const searchStr = `${nameEn} ${nameAr} ${phone}`.toLowerCase();
|
||||
const div = document.createElement('div');
|
||||
div.className = 'p-1';
|
||||
div.innerHTML = `
|
||||
<button class="btn btn-white btn-sm w-100 text-start rounded-2 customer-result-item p-2" type="button" data-id="${id}" data-name="${displayName}" data-phone="${phone}" data-search="${searchStr}" data-points="0">
|
||||
<div class="fw-bold small text-dark">${displayName}</div>
|
||||
<div class="text-muted small" style="font-size: 0.7rem;">${phone}</div>
|
||||
</button>
|
||||
`;
|
||||
list.prepend(div);
|
||||
}
|
||||
|
||||
if (typeof bootstrap !== 'undefined') {
|
||||
const modalEl = document.getElementById('addCustomerModal');
|
||||
if (modalEl) {
|
||||
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||
if (modal) modal.hide();
|
||||
}
|
||||
}
|
||||
form.reset();
|
||||
} else alert(res.error);
|
||||
} catch (e) { alert('Error'); }
|
||||
}
|
||||
24
pos.php
24
pos.php
@ -54,18 +54,19 @@ if ($edit_order_id) {
|
||||
}
|
||||
|
||||
// Get all categories
|
||||
$categories = db()->query("SELECT * FROM categories ORDER BY name_en ASC")->fetchAll();
|
||||
$categories = db()->query("SELECT * FROM categories WHERE is_deleted = 0 ORDER BY name_en ASC")->fetchAll();
|
||||
|
||||
// Get all items with details
|
||||
$stmt = db()->prepare("SELECT i.*, c.name_en as cat_en, c.name_ar as cat_ar
|
||||
FROM items i
|
||||
LEFT JOIN categories c ON i.category_id = c.id
|
||||
WHERE i.is_deleted = 0
|
||||
ORDER BY i.name_en ASC");
|
||||
$stmt->execute();
|
||||
$items_raw = $stmt->fetchAll();
|
||||
|
||||
// Get all services
|
||||
$services_raw = db()->query("SELECT * FROM services ORDER BY name_en ASC")->fetchAll();
|
||||
$services_raw = db()->query("SELECT * FROM services WHERE is_deleted = 0 ORDER BY name_en ASC")->fetchAll();
|
||||
|
||||
// Get prices for current branch or global
|
||||
if ($branch_id) {
|
||||
@ -395,7 +396,7 @@ $pageTitle = $edit_order ? ($lang == 'en' ? 'Edit Order #' . $edit_order['order_
|
||||
</div>
|
||||
|
||||
<!-- Hidden Iframe for Printing -->
|
||||
<iframe id="printFrame" style="display:none;"></iframe>
|
||||
|
||||
|
||||
<script>
|
||||
const itemsData = <?= json_encode((object)$items) ?>;
|
||||
@ -771,26 +772,11 @@ async function completeCheckout(paymentMethod) {
|
||||
const res = await response.json();
|
||||
if (res.success) {
|
||||
const orderId = res.order_id || editOrderId;
|
||||
printReceipt(orderId);
|
||||
if (!editOrderId) {
|
||||
cart = [];
|
||||
updateCart();
|
||||
localStorage.removeItem('pos_cart');
|
||||
}
|
||||
if (paymentModal) paymentModal.hide();
|
||||
setTimeout(() => { window.location.href = 'pos.php'; }, 2000);
|
||||
if (!editOrderId) { cart = []; updateCart(); localStorage.removeItem("pos_cart"); } window.location.href = "receipt.php?id=" + orderId;
|
||||
} else alert(res.error);
|
||||
} catch (e) { alert('Error'); }
|
||||
}
|
||||
|
||||
function printReceipt(orderId) {
|
||||
const iframe = document.getElementById('printFrame');
|
||||
iframe.src = 'receipt.php?id=' + orderId;
|
||||
iframe.onload = function() {
|
||||
iframe.contentWindow.focus();
|
||||
iframe.contentWindow.print();
|
||||
};
|
||||
}
|
||||
|
||||
async function saveCustomer() {
|
||||
const form = document.getElementById('addCustomerForm');
|
||||
|
||||
58
receipt.php
58
receipt.php
@ -39,10 +39,6 @@ $stmt->execute([$order_id]);
|
||||
$items = $stmt->fetchAll();
|
||||
|
||||
$lang = $_SESSION['lang'] ?? 'en';
|
||||
function is_arabic() {
|
||||
global $lang;
|
||||
return $lang === 'ar';
|
||||
}
|
||||
|
||||
function format_currency($amount) {
|
||||
return number_format($amount, decimals()) . ' ' . currency();
|
||||
@ -71,8 +67,8 @@ function format_currency($amount) {
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
@media print {
|
||||
body { background: white; margin: 0; padding: 0; }
|
||||
.receipt-container { margin: 0; box-shadow: none; width: 100%; }
|
||||
body { background: white; margin: 0; padding: 0; width: 80mm; }
|
||||
.receipt-container { margin: 0; box-shadow: none; width: 80mm; padding: 0; }
|
||||
.no-print { display: none; }
|
||||
}
|
||||
.header { text-align: center; border-bottom: 1px dashed #ccc; padding-bottom: 10px; margin-bottom: 10px; }
|
||||
@ -96,14 +92,20 @@ function format_currency($amount) {
|
||||
|
||||
<div class="receipt-container">
|
||||
<div class="header">
|
||||
<h5 class="fw-bold m-0"><?= $company['name_en'] ?? 'Laundry POS' ?></h5>
|
||||
<?php if ($company['name_ar']): ?>
|
||||
<div class="arabic-text small"><?= $company['name_ar'] ?></div>
|
||||
<?php if (!empty($company['logo'])): ?>
|
||||
<img src="<?= htmlspecialchars($company['logo']) ?>" alt="Logo" style="max-height: 80px; max-width: 100%; margin-bottom: 10px;">
|
||||
<?php endif; ?>
|
||||
<div class="small mt-1"><?= $order['branch_name_en'] ?> / <span class="arabic-text"><?= $order['branch_name_ar'] ?></span></div>
|
||||
<div class="small"><?= $order['branch_phone'] ?></div>
|
||||
<?php if ($company['vat_no']): ?>
|
||||
<div class="small">VAT: <?= $company['vat_no'] ?></div>
|
||||
<h5 class="fw-bold m-0"><?= htmlspecialchars($company['name_en'] ?? 'Laundry POS') ?></h5>
|
||||
<?php if (!empty($company['name_ar'])): ?>
|
||||
<div class="arabic-text small"><?= htmlspecialchars($company['name_ar']) ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="small mt-1"><?= htmlspecialchars($order['branch_name_en']) ?> / <span class="arabic-text"><?= htmlspecialchars($order['branch_name_ar']) ?></span></div>
|
||||
<div class="small"><?= htmlspecialchars($order['branch_phone']) ?></div>
|
||||
<?php if (!empty($company['ctr_no'])): ?>
|
||||
<div class="small">CR No: <?= htmlspecialchars($company['ctr_no']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($company['vat_no'])): ?>
|
||||
<div class="small">VAT No: <?= htmlspecialchars($company['vat_no']) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
@ -136,26 +138,30 @@ function format_currency($amount) {
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$calculated_subtotal = 0;
|
||||
foreach($items as $item) {
|
||||
$calculated_subtotal += $item['subtotal'];
|
||||
}
|
||||
?>
|
||||
<div class="totals">
|
||||
<div class="info-row">
|
||||
<span>Subtotal:</span>
|
||||
<span><?= format_currency($order['subtotal']) ?></span>
|
||||
<span><?= format_currency($calculated_subtotal) ?></span>
|
||||
</div>
|
||||
<?php if ($order['discount_amount'] > 0): ?>
|
||||
<?php if ($order['loyalty_discount'] > 0): ?>
|
||||
<div class="info-row">
|
||||
<span>Discount:</span>
|
||||
<span>-<?= format_currency($order['discount_amount']) ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($order['vat_amount'] > 0): ?>
|
||||
<div class="info-row">
|
||||
<span>VAT (<?= $order['vat_amount'] > 0 ? round(($order['vat_amount'] / ($order['subtotal'] ?: 1)) * 100) : 0 ?>%):</span>
|
||||
<span><?= format_currency($order['vat_amount']) ?></span>
|
||||
<span>Discount (Loyalty):</span>
|
||||
<span>-<?= format_currency($order['loyalty_discount']) ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="info-row">
|
||||
<span>VAT:</span>
|
||||
<span><?= format_currency($order['vat_total'] ?? 0) ?></span>
|
||||
</div>
|
||||
<div class="total-row mt-2">
|
||||
<span>Total:</span>
|
||||
<span><?= format_currency($order['total_amount']) ?></span>
|
||||
<span><?= format_currency($order['total_price']) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -179,6 +185,8 @@ function format_currency($amount) {
|
||||
displayValue: false
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
window.onload = function() { setTimeout(() => window.print(), 500); };
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user