From 9f9617ca455603690ff8b00db84a4fb5e0ceb8a4 Mon Sep 17 00:00:00 2001 From: Edwin Noorlander Date: Wed, 12 Nov 2025 09:51:01 +0100 Subject: [PATCH] Add development documentation and enhance SPA navigation with category/item detail views - Add DEVELOPMENT.md with complete LXC environment setup guide - Implement dynamic category tree with item navigation in sidebar - Add category detail view with items list via AJAX - Add item edit form with delete functionality - Enhance SPA routing to support query parameters for categories/items - Update Bootstrap styling with icons and improved navigation - Include SQLite database in repository for development --- DEVELOPMENT.md | 203 ++++++++++++++++++++++++ collections.sqlite | Bin 0 -> 20480 bytes config.php | 4 +- public/index.php | 74 +++++++-- public/js/app.js | 64 ++++++-- public/style.css | 32 ++++ src/Controllers/CategoryController.php | 24 +++ src/Controllers/ItemController.php | 24 +++ templates/layout.twig | 2 + templates/partials/category_detail.twig | 39 +++++ templates/partials/item_edit.twig | 58 +++++++ 11 files changed, 499 insertions(+), 25 deletions(-) create mode 100644 DEVELOPMENT.md create mode 100755 collections.sqlite create mode 100644 templates/partials/category_detail.twig create mode 100644 templates/partials/item_edit.twig diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..f3cf7ab --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,203 @@ +# LocalWeb Development Environment Guide + +## Overview + +This document describes the LocalWeb Collections PHP MVC application development environment setup using LXC containers. + +## Architecture + +### Host Environment +- **Location**: `/home/edwin/Documents/Projects/localweb/` +- **User**: `edwin` +- **Container Runtime**: Incus/LXC +- **Container Name**: `www` + +### LXC Container `www` +- **OS**: Linux distribution with Apache2 + PHP +- **Web Root**: `/var/www/localhost/` (mounted from host project directory) +- **Service**: `apache2.service` (running as systemd service) +- **Database**: SQLite (`collections.sqlite`) +- **PHP Version**: Compatible with Composer dependencies + +### Network Setup +- **LXC Proxy**: Container port 80 → Host port 80 +- **Access**: `http://localhost` on host serves the webapp +- **Internal**: Apache binds to `127.0.0.1:80` inside container + +## Project Structure + +``` +/home/edwin/Documents/Projects/localweb/ +├── src/ # PHP MVC source code +│ ├── Controllers/ # Route handlers +│ ├── Models/ # Data models +│ └── Services/ # Business logic +├── templates/ # Twig templates +├── public/ # Web root (entry point) +├── vendor/ # Composer dependencies +├── collections.sqlite # SQLite database +└── config.php # Configuration +``` + +## Development Workflow + +### 1. Local Development +- Edit files in `/home/edwin/Documents/Projects/localweb/` +- Changes are immediately reflected in the container via shared mount +- Test via `curl localhost` or browser at `http://localhost` + +### 2. Container Management +```bash +# Execute commands in container +incus exec www -- + +# Check Apache status +incus exec www -- systemctl status apache2 + +# View Apache logs +incus exec www -- tail -f /var/log/apache2/error.log + +# Reload Apache +incus exec www -- systemctl reload apache2 + +# Check PHP version +incus exec www -- php -v +``` + +### 3. Database Operations +```bash +# Access database in container +incus exec www -- sqlite3 /var/www/localhost/collections.sqlite + +# Check database permissions +incus exec www -- ls -la /var/www/localhost/collections.sqlite +``` + +## Application Details + +### Technology Stack +- **PHP**: MVC application with PSR-4 autoloading +- **Framework**: Custom MVC with FastRoute routing +- **Templating**: Twig templates +- **Database**: SQLite with PDO +- **Frontend**: Bootstrap 5 + vanilla JavaScript (SPA-like) +- **Dependencies**: Managed via Composer + +### Key Features +- **Collections Management**: CRUD operations for items +- **Categories**: Hierarchical category structure +- **QR Codes**: Automatic generation for items +- **Multi-language**: Dutch/English support +- **Search & Filter**: Dynamic content loading + +### Routing +- **Web Routes**: Full page loads (`/`, `/categories`, `/parts`) +- **API Routes**: AJAX endpoints (`/api/items`, `/api/categories`) +- **Entry Point**: `public/index.php` + +## Development Commands + +### Dependencies +```bash +# Install/update dependencies +composer install +composer update + +# Check for security issues +composer audit +``` + +### Testing +```bash +# Manual testing via curl +curl localhost +curl -s localhost | grep -i "collections" + +# Test specific endpoints +curl localhost/api/items +curl localhost/api/categories +``` + +### Debugging +```bash +# Check Apache error logs +incus exec www -- tail -f /var/log/apache2/error.log + +# Check access logs +incus exec www -- tail -f /var/log/apache2/access.log + +# Verify file permissions +incus exec www -- ls -la /var/www/localhost/ +``` + +## File Permissions + +### Container Permissions +- **Project files**: `ubuntu:www-data` ownership +- **Database**: `ubuntu:ubuntu` ownership +- **Apache**: Runs as `www-data` user (group access) + +### Common Issues +- Database permission errors: Check `collections.sqlite` ownership +- 403 errors: Verify file permissions in container +- 500 errors: Check Apache error logs + +## Environment Variables & Configuration + +### PHP Configuration +- Located in container PHP configuration +- Error reporting configured for development +- SQLite extension enabled + +### Application Config +- `config.php`: Database path and constants +- `SUPPORTED_LOCALES`: ['en', 'nl'] for i18n +- `DB_PATH`: Points to SQLite database file + +## Deployment Notes + +### Container Persistence +- Data persists in container filesystem +- Database file stored in project directory +- No external database dependencies + +### Backup Strategy +- Git repository for code +- SQLite database backup: `collections.sqlite.backup` +- Regular snapshots of LXC container recommended + +## Troubleshooting + +### Common Issues +1. **Apache not responding**: Check service status in container +2. **Database errors**: Verify SQLite file permissions +3. **404 errors**: Check `.htaccess` and routing configuration +4. **Permission denied**: Ensure `www-data` can read project files + +### Recovery Commands +```bash +# Restart Apache +incus exec www -- systemctl restart apache2 + +# Fix permissions (if needed) +incus exec www -- chown -R ubuntu:www-data /var/www/localhost/ +incus exec www -- chmod -R 755 /var/www/localhost/ + +# Check container status +incus list +incus info www +``` + +## Security Considerations + +- Container isolation via LXC +- No direct database access from host +- Apache runs with limited privileges +- SQLite database in project directory (not web-exposed) + +## Performance Notes + +- Lightweight SQLite database +- Minimal PHP dependencies +- Efficient routing with FastRoute +- Client-side caching for static assets \ No newline at end of file diff --git a/collections.sqlite b/collections.sqlite new file mode 100755 index 0000000000000000000000000000000000000000..348bd4b5b82afbaec35a4aeeca648bc192ce281b GIT binary patch literal 20480 zcmeI(Z%f-i90%~bq-yJ^84M{hgsU*HDE_N!l`&Yg*RHHqtu0LUAjIZcqiI^3*liCd zd$Kn&*z4F!@D1#Nd$0$CJs87>yQXSe*&Ys7{2smZce&gpzfZ`63uJFMZ+U`M8?CzG zu{gO;C?(GrBZSPzIwI?J>BNEShWw{}@!!H3ve5nz)-FjP?2(B}?eoM(8K6J_0uX=z z1Rwwb2tWV=5V#`(XN!TcNF+kvKlO|QTgdfS-Ssykn?;@~aaKxg+qvDtCfS1gm| z3MHQ9MYdDSZKaC)>;>Ovsd8yMSCFH(c%d{uFv2nFf|dBo5-V)WTF&PO`%U3iT2|Au z8jgS1U~k3nM6JfxkcGS_x+#KEaz1$O9(&(-@ITcPt+-e2}3~zPLb~NVY$k@B* zbSx5$(z93ooV^qGB4uEv-u{1ZOIW#(+b#1!`=-$nj@Kv8(mazY=eu{YhZ9#uBiSe= zmSc)HR|y-ZUc+zez3bMOd$B4XwP5FQ3u|A9s+}v^x%N%_c}E^VqzM5CKmY;|fB*y_ z009U<00Iyget~;5K&EKjXf}_8BGU=^DTR~(nWUa@JvFGxkXXWV2^p82s&+xN3+eO$-c6xI?xspf^Op-8- dHSu_TIiYj&wdLqpaddGlobal('app_name', APP_NAME); $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?')); -// Build category tree for sidebar -$sidebarHtml = ''; +// Sidebar will be built dynamically via AJAX +$sidebarHtml = ''; $twig->addGlobal('sidebar_html', $sidebarHtml); diff --git a/public/index.php b/public/index.php index 624f94e..4436344 100755 --- a/public/index.php +++ b/public/index.php @@ -13,6 +13,7 @@ use App\Router; use App\Controllers\ItemController; use App\Controllers\CategoryController; use App\Database; +use App\Models\Category; // Initialize Database (ensures tables exist) Database::getInstance(); @@ -40,25 +41,58 @@ $router->addRoute('GET', '/print/{id:\d+}', [ItemController::class, 'printQR']); // 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/list', [CategoryController::class, 'listCategoriesJson']); $router->addRoute('GET', '/api/categories/{id}', [CategoryController::class, 'getCategory']); -$router->addRoute('GET', '/api/parts', [ItemController::class, 'renderAddForm']); + +// Categories with optional filter +$router->addRoute('GET', '/api/categories', function() { + $categoryId = $_GET['category'] ?? null; + if ($categoryId) { + // Show category details with items + $controller = new CategoryController(); + $controller->showCategory($categoryId); + } else { + // Show all categories + $controller = new CategoryController(); + $controller->listCategories(); + } +}); + +// Parts with optional item filter +$router->addRoute('GET', '/api/parts', function() { + $itemId = $_GET['item'] ?? null; + if ($itemId) { + // Show item details/edit form + $controller = new ItemController(); + $controller->editItem($itemId); + } else { + // Show add form + $controller = new ItemController(); + $controller->renderAddForm(); + } +}); $router->addRoute('GET', '/api/tree', function() { try { - $db = App\Database\Database::getInstance(); - $categories = App\Models\Category::getAll($db); + $db = Database::getInstance(); + $categories = Category::getAll($db); - function build_category_tree($categories, $parentId = null, &$visited = [], $depth = 0, &$nodeCount = 0) { + function build_category_tree($categories, $db, $parentId = null, $visited = [], $depth = 0, $nodeCount = 0) { if ($depth > 5 || $nodeCount > 100) return []; $tree = []; foreach ($categories as $cat) { if ($cat['parent_id'] == $parentId && !in_array($cat['id'], $visited)) { $visited[] = $cat['id']; + + // Get items for this category + $itemsStmt = $db->prepare('SELECT id, name FROM items WHERE category_id = :category_id ORDER BY name'); + $itemsStmt->execute([':category_id' => $cat['id']]); + $items = $itemsStmt->fetchAll(PDO::FETCH_ASSOC); + $node = [ 'id' => $cat['id'], 'name' => $cat['name'], - 'children' => build_category_tree($categories, $cat['id'], $visited, $depth + 1, $nodeCount) + 'items' => $items, + 'children' => build_category_tree($categories, $db, $cat['id'], $visited, $depth + 1, $nodeCount) ]; $tree[] = $node; $nodeCount++; @@ -72,21 +106,39 @@ $router->addRoute('GET', '/api/tree', function() { if ($depth > 5) return ''; $html = ''; foreach ($nodes as $node) { - $html .= '
  • '; - $html .= '' . htmlspecialchars($node['name']) . ''; + $html .= '