import { CHAT_PERMISSIONS } from "@hivegrid/chat"; import { loadChatChannelSettings, type ChatChannelSettingsRecord } from "@hivegrid/chat/channel-settings.server"; import { archiveChatChannel as archiveManagedChatChannel, leaveGroupChat, removeChatChannelMember as removeManagedChatChannelMember, setChatChannelMember, setGroupChatPrivacy, } from "@hivegrid/chat/lifecycle.server"; import { buildChatChannelSettingsVM, type ChatChannelSettingsVM } from "@hivegrid/view-models"; import { withTenantRLS } from "~/utils/db/.server/tenantRls.server"; import { projectDetailPath } from "~/utils/integrations/projects/projectRoutePaths"; import { requireChatPermission, type ChatRouteContext } from "../routes/chatAccess.server"; export type ChatChannelSettingsLabels = Readonly<{ directMessage: string; unnamedGroup: string; unnamedProject: string; unknownProjectStage: string; }>; export async function loadChatChannelSettingsPage(input: { readonly context: ChatRouteContext; readonly tenantSlug: string; readonly chatId: string; readonly labels: ChatChannelSettingsLabels; }): Promise { const context = requireChatPermission(input.context, CHAT_PERMISSIONS.readOwnChannels); const record = await loadChatChannelSettings({ tenantId: context.tenantId, userId: context.userId, permissions: context.permissions, chatId: input.chatId, }); const project = record.channel.projectId ? await loadProjectSummary(context.tenantId, input.tenantSlug, record.channel.projectId, input.labels) : null; return buildChatChannelSettingsVM({ currentUserId: context.userId, channel: { id: record.channel.id, kind: record.channel.kind, title: channelTitle(record, context.userId, project?.label ?? null, input.labels), description: record.channel.description, isPrivate: record.channel.privacy !== "public", isArchived: record.channel.archivedAtIso !== null, project, }, members: record.members.map((member) => ({ user: profileInput(member), role: member.roleInChat, isProjectManaged: member.provenance.isProjectManaged, isManualGuest: member.provenance.isManualMember, manualRole: member.provenance.manualRoleInChat, })), tenantUsers: record.candidateUsers.map(profileInput), permissions: { canManageMembers: record.capabilities.canManageMembers, canManageAdmins: record.capabilities.canManageAdmins, canChangePrivacy: record.capabilities.canChangePrivacy, canLeave: record.capabilities.canLeave, canManageManualGuests: record.capabilities.canManageManualGuests, canArchive: record.capabilities.canArchive, }, }); } export async function addChatChannelMember(input: MutationInput & { readonly memberUserId: string }): Promise { await setChatChannelMember({ ...repositoryContext(input.context), chatId: input.chatId, requestId: input.requestId, memberUserId: input.memberUserId, roleInChat: "member" }); } export async function changeChatChannelMemberRole( input: MutationInput & { readonly memberUserId: string; readonly roleInChat: "admin" | "member" } ): Promise { await setChatChannelMember({ ...repositoryContext(input.context), chatId: input.chatId, requestId: input.requestId, memberUserId: input.memberUserId, roleInChat: input.roleInChat, }); } export async function removeChatChannelMember(input: MutationInput & { readonly memberUserId: string }): Promise { await removeManagedChatChannelMember({ ...repositoryContext(input.context), chatId: input.chatId, requestId: input.requestId, memberUserId: input.memberUserId, }); } export async function changeChatChannelPrivacy(input: MutationInput & { readonly privacy: "public" | "private" }): Promise { await setGroupChatPrivacy({ ...repositoryContext(input.context), chatId: input.chatId, requestId: input.requestId, isPrivate: input.privacy === "private", }); } export async function leaveChatChannel(input: MutationInput): Promise { await leaveGroupChat({ ...repositoryContext(input.context), chatId: input.chatId, requestId: input.requestId }); } export async function archiveChatChannel(input: MutationInput): Promise { await archiveManagedChatChannel({ ...repositoryContext(input.context), chatId: input.chatId, requestId: input.requestId }); } type MutationInput = Readonly<{ context: ChatRouteContext; chatId: string; requestId: string; }>; function repositoryContext(context: ChatRouteContext) { return { tenantId: context.tenantId, userId: context.userId, permissions: context.permissions }; } function profileInput(profile: { readonly userId: string; readonly displayName: string; readonly email: string; readonly avatarUrl: string | null; }) { return { userId: profile.userId, displayName: profile.displayName, email: profile.email, avatar: { imageUrl: profile.avatarUrl, fallbackLabel: fallbackLabel(profile.displayName, profile.email), }, }; } function fallbackLabel(displayName: string, email: string): string { const initials = displayName .split(/\s+/u) .filter(Boolean) .slice(0, 2) .map((part) => part.slice(0, 1)) .join("") .toLocaleUpperCase(); return initials || email.slice(0, 1).toLocaleUpperCase() || "?"; } function channelTitle( record: ChatChannelSettingsRecord, currentUserId: string, projectLabel: string | null, labels: ChatChannelSettingsLabels ): string { if (record.channel.kind === "group") return record.channel.name ?? labels.unnamedGroup; if (record.channel.kind === "project") return projectLabel ?? record.channel.name ?? labels.unnamedProject; return record.members.find((member) => member.userId !== currentUserId)?.displayName ?? labels.directMessage; } async function loadProjectSummary( tenantId: string, tenantSlug: string, projectId: string, labels: ChatChannelSettingsLabels ) { const project = await withTenantRLS(tenantId, (tx) => tx.projekt.findFirst({ where: { tenantId, id: projectId }, select: { id: true, projektNummer: true, projektname: true, stage: { select: { name: true } }, }, }) ); return { id: projectId, number: project?.projektNummer ?? projectId, label: project?.projektname?.trim() || labels.unnamedProject, stageLabel: project?.stage.name?.trim() || labels.unknownProjectStage, href: projectDetailPath(tenantSlug, projectId), }; }