Add new fields to items: id_code, image, location; implement QR code generation and printing; update translations and UI

This commit is contained in:
2025-11-11 17:59:23 +01:00
parent 921a74bbe2
commit a15c976106
61 changed files with 5514 additions and 83 deletions

View File

@@ -1,13 +1,13 @@
<?php
<?php
$autoloadPath = __DIR__ . '/../vendor/autoload.php';
if (!file_exists($autoloadPath)) {
http_response_code(500);
die("FATAL ERROR: Composer autoloader not found. Please run 'composer install' in the project root.");
}
require $autoloadPath;
require $autoloadPath;
require __DIR__ . '/../config.php';
require __DIR__ . '/../config.php';
use App\Router;
use App\Controllers\ItemController;
@@ -34,17 +34,19 @@ $router->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']);

View File

@@ -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 = '<option value="">-- Select Category --</option>';
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 = '<option value="">-- No Parent --</option>';
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'
})