first commit

This commit is contained in:
2025-11-11 17:00:02 +01:00
commit 921a74bbe2
549 changed files with 59325 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
<?php
namespace App\Controllers;
use App\Database;
use App\Models\Category;
class CategoryController
{
// Renders the full layout for categories page load
public static function index()
{
global $twig;
echo $twig->render('layout.twig', ['active_page' => 'categories']);
}
// Get single category (JSON)
public static function getCategory($id) {
header('Content-Type: application/json');
try {
$db = Database::getInstance();
$category = Category::getById($db, $id);
if ($category) {
echo json_encode($category);
} else {
http_response_code(404);
echo json_encode(['error' => 'Category not found']);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
}
// Returns the HTML content for the categories page (used by AJAX)
public static function listCategories()
{
global $twig;
try {
$db = Database::getInstance();
$categories = Category::getAll($db);
// Render only the content block
echo $twig->render('categories.twig', [
'categories' => $categories,
]);
} catch (\Exception $e) {
http_response_code(500);
error_log("Error fetching categories: " . $e->getMessage());
echo "Error: " . $e->getMessage();
}
}
// CRUD operations (returns JSON)
public static function create() {
header('Content-Type: application/json');
try {
$db = Database::getInstance();
$data = $_POST;
$name = trim($data['new_category_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']);
return;
}
$category = new Category($db, null, $name, $parentId);
if ($category->save()) {
echo json_encode(['success' => true, 'message' => 'Category added successfully', 'id' => $category->getId()]);
} else {
throw new Exception('Failed to save category');
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
}
public static function update($id) {
header('Content-Type: application/json');
try {
$db = Database::getInstance();
$data = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$existing = Category::getById($db, $id);
if (!$existing) {
http_response_code(404);
echo json_encode(['error' => 'Category not found']);
return;
}
$name = trim($data['category_name'] ?? $existing['name']);
if (empty($name)) {
http_response_code(400);
echo json_encode(['error' => 'Category name is required']);
return;
}
$category = new Category($db, $id, $name);
if ($category->save()) {
echo json_encode(['success' => true, 'message' => 'Category updated successfully']);
} else {
throw new Exception('Failed to update category');
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
}
public static function delete($id) {
header('Content-Type: application/json');
try {
$db = Database::getInstance();
if (Category::delete($db, $id)) {
echo json_encode(['success' => true, 'message' => 'Category deleted successfully']);
} else {
http_response_code(404);
echo json_encode(['error' => 'Category not found']);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace App\Controllers;
use App\Database;
use App\Models\Item;
use App\Models\Category;
class ItemController
{
// Renders the full layout for overview page load
public static function overview()
{
global $twig;
echo $twig->render('layout.twig', ['active_page' => 'overview']);
}
// Renders the full layout for add parts page
public static function addForm()
{
global $twig;
echo $twig->render('layout.twig', ['active_page' => 'parts']);
}
// Get single item (JSON)
public static function getItem($id) {
header('Content-Type: application/json');
try {
$db = Database::getInstance();
$item = Item::getById($db, $id);
if ($item) {
echo json_encode($item);
} else {
http_response_code(404);
echo json_encode(['error' => 'Part not found']);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
}
// Returns the HTML content for the add parts page (used by AJAX)
public static function renderAddForm()
{
global $twig;
try {
$db = Database::getInstance();
$categories = Category::getAll($db);
// Render only the content block
echo $twig->render('parts.twig', [
'categories' => $categories,
]);
} catch (\Exception $e) {
http_response_code(500);
error_log("Error rendering add form: " . $e->getMessage());
echo "Error: Could not load add form.";
}
}
// Returns the HTML content for the items page (used by AJAX)
public static function listItems()
{
global $twig;
try {
$db = Database::getInstance();
$search = trim($_GET['search'] ?? '');
$categoryId = !empty($_GET['category_id']) ? (int)$_GET['category_id'] : null;
$items = Item::getAllFiltered($db, $search, $categoryId);
$categories = Category::getAll($db);
// Render only the content block
echo $twig->render('items.twig', [
'items' => $items,
'categories' => $categories,
'search' => $search,
'selected_category' => $categoryId,
]);
} catch (\Exception $e) {
http_response_code(500);
error_log("Error fetching items: " . $e->getMessage());
echo "Error: Could not load items.";
}
}
// CRUD operations (returns JSON)
public static function create() {
header('Content-Type: application/json');
try {
$db = Database::getInstance();
$data = $_POST;
$name = trim($data['item_name'] ?? '');
$description = trim($data['item_description'] ?? '');
$categoryId = !empty($data['category_id']) ? (int)$data['category_id'] : null;
if (empty($name)) {
http_response_code(400);
echo json_encode(['error' => 'Part name is required']);
return;
}
$item = new Item($db, null, $name, $description, $categoryId);
if ($item->save()) {
echo json_encode(['success' => true, 'message' => 'Part added successfully']);
} else {
throw new Exception('Failed to save part');
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
}
public static function update($id) {
header('Content-Type: application/json');
try {
$db = Database::getInstance();
$data = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$existing = Item::getById($db, $id);
if (!$existing) {
http_response_code(404);
echo json_encode(['error' => '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'];
if (empty($name)) {
http_response_code(400);
echo json_encode(['error' => 'Part name is required']);
return;
}
$item = new Item($db, $id, $name, $description, $categoryId);
if ($item->save()) {
echo json_encode(['success' => true, 'message' => 'Part updated successfully']);
} else {
throw new Exception('Failed to update part');
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
}
public static function delete($id) {
header('Content-Type: application/json');
try {
$db = Database::getInstance();
if (Item::delete($db, $id)) {
echo json_encode(['success' => true, 'message' => 'Part deleted successfully']);
} else {
http_response_code(404);
echo json_encode(['error' => 'Part not found']);
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
}
}

46
src/Database.php Executable file
View File

@@ -0,0 +1,46 @@
<?php
namespace App;
use PDO;
use PDOException;
class Database {
private static ?PDO $instance = null;
// Gebruik de globale constante DB_PATH uit config.php
private static string $dbFile = DB_PATH;
public static function getInstance(): PDO {
if (self::$instance === null) {
try {
self::$instance = new PDO('sqlite:' . self::$dbFile);
self::$instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
self::createTables();
} catch (PDOException $e) {
error_log("Database connection failed: " . $e->getMessage());
// Verbeterde foutmelding voor de gebruiker
die("FATAL ERROR: Database connection failed. This is often a permission issue.
<br>Error: " . $e->getMessage() . "
<br>Database Path: " . self::$dbFile . "
<br>Please ensure the web server user (www-data) has write permissions to the database file and its directory.");
}
}
return self::$instance;
}
private static function createTables(): void {
$db = self::getInstance();
$db->exec('CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
parent_id INTEGER DEFAULT NULL
)');
$db->exec('CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
category_id INTEGER,
FOREIGN KEY (category_id) REFERENCES categories(id)
)');
}
}

40
src/Database.php~ Executable file
View File

@@ -0,0 +1,40 @@
<?php
namespace App\Database;
use PDO;
use PDOException;
class Database {
private static ?PDO $instance = null;
private static string $dbFile = '/var/www/localweb/collections.sqlite';
public static function getInstance(): PDO {
if (self::$instance === null) {
try {
self::$instance = new PDO('sqlite:' . self::$dbFile);
self::$instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
self::createTables();
} catch (PDOException $e) {
error_log("Database connection failed: " . $e->getMessage());
throw new PDOException("Database connection failed: could not open database file. Please check server logs for details.");
}
}
return self::$instance;
}
private static function createTables(): void {
$db = self::getInstance();
$db->exec('CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
)');
$db->exec('CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
category_id INTEGER,
FOREIGN KEY (category_id) REFERENCES categories(id)
)');
}
}

113
src/Models/Category.php Executable file
View File

@@ -0,0 +1,113 @@
<?php
namespace App\Models;
use PDO;
use PDOException;
class Category {
private PDO $db;
private ?int $id;
private string $name;
private ?int $parentId;
public function __construct(PDO $db, ?int $id = null, string $name = '', ?int $parentId = null) {
$this->db = $db;
$this->id = $id;
$this->name = $name;
$this->parentId = $parentId;
}
public function getId(): ?int {
return $this->id;
}
public function getName(): string {
return $this->name;
}
public function setName(string $name): void {
$this->name = $name;
}
public function getParentId(): ?int {
return $this->parentId;
}
public function setParentId(?int $parentId): void {
$this->parentId = $parentId;
}
public static function getAll(PDO $db): array {
try {
$stmt = $db->query('SELECT c.id, c.name, c.parent_id, p.name as parent_name FROM categories c LEFT JOIN categories p ON c.parent_id = p.id ORDER BY c.name');
$categories = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Add full path
foreach ($categories as &$category) {
$category['path'] = self::getFullPath($db, $category['id']);
}
return $categories;
} catch (PDOException $e) {
error_log("Model Error: Failed to get all categories - " . $e->getMessage());
throw $e;
}
}
public static function getFullPath(PDO $db, int $id, string $separator = ' / '): string {
$category = self::getById($db, $id);
if (!$category) return '';
if ($category['parent_id']) {
return self::getFullPath($db, $category['parent_id'], $separator) . $separator . $category['name'];
} else {
return $category['name'];
}
}
public static function getById(PDO $db, int $id): ?array {
try {
$stmt = $db->prepare('SELECT id, name, parent_id FROM categories WHERE id = :id');
$stmt->execute([':id' => $id]);
$category = $stmt->fetch(PDO::FETCH_ASSOC);
return $category ?: null;
} catch (PDOException $e) {
error_log("Model Error: Failed to get category by ID - " . $e->getMessage());
throw $e;
}
}
public function save(): bool {
if ($this->id === null) {
// Add new category
try {
$stmt = $this->db->prepare('INSERT INTO categories (name, parent_id) VALUES (:name, :parent_id)');
$stmt->execute([':name' => $this->name, ':parent_id' => $this->parentId]);
$this->id = (int)$this->db->lastInsertId();
return true;
} catch (PDOException $e) {
error_log("Model Error: Failed to save category - " . $e->getMessage());
throw $e;
}
} else {
// Update category
try {
$stmt = $this->db->prepare('UPDATE categories SET name = :name, parent_id = :parent_id WHERE id = :id');
$stmt->execute([':name' => $this->name, ':parent_id' => $this->parentId, ':id' => $this->id]);
return true;
} catch (PDOException $e) {
error_log("Model Error: Failed to update category - " . $e->getMessage());
throw $e;
}
}
}
public static function delete(PDO $db, int $id): bool {
try {
$stmt = $db->prepare('DELETE FROM categories WHERE id = :id');
$stmt->execute([':id' => $id]);
return $stmt->rowCount() > 0;
} catch (PDOException $e) {
error_log("Model Error: Failed to delete category - " . $e->getMessage());
throw $e;
}
}
}

168
src/Models/Item.php Executable file
View File

@@ -0,0 +1,168 @@
<?php
namespace App\Models;
use PDO;
use PDOException;
class Item {
private PDO $db;
private ?int $id;
private string $name;
private ?string $description;
private ?int $categoryId;
private ?string $categoryName;
public function __construct(PDO $db, ?int $id = null, string $name = '', ?string $description = null, ?int $categoryId = null, ?string $categoryName = null) {
$this->db = $db;
$this->id = $id;
$this->name = $name;
$this->description = $description;
$this->categoryId = $categoryId;
$this->categoryName = $categoryName;
}
public function getId(): ?int {
return $this->id;
}
public function getName(): string {
return $this->name;
}
public function setName(string $name): void {
$this->name = $name;
}
public function getDescription(): ?string {
return $this->description;
}
public function setDescription(?string $description): void {
$this->description = $description;
}
public function getCategoryId(): ?int {
return $this->categoryId;
}
public function setCategoryId(?int $categoryId): void {
$this->categoryId = $categoryId;
}
public function getCategoryName(): ?string {
return $this->categoryName;
}
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
WHERE 1=1';
$params = [];
if (!empty($search)) {
$query .= ' AND i.name LIKE :search';
$params[':search'] = '%' . $search . '%';
}
if ($categoryId !== null) {
$query .= ' AND i.category_id = :category_id';
$params[':category_id'] = $categoryId;
}
$query .= ' ORDER BY i.name';
$stmt = $db->prepare($query);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log("Model Error: Failed to get filtered items - " . $e->getMessage());
throw $e;
}
}
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
WHERE i.id = :id'
);
$stmt->execute([':id' => $id]);
$item = $stmt->fetch(PDO::FETCH_ASSOC);
return $item ?: null;
} catch (PDOException $e) {
error_log("Model Error: Failed to get item by ID - " . $e->getMessage());
throw $e;
}
}
public function save(): bool {
if ($this->id === null) {
// Add new item
try {
$stmt = $this->db->prepare(
'INSERT INTO items (name, description, category_id)
VALUES (:name, :description, :category_id)'
);
$stmt->execute([
':name' => $this->name,
':description' => $this->description,
':category_id' => $this->categoryId
]);
$this->id = (int)$this->db->lastInsertId();
return true;
} catch (PDOException $e) {
error_log("Model Error: Failed to save item - " . $e->getMessage());
throw $e;
}
} else {
// Update item (optional, not implemented in current controllers)
try {
$stmt = $this->db->prepare(
'UPDATE items
SET name = :name, description = :description, category_id = :category_id
WHERE id = :id'
);
$stmt->execute([
':name' => $this->name,
':description' => $this->description,
':category_id' => $this->categoryId,
':id' => $this->id
]);
return true;
} catch (PDOException $e) {
error_log("Model Error: Failed to update item - " . $e->getMessage());
throw $e;
}
}
}
public static function delete(PDO $db, int $id): bool {
try {
$stmt = $db->prepare('DELETE FROM items WHERE id = :id');
$stmt->execute([':id' => $id]);
return $stmt->rowCount() > 0;
} catch (PDOException $e) {
error_log("Model Error: Failed to delete item - " . $e->getMessage());
throw $e;
}
}
public function toArray(): array {
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'category_id' => $this->categoryId,
'category_name' => $this->categoryName
];
}
}

48
src/Router.php Executable file
View File

@@ -0,0 +1,48 @@
<?php
namespace App;
class Router {
private array $routes = [];
public function __construct() {
}
public function addRoute(string $method, string $path, mixed $handler): void {
$this->routes[] = ['method' => $method, 'path' => $path, 'handler' => $handler];
}
public function dispatch() {
$dispatcher = \FastRoute\simpleDispatcher(function($r) {
foreach ($this->routes as $route) {
$r->addRoute($route['method'], $route['path'], $route['handler']);
}
});
$method = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];
$path = parse_url($uri, PHP_URL_PATH);
$routeInfo = $dispatcher->dispatch($method, $path);
switch ($routeInfo[0]) {
case \FastRoute\Dispatcher::NOT_FOUND:
// Handle 404
http_response_code(404);
header('Content-Type: application/json');
echo json_encode(['error' => 'Not Found']);
break;
case \FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
// Handle 405
$allowedMethods = $routeInfo[1];
http_response_code(405);
header('Content-Type: application/json');
echo json_encode(['error' => 'Method Not Allowed', 'allowed' => $allowedMethods]);
break;
case \FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
// Call the handler with route variables
$handler(...array_values($vars));
break;
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Services;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\Loader\JsonFileLoader;
class TranslationService
{
private string $langDir;
private array $supportedLocales;
private string $defaultLocale;
public function __construct(string $langDir, array $supportedLocales, string $defaultLocale)
{
$this->langDir = $langDir;
$this->supportedLocales = $supportedLocales;
$this->defaultLocale = $defaultLocale;
}
public function getTranslator(string $locale): Translator
{
$translator = new Translator($locale);
$translator->addLoader('json', new JsonFileLoader());
foreach ($this->supportedLocales as $supportedLocale) {
$filePath = $this->langDir . '/' . $supportedLocale . '.json';
if (file_exists($filePath)) {
$translator->addResource('json', $filePath, $supportedLocale);
}
}
return $translator;
}
}