refactor(api): extract allocation read procedures
This commit is contained in:
@@ -0,0 +1,165 @@
|
|||||||
|
import { AllocationStatus } from "@capakraken/shared";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||||
|
import { anonymizeResource, getAnonymizationDirectory } from "../lib/anonymization.js";
|
||||||
|
import { planningReadProcedure } from "../trpc.js";
|
||||||
|
import { buildResourceAvailabilitySummary, buildResourceAvailabilityView } from "./allocation-availability.js";
|
||||||
|
import { ASSIGNMENT_INCLUDE, DEMAND_INCLUDE } from "./allocation-shared.js";
|
||||||
|
import { getDemandRequirementByIdOrThrow, loadAllocationReadModel, resolveAssignmentBySelection } from "./allocation-support.js";
|
||||||
|
|
||||||
|
export const allocationReadProcedures = {
|
||||||
|
list: planningReadProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
projectId: z.string().optional(),
|
||||||
|
resourceId: z.string().optional(),
|
||||||
|
status: z.nativeEnum(AllocationStatus).optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const readModel = await loadAllocationReadModel(ctx.db, input);
|
||||||
|
return readModel.allocations;
|
||||||
|
}),
|
||||||
|
|
||||||
|
listView: planningReadProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
projectId: z.string().optional(),
|
||||||
|
resourceId: z.string().optional(),
|
||||||
|
status: z.nativeEnum(AllocationStatus).optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ ctx, input }) => loadAllocationReadModel(ctx.db, input)),
|
||||||
|
|
||||||
|
listDemands: planningReadProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
projectId: z.string().optional(),
|
||||||
|
status: z.nativeEnum(AllocationStatus).optional(),
|
||||||
|
roleId: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const demands = await ctx.db.demandRequirement.findMany({
|
||||||
|
where: {
|
||||||
|
...(input.projectId ? { projectId: input.projectId } : {}),
|
||||||
|
...(input.status ? { status: input.status } : {}),
|
||||||
|
...(input.roleId ? { roleId: input.roleId } : {}),
|
||||||
|
},
|
||||||
|
include: DEMAND_INCLUDE,
|
||||||
|
orderBy: { startDate: "asc" },
|
||||||
|
});
|
||||||
|
const directory = await getAnonymizationDirectory(ctx.db);
|
||||||
|
if (!directory) {
|
||||||
|
return demands;
|
||||||
|
}
|
||||||
|
return demands.map((demand) => ({
|
||||||
|
...demand,
|
||||||
|
assignments: demand.assignments.map((assignment) => (
|
||||||
|
assignment.resource
|
||||||
|
? { ...assignment, resource: anonymizeResource(assignment.resource, directory) }
|
||||||
|
: assignment
|
||||||
|
)),
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
|
||||||
|
listAssignments: planningReadProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
projectId: z.string().optional(),
|
||||||
|
resourceId: z.string().optional(),
|
||||||
|
status: z.nativeEnum(AllocationStatus).optional(),
|
||||||
|
demandRequirementId: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const assignments = await ctx.db.assignment.findMany({
|
||||||
|
where: {
|
||||||
|
...(input.projectId ? { projectId: input.projectId } : {}),
|
||||||
|
...(input.resourceId ? { resourceId: input.resourceId } : {}),
|
||||||
|
...(input.status ? { status: input.status } : {}),
|
||||||
|
...(input.demandRequirementId ? { demandRequirementId: input.demandRequirementId } : {}),
|
||||||
|
},
|
||||||
|
include: ASSIGNMENT_INCLUDE,
|
||||||
|
orderBy: { startDate: "asc" },
|
||||||
|
});
|
||||||
|
const directory = await getAnonymizationDirectory(ctx.db);
|
||||||
|
if (!directory) {
|
||||||
|
return assignments;
|
||||||
|
}
|
||||||
|
return assignments.map((assignment) => (
|
||||||
|
assignment.resource
|
||||||
|
? { ...assignment, resource: anonymizeResource(assignment.resource, directory) }
|
||||||
|
: assignment
|
||||||
|
));
|
||||||
|
}),
|
||||||
|
|
||||||
|
getAssignmentById: planningReadProcedure
|
||||||
|
.input(z.object({ id: z.string() }))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const assignment = await findUniqueOrThrow(
|
||||||
|
ctx.db.assignment.findUnique({
|
||||||
|
where: { id: input.id },
|
||||||
|
include: ASSIGNMENT_INCLUDE,
|
||||||
|
}),
|
||||||
|
"Assignment",
|
||||||
|
);
|
||||||
|
const directory = await getAnonymizationDirectory(ctx.db);
|
||||||
|
if (!directory || !assignment.resource) {
|
||||||
|
return assignment;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...assignment,
|
||||||
|
resource: anonymizeResource(assignment.resource, directory),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
resolveAssignment: planningReadProcedure
|
||||||
|
.input(z.object({
|
||||||
|
assignmentId: z.string().optional(),
|
||||||
|
resourceId: z.string().optional(),
|
||||||
|
projectId: z.string().optional(),
|
||||||
|
startDate: z.coerce.date().optional(),
|
||||||
|
endDate: z.coerce.date().optional(),
|
||||||
|
selectionMode: z.enum(["WINDOW", "EXACT_START"]).default("EXACT_START"),
|
||||||
|
excludeCancelled: z.boolean().default(false),
|
||||||
|
}))
|
||||||
|
.query(async ({ ctx, input }) => resolveAssignmentBySelection(ctx.db, input)),
|
||||||
|
|
||||||
|
getDemandRequirementById: planningReadProcedure
|
||||||
|
.input(z.object({ id: z.string() }))
|
||||||
|
.query(async ({ ctx, input }) => getDemandRequirementByIdOrThrow(ctx.db, input.id)),
|
||||||
|
|
||||||
|
checkResourceAvailability: planningReadProcedure
|
||||||
|
.input(z.object({
|
||||||
|
resourceId: z.string(),
|
||||||
|
startDate: z.coerce.date(),
|
||||||
|
endDate: z.coerce.date(),
|
||||||
|
hoursPerDay: z.number().min(0.5).max(24).default(8),
|
||||||
|
}))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const { vacations: _vacations, ...availability } = await buildResourceAvailabilityView(ctx.db, input);
|
||||||
|
return availability;
|
||||||
|
}),
|
||||||
|
|
||||||
|
getResourceAvailabilityView: planningReadProcedure
|
||||||
|
.input(z.object({
|
||||||
|
resourceId: z.string(),
|
||||||
|
startDate: z.coerce.date(),
|
||||||
|
endDate: z.coerce.date(),
|
||||||
|
hoursPerDay: z.number().min(0.5).max(24).default(8),
|
||||||
|
}))
|
||||||
|
.query(async ({ ctx, input }) => buildResourceAvailabilityView(ctx.db, input)),
|
||||||
|
|
||||||
|
getResourceAvailabilitySummary: planningReadProcedure
|
||||||
|
.input(z.object({
|
||||||
|
resourceId: z.string(),
|
||||||
|
startDate: z.coerce.date(),
|
||||||
|
endDate: z.coerce.date(),
|
||||||
|
hoursPerDay: z.number().min(0.5).max(24).default(8),
|
||||||
|
}))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const availability = await buildResourceAvailabilityView(ctx.db, input);
|
||||||
|
return buildResourceAvailabilitySummary(availability, input);
|
||||||
|
}),
|
||||||
|
};
|
||||||
@@ -26,9 +26,7 @@ import {
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||||
import { anonymizeResource, getAnonymizationDirectory } from "../lib/anonymization.js";
|
|
||||||
import { emitAllocationCreated, emitAllocationDeleted, emitAllocationUpdated } from "../sse/event-bus.js";
|
import { emitAllocationCreated, emitAllocationDeleted, emitAllocationUpdated } from "../sse/event-bus.js";
|
||||||
import { buildResourceAvailabilitySummary, buildResourceAvailabilityView } from "./allocation-availability.js";
|
|
||||||
import {
|
import {
|
||||||
checkBudgetThresholdsInBackground,
|
checkBudgetThresholdsInBackground,
|
||||||
createDemandRequirementWithEffects,
|
createDemandRequirementWithEffects,
|
||||||
@@ -36,45 +34,19 @@ import {
|
|||||||
fillDemandRequirementWithEffects,
|
fillDemandRequirementWithEffects,
|
||||||
invalidateDashboardCacheInBackground,
|
invalidateDashboardCacheInBackground,
|
||||||
} from "./allocation-effects.js";
|
} from "./allocation-effects.js";
|
||||||
import {
|
import { allocationReadProcedures } from "./allocation-read-procedures.js";
|
||||||
ASSIGNMENT_INCLUDE,
|
import { ASSIGNMENT_INCLUDE, DEMAND_INCLUDE, toIsoDate } from "./allocation-shared.js";
|
||||||
DEMAND_INCLUDE,
|
|
||||||
toIsoDate,
|
|
||||||
} from "./allocation-shared.js";
|
|
||||||
import {
|
import {
|
||||||
buildCreateDemandRequirementInput,
|
buildCreateDemandRequirementInput,
|
||||||
findAllocationEntryOrNull,
|
findAllocationEntryOrNull,
|
||||||
getDemandRequirementByIdOrThrow,
|
getDemandRequirementByIdOrThrow,
|
||||||
loadAllocationReadModel,
|
|
||||||
resolveAssignmentBySelection,
|
|
||||||
toAssignmentUpdateInput,
|
toAssignmentUpdateInput,
|
||||||
toDemandRequirementUpdateInput,
|
toDemandRequirementUpdateInput,
|
||||||
} from "./allocation-support.js";
|
} from "./allocation-support.js";
|
||||||
import { createTRPCRouter, managerProcedure, planningReadProcedure, requirePermission } from "../trpc.js";
|
import { createTRPCRouter, managerProcedure, planningReadProcedure, requirePermission } from "../trpc.js";
|
||||||
|
|
||||||
export const allocationRouter = createTRPCRouter({
|
export const allocationRouter = createTRPCRouter({
|
||||||
list: planningReadProcedure
|
...allocationReadProcedures,
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
projectId: z.string().optional(),
|
|
||||||
resourceId: z.string().optional(),
|
|
||||||
status: z.nativeEnum(AllocationStatus).optional(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const readModel = await loadAllocationReadModel(ctx.db, input);
|
|
||||||
return readModel.allocations;
|
|
||||||
}),
|
|
||||||
|
|
||||||
listView: planningReadProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
projectId: z.string().optional(),
|
|
||||||
resourceId: z.string().optional(),
|
|
||||||
status: z.nativeEnum(AllocationStatus).optional(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.query(async ({ ctx, input }) => loadAllocationReadModel(ctx.db, input)),
|
|
||||||
|
|
||||||
create: managerProcedure
|
create: managerProcedure
|
||||||
.input(CreateAllocationSchema)
|
.input(CreateAllocationSchema)
|
||||||
@@ -141,134 +113,6 @@ export const allocationRouter = createTRPCRouter({
|
|||||||
return allocation;
|
return allocation;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
listDemands: planningReadProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
projectId: z.string().optional(),
|
|
||||||
status: z.nativeEnum(AllocationStatus).optional(),
|
|
||||||
roleId: z.string().optional(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const demands = await ctx.db.demandRequirement.findMany({
|
|
||||||
where: {
|
|
||||||
...(input.projectId ? { projectId: input.projectId } : {}),
|
|
||||||
...(input.status ? { status: input.status } : {}),
|
|
||||||
...(input.roleId ? { roleId: input.roleId } : {}),
|
|
||||||
},
|
|
||||||
include: DEMAND_INCLUDE,
|
|
||||||
orderBy: { startDate: "asc" },
|
|
||||||
});
|
|
||||||
const dir = await getAnonymizationDirectory(ctx.db);
|
|
||||||
if (!dir) return demands;
|
|
||||||
return demands.map((d) => ({
|
|
||||||
...d,
|
|
||||||
assignments: d.assignments.map((a) =>
|
|
||||||
a.resource ? { ...a, resource: anonymizeResource(a.resource, dir) } : a,
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
}),
|
|
||||||
|
|
||||||
listAssignments: planningReadProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
projectId: z.string().optional(),
|
|
||||||
resourceId: z.string().optional(),
|
|
||||||
status: z.nativeEnum(AllocationStatus).optional(),
|
|
||||||
demandRequirementId: z.string().optional(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const assignments = await ctx.db.assignment.findMany({
|
|
||||||
where: {
|
|
||||||
...(input.projectId ? { projectId: input.projectId } : {}),
|
|
||||||
...(input.resourceId ? { resourceId: input.resourceId } : {}),
|
|
||||||
...(input.status ? { status: input.status } : {}),
|
|
||||||
...(input.demandRequirementId ? { demandRequirementId: input.demandRequirementId } : {}),
|
|
||||||
},
|
|
||||||
include: ASSIGNMENT_INCLUDE,
|
|
||||||
orderBy: { startDate: "asc" },
|
|
||||||
});
|
|
||||||
const dir = await getAnonymizationDirectory(ctx.db);
|
|
||||||
if (!dir) return assignments;
|
|
||||||
return assignments.map((a) =>
|
|
||||||
a.resource ? { ...a, resource: anonymizeResource(a.resource, dir) } : a,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
|
|
||||||
getAssignmentById: planningReadProcedure
|
|
||||||
.input(z.object({ id: z.string() }))
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const assignment = await findUniqueOrThrow(
|
|
||||||
ctx.db.assignment.findUnique({
|
|
||||||
where: { id: input.id },
|
|
||||||
include: ASSIGNMENT_INCLUDE,
|
|
||||||
}),
|
|
||||||
"Assignment",
|
|
||||||
);
|
|
||||||
const dir = await getAnonymizationDirectory(ctx.db);
|
|
||||||
if (!dir || !assignment.resource) {
|
|
||||||
return assignment;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...assignment,
|
|
||||||
resource: anonymizeResource(assignment.resource, dir),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
|
|
||||||
resolveAssignment: planningReadProcedure
|
|
||||||
.input(z.object({
|
|
||||||
assignmentId: z.string().optional(),
|
|
||||||
resourceId: z.string().optional(),
|
|
||||||
projectId: z.string().optional(),
|
|
||||||
startDate: z.coerce.date().optional(),
|
|
||||||
endDate: z.coerce.date().optional(),
|
|
||||||
selectionMode: z.enum(["WINDOW", "EXACT_START"]).default("EXACT_START"),
|
|
||||||
excludeCancelled: z.boolean().default(false),
|
|
||||||
}))
|
|
||||||
.query(async ({ ctx, input }) => resolveAssignmentBySelection(ctx.db, input)),
|
|
||||||
|
|
||||||
getDemandRequirementById: planningReadProcedure
|
|
||||||
.input(z.object({ id: z.string() }))
|
|
||||||
.query(async ({ ctx, input }) => getDemandRequirementByIdOrThrow(ctx.db, input.id)),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check a resource's availability for a date range.
|
|
||||||
* Returns working days, existing allocations, conflict days, and available capacity.
|
|
||||||
*/
|
|
||||||
checkResourceAvailability: planningReadProcedure
|
|
||||||
.input(z.object({
|
|
||||||
resourceId: z.string(),
|
|
||||||
startDate: z.coerce.date(),
|
|
||||||
endDate: z.coerce.date(),
|
|
||||||
hoursPerDay: z.number().min(0.5).max(24).default(8),
|
|
||||||
}))
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const { vacations: _vacations, ...availability } = await buildResourceAvailabilityView(ctx.db, input);
|
|
||||||
return availability;
|
|
||||||
}),
|
|
||||||
|
|
||||||
getResourceAvailabilityView: planningReadProcedure
|
|
||||||
.input(z.object({
|
|
||||||
resourceId: z.string(),
|
|
||||||
startDate: z.coerce.date(),
|
|
||||||
endDate: z.coerce.date(),
|
|
||||||
hoursPerDay: z.number().min(0.5).max(24).default(8),
|
|
||||||
}))
|
|
||||||
.query(async ({ ctx, input }) => buildResourceAvailabilityView(ctx.db, input)),
|
|
||||||
|
|
||||||
getResourceAvailabilitySummary: planningReadProcedure
|
|
||||||
.input(z.object({
|
|
||||||
resourceId: z.string(),
|
|
||||||
startDate: z.coerce.date(),
|
|
||||||
endDate: z.coerce.date(),
|
|
||||||
hoursPerDay: z.number().min(0.5).max(24).default(8),
|
|
||||||
}))
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const availability = await buildResourceAvailabilityView(ctx.db, input);
|
|
||||||
return buildResourceAvailabilitySummary(availability, input);
|
|
||||||
}),
|
|
||||||
|
|
||||||
createDemandRequirement: managerProcedure
|
createDemandRequirement: managerProcedure
|
||||||
.input(CreateDemandRequirementSchema)
|
.input(CreateDemandRequirementSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user