Compare commits
No commits in common. "main" and "development" have entirely different histories.
main
...
developmen
55
README.md
@ -11,32 +11,14 @@ Een PHP MVC applicatie voor het beheren van collecties van onderdelen, gebouwd m
|
|||||||
- **Locatie**: Sla locatie informatie op voor onderdelen.
|
- **Locatie**: Sla locatie informatie op voor onderdelen.
|
||||||
- **QR Codes**: Automatisch gegenereerde QR codes voor elk onderdeel, met printbare stickers.
|
- **QR Codes**: Automatisch gegenereerde QR codes voor elk onderdeel, met printbare stickers.
|
||||||
- **Meertalig**: Ondersteuning voor Nederlands en Engels.
|
- **Meertalig**: Ondersteuning voor Nederlands en Engels.
|
||||||
- **Real-time Updates**: Boom navigatie wordt automatisch bijgewerkt na CRUD operaties.
|
|
||||||
|
|
||||||
## Installatie
|
## Installatie
|
||||||
|
|
||||||
1. Zorg voor een LXC container 'www' met Apache, PHP, en Composer.
|
1. Zorg voor een LXC container 'www' met Apache, PHP, en Composer.
|
||||||
2. Clone de repository naar `/var/www/localhost` in de container.
|
2. Clone de repository naar /var/www/localhost in de container.
|
||||||
3. Run `composer install` in de container.
|
3. Run `composer install` in de container.
|
||||||
4. Stel de database in: `collections.sqlite` wordt automatisch aangemaakt.
|
4. Stel de database in: `collections.sqlite` wordt automatisch aangemaakt.
|
||||||
5. Configureer Apache om als `ubuntu` gebruiker te draaien (zie Apache Configuratie).
|
5. Start Apache en ga naar http://localhost.
|
||||||
6. Start Apache en ga naar http://localhost.
|
|
||||||
|
|
||||||
## Apache Configuratie
|
|
||||||
|
|
||||||
De applicatie draait met Apache als `ubuntu` gebruiker voor development:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Apache gebruiker instellen
|
|
||||||
sudo nano /etc/apache2/envvars # Wijzig APACHE_RUN_USER=ubuntu
|
|
||||||
sudo nano /etc/apache2/apache2.conf # Wijzig User/Group naar ubuntu
|
|
||||||
sudo systemctl restart apache2
|
|
||||||
|
|
||||||
# Permissions instellen
|
|
||||||
sudo chown -R ubuntu:ubuntu /var/www/localhost
|
|
||||||
sudo chmod 755 /var/www/localhost/public/js/
|
|
||||||
sudo chmod 644 /var/www/localhost/public/js/*
|
|
||||||
```
|
|
||||||
|
|
||||||
## Gebruik
|
## Gebruik
|
||||||
|
|
||||||
@ -47,38 +29,11 @@ sudo chmod 644 /var/www/localhost/public/js/*
|
|||||||
- **QR Printen**: Klik op 'Print QR' om een sticker te printen.
|
- **QR Printen**: Klik op 'Print QR' om een sticker te printen.
|
||||||
- **Categorieën**: Beheer categorieën via de zijbalk.
|
- **Categorieën**: Beheer categorieën via de zijbalk.
|
||||||
|
|
||||||
## Project Structuur
|
|
||||||
|
|
||||||
```
|
|
||||||
├── src/
|
|
||||||
│ ├── Controllers/ # MVC Controllers
|
|
||||||
│ ├── Models/ # Data Models
|
|
||||||
│ └── Services/ # Business Logic
|
|
||||||
├── templates/ # Twig templates
|
|
||||||
├── public/
|
|
||||||
│ ├── js/ # Frontend JavaScript
|
|
||||||
│ └── uploads/ # Afbeeldingen en QR codes
|
|
||||||
├── lang/ # Vertalingen (NL/EN)
|
|
||||||
└── collections.sqlite # SQLite database
|
|
||||||
```
|
|
||||||
|
|
||||||
## Ontwikkeling
|
## Ontwikkeling
|
||||||
|
|
||||||
- **Dependencies**: `composer install` / `update`
|
- **Linting**: Geen specifieke tool, volg PSR-12.
|
||||||
- **Linting**: Volg PSR-12 handmatig
|
- **Testing**: Geen tests, gebruik curl voor API testing.
|
||||||
- **Testing**: `curl localhost` of `php test.php` voor individuele tests
|
- **Commits**: Elke wijziging heeft een git commit.
|
||||||
- **Database**: Gebruik `App\Database\Database::getInstance()` (geen directe PDO)
|
|
||||||
- **I18n**: `App\Services\TranslationService` (PHP) of `{{ trans('ID') }}` (Twig)
|
|
||||||
- **Commits**: Elke wijziging heeft een git commit
|
|
||||||
|
|
||||||
## Code Conventies
|
|
||||||
|
|
||||||
- **Architecture**: MVC patroon met PSR-4 namespacing
|
|
||||||
- **Naming**: Classes `PascalCase`, methods/variables `camelCase`
|
|
||||||
- **Templating**: Alleen Twig (`.twig` files)
|
|
||||||
- **Security**: Geen secrets/keys in code of logs
|
|
||||||
- **Types**: Type hints waar mogelijk
|
|
||||||
- **Comments**: Vermijd tenzij gevraagd
|
|
||||||
|
|
||||||
## Licentie
|
## Licentie
|
||||||
|
|
||||||
|
|||||||
@ -9,9 +9,9 @@
|
|||||||
#ServerName www.example.com
|
#ServerName www.example.com
|
||||||
|
|
||||||
ServerAdmin webmaster@localhost
|
ServerAdmin webmaster@localhost
|
||||||
DocumentRoot /home/edwin/Documents/Projecten/Collections/public
|
DocumentRoot /var/www/localhost/public
|
||||||
|
|
||||||
<Directory /home/edwin/Documents/Projecten/Collections/public>
|
<Directory /var/www/localhost/public>
|
||||||
AllowOverride All
|
AllowOverride All
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
|
|||||||
@ -1,58 +0,0 @@
|
|||||||
.category-tree, .category-tree ul {
|
|
||||||
list-style: none;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-tree li {
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category {
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.items {
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-link {
|
|
||||||
color: #007bff;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tree structure styling */
|
|
||||||
.category-link {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #495057 !important;
|
|
||||||
background-color: #f8f9fa !important;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 2px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-link:hover {
|
|
||||||
background-color: #e9ecef !important;
|
|
||||||
color: #212529 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-link {
|
|
||||||
font-weight: 400;
|
|
||||||
color: #6c757d !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
border-left: 3px solid #007bff;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-link:hover {
|
|
||||||
background-color: #f8f9fa !important;
|
|
||||||
color: #007bff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bi {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
@ -106,38 +106,27 @@ $router->addRoute('GET', '/api/tree', function() {
|
|||||||
if ($depth > 5) return '';
|
if ($depth > 5) return '';
|
||||||
$html = '';
|
$html = '';
|
||||||
foreach ($nodes as $node) {
|
foreach ($nodes as $node) {
|
||||||
$indent = str_repeat('│ ', $depth);
|
$html .= '<li class="nav-item">';
|
||||||
$isLast = false;
|
$html .= '<a class="nav-link category-link" href="#" data-route="/categories?category=' . $node['id'] . '">';
|
||||||
|
$html .= '<i class="bi bi-folder"></i> ' . htmlspecialchars($node['name']);
|
||||||
$html .= '<li class="tree-item" style="list-style: none; padding: 2px 0;">';
|
|
||||||
$html .= '<span class="tree-node" style="cursor: pointer;" onclick="toggleNode(this)">';
|
|
||||||
$html .= $indent . '├── ';
|
|
||||||
$html .= '<i class="bi bi-folder-fill text-warning"></i> ';
|
|
||||||
$html .= '<a href="#" class="category-link" style="text-decoration: none; color: inherit; outline: none; box-shadow: none; border: none; background: transparent;" data-route="/categories?category=' . $node['id'] . '">';
|
|
||||||
$html .= htmlspecialchars($node['name']);
|
|
||||||
$html .= '</a>';
|
$html .= '</a>';
|
||||||
$html .= '</span>';
|
|
||||||
|
|
||||||
// Add items as sub-items
|
// Add items as sub-items
|
||||||
if (!empty($node['items'])) {
|
if (!empty($node['items'])) {
|
||||||
$itemIndent = str_repeat('│ ', $depth + 1);
|
$html .= '<ul class="nav flex-column nav-pills ms-2">';
|
||||||
foreach ($node['items'] as $index => $item) {
|
foreach ($node['items'] as $item) {
|
||||||
$isLastItem = $index === count($node['items']) - 1;
|
$html .= '<li class="nav-item">';
|
||||||
$prefix = $isLastItem ? '└── ' : '├── ';
|
$html .= '<a class="nav-link item-link" href="#" data-route="/parts?item=' . $item['id'] . '">';
|
||||||
|
$html .= '<i class="bi bi-file-earmark"></i> ' . htmlspecialchars($item['name']);
|
||||||
$html .= '<li class="tree-item" style="list-style: none; padding: 2px 0;">';
|
|
||||||
$html .= $itemIndent . $prefix;
|
|
||||||
$html .= '<i class="bi bi-file-earmark-text" style="color: #6c757d;"></i> ';
|
|
||||||
$html .= '<a href="#" class="item-link" style="text-decoration: none; color: inherit; outline: none; box-shadow: none; border: none; background: transparent;" data-route="/parts?item=' . $item['id'] . '">';
|
|
||||||
$html .= htmlspecialchars($item['name']);
|
|
||||||
$html .= '</a>';
|
$html .= '</a>';
|
||||||
$html .= '</li>';
|
$html .= '</li>';
|
||||||
}
|
}
|
||||||
|
$html .= '</ul>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add subcategories
|
// Add subcategories
|
||||||
if (!empty($node['children'])) {
|
if (!empty($node['children'])) {
|
||||||
$html .= '<ul style="list-style: none; padding-left: 0; margin: 0;">';
|
$html .= '<ul class="nav flex-column nav-pills ms-2">';
|
||||||
$html .= render_category_tree($node['children'], $depth + 1);
|
$html .= render_category_tree($node['children'], $depth + 1);
|
||||||
$html .= '</ul>';
|
$html .= '</ul>';
|
||||||
}
|
}
|
||||||
@ -149,15 +138,15 @@ $router->addRoute('GET', '/api/tree', function() {
|
|||||||
|
|
||||||
$nodeCount = 0;
|
$nodeCount = 0;
|
||||||
$categoryTree = build_category_tree($categories, $db, null, [], 0, $nodeCount);
|
$categoryTree = build_category_tree($categories, $db, null, [], 0, $nodeCount);
|
||||||
$html = '<ul style="list-style: none; padding-left: 0; margin: 0; font-family: monospace;">' . render_category_tree($categoryTree) . '</ul>';
|
$html = '<ul class="nav flex-column nav-pills">' . render_category_tree($categoryTree) . '</ul>';
|
||||||
if (strlen($html) > 10000) {
|
if (strlen($html) > 10000) {
|
||||||
$html = '<ul style="list-style: none; padding-left: 0; margin: 0;"><li style="padding: 2px 0;">📁 <a href="#" style="text-decoration: none; color: inherit; outline: none; box-shadow: none; border: none; background: transparent;" data-route="/">Overview</a></li><li style="padding: 2px 0;">📁 <a href="#" style="text-decoration: none; color: inherit; outline: none; box-shadow: none; border: none; background: transparent;" data-route="/categories">Categories</a></li><li style="padding: 2px 0;">📁 <a href="#" style="text-decoration: none; color: inherit; outline: none; box-shadow: none; border: none; background: transparent;" data-route="/parts">Parts</a></li></ul>';
|
$html = '<ul class="nav flex-column nav-pills"><li class="nav-item"><a class="nav-link" href="#" data-route="/">Overview</a></li><li class="nav-item"><a class="nav-link" href="#" data-route="/categories">Categories</a></li><li class="nav-item"><a class="nav-link" href="#" data-route="/parts">Parts</a></li></ul>';
|
||||||
}
|
}
|
||||||
header('Content-Type: text/html');
|
header('Content-Type: text/html');
|
||||||
echo $html;
|
echo $html;
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
header('Content-Type: text/html');
|
header('Content-Type: text/html');
|
||||||
echo '<ul style="list-style: none; padding-left: 0; margin: 0;"><li style="padding: 2px 0;">📁 <a href="#" class="text-decoration-none text-dark" data-route="/">Overview</a></li><li style="padding: 2px 0;">📁 <a href="#" class="text-decoration-none text-dark" data-route="/categories">Categories</a></li><li style="padding: 2px 0;">📁 <a href="#" class="text-decoration-none text-dark" data-route="/parts">Parts</a></li></ul>';
|
echo '<ul class="nav flex-column nav-pills"><li class="nav-item"><a class="nav-link" href="#" data-route="/">Overview</a></li><li class="nav-item"><a class="nav-link" href="#" data-route="/categories">Categories</a></li><li class="nav-item"><a class="nav-link" href="#" data-route="/parts">Parts</a></li></ul>';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
142
public/js/app.js
@ -4,28 +4,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Global function for editing item from tree
|
// Global function for editing item from tree
|
||||||
window.editItem = function(id) {
|
window.editItem = function(id) {
|
||||||
|
// Create a mock button to reuse handleEditItem
|
||||||
const mockBtn = { getAttribute: (attr) => attr === 'data-id' ? id : null };
|
const mockBtn = { getAttribute: (attr) => attr === 'data-id' ? id : null };
|
||||||
handleEditItem.call(mockBtn);
|
handleEditItem.call(mockBtn);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to toggle tree nodes
|
// Function to toggle category children
|
||||||
window.toggleNode = function(element) {
|
window.toggleCategory = function(span) {
|
||||||
const li = element.closest('li');
|
const li = span.parentElement;
|
||||||
const childUl = li.querySelector(':scope > ul');
|
const ul = li.querySelector('ul'); // First ul is children
|
||||||
if (childUl) {
|
if (ul) {
|
||||||
const isHidden = childUl.style.display === 'none';
|
ul.style.display = ul.style.display === 'none' ? 'block' : 'none';
|
||||||
childUl.style.display = isHidden ? 'block' : 'none';
|
|
||||||
const icon = element.querySelector('i');
|
|
||||||
if (icon) {
|
|
||||||
if (isHidden) {
|
|
||||||
icon.className = 'bi bi-folder-fill text-warning';
|
|
||||||
} else {
|
|
||||||
icon.className = 'bi bi-folder text-warning';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Helper Functions ---
|
||||||
function setPageTitle(title) {
|
function setPageTitle(title) {
|
||||||
document.title = `${appName} - ${title}`;
|
document.title = `${appName} - ${title}`;
|
||||||
}
|
}
|
||||||
@ -34,24 +27,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
mainContent.innerHTML = '<div class="text-center p-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div></div>';
|
mainContent.innerHTML = '<div class="text-center p-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadTree() {
|
// Function to fetch content and update the main view
|
||||||
fetch('/api/tree')
|
|
||||||
.then(response => response.text())
|
|
||||||
.then(html => {
|
|
||||||
const sidebarTree = document.getElementById('sidebar-tree');
|
|
||||||
if (sidebarTree) {
|
|
||||||
sidebarTree.innerHTML = html;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error loading tree:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchContent(path, pushState = true) {
|
function fetchContent(path, pushState = true) {
|
||||||
console.log('fetchContent called with path:', path);
|
|
||||||
showLoading();
|
showLoading();
|
||||||
|
|
||||||
|
// Determine the API endpoint based on the path
|
||||||
let apiPath = '';
|
let apiPath = '';
|
||||||
let pageTitle = '';
|
let pageTitle = '';
|
||||||
let routePath = path;
|
let routePath = path;
|
||||||
@ -60,10 +40,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
apiPath = '/api/items';
|
apiPath = '/api/items';
|
||||||
pageTitle = 'Overview';
|
pageTitle = 'Overview';
|
||||||
routePath = '/';
|
routePath = '/';
|
||||||
} else if (path.startsWith('/items')) {
|
|
||||||
apiPath = '/api/items';
|
|
||||||
pageTitle = 'Overview';
|
|
||||||
routePath = '/';
|
|
||||||
} else if (path.startsWith('/categories')) {
|
} else if (path.startsWith('/categories')) {
|
||||||
if (path.includes('?category=')) {
|
if (path.includes('?category=')) {
|
||||||
const categoryId = path.split('?category=')[1];
|
const categoryId = path.split('?category=')[1];
|
||||||
@ -83,6 +59,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
pageTitle = 'Parts';
|
pageTitle = 'Parts';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Fallback
|
||||||
apiPath = '/api/items';
|
apiPath = '/api/items';
|
||||||
pageTitle = 'Overview';
|
pageTitle = 'Overview';
|
||||||
routePath = '/';
|
routePath = '/';
|
||||||
@ -102,6 +79,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
setPageTitle(pageTitle);
|
setPageTitle(pageTitle);
|
||||||
updateActiveLink(routePath);
|
updateActiveLink(routePath);
|
||||||
|
// Re-initialize any scripts specific to the loaded content
|
||||||
initPageSpecificScripts();
|
initPageSpecificScripts();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@ -110,6 +88,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update active class on sidebar links
|
||||||
function updateActiveLink(path) {
|
function updateActiveLink(path) {
|
||||||
document.querySelectorAll('.nav-link[data-route]').forEach(link => {
|
document.querySelectorAll('.nav-link[data-route]').forEach(link => {
|
||||||
link.classList.remove('active');
|
link.classList.remove('active');
|
||||||
@ -119,6 +98,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle browser back/forward buttons
|
||||||
window.onpopstate = function(event) {
|
window.onpopstate = function(event) {
|
||||||
if (event.state && event.state.html) {
|
if (event.state && event.state.html) {
|
||||||
mainContent.innerHTML = event.state.html;
|
mainContent.innerHTML = event.state.html;
|
||||||
@ -126,10 +106,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
updateActiveLink(event.state.path);
|
updateActiveLink(event.state.path);
|
||||||
initPageSpecificScripts();
|
initPageSpecificScripts();
|
||||||
} else {
|
} else {
|
||||||
|
// If no state, force a full load of the current path
|
||||||
fetchContent(window.location.pathname, false);
|
fetchContent(window.location.pathname, false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Event Listeners ---
|
||||||
|
|
||||||
|
// Navigation links (Navbar and Sidebar)
|
||||||
document.body.addEventListener('click', function(e) {
|
document.body.addEventListener('click', function(e) {
|
||||||
const link = e.target.closest('a[data-route]');
|
const link = e.target.closest('a[data-route]');
|
||||||
if (link) {
|
if (link) {
|
||||||
@ -156,6 +140,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const initialPath = window.location.pathname === '/' ? '/items' : window.location.pathname;
|
const initialPath = window.location.pathname === '/' ? '/items' : window.location.pathname;
|
||||||
fetchContent(initialPath, false);
|
fetchContent(initialPath, false);
|
||||||
|
|
||||||
|
// Page-specific scripts (e.g., form submissions)
|
||||||
function initPageSpecificScripts() {
|
function initPageSpecificScripts() {
|
||||||
// Overview page: filter form
|
// Overview page: filter form
|
||||||
const filterForm = document.getElementById('filterForm');
|
const filterForm = document.getElementById('filterForm');
|
||||||
@ -190,9 +175,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Categories page: save edit category
|
// Categories page: save edit category
|
||||||
const saveEditCategoryBtn = document.getElementById('saveEditCategoryBtn');
|
const saveEditBtn = document.getElementById('saveEditCategoryBtn');
|
||||||
if (saveEditCategoryBtn) {
|
if (saveEditBtn) {
|
||||||
saveEditCategoryBtn.addEventListener('click', handleSaveEditCategory);
|
saveEditBtn.addEventListener('click', handleSaveEditCategory);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Categories page: edit/delete buttons
|
// Categories page: edit/delete buttons
|
||||||
@ -216,7 +201,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
fetchContent('/', false);
|
fetchContent('/', false);
|
||||||
reloadTree(); // Reload tree after deletion
|
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || 'Error deleting item');
|
alert(data.error || 'Error deleting item');
|
||||||
}
|
}
|
||||||
@ -241,8 +225,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
fetchContent('/', false);
|
fetchContent('/', false); // Go to overview to see the new part
|
||||||
reloadTree(); // Reload tree after adding
|
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || 'Error adding part');
|
alert(data.error || 'Error adding part');
|
||||||
}
|
}
|
||||||
@ -253,6 +236,34 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAddCategoryFromItem() {
|
||||||
|
const name = document.getElementById('new_category_name').value.trim();
|
||||||
|
if (!name) return;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('category_name', name);
|
||||||
|
fetch('/api/categories', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const select = document.getElementById('category_id');
|
||||||
|
const option = new Option(name, data.id);
|
||||||
|
select.appendChild(option);
|
||||||
|
select.value = data.id;
|
||||||
|
document.getElementById('new_category_name').value = '';
|
||||||
|
document.getElementById('addCategoryForm').classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
alert(data.error || 'Error adding category');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error adding category');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function handleFilter(e) {
|
function handleFilter(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const search = document.getElementById('search').value;
|
const search = document.getElementById('search').value;
|
||||||
@ -264,7 +275,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(html => {
|
.then(html => {
|
||||||
document.getElementById('itemsList').outerHTML = html.match(/<ul[^>]*id="itemsList"[^>]*>[\s\S]*?<\/ul>/)[0];
|
document.getElementById('itemsList').outerHTML = html.match(/<ul[^>]*id="itemsList"[^>]*>[\s\S]*?<\/ul>/)[0];
|
||||||
initPageSpecificScripts();
|
initPageSpecificScripts(); // Re-init for new buttons
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
@ -273,6 +284,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
function handleEditItem() {
|
function handleEditItem() {
|
||||||
const id = this.getAttribute('data-id');
|
const id = this.getAttribute('data-id');
|
||||||
|
// Fetch current item data
|
||||||
fetch('/api/items/' + id)
|
fetch('/api/items/' + id)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@ -287,6 +299,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
} else {
|
} else {
|
||||||
currentImageDiv.innerHTML = '<p>Geen afbeelding</p>';
|
currentImageDiv.innerHTML = '<p>Geen afbeelding</p>';
|
||||||
}
|
}
|
||||||
|
// Populate category select
|
||||||
const categorySelect = document.getElementById('edit_item_category_id');
|
const categorySelect = document.getElementById('edit_item_category_id');
|
||||||
categorySelect.innerHTML = '<option value="">-- Select Category --</option>';
|
categorySelect.innerHTML = '<option value="">-- Select Category --</option>';
|
||||||
fetch('/api/categories/list')
|
fetch('/api/categories/list')
|
||||||
@ -300,10 +313,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
categorySelect.value = data.category_id || '';
|
categorySelect.value = data.category_id || '';
|
||||||
const modal = new bootstrap.Modal(document.getElementById('editItemModal'));
|
const modal = new bootstrap.Modal(document.getElementById('editItemModal'));
|
||||||
const saveEditItemBtn = document.getElementById('saveEditItemBtn');
|
|
||||||
if (saveEditItemBtn) {
|
|
||||||
saveEditItemBtn.addEventListener('click', handleSaveEditItem);
|
|
||||||
}
|
|
||||||
modal.show();
|
modal.show();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -326,7 +335,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
this.closest('li').remove();
|
this.closest('li').remove();
|
||||||
reloadTree(); // Reload tree after deletion
|
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || 'Error deleting part');
|
alert(data.error || 'Error deleting part');
|
||||||
}
|
}
|
||||||
@ -348,7 +356,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
fetchContent('/categories', false);
|
fetchContent('/categories', false);
|
||||||
reloadTree(); // Reload tree after adding
|
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || 'Error adding category');
|
alert(data.error || 'Error adding category');
|
||||||
}
|
}
|
||||||
@ -388,25 +395,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
// Hide modal
|
bootstrap.Modal.getInstance(document.getElementById('editItemModal')).hide();
|
||||||
const modalElement = document.getElementById('editItemModal');
|
fetchContent('/', false); // Reload overview
|
||||||
if (modalElement) {
|
|
||||||
modalElement.classList.remove('show', 'showing');
|
|
||||||
modalElement.style.display = 'none';
|
|
||||||
modalElement.setAttribute('aria-hidden', 'true');
|
|
||||||
document.body.classList.remove('modal-open');
|
|
||||||
const backdrops = document.querySelectorAll(' .modal-backdrop');
|
|
||||||
backdrops.forEach(backdrop => backdrop.remove());
|
|
||||||
}
|
|
||||||
fetchContent('/', false);
|
|
||||||
reloadTree(); // Reload tree after updating
|
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || 'Error updating part');
|
alert(data.error || 'Error updating part');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Update error:', error);
|
console.error('Error:', error);
|
||||||
alert('Error updating part: ' + error.message);
|
alert('Error updating part');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -435,30 +432,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
// Hide modal
|
bootstrap.Modal.getInstance(document.getElementById('editCategoryModal')).hide();
|
||||||
const modalElement = document.getElementById('editCategoryModal');
|
|
||||||
if (modalElement) {
|
|
||||||
modalElement.classList.remove('show', 'showing');
|
|
||||||
modalElement.style.display = 'none';
|
|
||||||
modalElement.setAttribute('aria-hidden', 'true');
|
|
||||||
document.body.classList.remove('modal-open');
|
|
||||||
const backdrops = document.querySelectorAll(' .modal-backdrop');
|
|
||||||
backdrops.forEach(backdrop => backdrop.remove());
|
|
||||||
}
|
|
||||||
fetchContent('/categories', false);
|
fetchContent('/categories', false);
|
||||||
reloadTree(); // Reload tree after updating
|
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || 'Error updating category');
|
alert(data.error || 'Error updating category');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Update error:', error);
|
console.error('Error:', error);
|
||||||
alert('Error updating category: ' + error.message);
|
alert('Error updating category');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEditCategory() {
|
function handleEditCategory() {
|
||||||
const id = this.getAttribute('data-id');
|
const id = this.getAttribute('data-id');
|
||||||
|
// Fetch current category data
|
||||||
fetch('/api/categories/' + id)
|
fetch('/api/categories/' + id)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -470,6 +458,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (data && !data.error) {
|
if (data && !data.error) {
|
||||||
document.getElementById('edit_category_id').value = data.id;
|
document.getElementById('edit_category_id').value = data.id;
|
||||||
document.getElementById('edit_category_name').value = data.name;
|
document.getElementById('edit_category_name').value = data.name;
|
||||||
|
// Populate parent select, excluding current category
|
||||||
const parentSelect = document.getElementById('edit_parent_category_id');
|
const parentSelect = document.getElementById('edit_parent_category_id');
|
||||||
parentSelect.innerHTML = '<option value="">-- No Parent --</option>';
|
parentSelect.innerHTML = '<option value="">-- No Parent --</option>';
|
||||||
fetch('/api/categories/list')
|
fetch('/api/categories/list')
|
||||||
@ -485,10 +474,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
parentSelect.value = data.parent_id || '';
|
parentSelect.value = data.parent_id || '';
|
||||||
const modal = new bootstrap.Modal(document.getElementById('editCategoryModal'));
|
const modal = new bootstrap.Modal(document.getElementById('editCategoryModal'));
|
||||||
const saveEditCategoryBtn = document.getElementById('saveEditCategoryBtn');
|
|
||||||
if (saveEditCategoryBtn) {
|
|
||||||
saveEditCategoryBtn.addEventListener('click', handleSaveEditCategory);
|
|
||||||
}
|
|
||||||
modal.show();
|
modal.show();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -511,7 +496,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
this.closest('li').remove();
|
this.closest('li').remove();
|
||||||
reloadTree(); // Reload tree after deletion
|
|
||||||
} else {
|
} else {
|
||||||
alert(data.error || 'Error deleting category');
|
alert(data.error || 'Error deleting category');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,537 +0,0 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const mainContent = document.getElementById('main-content');
|
|
||||||
const appName = document.querySelector('.navbar-brand').textContent;
|
|
||||||
|
|
||||||
// Global function for editing item from tree
|
|
||||||
window.editItem = function(id) {
|
|
||||||
// Create a mock button to reuse handleEditItem
|
|
||||||
const mockBtn = { getAttribute: (attr) => attr === 'data-id' ? id : null };
|
|
||||||
handleEditItem.call(mockBtn);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to toggle tree nodes
|
|
||||||
window.toggleNode = function(element) {
|
|
||||||
const li = element.closest('li');
|
|
||||||
const childUl = li.querySelector(':scope > ul');
|
|
||||||
if (childUl) {
|
|
||||||
const isHidden = childUl.style.display === 'none';
|
|
||||||
childUl.style.display = isHidden ? 'block' : 'none';
|
|
||||||
|
|
||||||
// Update the icon
|
|
||||||
const icon = element.querySelector('i');
|
|
||||||
if (icon) {
|
|
||||||
if (isHidden) {
|
|
||||||
icon.className = 'bi bi-folder-fill text-warning';
|
|
||||||
} else {
|
|
||||||
icon.className = 'bi bi-folder text-warning';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Helper Functions ---
|
|
||||||
function setPageTitle(title) {
|
|
||||||
document.title = `${appName} - ${title}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showLoading() {
|
|
||||||
mainContent.innerHTML = '<div class="text-center p-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div></div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to fetch content and update the main view
|
|
||||||
function fetchContent(path, pushState = true) {
|
|
||||||
showLoading();
|
|
||||||
|
|
||||||
// Determine the API endpoint based on the path
|
|
||||||
let apiPath = '';
|
|
||||||
let pageTitle = '';
|
|
||||||
let routePath = path;
|
|
||||||
|
|
||||||
if (path === '/' || path === '/overview') {
|
|
||||||
apiPath = '/api/items';
|
|
||||||
pageTitle = 'Overview';
|
|
||||||
routePath = '/';
|
|
||||||
} else if (path.startsWith('/categories')) {
|
|
||||||
if (path.includes('?category=')) {
|
|
||||||
const categoryId = path.split('?category=')[1];
|
|
||||||
apiPath = '/api/categories?category=' + categoryId;
|
|
||||||
pageTitle = 'Category Details';
|
|
||||||
} else {
|
|
||||||
apiPath = '/api/categories';
|
|
||||||
pageTitle = 'Categories';
|
|
||||||
}
|
|
||||||
} else if (path.startsWith('/parts')) {
|
|
||||||
if (path.includes('?item=')) {
|
|
||||||
const itemId = path.split('?item=')[1];
|
|
||||||
apiPath = '/api/parts?item=' + itemId;
|
|
||||||
pageTitle = 'Part Details';
|
|
||||||
} else {
|
|
||||||
apiPath = '/api/parts';
|
|
||||||
pageTitle = 'Parts';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback
|
|
||||||
apiPath = '/api/items';
|
|
||||||
pageTitle = 'Overview';
|
|
||||||
routePath = '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(apiPath)
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
return response.text();
|
|
||||||
})
|
|
||||||
.then(html => {
|
|
||||||
mainContent.innerHTML = html;
|
|
||||||
if (pushState) {
|
|
||||||
history.pushState({ html: html, path: routePath }, '', routePath);
|
|
||||||
}
|
|
||||||
setPageTitle(pageTitle);
|
|
||||||
updateActiveLink(routePath);
|
|
||||||
// Re-initialize any scripts specific to the loaded content
|
|
||||||
initPageSpecificScripts();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error fetching content:', error);
|
|
||||||
mainContent.innerHTML = `<div class="alert alert-danger">${error.message}</div>`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update active class on sidebar links
|
|
||||||
function updateActiveLink(path) {
|
|
||||||
document.querySelectorAll('.nav-link[data-route]').forEach(link => {
|
|
||||||
link.classList.remove('active');
|
|
||||||
if (link.getAttribute('data-route') === path) {
|
|
||||||
link.classList.add('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle browser back/forward buttons
|
|
||||||
window.onpopstate = function(event) {
|
|
||||||
if (event.state && event.state.html) {
|
|
||||||
mainContent.innerHTML = event.state.html;
|
|
||||||
setPageTitle(event.state.path.substring(1) || 'Home');
|
|
||||||
updateActiveLink(event.state.path);
|
|
||||||
initPageSpecificScripts();
|
|
||||||
} else {
|
|
||||||
// If no state, force a full load of the current path
|
|
||||||
fetchContent(window.location.pathname, false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Event Listeners ---
|
|
||||||
|
|
||||||
// Navigation links (Navbar and Sidebar)
|
|
||||||
document.body.addEventListener('click', function(e) {
|
|
||||||
const link = e.target.closest('a[data-route]');
|
|
||||||
if (link) {
|
|
||||||
e.preventDefault();
|
|
||||||
const route = link.getAttribute('data-route');
|
|
||||||
fetchContent(route);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load category tree first
|
|
||||||
fetch('/api/tree')
|
|
||||||
.then(response => response.text())
|
|
||||||
.then(html => {
|
|
||||||
const sidebarTree = document.getElementById('sidebar-tree');
|
|
||||||
if (sidebarTree) {
|
|
||||||
sidebarTree.innerHTML = html;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error loading tree:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initial page load
|
|
||||||
const initialPath = window.location.pathname === '/' ? '/items' : window.location.pathname;
|
|
||||||
fetchContent(initialPath, false);
|
|
||||||
|
|
||||||
// Page-specific scripts (e.g., form submissions)
|
|
||||||
function initPageSpecificScripts() {
|
|
||||||
// Overview page: filter form
|
|
||||||
const filterForm = document.getElementById('filterForm');
|
|
||||||
if (filterForm) {
|
|
||||||
filterForm.addEventListener('submit', handleFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overview page: edit/delete buttons
|
|
||||||
document.querySelectorAll('.edit-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', handleEditItem);
|
|
||||||
});
|
|
||||||
document.querySelectorAll('.delete-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', handleDeleteItem);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parts page: add item form
|
|
||||||
const addItemForm = document.getElementById('addItemForm');
|
|
||||||
if (addItemForm) {
|
|
||||||
addItemForm.addEventListener('submit', handleAddItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Categories page: add category form
|
|
||||||
const addCategoryForm = document.getElementById('addCategoryForm');
|
|
||||||
if (addCategoryForm) {
|
|
||||||
addCategoryForm.addEventListener('submit', handleAddCategory);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overview page: save edit item
|
|
||||||
const saveEditItemBtn = document.getElementById('saveEditItemBtn');
|
|
||||||
if (saveEditItemBtn) {
|
|
||||||
saveEditItemBtn.addEventListener('click', handleSaveEditItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Categories page: save edit category
|
|
||||||
const saveEditBtn = document.getElementById('saveEditCategoryBtn');
|
|
||||||
if (saveEditBtn) {
|
|
||||||
saveEditBtn.addEventListener('click', handleSaveEditCategory);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Categories page: edit/delete buttons
|
|
||||||
document.querySelectorAll('.edit-category-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', handleEditCategory);
|
|
||||||
});
|
|
||||||
document.querySelectorAll('.delete-category-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', handleDeleteCategory);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Item edit page: delete button
|
|
||||||
const deleteItemBtn = document.getElementById('deleteItemBtn');
|
|
||||||
if (deleteItemBtn) {
|
|
||||||
deleteItemBtn.addEventListener('click', function() {
|
|
||||||
const itemId = document.getElementById('edit_item_id').value;
|
|
||||||
if (confirm(window.translations.deletePartConfirm)) {
|
|
||||||
fetch('/api/items/' + itemId, {
|
|
||||||
method: 'DELETE'
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
fetchContent('/', false);
|
|
||||||
} else {
|
|
||||||
alert(data.error || 'Error deleting item');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
alert('Error deleting item');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handler functions
|
|
||||||
function handleAddItem(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const formData = new FormData(this);
|
|
||||||
fetch('/api/items', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
fetchContent('/', false); // Go to overview to see the new part
|
|
||||||
} else {
|
|
||||||
alert(data.error || 'Error adding part');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
alert('Error adding part');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAddCategoryFromItem() {
|
|
||||||
const name = document.getElementById('new_category_name').value.trim();
|
|
||||||
if (!name) return;
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('category_name', name);
|
|
||||||
fetch('/api/categories', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
const select = document.getElementById('category_id');
|
|
||||||
const option = new Option(name, data.id);
|
|
||||||
select.appendChild(option);
|
|
||||||
select.value = data.id;
|
|
||||||
document.getElementById('new_category_name').value = '';
|
|
||||||
document.getElementById('addCategoryForm').classList.add('hidden');
|
|
||||||
} else {
|
|
||||||
alert(data.error || 'Error adding category');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
alert('Error adding category');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFilter(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const search = document.getElementById('search').value;
|
|
||||||
const categoryId = document.getElementById('category_filter').value;
|
|
||||||
let url = '/api/items?';
|
|
||||||
if (search) url += 'search=' + encodeURIComponent(search) + '&';
|
|
||||||
if (categoryId) url += 'category_id=' + categoryId;
|
|
||||||
fetch(url)
|
|
||||||
.then(response => response.text())
|
|
||||||
.then(html => {
|
|
||||||
document.getElementById('itemsList').outerHTML = html.match(/<ul[^>]*id="itemsList"[^>]*>[\s\S]*?<\/ul>/)[0];
|
|
||||||
initPageSpecificScripts(); // Re-init for new buttons
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEditItem() {
|
|
||||||
const id = this.getAttribute('data-id');
|
|
||||||
// Fetch current item data
|
|
||||||
fetch('/api/items/' + id)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data) {
|
|
||||||
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_location').value = data.location || '';
|
|
||||||
const currentImageDiv = document.getElementById('current_image');
|
|
||||||
if (data.image) {
|
|
||||||
currentImageDiv.innerHTML = '<p>Huidige afbeelding:</p><img src="' + data.image + '" style="max-width: 100px;">';
|
|
||||||
} else {
|
|
||||||
currentImageDiv.innerHTML = '<p>Geen afbeelding</p>';
|
|
||||||
}
|
|
||||||
// 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');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
alert('Error fetching part');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteItem() {
|
|
||||||
const id = this.getAttribute('data-id');
|
|
||||||
if (confirm(window.translations.deletePartConfirm)) {
|
|
||||||
fetch('/api/items/' + id, {
|
|
||||||
method: 'DELETE'
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
this.closest('li').remove();
|
|
||||||
} else {
|
|
||||||
alert(data.error || 'Error deleting part');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
alert('Error deleting part');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAddCategory() {
|
|
||||||
const formData = new FormData(this);
|
|
||||||
fetch('/api/categories', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
fetchContent('/categories', false);
|
|
||||||
} else {
|
|
||||||
alert(data.error || 'Error adding category');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
alert('Error adding category');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSaveEditItem() {
|
|
||||||
const id = document.getElementById('edit_item_id').value;
|
|
||||||
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 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: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
const modalElement = document.getElementById('editItemModal');
|
|
||||||
const modalInstance = bootstrap.Modal.getInstance(modalElement);
|
|
||||||
if (modalInstance) {
|
|
||||||
modalInstance.hide();
|
|
||||||
} else {
|
|
||||||
modalElement.classList.remove('show');
|
|
||||||
modalElement.style.display = 'none';
|
|
||||||
document.body.classList.remove('modal-open');
|
|
||||||
const backdrop = document.querySelector('.modal-backdrop');
|
|
||||||
if (backdrop) backdrop.remove();
|
|
||||||
}
|
|
||||||
fetchContent('/', false);
|
|
||||||
} else {
|
|
||||||
alert(data.error || 'Error updating part');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Update error:', error);
|
|
||||||
alert('Error updating part: ' + error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function handleSaveEditCategory() {
|
|
||||||
const id = document.getElementById('edit_category_id').value;
|
|
||||||
const name = document.getElementById('edit_category_name').value.trim();
|
|
||||||
const parentId = document.getElementById('edit_parent_category_id').value;
|
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
alert('Category name is required');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
category_name: name,
|
|
||||||
parent_category_id: parentId || null
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch('/api/categories/' + id, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
const modalElement = document.getElementById('editCategoryModal');
|
|
||||||
const modalInstance = bootstrap.Modal.getInstance(modalElement);
|
|
||||||
if (modalInstance) {
|
|
||||||
modalInstance.hide();
|
|
||||||
} else {
|
|
||||||
modalElement.classList.remove('show');
|
|
||||||
modalElement.style.display = 'none';
|
|
||||||
document.body.classList.remove('modal-open');
|
|
||||||
const backdrop = document.querySelector('.modal-backdrop');
|
|
||||||
if (backdrop) backdrop.remove();
|
|
||||||
}
|
|
||||||
fetchContent('/categories', false);
|
|
||||||
} else {
|
|
||||||
alert(data.error || 'Error updating category');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Update error:', error);
|
|
||||||
alert('Error updating category: ' + error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function handleEditCategory() {
|
|
||||||
const id = this.getAttribute('data-id');
|
|
||||||
// Fetch current category data
|
|
||||||
fetch('/api/categories/' + id)
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
if (data && !data.error) {
|
|
||||||
document.getElementById('edit_category_id').value = data.id;
|
|
||||||
document.getElementById('edit_category_name').value = data.name;
|
|
||||||
// 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(data.error || 'Category not found');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
alert('Error fetching category: ' + error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteCategory() {
|
|
||||||
const id = this.getAttribute('data-id');
|
|
||||||
if (confirm(window.translations.deleteCategoryConfirm)) {
|
|
||||||
fetch('/api/categories/' + id, {
|
|
||||||
method: 'DELETE'
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
this.closest('li').remove();
|
|
||||||
} else {
|
|
||||||
alert(data.error || 'Error deleting category');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
alert('Error deleting category');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
Before Width: | Height: | Size: 688 KiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 688 KiB |
|
Before Width: | Height: | Size: 688 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
@ -136,8 +136,13 @@ class ItemController
|
|||||||
$description = trim($data['item_description'] ?? '');
|
$description = trim($data['item_description'] ?? '');
|
||||||
$categoryId = !empty($data['category_id']) ? (int)$data['category_id'] : null;
|
$categoryId = !empty($data['category_id']) ? (int)$data['category_id'] : null;
|
||||||
$location = trim($data['location'] ?? '');
|
$location = trim($data['location'] ?? '');
|
||||||
$idCode = strtoupper(substr(md5(uniqid()), 0, 8));
|
|
||||||
$imagePath = null;
|
if (empty($name)) {
|
||||||
|
http_response_code(400);
|
||||||
|
global $translator;
|
||||||
|
echo json_encode(['error' => $translator->trans('Part name is required')]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Generate unique id_code
|
// Generate unique id_code
|
||||||
$idCode = self::generateUniqueIdCode($db);
|
$idCode = self::generateUniqueIdCode($db);
|
||||||
@ -196,7 +201,7 @@ class ItemController
|
|||||||
mkdir($qrDir, 0755, true);
|
mkdir($qrDir, 0755, true);
|
||||||
}
|
}
|
||||||
$options = new QROptions([
|
$options = new QROptions([
|
||||||
'outputType' => QRCode::OUTPUT_MARKUP_SVG,
|
'outputType' => QRCode::OUTPUT_SVG,
|
||||||
'eccLevel' => QRCode::ECC_L,
|
'eccLevel' => QRCode::ECC_L,
|
||||||
]);
|
]);
|
||||||
$qrCode = new QRCode($options);
|
$qrCode = new QRCode($options);
|
||||||
@ -243,13 +248,8 @@ class ItemController
|
|||||||
|
|
||||||
// Handle image upload if new file
|
// Handle image upload if new file
|
||||||
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
|
||||||
$newImagePath = self::handleImageUpload($_FILES['image']);
|
$imagePath = self::handleImageUpload($_FILES['image']);
|
||||||
if ($newImagePath) {
|
|
||||||
$imagePath = $newImagePath;
|
|
||||||
error_log("Image updated: " . $imagePath);
|
error_log("Image updated: " . $imagePath);
|
||||||
} else {
|
|
||||||
error_log("Image upload failed, keeping existing image");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
error_log("No image update or error: " . ($_FILES['image']['error'] ?? 'no file'));
|
error_log("No image update or error: " . ($_FILES['image']['error'] ?? 'no file'));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,8 +14,6 @@ class Database {
|
|||||||
if (self::$instance === null) {
|
if (self::$instance === null) {
|
||||||
try {
|
try {
|
||||||
self::$instance = new PDO('sqlite:' . self::$dbFile);
|
self::$instance = new PDO('sqlite:' . self::$dbFile);
|
||||||
self::$instance->exec('PRAGMA journal_mode = WAL;');
|
|
||||||
self::$instance->exec('PRAGMA busy_timeout = 5000;');
|
|
||||||
self::$instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
self::$instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
self::createTables();
|
self::createTables();
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
@ -42,20 +40,7 @@ class Database {
|
|||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
category_id INTEGER,
|
category_id INTEGER,
|
||||||
id_code TEXT UNIQUE,
|
|
||||||
image TEXT,
|
|
||||||
location TEXT,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (category_id) REFERENCES categories(id)
|
FOREIGN KEY (category_id) REFERENCES categories(id)
|
||||||
)');
|
)');
|
||||||
|
|
||||||
// Create trigger for updated_at
|
|
||||||
$db->exec('CREATE TRIGGER IF NOT EXISTS update_items_updated_at
|
|
||||||
AFTER UPDATE ON items
|
|
||||||
FOR EACH ROW
|
|
||||||
BEGIN
|
|
||||||
UPDATE items SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
|
||||||
END');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,10 +15,8 @@ class Item {
|
|||||||
private ?string $idCode;
|
private ?string $idCode;
|
||||||
private ?string $image;
|
private ?string $image;
|
||||||
private ?string $location;
|
private ?string $location;
|
||||||
private ?string $createdAt;
|
|
||||||
private ?string $updatedAt;
|
|
||||||
|
|
||||||
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, ?string $createdAt = null, ?string $updatedAt = 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->db = $db;
|
||||||
$this->id = $id;
|
$this->id = $id;
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
@ -28,8 +26,6 @@ class Item {
|
|||||||
$this->idCode = $idCode;
|
$this->idCode = $idCode;
|
||||||
$this->image = $image;
|
$this->image = $image;
|
||||||
$this->location = $location;
|
$this->location = $location;
|
||||||
$this->createdAt = $createdAt;
|
|
||||||
$this->updatedAt = $updatedAt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int {
|
public function getId(): ?int {
|
||||||
@ -94,7 +90,7 @@ class Item {
|
|||||||
|
|
||||||
public static function getAllFiltered(PDO $db, string $search, ?int $categoryId): array {
|
public static function getAllFiltered(PDO $db, string $search, ?int $categoryId): array {
|
||||||
try {
|
try {
|
||||||
$query = 'SELECT i.id, i.name, i.description, c.name as category_name, i.category_id, i.id_code, i.image, i.location, i.created_at, i.updated_at
|
$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
|
FROM items i
|
||||||
LEFT JOIN categories c ON i.category_id = c.id
|
LEFT JOIN categories c ON i.category_id = c.id
|
||||||
WHERE 1=1';
|
WHERE 1=1';
|
||||||
@ -124,7 +120,7 @@ class Item {
|
|||||||
public static function getById(PDO $db, int $id): ?array {
|
public static function getById(PDO $db, int $id): ?array {
|
||||||
try {
|
try {
|
||||||
$stmt = $db->prepare(
|
$stmt = $db->prepare(
|
||||||
'SELECT i.id, i.name, i.description, c.name as category_name, i.category_id, i.id_code, i.image, i.location, i.created_at, i.updated_at
|
'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
|
FROM items i
|
||||||
LEFT JOIN categories c ON i.category_id = c.id
|
LEFT JOIN categories c ON i.category_id = c.id
|
||||||
WHERE i.id = :id'
|
WHERE i.id = :id'
|
||||||
@ -202,12 +198,7 @@ class Item {
|
|||||||
'name' => $this->name,
|
'name' => $this->name,
|
||||||
'description' => $this->description,
|
'description' => $this->description,
|
||||||
'category_id' => $this->categoryId,
|
'category_id' => $this->categoryId,
|
||||||
'category_name' => $this->categoryName,
|
'category_name' => $this->categoryName
|
||||||
'id_code' => $this->idCode,
|
|
||||||
'image' => $this->image,
|
|
||||||
'location' => $this->location,
|
|
||||||
'created_at' => $this->createdAt,
|
|
||||||
'updated_at' => $this->updatedAt
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,10 +43,6 @@
|
|||||||
{% if item.location %}
|
{% if item.location %}
|
||||||
<p><strong>{{ trans('Location') }}:</strong> {{ item.location }}</p>
|
<p><strong>{{ trans('Location') }}:</strong> {{ item.location }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p><small class="text-muted">
|
|
||||||
<strong>{{ trans('Created') }}:</strong> {{ item.created_at|date('d-m-Y H:i') }}<br>
|
|
||||||
<strong>{{ trans('Updated') }}:</strong> {{ item.updated_at|date('d-m-Y H:i') }}
|
|
||||||
</small></p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-sm btn-warning me-2 edit-btn" data-id="{{ item.id }}">{{ trans('Edit') }}</button>
|
<button class="btn btn-sm btn-warning me-2 edit-btn" data-id="{{ item.id }}">{{ trans('Edit') }}</button>
|
||||||
|
|||||||
@ -27,16 +27,6 @@
|
|||||||
<label for="edit_location" class="form-label">Location</label>
|
<label for="edit_location" class="form-label">Location</label>
|
||||||
<input type="text" class="form-control" id="edit_location" value="{{ item.location }}">
|
<input type="text" class="form-control" id="edit_location" value="{{ item.location }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Created</label>
|
|
||||||
<input type="text" class="form-control" value="{{ item.created_at|date('d-m-Y H:i') }}" readonly>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Last Updated</label>
|
|
||||||
<input type="text" class="form-control" value="{{ item.updated_at|date('d-m-Y H:i') }}" readonly>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|||||||