Initial commit of the Asset Management System, including project structure, Docker configuration, database migrations, and core application files. Added user authentication, asset management features, and basic UI components.

This commit is contained in:
2025-08-22 21:41:02 +02:00
parent b43a98f0ec
commit 677f70a19c
52 changed files with 5186 additions and 2 deletions

534
public/assets/css/style.css Normal file
View File

@@ -0,0 +1,534 @@
/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* Header */
.header {
background: #2c3e50;
color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: sticky;
top: 0;
z-index: 1000;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
}
.logo h1 {
font-size: 1.5rem;
font-weight: 600;
}
/* Navigation */
.nav-list {
display: flex;
list-style: none;
gap: 2rem;
}
.nav-link {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
transition: background-color 0.3s;
}
.nav-link:hover {
background-color: rgba(255,255,255,0.1);
}
.nav-dropdown {
position: relative;
}
.nav-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
background: white;
color: #333;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
border-radius: 4px;
min-width: 200px;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s;
}
.nav-dropdown:hover .nav-dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.nav-dropdown-menu li {
list-style: none;
}
.nav-dropdown-menu a {
color: #333;
text-decoration: none;
padding: 0.75rem 1rem;
display: block;
border-bottom: 1px solid #eee;
}
.nav-dropdown-menu a:hover {
background-color: #f8f9fa;
}
/* User Menu */
.user-menu {
position: relative;
}
.user-dropdown {
position: relative;
}
.user-link {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
background-color: rgba(255,255,255,0.1);
}
.user-dropdown-menu {
position: absolute;
top: 100%;
right: 0;
background: white;
color: #333;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
border-radius: 4px;
min-width: 150px;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s;
}
.user-dropdown:hover .user-dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.user-dropdown-menu li {
list-style: none;
}
.user-dropdown-menu a {
color: #333;
text-decoration: none;
padding: 0.75rem 1rem;
display: block;
border-bottom: 1px solid #eee;
}
.user-dropdown-menu a:hover {
background-color: #f8f9fa;
}
/* Main Content */
.main {
min-height: calc(100vh - 140px);
padding: 2rem 0;
}
/* Flash Messages */
.flash-messages {
margin-bottom: 2rem;
}
.alert {
padding: 1rem;
margin-bottom: 1rem;
border-radius: 4px;
position: relative;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-warning {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.alert-info {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.alert-close {
position: absolute;
top: 0.5rem;
right: 1rem;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: inherit;
}
/* Login Styles */
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-box {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-header h2 {
color: #2c3e50;
margin-bottom: 0.5rem;
}
.login-header p {
color: #7f8c8d;
}
/* Forms */
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #2c3e50;
}
.form-control {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.3s;
}
.form-control:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
/* Buttons */
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-primary:hover {
background-color: #2980b9;
}
.btn-secondary {
background-color: #95a5a6;
color: white;
}
.btn-secondary:hover {
background-color: #7f8c8d;
}
.btn-success {
background-color: #27ae60;
color: white;
}
.btn-success:hover {
background-color: #229954;
}
.btn-danger {
background-color: #e74c3c;
color: white;
}
.btn-danger:hover {
background-color: #c0392b;
}
.btn-block {
display: block;
width: 100%;
}
/* Tables */
.table-container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #eee;
}
.table th {
background-color: #f8f9fa;
font-weight: 600;
color: #2c3e50;
}
.table tbody tr:hover {
background-color: #f8f9fa;
}
/* Cards */
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.card-header {
border-bottom: 1px solid #eee;
padding-bottom: 1rem;
margin-bottom: 1rem;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: #2c3e50;
margin: 0;
}
/* Dashboard Stats */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
}
.stat-number {
font-size: 2rem;
font-weight: 700;
color: #3498db;
margin-bottom: 0.5rem;
}
.stat-label {
color: #7f8c8d;
font-size: 0.9rem;
}
/* Pagination */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
margin-top: 2rem;
}
.pagination a,
.pagination span {
padding: 0.5rem 1rem;
border: 1px solid #ddd;
border-radius: 4px;
text-decoration: none;
color: #333;
transition: all 0.3s;
}
.pagination a:hover {
background-color: #f8f9fa;
}
.pagination .current {
background-color: #3498db;
color: white;
border-color: #3498db;
}
/* Footer */
.footer {
background: #2c3e50;
color: white;
text-align: center;
padding: 1rem 0;
margin-top: auto;
}
/* Responsive Design */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 1rem;
}
.nav-list {
flex-direction: column;
gap: 0.5rem;
}
.nav-dropdown-menu,
.user-dropdown-menu {
position: static;
opacity: 1;
visibility: visible;
transform: none;
box-shadow: none;
background: transparent;
color: white;
}
.nav-dropdown-menu a,
.user-dropdown-menu a {
color: white;
border-bottom: none;
}
.stats-grid {
grid-template-columns: 1fr;
}
.table-container {
overflow-x: auto;
}
.table {
min-width: 600px;
}
}
/* Utility Classes */
.text-center { text-align: center; }
.text-right { text-align: right; }
.text-left { text-align: left; }
.mb-1 { margin-bottom: 0.5rem; }
.mb-2 { margin-bottom: 1rem; }
.mb-3 { margin-bottom: 1.5rem; }
.mb-4 { margin-bottom: 2rem; }
.mt-1 { margin-top: 0.5rem; }
.mt-2 { margin-top: 1rem; }
.mt-3 { margin-top: 1.5rem; }
.mt-4 { margin-top: 2rem; }
.d-none { display: none; }
.d-block { display: block; }
.d-flex { display: flex; }
.d-grid { display: grid; }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
.align-center { align-items: center; }
.w-100 { width: 100%; }
.h-100 { height: 100%; }
/* Print Styles */
@media print {
.header,
.footer,
.nav,
.user-menu,
.btn,
.pagination {
display: none !important;
}
.main {
padding: 0;
}
.container {
max-width: none;
padding: 0;
}
.card {
box-shadow: none;
border: 1px solid #ddd;
}
}

