feat: time clock links to latest client invoice + fix SSO verification token overflow
- Clock out and manual entry creation now auto-add a line item to the client's latest draft/sent invoice and return invoice info - Time clock page shows invoice badge on each entry with a link - Toast after clock-out/create includes "View Invoice" action when linked - MCP time_clock_in now accepts optional startedAt for backdating - MCP time_clock_out description updated to document invoice linking - Migration 0012: widen beenvoice_verification_token.value to text to fix varchar(255) overflow during Authentik PKCE OAuth flow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
ALTER TABLE "beenvoice_verification_token" ALTER COLUMN "value" TYPE text;
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user