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,
|
"when": 1749254400000,
|
||||||
"tag": "0011_time_entry_invoice_id",
|
"tag": "0011_time_entry_invoice_id",
|
||||||
"breakpoints": true
|
"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({
|
time_clock_in: defineTool({
|
||||||
description:
|
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: {
|
inputSchema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
description: { type: "string", maxLength: 500 },
|
description: { type: "string", maxLength: 500 },
|
||||||
clientId: { type: "string" },
|
clientId: { type: "string" },
|
||||||
rate: { type: "number", minimum: 0 },
|
rate: { type: "number", minimum: 0 },
|
||||||
|
startedAt: { type: "string", format: "date-time", description: "Optional backdated start time (ISO 8601). Defaults to now." },
|
||||||
},
|
},
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
@@ -423,8 +424,13 @@ const tools = {
|
|||||||
description: z.string().max(500).default(""),
|
description: z.string().max(500).default(""),
|
||||||
clientId: z.string().optional().or(z.literal("")),
|
clientId: z.string().optional().or(z.literal("")),
|
||||||
rate: z.number().min(0).optional(),
|
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({
|
time_clock_out: defineTool({
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ export const timeEntriesRouter = createTRPCRouter({
|
|||||||
description: z.string().max(500).default(""),
|
description: z.string().max(500).default(""),
|
||||||
clientId: z.string().optional().or(z.literal("")),
|
clientId: z.string().optional().or(z.literal("")),
|
||||||
rate: z.number().min(0).optional(),
|
rate: z.number().min(0).optional(),
|
||||||
|
startedAt: z.date().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
@@ -157,12 +158,17 @@ export const timeEntriesRouter = createTRPCRouter({
|
|||||||
if (!client) throw new TRPCError({ code: "FORBIDDEN", message: "Client not found" });
|
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
|
const [entry] = await ctx.db
|
||||||
.insert(timeEntries)
|
.insert(timeEntries)
|
||||||
.values({
|
.values({
|
||||||
description: input.description,
|
description: input.description,
|
||||||
clientId,
|
clientId,
|
||||||
startedAt: new Date(),
|
startedAt,
|
||||||
rate: input.rate ?? null,
|
rate: input.rate ?? null,
|
||||||
createdById: ctx.session.user.id,
|
createdById: ctx.session.user.id,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ export const verificationTokens = createTable(
|
|||||||
.primaryKey()
|
.primaryKey()
|
||||||
.$defaultFn(() => crypto.randomUUID()), // Matched DB: text
|
.$defaultFn(() => crypto.randomUUID()), // Matched DB: text
|
||||||
identifier: d.varchar({ length: 255 }).notNull(),
|
identifier: d.varchar({ length: 255 }).notNull(),
|
||||||
value: d.varchar({ length: 255 }).notNull(),
|
value: d.text().notNull(),
|
||||||
expiresAt: d.timestamp().notNull(),
|
expiresAt: d.timestamp().notNull(),
|
||||||
createdAt: d.timestamp().notNull().defaultNow(),
|
createdAt: d.timestamp().notNull().defaultNow(),
|
||||||
updatedAt: d
|
updatedAt: d
|
||||||
|
|||||||
Reference in New Issue
Block a user