Add new fields to items: id_code, image, location; implement QR code generation and printing; update translations and UI
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user