Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60a0928d2f | |||
| 4abbb50f8c |
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user