first commit

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

81
templates/categories.twig Executable file
View File

@@ -0,0 +1,81 @@
{% autoescape %}
<h1>{{ trans('Categories Overview') }}</h1>
<!-- Add Category Form -->
<div class="card mb-4">
<div class="card-header">{{ trans('Manage Categories') }}</div>
<div class="card-body">
<form id="addCategoryForm">
<div class="mb-3">
<label for="new_category_name" class="form-label">{{ trans('Category Name') }}</label>
<input type="text" class="form-control" id="new_category_name" name="new_category_name" required>
</div>
<div class="mb-3">
<label for="parent_category_id" class="form-label">{{ trans('Parent Category') }} ({{ trans('Optional') }})</label>
<select class="form-select" id="parent_category_id" name="parent_category_id">
<option value="">{{ trans('-- No Parent --') }}</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.path }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary">{{ trans('Add Category') }}</button>
</form>
</div>
</div>
<!-- Categories List -->
<h2>{{ trans('All Categories') }}</h2>
<ul id="categoriesList" class="list-group">
{% if categories %}
{% for category in categories %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<strong>{{ category.path }}</strong>
</div>
<div>
<button class="btn btn-sm btn-warning me-2 edit-category-btn" data-id="{{ category.id }}">Edit</button>
<button class="btn btn-sm btn-danger delete-category-btn" data-id="{{ category.id }}">Delete</button>
</div>
</li>
{% endfor %}
{% else %}
<li class="list-group-item">{{ trans('No categories found.') }}</li>
{% endif %}
</ul>
<!-- Edit Category Modal -->
<div class="modal fade" id="editCategoryModal" tabindex="-1" aria-labelledby="editCategoryModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editCategoryModalLabel">{{ trans('Edit Category') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editCategoryForm">
<input type="hidden" id="edit_category_id" name="id">
<div class="mb-3">
<label for="edit_category_name" class="form-label">{{ trans('Category Name') }}</label>
<input type="text" class="form-control" id="edit_category_name" name="category_name" required>
</div>
<div class="mb-3">
<label for="edit_parent_category_id" class="form-label">{{ trans('Parent Category') }} ({{ trans('Optional') }})</label>
<select class="form-select" id="edit_parent_category_id" name="parent_category_id">
<option value="">{{ trans('-- No Parent --') }}</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.path }}</option>
{% endfor %}
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ trans('Cancel') }}</button>
<button type="button" class="btn btn-primary" id="saveEditCategoryBtn">{{ trans('Save Changes') }}</button>
</div>
</div>
</div>
</div>
{% endautoescape %}

152
templates/index.mustache Executable file
View File

