103 lines
3.2 KiB
TypeScript
103 lines
3.2 KiB
TypeScript
import { CatalogError, foundationErrors } from "@hivegrid/foundation";
|
|
import { withTenantRLS, type TenantRlsClient } from "~/utils/db/.server/tenantRls.server";
|
|
|
|
export type HrAudienceKind = "all" | "team" | "location";
|
|
|
|
export type HrAudienceViewer = {
|
|
team: string | null;
|
|
locationId: string | null;
|
|
};
|
|
|
|
export type HrAudienceOption = {
|
|
kind: Exclude<HrAudienceKind, "all">;
|
|
ref: string;
|
|
label: string;
|
|
};
|
|
|
|
export function isHrAudienceVisible(
|
|
kind: HrAudienceKind,
|
|
ref: string | null,
|
|
viewer: HrAudienceViewer,
|
|
): boolean {
|
|
if (kind === "all") return true;
|
|
if (kind === "team") return ref != null && ref === viewer.team;
|
|
return ref != null && ref === viewer.locationId;
|
|
}
|
|
|
|
export async function requireValidHrAudienceWithTx(
|
|
tx: TenantRlsClient,
|
|
input: { tenantId: string; kind: HrAudienceKind; ref: string | null },
|
|
): Promise<void> {
|
|
if (input.kind === "all") {
|
|
if (input.ref) {
|
|
throw new CatalogError(foundationErrors.notFound, { audienceRef: input.ref });
|
|
}
|
|
return;
|
|
}
|
|
if (!input.ref) {
|
|
throw new CatalogError(foundationErrors.notFound, { audienceKind: input.kind });
|
|
}
|
|
if (input.kind === "team") {
|
|
const team = await tx.hrEmployeeProfile.findFirst({
|
|
where: { tenantId: input.tenantId, team: input.ref },
|
|
select: { id: true },
|
|
});
|
|
if (!team) throw new CatalogError(foundationErrors.notFound, { audienceRef: input.ref });
|
|
return;
|
|
}
|
|
const location = await tx.hrLocation.findFirst({
|
|
where: { tenantId: input.tenantId, id: input.ref, archivedAt: null },
|
|
select: { id: true },
|
|
});
|
|
if (!location) throw new CatalogError(foundationErrors.notFound, { audienceRef: input.ref });
|
|
}
|
|
|
|
export async function requireValidHrAudience(input: {
|
|
tenantId: string;
|
|
kind: HrAudienceKind;
|
|
ref: string | null;
|
|
}): Promise<void> {
|
|
await withTenantRLS(input.tenantId, (tx) => requireValidHrAudienceWithTx(tx, input));
|
|
}
|
|
|
|
export async function requireHrAudienceViewer(input: {
|
|
tenantId: string;
|
|
userId: string;
|
|
kind: HrAudienceKind;
|
|
ref: string | null;
|
|
canManage: boolean;
|
|
}): Promise<void> {
|
|
if (input.canManage || input.kind === "all") return;
|
|
const profile = await withTenantRLS(input.tenantId, (tx) =>
|
|
tx.hrEmployeeProfile.findFirst({
|
|
where: { tenantId: input.tenantId, userId: input.userId },
|
|
select: { team: true, locationId: true },
|
|
}),
|
|
);
|
|
if (!profile || !isHrAudienceVisible(input.kind, input.ref, profile)) {
|
|
throw new CatalogError(foundationErrors.notFound, { audienceRef: input.ref });
|
|
}
|
|
}
|
|
|
|
export async function listHrAudienceOptions(tenantId: string): Promise<HrAudienceOption[]> {
|
|
return withTenantRLS(tenantId, async (tx) => {
|
|
const [teams, locations] = await Promise.all([
|
|
tx.hrEmployeeProfile.findMany({
|
|
where: { tenantId, team: { not: null } },
|
|
select: { team: true },
|
|
distinct: ["team"],
|
|
orderBy: { team: "asc" },
|
|
}),
|
|
tx.hrLocation.findMany({
|
|
where: { tenantId, archivedAt: null },
|
|
select: { id: true, name: true },
|
|
orderBy: { name: "asc" },
|
|
}),
|
|
]);
|
|
return [
|
|
...teams.flatMap(({ team }) => (team ? [{ kind: "team" as const, ref: team, label: team }] : [])),
|
|
...locations.map(({ id, name }) => ({ kind: "location" as const, ref: id, label: name })),
|
|
];
|
|
});
|
|
}
|