initial
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "jahresplanung-backend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "API und Static-Server fuer die Jahresplanung-App",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "node src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.19.2",
|
||||
"cors": "^2.8.5",
|
||||
"mongodb": "^6.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Einmaliges Migrationsskript: liest eine alte data.json (aus der
|
||||
// JSON-Datei-Version der App) und schreibt die Inhalte nach MongoDB.
|
||||
//
|
||||
// Nutzung:
|
||||
// MONGODB_URI=mongodb://user:pass@host:27017 \
|
||||
// node scripts/migrate-json-to-mongo.js /pfad/zu/data.json
|
||||
//
|
||||
// Das Skript ist idempotent-freundlich: bestehende Dokumente mit gleicher
|
||||
// _id werden ueberschrieben (upsert), es entstehen keine Duplikate bei
|
||||
// mehrfacher Ausfuehrung.
|
||||
|
||||
const fs = require("fs");
|
||||
const { MongoClient } = require("mongodb");
|
||||
|
||||
async function main() {
|
||||
const filePath = process.argv[2];
|
||||
if (!filePath) {
|
||||
console.error("Bitte Pfad zur data.json angeben.");
|
||||
console.error("Beispiel: node scripts/migrate-json-to-mongo.js ./data.json");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error(`Datei nicht gefunden: ${filePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
const data = JSON.parse(raw);
|
||||
|
||||
const uri = process.env.MONGODB_URI || "mongodb://localhost:27017";
|
||||
const dbName = process.env.MONGODB_DB || "jahresplanung";
|
||||
|
||||
const client = new MongoClient(uri);
|
||||
await client.connect();
|
||||
const db = client.db(dbName);
|
||||
|
||||
const collections = {
|
||||
people: data.people || [],
|
||||
projects: data.projects || [],
|
||||
tasks: data.tasks || [],
|
||||
absences: data.absences || []
|
||||
};
|
||||
|
||||
for (const [name, items] of Object.entries(collections)) {
|
||||
if (items.length === 0) {
|
||||
console.log(`- ${name}: keine Eintraege, uebersprungen`);
|
||||
continue;
|
||||
}
|
||||
const ops = items.map((item) => {
|
||||
const { id, ...rest } = item; // "id" -> "_id" umbenennen
|
||||
return {
|
||||
replaceOne: {
|
||||
filter: { _id: id },
|
||||
replacement: { _id: id, ...rest },
|
||||
upsert: true
|
||||
}
|
||||
};
|
||||
});
|
||||
const result = await db.collection(name).bulkWrite(ops);
|
||||
console.log(
|
||||
`- ${name}: ${result.upsertedCount} neu, ${result.modifiedCount} aktualisiert`
|
||||
);
|
||||
}
|
||||
|
||||
await client.close();
|
||||
console.log("Migration abgeschlossen.");
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("Migration fehlgeschlagen:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
const { MongoClient, ObjectId } = require("mongodb");
|
||||
|
||||
const uri = process.env.MONGODB_URI || "mongodb://localhost:27017/jahresplanung";
|
||||
|
||||
let client;
|
||||
let dbPromise;
|
||||
|
||||
// Stellt sicher, dass nur eine Verbindung aufgebaut wird und wiederverwendet wird.
|
||||
function connect() {
|
||||
if (!dbPromise) {
|
||||
client = new MongoClient(uri);
|
||||
dbPromise = client
|
||||
.connect()
|
||||
.then((c) => c.db())
|
||||
.catch((err) => {
|
||||
dbPromise = null; // Bei Fehler erneuten Verbindungsversuch beim naechsten Aufruf erlauben
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return dbPromise;
|
||||
}
|
||||
|
||||
// Wandelt eine String-ID in eine ObjectId um, oder liefert null bei ungueltigem Format.
|
||||
function toId(idStr) {
|
||||
if (!idStr || !ObjectId.isValid(idStr)) return null;
|
||||
return new ObjectId(idStr);
|
||||
}
|
||||
|
||||
// Wandelt ein Mongo-Dokument (_id) in die vom Frontend erwartete Form (id) um.
|
||||
function serialize(doc) {
|
||||
if (!doc) return null;
|
||||
const { _id, ...rest } = doc;
|
||||
return { id: _id.toString(), ...rest };
|
||||
}
|
||||
|
||||
module.exports = { connect, toId, serialize, ObjectId };
|
||||
@@ -0,0 +1,19 @@
|
||||
const crypto = require("crypto");
|
||||
|
||||
function newId() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
// Wir speichern unsere eigene UUID direkt als Mongo _id, damit das Frontend
|
||||
// weiterhin ein einfaches "id"-Feld sieht und keine ObjectId-Logik braucht.
|
||||
function toClient(doc) {
|
||||
if (!doc) return null;
|
||||
const { _id, ...rest } = doc;
|
||||
return { id: _id, ...rest };
|
||||
}
|
||||
|
||||
function toClientList(docs) {
|
||||
return docs.map(toClient);
|
||||
}
|
||||
|
||||
module.exports = { newId, toClient, toClientList };
|
||||
@@ -0,0 +1,45 @@
|
||||
const { MongoClient } = require("mongodb");
|
||||
|
||||
const uri = process.env.MONGODB_URI || "mongodb://localhost:27017";
|
||||
const dbName = process.env.MONGODB_DB || "jahresplanung";
|
||||
|
||||
let client;
|
||||
let db;
|
||||
|
||||
async function connect(retries = 10, delayMs = 2000) {
|
||||
if (db) return db;
|
||||
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
client = new MongoClient(uri, { serverSelectionTimeoutMS: 5000 });
|
||||
await client.connect();
|
||||
db = client.db(dbName);
|
||||
|
||||
await db.collection("tasks").createIndex({ projectId: 1 });
|
||||
await db.collection("tasks").createIndex({ assigneeId: 1 });
|
||||
await db.collection("absences").createIndex({ personId: 1 });
|
||||
|
||||
console.log(`Verbunden mit MongoDB-Datenbank "${dbName}"`);
|
||||
return db;
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`MongoDB-Verbindungsversuch ${attempt}/${retries} fehlgeschlagen: ${err.message}`
|
||||
);
|
||||
if (attempt === retries) throw err;
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDb() {
|
||||
if (!db) {
|
||||
throw new Error("MongoDB ist noch nicht verbunden. connect() muss zuerst aufgerufen werden.");
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
async function close() {
|
||||
if (client) await client.close();
|
||||
}
|
||||
|
||||
module.exports = { connect, getDb, close };
|
||||
@@ -0,0 +1,53 @@
|
||||
const path = require("path");
|
||||
const express = require("express");
|
||||
const cors = require("cors");
|
||||
const { connect } = require("./db");
|
||||
|
||||
const peopleRouter = require("./routes/people");
|
||||
const projectsRouter = require("./routes/projects");
|
||||
const tasksRouter = require("./routes/tasks");
|
||||
const absencesRouter = require("./routes/absences");
|
||||
const capacityRouter = require("./routes/capacity");
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
app.get("/api/health", (req, res) => res.json({ status: "ok" }));
|
||||
|
||||
app.use("/api/people", peopleRouter);
|
||||
app.use("/api/projects", projectsRouter);
|
||||
app.use("/api/tasks", tasksRouter);
|
||||
app.use("/api/absences", absencesRouter);
|
||||
app.use("/api/capacity", capacityRouter);
|
||||
|
||||
// Gebautes Frontend (Vite -> dist) ausliefern
|
||||
const frontendDist = path.join(__dirname, "..", "public");
|
||||
app.use(express.static(frontendDist));
|
||||
|
||||
// SPA-Fallback: alles, was nicht /api ist, liefert die index.html aus
|
||||
app.get(/^(?!\/api).*/, (req, res) => {
|
||||
res.sendFile(path.join(frontendDist, "index.html"));
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: "Interner Serverfehler" });
|
||||
});
|
||||
|
||||
async function start() {
|
||||
try {
|
||||
await connect();
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Jahresplanung-Server laeuft auf Port ${PORT}`);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Konnte keine Verbindung zu MongoDB herstellen:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
start();
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
const express = require("express");
|
||||
const { connect, toId, serialize } = require("../db");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
const db = await connect();
|
||||
const absences = await db.collection("absences").find().sort({ startDate: 1 }).toArray();
|
||||
res.json(absences.map(serialize));
|
||||
});
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
const { personId, startDate, endDate, note } = req.body;
|
||||
if (!personId || !startDate || !endDate) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "personId, startDate und endDate sind erforderlich" });
|
||||
}
|
||||
const db = await connect();
|
||||
const personObjectId = toId(personId);
|
||||
if (!personObjectId || !(await db.collection("people").findOne({ _id: personObjectId }))) {
|
||||
return res.status(400).json({ error: "Unbekannte Person" });
|
||||
}
|
||||
const doc = { personId, startDate, endDate, note: note || "" };
|
||||
const result = await db.collection("absences").insertOne(doc);
|
||||
res.status(201).json(serialize({ _id: result.insertedId, ...doc }));
|
||||
});
|
||||
|
||||
router.delete("/:id", async (req, res) => {
|
||||
const _id = toId(req.params.id);
|
||||
if (!_id) return res.status(404).json({ error: "Abwesenheit nicht gefunden" });
|
||||
const db = await connect();
|
||||
const result = await db.collection("absences").deleteOne({ _id });
|
||||
if (result.deletedCount === 0) {
|
||||
return res.status(404).json({ error: "Abwesenheit nicht gefunden" });
|
||||
}
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,86 @@
|
||||
const express = require("express");
|
||||
const { connect } = require("../db");
|
||||
const {
|
||||
countWorkdays,
|
||||
overlapWorkdays,
|
||||
monthsInRange,
|
||||
monthBounds
|
||||
} = require("../utils/dateUtils");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// GET /api/capacity?from=YYYY-MM-DD&to=YYYY-MM-DD
|
||||
// Liefert pro Person und Monat: Kapazitaet (Stunden), zugewiesene Stunden,
|
||||
// Auslastung in Prozent und den Ueberlast-Status.
|
||||
router.get("/", async (req, res) => {
|
||||
const db = await connect();
|
||||
const from = req.query.from || `${new Date().getFullYear()}-01-01`;
|
||||
const to = req.query.to || `${new Date().getFullYear()}-12-31`;
|
||||
const months = monthsInRange(from, to);
|
||||
|
||||
const [people, absences, tasks] = await Promise.all([
|
||||
db.collection("people").find().sort({ name: 1 }).toArray(),
|
||||
db.collection("absences").find().toArray(),
|
||||
db.collection("tasks").find().toArray()
|
||||
]);
|
||||
|
||||
const result = people.map((person) => {
|
||||
const personId = person._id.toString();
|
||||
const dailyHours = person.weeklyHours / 5;
|
||||
|
||||
const personAbsences = absences.filter((a) => a.personId === personId);
|
||||
const personTasks = tasks.filter((t) => t.assigneeId === personId);
|
||||
|
||||
const monthsData = months.map((month) => {
|
||||
const { start: mStart, end: mEnd } = monthBounds(month);
|
||||
const workdaysInMonth = countWorkdays(mStart, mEnd);
|
||||
|
||||
let absenceWorkdays = 0;
|
||||
personAbsences.forEach((a) => {
|
||||
absenceWorkdays += overlapWorkdays(a.startDate, a.endDate, mStart, mEnd);
|
||||
});
|
||||
absenceWorkdays = Math.min(absenceWorkdays, workdaysInMonth);
|
||||
|
||||
const capacityHours = Math.max(
|
||||
0,
|
||||
(workdaysInMonth - absenceWorkdays) * dailyHours
|
||||
);
|
||||
|
||||
let assignedHours = 0;
|
||||
personTasks.forEach((task) => {
|
||||
const taskWorkdays = countWorkdays(task.startDate, task.endDate);
|
||||
if (taskWorkdays === 0) return;
|
||||
const dailyRate = task.estimatedHours / taskWorkdays;
|
||||
const overlap = overlapWorkdays(task.startDate, task.endDate, mStart, mEnd);
|
||||
assignedHours += dailyRate * overlap;
|
||||
});
|
||||
|
||||
const utilization =
|
||||
capacityHours > 0
|
||||
? Math.round((assignedHours / capacityHours) * 100)
|
||||
: assignedHours > 0
|
||||
? 999
|
||||
: 0;
|
||||
|
||||
return {
|
||||
month,
|
||||
capacityHours: Math.round(capacityHours * 10) / 10,
|
||||
assignedHours: Math.round(assignedHours * 10) / 10,
|
||||
utilization,
|
||||
overloaded: assignedHours > capacityHours + 0.5
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
personId,
|
||||
name: person.name,
|
||||
color: person.color,
|
||||
weeklyHours: person.weeklyHours,
|
||||
months: monthsData
|
||||
};
|
||||
});
|
||||
|
||||
res.json({ from, to, months, people: result });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,68 @@
|
||||
const express = require("express");
|
||||
const { connect, toId, serialize } = require("../db");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
const db = await connect();
|
||||
const people = await db.collection("people").find().sort({ name: 1 }).toArray();
|
||||
res.json(people.map(serialize));
|
||||
});
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
const { name, weeklyHours, color, role } = req.body;
|
||||
if (!name || typeof name !== "string") {
|
||||
return res.status(400).json({ error: "name ist erforderlich" });
|
||||
}
|
||||
const db = await connect();
|
||||
const doc = {
|
||||
name,
|
||||
role: role || "",
|
||||
weeklyHours: Number(weeklyHours) || 40,
|
||||
color: color || "#2F6F4F"
|
||||
};
|
||||
const result = await db.collection("people").insertOne(doc);
|
||||
res.status(201).json(serialize({ _id: result.insertedId, ...doc }));
|
||||
});
|
||||
|
||||
router.put("/:id", async (req, res) => {
|
||||
const _id = toId(req.params.id);
|
||||
if (!_id) return res.status(404).json({ error: "Person nicht gefunden" });
|
||||
|
||||
const { name, weeklyHours, color, role } = req.body;
|
||||
const update = {};
|
||||
if (name !== undefined) update.name = name;
|
||||
if (role !== undefined) update.role = role;
|
||||
if (weeklyHours !== undefined) update.weeklyHours = Number(weeklyHours);
|
||||
if (color !== undefined) update.color = color;
|
||||
|
||||
const db = await connect();
|
||||
const updated = await db
|
||||
.collection("people")
|
||||
.findOneAndUpdate({ _id }, { $set: update }, { returnDocument: "after" });
|
||||
|
||||
if (!updated) return res.status(404).json({ error: "Person nicht gefunden" });
|
||||
res.json(serialize(updated));
|
||||
});
|
||||
|
||||
router.delete("/:id", async (req, res) => {
|
||||
const _id = toId(req.params.id);
|
||||
if (!_id) return res.status(404).json({ error: "Person nicht gefunden" });
|
||||
|
||||
const db = await connect();
|
||||
const result = await db.collection("people").deleteOne({ _id });
|
||||
if (result.deletedCount === 0) {
|
||||
return res.status(404).json({ error: "Person nicht gefunden" });
|
||||
}
|
||||
|
||||
const idStr = req.params.id;
|
||||
// Zuweisungen und Abwesenheiten der geloeschten Person aufraeumen
|
||||
await db
|
||||
.collection("tasks")
|
||||
.updateMany({ assigneeId: idStr }, { $set: { assigneeId: null } });
|
||||
await db.collection("absences").deleteMany({ personId: idStr });
|
||||
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,59 @@
|
||||
const express = require("express");
|
||||
const { connect, toId, serialize } = require("../db");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
const db = await connect();
|
||||
const projects = await db.collection("projects").find().sort({ name: 1 }).toArray();
|
||||
res.json(projects.map(serialize));
|
||||
});
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
const { name, description, color } = req.body;
|
||||
if (!name || typeof name !== "string") {
|
||||
return res.status(400).json({ error: "name ist erforderlich" });
|
||||
}
|
||||
const db = await connect();
|
||||
const doc = {
|
||||
name,
|
||||
description: description || "",
|
||||
color: color || "#3E5C50"
|
||||
};
|
||||
const result = await db.collection("projects").insertOne(doc);
|
||||
res.status(201).json(serialize({ _id: result.insertedId, ...doc }));
|
||||
});
|
||||
|
||||
router.put("/:id", async (req, res) => {
|
||||
const _id = toId(req.params.id);
|
||||
if (!_id) return res.status(404).json({ error: "Projekt nicht gefunden" });
|
||||
|
||||
const { name, description, color } = req.body;
|
||||
const update = {};
|
||||
if (name !== undefined) update.name = name;
|
||||
if (description !== undefined) update.description = description;
|
||||
if (color !== undefined) update.color = color;
|
||||
|
||||
const db = await connect();
|
||||
const updated = await db
|
||||
.collection("projects")
|
||||
.findOneAndUpdate({ _id }, { $set: update }, { returnDocument: "after" });
|
||||
|
||||
if (!updated) return res.status(404).json({ error: "Projekt nicht gefunden" });
|
||||
res.json(serialize(updated));
|
||||
});
|
||||
|
||||
router.delete("/:id", async (req, res) => {
|
||||
const _id = toId(req.params.id);
|
||||
if (!_id) return res.status(404).json({ error: "Projekt nicht gefunden" });
|
||||
|
||||
const db = await connect();
|
||||
const result = await db.collection("projects").deleteOne({ _id });
|
||||
if (result.deletedCount === 0) {
|
||||
return res.status(404).json({ error: "Projekt nicht gefunden" });
|
||||
}
|
||||
await db.collection("tasks").deleteMany({ projectId: req.params.id });
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,110 @@
|
||||
const express = require("express");
|
||||
const { connect, toId, serialize } = require("../db");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
const db = await connect();
|
||||
const tasks = await db.collection("tasks").find().sort({ startDate: 1 }).toArray();
|
||||
res.json(tasks.map(serialize));
|
||||
});
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
const {
|
||||
projectId,
|
||||
name,
|
||||
startDate,
|
||||
endDate,
|
||||
estimatedHours,
|
||||
assigneeId,
|
||||
status,
|
||||
priority,
|
||||
dependsOn
|
||||
} = req.body;
|
||||
|
||||
if (!projectId || !name || !startDate || !endDate) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "projectId, name, startDate und endDate sind erforderlich" });
|
||||
}
|
||||
if (startDate > endDate) {
|
||||
return res.status(400).json({ error: "startDate muss vor endDate liegen" });
|
||||
}
|
||||
|
||||
const db = await connect();
|
||||
const projectObjectId = toId(projectId);
|
||||
if (!projectObjectId || !(await db.collection("projects").findOne({ _id: projectObjectId }))) {
|
||||
return res.status(400).json({ error: "Unbekanntes Projekt" });
|
||||
}
|
||||
|
||||
const doc = {
|
||||
projectId,
|
||||
name,
|
||||
startDate,
|
||||
endDate,
|
||||
estimatedHours: Number(estimatedHours) || 0,
|
||||
assigneeId: assigneeId || null,
|
||||
status: status || "geplant",
|
||||
priority: priority || "mittel",
|
||||
dependsOn: dependsOn || null
|
||||
};
|
||||
const result = await db.collection("tasks").insertOne(doc);
|
||||
res.status(201).json(serialize({ _id: result.insertedId, ...doc }));
|
||||
});
|
||||
|
||||
router.put("/:id", async (req, res) => {
|
||||
const _id = toId(req.params.id);
|
||||
if (!_id) return res.status(404).json({ error: "Aufgabe nicht gefunden" });
|
||||
|
||||
const db = await connect();
|
||||
const existing = await db.collection("tasks").findOne({ _id });
|
||||
if (!existing) return res.status(404).json({ error: "Aufgabe nicht gefunden" });
|
||||
|
||||
const fields = [
|
||||
"projectId",
|
||||
"name",
|
||||
"startDate",
|
||||
"endDate",
|
||||
"estimatedHours",
|
||||
"assigneeId",
|
||||
"status",
|
||||
"priority",
|
||||
"dependsOn"
|
||||
];
|
||||
const update = {};
|
||||
fields.forEach((f) => {
|
||||
if (req.body[f] !== undefined) {
|
||||
update[f] = f === "estimatedHours" ? Number(req.body[f]) : req.body[f];
|
||||
}
|
||||
});
|
||||
|
||||
const merged = { ...existing, ...update };
|
||||
if (merged.startDate > merged.endDate) {
|
||||
return res.status(400).json({ error: "startDate muss vor endDate liegen" });
|
||||
}
|
||||
|
||||
const updated = await db
|
||||
.collection("tasks")
|
||||
.findOneAndUpdate({ _id }, { $set: update }, { returnDocument: "after" });
|
||||
|
||||
res.json(serialize(updated));
|
||||
});
|
||||
|
||||
router.delete("/:id", async (req, res) => {
|
||||
const _id = toId(req.params.id);
|
||||
if (!_id) return res.status(404).json({ error: "Aufgabe nicht gefunden" });
|
||||
|
||||
const db = await connect();
|
||||
const result = await db.collection("tasks").deleteOne({ _id });
|
||||
if (result.deletedCount === 0) {
|
||||
return res.status(404).json({ error: "Aufgabe nicht gefunden" });
|
||||
}
|
||||
// Abhaengigkeiten auf geloeschte Aufgabe aufraeumen
|
||||
await db
|
||||
.collection("tasks")
|
||||
.updateMany({ dependsOn: req.params.id }, { $set: { dependsOn: null } });
|
||||
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,78 @@
|
||||
// Alle Datumswerte werden als "YYYY-MM-DD" Strings gehandhabt und als UTC
|
||||
// Datum interpretiert, damit Zeitzonen keine Off-by-one Fehler verursachen.
|
||||
|
||||
function parseDate(str) {
|
||||
const [y, m, d] = str.split("-").map(Number);
|
||||
return new Date(Date.UTC(y, m - 1, d));
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function addDays(date, days) {
|
||||
const d = new Date(date);
|
||||
d.setUTCDate(d.getUTCDate() + days);
|
||||
return d;
|
||||
}
|
||||
|
||||
function isWeekday(date) {
|
||||
const day = date.getUTCDay();
|
||||
return day !== 0 && day !== 6;
|
||||
}
|
||||
|
||||
// Anzahl Werktage (Mo-Fr) zwischen start und end (inklusive beider Enden)
|
||||
function countWorkdays(startStr, endStr) {
|
||||
let start = parseDate(startStr);
|
||||
let end = parseDate(endStr);
|
||||
if (start > end) return 0;
|
||||
let count = 0;
|
||||
let cur = start;
|
||||
while (cur <= end) {
|
||||
if (isWeekday(cur)) count++;
|
||||
cur = addDays(cur, 1);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Ueberschneidung zweier Datumsbereiche in Werktagen
|
||||
function overlapWorkdays(aStart, aEnd, bStart, bEnd) {
|
||||
const start = parseDate(aStart) > parseDate(bStart) ? aStart : bStart;
|
||||
const end = parseDate(aEnd) < parseDate(bEnd) ? aEnd : bEnd;
|
||||
if (parseDate(start) > parseDate(end)) return 0;
|
||||
return countWorkdays(start, end);
|
||||
}
|
||||
|
||||
// Liste von Monaten (YYYY-MM) zwischen start und end (inklusive)
|
||||
function monthsInRange(startStr, endStr) {
|
||||
const start = parseDate(startStr);
|
||||
const end = parseDate(endStr);
|
||||
const months = [];
|
||||
let cur = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), 1));
|
||||
const last = new Date(Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), 1));
|
||||
while (cur <= last) {
|
||||
const y = cur.getUTCFullYear();
|
||||
const m = String(cur.getUTCMonth() + 1).padStart(2, "0");
|
||||
months.push(`${y}-${m}`);
|
||||
cur = new Date(Date.UTC(y, cur.getUTCMonth() + 1, 1));
|
||||
}
|
||||
return months;
|
||||
}
|
||||
|
||||
function monthBounds(monthStr) {
|
||||
const [y, m] = monthStr.split("-").map(Number);
|
||||
const start = new Date(Date.UTC(y, m - 1, 1));
|
||||
const end = new Date(Date.UTC(y, m, 0));
|
||||
return { start: formatDate(start), end: formatDate(end) };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseDate,
|
||||
formatDate,
|
||||
addDays,
|
||||
isWeekday,
|
||||
countWorkdays,
|
||||
overlapWorkdays,
|
||||
monthsInRange,
|
||||
monthBounds
|
||||
};
|
||||
Reference in New Issue
Block a user