🚀 Release: CorePlan v1.0.2 Community Edition

This commit is contained in:
root
2026-08-26 11:16:37 +02:00
parent 1cdf339b25
commit 6d2345041d
5 changed files with 252 additions and 34 deletions
+130 -13
View File
@@ -62,7 +62,6 @@
.deadline-warn { background: rgba(245, 158, 11, 0.1); border-left: 3px solid var(--accent-orange); }
.deadline-danger { background: rgba(239, 68, 68, 0.2); border-left: 3px solid var(--accent-red); color: var(--accent-red); }
/* Kalender CSS */
.cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; margin-top: 10px;}
.cal-day-header { text-align: center; font-size: 0.75rem; color: var(--text-muted); font-weight: bold; margin-bottom: 5px; }
.cal-day { background: var(--bg-color); min-height: 50px; border-radius: 4px; padding: 5px; font-size: 0.8rem; position: relative; border: 1px solid transparent; cursor: pointer; transition: background 0.2s; }
@@ -70,14 +69,27 @@
.cal-day.today { border-color: var(--accent-blue); }
.cal-day.empty { background: transparent; cursor: default; }
.cal-event-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-top: 2px; margin-right: 2px; }
/* Accordion CSS für native Details */
details > summary { list-style: none; user-select: none; }
details > summary::-webkit-details-marker { display: none; }
/* NEU: Native Print PDF Report Styles */
@media print {
body { background: white !important; color: black !important; padding: 0 !important; margin: 0 !important; }
.container, .modal-overlay, #dashboardView, #loginView, #forgotView, #resetView { display: none !important; }
#printArea { display: block !important; position: absolute; left: 0; top: 0; width: 100%; background: white; color: black; font-family: Arial, sans-serif; padding: 10mm; }
#printArea h1 { color: #3b82f6; text-align: left; border-bottom: 2px solid #ccc; padding-bottom: 10px; margin-bottom: 20px;}
#printArea h2 { color: #555; margin-top: 30px; border-bottom: 1px solid #ccc; padding-bottom: 5px;}
.print-task { border: 1px solid #ddd; padding: 15px; margin-bottom: 15px; border-radius: 6px; page-break-inside: avoid; }
.print-task h3 { margin: 0 0 10px 0; font-size: 1.1rem; }
.print-comment { padding-left: 20px; font-style: italic; color: #555; margin-bottom: 5px; font-size: 0.9rem;}
}
</style>
</head>
<body>
<!-- DRUCK BEREICH (Unsichtbar im Browser, sichtbar beim Drucken) -->
<div id="printArea" style="display: none;"></div>
<div class="container" id="loginView">
<h1>CorePlan Login</h1>
<div class="error-msg" id="loginError">Login fehlgeschlagen.</div>
@@ -85,6 +97,30 @@
<div class="form-group"><label>Benutzername</label><input type="text" id="username" required></div>
<div class="form-group"><label>Passwort</label><input type="password" id="password" required></div>
<button type="submit">Anmelden</button>
<div style="text-align:center; margin-top:1rem; font-size:0.8rem;"><a href="#" onclick="toggleAuthView('forgot')" style="color:var(--text-muted); text-decoration:none;">Passwort vergessen?</a></div>
</form>
</div>
<!-- Passwort Vergessen -->
<div class="container hidden" id="forgotView">
<h1>Passwort zurücksetzen</h1>
<p style="font-size:0.85rem; color:var(--text-muted); text-align:center;">Gib deine E-Mail Adresse ein. Wenn ein Account existiert, senden wir dir einen Link.</p>
<div class="error-msg" id="forgotError" style="color:var(--accent-green);"></div>
<form id="forgotForm">
<div class="form-group"><label>E-Mail Adresse</label><input type="email" id="forgotEmail" required></div>
<button type="submit">Link anfordern</button>
<div style="text-align:center; margin-top:1rem; font-size:0.8rem;"><a href="#" onclick="toggleAuthView('login')" style="color:var(--text-muted); text-decoration:none;">Zurück zum Login</a></div>
</form>
</div>
<!-- Neues Passwort vergeben -->
<div class="container hidden" id="resetView">
<h1>Neues Passwort</h1>
<div class="error-msg" id="resetError"></div>
<form id="resetForm">
<input type="hidden" id="resetToken">
<div class="form-group"><label>Neues Passwort</label><input type="password" id="resetPassword" required minlength="4"></div>
<button type="submit">Passwort speichern</button>
</form>
</div>
@@ -156,7 +192,7 @@
</div>
<div id="tabProjects" class="hidden">
<div class="log-container">
<div class="log-container" id="newProjectFormContainer">
<h3 style="margin-top: 0; margin-bottom: 1rem;">Neues Projekt starten</h3>
<form id="projectForm" style="display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-start;">
<div style="flex: 1; min-width: 200px;"><input type="text" id="projTitle" placeholder="Projekt Titel" required></div>
@@ -235,7 +271,13 @@
<div style="display:flex; justify-content: space-between; margin-bottom: 1rem;"><span id="modalProjectStatus" class="badge">Aktiv</span></div>
<div id="modalBriefingBox" class="info-box hidden"><strong>Steckbrief:</strong> <div id="modalBriefingText" style="margin-top: 5px; line-height: 1.4;"></div></div>
<div id="modalCloseComment" style="display:none; color: var(--text-muted); font-size: 0.9rem; font-style: italic; margin-bottom: 1.5rem; border-left: 3px solid var(--surface-hover); padding-left: 10px;"></div>
<div id="pmControls" class="pm-controls hidden"><button onclick="assignProjectToMe()" style="background-color: var(--surface-color); border: 1px solid var(--text-muted); color: var(--text-main); width: auto; font-size: 0.85rem;">🙋 Mir zuweisen</button><button onclick="extendProject()" style="background-color: var(--accent-blue); width: auto; font-size: 0.85rem;">⏳ Verlängern</button><button onclick="closeProject('closed')" style="background-color: var(--accent-green); width: auto; font-size: 0.85rem;">✔ Abschließen</button><button onclick="closeProject('canceled')" style="background-color: var(--accent-red); width: auto; font-size: 0.85rem;">✖ Abbrechen</button></div>
<div id="pmControls" class="pm-controls hidden">
<button onclick="assignProjectToMe()" style="background-color: var(--surface-color); border: 1px solid var(--text-muted); color: var(--text-main); width: auto; font-size: 0.85rem;">🙋 Mir zuweisen</button>
<button onclick="extendProject()" style="background-color: var(--accent-blue); width: auto; font-size: 0.85rem;">⏳ Verlängern</button>
<button onclick="closeProject('closed')" style="background-color: var(--accent-green); width: auto; font-size: 0.85rem;">✔ Abschließen</button>
<button onclick="closeProject('canceled')" style="background-color: var(--accent-red); width: auto; font-size: 0.85rem;">✖ Abbrechen</button>
<button id="btnReport" onclick="generateReport()" style="background-color: var(--surface-color); border: 1px solid var(--accent-orange); color: var(--accent-orange); width: auto; font-size: 0.85rem; margin-left: auto;">📄 Report</button>
</div>
<form id="taskAddForm" style="display: flex; gap: 10px; margin-bottom: 2rem; flex-wrap: wrap; background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px;">
<input type="text" id="newTaskPhase" placeholder="Phase (z.B. Phase 1)" required style="flex: 1; min-width: 120px;">
<input type="text" id="newTaskTitle" placeholder="Neues To-Do..." required style="flex: 2; min-width: 200px;">
@@ -305,13 +347,61 @@
let currentUserRole = 'user'; let currentUserId = null; let allUsers = [];
let currentCalDate = new Date(); let cachedProjects = []; let cachedTasks = [];
document.addEventListener('DOMContentLoaded', checkSession);
document.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('reset_token');
if (token) {
document.getElementById('loginView').classList.add('hidden');
document.getElementById('dashboardView').classList.add('hidden');
document.getElementById('resetView').classList.remove('hidden');
document.getElementById('resetToken').value = token;
} else {
checkSession();
}
});
function toggleAuthView(view) {
document.getElementById('loginView').classList.add('hidden');
document.getElementById('forgotView').classList.add('hidden');
document.getElementById('resetView').classList.add('hidden');
document.getElementById(view + 'View').classList.remove('hidden');
document.getElementById('forgotError').style.display = 'none';
}
document.getElementById('forgotForm').addEventListener('submit', function(e) {
e.preventDefault();
const fd = new FormData(); fd.append('email', document.getElementById('forgotEmail').value);
fetch('/api/password/forgot', { method: 'POST', body: fd }).then(res => res.json()).then(data => {
const msgBox = document.getElementById('forgotError');
msgBox.innerText = 'Falls die E-Mail existiert, wurde ein Link versendet.';
msgBox.style.color = 'var(--accent-green)';
msgBox.style.display = 'block';
document.getElementById('forgotForm').reset();
});
});
document.getElementById('resetForm').addEventListener('submit', function(e) {
e.preventDefault();
const fd = new FormData();
fd.append('reset_token', document.getElementById('resetToken').value);
fd.append('new_password', document.getElementById('resetPassword').value);
fetch('/api/password/reset', { method: 'POST', body: fd }).then(res => res.json()).then(data => {
if(data.success) {
alert('Passwort erfolgreich geändert! Du kannst dich jetzt anmelden.');
window.location.href = '/';
} else {
const msgBox = document.getElementById('resetError');
msgBox.innerText = data.error; msgBox.style.display = 'block';
}
});
});
function checkSession() {
fetch('/api/me').then(res => res.json()).then(data => {
if (data.success) {
currentUserRole = data.role; currentUserId = data.user_id; document.getElementById('userRoleDisplay').innerText = data.role.toUpperCase();
if(currentUserRole !== 'admin') { document.getElementById('navLogs').style.display = 'none'; document.getElementById('navSettings').style.display = 'none'; document.getElementById('navUsers').style.display = 'none'; }
if(currentUserRole === 'user') { document.getElementById('newProjectFormContainer').style.display = 'none'; }
fetchAllUsers(); showDashboard(data.username);
} else { document.getElementById('loginView').classList.remove('hidden'); }
});
@@ -335,7 +425,6 @@
function showDashboard(username) { document.getElementById('loginView').classList.add('hidden'); document.getElementById('dashboardView').classList.remove('hidden'); document.getElementById('userNameDisplay').innerText = username; toggleView('myarea'); }
function toggleView(tab) { document.getElementById('tabMyarea').classList.add('hidden'); document.getElementById('tabProjects').classList.add('hidden'); document.getElementById('tabLogs').classList.add('hidden'); document.getElementById('tabSettings').classList.add('hidden'); document.getElementById('tabUsers').classList.add('hidden'); const target = document.getElementById('tab'+tab.charAt(0).toUpperCase() + tab.slice(1)); if (target) target.classList.remove('hidden'); if (tab === 'projects') loadProjects(); else if (tab === 'logs') loadAuditLogs(); else if (tab === 'settings') loadSettings(); else if (tab === 'users') loadUsers(); else if (tab === 'myarea') loadMyArea(); }
// --- MEIN BEREICH, TASKS & KALENDER ---
function loadMyArea() {
fetch('/api/myarea').then(res => res.json()).then(data => {
if(data.success) {
@@ -422,7 +511,7 @@
function renderCalendar() {
const year = currentCalDate.getFullYear(); const month = currentCalDate.getMonth(); const today = new Date();
const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate();
let firstDayIndex = firstDay === 0 ? 6 : firstDay - 1; // Start with Monday
let firstDayIndex = firstDay === 0 ? 6 : firstDay - 1;
const monthNames = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"];
document.getElementById('calMonthYear').innerText = `${monthNames[month]} ${year}`;
@@ -470,7 +559,6 @@
document.getElementById('dayViewModal').classList.remove('hidden');
}
// --- COMMENTS (Notizen) ---
function openComments(taskId, title) {
document.getElementById('commentTaskId').value = taskId;
document.getElementById('commentsTaskTitle').innerText = 'Notizen: ' + title;
@@ -502,7 +590,6 @@
function toggleTaskGlobal(taskId, projectId, state) { let comment = prompt("Kurzer Kommentar zur Erledigung:", ""); if (comment === null) return; const formData = new FormData(); formData.append('task_id', taskId); formData.append('project_id', projectId); formData.append('comment', comment); fetch('/api/tasks/toggle', { method: 'POST', body: formData }).then(res=>res.json()).then(data=>{ if(data.success) loadMyArea(); }); }
// --- PROJEKTE ---
function loadProjects() {
fetch('/api/projects').then(res => res.json()).then(data => {
if (data.success) {
@@ -537,7 +624,7 @@
const briefingBox = document.getElementById('modalBriefingBox'); const briefingText = document.getElementById('modalBriefingText');
if (briefing && briefing.trim() !== '') { briefingText.innerHTML = briefing; briefingBox.classList.remove('hidden'); } else { briefingBox.classList.add('hidden'); }
if (status !== 'active') { badge.innerText = status === 'canceled' ? 'Abgebrochen (Schreibgeschützt)' : 'Abgeschlossen (Schreibgeschützt)'; badge.style.background = status === 'canceled' ? 'var(--accent-red)' : 'var(--text-muted)'; pmControls.classList.add('hidden'); taskForm.style.display = 'none'; if (closeComment) { commentBox.innerText = `"${closeComment}"`; commentBox.style.display = 'block'; } else { commentBox.style.display = 'none'; } }
else { badge.innerText = 'Aktiv'; badge.style.background = 'var(--accent-green)'; taskForm.style.display = 'flex'; commentBox.style.display = 'none'; if(currentUserRole === 'pm' || currentUserRole === 'admin') pmControls.classList.remove('hidden'); else pmControls.classList.add('hidden'); }
else { badge.innerText = 'Aktiv'; badge.style.background = 'var(--accent-green)'; taskForm.style.display = 'flex'; commentBox.style.display = 'none'; if(currentUserRole === 'pm' || currentUserRole === 'admin') { pmControls.classList.remove('hidden'); document.getElementById('btnReport').style.display = 'inline-block'; } else { pmControls.classList.add('hidden'); document.getElementById('btnReport').style.display = 'none'; } }
loadTasks();
}
function closeTaskModal() { document.getElementById('taskModal').classList.add('hidden'); currentProjectId = null; loadProjects(); }
@@ -545,6 +632,38 @@
function extendProject() { const newDate = prompt("Bitte neue Deadline eingeben (JJJJ-MM-TT):", ""); if (!newDate) return; const formData = new FormData(); formData.append('id', currentProjectId); formData.append('deadline', newDate); fetch('/api/projects/extend', { method: 'POST', body: formData }).then(res=>res.json()).then(data=>{ if(data.success) closeTaskModal(); else alert(data.error); }); }
function closeProject(status) { const actionText = status === 'canceled' ? 'Abbruch' : 'Abschluss'; const comment = prompt(`Bitte hinterlasse einen zwingenden Kommentar zum ${actionText}:`, ""); if (!comment) { alert('Aktion abgebrochen.'); return; } if(!confirm(`Bist du sicher? Das Projekt wird permanent auf "${status}" gesetzt!`)) return; const formData = new FormData(); formData.append('id', currentProjectId); formData.append('status', status); formData.append('comment', comment); fetch('/api/projects/close', { method: 'POST', body: formData }).then(res=>res.json()).then(data=>{ if(data.success) closeTaskModal(); else alert(data.error); }); }
// NEU: Native Print-Report Engine
function generateReport() {
fetch('/api/projects/report?id=' + currentProjectId).then(res => res.json()).then(data => {
if(!data.success) return;
const p = data.project;
let html = `<h1>Projekt-Report: ${p.title}</h1>`;
html += `<p style="font-size: 1.1rem; border: 1px solid #ccc; padding: 10px; border-radius: 6px;"><strong>Status:</strong> ${p.status.toUpperCase()} &nbsp;|&nbsp; <strong>Ziel:</strong> ${p.deadline || '-'} &nbsp;|&nbsp; <strong>Verantwortlich:</strong> ${p.assigned_username || '-'}</p>`;
html += `<p style="font-size: 1rem; color: #444;"><strong>Kurzbeschreibung:</strong> ${p.description || '-'}</p>`;
if (p.briefing) html += `<p style="font-size: 1rem; color: #444;"><strong>Steckbrief / Zielsetzung:</strong><br>${p.briefing.replace(/\n/g, '<br>')}</p>`;
html += `<h2 style="margin-top: 30px;">Aufgaben & Notizen</h2>`;
data.tasks.forEach(t => {
let status = t.is_completed ? '✅ Erledigt' : '⏳ Offen';
let assign = t.assigned_username ? ` | Zuweisung: ${t.assigned_username}` : '';
html += `<div class="print-task">`;
html += `<h3>${t.title} <span style="font-size:0.9rem; font-weight:normal; color:#666;">(${status}${assign})</span></h3>`;
let tComments = data.comments.filter(c => c.task_id === t.id);
if(tComments.length > 0) {
html += `<ul style="margin: 5px 0; padding-left: 20px;">`;
tComments.forEach(c => { html += `<li class="print-comment"><strong>${c.username}</strong> (${c.created_at.substring(0,16)}): ${c.comment}</li>`; });
html += `</ul>`;
} else { html += `<p class="print-comment">Keine Notizen erfasst.</p>`; }
html += `</div>`;
});
document.getElementById('printArea').innerHTML = html;
window.print();
setTimeout(() => { document.getElementById('printArea').innerHTML = ''; }, 2000);
});
}
function loadTasks() {
fetch('/api/tasks?project_id=' + currentProjectId).then(res => res.json()).then(data => {
if(data.success) {
@@ -576,7 +695,6 @@
function loadAuditLogs() { fetch('/api/logs').then(res => res.json()).then(data => { if (data.success) { const tbody = document.getElementById('logTableBody'); tbody.innerHTML = ''; data.data.forEach(log => { tbody.innerHTML += `<tr><td style="color: var(--text-muted);">${log.timestamp}</td><td><strong>${log.username}</strong></td><td>${log.action}</td><td>${log.target || '-'}</td></tr>`; }); } }); }
// NEU: Asynchrone Funktion zur Vermeidung von SQLite "database is locked" Errors
async function saveSpecificSettings(keys, values) {
for(let i=0; i<keys.length; i++) {
let fd = new FormData();
@@ -628,7 +746,6 @@
document.getElementById('licenseForm').addEventListener('submit', function(e) { e.preventDefault(); const formData = new FormData(); formData.append('config_key', 'LICENSE_KEY'); formData.append('config_value', document.getElementById('licenseInput').value); fetch('/api/settings', { method: 'POST', body: formData }).then(res => res.json()).then(data => { if (data.success) { document.getElementById('licenseInput').value = ''; loadSettings(); } }); });
// --- USERS ---
function loadUsers() { fetch('/api/users').then(res => res.json()).then(data => { if (data.success) { const tbody = document.getElementById('usersTableBody'); tbody.innerHTML = ''; data.data.forEach(u => { const safeName = u.username.replace(/'/g, "\\'"); const safeEmail = (u.email || '').replace(/'/g, "\\'"); tbody.innerHTML += `<tr><td>${u.id}</td><td><strong>${u.username}</strong></td><td>${u.email || '-'}</td><td><span class="badge">${u.role}</span></td><td style="text-align: right;"><button class="btn-icon" onclick="openEditUserModal(${u.id}, '${safeName}', '${safeEmail}', '${u.role}')" title="Bearbeiten">✏️</button><button class="btn-icon del" onclick="deleteUser(${u.id})" title="Löschen">🗑️</button></td></tr>`; }); } }); }
function openEditUserModal(id, username, email, role) { document.getElementById('editUserId').value = id; document.getElementById('editUserName').value = username; document.getElementById('editUserEmail').value = email; document.getElementById('editUserRole').value = role; document.getElementById('editUserPass').value = ''; document.getElementById('editUserModal').classList.remove('hidden'); }
+56 -2
View File
@@ -102,7 +102,6 @@ function handleCreateUser() {
exit;
}
// --- NEU: USER BEARBEITEN ---
function handleEditUser() {
global $pdo; @session_start();
if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; }
@@ -111,7 +110,7 @@ function handleEditUser() {
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$role = $_POST['role'] ?? 'user';
$password = $_POST['password'] ?? ''; // Optional
$password = $_POST['password'] ?? '';
if (!in_array($role, ['admin', 'pm', 'user'])) $role = 'user';
@@ -122,6 +121,11 @@ function handleEditUser() {
} else {
$pdo->prepare("UPDATE users SET username = ?, email = ?, role = ? WHERE id = ?")->execute([$username, $email, $role, $id]);
}
// BUGFIX: Alte Logeinträge und aktuelle Session bei Namensänderung aktualisieren
$pdo->prepare("UPDATE audit_logs SET username = ? WHERE user_id = ?")->execute([$username, $id]);
if ($id == $_SESSION['user_id']) { $_SESSION['username'] = $username; }
$pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'User bearbeitet', "User ID: $id ($username)"]);
echo json_encode(['success' => true]);
} catch (PDOException $e) {
@@ -160,3 +164,53 @@ function handleTestGotify() {
if($res === true) echo json_encode(['success' => true]); else echo json_encode(['success' => false, 'error' => $res]);
exit;
}
// --- NEU: PASSWORD RECOVERY SYSTEM ---
function handleForgotPassword() {
global $pdo;
$email = trim($_POST['email'] ?? '');
if (empty($email)) { echo json_encode(['success' => true]); exit; } // Anti-Enumeration
$stmt = $pdo->prepare("SELECT id, username FROM users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user) {
$token = bin2hex(random_bytes(32));
$expires = date('Y-m-d H:i:s', time() + 3600); // 1 Std
$pdo->prepare("UPDATE users SET reset_token = ?, reset_expires = ? WHERE id = ?")->execute([$token, $expires, $user['id']]);
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
$resetLink = $protocol . $_SERVER['HTTP_HOST'] . "/?reset_token=" . $token;
$subject = "CorePlan - Passwort zurücksetzen";
$message = "Hallo " . $user['username'] . ",\n\njemand hat das Zurücksetzen deines Passworts angefordert.\n\nKlicke auf diesen Link, um ein neues Passwort zu vergeben (der Link ist 60 Minuten gültig):\n$resetLink\n\nFalls du das nicht warst, kannst du diese E-Mail einfach ignorieren.\n\nDein CorePlan System";
sendSmtpMail($email, $subject, $message);
$pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$user['id'], $user['username'], 'Password Reset angefordert', $_SERVER['REMOTE_ADDR']]);
}
echo json_encode(['success' => true]); exit;
}
function handleResetPassword() {
global $pdo;
$token = trim($_POST['reset_token'] ?? '');
$newPass = $_POST['new_password'] ?? '';
if (empty($token) || strlen($newPass) < 4) { echo json_encode(['success' => false, 'error' => 'Ungültige Eingabe oder Passwort zu kurz.']); exit; }
$stmt = $pdo->prepare("SELECT id, username FROM users WHERE reset_token = ? AND reset_expires > CURRENT_TIMESTAMP");
$stmt->execute([$token]);
$user = $stmt->fetch();
if ($user) {
$hash = password_hash($newPass, PASSWORD_DEFAULT);
$pdo->prepare("UPDATE users SET password_hash = ?, reset_token = NULL, reset_expires = NULL WHERE id = ?")->execute([$hash, $user['id']]);
$pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$user['id'], $user['username'], 'Password erfolgreich resettet', $_SERVER['REMOTE_ADDR']]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Token ungültig oder abgelaufen.']);
}
exit;
}
?>
+3 -2
View File
@@ -6,13 +6,15 @@ try {
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
// NEU: Concurrency Fixes für SQLite (verhindert "database is locked" Errors)
// Concurrency Fixes für SQLite (verhindert "database is locked" Errors)
$pdo->setAttribute(PDO::ATTR_TIMEOUT, 5);
$pdo->exec("PRAGMA journal_mode = WAL;");
$pdo->exec("PRAGMA busy_timeout = 5000;");
$pdo->exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT DEFAULT 'user')");
try { $pdo->exec("ALTER TABLE users ADD COLUMN email TEXT"); } catch (PDOException $e) {}
try { $pdo->exec("ALTER TABLE users ADD COLUMN reset_token TEXT"); } catch (PDOException $e) {} // NEU
try { $pdo->exec("ALTER TABLE users ADD COLUMN reset_expires DATETIME"); } catch (PDOException $e) {} // NEU
$pdo->exec("CREATE TABLE IF NOT EXISTS audit_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, username TEXT, action TEXT, target TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)");
$pdo->exec("CREATE TABLE IF NOT EXISTS settings (config_key TEXT PRIMARY KEY, config_value TEXT NOT NULL)");
@@ -33,7 +35,6 @@ try {
try { $pdo->exec("ALTER TABLE tasks ADD COLUMN completion_comment TEXT"); } catch (PDOException $e) {}
try { $pdo->exec("ALTER TABLE tasks ADD COLUMN assigned_to INTEGER"); } catch (PDOException $e) {}
// NEU: Auto-Migration für Task Notizen
$pdo->exec("CREATE TABLE IF NOT EXISTS task_comments (id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, username TEXT, comment TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)");
$stmt = $pdo->query("SELECT COUNT(*) FROM users");
+54 -12
View File
@@ -7,7 +7,7 @@ function getProjectLimit() {
$parts = explode('.', $license);
if (count($parts) !== 2) return 3;
$payload = json_decode(base64_decode($parts[0]), true);
$secret = 'TESSMANN_COREPLAN_SECURE_2026_MASTER_KEY';
$secret = 'hierkoennteetwasgeheimesstehenwasnichtsofortjederverstehtabermankannesjaversuchenzuerraten';
$expectedSig = hash_hmac('sha256', $parts[0], $secret);
if (!hash_equals($expectedSig, $parts[1])) return 3;
if (isset($payload['expires']) && strtotime($payload['expires']) < time()) return 3;
@@ -19,15 +19,34 @@ function isPMorAdmin() { return in_array($_SESSION['role'] ?? '', ['admin', 'pm'
function handleGetProjects() {
global $pdo; @session_start();
if (!isset($_SESSION['user_id'])) { http_response_code(401); exit; }
$stmt = $pdo->query("
SELECT p.*, u.username as assigned_username,
(SELECT COUNT(*) FROM tasks WHERE project_id = p.id) as total_tasks,
(SELECT COUNT(*) FROM tasks WHERE project_id = p.id AND is_completed = 1) as completed_tasks
FROM projects p
LEFT JOIN users u ON p.assigned_to = u.id
ORDER BY CASE WHEN p.status = 'closed' THEN 1 WHEN p.status = 'canceled' THEN 2 ELSE 0 END, p.deadline ASC, p.id DESC
");
echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit;
$role = $_SESSION['role'] ?? 'user';
$userId = $_SESSION['user_id'];
// BUGFIX: Normale User sehen nur Projekte, die ihnen zugewiesen sind!
if ($role === 'admin' || $role === 'pm') {
$stmt = $pdo->query("
SELECT p.*, u.username as assigned_username,
(SELECT COUNT(*) FROM tasks WHERE project_id = p.id) as total_tasks,
(SELECT COUNT(*) FROM tasks WHERE project_id = p.id AND is_completed = 1) as completed_tasks
FROM projects p
LEFT JOIN users u ON p.assigned_to = u.id
ORDER BY CASE WHEN p.status = 'closed' THEN 1 WHEN p.status = 'canceled' THEN 2 ELSE 0 END, p.deadline ASC, p.id DESC
");
echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit;
} else {
$stmt = $pdo->prepare("
SELECT p.*, u.username as assigned_username,
(SELECT COUNT(*) FROM tasks WHERE project_id = p.id) as total_tasks,
(SELECT COUNT(*) FROM tasks WHERE project_id = p.id AND is_completed = 1) as completed_tasks
FROM projects p
LEFT JOIN users u ON p.assigned_to = u.id
WHERE p.assigned_to = ?
ORDER BY CASE WHEN p.status = 'closed' THEN 1 WHEN p.status = 'canceled' THEN 2 ELSE 0 END, p.deadline ASC, p.id DESC
");
$stmt->execute([$userId]);
echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit;
}
}
function handleCreateProject() {
@@ -102,6 +121,31 @@ function handleDeleteProject() {
echo json_encode(['success' => true]); exit;
}
// NEU: Backend für den Projekt Print Report
function handleGetProjectReport() {
global $pdo; @session_start();
if (!isset($_SESSION['user_id'])) { http_response_code(401); exit; }
$id = $_GET['id'] ?? 0;
// Projekt ziehen
$stmt = $pdo->prepare("SELECT p.*, u.username as assigned_username FROM projects p LEFT JOIN users u ON p.assigned_to = u.id WHERE p.id = ?");
$stmt->execute([$id]);
$project = $stmt->fetch();
if (!$project) { echo json_encode(['success' => false]); exit; }
// Tasks ziehen
$stmt = $pdo->prepare("SELECT t.*, u.username as assigned_username FROM tasks t LEFT JOIN users u ON t.assigned_to = u.id WHERE t.project_id = ? ORDER BY t.is_completed ASC, t.phase ASC, t.priority ASC");
$stmt->execute([$id]);
$tasks = $stmt->fetchAll();
// Alle Kommentare zu diesen Tasks ziehen
$stmt = $pdo->prepare("SELECT c.* FROM task_comments c JOIN tasks t ON c.task_id = t.id WHERE t.project_id = ? ORDER BY c.created_at ASC");
$stmt->execute([$id]);
$comments = $stmt->fetchAll();
echo json_encode(['success' => true, 'project' => $project, 'tasks' => $tasks, 'comments' => $comments]); exit;
}
function handleGetTasks() {
global $pdo; @session_start();
if (!isset($_SESSION['user_id'])) { http_response_code(401); exit; }
@@ -189,7 +233,6 @@ function handleGetMyArea() {
echo json_encode(['success' => true, 'projects' => $myProjects, 'tasks' => $myTasks]); exit;
}
// NEU: Task Notizen laden
function handleGetTaskComments() {
global $pdo; @session_start();
if (!isset($_SESSION['user_id'])) { http_response_code(401); exit; }
@@ -199,7 +242,6 @@ function handleGetTaskComments() {
echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit;
}
// NEU: Task Notiz hinzufügen
function handleAddTaskComment() {
global $pdo; @session_start();
if (!isset($_SESSION['user_id'])) { http_response_code(401); exit; }
+7 -3
View File
@@ -4,6 +4,8 @@ $request = $_SERVER['REQUEST_URI']; $path = parse_url($request, PHP_URL_PATH); $
if ($path === '/' || $path === '') { header('Content-Type: text/html'); readfile('app.html'); exit;
} elseif ($path === '/api/login' && $method === 'POST') { handleLogin();
} elseif ($path === '/api/logout') { handleLogout();
} elseif ($path === '/api/password/forgot' && $method === 'POST') { handleForgotPassword(); // NEU
} elseif ($path === '/api/password/reset' && $method === 'POST') { handleResetPassword(); // NEU
} elseif ($path === '/api/status' && $method === 'GET') { echo json_encode(['success' => true, 'status' => 'online', 'system' => 'CorePlan']);
} elseif ($path === '/api/logs' && $method === 'GET') { handleGetLogs();
} elseif ($path === '/api/settings' && $method === 'GET') { handleGetSettings();
@@ -14,7 +16,7 @@ if ($path === '/' || $path === '') { header('Content-Type: text/html'); readfile
} elseif ($path === '/api/password' && $method === 'POST') { handleChangePassword();
} elseif ($path === '/api/users' && $method === 'GET') { handleGetUsers();
} elseif ($path === '/api/users' && $method === 'POST') { handleCreateUser();
} elseif ($path === '/api/users/edit' && $method === 'POST') { handleEditUser(); // NEU
} elseif ($path === '/api/users/edit' && $method === 'POST') { handleEditUser();
} elseif ($path === '/api/users/delete' && $method === 'POST') { handleDeleteUser();
// --- COREPLAN ENDPUNKTE ---
} elseif ($path === '/api/projects' && $method === 'GET') { handleGetProjects();
@@ -23,14 +25,16 @@ if ($path === '/' || $path === '') { header('Content-Type: text/html'); readfile
} elseif ($path === '/api/projects/extend' && $method === 'POST') { handleExtendProject();
} elseif ($path === '/api/projects/assign' && $method === 'POST') { handleAssignProject();
} elseif ($path === '/api/projects/delete' && $method === 'POST') { handleDeleteProject();
} elseif ($path === '/api/projects/report' && $method === 'GET') { handleGetProjectReport(); // NEU
} elseif ($path === '/api/tasks' && $method === 'GET') { handleGetTasks();
} elseif ($path === '/api/tasks/create' && $method === 'POST') { handleCreateTask();
} elseif ($path === '/api/tasks/toggle' && $method === 'POST') { handleToggleTask();
} elseif ($path === '/api/tasks/edit' && $method === 'POST') { handleEditTask();
} elseif ($path === '/api/tasks/delete' && $method === 'POST') { handleDeleteTask();
} elseif ($path === '/api/tasks/comments' && $method === 'GET') { handleGetTaskComments(); // NEU
} elseif ($path === '/api/tasks/comment' && $method === 'POST') { handleAddTaskComment(); // NEU
} elseif ($path === '/api/tasks/comments' && $method === 'GET') { handleGetTaskComments();
} elseif ($path === '/api/tasks/comment' && $method === 'POST') { handleAddTaskComment();
} elseif ($path === '/api/myarea' && $method === 'GET') { handleGetMyArea();
} elseif ($path === '/api/me' && $method === 'GET') {
@session_start(); if (isset($_SESSION['user_id'])) { echo json_encode(['success' => true, 'username' => $_SESSION['username'], 'role' => $_SESSION['role'], 'user_id' => $_SESSION['user_id']]); } else { http_response_code(401); echo json_encode(['success' => false, 'error' => 'Not authenticated']); } exit;
} else { http_response_code(404); echo json_encode(['success' => false, 'error' => 'Endpoint not found']); }
?>