169 lines
4.7 KiB
TypeScript
169 lines
4.7 KiB
TypeScript
import { z } from 'zod'
|
|
import { router, memberProcedure, adminProcedure } from '../trpc'
|
|
import { auth } from '@/lib/auth'
|
|
import { sendInviteEmail } from '@/lib/email'
|
|
|
|
const MemberInput = z.object({
|
|
name: z.string().min(2),
|
|
betrieb: z.string().min(2),
|
|
sparte: z.string().min(2),
|
|
ort: z.string().min(2),
|
|
telefon: z.string().optional(),
|
|
email: z.string().email(),
|
|
status: z.enum(['aktiv', 'ruhend', 'ausgetreten']).default('aktiv'),
|
|
istAusbildungsbetrieb: z.boolean().default(false),
|
|
seit: z.number().int().min(1900).max(2100).optional(),
|
|
})
|
|
|
|
export const membersRouter = router({
|
|
/**
|
|
* List all members in the user's org
|
|
*/
|
|
list: memberProcedure
|
|
.input(
|
|
z.object({
|
|
search: z.string().optional(),
|
|
status: z.enum(['aktiv', 'ruhend', 'ausgetreten']).optional(),
|
|
ausbildungsbetrieb: z.boolean().optional(),
|
|
})
|
|
)
|
|
.query(async ({ ctx, input }) => {
|
|
const members = await ctx.prisma.member.findMany({
|
|
where: {
|
|
orgId: ctx.orgId,
|
|
...(input.status && { status: input.status }),
|
|
...(input.ausbildungsbetrieb !== undefined && {
|
|
istAusbildungsbetrieb: input.ausbildungsbetrieb,
|
|
}),
|
|
...(input.search && {
|
|
OR: [
|
|
{ name: { contains: input.search } },
|
|
{ betrieb: { contains: input.search } },
|
|
{ ort: { contains: input.search } },
|
|
{ sparte: { contains: input.search } },
|
|
],
|
|
}),
|
|
},
|
|
orderBy: { name: 'asc' },
|
|
})
|
|
return members
|
|
}),
|
|
|
|
/**
|
|
* Get a single member by ID
|
|
*/
|
|
byId: memberProcedure
|
|
.input(z.object({ id: z.string() }))
|
|
.query(async ({ ctx, input }) => {
|
|
const member = await ctx.prisma.member.findFirst({
|
|
where: { id: input.id, orgId: ctx.orgId },
|
|
})
|
|
if (!member) throw new Error('Member not found')
|
|
return member
|
|
}),
|
|
|
|
/**
|
|
* Create a new member (admin only)
|
|
*/
|
|
create: adminProcedure.input(MemberInput).mutation(async ({ ctx, input }) => {
|
|
const member = await ctx.prisma.member.create({
|
|
data: {
|
|
...input,
|
|
orgId: ctx.orgId,
|
|
},
|
|
})
|
|
return member
|
|
}),
|
|
|
|
/**
|
|
* Create member + send invite email (admin only)
|
|
*/
|
|
invite: adminProcedure
|
|
.input(MemberInput)
|
|
.mutation(async ({ ctx, input }) => {
|
|
// 1. Create member record
|
|
const member = await ctx.prisma.member.create({
|
|
data: { ...input, orgId: ctx.orgId },
|
|
})
|
|
|
|
// 2. Create/get User via better-auth admin
|
|
try {
|
|
await auth.api.createUser({
|
|
body: {
|
|
name: input.name,
|
|
email: input.email,
|
|
role: 'user',
|
|
password: undefined,
|
|
},
|
|
})
|
|
} catch {
|
|
// User may already exist — that's ok
|
|
}
|
|
|
|
// 3. Send magic link
|
|
const org = await ctx.prisma.organization.findUniqueOrThrow({
|
|
where: { id: ctx.orgId },
|
|
})
|
|
await sendInviteEmail({
|
|
to: input.email,
|
|
memberName: input.name,
|
|
orgName: org.name,
|
|
apiUrl: process.env.BETTER_AUTH_URL!,
|
|
})
|
|
|
|
return member
|
|
}),
|
|
|
|
/**
|
|
* Update member (admin only)
|
|
*/
|
|
update: adminProcedure
|
|
.input(z.object({ id: z.string(), data: MemberInput.partial() }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
const member = await ctx.prisma.member.updateMany({
|
|
where: { id: input.id, orgId: ctx.orgId },
|
|
data: input.data,
|
|
})
|
|
// Keep user.name in sync when member name changes
|
|
if (input.data.name) {
|
|
const m = await ctx.prisma.member.findFirst({ where: { id: input.id }, select: { userId: true } })
|
|
if (m?.userId) {
|
|
await ctx.prisma.user.update({ where: { id: m.userId }, data: { name: input.data.name } })
|
|
}
|
|
}
|
|
return member
|
|
}),
|
|
|
|
/**
|
|
* Send/resend invite to existing member (admin only)
|
|
*/
|
|
resendInvite: adminProcedure
|
|
.input(z.object({ memberId: z.string() }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
const member = await ctx.prisma.member.findFirstOrThrow({
|
|
where: { id: input.memberId, orgId: ctx.orgId },
|
|
})
|
|
const org = await ctx.prisma.organization.findUniqueOrThrow({
|
|
where: { id: ctx.orgId },
|
|
})
|
|
await sendInviteEmail({
|
|
to: member.email,
|
|
memberName: member.name,
|
|
orgName: org.name,
|
|
apiUrl: process.env.BETTER_AUTH_URL!,
|
|
})
|
|
return { success: true }
|
|
}),
|
|
|
|
/**
|
|
* Get own member profile
|
|
*/
|
|
me: memberProcedure.query(async ({ ctx }) => {
|
|
const member = await ctx.prisma.member.findFirst({
|
|
where: { userId: ctx.session.user.id, orgId: ctx.orgId },
|
|
include: { org: true },
|
|
})
|
|
return member
|
|
}),
|
|
})
|