Rebuild as InnungsApp project: replace stadtwerke analysis with full documentation
- PRD: vollständige Produktspezifikation (5 Module, Scope, Akzeptanzkriterien) - ARCHITECTURE: Tech Stack, Ordnerstruktur, Multi-Tenancy, Push, Kosten - DATABASE_SCHEMA: Vollständiges SQL-Schema mit RLS Policies und Views - USER_STORIES: 40+ Stories nach Rolle (Admin, Mitglied, Azubi, Obermeister) - PERSONAS: 5 detaillierte Nutzerprofile mit Alltag, Zitaten und Erwartungen - BUSINESS_MODEL: Preistabellen, Unit Economics, Revenue-Projektionen, Distribution - ROADMAP: 6 Phasen, Sprint-Planung, Meilensteine und KPIs - COMPETITIVE_ANALYSIS: Wettbewerbsmatrix, USPs, Preispositionierung - API_DESIGN: Supabase Query Patterns, Edge Functions, Realtime Subscriptions - ONBOARDING_FLOWS: 7 User Flows von Setup bis Fehlerfall - GTM_STRATEGY: 3-Phasen-Vertrieb, Outreach-Sequenz, Einwandbehandlung - AZUBI_MODULE: Video-Feed, 1-Click-Apply, Chat, Berichtsheft, Quiz - DSGVO_KONZEPT: Rechtsgrundlagen, TOMs, AVV, Minderjährige, Incident Response - FEATURES_BACKLOG: 72 Features nach MoSCoW + Technische Schulden Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
161
innungsapp/apps/admin/server/routers/members.ts
Normal file
161
innungsapp/apps/admin/server/routers/members.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
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
|
||||
}),
|
||||
})
|
||||
Reference in New Issue
Block a user