diff --git a/assets/css/custom.css b/assets/css/custom.css index 4688093..1334fb9 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -136,4 +136,19 @@ body.auth-body { /* Sidebar Sub-menu */ [data-bs-toggle="collapse"][aria-expanded="true"] .toggle-icon { transform: rotate(180deg); -} \ No newline at end of file +} +/* Fix for btn-close in modal-header */ +.modal-header { + position: relative; +} +.modal-header .btn-close { + margin: 0; + position: absolute; + top: 1rem; +} +[dir="rtl"] .modal-header .btn-close { + left: 1rem; +} +[dir="ltr"] .modal-header .btn-close { + right: 1rem; +} diff --git a/assets/images/items/18311171.jpg b/assets/images/items/18311171.jpg new file mode 100644 index 0000000..0b0206d Binary files /dev/null and b/assets/images/items/18311171.jpg differ diff --git a/assets/images/items/18641665.jpg b/assets/images/items/18641665.jpg new file mode 100644 index 0000000..e6deac9 Binary files /dev/null and b/assets/images/items/18641665.jpg differ diff --git a/assets/images/items/2537658.jpg b/assets/images/items/2537658.jpg new file mode 100644 index 0000000..02bcbb7 Binary files /dev/null and b/assets/images/items/2537658.jpg differ diff --git a/assets/images/items/29765806.jpg b/assets/images/items/29765806.jpg new file mode 100644 index 0000000..37e90e9 Binary files /dev/null and b/assets/images/items/29765806.jpg differ diff --git a/assets/images/items/9058886.jpg b/assets/images/items/9058886.jpg new file mode 100644 index 0000000..2ffd222 Binary files /dev/null and b/assets/images/items/9058886.jpg differ diff --git a/cookies.txt b/cookies.txt new file mode 100644 index 0000000..40643ba --- /dev/null +++ b/cookies.txt @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +127.0.0.1 FALSE / FALSE 0 PHPSESSID b0jh7ohno1tnaa8tkh48odf595 diff --git a/db_seed.php b/db_seed.php new file mode 100644 index 0000000..6f99dac --- /dev/null +++ b/db_seed.php @@ -0,0 +1,36 @@ +exec(" +CREATE TABLE IF NOT EXISTS items ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + sku VARCHAR(50) NOT NULL UNIQUE, + name VARCHAR(200) NOT NULL, + price DECIMAL(10,3) NOT NULL, + base_stock INT NOT NULL DEFAULT 0, + vat DECIMAL(5,3) NOT NULL DEFAULT 5.000, + category_id INT UNSIGNED NULL, + supplier_id INT UNSIGNED NULL, + image_url VARCHAR(255) NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL, + FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL +); +"); + +// 2. Insert Categories +$db->exec("INSERT IGNORE INTO categories (id, name_ar, name_en) VALUES +(1, 'إلكترونيات', 'Electronics'), +(2, 'إكسسوارات', 'Accessories'), +(3, 'ملابس', 'Clothing');"); + +// 3. Insert Suppliers +$db->exec("INSERT IGNORE INTO suppliers (id, name, contact_person, phone) VALUES +(1, 'TechCorp', 'John Doe', '123456789'), +(2, 'ElectroWholesale', 'Jane Smith', '987654321'), +(3, 'StyleCo', 'Mike Johnson', '555666777');"); + +echo "Database schema and base entities created.\n"; + diff --git a/includes/app.php b/includes/app.php index e804b31..4d391ed 100644 --- a/includes/app.php +++ b/includes/app.php @@ -193,14 +193,30 @@ function can_access_branch(string $branchCode): bool function catalog(): array { - return [ - 'baklava_box' => ['sku' => 'baklava_box', 'name_ar' => 'بقلاوة مشكلة', 'name_en' => 'Mixed Baklava Box', 'price' => 18.50, 'base_stock' => 72, 'unit_ar' => 'علبة', 'unit_en' => 'box'], - 'date_truffles' => ['sku' => 'date_truffles', 'name_ar' => 'ترافل التمر', 'name_en' => 'Date Truffles', 'price' => 9.25, 'base_stock' => 120, 'unit_ar' => 'علبة', 'unit_en' => 'box'], - 'saffron_maamoul' => ['sku' => 'saffron_maamoul', 'name_ar' => 'معمول الزعفران', 'name_en' => 'Saffron Maamoul', 'price' => 7.80, 'base_stock' => 88, 'unit_ar' => 'صندوق', 'unit_en' => 'pack'], - 'pistachio_bites' => ['sku' => 'pistachio_bites', 'name_ar' => 'لقيمات الفستق', 'name_en' => 'Pistachio Bites', 'price' => 11.40, 'base_stock' => 64, 'unit_ar' => 'علبة', 'unit_en' => 'box'], - 'halwa_classic' => ['sku' => 'halwa_classic', 'name_ar' => 'حلوى عمانية كلاسيك', 'name_en' => 'Classic Omani Halwa', 'price' => 6.20, 'base_stock' => 150, 'unit_ar' => 'عبوة', 'unit_en' => 'jar'], - 'gift_tin' => ['sku' => 'gift_tin', 'name_ar' => 'علبة هدايا فاخرة', 'name_en' => 'Premium Gift Tin', 'price' => 24.00, 'base_stock' => 36, 'unit_ar' => 'علبة', 'unit_en' => 'tin'], - ]; + try { + $db = db(); + $stmt = $db->query("SELECT * FROM items"); + $items = $stmt->fetchAll(PDO::FETCH_ASSOC); + $catalog = []; + foreach ($items as $item) { + $catalog[$item["sku"]] = [ + "sku" => $item["sku"], + "name_ar" => $item["name"], + "name_en" => $item["name"], + "price" => (float)$item["price"], + "base_stock" => (int)$item["base_stock"], + "vat" => (float)$item["vat"], + "category_id" => $item["category_id"], + "supplier_id" => $item["supplier_id"], + "image_url" => $item["image_url"], + "unit_ar" => "قطعة", + "unit_en" => "pcs" + ]; + } + return $catalog; + } catch (Throwable $e) { + return []; + } } function product_label(string $sku): string @@ -210,12 +226,13 @@ function product_label(string $sku): string return $sku; } - return current_lang() === 'ar' ? $item['name_ar'] : $item['name_en']; + return current_lang() === "ar" ? $item["name_ar"] : $item["name_en"]; } + function currency(float $amount): string { - return number_format($amount, 2) . ' ' . tr('ر.ع', 'OMR'); + return number_format($amount, 3) . ' ' . tr('ر.ع', 'OMR'); } function sale_mode_label(string $mode): string @@ -440,7 +457,11 @@ function stock_snapshot(): array 'base_stock' => $base, 'sold' => $used, 'available' => max(0, $base - $used), - 'price' => (float) $item['price'], + 'price' => $item['price'], + 'category_id' => $item['category_id'], + 'supplier_id' => $item['supplier_id'], + 'image_url' => $item['image_url'], + 'vat' => $item['vat'], ]; } diff --git a/includes/pexels.php b/includes/pexels.php new file mode 100644 index 0000000..0c04a85 --- /dev/null +++ b/includes/pexels.php @@ -0,0 +1,25 @@ + 0 ? $k : 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18'; +} +function pexels_get($url) { + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ 'Authorization: '. pexels_key() ], + CURLOPT_TIMEOUT => 15, + ]); + $resp = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + if ($code >= 200 && $code < 300 && $resp) return json_decode($resp, true); + return null; +} +function download_to($srcUrl, $destPath) { + $data = file_get_contents($srcUrl); + if ($data === false) return false; + if (!is_dir(dirname($destPath))) mkdir(dirname($destPath), 0775, true); + return file_put_contents($destPath, $data) !== false; +} diff --git a/patch_app.php b/patch_app.php new file mode 100644 index 0000000..1a7d38b --- /dev/null +++ b/patch_app.php @@ -0,0 +1,17 @@ + max(0, $base - $used), + 'price' => $item['price'] + , + 'available' => max(0, $base - $used), + 'price' => $item['price'], + 'category_id' => $item['category_id'], + 'supplier_id' => $item['supplier_id'], + 'image_url' => $item['image_url'], + 'vat' => $item['vat'] + , + $content +); +file_put_contents('includes/app.php', $content); + diff --git a/patch_modal.php b/patch_modal.php new file mode 100644 index 0000000..fb4cee1 --- /dev/null +++ b/patch_modal.php @@ -0,0 +1,19 @@ +<\?= h\(\$row[\'name\']\) \?><\/td>/', "$1\n$1 \n$1 \" alt=\"\" class=\"rounded\" style=\"width: 40px; height: 40px; object-fit: cover;\">\n$1 \n$1
\n$1 \n$1\n$1", $content); +file_put_contents('stock.php', $content); \ No newline at end of file diff --git a/purchases.php b/purchases.php index 17dfe67..7500345 100644 --- a/purchases.php +++ b/purchases.php @@ -10,11 +10,11 @@ $allPurchases = purchase_pipeline(); $search = $_GET['q'] ?? ''; $filteredPurchases = []; if ($search) { - $lowerSearch = mb_strtolower($search); + $lowerSearch = strtolower($search); foreach ($allPurchases as $key => $row) { if ( - str_contains(mb_strtolower($row['supplier']), $lowerSearch) || - str_contains(mb_strtolower($row['reference']), $lowerSearch) + str_contains(strtolower((string)$row['supplier']), $lowerSearch) || + str_contains(strtolower((string)$row['reference']), $lowerSearch) ) { $filteredPurchases[$key] = $row; } diff --git a/seed_items.php b/seed_items.php new file mode 100644 index 0000000..fd6e7c8 --- /dev/null +++ b/seed_items.php @@ -0,0 +1,61 @@ + '10000001', 'name' => 'Laptop Pro 15', 'price' => 1200.000, 'base_stock' => 10, 'category_id' => 1, 'supplier_id' => 1, 'query' => 'laptop'], + ['sku' => '10000002', 'name' => 'Wireless Mouse', 'price' => 25.500, 'base_stock' => 50, 'category_id' => 2, 'supplier_id' => 2, 'query' => 'computer mouse'], + ['sku' => '10000003', 'name' => 'Mechanical Keyboard', 'price' => 85.000, 'base_stock' => 30, 'category_id' => 2, 'supplier_id' => 1, 'query' => 'keyboard'], + ['sku' => '10000004', 'name' => 'USB-C Hub', 'price' => 40.000, 'base_stock' => 100, 'category_id' => 2, 'supplier_id' => 2, 'query' => 'usb cable'], + ['sku' => '10000005', 'name' => 'Cotton T-Shirt', 'price' => 15.000, 'base_stock' => 200, 'category_id' => 3, 'supplier_id' => 3, 'query' => 't-shirt'], +]; + +foreach ($items as $item) { + // Check if sku already exists + $stmt = $db->prepare("SELECT id FROM items WHERE sku = ?"); + $stmt->execute([$item['sku']]); + if ($stmt->fetch()) { + echo "SKU {" . $item['sku'] . "} already exists.\n"; + continue; + } + + $imageUrl = null; + $localImagePath = null; + + // Fetch image from pexels + $url = 'https://api.pexels.com/v1/search?query=' . urlencode($item['query']) . '&orientation=square&per_page=1&page=1'; + $data = pexels_get($url); + if ($data && !empty($data['photos'])) { + $photo = $data['photos'][0]; + $src = $photo['src']['medium'] ?? ($photo['src']['original'] ?? null); + if ($src) { + $dest = __DIR__ . '/assets/images/items/' . $photo['id'] . '.jpg'; + if (download_to($src, $dest)) { + $localImagePath = 'assets/images/items/' . $photo['id'] . '.jpg'; + } + } + } + + if (!$localImagePath) { + $localImagePath = 'https://picsum.photos/400?random=' . rand(1, 1000); // fallback + } + + $stmt = $db->prepare("INSERT INTO items (sku, name, price, base_stock, vat, category_id, supplier_id, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([ + $item['sku'], + $item['name'], + $item['price'], + $item['base_stock'], + 5.000, // default VAT + $item['category_id'], + $item['supplier_id'], + $localImagePath + ]); + echo "Inserted {" . $item['name'] . "}\n"; +} + +echo "Done seeding items.\n"; + diff --git a/stock.php b/stock.php index 0ef05f5..991e9f2 100644 --- a/stock.php +++ b/stock.php @@ -22,21 +22,23 @@ try { // Ignore if not present } -// Search logic +// Search and filter logic $search = $_GET['q'] ?? ''; +$catFilter = $_GET['category'] ?? ''; +$supFilter = $_GET['supplier'] ?? ''; $filteredStock = []; -if ($search && empty($dbError)) { - $lowerSearch = mb_strtolower($search); + +if (empty($dbError)) { + $lowerSearch = strtolower($search); foreach ($allStock as $key => $row) { - if ( - str_contains(mb_strtolower($row['sku']), $lowerSearch) || - str_contains(mb_strtolower($row['name']), $lowerSearch) - ) { + $matchSearch = !$search || str_contains(strtolower((string)$row['sku']), $lowerSearch) || str_contains(strtolower((string)$row['name']), $lowerSearch); + $matchCat = !$catFilter || (isset($row['category_id']) && $row['category_id'] == $catFilter); + $matchSup = !$supFilter || (isset($row['supplier_id']) && $row['supplier_id'] == $supFilter); + + if ($matchSearch && $matchCat && $matchSup) { $filteredStock[$key] = $row; } } -} else { - $filteredStock = $allStock; } // Pagination logic @@ -63,10 +65,32 @@ require __DIR__ . '/includes/header.php'; -
-
- - + +
+
+ +
+
+ +
+
+ +
+
+ +
@@ -77,9 +101,10 @@ require __DIR__ . '/includes/header.php';
- + + @@ -109,7 +134,7 @@ require __DIR__ . '/includes/header.php';
SKU - +