import type { Actor, ProjectGoal, ProjectGoalStatus } from "@rakazo/contracts"; import type { PrismaClient } from "./client.js"; import { IsolationError } from "./scope.js"; export const PROJECT_GOAL_STATUSES = ["open", "doing", "done"] as const; export const VOICE_FIRST_GOAL_TITLES = ["Voice First", "Voice First Feature"] as const; const EVOO_SECTION_NAME = /^ev[oO]{2}s?$/; type ProjectGoalRow = { id: string; sectionId: string; groupId: string | null; title: string; description: string | null; status: string; createdAt: Date; updatedAt: Date; }; export function isProjectGoalStatus(value: unknown): value is ProjectGoalStatus { return typeof value === "string" && (PROJECT_GOAL_STATUSES as readonly string[]).includes(value); } export function coerceProjectGoalStatus(value: unknown): ProjectGoalStatus { return isProjectGoalStatus(value) ? value : "open"; } export function mapProjectGoal(row: ProjectGoalRow): ProjectGoal { return { id: row.id, sectionId: row.sectionId, groupId: row.groupId, title: row.title, description: row.description, status: coerceProjectGoalStatus(row.status), createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), }; } async function getOwnedSection( prisma: PrismaClient, actor: Actor, sectionId: string, ): Promise<{ id: string; name: string }> { const section = await prisma.botSection.findFirst({ where: { id: sectionId, spaceId: actor.spaceId, userId: actor.userId, }, select: { id: true, name: true }, }); if (!section) throw new IsolationError(); return section; } async function assertOwnedGroupInSection( prisma: PrismaClient, actor: Actor, groupId: string, sectionId: string, ) { const group = await prisma.chatGroup.findFirst({ where: { id: groupId, spaceId: actor.spaceId, userId: actor.userId, sectionId, archivedAt: null, }, select: { id: true }, }); if (!group) throw new IsolationError(); } async function ensureVoiceFirstGoal( prisma: PrismaClient, actor: Actor, section: { id: string; name: string }, linkGroupId?: string, ) { if (!EVOO_SECTION_NAME.test(section.name.trim())) return; const existing = await prisma.projectGoal.findFirst({ where: { spaceId: actor.spaceId, userId: actor.userId, sectionId: section.id, OR: [ { title: { in: [...VOICE_FIRST_GOAL_TITLES] } }, { title: { contains: "voice first", mode: "insensitive" } }, ], }, select: { id: true }, }); if (existing) return; if (linkGroupId) { await assertOwnedGroupInSection(prisma, actor, linkGroupId, section.id); } await prisma.projectGoal.create({ data: { spaceId: actor.spaceId, userId: actor.userId, sectionId: section.id, groupId: linkGroupId ?? null, title: "Voice First", description: "Voice-first Interaktion als primäre evOo-Erfahrung — sprechen statt tippen, wo es passt.", status: "open", }, }); } export function createProjectGoalRepos(prisma: PrismaClient) { return { async listGoals( actor: Actor, input: { sectionId: string; groupId?: string; seedDefaults?: boolean }, ): Promise { const section = await getOwnedSection(prisma, actor, input.sectionId); if (input.seedDefaults) { await ensureVoiceFirstGoal(prisma, actor, section, input.groupId); } const rows = await prisma.projectGoal.findMany({ where: { spaceId: actor.spaceId, userId: actor.userId, sectionId: input.sectionId, }, orderBy: [{ updatedAt: "desc" }, { createdAt: "desc" }], }); return rows.map(mapProjectGoal); }, async createGoal( actor: Actor, input: { sectionId: string; title: string; description?: string; status: ProjectGoalStatus; groupId?: string; }, ): Promise { await getOwnedSection(prisma, actor, input.sectionId); if (input.groupId) { await assertOwnedGroupInSection(prisma, actor, input.groupId, input.sectionId); } const row = await prisma.projectGoal.create({ data: { spaceId: actor.spaceId, userId: actor.userId, sectionId: input.sectionId, groupId: input.groupId ?? null, title: input.title.trim(), description: input.description?.trim() || null, status: input.status, }, }); return mapProjectGoal(row); }, async updateGoal( actor: Actor, input: { goalId: string; title?: string; description?: string | null; status?: ProjectGoalStatus; groupId?: string | null; }, ): Promise { const existing = await prisma.projectGoal.findFirst({ where: { id: input.goalId, spaceId: actor.spaceId, userId: actor.userId, }, }); if (!existing) throw new IsolationError(); if (input.groupId) { await assertOwnedGroupInSection(prisma, actor, input.groupId, existing.sectionId); } const row = await prisma.projectGoal.update({ where: { id: existing.id }, data: { ...(input.title !== undefined ? { title: input.title.trim() } : {}), ...(input.description !== undefined ? { description: input.description?.trim() || null } : {}), ...(input.status !== undefined ? { status: input.status } : {}), ...(input.groupId !== undefined ? { groupId: input.groupId } : {}), }, }); return mapProjectGoal(row); }, }; }