Add org invites section to dashboard with accept/decline flow

Backend:
- Add me.invites endpoints (list, get, accept, decline) to API contract
- Create invites procedures for fetching user's pending invites
- Only show invites if email matches and is verified
- Refactor me routes into me/_routes.ts for consistency

Frontend:
- Add pending invitations section to /dashboard page
- Create /account/org-invites/[inviteId] page for accept/decline
- Show invite details (org, role, inviter, dates)
- Redirect to org dashboard after accepting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-10 17:11:22 +08:00
parent 9f4c6ac0b9
commit 39863bd947
11 changed files with 779 additions and 181 deletions

View File

@@ -45,6 +45,7 @@ import {
setupProfileInputSchema,
trustDeviceInputSchema,
updateProfileInputSchema,
userInviteOutputSchema,
userProfileSchema,
} from "./schemas/user.js";
@@ -147,6 +148,20 @@ export const contract = oc.router({
.output(successResponseSchema),
}),
// Org invites for the current user
invites: oc.router({
list: oc.output(z.array(userInviteOutputSchema)),
get: oc
.input(z.object({ inviteId: z.number() }))
.output(userInviteOutputSchema),
accept: oc
.input(z.object({ inviteId: z.number() }))
.output(successResponseSchema),
decline: oc
.input(z.object({ inviteId: z.number() }))
.output(successResponseSchema),
}),
// Sessions & devices
listSessions: oc.output(z.array(sessionOutputSchema)),
revokeSession: oc

View File

@@ -1,5 +1,6 @@
import * as z from "zod";
import { nonEmptyString, optionalString, phoneSchema } from "./common.js";
import { orgRoleSchema } from "./org.js";
/**
* User profile schema
@@ -132,3 +133,21 @@ export const authStatusOutputSchema = z.object({
sessionAuthStatusSchema,
]),
});
/**
* User invite output schema
* Returned by me.invites.list - includes org info for the user's pending invites
*/
export const userInviteOutputSchema = z.object({
id: z.number(),
org: z.object({
id: z.number(),
slug: z.string(),
displayName: z.string(),
logoUrl: z.string().nullable(),
}),
role: orgRoleSchema,
invitedBy: z.string(),
createdAt: z.date(),
expiresAt: z.date(),
});