diff --git a/.phase15-state.json b/.phase15-state.json index 988b051..1fc69fd 100644 --- a/.phase15-state.json +++ b/.phase15-state.json @@ -1,9 +1,10 @@ { "completed_features": [], - "current_feature": "password-reset", + "current_feature": "weekly-summary-email-stub", "started_at": "2026-05-23T06:57:51.069062", "attempted_features": [ "saved-views", - "webhook-trigger-events" + "webhook-trigger-events", + "password-reset" ] } \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index eb64bee..f5cf8cb 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -1818,3 +1818,22 @@ src/index.ts(27,25): error TS2769: No overload matches this call. Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy +- `07:04:26` **INFO** Committed feature password-reset +- `07:04:27` **INFO** Pushed: rc=0 + +## Phase-3 Feature: weekly-summary-email-stub (2026-05-23 07:04:27) + +- `07:04:27` **INFO** Description: Cron-stub für weekly-summary-email (Endpoint manuell triggerbar) +- `07:04:27` **INFO** Generating apps/api/src/routes/notifications.ts (Fastify-Plugin /api/notifications. POST /send-weekly-summary (admin-on…) +- `07:04:37` **INFO** wrote 1088 chars in 9.8s (attempt 1) +- `07:04:37` **INFO** Generating apps/api/src/services/email.ts (ERWEITERT — füge sendWeeklySummary(user) Methode. Fetched user's time-…) +- `07:04:55` **INFO** wrote 2190 chars in 18.4s (attempt 1) +- `07:04:55` **INFO** Running tsc --noEmit on api… +- `07:04:57` **WARN** tsc errors: +src/index.ts(27,25): error TS2769: No overload matches this call. + Overload 1 of 3, '(plugin: FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTypeProvider>, opts: { ...; }, done: (err?: Error | undefined) => void): void'. + Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts index beb058e..ec27583 100644 --- a/apps/api/src/routes/auth.ts +++ b/apps/api/src/routes/auth.ts @@ -4,7 +4,7 @@ import { db } from "../db" import { users, passwordResets } from "../db/schema" import { eq, and, gt } from "drizzle-orm" import { LoginRequestSchema, ForgotPasswordRequestSchema, ResetPasswordRequestSchema } from "@emberclone/shared" -import { emailService } from "../services/emailService" +import { emailService } from "../services/email" import { randomBytes } from "crypto" export default async function authRoutes(fastify: FastifyInstance) { diff --git a/apps/api/src/routes/notifications.ts b/apps/api/src/routes/notifications.ts new file mode 100644 index 0000000..624c9d7 --- /dev/null +++ b/apps/api/src/routes/notifications.ts @@ -0,0 +1,38 @@ +import { FastifyInstance } from "fastify" +import { db } from "../db" +import { users } from "../db/schema" +import { emailService } from "../services/email" + +export default async function notificationRoutes(fastify: FastifyInstance) { + fastify.addHook("preHandler", async (request, reply) => { + try { + await request.jwtVerify() + } catch (err) { + return reply.code(401).send({ message: "Unauthorized" }) + } + }) + + const isAdmin = (request: any) => { + return (request.user as { sub: string, role: string })?.role === "admin" + } + + fastify.post("/send-weekly-summary", async (request, reply) => { + if (!isAdmin(request)) { + return reply.code(403).send({ message: "Forbidden: Admin role required" }) + } + + const allUsers = await db.select().from(users) + let sentCount = 0 + + for (const user of allUsers) { + try { + await emailService.sendWeeklySummary(user) + sentCount++ + } catch (error) { + fastify.log.error(`Failed to send weekly summary to ${user.email}: ${error}`) + } + } + + return { sent: sentCount } + }) +} \ No newline at end of file diff --git a/apps/api/src/services/email.ts b/apps/api/src/services/email.ts index 5e977c4..9faf447 100644 --- a/apps/api/src/services/email.ts +++ b/apps/api/src/services/email.ts @@ -1,4 +1,7 @@ import type { UserSelect as User } from "@emberclone/shared"; +import { db } from "../db"; +import { timeEntries } from "../db/schema"; +import { and, eq, gte } from "drizzle-orm"; class EmailService { private async send(to: string, subject: string, body: string) { @@ -28,6 +31,27 @@ class EmailService { const body = `Hi ${user.name || "there"}, this is your daily reminder to check your EmberClone dashboard!`; await this.send(user.email, subject, body); } + + async sendWeeklySummary(user: User) { + const oneWeekAgo = new Date(); + oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); + + const entries = await db + .select() + .from(timeEntries) + .where( + and( + eq(timeEntries.userId, user.id), + gte(timeEntries.createdAt, oneWeekAgo) + ) + ); + + const totalHours = entries.reduce((acc, entry) => acc + (entry.duration || 0), 0) / 60; + const subject = "Your Weekly Summary"; + const body = `Hi ${user.name || "there"},\n\nHere is your summary for the last 7 days:\n- Total entries: ${entries.length}\n- Total time tracked: ${totalHours.toFixed(2)} hours\n\nKeep up the great work!`; + + await this.send(user.email, subject, body); + } } export const emailService = new EmailService(); \ No newline at end of file