diff --git a/collections.sqlite.old b/collections.sqlite.old new file mode 100644 index 0000000..240e16e Binary files /dev/null and b/collections.sqlite.old differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..7207508 --- /dev/null +++ b/public/favicon.ico @@ -0,0 +1,58 @@ +.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; +} \ No newline at end of file diff --git a/public/js/app.js.backup b/public/js/app.js.backup new file mode 100644 index 0000000..05684cc --- /dev/null +++ b/public/js/app.js.backup @@ -0,0 +1,537 @@ +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 = '
Loading...
'; + } + + // 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 = `
${error.message}
`; + }); + } + + // 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(/]*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 = '

Huidige afbeelding:

'; + } else { + currentImageDiv.innerHTML = '

Geen afbeelding

'; + } + // Populate category select + const categorySelect = document.getElementById('edit_item_category_id'); + categorySelect.innerHTML = ''; + 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 = ''; + 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'); + }); + } + } +}); diff --git a/public/uploads/69149ec32f098_IMG_3901.jpeg b/public/uploads/69149ec32f098_IMG_3901.jpeg new file mode 100644 index 0000000..4b12fcd Binary files /dev/null and b/public/uploads/69149ec32f098_IMG_3901.jpeg differ diff --git a/public/uploads/69149f1c1e92a_pexels-marta-nogueira-589022975-17151646.jpg b/public/uploads/69149f1c1e92a_pexels-marta-nogueira-589022975-17151646.jpg new file mode 100644 index 0000000..2a8682e Binary files /dev/null and b/public/uploads/69149f1c1e92a_pexels-marta-nogueira-589022975-17151646.jpg differ diff --git a/public/uploads/69149f23209a8_pexels-rodrigo-ortega-2044210904-29161722.jpg b/public/uploads/69149f23209a8_pexels-rodrigo-ortega-2044210904-29161722.jpg new file mode 100644 index 0000000..5c984ad Binary files /dev/null and b/public/uploads/69149f23209a8_pexels-rodrigo-ortega-2044210904-29161722.jpg differ diff --git a/public/uploads/6914a2f617afa_pexels-rodrigo-ortega-2044210904-29161722.jpg b/public/uploads/6914a2f617afa_pexels-rodrigo-ortega-2044210904-29161722.jpg new file mode 100644 index 0000000..5c984ad Binary files /dev/null and b/public/uploads/6914a2f617afa_pexels-rodrigo-ortega-2044210904-29161722.jpg differ diff --git a/public/uploads/6914a5e96e286_pexels-rodrigo-ortega-2044210904-29161722.jpg b/public/uploads/6914a5e96e286_pexels-rodrigo-ortega-2044210904-29161722.jpg new file mode 100644 index 0000000..5c984ad Binary files /dev/null and b/public/uploads/6914a5e96e286_pexels-rodrigo-ortega-2044210904-29161722.jpg differ diff --git a/public/uploads/6914a65007e06_pexels-marta-nogueira-589022975-17151646.jpg b/public/uploads/6914a65007e06_pexels-marta-nogueira-589022975-17151646.jpg new file mode 100644 index 0000000..2a8682e Binary files /dev/null and b/public/uploads/6914a65007e06_pexels-marta-nogueira-589022975-17151646.jpg differ diff --git a/public/uploads/6914aa822cf9c_pexels-rodrigo-ortega-2044210904-29161722.jpg b/public/uploads/6914aa822cf9c_pexels-rodrigo-ortega-2044210904-29161722.jpg new file mode 100644 index 0000000..5c984ad Binary files /dev/null and b/public/uploads/6914aa822cf9c_pexels-rodrigo-ortega-2044210904-29161722.jpg differ diff --git a/public/uploads/6914ab80de508_IMG_3901.jpeg b/public/uploads/6914ab80de508_IMG_3901.jpeg new file mode 100644 index 0000000..4b12fcd Binary files /dev/null and b/public/uploads/6914ab80de508_IMG_3901.jpeg differ diff --git a/public/uploads/6914aba8ca17f_IMG_3901.jpeg b/public/uploads/6914aba8ca17f_IMG_3901.jpeg new file mode 100644 index 0000000..4b12fcd Binary files /dev/null and b/public/uploads/6914aba8ca17f_IMG_3901.jpeg differ diff --git a/public/uploads/6914b0c9b5723_pexels-rodrigo-ortega-2044210904-29161722.jpg b/public/uploads/6914b0c9b5723_pexels-rodrigo-ortega-2044210904-29161722.jpg new file mode 100644 index 0000000..5c984ad Binary files /dev/null and b/public/uploads/6914b0c9b5723_pexels-rodrigo-ortega-2044210904-29161722.jpg differ diff --git a/public/uploads/6914b0fc0507a_pexels-rodrigo-ortega-2044210904-29161722.jpg b/public/uploads/6914b0fc0507a_pexels-rodrigo-ortega-2044210904-29161722.jpg new file mode 100644 index 0000000..5c984ad Binary files /dev/null and b/public/uploads/6914b0fc0507a_pexels-rodrigo-ortega-2044210904-29161722.jpg differ diff --git a/public/uploads/6914b10675315_r_D.jpg b/public/uploads/6914b10675315_r_D.jpg new file mode 100644 index 0000000..7a25cd0 Binary files /dev/null and b/public/uploads/6914b10675315_r_D.jpg differ diff --git a/public/uploads/6914b1160357d_r_D.jpg b/public/uploads/6914b1160357d_r_D.jpg new file mode 100644 index 0000000..7a25cd0 Binary files /dev/null and b/public/uploads/6914b1160357d_r_D.jpg differ diff --git a/public/uploads/qr/06F0FF92.svg b/public/uploads/qr/06F0FF92.svg new file mode 100644 index 0000000..a268aec --- /dev/null +++ b/public/uploads/qr/06F0FF92.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/uploads/qr/0FBBA098.svg b/public/uploads/qr/0FBBA098.svg new file mode 100644 index 0000000..506a5ba --- /dev/null +++ b/public/uploads/qr/0FBBA098.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/uploads/qr/155488D0.svg b/public/uploads/qr/155488D0.svg new file mode 100644 index 0000000..46f1035 --- /dev/null +++ b/public/uploads/qr/155488D0.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/uploads/qr/7B1EC871.svg b/public/uploads/qr/7B1EC871.svg new file mode 100644 index 0000000..16c20b3 --- /dev/null +++ b/public/uploads/qr/7B1EC871.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/uploads/qr/80A9EBF2.svg b/public/uploads/qr/80A9EBF2.svg new file mode 100644 index 0000000..1c27bb6 --- /dev/null +++ b/public/uploads/qr/80A9EBF2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/uploads/qr/A28E2207.svg b/public/uploads/qr/A28E2207.svg new file mode 100644 index 0000000..0d784f8 --- /dev/null +++ b/public/uploads/qr/A28E2207.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/uploads/qr/BEB4BE8F.svg b/public/uploads/qr/BEB4BE8F.svg new file mode 100644 index 0000000..ddd2e6c --- /dev/null +++ b/public/uploads/qr/BEB4BE8F.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/uploads/qr/C2254256.svg b/public/uploads/qr/C2254256.svg new file mode 100644 index 0000000..d4d2119 --- /dev/null +++ b/public/uploads/qr/C2254256.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/uploads/qr/ED39CE82.svg b/public/uploads/qr/ED39CE82.svg new file mode 100644 index 0000000..81171af --- /dev/null +++ b/public/uploads/qr/ED39CE82.svg @@ -0,0 +1,4 @@ + + + +