Added Teams
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 26s
Build and Push Docker Images / build-frontend (push) Successful in 1m7s

Minor Rework of Plantafel
This commit is contained in:
2026-04-14 21:17:05 +02:00
parent 6fcaf3f65c
commit 849e24092e
22 changed files with 773 additions and 81 deletions

View File

@@ -9,6 +9,7 @@ import {
import {and, eq, inArray} from "drizzle-orm"
import { enrichProfilesWithBranches } from "../../utils/profileBranches"
import { enrichProfilesWithTeams } from "../../utils/profileTeams"
export default async function tenantRoutesInternal(server: FastifyInstance) {
@@ -62,7 +63,8 @@ export default async function tenantRoutesInternal(server: FastifyInstance) {
eq(authProfiles.tenant_id, tenantId),
inArray(authProfiles.user_id, userIds)
))
const profiles = await enrichProfilesWithBranches(server, profileRows)
const profilesWithBranches = await enrichProfilesWithBranches(server, profileRows)
const profiles = await enrichProfilesWithTeams(server, profilesWithBranches)
const combined = users.map(u => {
const profile = profiles.find(p => p.user_id === u.id)
@@ -98,7 +100,8 @@ export default async function tenantRoutesInternal(server: FastifyInstance) {
.from(authProfiles)
.where(eq(authProfiles.tenant_id, tenantId))
return await enrichProfilesWithBranches(server, profileRows)
const profilesWithBranches = await enrichProfilesWithBranches(server, profileRows)
return await enrichProfilesWithTeams(server, profilesWithBranches)
} catch (err) {
console.error("/tenant/profiles ERROR:", err)

View File

@@ -9,6 +9,11 @@ import {
resolveTenantBranchIds,
syncProfileBranches,
} from "../utils/profileBranches";
import {
enrichProfilesWithTeams,
resolveTenantTeamIds,
syncProfileTeams,
} from "../utils/profileTeams";
export default async function authProfilesRoutes(server: FastifyInstance) {
@@ -24,7 +29,10 @@ export default async function authProfilesRoutes(server: FastifyInstance) {
return reply.code(400).send({ error: "No tenant selected" });
}
const profile = await loadProfileWithBranches(server, id, tenantId)
const profileWithBranches = await loadProfileWithBranches(server, id, tenantId)
const [profile] = profileWithBranches
? await enrichProfilesWithTeams(server, [profileWithBranches])
: [null]
if (!profile) {
return reply.code(404).send({ error: "User not found or not in tenant" });
@@ -45,7 +53,7 @@ export default async function authProfilesRoutes(server: FastifyInstance) {
const forbidden = [
"id", "user_id", "tenant_id", "created_at", "updated_at",
"updatedAt", "updatedBy", "old_profile_id", "full_name",
"branch", "branches", "branch_ids"
"branch"
]
forbidden.forEach(f => delete cleaned[f])
@@ -95,6 +103,19 @@ export default async function authProfilesRoutes(server: FastifyInstance) {
],
body.branch_id ?? body.branch?.id ?? null
)
const teamIds = await resolveTenantTeamIds(
server,
tenantId,
[
...(Array.isArray(body.team_ids) ? body.team_ids : []),
...(Array.isArray(body.teams) ? body.teams : []),
],
)
delete body.branch_ids
delete body.branches
delete body.team_ids
delete body.teams
const updateData = {
...body,
@@ -119,8 +140,12 @@ export default async function authProfilesRoutes(server: FastifyInstance) {
}
await syncProfileBranches(server, id, branchIds, userId)
await syncProfileTeams(server, id, teamIds, userId)
const profile = await loadProfileWithBranches(server, id, tenantId)
const profileWithBranches = await loadProfileWithBranches(server, id, tenantId)
const [profile] = profileWithBranches
? await enrichProfilesWithTeams(server, [profileWithBranches])
: [null]
return profile || updated[0]
} catch (err) {
@@ -128,6 +153,9 @@ export default async function authProfilesRoutes(server: FastifyInstance) {
if (err instanceof Error && ["INVALID_BRANCH_SELECTION", "INVALID_PRIMARY_BRANCH"].includes(err.message)) {
return reply.code(400).send({ error: "Ungültige Niederlassungsauswahl" })
}
if (err instanceof Error && err.message === "INVALID_TEAM_SELECTION") {
return reply.code(400).send({ error: "Ungültige Teamauswahl" })
}
return reply.code(500).send({ error: "Internal Server Error" })
}
})

View File

@@ -13,6 +13,7 @@ import {
import {and, desc, eq, inArray} from "drizzle-orm"
import { enrichProfilesWithBranches } from "../utils/profileBranches"
import { enrichProfilesWithTeams } from "../utils/profileTeams"
export default async function tenantRoutes(server: FastifyInstance) {
@@ -132,7 +133,8 @@ export default async function tenantRoutes(server: FastifyInstance) {
eq(authProfiles.tenant_id, tenantId),
inArray(authProfiles.user_id, userIds)
))
const profiles = await enrichProfilesWithBranches(server, profileRows)
const profilesWithBranches = await enrichProfilesWithBranches(server, profileRows)
const profiles = await enrichProfilesWithTeams(server, profilesWithBranches)
const combined = users.map(u => {
const profile = profiles.find(p => p.user_id === u.id)
@@ -167,7 +169,8 @@ export default async function tenantRoutes(server: FastifyInstance) {
.from(authProfiles)
.where(eq(authProfiles.tenant_id, tenantId))
const data = await enrichProfilesWithBranches(server, profileRows)
const profilesWithBranches = await enrichProfilesWithBranches(server, profileRows)
const data = await enrichProfilesWithTeams(server, profilesWithBranches)
return { data }
} catch (err) {

View File

@@ -32,6 +32,7 @@ const HISTORY_ENTITY_LABELS: Record<string, string> = {
incominginvoices: "Eingangsrechnungen",
files: "Dateien",
memberrelations: "Mitgliedsverhältnisse",
teams: "Teams",
}
export function getHistoryEntityLabel(entity: string) {

View File

@@ -0,0 +1,117 @@
import { and, eq, inArray } from "drizzle-orm"
import { FastifyInstance } from "fastify"
import { authProfileTeams, branches, teams } from "../../db/schema"
function normalizeTeamIds(values: any[]): number[] {
return [...new Set(
values
.map((value) => {
if (typeof value === "number") return value
if (typeof value === "string" && value.trim()) return Number(value)
if (value && typeof value === "object" && "id" in value) return Number(value.id)
return NaN
})
.filter((value) => Number.isFinite(value))
)]
}
export async function enrichProfilesWithTeams(server: FastifyInstance, profiles: any[]) {
if (!profiles.length) return profiles
const profileIds = profiles.map((profile) => profile.id).filter(Boolean)
if (!profileIds.length) return profiles
const profileTeamRows = await server.db
.select()
.from(authProfileTeams)
.where(inArray(authProfileTeams.profile_id, profileIds))
const teamIds = [...new Set(profileTeamRows.map((row) => row.team_id).filter(Boolean))]
const teamRows = teamIds.length
? await server.db.select().from(teams).where(inArray(teams.id, teamIds))
: []
const branchIds = [...new Set(teamRows.map((team) => team.branch).filter(Boolean))]
const branchRows = branchIds.length
? await server.db.select().from(branches).where(inArray(branches.id, branchIds))
: []
const branchMap = new Map(branchRows.map((branch) => [branch.id, branch]))
const teamMap = new Map(teamRows.map((team) => [
team.id,
{
...team,
branch: team.branch ? branchMap.get(team.branch) || null : null,
},
]))
const teamIdsByProfile = new Map<string, number[]>()
for (const row of profileTeamRows) {
const current = teamIdsByProfile.get(row.profile_id) || []
current.push(row.team_id)
teamIdsByProfile.set(row.profile_id, current)
}
return profiles.map((profile) => {
const assignedTeamIds = [...new Set(teamIdsByProfile.get(profile.id) || [])]
return {
...profile,
teams: assignedTeamIds
.map((teamId) => teamMap.get(teamId))
.filter(Boolean),
team_ids: assignedTeamIds,
}
})
}
export async function resolveTenantTeamIds(
server: FastifyInstance,
tenantId: number,
values: any[],
) {
const requestedTeamIds = normalizeTeamIds(values)
if (!requestedTeamIds.length) {
return []
}
const validTeams = await server.db
.select({ id: teams.id })
.from(teams)
.where(
and(
eq(teams.tenant, tenantId),
inArray(teams.id, requestedTeamIds)
)
)
const validTeamIds = validTeams.map((team) => team.id)
if (validTeamIds.length !== requestedTeamIds.length) {
throw new Error("INVALID_TEAM_SELECTION")
}
return validTeamIds
}
export async function syncProfileTeams(
server: FastifyInstance,
profileId: string,
teamIds: number[],
userId?: string | null
) {
await server.db
.delete(authProfileTeams)
.where(eq(authProfileTeams.profile_id, profileId))
if (!teamIds.length) return
await server.db
.insert(authProfileTeams)
.values(teamIds.map((teamId) => ({
profile_id: profileId,
team_id: teamId,
created_by: userId || null,
})))
}

View File

@@ -36,6 +36,7 @@ import {
spaces,
statementallocations,
tasks,
teams,
texttemplates,
units,
vehicles,
@@ -171,6 +172,11 @@ export const resourceConfig = {
searchColumns: ["name","number","description"],
numberRangeHolder: "number",
},
teams: {
table: teams,
searchColumns: ["name", "description"],
mtoLoad: ["branch"],
},
tasks: {
table: tasks,
},