refactor(api): split resource read models

This commit is contained in:
2026-04-01 07:38:03 +02:00
parent 41916a4e46
commit 3e53471f05
4 changed files with 169 additions and 168 deletions
@@ -9,10 +9,10 @@ import {
import { assertCanReadResource } from "../lib/resource-access.js"; import { assertCanReadResource } from "../lib/resource-access.js";
import { protectedProcedure } from "../trpc.js"; import { protectedProcedure } from "../trpc.js";
import { import {
mapResourceDetail,
readResourceByIdentifierDetailSnapshot, readResourceByIdentifierDetailSnapshot,
resolveResourceIdentifierSnapshot, resolveResourceIdentifierSnapshot,
} from "./resource-read-shared.js"; } from "./resource-read-shared.js";
import { mapResourceDetail } from "./resource-read-models.js";
export const resourceIdentifierReadProcedures = { export const resourceIdentifierReadProcedures = {
resolveByIdentifier: protectedProcedure resolveByIdentifier: protectedProcedure
@@ -0,0 +1,160 @@
import { fmtEur } from "../lib/format-utils.js";
export const RESOURCE_SUMMARY_SELECT = {
id: true,
eid: true,
displayName: true,
chapter: true,
isActive: true,
areaRole: { select: { name: true } },
country: { select: { code: true, name: true } },
metroCity: { select: { name: true } },
orgUnit: { select: { name: true } },
} as const;
export const RESOURCE_SUMMARY_DETAIL_SELECT = {
...RESOURCE_SUMMARY_SELECT,
fte: true,
lcrCents: true,
chargeabilityTarget: true,
} as const;
export const RESOURCE_IDENTIFIER_SELECT = {
id: true,
eid: true,
displayName: true,
chapter: true,
isActive: true,
} as const;
export const RESOURCE_IDENTIFIER_DETAIL_SELECT = {
...RESOURCE_IDENTIFIER_SELECT,
email: true,
fte: true,
lcrCents: true,
ucrCents: true,
chargeabilityTarget: true,
availability: true,
skills: true,
postalCode: true,
federalState: true,
areaRole: { select: { name: true, color: true } },
country: { select: { code: true, name: true, dailyWorkingHours: true } },
metroCity: { select: { name: true } },
managementLevelGroup: { select: { name: true, targetPercentage: true } },
orgUnit: { select: { name: true, level: true } },
_count: { select: { assignments: true, vacations: true } },
} as const;
export const RESOURCE_DIRECTORY_SELECT = {
id: true,
eid: true,
displayName: true,
chapter: true,
isActive: true,
} as const;
export function mapResourceSummary(resource: {
id: string;
eid: string;
displayName: string;
chapter: string | null;
isActive: boolean;
areaRole: { name: string } | null;
country: { code: string; name: string } | null;
metroCity: { name: string } | null;
orgUnit: { name: string } | null;
}) {
return {
id: resource.id,
eid: resource.eid,
name: resource.displayName,
chapter: resource.chapter,
role: resource.areaRole?.name ?? null,
country: resource.country?.name ?? resource.country?.code ?? null,
countryCode: resource.country?.code ?? null,
metroCity: resource.metroCity?.name ?? null,
orgUnit: resource.orgUnit?.name ?? null,
active: resource.isActive,
};
}
export function mapResourceSummaryDetail(resource: {
id: string;
eid: string;
displayName: string;
chapter: string | null;
fte: number | null;
lcrCents: number | null;
chargeabilityTarget: number | null;
isActive: boolean;
areaRole: { name: string } | null;
country: { code: string; name: string } | null;
metroCity: { name: string } | null;
orgUnit: { name: string } | null;
}) {
return {
id: resource.id,
eid: resource.eid,
name: resource.displayName,
chapter: resource.chapter,
role: resource.areaRole?.name ?? null,
country: resource.country?.name ?? resource.country?.code ?? null,
countryCode: resource.country?.code ?? null,
metroCity: resource.metroCity?.name ?? null,
orgUnit: resource.orgUnit?.name ?? null,
fte: resource.fte,
lcr: resource.lcrCents != null ? fmtEur(resource.lcrCents) : null,
chargeabilityTarget: resource.chargeabilityTarget != null ? `${resource.chargeabilityTarget}%` : null,
active: resource.isActive,
};
}
export function mapResourceDetail(resource: {
id: string;
eid: string;
displayName: string;
email: string | null;
chapter: string | null;
fte: number | null;
lcrCents: number | null;
ucrCents: number | null;
chargeabilityTarget: number | null;
isActive: boolean;
skills: unknown;
postalCode: string | null;
federalState: string | null;
areaRole: { name: string; color: string | null } | null;
country: { code: string; name: string; dailyWorkingHours: number | null } | null;
metroCity: { name: string } | null;
managementLevelGroup: { name: string; targetPercentage: number | null } | null;
orgUnit: { name: string; level: number } | null;
_count: { assignments: number; vacations: number };
}) {
const skills = Array.isArray(resource.skills) ? resource.skills as { name?: string; level?: number }[] : [];
return {
id: resource.id,
eid: resource.eid,
name: resource.displayName,
email: resource.email,
chapter: resource.chapter,
role: resource.areaRole?.name ?? null,
country: resource.country?.name ?? resource.country?.code ?? null,
countryCode: resource.country?.code ?? null,
countryHours: resource.country?.dailyWorkingHours ?? 8,
metroCity: resource.metroCity?.name ?? null,
fte: resource.fte,
lcr: resource.lcrCents != null ? fmtEur(resource.lcrCents) : null,
ucr: resource.ucrCents != null ? fmtEur(resource.ucrCents) : null,
chargeabilityTarget: resource.chargeabilityTarget != null ? `${resource.chargeabilityTarget}%` : null,
managementLevel: resource.managementLevelGroup?.name ?? null,
orgUnit: resource.orgUnit?.name ?? null,
postalCode: resource.postalCode,
federalState: resource.federalState,
active: resource.isActive,
totalAssignments: resource._count.assignments,
totalVacations: resource._count.vacations,
skillCount: skills.length,
topSkills: skills.slice(0, 10).map((skill) => `${skill.name ?? "?"} (${skill.level ?? "?"})`),
};
}
+7 -165
View File
@@ -9,13 +9,19 @@ import {
getAnonymizationDirectory, getAnonymizationDirectory,
resolveResourceIdsByDisplayedEids, resolveResourceIdsByDisplayedEids,
} from "../lib/anonymization.js"; } from "../lib/anonymization.js";
import { fmtEur } from "../lib/format-utils.js";
import type { TRPCContext } from "../trpc.js"; import type { TRPCContext } from "../trpc.js";
import { import {
assertCanReadResource, assertCanReadResource,
canReadAllResources, canReadAllResources,
type ResourceReadContext, type ResourceReadContext,
} from "../lib/resource-access.js"; } from "../lib/resource-access.js";
import {
RESOURCE_DIRECTORY_SELECT,
RESOURCE_IDENTIFIER_DETAIL_SELECT,
RESOURCE_IDENTIFIER_SELECT,
RESOURCE_SUMMARY_DETAIL_SELECT,
RESOURCE_SUMMARY_SELECT,
} from "./resource-read-models.js";
function parseResourceCursor(cursor: string | undefined): { displayName: string; id: string } | null { function parseResourceCursor(cursor: string | undefined): { displayName: string; id: string } | null {
if (!cursor) return null; if (!cursor) return null;
@@ -30,162 +36,6 @@ function parseResourceCursor(cursor: string | undefined): { displayName: string;
return null; return null;
} }
const RESOURCE_SUMMARY_SELECT = {
id: true,
eid: true,
displayName: true,
chapter: true,
isActive: true,
areaRole: { select: { name: true } },
country: { select: { code: true, name: true } },
metroCity: { select: { name: true } },
orgUnit: { select: { name: true } },
} as const;
const RESOURCE_SUMMARY_DETAIL_SELECT = {
...RESOURCE_SUMMARY_SELECT,
fte: true,
lcrCents: true,
chargeabilityTarget: true,
} as const;
const RESOURCE_IDENTIFIER_SELECT = {
id: true,
eid: true,
displayName: true,
chapter: true,
isActive: true,
} as const;
const RESOURCE_IDENTIFIER_DETAIL_SELECT = {
...RESOURCE_IDENTIFIER_SELECT,
id: true,
eid: true,
displayName: true,
email: true,
chapter: true,
fte: true,
lcrCents: true,
ucrCents: true,
chargeabilityTarget: true,
isActive: true,
availability: true,
skills: true,
postalCode: true,
federalState: true,
areaRole: { select: { name: true, color: true } },
country: { select: { code: true, name: true, dailyWorkingHours: true } },
metroCity: { select: { name: true } },
managementLevelGroup: { select: { name: true, targetPercentage: true } },
orgUnit: { select: { name: true, level: true } },
_count: { select: { assignments: true, vacations: true } },
} as const;
export function mapResourceSummary(resource: {
id: string;
eid: string;
displayName: string;
chapter: string | null;
isActive: boolean;
areaRole: { name: string } | null;
country: { code: string; name: string } | null;
metroCity: { name: string } | null;
orgUnit: { name: string } | null;
}) {
return {
id: resource.id,
eid: resource.eid,
name: resource.displayName,
chapter: resource.chapter,
role: resource.areaRole?.name ?? null,
country: resource.country?.name ?? resource.country?.code ?? null,
countryCode: resource.country?.code ?? null,
metroCity: resource.metroCity?.name ?? null,
orgUnit: resource.orgUnit?.name ?? null,
active: resource.isActive,
};
}
export function mapResourceSummaryDetail(resource: {
id: string;
eid: string;
displayName: string;
chapter: string | null;
fte: number | null;
lcrCents: number | null;
chargeabilityTarget: number | null;
isActive: boolean;
areaRole: { name: string } | null;
country: { code: string; name: string } | null;
metroCity: { name: string } | null;
orgUnit: { name: string } | null;
}) {
return {
id: resource.id,
eid: resource.eid,
name: resource.displayName,
chapter: resource.chapter,
role: resource.areaRole?.name ?? null,
country: resource.country?.name ?? resource.country?.code ?? null,
countryCode: resource.country?.code ?? null,
metroCity: resource.metroCity?.name ?? null,
orgUnit: resource.orgUnit?.name ?? null,
fte: resource.fte,
lcr: resource.lcrCents != null ? fmtEur(resource.lcrCents) : null,
chargeabilityTarget: resource.chargeabilityTarget != null ? `${resource.chargeabilityTarget}%` : null,
active: resource.isActive,
};
}
export function mapResourceDetail(resource: {
id: string;
eid: string;
displayName: string;
email: string | null;
chapter: string | null;
fte: number | null;
lcrCents: number | null;
ucrCents: number | null;
chargeabilityTarget: number | null;
isActive: boolean;
skills: unknown;
postalCode: string | null;
federalState: string | null;
areaRole: { name: string; color: string | null } | null;
country: { code: string; name: string; dailyWorkingHours: number | null } | null;
metroCity: { name: string } | null;
managementLevelGroup: { name: string; targetPercentage: number | null } | null;
orgUnit: { name: string; level: number } | null;
_count: { assignments: number; vacations: number };
}) {
const skills = Array.isArray(resource.skills) ? resource.skills as { name?: string; level?: number }[] : [];
return {
id: resource.id,
eid: resource.eid,
name: resource.displayName,
email: resource.email,
chapter: resource.chapter,
role: resource.areaRole?.name ?? null,
country: resource.country?.name ?? resource.country?.code ?? null,
countryCode: resource.country?.code ?? null,
countryHours: resource.country?.dailyWorkingHours ?? 8,
metroCity: resource.metroCity?.name ?? null,
fte: resource.fte,
lcr: resource.lcrCents != null ? fmtEur(resource.lcrCents) : null,
ucr: resource.ucrCents != null ? fmtEur(resource.ucrCents) : null,
chargeabilityTarget: resource.chargeabilityTarget != null ? `${resource.chargeabilityTarget}%` : null,
managementLevel: resource.managementLevelGroup?.name ?? null,
orgUnit: resource.orgUnit?.name ?? null,
postalCode: resource.postalCode,
federalState: resource.federalState,
active: resource.isActive,
totalAssignments: resource._count.assignments,
totalVacations: resource._count.vacations,
skillCount: skills.length,
topSkills: skills.slice(0, 10).map((skill) => `${skill.name ?? "?"} (${skill.level ?? "?"})`),
};
}
function isBroadResourceLookupAllowed(ctx: Pick<TRPCContext, "dbUser" | "roleDefaults">): boolean { function isBroadResourceLookupAllowed(ctx: Pick<TRPCContext, "dbUser" | "roleDefaults">): boolean {
return canReadAllResources(ctx); return canReadAllResources(ctx);
} }
@@ -218,14 +68,6 @@ export const ResourceListQuerySchema = ResourceDirectoryQuerySchema.extend({
})).optional(), })).optional(),
}); });
const RESOURCE_DIRECTORY_SELECT = {
id: true,
eid: true,
displayName: true,
chapter: true,
isActive: true,
} as const;
export async function listStaffResources( export async function listStaffResources(
ctx: Pick<TRPCContext, "db">, ctx: Pick<TRPCContext, "db">,
input: z.infer<typeof ResourceListQuerySchema>, input: z.infer<typeof ResourceListQuerySchema>,
@@ -25,11 +25,10 @@ import {
ResourceListQuerySchema, ResourceListQuerySchema,
listResourceDirectoryEntries, listResourceDirectoryEntries,
listStaffResources, listStaffResources,
mapResourceSummary,
mapResourceSummaryDetail,
readResourceSummariesSnapshot, readResourceSummariesSnapshot,
readResourceSummaryDetailsSnapshot, readResourceSummaryDetailsSnapshot,
} from "./resource-read-shared.js"; } from "./resource-read-shared.js";
import { mapResourceSummary, mapResourceSummaryDetail } from "./resource-read-models.js";
type ResourceSummaryReadContext = Pick<TRPCContext, "db" | "dbUser" | "roleDefaults">; type ResourceSummaryReadContext = Pick<TRPCContext, "db" | "dbUser" | "roleDefaults">;