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

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

View File

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

View File

@@ -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');

View File

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