2 Commits

Author SHA1 Message Date
admin 60a0928d2f Automatische Datenbank-Backups
Sichert die SQLite-Datenbank periodisch (Standard: alle 24h) nach
server/data/backups und räumt alte Sicherungen auf (Standard: 14
behalten). Zusätzlich per POST /api/backup manuell auslösbar.
2026-07-04 11:56:38 +02:00
admin 4abbb50f8c Aufgaben-Tabelle: inline bearbeitbar, Zuweisung direkt sichtbar
Enddatum und Status lassen sich jetzt direkt in der Tabelle ändern,
Zuweisungen können entfernt werden und das Zuweisungsformular ist
direkt in der Zugewiesen-Spalte statt versteckt im Verwalten-Bereich,
damit auch bei bereits bestehenden Aufgaben nachträglich Mitarbeiter
zugewiesen werden können. Zusätzlich verhindert ein Guard doppeltes
Duplizieren durch Mehrfachklick.
2026-07-04 11:56:12 +02:00
4 changed files with 130 additions and 14 deletions
+44 -13
View File
@@ -7,6 +7,7 @@ const newProject = reactive({ name: '', description: '', start_date: '', end_dat
const newTask = reactive({});
const newAssignment = reactive({});
const newDuplicate = reactive({});
const duplicating = reactive({});
const expanded = reactive({});
async function addProject() {
@@ -45,10 +46,16 @@ function duplicateForm(taskId) {
}
async function duplicateTask(projectId, taskId) {
if (duplicating[taskId]) return;
const form = duplicateForm(taskId);
if (!form.start_date || !form.end_date) return;
await store.duplicateTask(projectId, taskId, { ...form });
Object.assign(form, { start_date: '', end_date: '' });
duplicating[taskId] = true;
try {
await store.duplicateTask(projectId, taskId, { ...form });
Object.assign(form, { start_date: '', end_date: '' });
} finally {
duplicating[taskId] = false;
}
}
</script>
@@ -89,13 +96,41 @@ async function duplicateTask(projectId, taskId) {
<tr v-for="task in project.tasks" :key="task.id">
<td>{{ task.name }}</td>
<td>{{ formatDate(task.start_date) }}</td>
<td>{{ formatDate(task.end_date) }}</td>
<td>
<input
type="date"
:value="task.end_date"
@change="store.updateTask(project.id, task.id, { end_date: $event.target.value })"
/>
</td>
<td>{{ task.estimated_hours }}</td>
<td>{{ task.status }}</td>
<td>
<select
:value="task.status"
@change="store.updateTask(project.id, task.id, { status: $event.target.value })"
>
<option value="geplant">geplant</option>
<option value="in Arbeit">in Arbeit</option>
<option value="erledigt">erledigt</option>
</select>
</td>
<td>
<span v-for="a in task.assignments" :key="a.id" class="tag">
{{ a.employee_name }} ({{ a.allocated_hours_per_week }}h)
<button
class="tag-remove"
title="Zuweisung entfernen"
@click="store.unassignEmployee(project.id, task.id, a.id)"
>×</button>
</span>
<div class="assign-inline">
<select v-model="assignmentForm(task.id).employee_id">
<option value="" disabled>Mitarbeiter wählen</option>
<option v-for="e in store.employees" :key="e.id" :value="e.id">{{ fullName(e) }}</option>
</select>
<input v-model.number="assignmentForm(task.id).allocated_hours_per_week" type="number" style="width: 70px" placeholder="Std/Woche" />
<button class="btn secondary" @click="addAssignment(project.id, task.id)">Zuweisen</button>
</div>
</td>
</tr>
</tbody>
@@ -118,19 +153,15 @@ async function duplicateTask(projectId, taskId) {
<div v-for="task in project.tasks" :key="'manage-' + task.id" style="border: 1px solid var(--border); border-radius: 6px; padding: 8px; margin-bottom: 8px">
<strong>{{ task.name }}</strong>
<div class="form-row">
<select v-model="assignmentForm(task.id).employee_id">
<option value="" disabled>Mitarbeiter wählen</option>
<option v-for="e in store.employees" :key="e.id" :value="e.id">{{ fullName(e) }}</option>
</select>
<input v-model.number="assignmentForm(task.id).allocated_hours_per_week" type="number" style="width: 90px" placeholder="Std/Woche" />
<button class="btn secondary" @click="addAssignment(project.id, task.id)">Zuweisen</button>
</div>
<div class="form-row">
<span class="muted">Aufgabe zu anderem Zeitraum wiederholen:</span>
<input v-model="duplicateForm(task.id).start_date" type="date" />
<input v-model="duplicateForm(task.id).end_date" type="date" />
<button class="btn secondary" @click="duplicateTask(project.id, task.id)">Duplizieren</button>
<button
class="btn secondary"
:disabled="duplicating[task.id]"
@click="duplicateTask(project.id, task.id)"
>Duplizieren</button>
<button class="btn danger" @click="store.deleteTask(project.id, task.id)">Aufgabe löschen</button>
</div>
</div>
+31 -1
View File
@@ -212,7 +212,9 @@ button.btn.danger {
}
.tag {
display: inline-block;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
@@ -221,6 +223,34 @@ button.btn.danger {
margin-right: 4px;
}
.tag-remove {
border: none;
background: none;
cursor: pointer;
color: inherit;
font-size: 13px;
line-height: 1;
padding: 0;
}
.assign-inline {
display: flex;
gap: 4px;
align-items: center;
margin-top: 6px;
}
.assign-inline select,
.assign-inline input {
padding: 4px 6px;
font-size: 12px;
}
.assign-inline button {
padding: 4px 8px;
font-size: 12px;
}
.muted {
color: var(--muted);
font-size: 13px;
+42
View File
@@ -0,0 +1,42 @@
const path = require('path');
const fs = require('fs');
const db = require('./db');
const dataDir = path.join(__dirname, 'data');
const backupDir = path.join(dataDir, 'backups');
const INTERVAL_HOURS = Number(process.env.BACKUP_INTERVAL_HOURS) || 24;
const RETENTION_COUNT = Number(process.env.BACKUP_RETENTION_COUNT) || 14;
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, '-');
}
async function runBackup() {
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
const target = path.join(backupDir, `data-${timestamp()}.db`);
await db.backup(target);
cleanupOldBackups();
console.log(`Backup geschrieben: ${target}`);
return target;
}
function cleanupOldBackups() {
const files = fs
.readdirSync(backupDir)
.filter((f) => f.startsWith('data-') && f.endsWith('.db'))
.sort();
const excess = files.length - RETENTION_COUNT;
for (let i = 0; i < excess; i++) {
fs.unlinkSync(path.join(backupDir, files[i]));
}
}
function startBackupScheduler() {
runBackup().catch((err) => console.error('Backup fehlgeschlagen:', err));
setInterval(() => {
runBackup().catch((err) => console.error('Backup fehlgeschlagen:', err));
}, INTERVAL_HOURS * 60 * 60 * 1000);
}
module.exports = { startBackupScheduler, runBackup };
+13
View File
@@ -1,6 +1,7 @@
const express = require('express');
const cors = require('cors');
const db = require('./db');
const { startBackupScheduler, runBackup } = require('./backup');
const app = express();
app.use(cors());
@@ -364,6 +365,17 @@ app.get('/api/utilization', (req, res) => {
app.get('/health', (req, res) => res.json({ ok: true }));
// ---------- backup ----------
app.post('/api/backup', async (req, res) => {
try {
const file = await runBackup();
res.status(201).json({ file });
} catch (err) {
res.status(500).json({ error: 'Backup fehlgeschlagen' });
}
});
// serve built frontend in production
const path = require('path');
const fs = require('fs');
@@ -377,4 +389,5 @@ if (fs.existsSync(clientDist)) {
app.listen(PORT, () => {
console.log(`Ressourcenplanung-API läuft auf http://localhost:${PORT}`);
startBackupScheduler();
});