diff --git a/add_col.php b/add_col.php new file mode 100644 index 0000000..322fe2f --- /dev/null +++ b/add_col.php @@ -0,0 +1 @@ +exec("ALTER TABLE items ADD COLUMN id_code TEXT"); echo "done"; diff --git a/add_col2.php b/add_col2.php new file mode 100644 index 0000000..6a3100b --- /dev/null +++ b/add_col2.php @@ -0,0 +1 @@ +exec("ALTER TABLE items ADD COLUMN image TEXT"); echo "done"; diff --git a/add_col3.php b/add_col3.php new file mode 100644 index 0000000..d728564 --- /dev/null +++ b/add_col3.php @@ -0,0 +1 @@ +exec("ALTER TABLE items ADD COLUMN location TEXT"); echo "done"; diff --git a/add_columns.php b/add_columns.php new file mode 100644 index 0000000..2f933d8 --- /dev/null +++ b/add_columns.php @@ -0,0 +1,8 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +try { + ->exec('ALTER TABLE items ADD COLUMN id_code TEXT'); + echo id_code \ No newline at end of file diff --git a/check_items.php b/check_items.php new file mode 100644 index 0000000..f7a25b9 --- /dev/null +++ b/check_items.php @@ -0,0 +1,7 @@ +query('SELECT id, name FROM items'); + = ->fetchAll(PDO::FETCH_ASSOC); +print_r(); diff --git a/collections.sqlite b/collections.sqlite index f898723..637a4ec 100644 Binary files a/collections.sqlite and b/collections.sqlite differ diff --git a/composer.json b/composer.json index 05cf52f..c201a9e 100755 --- a/composer.json +++ b/composer.json @@ -4,7 +4,8 @@ "nikic/fast-route": "^1.3", "symfony/translation": "^7.3", "twig/twig": "^3.22", - "guzzlehttp/psr7": "^2.6" + "guzzlehttp/psr7": "^2.6", + "chillerlan/php-qrcode": "^4.3" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 03db270..d3e02fb 100755 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,151 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "70a9315b6260e925e9e24d4f9c5a02b3", + "content-hash": "b8e1d1dcdd0595d7ca4939c15aae5295", "packages": [ + { + "name": "chillerlan/php-qrcode", + "version": "4.4.2", + "source": { + "type": "git", + "url": "https://github.com/chillerlan/php-qrcode.git", + "reference": "345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4", + "reference": "345ed8e4ffb56e6b3fcd9f42e3970b9026fa6ce4", + "shasum": "" + }, + "require": { + "chillerlan/php-settings-container": "^2.1.6 || ^3.2.1", + "ext-mbstring": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phan/phan": "^5.4.5", + "phpmd/phpmd": "^2.15", + "phpunit/phpunit": "^9.6", + "setasign/fpdf": "^1.8.2", + "squizlabs/php_codesniffer": "^3.11" + }, + "suggest": { + "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.", + "setasign/fpdf": "Required to use the QR FPDF output.", + "simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code" + }, + "type": "library", + "autoload": { + "psr-4": { + "chillerlan\\QRCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kazuhiko Arase", + "homepage": "https://github.com/kazuhikoarase" + }, + { + "name": "Smiley", + "email": "smiley@chillerlan.net", + "homepage": "https://github.com/codemasher" + }, + { + "name": "Contributors", + "homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors" + } + ], + "description": "A QR code generator with a user friendly API. PHP 7.4+", + "homepage": "https://github.com/chillerlan/php-qrcode", + "keywords": [ + "phpqrcode", + "qr", + "qr code", + "qrcode", + "qrcode-generator" + ], + "support": { + "issues": "https://github.com/chillerlan/php-qrcode/issues", + "source": "https://github.com/chillerlan/php-qrcode/tree/4.4.2" + }, + "funding": [ + { + "url": "https://ko-fi.com/codemasher", + "type": "ko_fi" + } + ], + "time": "2024-11-15T15:36:24+00:00" + }, + { + "name": "chillerlan/php-settings-container", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/chillerlan/php-settings-container.git", + "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/95ed3e9676a1d47cab2e3174d19b43f5dbf52681", + "reference": "95ed3e9676a1d47cab2e3174d19b43f5dbf52681", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^8.1" + }, + "require-dev": { + "phpmd/phpmd": "^2.15", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-deprecation-rules": "^1.2", + "phpunit/phpunit": "^10.5", + "squizlabs/php_codesniffer": "^3.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "chillerlan\\Settings\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Smiley", + "email": "smiley@chillerlan.net", + "homepage": "https://github.com/codemasher" + } + ], + "description": "A container class for immutable settings objects. Not a DI container.", + "homepage": "https://github.com/chillerlan/php-settings-container", + "keywords": [ + "Settings", + "configuration", + "container", + "helper" + ], + "support": { + "issues": "https://github.com/chillerlan/php-settings-container/issues", + "source": "https://github.com/chillerlan/php-settings-container" + }, + "funding": [ + { + "url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4", + "type": "custom" + }, + { + "url": "https://ko-fi.com/codemasher", + "type": "ko_fi" + } + ], + "time": "2024-07-16T11:13:48+00:00" + }, { "name": "guzzlehttp/psr7", "version": "2.8.0", diff --git a/config.php b/config.php index fec43c3..a216718 100755 --- a/config.php +++ b/config.php @@ -41,3 +41,7 @@ $twig->addFunction(new \Twig\TwigFunction('trans', function (string $id, array $ $twig->addGlobal('current_locale', $locale); $twig->addGlobal('supported_locales', SUPPORTED_LOCALES); $twig->addGlobal('app_name', APP_NAME); + +// Add translated confirm messages for JS +$twig->addGlobal('delete_part_confirm', $translator->trans('Are you sure you want to delete this part?')); +$twig->addGlobal('delete_category_confirm', $translator->trans('Are you sure you want to delete this category?')); diff --git a/fix_db.php b/fix_db.php new file mode 100644 index 0000000..7b2b6f9 --- /dev/null +++ b/fix_db.php @@ -0,0 +1,24 @@ +prepare('UPDATE categories SET parent_id = NULL WHERE id = :id'); + $stmt->execute([':id' => $cat['id']]); + echo "Fixed by setting parent_id to NULL\n"; + } +} + +echo "Done checking and fixing circular references.\n"; diff --git a/lang/en.json b/lang/en.json index e443680..6537978 100755 --- a/lang/en.json +++ b/lang/en.json @@ -54,5 +54,14 @@ "Edit Category": "Edit Category", "Edit Part": "Edit Part", "Save Changes": "Save Changes", - "Cancel": "Cancel" + "Cancel": "Cancel", + "A category cannot be its own parent": "A category cannot be its own parent", + "Cannot set a descendant as parent (circular reference)": "Cannot set a descendant as parent (circular reference)", + "Category not found": "Category not found", + "Server error": "Server error", + "Part not found": "Part not found", + "Are you sure you want to delete this part?": "Are you sure you want to delete this part?", + "Are you sure you want to delete this category?": "Are you sure you want to delete this category?", + "Image": "Image", + "Location": "Location" } \ No newline at end of file diff --git a/lang/nl.json b/lang/nl.json index fb240c0..87d2cb8 100755 --- a/lang/nl.json +++ b/lang/nl.json @@ -54,5 +54,14 @@ "Edit Category": "Categorie Bewerken", "Edit Part": "Onderdeel Bewerken", "Save Changes": "Wijzigingen Opslaan", - "Cancel": "Annuleren" + "Cancel": "Annuleren", + "A category cannot be its own parent": "Een categorie kan niet zijn eigen bovenliggende zijn", + "Cannot set a descendant as parent (circular reference)": "Kan geen afstammeling als bovenliggende instellen (circulaire referentie)", + "Category not found": "Categorie niet gevonden", + "Server error": "Server fout", + "Part not found": "Onderdeel niet gevonden", + "Are you sure you want to delete this part?": "Weet je zeker dat je dit onderdeel wilt verwijderen?", + "Are you sure you want to delete this category?": "Weet je zeker dat je deze categorie wilt verwijderen?", + "Image": "Afbeelding", + "Location": "Locatie" } \ No newline at end of file diff --git a/migrate_db.php b/migrate_db.php new file mode 100644 index 0000000..53690d9 --- /dev/null +++ b/migrate_db.php @@ -0,0 +1,9 @@ +exec('ALTER TABLE items ADD COLUMN id_code TEXT UNIQUE'); +$db->exec('ALTER TABLE items ADD COLUMN image TEXT'); +$db->exec('ALTER TABLE items ADD COLUMN location TEXT'); +echo 'Columns added to items table.\n'; diff --git a/public/index.php b/public/index.php index 2e8557e..c35ba0f 100644 --- a/public/index.php +++ b/public/index.php @@ -1,13 +1,13 @@ -addRoute('GET', '/lang/{locale}', function ($locale) { $router->addRoute('GET', '/', [ItemController::class, 'overview']); $router->addRoute('GET', '/categories', [CategoryController::class, 'index']); $router->addRoute('GET', '/parts', [ItemController::class, 'addForm']); +$router->addRoute('GET', '/print/{id:\d+}', [ItemController::class, 'printQR']); // --- API Routes (AJAX Content) --- // These routes return only the Twig block content, not the full layout. $router->addRoute('GET', '/api/items', [ItemController::class, 'listItems']); +$router->addRoute('GET', '/api/items/{id:\d+}', [ItemController::class, 'getItem']); $router->addRoute('GET', '/api/categories', [CategoryController::class, 'listCategories']); -$router->addRoute('GET', '/api/categories/{id:\d+}', [CategoryController::class, 'getCategory']); +$router->addRoute('GET', '/api/categories/list', [CategoryController::class, 'listCategoriesJson']); +$router->addRoute('GET', '/api/categories/{id}', [CategoryController::class, 'getCategory']); $router->addRoute('GET', '/api/parts', [ItemController::class, 'renderAddForm']); // --- API CRUD Routes --- // Items -$router->addRoute('GET', '/api/items/{id:\d+}', [ItemController::class, 'getItem']); $router->addRoute('POST', '/api/items', [ItemController::class, 'create']); $router->addRoute('PUT', '/api/items/{id:\d+}', [ItemController::class, 'update']); $router->addRoute('DELETE', '/api/items/{id:\d+}', [ItemController::class, 'delete']); diff --git a/public/js/app.js b/public/js/app.js index 9a3e24a..203b320 100755 --- a/public/js/app.js +++ b/public/js/app.js @@ -159,7 +159,7 @@ document.addEventListener('DOMContentLoaded', function() { .then(response => response.json()) .then(data => { if (data.success) { - fetchContent('/parts', false); + fetchContent('/', false); // Go to overview to see the new part } else { alert(data.error || 'Error adding part'); } @@ -226,9 +226,23 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('edit_item_id').value = data.id; document.getElementById('edit_item_name').value = data.name; document.getElementById('edit_item_description').value = data.description || ''; - document.getElementById('edit_item_category_id').value = data.category_id || ''; - const modal = new bootstrap.Modal(document.getElementById('editItemModal')); - modal.show(); + document.getElementById('edit_location').value = data.location || ''; + // Populate category select + const categorySelect = document.getElementById('edit_item_category_id'); + categorySelect.innerHTML = ''; + fetch('/api/categories/list') + .then(resp => resp.json()) + .then(categories => { + categories.forEach(cat => { + const option = document.createElement('option'); + option.value = cat.id; + option.textContent = cat.path; + categorySelect.appendChild(option); + }); + categorySelect.value = data.category_id || ''; + const modal = new bootstrap.Modal(document.getElementById('editItemModal')); + modal.show(); + }); } else { alert('Part not found'); } @@ -241,7 +255,7 @@ document.addEventListener('DOMContentLoaded', function() { function handleDeleteItem() { const id = this.getAttribute('data-id'); - if (confirm('Are you sure you want to delete this part?')) { + if (confirm(window.translations.deletePartConfirm)) { fetch('/api/items/' + id, { method: 'DELETE' }) @@ -285,30 +299,32 @@ document.addEventListener('DOMContentLoaded', function() { const name = document.getElementById('edit_item_name').value.trim(); const description = document.getElementById('edit_item_description').value; const categoryId = document.getElementById('edit_item_category_id').value; + const location = document.getElementById('edit_location').value.trim(); + const imageFile = document.getElementById('edit_image').files[0]; if (!name) { alert('Part name is required'); return; } - const data = { - item_name: name, - item_description: description, - category_id: categoryId || null - }; + const formData = new FormData(); + formData.append('item_name', name); + formData.append('item_description', description); + formData.append('category_id', categoryId || ''); + formData.append('location', location); + if (imageFile) { + formData.append('image', imageFile); + } fetch('/api/items/' + id, { method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) + body: formData }) .then(response => response.json()) .then(data => { if (data.success) { bootstrap.Modal.getInstance(document.getElementById('editItemModal')).hide(); - fetchContent('/overview', false); // Assuming current is overview + fetchContent('/', false); // Reload overview } else { alert(data.error || 'Error updating part'); } @@ -360,27 +376,47 @@ document.addEventListener('DOMContentLoaded', function() { const id = this.getAttribute('data-id'); // Fetch current category data fetch('/api/categories/' + id) - .then(response => response.json()) + .then(response => { + if (!response.ok) { + throw new Error('HTTP ' + response.status + ': ' + response.statusText); + } + return response.json(); + }) .then(data => { - if (data) { + if (data && !data.error) { document.getElementById('edit_category_id').value = data.id; document.getElementById('edit_category_name').value = data.name; - document.getElementById('edit_parent_category_id').value = data.parent_id || ''; - const modal = new bootstrap.Modal(document.getElementById('editCategoryModal')); - modal.show(); + // Populate parent select, excluding current category + const parentSelect = document.getElementById('edit_parent_category_id'); + parentSelect.innerHTML = ''; + fetch('/api/categories/list') + .then(resp => resp.json()) + .then(categories => { + categories.forEach(cat => { + if (cat.id != data.id) { + const option = document.createElement('option'); + option.value = cat.id; + option.textContent = cat.path; + parentSelect.appendChild(option); + } + }); + parentSelect.value = data.parent_id || ''; + const modal = new bootstrap.Modal(document.getElementById('editCategoryModal')); + modal.show(); + }); } else { - alert('Category not found'); + alert(data.error || 'Category not found'); } }) .catch(error => { console.error('Error:', error); - alert('Error fetching category'); + alert('Error fetching category: ' + error.message); }); } function handleDeleteCategory() { const id = this.getAttribute('data-id'); - if (confirm('Are you sure you want to delete this category?')) { + if (confirm(window.translations.deleteCategoryConfirm)) { fetch('/api/categories/' + id, { method: 'DELETE' }) diff --git a/src/Controllers/CategoryController.php b/src/Controllers/CategoryController.php index ccfa6e9..f47c8cf 100755 --- a/src/Controllers/CategoryController.php +++ b/src/Controllers/CategoryController.php @@ -14,6 +14,8 @@ class CategoryController echo $twig->render('layout.twig', ['active_page' => 'categories']); } + + // Get single category (JSON) public static function getCategory($id) { header('Content-Type: application/json'); @@ -24,8 +26,24 @@ class CategoryController echo json_encode($category); } else { http_response_code(404); - echo json_encode(['error' => 'Category not found']); + global $translator; + echo json_encode(['error' => $translator->trans('Category not found')]); } + } catch (Exception $e) { + error_log('Error in getCategory: ' . $e->getMessage()); + http_response_code(500); + global $translator; + echo json_encode(['error' => $translator->trans('Server error')]); + } + } + + // Returns JSON list of categories + public static function listCategoriesJson() { + header('Content-Type: application/json'); + try { + $db = Database::getInstance(); + $categories = Category::getAll($db); + echo json_encode($categories); } catch (Exception $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); @@ -63,7 +81,8 @@ class CategoryController if (empty($name)) { http_response_code(400); - echo json_encode(['error' => 'Category name is required']); + global $translator; + echo json_encode(['error' => $translator->trans('Category name is required')]); return; } @@ -88,19 +107,39 @@ class CategoryController $existing = Category::getById($db, $id); if (!$existing) { http_response_code(404); - echo json_encode(['error' => 'Category not found']); + global $translator; + echo json_encode(['error' => $translator->trans('Category not found')]); return; } $name = trim($data['category_name'] ?? $existing['name']); + $parentId = !empty($data['parent_category_id']) ? (int)$data['parent_category_id'] : null; if (empty($name)) { http_response_code(400); - echo json_encode(['error' => 'Category name is required']); + global $translator; + echo json_encode(['error' => $translator->trans('Category name is required')]); return; } - $category = new Category($db, $id, $name); + // Prevent circular references + if ($parentId !== null) { + if ($parentId == $id) { + http_response_code(400); + global $translator; + echo json_encode(['error' => $translator->trans('A category cannot be its own parent')]); + return; + } + $descendants = Category::getDescendantIds($db, $id); + if (in_array($parentId, $descendants)) { + http_response_code(400); + global $translator; + echo json_encode(['error' => $translator->trans('Cannot set a descendant as parent (circular reference)')]); + return; + } + } + + $category = new Category($db, $id, $name, $parentId); if ($category->save()) { echo json_encode(['success' => true, 'message' => 'Category updated successfully']); } else { @@ -120,7 +159,8 @@ class CategoryController echo json_encode(['success' => true, 'message' => 'Category deleted successfully']); } else { http_response_code(404); - echo json_encode(['error' => 'Category not found']); + global $translator; + echo json_encode(['error' => $translator->trans('Category not found')]); } } catch (Exception $e) { http_response_code(500); diff --git a/src/Controllers/ItemController.php b/src/Controllers/ItemController.php index eb2ea61..7dbddc5 100755 --- a/src/Controllers/ItemController.php +++ b/src/Controllers/ItemController.php @@ -5,6 +5,8 @@ namespace App\Controllers; use App\Database; use App\Models\Item; use App\Models\Category; +use chillerlan\QRCode\QRCode; +use chillerlan\QRCode\QROptions; class ItemController { @@ -12,7 +14,11 @@ class ItemController public static function overview() { global $twig; - echo $twig->render('layout.twig', ['active_page' => 'overview']); + try { + echo $twig->render('layout.twig', ['active_page' => 'overview']); + } catch (Exception $e) { + echo "Twig Error: " . $e->getMessage(); + } } // Renders the full layout for add parts page @@ -32,11 +38,14 @@ class ItemController echo json_encode($item); } else { http_response_code(404); - echo json_encode(['error' => 'Part not found']); + global $translator; + echo json_encode(['error' => $translator->trans('Part not found')]); } } catch (Exception $e) { + error_log('Error in getItem: ' . $e->getMessage()); http_response_code(500); - echo json_encode(['error' => $e->getMessage()]); + global $translator; + echo json_encode(['error' => $translator->trans('Server error')]); } } @@ -69,6 +78,13 @@ class ItemController $categoryId = !empty($_GET['category_id']) ? (int)$_GET['category_id'] : null; $items = Item::getAllFiltered($db, $search, $categoryId); + foreach ($items as &$item) { + if ($item['category_id']) { + $item['category_path'] = Category::getFullPath($db, $item['category_id']); + } else { + $item['category_path'] = null; + } + } $categories = Category::getAll($db); // Render only the content block @@ -79,8 +95,8 @@ class ItemController 'selected_category' => $categoryId, ]); } catch (\Exception $e) { + error_log("Error in listItems: " . $e->getMessage()); http_response_code(500); - error_log("Error fetching items: " . $e->getMessage()); echo "Error: Could not load items."; } } @@ -95,15 +111,28 @@ class ItemController $name = trim($data['item_name'] ?? ''); $description = trim($data['item_description'] ?? ''); $categoryId = !empty($data['category_id']) ? (int)$data['category_id'] : null; + $location = trim($data['location'] ?? ''); if (empty($name)) { http_response_code(400); - echo json_encode(['error' => 'Part name is required']); + global $translator; + echo json_encode(['error' => $translator->trans('Part name is required')]); return; } - $item = new Item($db, null, $name, $description, $categoryId); + // Generate unique id_code + $idCode = self::generateUniqueIdCode($db); + + // Handle image upload + $imagePath = null; + if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) { + $imagePath = self::handleImageUpload($_FILES['image']); + } + + $item = new Item($db, null, $name, $description, $categoryId, null, $idCode, $imagePath, $location); if ($item->save()) { + // Generate QR code + self::generateQRCode($idCode); echo json_encode(['success' => true, 'message' => 'Part added successfully']); } else { throw new Exception('Failed to save part'); @@ -114,6 +143,55 @@ class ItemController } } + private static function generateUniqueIdCode(PDO $db): string { + do { + $code = strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 8)); + $stmt = $db->prepare('SELECT id FROM items WHERE id_code = :code'); + $stmt->execute([':code' => $code]); + } while ($stmt->fetch()); + return $code; + } + + private static function handleImageUpload(array $file): ?string { + $uploadDir = __DIR__ . '/../../public/uploads/'; + if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0755, true); + } + $fileName = uniqid() . '_' . basename($file['name']); + $targetPath = $uploadDir . $fileName; + if (move_uploaded_file($file['tmp_name'], $targetPath)) { + return 'uploads/' . $fileName; + } + return null; + } + + private static function generateQRCode(string $idCode): void { + $qrDir = __DIR__ . '/../../public/uploads/qr/'; + if (!is_dir($qrDir)) { + mkdir($qrDir, 0755, true); + } + $options = new QROptions([ + 'outputType' => QRCode::OUTPUT_IMAGE_PNG, + 'eccLevel' => QRCode::ECC_L, + 'imageTransparent' => false, + 'imageBase64' => false, + ]); + $qrCode = new QRCode($options); + $qrCode->render($idCode, $qrDir . $idCode . '.png'); + } + + public static function printQR($id) { + global $twig; + $db = Database::getInstance(); + $item = Item::getById($db, $id); + if (!$item) { + http_response_code(404); + echo "Item not found"; + return; + } + echo $twig->render('print_qr.twig', ['item' => $item]); + } + public static function update($id) { header('Content-Type: application/json'); try { @@ -123,21 +201,31 @@ class ItemController $existing = Item::getById($db, $id); if (!$existing) { http_response_code(404); - echo json_encode(['error' => 'Part not found']); + global $translator; + echo json_encode(['error' => $translator->trans('Part not found')]); return; } $name = trim($data['item_name'] ?? $existing['name']); $description = trim($data['item_description'] ?? $existing['description']); $categoryId = isset($data['category_id']) && $data['category_id'] !== '' ? (int)$data['category_id'] : $existing['category_id']; + $location = trim($data['location'] ?? $existing['location']); + $idCode = $existing['id_code']; // Keep existing id_code + $imagePath = $existing['image']; + + // Handle image upload if new file + if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) { + $imagePath = self::handleImageUpload($_FILES['image']); + } if (empty($name)) { http_response_code(400); - echo json_encode(['error' => 'Part name is required']); + global $translator; + echo json_encode(['error' => $translator->trans('Part name is required')]); return; } - $item = new Item($db, $id, $name, $description, $categoryId); + $item = new Item($db, $id, $name, $description, $categoryId, null, $idCode, $imagePath, $location); if ($item->save()) { echo json_encode(['success' => true, 'message' => 'Part updated successfully']); } else { @@ -157,7 +245,8 @@ class ItemController echo json_encode(['success' => true, 'message' => 'Part deleted successfully']); } else { http_response_code(404); - echo json_encode(['error' => 'Part not found']); + global $translator; + echo json_encode(['error' => $translator->trans('Part not found')]); } } catch (Exception $e) { http_response_code(500); diff --git a/src/Models/Category.php b/src/Models/Category.php index 8e6e982..cf70907 100755 --- a/src/Models/Category.php +++ b/src/Models/Category.php @@ -53,16 +53,36 @@ class Category { } } - public static function getFullPath(PDO $db, int $id, string $separator = ' / '): string { + public static function getFullPath(PDO $db, int $id, string $separator = ' / ', array &$visited = []): string { + if (in_array($id, $visited)) { + return '[Circular]'; // Indicate circular reference + } $category = self::getById($db, $id); if (!$category) return ''; if ($category['parent_id']) { - return self::getFullPath($db, $category['parent_id'], $separator) . $separator . $category['name']; + $visited[] = $id; + return self::getFullPath($db, $category['parent_id'], $separator, $visited) . $separator . $category['name']; } else { return $category['name']; } } + public static function getDescendantIds(PDO $db, int $id, array &$visited = []): array { + if (in_array($id, $visited)) { + return []; // Prevent infinite recursion + } + $visited[] = $id; + $descendants = []; + $stmt = $db->prepare('SELECT id FROM categories WHERE parent_id = :id'); + $stmt->execute([':id' => $id]); + $children = $stmt->fetchAll(PDO::FETCH_COLUMN); + foreach ($children as $childId) { + $descendants[] = $childId; + $descendants = array_merge($descendants, self::getDescendantIds($db, $childId, $visited)); + } + return $descendants; + } + public static function getById(PDO $db, int $id): ?array { try { $stmt = $db->prepare('SELECT id, name, parent_id FROM categories WHERE id = :id'); diff --git a/src/Models/Item.php b/src/Models/Item.php index eeab060..b374951 100755 --- a/src/Models/Item.php +++ b/src/Models/Item.php @@ -12,14 +12,20 @@ class Item { private ?string $description; private ?int $categoryId; private ?string $categoryName; + private ?string $idCode; + private ?string $image; + private ?string $location; - public function __construct(PDO $db, ?int $id = null, string $name = '', ?string $description = null, ?int $categoryId = null, ?string $categoryName = null) { + public function __construct(PDO $db, ?int $id = null, string $name = '', ?string $description = null, ?int $categoryId = null, ?string $categoryName = null, ?string $idCode = null, ?string $image = null, ?string $location = null) { $this->db = $db; $this->id = $id; $this->name = $name; $this->description = $description; $this->categoryId = $categoryId; $this->categoryName = $categoryName; + $this->idCode = $idCode; + $this->image = $image; + $this->location = $location; } public function getId(): ?int { @@ -54,15 +60,39 @@ class Item { return $this->categoryName; } + public function getIdCode(): ?string { + return $this->idCode; + } + + public function setIdCode(?string $idCode): void { + $this->idCode = $idCode; + } + + public function getImage(): ?string { + return $this->image; + } + + public function setImage(?string $image): void { + $this->image = $image; + } + + public function getLocation(): ?string { + return $this->location; + } + + public function setLocation(?string $location): void { + $this->location = $location; + } + public static function getAll(PDO $db): array { return self::getAllFiltered($db, '', null); } public static function getAllFiltered(PDO $db, string $search, ?int $categoryId): array { try { - $query = 'SELECT i.id, i.name, i.description, c.name as category_name, i.category_id - FROM items i - LEFT JOIN categories c ON i.category_id = c.id + $query = 'SELECT i.id, i.name, i.description, c.name as category_name, i.category_id, i.id_code, i.image, i.location + FROM items i + LEFT JOIN categories c ON i.category_id = c.id WHERE 1=1'; $params = []; @@ -90,9 +120,9 @@ class Item { public static function getById(PDO $db, int $id): ?array { try { $stmt = $db->prepare( - 'SELECT i.id, i.name, i.description, c.name as category_name, i.category_id - FROM items i - LEFT JOIN categories c ON i.category_id = c.id + 'SELECT i.id, i.name, i.description, c.name as category_name, i.category_id, i.id_code, i.image, i.location + FROM items i + LEFT JOIN categories c ON i.category_id = c.id WHERE i.id = :id' ); $stmt->execute([':id' => $id]); @@ -109,13 +139,16 @@ class Item { // Add new item try { $stmt = $this->db->prepare( - 'INSERT INTO items (name, description, category_id) - VALUES (:name, :description, :category_id)' + 'INSERT INTO items (name, description, category_id, id_code, image, location) + VALUES (:name, :description, :category_id, :id_code, :image, :location)' ); $stmt->execute([ ':name' => $this->name, ':description' => $this->description, - ':category_id' => $this->categoryId + ':category_id' => $this->categoryId, + ':id_code' => $this->idCode, + ':image' => $this->image, + ':location' => $this->location ]); $this->id = (int)$this->db->lastInsertId(); return true; @@ -127,14 +160,17 @@ class Item { // Update item (optional, not implemented in current controllers) try { $stmt = $this->db->prepare( - 'UPDATE items - SET name = :name, description = :description, category_id = :category_id + 'UPDATE items + SET name = :name, description = :description, category_id = :category_id, id_code = :id_code, image = :image, location = :location WHERE id = :id' ); $stmt->execute([ ':name' => $this->name, ':description' => $this->description, ':category_id' => $this->categoryId, + ':id_code' => $this->idCode, + ':image' => $this->image, + ':location' => $this->location, ':id' => $this->id ]); return true; diff --git a/templates/items.twig b/templates/items.twig index 32f7231..c45c5fe 100755 --- a/templates/items.twig +++ b/templates/items.twig @@ -32,16 +32,23 @@