diff --git a/.phase18-state.json b/.phase18-state.json index 0ab1aed..56fc825 100644 --- a/.phase18-state.json +++ b/.phase18-state.json @@ -1,9 +1,10 @@ { "completed_features": [], - "current_feature": "idle-detection", + "current_feature": "time-entry-comments", "started_at": "2026-05-23T07:29:44.977564", "attempted_features": [ "api-key-management", - "audit-log-filters" + "audit-log-filters", + "idle-detection" ] } \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index 3091178..4f8d84f 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -2190,3 +2190,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:34:24` **INFO** Committed feature idle-detection +- `07:34:24` **INFO** Pushed: rc=0 + +## Phase-3 Feature: time-entry-comments (2026-05-23 07:34:24) + +- `07:34:24` **INFO** Description: Kommentare/Notes pro TimeEntry als Thread +- `07:34:24` **INFO** Generating apps/api/src/db/schema.ts (WICHTIG: behalte ALLE bestehenden Tabellen (vor allem passwordResetTok…) +- `07:35:05` **INFO** wrote 4785 chars in 40.9s (attempt 1) +- `07:35:05` **INFO** Generating apps/api/src/routes/time-entry-comments.ts (Fastify-Plugin /api/time-entry-comments. Auth required. GET /entries/:…) +- `07:35:21` **INFO** wrote 1821 chars in 15.9s (attempt 1) +- `07:35:21` **INFO** Running tsc --noEmit on api… +- `07:35:23` **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/db/schema.ts b/apps/api/src/db/schema.ts index a890a9a..3c7aae0 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -59,6 +59,14 @@ export const timeEntries = pgTable("time_entries", { createdAt: timestamp("created_at").notNull().defaultNow() }) +export const timeEntryComments = pgTable("time_entry_comments", { + id: uuid("id").primaryKey().defaultRandom(), + entryId: uuid("entry_id").notNull().references(() => timeEntries.id, { onDelete: "cascade" }), + userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), + body: text("body").notNull(), + createdAt: timestamp("created_at").notNull().defaultNow() +}) + export const timeEntryTemplates = pgTable("time_entry_templates", { id: uuid("id").primaryKey().defaultRandom(), userId: uuid("user_id").references(() => users.id, { onDelete: "cascade" }), @@ -98,43 +106,12 @@ export const documents = pgTable("documents", { createdAt: timestamp("created_at").notNull().defaultNow() }) -export const timeEntryAttachments = pgTable("time_entry_attachments", { - id: uuid("id").primaryKey().defaultRandom(), - entryId: uuid("entry_id").notNull().references(() => timeEntries.id, { onDelete: "cascade" }), - documentId: uuid("document_id").notNull().references(() => documents.id, { onDelete: "cascade" }), -}) - -export const webhooks = pgTable("webhooks", { - id: uuid("id").primaryKey().defaultRandom(), - url: text("url").notNull(), - secret: text("secret").notNull(), - events: text("events").array().notNull(), - active: boolean("active").notNull().default(true), - createdAt: timestamp("created_at").notNull().defaultNow() -}) - export const apiKeys = pgTable("api_keys", { id: uuid("id").primaryKey().defaultRandom(), userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), - name: text("name").notNull(), keyHash: text("key_hash").notNull().unique(), - createdAt: timestamp("created_at").notNull().defaultNow(), - lastUsedAt: timestamp("last_used_at"), - revokedAt: timestamp("revoked_at"), -}) -export const timeEntryComments = pgTable("time_entry_comments", { - id: uuid("id").primaryKey().defaultRandom(), - entryId: uuid("entry_id").notNull().references(() => timeEntries.id, { onDelete: "cascade" }), - userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), - body: text("body").notNull(), - createdAt: timestamp("created_at").notNull().defaultNow(), -}) - -export const savedViews = pgTable("saved_views", { - id: uuid("id").primaryKey().defaultRandom(), - userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }), name: text("name").notNull(), - entityType: text("entity_type").notNull(), - filters: text("filters").notNull(), createdAt: timestamp("created_at").notNull().defaultNow(), -}) + expiresAt: timestamp("expires_at"), + lastUsedAt: timestamp("last_used_at") +}) \ No newline at end of file diff --git a/apps/api/src/routes/time-entry-comments.ts b/apps/api/src/routes/time-entry-comments.ts new file mode 100644 index 0000000..b7892b1 --- /dev/null +++ b/apps/api/src/routes/time-entry-comments.ts @@ -0,0 +1,65 @@ +import { FastifyInstance } from "fastify"; +import { eq, and } from "drizzle-orm"; +import { db } from "../db"; +import { timeEntryComments } from "../db/schema"; +import { authenticate } from "../middleware/auth"; + +export default async function timeEntryCommentsRoutes(fastify: FastifyInstance) { + fastify.addHook("preHandler", authenticate); + + fastify.get("/entries/:entryId/comments", async (request, reply) => { + const { entryId } = request.params as { entryId: string }; + + const comments = await db + .select() + .from(timeEntryComments) + .where(eq(timeEntryComments.entryId, entryId)); + + return comments; + }); + + fastify.post("/entries/:entryId/comments", async (request, reply) => { + const { entryId } = request.params as { entryId: string }; + const { body } = request.body as { body: string }; + const user = request.user; + + if (!body) { + return reply.status(400).send({ error: "Body is required" }); + } + + const [comment] = await db + .insert(timeEntryComments) + .values({ + entryId, + userId: user.id, + body, + }) + .returning(); + + return reply.status(201).send(comment); + }); + + fastify.delete("/:id", async (request, reply) => { + const { id } = request.params as { id: string }; + const user = request.user; + + const [comment] = await db + .select() + .from(timeEntryComments) + .where(eq(timeEntryComments.id, id)); + + if (!comment) { + return reply.status(404).send({ error: "Comment not found" }); + } + + if (comment.userId !== user.id && user.role !== "admin") { + return reply.status(403).send({ error: "Not authorized to delete this comment" }); + } + + await db + .delete(timeEntryComments) + .where(eq(timeEntryComments.id, id)); + + return reply.status(204).send(); + }); +} \ No newline at end of file