Configure Apache to run as ubuntu user
- Change Apache user from www-data to ubuntu in systemd override - Update file ownership to ubuntu:ubuntu for web directory - Fix permission issues for git operations and file access - Apache now runs with same user as file ownership - Resolves permission denied errors for development workflow
BIN
collections.sqlite.old
Normal file
58
public/favicon.ico
Normal file
@@ -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;
|
||||
}
|
||||
537
public/js/app.js.backup
Normal file
@@ -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 = '<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');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
BIN
public/uploads/69149ec32f098_IMG_3901.jpeg
Normal file
|
After Width: | Height: | Size: 688 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/uploads/6914ab80de508_IMG_3901.jpeg
Normal file
|
After Width: | Height: | Size: 688 KiB |
BIN
public/uploads/6914aba8ca17f_IMG_3901.jpeg
Normal file
|
After Width: | Height: | Size: 688 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/uploads/6914b10675315_r_D.jpg
Normal file
|
After Width: | Height: | Size: 281 KiB |
BIN
public/uploads/6914b1160357d_r_D.jpg
Normal file
|
After Width: | Height: | Size: 281 KiB |
4
public/uploads/qr/06F0FF92.svg
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
4
public/uploads/qr/0FBBA098.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
4
public/uploads/qr/155488D0.svg
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
4
public/uploads/qr/7B1EC871.svg
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
4
public/uploads/qr/80A9EBF2.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
4
public/uploads/qr/A28E2207.svg
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
4
public/uploads/qr/BEB4BE8F.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
4
public/uploads/qr/C2254256.svg
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
4
public/uploads/qr/ED39CE82.svg
Normal file
|
After Width: | Height: | Size: 6.3 KiB |