refactor(api): split resource read models
This commit is contained in:
@@ -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 ?? "?"})`),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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">;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user