@@ -0,0 +1,152 @@
<html>
<head>
<title>Collection Manager - Add Item</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Add New Item</h1>
<nav>
<a href="index.php">Add Item</a> | <a href="search.php">Search/Overview</a>
</nav>
<form action="index.php" method="post">
<input type="hidden" name="action" value="add_item">
<div>
<label for="item_name">Item Name:</label>
<input type="text" id="item_name" name="item_name" required>
</div>
<div>
<label for="item_description">Description:</label>
<textarea id="item_description" name="item_description" rows="3"></textarea>
</div>
<div>
<label for="category_id">Category:</label>
<select id="category_id" name="category_id" required>
<option value="">-- Select Category --</option>
{{#categories}}
<option value="{{id}}">{{name}}</option>
{{/categories}}
</select>
<button type="button" id="showAddCategoryFormBtn">+</button>
</div>
<div id="addCategoryForm" style="display: none;">
<label for="new_category_name">New Category:</label>
<input type="text" id="new_category_name" name="new_category_name">
<button type="button" id="addCategoryBtn">Add Category</button>
</div>
<button type="submit">Add Item</button>
</form>
<h2>Existing Items</h2>
<ul>
{{#items}}
<li>
<h3>{{name}}</h3>
<p>Category: {{category_name}}</p>
<p>{{description}}</p>
</li>
{{/items}}
{{^items}}
<li>No items yet.</li>
{{/items}}
</ul>
<h2>Categories</h2>
<ul>
{{#categories}}
<li>{{name}}</li>
{{/categories}}
</ul>
<div class="add-category-section">
<label for="new_category_name_direct">New Category:</label>
<input type="text" id="new_category_name_direct" name="new_category_name_direct">
<button id="addCategoryDirectBtn">Add Category</button>
</div>
<script>
// JavaScript for dynamic category adding and potentially drag/drop later
document.addEventListener('DOMContentLoaded', function() {
const showAddCategoryFormBtn = document.getElementById('showAddCategoryFormBtn');
const addCategoryForm = document.getElementById('addCategoryForm');
const newCategoryNameInput = document.getElementById('new_category_name');
const addCategoryBtn = document.getElementById('addCategoryBtn');
const newCategoryNameDirectInput = document.getElementById('new_category_name_direct');
const addCategoryDirectBtn = document.getElementById('addCategoryDirectBtn');
showAddCategoryFormBtn.addEventListener('click', function() { addCategoryForm.style.display = addCategoryForm.style.display === 'none' ? 'block' : 'none'; });
function addCategory(name, isDirect) {
const formData = new FormData();
formData.append('action', 'add_category');
formData.append('category_name', name);
fetch(window.location.href, {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
alert(data.message);
if (data.success) {
if (isDirect) {
newCategoryNameDirectInput.value = '';
} else {
newCategoryNameInput.value = '';
addCategoryForm.style.display = 'none';
}
// Refresh categories
fetchCategories();
}
})
.catch(error => console.error('Error:', error));
}
addCategoryBtn.addEventListener('click', () => addCategory(newCategoryNameInput.value, false));
addCategoryDirectBtn.addEventListener('click', () => addCategory(newCategoryNameDirectInput.value, true));
// Function to fetch and update categories in UI
function fetchCategories() {
fetch(window.location.href + '?action=get_categories')
.then(response => response.json())
.then(data => {
if (data.success && data.categories) {
const categorySelect = document.getElementById('category_id');
const searchCategorySelect = document.getElementById('search_category_id'); // Assuming this exists on search page
const categoryListUl = document.querySelector('.category-management ul'); // Assuming this exists on index page
// Clear existing options/list items
categorySelect.innerHTML = '<option value="">-- Select Category --</option>';
if (searchCategorySelect) searchCategorySelect.innerHTML = '<option value="">-- All Categories --</option>';
if (categoryListUl) categoryListUl.innerHTML = '';
data.categories.forEach(cat => {
const opt1 = document.createElement('option');
opt1.value = cat.id;
opt1.textContent = cat.name;
categorySelect.appendChild(opt1);
if (searchCategorySelect) {
const opt2 = document.createElement('option');
opt2.value = cat.id;
opt2.textContent = cat.name;
searchCategorySelect.appendChild(opt2);
}
if (categoryListUl) {
const li = document.createElement('li');
li.textContent = cat.name;
categoryListUl.appendChild(li);
}
});
}
})
.catch(error => console.error('Error fetching categories:', error));
}
// Initial fetch
fetchCategories();
});
</script>
</body>
</html>

90
templates/items.twig Executable file
View File

@@ -0,0 +1,90 @@
{% autoescape %}
<h1>{{ trans('Parts Overview') }}</h1>
<!-- Filter and Search Form -->
<div class="card mb-4">
<div class="card-header">{{ trans('Filter and Search') }}</div>
<div class="card-body">
<form id="filterForm">
<div class="row">
<div class="col-md-6">
<label for="search" class="form-label">{{ trans('Search') }}</label>
<input type="text" class="form-control" id="search" name="search" value="{{ search }}" placeholder="{{ trans('Search by name') }}">
</div>
<div class="col-md-4">
<label for="category_filter" class="form-label">{{ trans('Category') }}</label>
<select class="form-select" id="category_filter" name="category_id">
<option value="">{{ trans('-- All Categories --') }}</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if selected_category == category.id %}selected{% endif %}>{{ category.path }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary">{{ trans('Filter') }}</button>
</div>
</div>
</form>
</div>
</div>
<!-- Items List -->
<ul id="itemsList" class="list-group mb-4">
{% if items %}
{% for item in items %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<h5>{{ item.name }}</h5>
<p class="text-muted">{{ trans('Category') }}: {{ item.category_name ?: trans('Uncategorized') }}</p>
<p>{{ item.description }}</p>
</div>
<div>
<button class="btn btn-sm btn-warning me-2 edit-btn" data-id="{{ item.id }}">Edit</button>
<button class="btn btn-sm btn-danger delete-btn" data-id="{{ item.id }}">Delete</button>
</div>
</li>
{% endfor %}
{% else %}
<li class="list-group-item">{{ trans('No items found.') }}</li>
{% endif %}
</ul>
<!-- Edit Item Modal -->
<div class="modal fade" id="editItemModal" tabindex="-1" aria-labelledby="editItemModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editItemModalLabel">{{ trans('Edit Part') }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editItemForm">
<input type="hidden" id="edit_item_id" name="id">
<div class="mb-3">
<label for="edit_item_name" class="form-label">{{ trans('Part Name') }}</label>
<input type="text" class="form-control" id="edit_item_name" name="item_name" required>
</div>
<div class="mb-3">
<label for="edit_item_description" class="form-label">{{ trans('Description') }}</label>
<textarea class="form-control" id="edit_item_description" name="item_description" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="edit_item_category_id" class="form-label">{{ trans('Category') }}</label>
<select class="form-select" id="edit_item_category_id" name="category_id">
<option value="">{{ trans('-- Select Category --') }}</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.path }}</option>
{% endfor %}
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ trans('Cancel') }}</button>
<button type="button" class="btn btn-primary" id="saveEditItemBtn">{{ trans('Save Changes') }}</button>
</div>
</div>
</div>
</div>
{% endautoescape %}

37
templates/layout.twig Executable file
View File

@@ -0,0 +1,37 @@
{% autoescape %}
<!DOCTYPE html>
<html lang="{{ current_locale }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ app_name }}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="style.css"> <!-- Custom CSS -->
</head>
<body>
{% include 'partials/navbar.twig' %}
<div class="container-fluid mt-4">
<div class="row">
<div class="col-md-3 col-lg-2 d-none d-md-block sidebar">
{% include 'partials/sidebar.twig' %}
</div>
<div class="col-md-9 col-lg-10 content">
<div id="main-content">
{% block content %}
<!-- Content will be loaded here via AJAX -->
{% endblock %}
</div>
</div>
</div>
</div>
<!-- Bootstrap JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2AEu8T+c+7f+z/j43" crossorigin="anonymous"></script>
<!-- Custom JS -->
<script src="js/app.js"></script>
</body>
</html>
{% endautoescape %}

53
templates/overview.twig Normal file
View File

@@ -0,0 +1,53 @@
{% autoescape %}
<h1>{{ trans('Overview') }}</h1>
<!-- Filter and Search Form -->
<div class="card mb-4">
<div class="card-header">{{ trans('Filter and Search') }}</div>
<div class="card-body">
<form id="filterForm">
<div class="row">
<div class="col-md-6">
<label for="search" class="form-label">{{ trans('Search') }}</label>
<input type="text" class="form-control" id="search" name="search" value="{{ search }}" placeholder="{{ trans('Search by name') }}">
</div>
<div class="col-md-4">
<label for="category_filter" class="form-label">{{ trans('Category') }}</label>
<select class="form-select" id="category_filter" name="category_id">
<option value="">{{ trans('-- All Categories --') }}</option>
{% for category in categories %}
<option value="{{ category.id }}" {% if selected_category == category.id %}selected{% endif %}>{{ category.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary">{{ trans('Filter') }}</button>
</div>
</div>
</form>
</div>
</div>
<!-- Items List -->
<h2>{{ trans('All Parts') }}</h2>
<ul id="itemsList" class="list-group mb-4">
{% if items %}
{% for item in items %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<h5>{{ item.name }}</h5>
<p class="text-muted">{{ trans('Category') }}: {{ item.category_name ?: trans('Uncategorized') }}</p>
<p>{{ item.description }}</p>
</div>
<div>
<button class="btn btn-sm btn-warning me-2 edit-btn" data-id="{{ item.id }}">Edit</button>
<button class="btn btn-sm btn-danger delete-btn" data-id="{{ item.id }}">Delete</button>
</div>
</li>
{% endfor %}
{% else %}
<li class="list-group-item">{{ trans('No items found.') }}</li>
{% endif %}
</ul>
{% endautoescape %}

32
templates/partials/navbar.twig Executable file
View File

@@ -0,0 +1,32 @@
{% autoescape %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container-fluid">
<a class="navbar-brand" href="#" data-route="/">{{app_name}}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#" data-route="/">{{ trans('Overview') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-route="/categories">{{ trans('Categories') }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" data-route="/parts">{{ trans('Parts') }}</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<select class="form-select form-select-sm" onchange="window.location.href='/lang/' + this.value">
{% for locale in supported_locales %}
<option value="{{ locale }}" {% if current_locale == locale %}selected{% endif %}>{{ trans('language_' ~ locale) }}</option>
{% endfor %}
</select>
</li>
</ul>
</div>
</div>
</nav>
{% endautoescape %}

13
templates/partials/sidebar.twig Executable file
View File

@@ -0,0 +1,13 @@
{% autoescape %}
<ul class="nav flex-column nav-pills">
<li class="nav-item">
<a class="nav-link {% if active_page == 'overview' %}active{% endif %}" href="#" data-route="/">{{ trans('Overview') }}</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'categories' %}active{% endif %}" href="#" data-route="/categories">{{ trans('Categories') }}</a>
</li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'parts' %}active{% endif %}" href="#" data-route="/parts">{{ trans('Parts') }}</a>
</li>
</ul>
{% endautoescape %}

30
templates/parts.twig Normal file
View File

@@ -0,0 +1,30 @@
{% autoescape %}
<h1>{{ trans('Add New Part') }}</h1>
<div class="card mb-4">
<div class="card-body">
<form id="addItemForm">
<input type="hidden" name="action" value="add_item">
<div class="mb-3">
<label for="item_name" class="form-label">{{ trans('Part Name') }}</label>
<input type="text" class="form-control" id="item_name" name="item_name" required>
</div>
<div class="mb-3">
<label for="item_description" class="form-label">{{ trans('Description') }}</label>
<textarea class="form-control" id="item_description" name="item_description" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="category_id" class="form-label">{{ trans('Category') }}</label>
<select class="form-select" id="category_id" name="category_id" required>
<option value="">{{ trans('-- Select Category --') }}</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.path }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary">{{ trans('Add Part') }}</button>
</form>
</div>
</div>
{% endautoescape %}

114
templates/search.mustache Executable file
View File

@@ -0,0 +1,114 @@
<html>
<head>
<title>Collection Manager - Search</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Search Items</h1>
<nav>
<a href="index.php">Add Item</a> | <a href="search.php">Search/Overview</a>
</nav>
<form id="searchForm">
<input type="hidden" name="action" value="search_items">
<div>
<label for="search_term">Search by Name:</label>
<input type="text" id="search_term" name="q">
</div>
<div>
<label for="search_category_id">Filter by Category:</label>
<select id="search_category_id" name="category_id">
<option value="">-- All Categories --</option>
{{#categories}}
<option value="{{id}}">{{name}}</option>
{{/categories}}
</select>
</div>
<button type="submit">Search</button>
</form>
<h2>Search Results</h2>
<ul id="searchResultsList">
{{#items}}
<li>
<h3>{{name}}</h3>
<p>Category: {{category_name}}</p>
<p>{{description}}</p>
</li>
{{/items}}
{{^items}}
<li>No items found.</li>
{{/items}}
</ul>
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchForm = document.getElementById('searchForm');
const searchResultsList = document.getElementById('searchResultsList');
const searchCategoryIdSelect = document.getElementById('search_category_id');
// Function to fetch categories (needed for the dropdown)
function fetchCategories() {
// Use the same endpoint as index.php to get categories
fetch('index.php?action=get_categories')
.then(response => response.json())
.then(data => {
if (data.success && data.categories) {
searchCategoryIdSelect.innerHTML = '<option value="">-- All Categories --</option>'; // Reset
data.categories.forEach(cat => {
const option = document.createElement('option');
option.value = cat.id;
option.textContent = cat.name;
searchCategoryIdSelect.appendChild(option);
});
}
})
.catch(error => console.error('Error fetching categories:', error));
}
// Handle search form submission
searchForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(searchForm);
const params = new URLSearchParams();
formData.forEach((value, key) => {
params.append(key, value);
});
params.append('action', 'search_items'); // Ensure action is set
fetch('index.php?' + params.toString(), {
method: 'GET'
})
.then(response => response.json())
.then(data => {
displayResults(data.items || []);
})
.catch(error => {
console.error('Error searching items:', error);
searchResultsList.innerHTML = '<li>Error searching items.</li>';
});
});
// Display Search Results
function displayResults(items) {
searchResultsList.innerHTML = ''; // Clear previous results
if (items.length === 0) {
searchResultsList.innerHTML = '<li>No items found.</li>';
return;
}
items.forEach(item => {
const li = document.createElement('li');
li.innerHTML = `<h3>${item.name}</h3>
<p>Category: ${item.category_name || 'Uncategorized'}</p>
<p>${item.description || 'No description available.'}</p>`;
searchResultsList.appendChild(li);
});
}
// Initial fetch of categories for the dropdown
fetchCategories();
});
</script>
</body>
</html>