diff --git a/backend/package.json b/backend/package.json index 0e13721..535e176 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,6 +10,8 @@ "build": "tsc -p tsconfig.json", "start": "node dist/app.js", "dunning:run": "ts-node -r dotenv/config src/jobs/dailyDunningJob.ts", + "cleaning-reminder:run": "ts-node -r dotenv/config src/jobs/cleaningReminderJob.ts", + "trash-reminder:run": "ts-node -r dotenv/config src/jobs/trashReminderJob.ts", "test:webhook": "ts-node -r dotenv/config scripts/send-test-webhook.ts", "prisma:generate": "prisma generate --schema=../prisma/schema.prisma", "prisma:migrate": "prisma migrate dev --schema=../prisma/schema.prisma", diff --git a/backend/src/jobs/cleaningReminderJob.ts b/backend/src/jobs/cleaningReminderJob.ts new file mode 100644 index 0000000..68f56bd --- /dev/null +++ b/backend/src/jobs/cleaningReminderJob.ts @@ -0,0 +1,62 @@ +import 'dotenv/config'; +import { PrismaClient, CleaningArea } from '@prisma/client'; +import { sendPushNotification } from '../services/notificationService'; + +const prisma = new PrismaClient(); + +const AREA_LABEL: Record = { + BATHROOM_1: 'Bad 1', + BATHROOM_2: 'Bad 2', + KITCHEN: 'Wohnküche', + HALLWAY_LAUNDRY: 'Waschraum/Flur', +}; + +/** + * Täglicher Cron-Job (analog dailyDunningJob.ts): erinnert Mieter per Push an + * eine offene Putzaufgabe, die morgen fällig ist. Verschickt (max. 1x je + * Aufgabe) und markiert per `reminderSentAt`. + */ +export async function runCleaningReminderJob(referenceDate: Date = new Date()) { + const tomorrowStart = new Date(referenceDate); + tomorrowStart.setDate(tomorrowStart.getDate() + 1); + tomorrowStart.setHours(0, 0, 0, 0); + const tomorrowEnd = new Date(tomorrowStart); + tomorrowEnd.setHours(23, 59, 59, 999); + + const dueTasks = await prisma.cleaningTask.findMany({ + where: { + status: 'PENDING', + dueDate: { gte: tomorrowStart, lte: tomorrowEnd }, + reminderSentAt: null, + }, + include: { assignedUser: true }, + }); + + for (const task of dueTasks) { + await prisma.cleaningTask.update({ + where: { id: task.id }, + data: { reminderSentAt: new Date() }, + }); + + await sendPushNotification({ + userId: task.assignedUserId, + type: 'CLEANING_REMINDER', + title: 'Putzplan-Erinnerung', + body: `Morgen ist "${AREA_LABEL[task.area]}" an der Reihe. Bitte nicht vergessen!`, + }); + } + + return { remindersSent: dueTasks.length }; +} + +if (require.main === module) { + runCleaningReminderJob() + .then((result) => { + console.log(`[cleaningReminderJob] ${result.remindersSent} Erinnerungen versendet.`); + return prisma.$disconnect(); + }) + .catch((err) => { + console.error('[cleaningReminderJob] Fehlgeschlagen:', err); + process.exit(1); + }); +} diff --git a/backend/src/jobs/trashReminderJob.ts b/backend/src/jobs/trashReminderJob.ts new file mode 100644 index 0000000..24dd13d --- /dev/null +++ b/backend/src/jobs/trashReminderJob.ts @@ -0,0 +1,70 @@ +import 'dotenv/config'; +import { PrismaClient, TrashType } from '@prisma/client'; +import { sendPushNotification } from '../services/notificationService'; + +const prisma = new PrismaClient(); + +const TRASH_LABEL: Record = { + RESTMUELL: 'Restmüll', + BIOMUELL: 'Biomüll', + GELBER_SACK: 'Gelber Sack', + PAPIER: 'Papiertonne', + GLAS: 'Glas', +}; + +/** + * Täglicher Cron-Job (analog dailyDunningJob.ts): "Vorabend-Push" für den + * Müll-Kalender — erinnert alle Mieter am Vorabend an eine Abholung am + * nächsten Tag. Verschickt (max. 1x je Termin) und markiert per + * `reminderSentAt`. + */ +export async function runTrashReminderJob(referenceDate: Date = new Date()) { + const tomorrowStart = new Date(referenceDate); + tomorrowStart.setDate(tomorrowStart.getDate() + 1); + tomorrowStart.setHours(0, 0, 0, 0); + const tomorrowEnd = new Date(tomorrowStart); + tomorrowEnd.setHours(23, 59, 59, 999); + + const dueEntries = await prisma.trashSchedule.findMany({ + where: { + date: { gte: tomorrowStart, lte: tomorrowEnd }, + reminderSentAt: null, + }, + }); + + if (!dueEntries.length) return { remindersSent: 0 }; + + const tenants = await prisma.user.findMany({ where: { role: 'TENANT' } }); + + for (const entry of dueEntries) { + await prisma.trashSchedule.update({ + where: { id: entry.id }, + data: { reminderSentAt: new Date() }, + }); + + await Promise.all( + tenants.map((tenant) => + sendPushNotification({ + userId: tenant.id, + type: 'TRASH_REMINDER', + title: 'Müll-Erinnerung', + body: `Morgen wird "${TRASH_LABEL[entry.type]}" abgeholt. Bitte heute Abend rausstellen.`, + }), + ), + ); + } + + return { remindersSent: dueEntries.length }; +} + +if (require.main === module) { + runTrashReminderJob() + .then((result) => { + console.log(`[trashReminderJob] ${result.remindersSent} Erinnerungen versendet.`); + return prisma.$disconnect(); + }) + .catch((err) => { + console.error('[trashReminderJob] Fehlgeschlagen:', err); + process.exit(1); + }); +} diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index a8a9c51..c42d06b 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -38,3 +38,18 @@ authRouter.get('/auth/me', requireAuth, async (req: AuthedRequest, res: Response if (!user) return res.status(404).json({ error: 'Nutzer nicht gefunden' }); res.status(200).json({ user: publicUser(user) }); }); + +// PUT /v1/auth/push-token { pushToken } -> registriert die OneSignal Player ID +// des aktuellen Geräts. Ohne registrierten Token bleibt sendPushNotification() +// ein reines In-App-Log (siehe services/notificationService.ts). +authRouter.put('/auth/push-token', requireAuth, async (req: AuthedRequest, res: Response) => { + const { pushToken } = req.body || {}; + if (pushToken !== null && typeof pushToken !== 'string') { + return res.status(400).json({ error: 'pushToken muss ein String oder null sein' }); + } + await prisma.user.update({ + where: { id: req.user!.id }, + data: { pushToken: pushToken || null }, + }); + res.status(200).json({ ok: true }); +});