feat: setup items, categories, and suppliers database tables with real demo data and images from pexels

This commit is contained in:
Flatlogic Bot 2026-04-19 02:43:31 +00:00
parent ccaa56bcff
commit 033b73cd60
17 changed files with 286 additions and 44 deletions

View File

@ -136,4 +136,19 @@ body.auth-body {
/* Sidebar Sub-menu */
[data-bs-toggle="collapse"][aria-expanded="true"] .toggle-icon {
transform: rotate(180deg);
}
}
/* 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

5
cookies.txt Normal file
View File

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

36
db_seed.php Normal file
View File

@ -0,0 +1,36 @@
<?php
require 'db/config.php';
$db = db();
// 1. Create items table
$db->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";

View File

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

25
includes/pexels.php Normal file
View File

@ -0,0 +1,25 @@
<?php
function pexels_key() {
$k = getenv('PEXELS_KEY');
return $k && strlen($k) > 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;
}

17
patch_app.php Normal file
View File

@ -0,0 +1,17 @@
<?php
$content = file_get_contents('includes/app.php');
$content = str_replace(
'available' => 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);

19
patch_modal.php Normal file
View File

@ -0,0 +1,19 @@
<?php
$content = file_get_contents('stock.php');
$content = str_replace(
"function openItemModal(sku = '', name = '', price = '', base_stock = '') {",
"function openItemModal(sku = '', name = '', price = '', base_stock = '', vat = '5', category_id = '', supplier_id = '', image_url = '') {",
$content
);
$content = str_replace(
"document.getElementById('item_vat').value = '5';",
"document.getElementById('item_vat').value = vat;\n document.getElementById('item_category').value = category_id;\n document.getElementById('item_supplier').value = supplier_id;\n \n // Remove old image preview if any\n const oldPreview = document.getElementById('image_preview');\n if (oldPreview) oldPreview.remove();\n \n if (image_url) {\n const preview = document.createElement('img');\n preview.id = 'image_preview';\n preview.src = image_url;\n preview.style.maxHeight = '100px';\n preview.className = 'mt-2 rounded';\n document.getElementById('item_picture').parentElement.appendChild(preview);\n }",
$content
);
$content = str_replace(
"document.getElementById('item_category').value = '';\n document.getElementById('item_supplier').value = '';",
"",
$content
);
file_put_contents('stock.php', $content);

4
patch_table.php Normal file
View File

@ -0,0 +1,4 @@
<?php
$content = file_get_contents('stock.php');
$content = preg_replace('/(\s*)<td><\?= h\(\$row[\'name\']\) \?><\/td>/', "$1<td>\n$1 <?php if (!empty(\$row['image_url'])): ?>\n$1 <img src=\"<?= h(\$row['image_url']) ?>\" alt=\"\" class=\"rounded\" style=\"width: 40px; height: 40px; object-fit: cover;\">\n$1 <?php else: ?>\n$1 <div class=\"bg-light rounded d-flex align-items-center justify-content-center text-muted\" style=\"width: 40px; height: 40px;\"><i class=\"bi bi-image\"></i></div>\n$1 <?php endif; ?>\n$1</td>\n$1<td><?= h(\$row['name']) ?></td>", $content);
file_put_contents('stock.php', $content);

View File

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

61
seed_items.php Normal file
View File

@ -0,0 +1,61 @@
<?php
require 'db/config.php';
require 'includes/pexels.php';
$db = db();
// Define items to insert
$items = [
['sku' => '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";

View File

@ -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';
</div>
</div>
<form class="d-flex mb-3" method="GET" action="stock.php">
<div class="input-group" style="max-width: 400px;">
<input type="text" name="q" class="form-control" placeholder="<?= h(tr('بحث برمز الصنف أو الاسم...', 'Search by SKU or name...')) ?>" value="<?= h($search) ?>">
<button class="btn btn-outline-secondary" type="submit"><i class="bi bi-search"></i></button>
<form class="mb-4" method="GET" action="stock.php">
<div class="row g-2">
<div class="col-md-4">
<input type="text" name="q" class="form-control" placeholder="<?= h(tr('بحث برمز الصنف أو الاسم...', 'Search by SKU or name...')) ?>" value="<?= h($search) ?>">
</div>
<div class="col-md-3">
<select name="category" class="form-select">
<option value=""><?= h(tr('الكل (التصنيف)', 'All Categories')) ?></option>
<?php foreach($categories as $cat): ?>
<option value="<?= h($cat['id']) ?>" <?= ($catFilter == $cat['id']) ? 'selected' : '' ?>><?= h(current_lang() === 'ar' ? $cat['name_ar'] : $cat['name_en']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<select name="supplier" class="form-select">
<option value=""><?= h(tr('الكل (المورد)', 'All Suppliers')) ?></option>
<?php foreach($suppliers as $sup): ?>
<option value="<?= h($sup['id']) ?>" <?= ($supFilter == $sup['id']) ? 'selected' : '' ?>><?= h($sup['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<button class="btn btn-primary w-100" type="submit">
<i class="bi bi-funnel"></i> <?= h(tr('تصفية', 'Filter')) ?>
</button>
</div>
</div>
</form>
</section>
@ -77,9 +101,10 @@ require __DIR__ . '/includes/header.php';
<?php else: ?>
<div class="table-responsive">
<table class="table app-table align-middle mb-0">
<thead>
<thead class="table-primary">
<tr>
<th>SKU</th>
<th width="60"><?= h(tr('صورة', 'Pic')) ?></th>
<th><?= h(tr('الصنف', 'Product')) ?></th>
<th><?= h(tr('السعر', 'Price')) ?></th>
<th><?= h(tr('افتتاحي', 'Opening')) ?></th>
@ -109,7 +134,7 @@ require __DIR__ . '/includes/header.php';
<?php endif; ?>
</td>
<td class="text-end">
<button class="btn btn-sm btn-light text-primary border" onclick="openItemModal('<?= h($row['sku']) ?>', '<?= h(addslashes($row['name'])) ?>', '<?= h($row['price']) ?>', '<?= h($row['base_stock']) ?>')" data-bs-toggle="tooltip" title="<?= h(tr('تعديل', 'Edit')) ?>">
<button class="btn btn-sm btn-light text-primary border" onclick="openItemModal('<?= h($row['sku']) ?>', '<?= h(addslashes($row['name'])) ?>', '<?= h($row['price']) ?>', '<?= h($row['base_stock']) ?>', '<?= h($row['vat'] ?? 5) ?>', '<?= h($row['category_id'] ?? '') ?>', '<?= h($row['supplier_id'] ?? '') ?>', '<?= h($row['image_url'] ?? '') ?>')" data-bs-toggle="tooltip" title="<?= h(tr('تعديل', 'Edit')) ?>">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-light text-danger border" onclick="mockDelete()" data-bs-toggle="tooltip" title="<?= h(tr('حذف', 'Delete')) ?>">
@ -127,7 +152,7 @@ require __DIR__ . '/includes/header.php';
<ul class="pagination justify-content-center mb-0">
<?php for($i=1; $i<=$totalPages; $i++): ?>
<li class="page-item <?= $i === $page ? 'active' : '' ?>">
<a class="page-link" href="<?= h(url_for('stock.php', ['p' => $i, 'q' => $search])) ?>"><?= $i ?></a>
<a class="page-link" href="<?= h(url_for('stock.php', ['p' => $i, 'q' => $search, 'category' => $catFilter, 'supplier' => $supFilter])) ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
</ul>
@ -141,9 +166,9 @@ require __DIR__ . '/includes/header.php';
<div class="modal-dialog">
<div class="modal-content">
<form onsubmit="handleItemSubmit(event)">
<div class="modal-header">
<div class="modal-header bg-primary text-white ">
<h5 class="modal-title" id="itemModalLabel"><?= h(tr('إضافة / تعديل صنف', 'Add / Edit Item')) ?></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<button type="button" class="btn-close btn-close-white " data-bs-dismiss="modal" aria-label="Close" ></button>
</div>
<div class="modal-body">
<div class="mb-3 text-center">
@ -166,7 +191,7 @@ require __DIR__ . '/includes/header.php';
<div class="row mb-3">
<div class="col-4">
<label class="form-label"><?= h(tr('السعر', 'Price')) ?></label>
<input type="number" step="0.01" class="form-control" id="item_price" required>
<input type="number" step="0.001" class="form-control" id="item_price" required>
</div>
<div class="col-4">
<label class="form-label"><?= h(tr('الرصيد الافتتاحي', 'Opening Stock')) ?></label>
@ -174,7 +199,7 @@ require __DIR__ . '/includes/header.php';
</div>
<div class="col-4">
<label class="form-label"><?= h(tr('الضريبة (VAT %)', 'VAT %')) ?></label>
<input type="number" step="0.01" class="form-control" id="item_vat" value="5" required>
<input type="number" step="0.001" class="form-control" id="item_vat" value="5" required>
</div>
</div>
<div class="mb-3">
@ -227,15 +252,29 @@ document.addEventListener('DOMContentLoaded', function () {
itemModalObj = new bootstrap.Modal(document.getElementById('itemModal'));
});
function openItemModal(sku = '', name = '', price = '', base_stock = '') {
function openItemModal(sku = '', name = '', price = '', base_stock = '', vat = '5', category_id = '', supplier_id = '', image_url = '') {
document.getElementById('item_sku').value = sku;
document.getElementById('item_name').value = name;
document.getElementById('item_price').value = price;
document.getElementById('item_base_stock').value = base_stock;
document.getElementById('item_vat').value = '5';
document.getElementById('item_vat').value = vat;
document.getElementById('item_category').value = category_id;
document.getElementById('item_supplier').value = supplier_id;
// Remove old image preview if any
const oldPreview = document.getElementById('image_preview');
if (oldPreview) oldPreview.remove();
if (image_url) {
const preview = document.createElement('img');
preview.id = 'image_preview';
preview.src = image_url;
preview.style.maxHeight = '100px';
preview.className = 'mt-2 rounded';
document.getElementById('item_picture').parentElement.appendChild(preview);
}
document.getElementById('item_picture').value = '';
document.getElementById('item_category').value = '';
document.getElementById('item_supplier').value = '';
itemModalObj.show();
}
@ -273,4 +312,4 @@ function mockDelete() {
}
</script>
<?php require __DIR__ . '/includes/footer.php'; ?>
<?php require __DIR__ . '/includes/footer.php'; ?>

View File

@ -10,12 +10,12 @@ $allAccounts = demo_users();
$search = $_GET['q'] ?? '';
$filteredAccounts = [];
if ($search) {
$lowerSearch = mb_strtolower($search);
$lowerSearch = strtolower($search);
foreach ($allAccounts as $key => $acc) {
if (
str_contains(mb_strtolower($acc['name_ar']), $lowerSearch) ||
str_contains(mb_strtolower($acc['name_en']), $lowerSearch) ||
str_contains(mb_strtolower($acc['username']), $lowerSearch)
str_contains(strtolower((string)$acc['name_ar']), $lowerSearch) ||
str_contains(strtolower((string)$acc['name_en']), $lowerSearch) ||
str_contains(strtolower((string)$acc['username']), $lowerSearch)
) {
$filteredAccounts[$key] = $acc;
}