diff --git a/drizzle/0012_verification_token_value_text.sql b/drizzle/0012_verification_token_value_text.sql new file mode 100644 index 0000000..2cf46ec --- /dev/null +++ b/drizzle/0012_verification_token_value_text.sql @@ -0,0 +1 @@ +ALTER TABLE "beenvoice_verification_token" ALTER COLUMN "value" TYPE text; diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 5b5e08a..7af12ea 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -85,6 +85,13 @@ "when": 1749254400000, "tag": "0011_time_entry_invoice_id", "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1749340800000, + "tag": "0012_verification_token_value_text", + "breakpoints": true } ] } diff --git a/src/app/api/mcp/route.ts b/src/app/api/mcp/route.ts index 37c94b0..a12bae4 100644 --- a/src/app/api/mcp/route.ts +++ b/src/app/api/mcp/route.ts @@ -409,13 +409,14 @@ const tools = { }), time_clock_in: defineTool({ description: - "Start a time clock entry for the authenticated user. Fails if a timer is already running.", + "Start a time clock entry for the authenticated user. Fails if a timer is already running. Use startedAt to backdate the start time (e.g. if you forgot to clock in earlier). Cannot be in the future.", inputSchema: { type: "object", properties: { description: { type: "string", maxLength: 500 }, clientId: { type: "string" }, rate: { type: "number", minimum: 0 }, + startedAt: { type: "string", format: "date-time", description: "Optional backdated start time (ISO 8601). Defaults to now." }, }, additionalProperties: false, }, @@ -423,8 +424,13 @@ const tools = { description: z.string().max(500).default(""), clientId: z.string().optional().or(z.literal("")), rate: z.number().min(0).optional(), + startedAt: z.string().optional(), }), - handler: async (input, caller) => caller.timeEntries.clockIn(input), + handler: async (input, caller) => + caller.timeEntries.clockIn({ + ...input, + startedAt: input.startedAt ? parseDate(input.startedAt, "startedAt") : undefined, + }), }), time_clock_out: defineTool({ description: diff --git a/src/server/api/routers/time-entries.ts b/src/server/api/routers/time-entries.ts index 91239ff..4af57a3 100644 --- a/src/server/api/routers/time-entries.ts +++ b/src/server/api/routers/time-entries.ts @@ -133,6 +133,7 @@ export const timeEntriesRouter = createTRPCRouter({ description: z.string().max(500).default(""), clientId: z.string().optional().or(z.literal("")), rate: z.number().min(0).optional(), + startedAt: z.date().optional(), }), ) .mutation(async ({ ctx, input }) => { @@ -157,12 +158,17 @@ export const timeEntriesRouter = createTRPCRouter({ if (!client) throw new TRPCError({ code: "FORBIDDEN", message: "Client not found" }); } + const startedAt = input.startedAt ?? new Date(); + if (startedAt > new Date()) { + throw new TRPCError({ code: "BAD_REQUEST", message: "startedAt cannot be in the future" }); + } + const [entry] = await ctx.db .insert(timeEntries) .values({ description: input.description, clientId, - startedAt: new Date(), + startedAt, rate: input.rate ?? null, createdById: ctx.session.user.id, }) diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 8ee1bd2..47df734 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -202,7 +202,7 @@ export const verificationTokens = createTable( .primaryKey() .$defaultFn(() => crypto.randomUUID()), // Matched DB: text identifier: d.varchar({ length: 255 }).notNull(), - value: d.varchar({ length: 255 }).notNull(), + value: d.text().notNull(), expiresAt: d.timestamp().notNull(), createdAt: d.timestamp().notNull().defaultNow(), updatedAt: d