466
public/assets/js/app.js Normal file
View File

@@ -0,0 +1,466 @@
/**
* Asset Management System - JavaScript
*/
document.addEventListener('DOMContentLoaded', function() {
// Initialize all components
initAlerts();
initDropdowns();
initForms();
initTables();
initModals();
initFileUploads();
});
/**
* Alert handling
*/
function initAlerts() {
// Auto-hide alerts after 5 seconds
const alerts = document.querySelectorAll('.alert');
alerts.forEach(alert => {
setTimeout(() => {
if (alert.parentElement) {
alert.style.opacity = '0';
setTimeout(() => {
alert.remove();
}, 300);
}
}, 5000);
});
// Close button functionality
document.addEventListener('click', function(e) {
if (e.target.classList.contains('alert-close')) {
const alert = e.target.closest('.alert');
if (alert) {
alert.style.opacity = '0';
setTimeout(() => {
alert.remove();
}, 300);
}
}
});
}
/**
* Dropdown handling
*/
function initDropdowns() {
// Close dropdowns when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.nav-dropdown, .user-dropdown')) {
const dropdowns = document.querySelectorAll('.nav-dropdown-menu, .user-dropdown-menu');
dropdowns.forEach(dropdown => {
dropdown.style.opacity = '0';
dropdown.style.visibility = 'hidden';
dropdown.style.transform = 'translateY(-10px)';
});
}
});
}
/**
* Form handling
*/
function initForms() {
// Form validation
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', function(e) {
const requiredFields = form.querySelectorAll('[required]');
let isValid = true;
requiredFields.forEach(field => {
if (!field.value.trim()) {
isValid = false;
field.classList.add('error');
} else {
field.classList.remove('error');
}
});
if (!isValid) {
e.preventDefault();
showAlert('Bitte füllen Sie alle erforderlichen Felder aus.', 'error');
}
});
});
// Real-time validation
document.addEventListener('input', function(e) {
if (e.target.hasAttribute('required')) {
if (e.target.value.trim()) {
e.target.classList.remove('error');
} else {
e.target.classList.add('error');
}
}
});
}
/**
* Table handling
*/
function initTables() {
// Sortable tables
const sortableHeaders = document.querySelectorAll('.table th[data-sort]');
sortableHeaders.forEach(header => {
header.addEventListener('click', function() {
const table = this.closest('.table');
const column = this.dataset.sort;
const currentOrder = this.dataset.order || 'asc';
const newOrder = currentOrder === 'asc' ? 'desc' : 'asc';
// Update URL with sort parameters
const url = new URL(window.location);
url.searchParams.set('sort', column);
url.searchParams.set('order', newOrder);
window.location.href = url.toString();
});
});
// Bulk actions
const bulkCheckbox = document.getElementById('bulk-select-all');
if (bulkCheckbox) {
bulkCheckbox.addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.bulk-select');
checkboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
updateBulkActions();
});
}
document.addEventListener('change', function(e) {
if (e.target.classList.contains('bulk-select')) {
updateBulkActions();
}
});
}
/**
* Update bulk actions visibility
*/
function updateBulkActions() {
const selectedItems = document.querySelectorAll('.bulk-select:checked');
const bulkActions = document.querySelector('.bulk-actions');
if (bulkActions) {
if (selectedItems.length > 0) {
bulkActions.style.display = 'block';
bulkActions.querySelector('.selected-count').textContent = selectedItems.length;
} else {
bulkActions.style.display = 'none';
}
}
}
/**
* Modal handling
*/
function initModals() {
// Open modal
document.addEventListener('click', function(e) {
if (e.target.hasAttribute('data-modal')) {
const modalId = e.target.dataset.modal;
const modal = document.getElementById(modalId);
if (modal) {
modal.style.display = 'block';
document.body.style.overflow = 'hidden';
}
}
});
// Close modal
document.addEventListener('click', function(e) {
if (e.target.classList.contains('modal-close') || e.target.classList.contains('modal')) {
const modal = e.target.closest('.modal');
if (modal) {
modal.style.display = 'none';
document.body.style.overflow = 'auto';
}
}
});
// Close modal with Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const openModal = document.querySelector('.modal[style*="display: block"]');
if (openModal) {
openModal.style.display = 'none';
document.body.style.overflow = 'auto';
}
}
});
}
/**
* File upload handling
*/
function initFileUploads() {
const fileInputs = document.querySelectorAll('input[type="file"]');
fileInputs.forEach(input => {
input.addEventListener('change', function() {
const files = Array.from(this.files);
const maxSize = this.dataset.maxSize || 52428800; // 50MB default
const allowedTypes = this.dataset.allowedTypes ? this.dataset.allowedTypes.split(',') : [];
files.forEach(file => {
// Check file size
if (file.size > maxSize) {
showAlert(`Datei "${file.name}" ist zu groß. Maximale Größe: ${formatBytes(maxSize)}`, 'error');
this.value = '';
return;
}
// Check file type
if (allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
showAlert(`Dateityp "${file.type}" ist nicht erlaubt.`, 'error');
this.value = '';
return;
}
});
});
});
}
/**
* Search functionality
*/
function initSearch() {
const searchInput = document.getElementById('search-input');
if (searchInput) {
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const query = this.value.trim();
if (query.length >= 2 || query.length === 0) {
performSearch(query);
}
}, 300);
});
}
}
/**
* Perform search
*/
function performSearch(query) {
const url = new URL(window.location);
if (query) {
url.searchParams.set('search', query);
} else {
url.searchParams.delete('search');
}
url.searchParams.delete('page'); // Reset to first page
window.location.href = url.toString();
}
/**
* Export functionality
*/
function exportData(format) {
const url = new URL(window.location);
url.searchParams.set('export', format);
window.location.href = url.toString();
}
/**
* Print functionality
*/
function printPage() {
window.print();
}
/**
* QR Code scanner
*/
function initQRScanner() {
const scanButton = document.getElementById('scan-qr');
if (scanButton) {
scanButton.addEventListener('click', function() {
// This would integrate with a QR code scanner library
// For now, we'll show a prompt
const code = prompt('Bitte QR-Code scannen oder Inventarnummer eingeben:');
if (code) {
document.getElementById('inventory-number').value = code;
document.getElementById('scan-form').submit();
}
});
}
}
/**
* Asset assignment
*/
function assignAsset(assetId) {
const userId = prompt('Bitte Benutzer-ID eingeben:');
if (userId) {
const form = document.createElement('form');
form.method = 'POST';
form.action = `/assets/${assetId}/assign`;
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrf_token';
csrfInput.value = document.querySelector('input[name="csrf_token"]').value;
const userInput = document.createElement('input');
userInput.type = 'hidden';
userInput.name = 'user_id';
userInput.value = userId;
form.appendChild(csrfInput);
form.appendChild(userInput);
document.body.appendChild(form);
form.submit();
}
}
/**
* Delete confirmation
*/
function confirmDelete(message = 'Sind Sie sicher, dass Sie diesen Eintrag löschen möchten?') {
return confirm(message);
}
/**
* Show alert message
*/
function showAlert(message, type = 'info') {
const alertContainer = document.querySelector('.flash-messages') || document.body;
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.innerHTML = `
${message}
<button type="button" class="alert-close">&times;</button>
`;
alertContainer.appendChild(alert);
// Auto-hide after 5 seconds
setTimeout(() => {
alert.style.opacity = '0';
setTimeout(() => {
alert.remove();
}, 300);
}, 5000);
}
/**
* Format bytes to human readable
*/
function formatBytes(bytes) {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
}
/**
* Debounce function
*/
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* Throttle function
*/
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
/**
* AJAX helper
*/
function ajax(url, options = {}) {
const defaultOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
};
const config = { ...defaultOptions, ...options };
return fetch(url, config)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.catch(error => {
console.error('AJAX error:', error);
throw error;
});
}
/**
* Local storage helper
*/
const Storage = {
set: function(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error('Error saving to localStorage', e);
}
},
get: function(key, defaultValue = null) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (e) {
console.error('Error reading from localStorage', e);
return defaultValue;
}
},
remove: function(key) {
try {
localStorage.removeItem(key);
} catch (e) {
console.error('Error removing from localStorage', e);
}
}
};
// Initialize search if on a page with search
if (document.getElementById('search-input')) {
initSearch();
}
// Initialize QR scanner if on inventory page
if (document.getElementById('scan-qr')) {
initQRScanner();
}

26
public/index.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
/**
* Front Controller - Asset Management System
*/
// Start session
session_start();
// Load Composer autoloader
require_once __DIR__ . '/../vendor/autoload.php';
// Load environment variables
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
// Load configuration
require_once __DIR__ . '/../config/config.php';
// Load routes
require_once __DIR__ . '/../config/routes.php';
// Initialize application
$app = new App\Core\Application();
// Handle the request
$app->handle();