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 newTask = reactive({});
|
||||||
const newAssignment = reactive({});
|
const newAssignment = reactive({});
|
||||||
const newDuplicate = reactive({});
|
const newDuplicate = reactive({});
|
||||||
|
const duplicating = reactive({});
|
||||||
const expanded = reactive({});
|
const expanded = reactive({});
|
||||||
|
|
||||||
async function addProject() {
|
async function addProject() {
|
||||||
@@ -45,10 +46,16 @@ function duplicateForm(taskId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function duplicateTask(projectId, taskId) {
|
async function duplicateTask(projectId, taskId) {
|
||||||
|
if (duplicating[taskId]) return;
|
||||||
const form = duplicateForm(taskId);
|
const form = duplicateForm(taskId);
|
||||||
if (!form.start_date || !form.end_date) return;
|
if (!form.start_date || !form.end_date) return;
|
||||||
await store.duplicateTask(projectId, taskId, { ...form });
|
duplicating[taskId] = true;
|
||||||
Object.assign(form, { start_date: '', end_date: '' });
|
try {
|
||||||
|
await store.duplicateTask(projectId, taskId, { ...form });
|
||||||
|
Object.assign(form, { start_date: '', end_date: '' });
|
||||||
|
} finally {
|
||||||
|
duplicating[taskId] = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -89,13 +96,41 @@ async function duplicateTask(projectId, taskId) {
|
|||||||
<tr v-for="task in project.tasks" :key="task.id">
|
<tr v-for="task in project.tasks" :key="task.id">
|
||||||
<td>{{ task.name }}</td>
|
<td>{{ task.name }}</td>
|
||||||
<td>{{ formatDate(task.start_date) }}</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.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>
|
<td>
|
||||||
<span v-for="a in task.assignments" :key="a.id" class="tag">
|
<span v-for="a in task.assignments" :key="a.id" class="tag">
|
||||||
{{ a.employee_name }} ({{ a.allocated_hours_per_week }}h)
|
{{ 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>
|
</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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</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">
|
<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>
|
<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">
|
<div class="form-row">
|
||||||
<span class="muted">Aufgabe zu anderem Zeitraum wiederholen:</span>
|
<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).start_date" type="date" />
|
||||||
<input v-model="duplicateForm(task.id).end_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>
|
<button class="btn danger" @click="store.deleteTask(project.id, task.id)">Aufgabe löschen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+31
-1
@@ -212,7 +212,9 @@ button.btn.danger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -221,6 +223,34 @@ button.btn.danger {
|
|||||||
margin-right: 4px;
|
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 {
|
.muted {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 13px;
|
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 express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const db = require('./db');
|
const db = require('./db');
|
||||||
|
const { startBackupScheduler, runBackup } = require('./backup');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
@@ -364,6 +365,17 @@ app.get('/api/utilization', (req, res) => {
|
|||||||
|
|
||||||
app.get('/health', (req, res) => res.json({ ok: true }));
|
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
|
// serve built frontend in production
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -377,4 +389,5 @@ if (fs.existsSync(clientDist)) {
|
|||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Ressourcenplanung-API läuft auf http://localhost:${PORT}`);
|
console.log(`Ressourcenplanung-API läuft auf http://localhost:${PORT}`);
|
||||||
|
startBackupScheduler();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user