From a15c976106ada21184c1180c8184ce5146cf9e08 Mon Sep 17 00:00:00 2001
From: Edwin Noorlander
Date: Tue, 11 Nov 2025 17:59:23 +0100
Subject: [PATCH] Add new fields to items: id_code, image, location; implement
QR code generation and printing; update translations and UI
---
add_col.php | 1 +
add_col2.php | 1 +
add_col3.php | 1 +
add_columns.php | 8 +
check_items.php | 7 +
collections.sqlite | Bin 20480 -> 20480 bytes
composer.json | 3 +-
composer.lock | 145 +++-
config.php | 4 +
fix_db.php | 24 +
lang/en.json | 11 +-
lang/nl.json | 11 +-
migrate_db.php | 9 +
public/index.php | 12 +-
public/js/app.js | 82 +-
src/Controllers/CategoryController.php | 52 +-
src/Controllers/ItemController.php | 109 ++-
src/Models/Category.php | 24 +-
src/Models/Item.php | 60 +-
templates/items.twig | 55 +-
templates/layout.twig | 9 +-
templates/parts.twig | 8 +
templates/print_qr.twig | 32 +
test_trans.php | 1 +
.../inspectionProfiles/Project_Default.xml | 6 +
vendor/chillerlan/php-qrcode/LICENSE | 21 +
vendor/chillerlan/php-qrcode/README.md | 429 ++++++++++
vendor/chillerlan/php-qrcode/composer.json | 62 ++
.../php-qrcode/src/Data/AlphaNum.php | 60 ++
.../chillerlan/php-qrcode/src/Data/Byte.php | 44 +
.../chillerlan/php-qrcode/src/Data/Kanji.php | 69 ++
.../php-qrcode/src/Data/MaskPatternTester.php | 203 +++++
.../chillerlan/php-qrcode/src/Data/Number.php | 77 ++
.../src/Data/QRCodeDataException.php | 17 +
.../php-qrcode/src/Data/QRDataAbstract.php | 311 +++++++
.../php-qrcode/src/Data/QRDataInterface.php | 200 +++++
.../php-qrcode/src/Data/QRMatrix.php | 779 ++++++++++++++++++
.../php-qrcode/src/Helpers/BitBuffer.php | 89 ++
.../php-qrcode/src/Helpers/Polynomial.php | 178 ++++
.../src/Output/QRCodeOutputException.php | 17 +
.../php-qrcode/src/Output/QRFpdf.php | 113 +++
.../php-qrcode/src/Output/QRImage.php | 217 +++++
.../php-qrcode/src/Output/QRImagick.php | 119 +++
.../php-qrcode/src/Output/QRMarkup.php | 162 ++++
.../src/Output/QROutputAbstract.php | 130 +++
.../src/Output/QROutputInterface.php | 57 ++
.../php-qrcode/src/Output/QRString.php | 76 ++
vendor/chillerlan/php-qrcode/src/QRCode.php | 313 +++++++
.../php-qrcode/src/QRCodeException.php | 20 +
.../chillerlan/php-qrcode/src/QROptions.php | 54 ++
.../php-qrcode/src/QROptionsTrait.php | 341 ++++++++
.../chillerlan/php-settings-container/LICENSE | 21 +
.../php-settings-container/README.md | 167 ++++
.../php-settings-container/composer.json | 52 ++
.../rules-magic-access.neon | 4 +
.../src/SettingsContainerAbstract.php | 252 ++++++
.../src/SettingsContainerInterface.php | 86 ++
vendor/composer/autoload_psr4.php | 2 +
vendor/composer/autoload_static.php | 13 +
vendor/composer/installed.json | 149 ++++
vendor/composer/installed.php | 18 +
61 files changed, 5514 insertions(+), 83 deletions(-)
create mode 100644 add_col.php
create mode 100644 add_col2.php
create mode 100644 add_col3.php
create mode 100644 add_columns.php
create mode 100644 check_items.php
create mode 100644 fix_db.php
create mode 100644 migrate_db.php
create mode 100644 templates/print_qr.twig
create mode 100644 test_trans.php
create mode 100644 vendor/chillerlan/php-qrcode/.idea/inspectionProfiles/Project_Default.xml
create mode 100644 vendor/chillerlan/php-qrcode/LICENSE
create mode 100644 vendor/chillerlan/php-qrcode/README.md
create mode 100644 vendor/chillerlan/php-qrcode/composer.json
create mode 100644 vendor/chillerlan/php-qrcode/src/Data/AlphaNum.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Data/Byte.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Data/Kanji.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Data/MaskPatternTester.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Data/Number.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Data/QRCodeDataException.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Data/QRDataAbstract.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Data/QRDataInterface.php
create mode 100755 vendor/chillerlan/php-qrcode/src/Data/QRMatrix.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Helpers/BitBuffer.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Helpers/Polynomial.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Output/QRCodeOutputException.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Output/QRFpdf.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Output/QRImage.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Output/QRImagick.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Output/QRMarkup.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Output/QROutputAbstract.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Output/QROutputInterface.php
create mode 100644 vendor/chillerlan/php-qrcode/src/Output/QRString.php
create mode 100755 vendor/chillerlan/php-qrcode/src/QRCode.php
create mode 100644 vendor/chillerlan/php-qrcode/src/QRCodeException.php
create mode 100644 vendor/chillerlan/php-qrcode/src/QROptions.php
create mode 100644 vendor/chillerlan/php-qrcode/src/QROptionsTrait.php
create mode 100644 vendor/chillerlan/php-settings-container/LICENSE
create mode 100644 vendor/chillerlan/php-settings-container/README.md
create mode 100644 vendor/chillerlan/php-settings-container/composer.json
create mode 100644 vendor/chillerlan/php-settings-container/rules-magic-access.neon
create mode 100644 vendor/chillerlan/php-settings-container/src/SettingsContainerAbstract.php
create mode 100644 vendor/chillerlan/php-settings-container/src/SettingsContainerInterface.php
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 f89872316f0226d4af7799d2596e5abb1aae7e35..637a4ec90b722df6e4a61baa1964a0cd41f0461b 100644
GIT binary patch
delta 324
zcmZozz}T>Wae}mqH%6QAq~9vQA$99}FzKB@BE&_z&|f<1Ja($jjBF
z&%!P)F3#9uJ9#OuiY6y!;Os
znE5X=@n7bD$^T`upukanYi>ps21&-;#DapXR3@P9oM0DlGO;j-GM1zkmoRfM1DOn&
zsU^h>3|v4-X8tD({7?A5@xKKcaE)JsgISajBnwms(ZR&YEXfHmkQb<)iT^1B{|El3
zK-D+-RXLfM#The8Qge%$_!yZL8Iu!BQq%K`GE4o|2rEQJkKZRh*HYSCO2QSCyO##7VhK0PmYn;s5{u
delta 180
zcmZozz}T>Wae}lUGXnzyD-go~(?lI(QDz3cvQA$99}FzKa~b%4@E_(|#yfYjpnxVX
zR}&ixySTVGV{`W8I^M0DWB8Ubasu@-@_%FC|F&7s;0pi52@)b4j4TYI3?-?>C0t<1
z9}N6IppsHT?98H^ASqt3)JF#Xk5DOLQC>!7Mb6~JlGOD4qRiA{kO~F?sH_?QJS{7*
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 @@
{% if items %}
{% for item in items %}
- -
-
-
{{ item.name }}
-
{{ trans('Category') }}: {{ item.category_name ?: trans('Uncategorized') }}
-
{{ item.description }}
-
-
-
-
-
+ -
+
+
{{ item.name }} ({{ item.id_code }})
+
{{ trans('Category') }}: {{ item.category_path ?: trans('Uncategorized') }}
+
{{ item.description }}
+ {% if item.image %}
+

+ {% endif %}
+ {% if item.location %}
+
{{ trans('Location') }}: {{ item.location }}
+ {% endif %}
+
+
+
+
+
+
{% endfor %}
{% else %}
@@ -68,16 +75,24 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
{{ item.name }}
+
ID Code: {{ item.id_code }}
+

+
+
Description: {{ item.description }}
+
Category: {{ item.category_path ?: 'Uncategorized' }}
+ {% if item.location %}
+
Location: {{ item.location }}
+ {% endif %}
+
+
+
+
diff --git a/templates/parts.twig b/templates/parts.twig
index 925bb94..bc3eace 100644
--- a/templates/parts.twig
+++ b/templates/parts.twig
@@ -22,6 +22,14 @@
{% endfor %}
+
diff --git a/templates/print_qr.twig b/templates/print_qr.twig
new file mode 100644
index 0000000..9522819
--- /dev/null
+++ b/templates/print_qr.twig
@@ -0,0 +1,32 @@
+{% autoescape %}
+
+
+