From f8ccd73ba64a76a085664f86fc5ef05d2f660ace Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 4 Mar 2026 03:04:10 +0000 Subject: [PATCH] Autosave: 20260304-030410 --- admin.php | 22 - cookies.txt | 2 +- db/config.php | 22 + db/migrations/18_add_is_deleted_to_items.sql | 3 + items.php | 816 ++++++++++--------- orders.php | 57 +- pos.js | 420 ++++++++++ pos.php | 24 +- receipt.php | 58 +- 9 files changed, 957 insertions(+), 467 deletions(-) create mode 100644 db/migrations/18_add_is_deleted_to_items.sql create mode 100644 pos.js diff --git a/admin.php b/admin.php index d154764..6ff6896 100644 --- a/admin.php +++ b/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'); diff --git a/cookies.txt b/cookies.txt index 99f826f..b1857ad 100644 --- a/cookies.txt +++ b/cookies.txt @@ -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 diff --git a/db/config.php b/db/config.php index 8a9af69..3e2eb62 100644 --- a/db/config.php +++ b/db/config.php @@ -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'; + } } \ No newline at end of file diff --git a/db/migrations/18_add_is_deleted_to_items.sql b/db/migrations/18_add_is_deleted_to_items.sql new file mode 100644 index 0000000..f1569ba --- /dev/null +++ b/db/migrations/18_add_is_deleted_to_items.sql @@ -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; diff --git a/items.php b/items.php index 018d21c..6c3d13d 100644 --- a/items.php +++ b/items.php @@ -1,17 +1,81 @@ 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): ?> - - - - - - - - - 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): ?> - - - - - - - - - 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): ?> - - - - - - - - - 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): ?> + + + + + + + + + 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): ?> @@ -375,154 +372,178 @@ function renderServiceList($lang) { -
-
-
-
-
- - -
-
-
- -
-
- -
-
-
-
- -
-
-
-
- - <?= htmlspecialchars($item['name_en']) ?> -
- -
- -
- - - -
-
-
-
-

- -
-
- -
-
- - - - - - - -
-
+ +
+
+
+
+ +
-
- +
+ +
+
+ +
+
- + +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ +
+ +
+ + + + + + +
+ + + + + + +
+
+
+
+ + +
+ +

+
+