stadtwerke/innungsapp/apps/admin/server/routers/members.ts

162 lines
4.4 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, 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
}),
})