first commit
This commit is contained in:
5
public/.htaccess
Normal file
5
public/.htaccess
Normal file
@@ -0,0 +1,5 @@
|
||||
RewriteEngine On
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . index.php [L]
|
||||
33
public/000-default.conf
Normal file
33
public/000-default.conf
Normal file
@@ -0,0 +1,33 @@
|
||||
<VirtualHost *:80>
|
||||
# The ServerName directive sets the request scheme, hostname and port that
|
||||
# the server uses to identify itself. This is used when creating
|
||||
# redirection URLs. In the context of virtual hosts, the ServerName
|
||||
# specifies what hostname must appear in the request's Host: header to
|
||||
# match this virtual host. For the default virtual host (this file) this
|
||||
# value is not decisive as it is used as a last resort host regardless.
|
||||
# However, you must set it for any further virtual host explicitly.
|
||||
#ServerName www.example.com
|
||||
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/localhost/public
|
||||
|
||||
<Directory /var/www/localhost/public>
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
|
||||
# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
|
||||
# error, crit, alert, emerg.
|
||||
# It is also possible to configure the loglevel for particular
|
||||
# modules, e.g.
|
||||
#LogLevel info ssl:warn
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
|
||||
# For most configuration files from conf-available/, which are
|
||||
# enabled or disabled at a global level, it is possible to
|
||||
# include a line for only one particular virtual host. For example the
|
||||
# following line enables the CGI configuration for this host only
|
||||
# after it has been globally disabled with "a2disconf".
|
||||
#Include conf-available/serve-cgi-bin.conf
|
||||
</VirtualHost>
|
||||
58
public/index.php
Normal file
58
public/index.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
$autoloadPath = __DIR__ . '/../vendor/autoload.php';
|
||||
if (!file_exists($autoloadPath)) {
|
||||
http_response_code(500);
|
||||
die("FATAL ERROR: Composer autoloader not found. Please run 'composer install' in the project root.");
|
||||
}
|
||||
require $autoloadPath;
|
||||
|
||||
require __DIR__ . '/../config.php';
|
||||
|
||||
use App\Router;
|
||||
use App\Controllers\ItemController;
|
||||
use App\Controllers\CategoryController;
|
||||
use App\Database;
|
||||
|
||||
// Initialize Database (ensures tables exist)
|
||||
Database::getInstance();
|
||||
|
||||
$router = new Router();
|
||||
|
||||
// --- Language Switch Route ---
|
||||
$router->addRoute('GET', '/lang/{locale}', function ($locale) {
|
||||
if (in_array($locale, SUPPORTED_LOCALES)) {
|
||||
$_SESSION['locale'] = $locale;
|
||||
}
|
||||
// Redirect back to the page the user came from, or home
|
||||
$referer = $_SERVER['HTTP_REFERER'] ?? '/';
|
||||
header("Location: " . $referer);
|
||||
exit;
|
||||
});
|
||||
|
||||
// --- Web Routes (Full Page/Initial Load) ---
|
||||
$router->addRoute('GET', '/', [ItemController::class, 'overview']);
|
||||
$router->addRoute('GET', '/categories', [CategoryController::class, 'index']);
|
||||
$router->addRoute('GET', '/parts', [ItemController::class, 'addForm']);
|
||||
|
||||
// --- API Routes (AJAX Content) ---
|
||||
// These routes return only the Twig block content, not the full layout.
|
||||
$router->addRoute('GET', '/api/items', [ItemController::class, 'listItems']);
|
||||
$router->addRoute('GET', '/api/categories', [CategoryController::class, 'listCategories']);
|
||||
$router->addRoute('GET', '/api/categories/{id:\d+}', [CategoryController::class, 'getCategory']);
|
||||
$router->addRoute('GET', '/api/parts', [ItemController::class, 'renderAddForm']);
|
||||
|
||||
// --- API CRUD Routes ---
|
||||
// Items
|
||||
$router->addRoute('GET', '/api/items/{id:\d+}', [ItemController::class, 'getItem']);
|
||||
$router->addRoute('POST', '/api/items', [ItemController::class, 'create']);
|
||||
$router->addRoute('PUT', '/api/items/{id:\d+}', [ItemController::class, 'update']);
|
||||
$router->addRoute('DELETE', '/api/items/{id:\d+}', [ItemController::class, 'delete']);
|
||||
|
||||
// Categories
|
||||
$router->addRoute('POST', '/api/categories', [CategoryController::class, 'create']);
|
||||
$router->addRoute('PUT', '/api/categories/{id:\d+}', [CategoryController::class, 'update']);
|
||||
$router->addRoute('DELETE', '/api/categories/{id:\d+}', [CategoryController::class, 'delete']);
|
||||
|
||||
|
||||
$router->dispatch();
|
||||
401
public/js/app.js
Executable file
401
public/js/app.js
Executable file
@@ -0,0 +1,401 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const mainContent = document.getElementById('main-content');
|
||||
const appName = document.querySelector('.navbar-brand').textContent;
|
||||
|
||||
// --- 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 === '/categories') {
|
||||
apiPath = '/api/categories';
|
||||
pageTitle = 'Categories';
|
||||
} else if (path === '/parts') {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
// 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('/parts', false);
|
||||
} 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_item_category_id').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('Are you sure you want to delete this part?')) {
|
||||
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;
|
||||
|
||||
if (!name) {
|
||||
alert('Part name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
item_name: name,
|
||||
item_description: description,
|
||||
category_id: categoryId || null
|
||||
};
|
||||
|
||||
fetch('/api/items/' + id, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('editItemModal')).hide();
|
||||
fetchContent('/overview', false); // Assuming current is overview
|
||||
} else {
|
||||
alert(data.error || 'Error updating part');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error updating part');
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
bootstrap.Modal.getInstance(document.getElementById('editCategoryModal')).hide();
|
||||
fetchContent('/categories', false);
|
||||
} else {
|
||||
alert(data.error || 'Error updating category');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error updating category');
|
||||
});
|
||||
}
|
||||
|
||||
function handleEditCategory() {
|
||||
const id = this.getAttribute('data-id');
|
||||
// Fetch current category data
|
||||
fetch('/api/categories/' + id)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data) {
|
||||
document.getElementById('edit_category_id').value = data.id;
|
||||
document.getElementById('edit_category_name').value = data.name;
|
||||
document.getElementById('edit_parent_category_id').value = data.parent_id || '';
|
||||
const modal = new bootstrap.Modal(document.getElementById('editCategoryModal'));
|
||||
modal.show();
|
||||
} else {
|
||||
alert('Category not found');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error fetching category');
|
||||
});
|
||||
}
|
||||
|
||||
function handleDeleteCategory() {
|
||||
const id = this.getAttribute('data-id');
|
||||
if (confirm('Are you sure you want to delete this category?')) {
|
||||
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');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
1
public/search.php
Executable file
1
public/search.php
Executable file
@@ -0,0 +1 @@
|
||||
<?php\n\nrequire __DIR__ . \'/../vendor/autoload.php\';\nrequire __DIR__ . \'/../src/Database.php\';\nrequire __DIR__ . \'/../src/Models/Item.php\';\nrequire __DIR__ . \'/../src/Models/Category.php\';\nrequire __DIR__ . \'/../src/Services/TranslationService.php\';\n\nuse Psr\\Http\\Message\\ServerRequestInterface;\nuse Psr\\Http\\Message\\ResponseInterface;\nuse Slim\\Psr7\\Factory\\ResponseFactory;\nuse Slim\\Psr7\\Factory\\ServerRequestFactory;\n\n// --- Setup ---\n$db = App\\Database\\Database::getInstance();\n$translator = new App\\Services\\TranslationService();\n\n// --- Handle Search Action ---\n// This endpoint is specifically for AJAX search requests\nif (($_SERVER['REQUEST_METHOD'] === 'GET') && isset($_GET['action']) && $_GET['action'] === 'search_items') { \n $locale = $translator->getCurrentLocale(); // Get current locale\n if (isset($_GET['locale'])) {\n $requestedLocale = filter_input(INPUT_GET, 'locale', FILTER_SANITIZE_STRING);\n if (in_array($requestedLocale, ['en', 'nl'])) {\n $locale = $requestedLocale;\n $translator->setLocale($locale); // Set locale for this request\n }\n }\n \n $searchTerm = filter_input(INPUT_GET, 'q', FILTER_SANITIZE_STRING);\n $categoryId = filter_input(INPUT_GET, 'category_id', FILTER_VALIDATE_INT);\n\n $sql = 'SELECT i.id, i.name, i.description, c.name as category_name, i.category_id \n FROM items i \n LEFT JOIN categories c ON i.category_id = c.id WHERE 1=1';\n $params = [];\n\n if ($searchTerm) {\n $sql .= ' AND i.name LIKE :searchTerm';\n $params[':searchTerm'] = '%' . $searchTerm . '%';\n }\n if ($categoryId !== false) {\n $sql .= ' AND i.category_id = :category_id';\n $params[':category_id'] = $categoryId;\n }\n $sql .= ' ORDER BY i.name';\n\n header('Content-Type: application/json');\n try {\n $stmt = $db->prepare($sql);\n $stmt->execute($params);\n $items = $stmt->fetchAll(PDO::FETCH_ASSOC);\n echo json_encode(['success' => true, 'items' => $items]);\n } catch (PDOException $e) {\n error_log("Error searching items: " . $e->getMessage());\n echo json_encode(['success' => false, 'message' => $translator->trans('Error searching items.')]);\n }\n exit;\n}\n\n// If this file is accessed directly without the correct action, return a 404 or error.\nheader('HTTP/1.0 404 Not Found');\necho json_encode(['error' => 'This is an API endpoint. Access via AJAX or main router.']);\n\n?>
|
||||
0
public/style.css
Executable file
0
public/style.css
Executable file
1
public/test.php
Normal file
1
public/test.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php phpinfo(); ?>
|
||||
Reference in New Issue
Block a user