Putzplan zu WG-Kalender erweitert: freie Ereignisse (Einkaufen/Gaeste-Party/Ruhe-Lernen/Sonstiges) fuer Mieter
This commit is contained in:
parent
6d017c993d
commit
e979ce7f1c
@ -1,18 +1,21 @@
|
|||||||
import { Router, Response } from 'express';
|
import { Router, Response } from 'express';
|
||||||
import { PrismaClient, CleaningStatus } from '@prisma/client';
|
import { PrismaClient, CleaningStatus, WgEventCategory } from '@prisma/client';
|
||||||
import { AuthedRequest, requireAuth, requireRole } from '../middleware/auth';
|
import { AuthedRequest, requireAuth, requireRole } from '../middleware/auth';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router für den Putzplan (rotierende Reinigungsaufgaben).
|
* Router für den WG-Kalender: Putzplan (rotierende Reinigungsaufgaben),
|
||||||
|
* Abwesenheiten und freie WG-Ereignisse (Einkaufen, Gäste/Party, Ruhe/Lernen,
|
||||||
|
* Sonstiges) — alles gemeinsam im selben Kalender sichtbar.
|
||||||
*
|
*
|
||||||
* Zugriffsregel: der Putzplan wird von den Mietern selbst gepflegt — jeder
|
* Zugriffsregel: der WG-Kalender wird von den Mietern selbst gepflegt — jeder
|
||||||
* eingeloggte Nutzer (Mieter wie Vermieter/Admin) kann Putzbereiche anlegen
|
* eingeloggte Nutzer (Mieter wie Vermieter/Admin) kann Putzbereiche anlegen,
|
||||||
* und Aufgaben eintragen/zuweisen. Ein Mieter sieht (und kann abhaken) nur
|
* Aufgaben eintragen/zuweisen und freie Kalendereinträge für sich selbst
|
||||||
* seine eigenen zugewiesenen Aufgaben; Vermieter/Admin sehen den kompletten
|
* anlegen. Ein Mieter sieht (und kann abhaken) nur seine eigenen zugewiesenen
|
||||||
* Plan. Die Bestätigung/Verifizierung erledigter Aufgaben bleibt Vermieter/
|
* Putzaufgaben; Vermieter/Admin sehen den kompletten Plan. Die Bestätigung/
|
||||||
* Admin vorbehalten (Qualitätskontrolle).
|
* Verifizierung erledigter Putzaufgaben bleibt Vermieter/Admin vorbehalten
|
||||||
|
* (Qualitätskontrolle).
|
||||||
*/
|
*/
|
||||||
export const cleaningTasksRouter = Router();
|
export const cleaningTasksRouter = Router();
|
||||||
|
|
||||||
@ -211,8 +214,19 @@ const ABSENCE_SELECT = {
|
|||||||
user: { select: { id: true, fullName: true } },
|
user: { select: { id: true, fullName: true } },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const EVENT_SELECT = {
|
||||||
|
id: true,
|
||||||
|
title: true,
|
||||||
|
category: true,
|
||||||
|
note: true,
|
||||||
|
startDate: true,
|
||||||
|
endDate: true,
|
||||||
|
createdBy: { select: { id: true, fullName: true } },
|
||||||
|
};
|
||||||
|
|
||||||
// GET /cleaning-calendar?from=YYYY-MM-DD&to=YYYY-MM-DD
|
// GET /cleaning-calendar?from=YYYY-MM-DD&to=YYYY-MM-DD
|
||||||
// Liefert alle Putzaufgaben + alle Abwesenheiten im Zeitraum (für alle Rollen sichtbar).
|
// Liefert alle Putzaufgaben + Abwesenheiten + freien WG-Ereignisse im
|
||||||
|
// Zeitraum (für alle Rollen sichtbar) — der gemeinsame WG-Kalender.
|
||||||
cleaningTasksRouter.get(
|
cleaningTasksRouter.get(
|
||||||
'/cleaning-calendar',
|
'/cleaning-calendar',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
@ -238,7 +252,15 @@ cleaningTasksRouter.get(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const [tasks, absences] = await Promise.all([
|
const eventWhere: Record<string, unknown> = {};
|
||||||
|
if (fromDate || toDate) {
|
||||||
|
eventWhere.AND = [
|
||||||
|
fromDate ? { endDate: { gte: fromDate } } : {},
|
||||||
|
toDate ? { startDate: { lte: toDate } } : {},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [tasks, absences, events] = await Promise.all([
|
||||||
prisma.cleaningTask.findMany({
|
prisma.cleaningTask.findMany({
|
||||||
where: taskWhere,
|
where: taskWhere,
|
||||||
orderBy: [{ dueDate: 'asc' }],
|
orderBy: [{ dueDate: 'asc' }],
|
||||||
@ -249,9 +271,71 @@ cleaningTasksRouter.get(
|
|||||||
orderBy: [{ startDate: 'asc' }],
|
orderBy: [{ startDate: 'asc' }],
|
||||||
select: ABSENCE_SELECT,
|
select: ABSENCE_SELECT,
|
||||||
}),
|
}),
|
||||||
|
prisma.wgEvent.findMany({
|
||||||
|
where: eventWhere,
|
||||||
|
orderBy: [{ startDate: 'asc' }],
|
||||||
|
select: EVENT_SELECT,
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
res.status(200).json({ tasks, absences });
|
res.status(200).json({ tasks, absences, events });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /wg-events — jeder eingeloggte Nutzer trägt einen freien
|
||||||
|
// Kalendereintrag für sich selbst ein (Einkaufen, Gäste/Party, Ruhe/Lernen,
|
||||||
|
// Sonstiges oder frei betitelt).
|
||||||
|
cleaningTasksRouter.post(
|
||||||
|
'/wg-events',
|
||||||
|
requireAuth,
|
||||||
|
async (req: AuthedRequest, res: Response) => {
|
||||||
|
const { title, category, note, startDate, endDate } = req.body || {};
|
||||||
|
|
||||||
|
if (!title || !String(title).trim()) {
|
||||||
|
return res.status(400).json({ error: 'Titel ist erforderlich' });
|
||||||
|
}
|
||||||
|
if (!startDate || Number.isNaN(Date.parse(startDate))) {
|
||||||
|
return res.status(400).json({ error: 'startDate (Datum) ist erforderlich' });
|
||||||
|
}
|
||||||
|
const parsedStart = new Date(startDate);
|
||||||
|
const parsedEnd = endDate && !Number.isNaN(Date.parse(endDate)) ? new Date(endDate) : parsedStart;
|
||||||
|
if (parsedEnd < parsedStart) {
|
||||||
|
return res.status(400).json({ error: 'endDate darf nicht vor startDate liegen' });
|
||||||
|
}
|
||||||
|
const validCategories = Object.values(WgEventCategory) as string[];
|
||||||
|
const resolvedCategory = category && validCategories.includes(category) ? category : WgEventCategory.SONSTIGES;
|
||||||
|
|
||||||
|
const event = await prisma.wgEvent.create({
|
||||||
|
data: {
|
||||||
|
createdById: req.user!.id,
|
||||||
|
title: String(title).trim().slice(0, 120),
|
||||||
|
category: resolvedCategory,
|
||||||
|
note: typeof note === 'string' && note.trim() ? note.trim().slice(0, 280) : null,
|
||||||
|
startDate: parsedStart,
|
||||||
|
endDate: parsedEnd,
|
||||||
|
},
|
||||||
|
select: EVENT_SELECT,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({ event });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// DELETE /wg-events/:id — eigener Eintrag löschbar, oder Vermieter/Admin jederzeit.
|
||||||
|
cleaningTasksRouter.delete(
|
||||||
|
'/wg-events/:id',
|
||||||
|
requireAuth,
|
||||||
|
async (req: AuthedRequest, res: Response) => {
|
||||||
|
const event = await prisma.wgEvent.findUnique({ where: { id: req.params.id } });
|
||||||
|
if (!event) return res.status(404).json({ error: 'Eintrag nicht gefunden' });
|
||||||
|
|
||||||
|
const isLandlord = req.user!.role === 'LANDLORD' || req.user!.role === 'ADMIN';
|
||||||
|
if (!isLandlord && event.createdById !== req.user!.id) {
|
||||||
|
return res.status(403).json({ error: 'Du kannst nur deine eigenen Einträge löschen' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.wgEvent.delete({ where: { id: req.params.id } });
|
||||||
|
res.status(204).send();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -183,6 +183,7 @@ model User {
|
|||||||
assignedTickets Ticket[] @relation("TicketAssignee")
|
assignedTickets Ticket[] @relation("TicketAssignee")
|
||||||
cleaningTasks CleaningTask[]
|
cleaningTasks CleaningTask[]
|
||||||
cleaningAbsences CleaningAbsence[]
|
cleaningAbsences CleaningAbsence[]
|
||||||
|
wgEventsCreated WgEvent[] @relation("WgEventsCreated")
|
||||||
expensesPaid Expense[]
|
expensesPaid Expense[]
|
||||||
expenseShares ExpenseShare[]
|
expenseShares ExpenseShare[]
|
||||||
handoverProtocols HandoverProtocol[]
|
handoverProtocols HandoverProtocol[]
|
||||||
@ -420,6 +421,34 @@ model CleaningAbsence {
|
|||||||
@@map("cleaning_absences")
|
@@map("cleaning_absences")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Freie Kalendereinträge im WG-Kalender (z. B. "gehe einkaufen", "Gäste/Party
|
||||||
|
// am Wochenende", "brauche Ruhe zum Lernen") — jeder Mieter kann für sich
|
||||||
|
// selbst Einträge anlegen, damit das Zusammenleben besser koordiniert werden
|
||||||
|
// kann. Ergänzt Putzaufgaben/Abwesenheiten/Ankündigungen im selben Kalender.
|
||||||
|
enum WgEventCategory {
|
||||||
|
EINKAUFEN
|
||||||
|
GAESTE_PARTY
|
||||||
|
RUHE_LERNEN
|
||||||
|
SONSTIGES
|
||||||
|
}
|
||||||
|
|
||||||
|
model WgEvent {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
createdById String @map("created_by_id")
|
||||||
|
createdBy User @relation("WgEventsCreated", fields: [createdById], references: [id])
|
||||||
|
|
||||||
|
title String
|
||||||
|
category WgEventCategory @default(SONSTIGES)
|
||||||
|
note String?
|
||||||
|
startDate DateTime @map("start_date")
|
||||||
|
endDate DateTime @map("end_date") // gleich startDate bei eintägigen Einträgen
|
||||||
|
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
|
@@index([startDate])
|
||||||
|
@@map("wg_events")
|
||||||
|
}
|
||||||
|
|
||||||
// Ankündigungen des Vermieters (z. B. "Schornsteinfeger kommt am...",
|
// Ankündigungen des Vermieters (z. B. "Schornsteinfeger kommt am...",
|
||||||
// "Handwerker kommt am..."), für alle Mieter im Dashboard sichtbar.
|
// "Handwerker kommt am..."), für alle Mieter im Dashboard sichtbar.
|
||||||
model Announcement {
|
model Announcement {
|
||||||
|
|||||||
@ -327,6 +327,10 @@
|
|||||||
.calendar-badge-task-missed { background: #FBE0DE; color: #C23B2E; }
|
.calendar-badge-task-missed { background: #FBE0DE; color: #C23B2E; }
|
||||||
.calendar-badge-absence { background: var(--yellow-bg, #FDF3DC); color: var(--yellow, #B4780A); }
|
.calendar-badge-absence { background: var(--yellow-bg, #FDF3DC); color: var(--yellow, #B4780A); }
|
||||||
.calendar-badge-announcement { background: #F4E3FB; color: #8A2EBE; }
|
.calendar-badge-announcement { background: #F4E3FB; color: #8A2EBE; }
|
||||||
|
.calendar-badge-event-EINKAUFEN { background: #DBF3EF; color: #157A6E; }
|
||||||
|
.calendar-badge-event-GAESTE_PARTY { background: #FCE4D6; color: #C2540C; }
|
||||||
|
.calendar-badge-event-RUHE_LERNEN { background: #E4E9F7; color: #33507D; }
|
||||||
|
.calendar-badge-event-SONSTIGES { background: #ECECEC; color: #555555; }
|
||||||
|
|
||||||
.inventory-thumb {
|
.inventory-thumb {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
@ -593,7 +597,7 @@
|
|||||||
<button class="nav-item nav-subitem" data-page="page-tenant-invites" onclick="switchPage('page-tenant-invites')">Mietereinladungen</button>
|
<button class="nav-item nav-subitem" data-page="page-tenant-invites" onclick="switchPage('page-tenant-invites')">Mietereinladungen</button>
|
||||||
<button class="nav-item nav-subitem" data-page="page-tenant-data" onclick="switchPage('page-tenant-data')">Mieterdaten</button>
|
<button class="nav-item nav-subitem" data-page="page-tenant-data" onclick="switchPage('page-tenant-data')">Mieterdaten</button>
|
||||||
<button class="nav-item" data-page="page-tickets" onclick="switchPage('page-tickets')"><span class="nav-icon">🛠️</span>Tickets & Handwerker</button>
|
<button class="nav-item" data-page="page-tickets" onclick="switchPage('page-tickets')"><span class="nav-icon">🛠️</span>Tickets & Handwerker</button>
|
||||||
<button class="nav-item" data-page="page-cleaning" onclick="switchPage('page-cleaning')"><span class="nav-icon">🧹</span>Putzplan</button>
|
<button class="nav-item" data-page="page-cleaning" onclick="switchPage('page-cleaning')"><span class="nav-icon">🗓️</span>WG-Kalender</button>
|
||||||
<button class="nav-item" data-page="page-finance" onclick="switchPage('page-finance')"><span class="nav-icon">💶</span>WG-Kasse</button>
|
<button class="nav-item" data-page="page-finance" onclick="switchPage('page-finance')"><span class="nav-icon">💶</span>WG-Kasse</button>
|
||||||
<button class="nav-item" data-page="page-inventory" onclick="switchPage('page-inventory')"><span class="nav-icon">📦</span>Inventar & Übergabe</button>
|
<button class="nav-item" data-page="page-inventory" onclick="switchPage('page-inventory')"><span class="nav-icon">📦</span>Inventar & Übergabe</button>
|
||||||
<button class="nav-item" data-page="page-access" onclick="switchPage('page-access')"><span class="nav-icon">🔑</span>Zugang</button>
|
<button class="nav-item" data-page="page-access" onclick="switchPage('page-access')"><span class="nav-icon">🔑</span>Zugang</button>
|
||||||
@ -866,9 +870,9 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="cleaningCalendarSection">
|
<section id="cleaningCalendarSection">
|
||||||
<h2>Putzplan-Kalender & Abwesenheiten</h2>
|
<h2>WG-Kalender</h2>
|
||||||
<p style="color:var(--text-muted); font-size:13px; margin-top:-8px;">
|
<p style="color:var(--text-muted); font-size:13px; margin-top:-8px;">
|
||||||
Zeigt für alle sichtbar, wer wann für welchen Bereich zuständig ist und ob die Aufgabe bereits erledigt wurde, eingetragene Abwesenheiten (Urlaub, "kann nicht") sowie Ankündigungen mit Termin (z. B. Handwerker) inkl. Rückmeldung, ob die Person da war.
|
Zeigt für alle sichtbar, wer wann für welchen Bereich zuständig ist und ob die Putzaufgabe bereits erledigt wurde, eingetragene Abwesenheiten (Urlaub, "kann nicht"), Ankündigungen mit Termin (z. B. Handwerker) inkl. Rückmeldung, ob die Person da war — sowie freie WG-Ereignisse wie "gehe einkaufen", "Gäste/Party" oder "brauche Ruhe zum Lernen".
|
||||||
</p>
|
</p>
|
||||||
<div style="display:flex; flex-wrap:wrap; gap:10px; margin-bottom:12px; font-size:13px;">
|
<div style="display:flex; flex-wrap:wrap; gap:10px; margin-bottom:12px; font-size:13px;">
|
||||||
<span class="calendar-badge calendar-badge-task-pending" style="padding:3px 8px;">○ Offen</span>
|
<span class="calendar-badge calendar-badge-task-pending" style="padding:3px 8px;">○ Offen</span>
|
||||||
@ -877,6 +881,10 @@
|
|||||||
<span class="calendar-badge calendar-badge-task-missed" style="padding:3px 8px;">✕ Verpasst</span>
|
<span class="calendar-badge calendar-badge-task-missed" style="padding:3px 8px;">✕ Verpasst</span>
|
||||||
<span class="calendar-badge calendar-badge-absence" style="padding:3px 8px;">Abwesend</span>
|
<span class="calendar-badge calendar-badge-absence" style="padding:3px 8px;">Abwesend</span>
|
||||||
<span class="calendar-badge calendar-badge-announcement" style="padding:3px 8px;">📢 Ankündigung</span>
|
<span class="calendar-badge calendar-badge-announcement" style="padding:3px 8px;">📢 Ankündigung</span>
|
||||||
|
<span class="calendar-badge calendar-badge-event-EINKAUFEN" style="padding:3px 8px;">🛒 Einkaufen</span>
|
||||||
|
<span class="calendar-badge calendar-badge-event-GAESTE_PARTY" style="padding:3px 8px;">🎉 Gäste/Party</span>
|
||||||
|
<span class="calendar-badge calendar-badge-event-RUHE_LERNEN" style="padding:3px 8px;">🤫 Ruhe/Lernen</span>
|
||||||
|
<span class="calendar-badge calendar-badge-event-SONSTIGES" style="padding:3px 8px;">📌 Sonstiges</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="invite-card" style="margin-bottom:16px;">
|
<div class="invite-card" style="margin-bottom:16px;">
|
||||||
<div id="absenceFormError" style="display:none" class="form-error"></div>
|
<div id="absenceFormError" style="display:none" class="form-error"></div>
|
||||||
@ -901,6 +909,42 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3 style="font-size:14px;">WG-Ereignis eintragen</h3>
|
||||||
|
<p style="color:var(--text-muted); font-size:13px; margin-top:-8px;">
|
||||||
|
Für alles, was die Mitbewohner wissen sollten — z. B. "gehe einkaufen", "hab am Samstag Gäste/Party" oder "muss lernen, bitte Ruhe".
|
||||||
|
</p>
|
||||||
|
<div class="invite-card" style="margin-bottom:16px;">
|
||||||
|
<div id="eventFormError" style="display:none" class="form-error"></div>
|
||||||
|
<form id="eventForm" class="invite-form-row" style="flex-wrap:wrap;">
|
||||||
|
<div class="field" style="flex:2 1 200px;">
|
||||||
|
<label for="eventTitle">Titel</label>
|
||||||
|
<input type="text" id="eventTitle" required maxlength="120" placeholder="z. B. Party am Samstag" />
|
||||||
|
</div>
|
||||||
|
<div class="field" style="flex:1 1 160px;">
|
||||||
|
<label for="eventCategory">Kategorie</label>
|
||||||
|
<select id="eventCategory">
|
||||||
|
<option value="EINKAUFEN">🛒 Einkaufen</option>
|
||||||
|
<option value="GAESTE_PARTY">🎉 Gäste/Party</option>
|
||||||
|
<option value="RUHE_LERNEN">🤫 Ruhe/Lernen</option>
|
||||||
|
<option value="SONSTIGES" selected>📌 Sonstiges</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field" style="flex:1 1 150px;">
|
||||||
|
<label for="eventStart">Von</label>
|
||||||
|
<input type="date" id="eventStart" required />
|
||||||
|
</div>
|
||||||
|
<div class="field" style="flex:1 1 150px;">
|
||||||
|
<label for="eventEnd">Bis (optional)</label>
|
||||||
|
<input type="date" id="eventEnd" />
|
||||||
|
</div>
|
||||||
|
<div class="field" style="flex:2 1 220px;">
|
||||||
|
<label for="eventNote">Notiz (optional)</label>
|
||||||
|
<input type="text" id="eventNote" maxlength="280" placeholder="z. B. ab 20 Uhr, gerne mitfeiern" />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary" style="width:auto" id="eventSubmitBtn">Eintragen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:10px;">
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:10px;">
|
||||||
<button type="button" class="btn-secondary" id="calPrevBtn">‹ Vorheriger Monat</button>
|
<button type="button" class="btn-secondary" id="calPrevBtn">‹ Vorheriger Monat</button>
|
||||||
<strong id="calMonthLabel"></strong>
|
<strong id="calMonthLabel"></strong>
|
||||||
@ -909,6 +953,9 @@
|
|||||||
<div class="calendar-grid" id="calendarWeekdays"></div>
|
<div class="calendar-grid" id="calendarWeekdays"></div>
|
||||||
<div class="calendar-grid" id="calendarGrid" style="margin-top:6px;"></div>
|
<div class="calendar-grid" id="calendarGrid" style="margin-top:6px;"></div>
|
||||||
|
|
||||||
|
<h3 style="margin-top:20px; font-size:14px;">WG-Ereignisse im angezeigten Monat</h3>
|
||||||
|
<div class="ticket-list" id="eventList"></div>
|
||||||
|
|
||||||
<h3 style="margin-top:20px; font-size:14px;">Abwesenheiten im angezeigten Monat</h3>
|
<h3 style="margin-top:20px; font-size:14px;">Abwesenheiten im angezeigten Monat</h3>
|
||||||
<div class="ticket-list" id="absenceList"></div>
|
<div class="ticket-list" id="absenceList"></div>
|
||||||
</section>
|
</section>
|
||||||
@ -1493,6 +1540,7 @@
|
|||||||
|
|
||||||
initTicketForm();
|
initTicketForm();
|
||||||
initAbsenceForm();
|
initAbsenceForm();
|
||||||
|
initEventForm();
|
||||||
initExpenseForm();
|
initExpenseForm();
|
||||||
if (isLandlord) initInventoryForm();
|
if (isLandlord) initInventoryForm();
|
||||||
if (isLandlord) initHandoverForm();
|
if (isLandlord) initHandoverForm();
|
||||||
@ -2189,7 +2237,8 @@
|
|||||||
|
|
||||||
const WEEKDAY_LABELS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
const WEEKDAY_LABELS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||||
let calendarMonthDate = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
|
let calendarMonthDate = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
|
||||||
let calendarData = { tasks: [], absences: [] };
|
let calendarData = { tasks: [], absences: [], events: [] };
|
||||||
|
const EVENT_CATEGORY_ICON = { EINKAUFEN: '🛒', GAESTE_PARTY: '🎉', RUHE_LERNEN: '🤫', SONSTIGES: '📌' };
|
||||||
let calendarAnnouncements = [];
|
let calendarAnnouncements = [];
|
||||||
|
|
||||||
function initAbsenceForm() {
|
function initAbsenceForm() {
|
||||||
@ -2243,6 +2292,38 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initEventForm() {
|
||||||
|
const form = document.getElementById('eventForm');
|
||||||
|
const errorBox = document.getElementById('eventFormError');
|
||||||
|
form.onsubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
errorBox.style.display = 'none';
|
||||||
|
const submitBtn = document.getElementById('eventSubmitBtn');
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.textContent = 'Trägt ein…';
|
||||||
|
try {
|
||||||
|
const title = document.getElementById('eventTitle').value.trim();
|
||||||
|
const category = document.getElementById('eventCategory').value;
|
||||||
|
const startDate = document.getElementById('eventStart').value;
|
||||||
|
const endDate = document.getElementById('eventEnd').value || undefined;
|
||||||
|
const note = document.getElementById('eventNote').value;
|
||||||
|
await apiFetch('/wg-events', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ title, category, startDate, endDate, note }),
|
||||||
|
});
|
||||||
|
form.reset();
|
||||||
|
document.getElementById('eventCategory').value = 'SONSTIGES';
|
||||||
|
loadCalendar();
|
||||||
|
} catch (err) {
|
||||||
|
errorBox.textContent = err.message;
|
||||||
|
errorBox.style.display = 'block';
|
||||||
|
} finally {
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.textContent = 'Eintragen';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function loadCalendar() {
|
async function loadCalendar() {
|
||||||
if (!currentUser) return;
|
if (!currentUser) return;
|
||||||
const monthStart = new Date(calendarMonthDate.getFullYear(), calendarMonthDate.getMonth(), 1);
|
const monthStart = new Date(calendarMonthDate.getFullYear(), calendarMonthDate.getMonth(), 1);
|
||||||
@ -2313,10 +2394,44 @@
|
|||||||
const attendanceSuffix = a.attendanceStatus ? (a.attendanceStatus === 'PRESENT' ? ' ✓' : ' ✕') : '';
|
const attendanceSuffix = a.attendanceStatus ? (a.attendanceStatus === 'PRESENT' ? ' ✓' : ' ✕') : '';
|
||||||
cell.appendChild(el('div', 'calendar-badge calendar-badge-announcement', `📢 ${a.title}${attendanceSuffix}`));
|
cell.appendChild(el('div', 'calendar-badge calendar-badge-announcement', `📢 ${a.title}${attendanceSuffix}`));
|
||||||
});
|
});
|
||||||
|
(calendarData.events || []).filter(ev => d >= stripTime(new Date(ev.startDate)) && d <= stripTime(new Date(ev.endDate))).forEach(ev => {
|
||||||
|
const icon = EVENT_CATEGORY_ICON[ev.category] || '📌';
|
||||||
|
const badge = el('div', `calendar-badge calendar-badge-event-${ev.category}`,
|
||||||
|
`${icon} ${ev.title} · ${ev.createdBy.fullName.split(' ')[0]}`);
|
||||||
|
cell.appendChild(badge);
|
||||||
|
});
|
||||||
|
|
||||||
grid.appendChild(cell);
|
grid.appendChild(cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const eventList = document.getElementById('eventList');
|
||||||
|
eventList.innerHTML = '';
|
||||||
|
if (!(calendarData.events || []).length) {
|
||||||
|
eventList.appendChild(el('div', 'empty-state', 'Keine WG-Ereignisse in diesem Monat eingetragen.'));
|
||||||
|
} else {
|
||||||
|
const isLandlordForEvents = currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN';
|
||||||
|
calendarData.events.forEach(ev => {
|
||||||
|
const row = el('div', 'ticket-row');
|
||||||
|
const left = el('div');
|
||||||
|
const icon = EVENT_CATEGORY_ICON[ev.category] || '📌';
|
||||||
|
left.appendChild(el('div', 'title', `${icon} ${ev.title}`));
|
||||||
|
const rangeText = sameDay(new Date(ev.startDate), new Date(ev.endDate))
|
||||||
|
? new Date(ev.startDate).toLocaleDateString('de-DE')
|
||||||
|
: `${new Date(ev.startDate).toLocaleDateString('de-DE')} – ${new Date(ev.endDate).toLocaleDateString('de-DE')}`;
|
||||||
|
left.appendChild(el('div', 'meta-text',
|
||||||
|
`${rangeText} · ${ev.createdBy.fullName}${ev.note ? ' · ' + ev.note : ''}`));
|
||||||
|
row.appendChild(left);
|
||||||
|
if (isLandlordForEvents || ev.createdBy.id === currentUser.id) {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'btn-secondary';
|
||||||
|
btn.textContent = 'Löschen';
|
||||||
|
btn.onclick = () => deleteWgEvent(ev.id);
|
||||||
|
row.appendChild(btn);
|
||||||
|
}
|
||||||
|
eventList.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const absenceList = document.getElementById('absenceList');
|
const absenceList = document.getElementById('absenceList');
|
||||||
absenceList.innerHTML = '';
|
absenceList.innerHTML = '';
|
||||||
if (!calendarData.absences.length) {
|
if (!calendarData.absences.length) {
|
||||||
@ -2360,6 +2475,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteWgEvent(id) {
|
||||||
|
try {
|
||||||
|
await apiFetch(`/wg-events/${id}`, { method: 'DELETE' });
|
||||||
|
loadCalendar();
|
||||||
|
} catch (err) {
|
||||||
|
alert(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// WG-KASSE (AUSGABEN-TEILER)
|
// WG-KASSE (AUSGABEN-TEILER)
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user