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, mode: 'insensitive' } }, { betrieb: { contains: input.search, mode: 'insensitive' } }, { ort: { contains: input.search, mode: 'insensitive' } }, { sparte: { contains: input.search, mode: 'insensitive' } }, ], }), }, 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, }) 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 }), })