Autosave: 20260304-030410

This commit is contained in:
Flatlogic Bot 2026-03-04 03:04:10 +00:00
parent f08a6b75bb
commit f8ccd73ba6
9 changed files with 957 additions and 467 deletions

View File

@ -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');

View File

@ -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

View File

@ -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';
}
}

View File

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

816
items.php
View File

@ -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'; ?>

View File

@ -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
View File

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

24
pos.php
View File

@ -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');

View File

@ -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>