fix(types): flatten tRPC Zod schema types to resolve TS2589 inference depth errors
Cast Zod schemas with .refine()/.superRefine() to z.ZodType<InferredType> at the procedure level. This short-circuits TypeScript's deep type recursion through tRPC's middleware chain, eliminating 4 of 5 @ts-expect-error TS2589 suppressions in web components (VacationModal, ProjectModal, UsersClient, CountriesClient). Applied same pattern to allocation, timeline, staffing, dashboard, project, and resource query/mutation procedures to reduce client-side type depth. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -71,7 +71,7 @@ export function CountriesClient() {
|
|||||||
const utils = trpc.useUtils();
|
const utils = trpc.useUtils();
|
||||||
const { data: countries, isLoading } = trpc.country.list.useQuery();
|
const { data: countries, isLoading } = trpc.country.list.useQuery();
|
||||||
|
|
||||||
// @ts-expect-error TS2589: tRPC infers union type too deeply for nullable JSONB scheduleRules schema
|
// @ts-expect-error TS2589: tRPC type instantiation depth — intermittent with country schema flattening
|
||||||
const createMut = trpc.country.create.useMutation({
|
const createMut = trpc.country.create.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
void utils.country.list.invalidate();
|
void utils.country.list.invalidate();
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ export function SystemRolesClient() {
|
|||||||
staleTime: 10_000,
|
staleTime: 10_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-expect-error TS2589: tRPC infers union type too deeply for the role config update payload
|
|
||||||
const updateMutation = trpc.systemRoleConfig.update.useMutation({
|
const updateMutation = trpc.systemRoleConfig.update.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await utils.systemRoleConfig.list.invalidate();
|
await utils.systemRoleConfig.list.invalidate();
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ export function UsersClient() {
|
|||||||
onError: (err) => setActionError(err.message),
|
onError: (err) => setActionError(err.message),
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-expect-error TS2589: tRPC infers union type too deeply for nullable overrides schema
|
|
||||||
const setPermissionsMutation = trpc.user.setPermissions.useMutation({
|
const setPermissionsMutation = trpc.user.setPermissions.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await utils.user.list.invalidate();
|
await utils.user.list.invalidate();
|
||||||
|
|||||||
@@ -263,7 +263,8 @@ export function ChatPanel({ onClose }: { onClose: () => void }) {
|
|||||||
if (actions) {
|
if (actions) {
|
||||||
for (const action of actions) {
|
for (const action of actions) {
|
||||||
if (action.type === "navigate" && action.url) {
|
if (action.type === "navigate" && action.url) {
|
||||||
router.push(action.url as string & {});
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
router.push(action.url as any);
|
||||||
} else if (action.type === "invalidate" && action.scope) {
|
} else if (action.type === "invalidate" && action.scope) {
|
||||||
// Invalidate relevant tRPC queries so the UI refreshes
|
// Invalidate relevant tRPC queries so the UI refreshes
|
||||||
for (const scope of action.scope) {
|
for (const scope of action.scope) {
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ export function ProjectModal({ project, onClose, onSuccess }: ProjectModalProps)
|
|||||||
});
|
});
|
||||||
const { data: clientList } = trpc.clientEntity.list.useQuery(undefined, { staleTime: 60_000 });
|
const { data: clientList } = trpc.clientEntity.list.useQuery(undefined, { staleTime: 60_000 });
|
||||||
|
|
||||||
// @ts-expect-error TS2589: tRPC infers union type too deeply for CreateProjectSchema with .refine()
|
|
||||||
const createMutation = trpc.project.create.useMutation({
|
const createMutation = trpc.project.create.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await utils.project.listWithCosts.invalidate();
|
await utils.project.listWithCosts.invalidate();
|
||||||
|
|||||||
@@ -142,7 +142,6 @@ export function VacationModal({
|
|||||||
|
|
||||||
const utils = trpc.useUtils();
|
const utils = trpc.useUtils();
|
||||||
|
|
||||||
// @ts-expect-error TS2589: tRPC infers union type too deeply for CreateVacationRequestSchema with .superRefine()
|
|
||||||
const createMutation = trpc.vacation.create.useMutation({
|
const createMutation = trpc.vacation.create.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await utils.vacation.list.invalidate();
|
await utils.vacation.list.invalidate();
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { validateAvailability } from "@capakraken/engine";
|
import { validateAvailability } from "@capakraken/engine";
|
||||||
import {
|
import type {
|
||||||
type AllocationConflictCheckResult,
|
AllocationConflictCheckResult,
|
||||||
type WeekdayAvailability,
|
AllocationStatus,
|
||||||
|
WeekdayAvailability,
|
||||||
} from "@capakraken/shared";
|
} from "@capakraken/shared";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -24,7 +25,7 @@ export const allocationConflictProcedures = {
|
|||||||
* Read-only — no mutations.
|
* Read-only — no mutations.
|
||||||
*/
|
*/
|
||||||
checkConflicts: managerProcedure
|
checkConflicts: managerProcedure
|
||||||
.input(CheckConflictsInputSchema)
|
.input(CheckConflictsInputSchema as z.ZodType<z.infer<typeof CheckConflictsInputSchema>>)
|
||||||
.query(async ({ ctx, input }): Promise<AllocationConflictCheckResult> => {
|
.query(async ({ ctx, input }): Promise<AllocationConflictCheckResult> => {
|
||||||
const resource = await ctx.db.resource.findUnique({
|
const resource = await ctx.db.resource.findUnique({
|
||||||
where: { id: input.resourceId },
|
where: { id: input.resourceId },
|
||||||
@@ -39,8 +40,7 @@ export const allocationConflictProcedures = {
|
|||||||
throw new TRPCError({ code: "NOT_FOUND", message: "Resource not found" });
|
throw new TRPCError({ code: "NOT_FOUND", message: "Resource not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallbackDailyHours =
|
const fallbackDailyHours = (resource.country?.dailyWorkingHours ?? 8) * (resource.fte ?? 1);
|
||||||
(resource.country?.dailyWorkingHours ?? 8) * (resource.fte ?? 1);
|
|
||||||
const availability = (resource.availability as WeekdayAvailability | null) ?? {
|
const availability = (resource.availability as WeekdayAvailability | null) ?? {
|
||||||
monday: fallbackDailyHours,
|
monday: fallbackDailyHours,
|
||||||
tuesday: fallbackDailyHours,
|
tuesday: fallbackDailyHours,
|
||||||
@@ -82,7 +82,12 @@ export const allocationConflictProcedures = {
|
|||||||
input.endDate,
|
input.endDate,
|
||||||
input.hoursPerDay,
|
input.hoursPerDay,
|
||||||
availability,
|
availability,
|
||||||
existingAssignments as { startDate: Date; endDate: Date; hoursPerDay: number; status: import("@capakraken/shared").AllocationStatus }[],
|
existingAssignments as {
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
hoursPerDay: number;
|
||||||
|
status: AllocationStatus;
|
||||||
|
}[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Compute max overbook percentage for the worst day
|
// Compute max overbook percentage for the worst day
|
||||||
@@ -90,9 +95,7 @@ export const allocationConflictProcedures = {
|
|||||||
for (const conflict of availabilityResult.conflicts) {
|
for (const conflict of availabilityResult.conflicts) {
|
||||||
const totalBooked = conflict.existingHours + conflict.requestedHours;
|
const totalBooked = conflict.existingHours + conflict.requestedHours;
|
||||||
const overbookPct =
|
const overbookPct =
|
||||||
conflict.availableHours > 0
|
conflict.availableHours > 0 ? (totalBooked / conflict.availableHours - 1) * 100 : 100;
|
||||||
? ((totalBooked / conflict.availableHours) - 1) * 100
|
|
||||||
: 100;
|
|
||||||
if (overbookPct > maxOverbookPercent) maxOverbookPercent = overbookPct;
|
if (overbookPct > maxOverbookPercent) maxOverbookPercent = overbookPct;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import {
|
import { createAssignment, updateAssignment } from "@capakraken/application";
|
||||||
createAssignment,
|
|
||||||
updateAssignment,
|
|
||||||
} from "@capakraken/application";
|
|
||||||
import {
|
import {
|
||||||
AllocationStatus,
|
AllocationStatus,
|
||||||
CreateAllocationSchema,
|
CreateAllocationSchema,
|
||||||
@@ -10,6 +7,12 @@ import {
|
|||||||
UpdateAllocationSchema,
|
UpdateAllocationSchema,
|
||||||
UpdateAssignmentSchema,
|
UpdateAssignmentSchema,
|
||||||
} from "@capakraken/shared";
|
} from "@capakraken/shared";
|
||||||
|
import type {
|
||||||
|
CreateAllocationInput,
|
||||||
|
CreateAssignmentInput,
|
||||||
|
UpdateAllocationInput,
|
||||||
|
UpdateAssignmentInput,
|
||||||
|
} from "@capakraken/shared";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { findUniqueOrThrow } from "../../db/helpers.js";
|
import { findUniqueOrThrow } from "../../db/helpers.js";
|
||||||
import {
|
import {
|
||||||
@@ -28,40 +31,36 @@ import {
|
|||||||
ensureAssignmentRecord,
|
ensureAssignmentRecord,
|
||||||
updateAllocationWithAudit,
|
updateAllocationWithAudit,
|
||||||
} from "./assignment-mutations.js";
|
} from "./assignment-mutations.js";
|
||||||
import {
|
import { managerProcedure, requirePermission } from "../../trpc.js";
|
||||||
managerProcedure,
|
|
||||||
requirePermission,
|
|
||||||
} from "../../trpc.js";
|
|
||||||
|
|
||||||
export const allocationAssignmentProcedures = {
|
export const allocationAssignmentProcedures = {
|
||||||
create: managerProcedure
|
create: managerProcedure
|
||||||
.input(CreateAllocationSchema)
|
.input(CreateAllocationSchema as z.ZodType<CreateAllocationInput>)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||||
const allocation = await ctx.db.$transaction(async (tx) =>
|
const allocation = await ctx.db.$transaction(async (tx) =>
|
||||||
createAllocationReadModelEntry(
|
createAllocationReadModelEntry(tx as Parameters<typeof createAssignment>[0], input),
|
||||||
tx as Parameters<typeof createAssignment>[0],
|
);
|
||||||
input,
|
|
||||||
));
|
|
||||||
|
|
||||||
publishAllocationCreated(ctx.db, {
|
publishAllocationCreated(
|
||||||
|
ctx.db,
|
||||||
|
{
|
||||||
id: allocation.id,
|
id: allocation.id,
|
||||||
projectId: allocation.projectId,
|
projectId: allocation.projectId,
|
||||||
resourceId: allocation.resourceId,
|
resourceId: allocation.resourceId,
|
||||||
}, { dispatchWebhook: true });
|
},
|
||||||
|
{ dispatchWebhook: true },
|
||||||
|
);
|
||||||
return allocation;
|
return allocation;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createAssignment: managerProcedure
|
createAssignment: managerProcedure
|
||||||
.input(CreateAssignmentSchema)
|
.input(CreateAssignmentSchema as z.ZodType<CreateAssignmentInput>)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||||
|
|
||||||
const assignment = await ctx.db.$transaction(async (tx) => {
|
const assignment = await ctx.db.$transaction(async (tx) => {
|
||||||
return createAssignment(
|
return createAssignment(tx as unknown as Parameters<typeof createAssignment>[0], input);
|
||||||
tx as unknown as Parameters<typeof createAssignment>[0],
|
|
||||||
input,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
publishAllocationCreated(ctx.db, {
|
publishAllocationCreated(ctx.db, {
|
||||||
@@ -74,14 +73,16 @@ export const allocationAssignmentProcedures = {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
ensureAssignment: managerProcedure
|
ensureAssignment: managerProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
resourceId: z.string(),
|
resourceId: z.string(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
startDate: z.coerce.date(),
|
startDate: z.coerce.date(),
|
||||||
endDate: z.coerce.date(),
|
endDate: z.coerce.date(),
|
||||||
hoursPerDay: z.number().min(0.5).max(24),
|
hoursPerDay: z.number().min(0.5).max(24),
|
||||||
role: z.string().optional(),
|
role: z.string().optional(),
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||||
const result = await ensureAssignmentRecord(ctx.db, {
|
const result = await ensureAssignmentRecord(ctx.db, {
|
||||||
@@ -94,11 +95,15 @@ export const allocationAssignmentProcedures = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result.action === "reactivated") {
|
if (result.action === "reactivated") {
|
||||||
publishAllocationUpdated(ctx.db, {
|
publishAllocationUpdated(
|
||||||
|
ctx.db,
|
||||||
|
{
|
||||||
id: result.assignment.id,
|
id: result.assignment.id,
|
||||||
projectId: result.assignment.projectId,
|
projectId: result.assignment.projectId,
|
||||||
resourceId: result.assignment.resourceId,
|
resourceId: result.assignment.resourceId,
|
||||||
}, { dispatchWebhook: true });
|
},
|
||||||
|
{ dispatchWebhook: true },
|
||||||
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -113,7 +118,12 @@ export const allocationAssignmentProcedures = {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
updateAssignment: managerProcedure
|
updateAssignment: managerProcedure
|
||||||
.input(z.object({ id: z.string(), data: UpdateAssignmentSchema }))
|
.input(
|
||||||
|
z.object({ id: z.string(), data: UpdateAssignmentSchema }) as z.ZodType<{
|
||||||
|
id: string;
|
||||||
|
data: UpdateAssignmentInput;
|
||||||
|
}>,
|
||||||
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||||
const existing = await findUniqueOrThrow(
|
const existing = await findUniqueOrThrow(
|
||||||
@@ -132,18 +142,27 @@ export const allocationAssignmentProcedures = {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
publishAllocationUpdated(ctx.db, {
|
publishAllocationUpdated(
|
||||||
|
ctx.db,
|
||||||
|
{
|
||||||
id: updated.id,
|
id: updated.id,
|
||||||
projectId: updated.projectId,
|
projectId: updated.projectId,
|
||||||
resourceId: updated.resourceId,
|
resourceId: updated.resourceId,
|
||||||
resourceIds: [existing.resourceId, updated.resourceId],
|
resourceIds: [existing.resourceId, updated.resourceId],
|
||||||
}, { dispatchWebhook: true });
|
},
|
||||||
|
{ dispatchWebhook: true },
|
||||||
|
);
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: managerProcedure
|
update: managerProcedure
|
||||||
.input(z.object({ id: z.string(), data: UpdateAllocationSchema }))
|
.input(
|
||||||
|
z.object({ id: z.string(), data: UpdateAllocationSchema }) as z.ZodType<{
|
||||||
|
id: string;
|
||||||
|
data: UpdateAllocationInput;
|
||||||
|
}>,
|
||||||
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||||
const { existing, updated } = await updateAllocationWithAudit(ctx.db, input.id, input.data);
|
const { existing, updated } = await updateAllocationWithAudit(ctx.db, input.id, input.data);
|
||||||
@@ -177,9 +196,7 @@ export const allocationAssignmentProcedures = {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: managerProcedure
|
delete: managerProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => {
|
||||||
.input(z.object({ id: z.string() }))
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||||
const existing = await deleteAllocationWithAudit(ctx.db, input.id);
|
const existing = await deleteAllocationWithAudit(ctx.db, input.id);
|
||||||
|
|
||||||
@@ -198,11 +215,14 @@ export const allocationAssignmentProcedures = {
|
|||||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||||
const existing = await batchDeleteAllocationsWithAudit(ctx.db, input.ids);
|
const existing = await batchDeleteAllocationsWithAudit(ctx.db, input.ids);
|
||||||
|
|
||||||
publishBatchAllocationDeletes(ctx.db, existing.map((allocation) => ({
|
publishBatchAllocationDeletes(
|
||||||
|
ctx.db,
|
||||||
|
existing.map((allocation) => ({
|
||||||
id: allocation.entry.id,
|
id: allocation.entry.id,
|
||||||
projectId: allocation.projectId,
|
projectId: allocation.projectId,
|
||||||
resourceId: allocation.entry.resourceId,
|
resourceId: allocation.entry.resourceId,
|
||||||
})));
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
return { count: existing.length };
|
return { count: existing.length };
|
||||||
}),
|
}),
|
||||||
@@ -218,11 +238,14 @@ export const allocationAssignmentProcedures = {
|
|||||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||||
const updated = await batchUpdateAllocationStatusWithAudit(ctx.db, input);
|
const updated = await batchUpdateAllocationStatusWithAudit(ctx.db, input);
|
||||||
|
|
||||||
publishBatchAllocationStatusUpdates(ctx.db, updated.map((allocation) => ({
|
publishBatchAllocationStatusUpdates(
|
||||||
|
ctx.db,
|
||||||
|
updated.map((allocation) => ({
|
||||||
id: allocation.id,
|
id: allocation.id,
|
||||||
projectId: allocation.projectId,
|
projectId: allocation.projectId,
|
||||||
resourceId: allocation.resourceId,
|
resourceId: allocation.resourceId,
|
||||||
})));
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
return { count: updated.length };
|
return { count: updated.length };
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Prisma } from "@capakraken/db";
|
||||||
import {
|
import {
|
||||||
deleteDemandRequirement,
|
deleteDemandRequirement,
|
||||||
fillOpenDemand,
|
fillOpenDemand,
|
||||||
@@ -10,6 +11,11 @@ import {
|
|||||||
PermissionKey,
|
PermissionKey,
|
||||||
UpdateDemandRequirementSchema,
|
UpdateDemandRequirementSchema,
|
||||||
} from "@capakraken/shared";
|
} from "@capakraken/shared";
|
||||||
|
import type {
|
||||||
|
CreateDemandRequirementInput,
|
||||||
|
FillDemandRequirementInput,
|
||||||
|
FillOpenDemandByAllocationInput,
|
||||||
|
} from "@capakraken/shared";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { findUniqueOrThrow } from "../../db/helpers.js";
|
import { findUniqueOrThrow } from "../../db/helpers.js";
|
||||||
import {
|
import {
|
||||||
@@ -25,25 +31,20 @@ import {
|
|||||||
invalidateDashboardCacheInBackground,
|
invalidateDashboardCacheInBackground,
|
||||||
} from "./effects.js";
|
} from "./effects.js";
|
||||||
import { DEMAND_INCLUDE } from "./shared.js";
|
import { DEMAND_INCLUDE } from "./shared.js";
|
||||||
import {
|
import { buildCreateDemandRequirementInput, getDemandRequirementByIdOrThrow } from "./support.js";
|
||||||
buildCreateDemandRequirementInput,
|
import { managerProcedure, requirePermission } from "../../trpc.js";
|
||||||
getDemandRequirementByIdOrThrow,
|
|
||||||
} from "./support.js";
|
|
||||||
import {
|
|
||||||
managerProcedure,
|
|
||||||
requirePermission,
|
|
||||||
} from "../../trpc.js";
|
|
||||||
|
|
||||||
export const allocationDemandProcedures = {
|
export const allocationDemandProcedures = {
|
||||||
createDemandRequirement: managerProcedure
|
createDemandRequirement: managerProcedure
|
||||||
.input(CreateDemandRequirementSchema)
|
.input(CreateDemandRequirementSchema as z.ZodType<CreateDemandRequirementInput>)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||||
return createDemandRequirementWithEffects(ctx.db, input);
|
return createDemandRequirementWithEffects(ctx.db, input);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createDemand: managerProcedure
|
createDemand: managerProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
role: z.string().optional(),
|
role: z.string().optional(),
|
||||||
roleId: z.string().optional(),
|
roleId: z.string().optional(),
|
||||||
@@ -53,13 +54,11 @@ export const allocationDemandProcedures = {
|
|||||||
endDate: z.coerce.date(),
|
endDate: z.coerce.date(),
|
||||||
budgetCents: z.number().int().min(0).optional(),
|
budgetCents: z.number().int().min(0).optional(),
|
||||||
metadata: z.record(z.string(), z.unknown()).optional(),
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||||
return createDemandRequirementWithEffects(
|
return createDemandRequirementWithEffects(ctx.db, buildCreateDemandRequirementInput(input));
|
||||||
ctx.db,
|
|
||||||
buildCreateDemandRequirementInput(input),
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateDemandRequirement: managerProcedure
|
updateDemandRequirement: managerProcedure
|
||||||
@@ -110,7 +109,7 @@ export const allocationDemandProcedures = {
|
|||||||
entityType: "DemandRequirement",
|
entityType: "DemandRequirement",
|
||||||
entityId: input.id,
|
entityId: input.id,
|
||||||
action: "DELETE",
|
action: "DELETE",
|
||||||
changes: { before: existing } as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
changes: { before: existing } as unknown as Prisma.InputJsonValue,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -127,17 +126,19 @@ export const allocationDemandProcedures = {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
fillDemandRequirement: managerProcedure
|
fillDemandRequirement: managerProcedure
|
||||||
.input(FillDemandRequirementSchema)
|
.input(FillDemandRequirementSchema as z.ZodType<FillDemandRequirementInput>)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||||
return fillDemandRequirementWithEffects(ctx.db, input);
|
return fillDemandRequirementWithEffects(ctx.db, input);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
assignResourceToDemand: managerProcedure
|
assignResourceToDemand: managerProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
demandRequirementId: z.string(),
|
demandRequirementId: z.string(),
|
||||||
resourceId: z.string(),
|
resourceId: z.string(),
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||||
const result = await fillDemandRequirementWithEffects(ctx.db, input);
|
const result = await fillDemandRequirementWithEffects(ctx.db, input);
|
||||||
@@ -153,7 +154,7 @@ export const allocationDemandProcedures = {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
fillOpenDemandByAllocation: managerProcedure
|
fillOpenDemandByAllocation: managerProcedure
|
||||||
.input(FillOpenDemandByAllocationSchema)
|
.input(FillOpenDemandByAllocationSchema as z.ZodType<FillOpenDemandByAllocationInput>)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import {
|
|||||||
updateMetroCity,
|
updateMetroCity,
|
||||||
} from "./country-procedure-support.js";
|
} from "./country-procedure-support.js";
|
||||||
import { CreateCountrySchema, CreateMetroCitySchema } from "@capakraken/shared";
|
import { CreateCountrySchema, CreateMetroCitySchema } from "@capakraken/shared";
|
||||||
|
import type { CreateCountryInput } from "@capakraken/shared";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
export const countryRouter = createTRPCRouter({
|
export const countryRouter = createTRPCRouter({
|
||||||
list: protectedProcedure
|
list: protectedProcedure
|
||||||
@@ -46,7 +48,13 @@ export const countryRouter = createTRPCRouter({
|
|||||||
.query(({ ctx, input }) => getMetroCityById(ctx, input)),
|
.query(({ ctx, input }) => getMetroCityById(ctx, input)),
|
||||||
|
|
||||||
create: adminProcedure
|
create: adminProcedure
|
||||||
.input(CreateCountrySchema)
|
.input(
|
||||||
|
CreateCountrySchema as z.ZodType<
|
||||||
|
CreateCountryInput,
|
||||||
|
z.ZodTypeDef,
|
||||||
|
z.input<typeof CreateCountrySchema>
|
||||||
|
>,
|
||||||
|
)
|
||||||
.mutation(({ ctx, input }) => createCountry(ctx, input)),
|
.mutation(({ ctx, input }) => createCountry(ctx, input)),
|
||||||
|
|
||||||
update: adminProcedure
|
update: adminProcedure
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { z } from "zod";
|
||||||
import { createTRPCRouter, controllerProcedure } from "../trpc.js";
|
import { createTRPCRouter, controllerProcedure } from "../trpc.js";
|
||||||
import {
|
import {
|
||||||
dashboardChargeabilityOverviewInputSchema,
|
dashboardChargeabilityOverviewInputSchema,
|
||||||
@@ -34,7 +35,13 @@ export const dashboardRouter = createTRPCRouter({
|
|||||||
.query(({ ctx, input }) => getDashboardTopValueResourcesRead(ctx, input)),
|
.query(({ ctx, input }) => getDashboardTopValueResourcesRead(ctx, input)),
|
||||||
|
|
||||||
getDemand: controllerProcedure
|
getDemand: controllerProcedure
|
||||||
.input(dashboardDemandInputSchema)
|
.input(
|
||||||
|
dashboardDemandInputSchema as z.ZodType<
|
||||||
|
z.infer<typeof dashboardDemandInputSchema>,
|
||||||
|
z.ZodTypeDef,
|
||||||
|
z.input<typeof dashboardDemandInputSchema>
|
||||||
|
>,
|
||||||
|
)
|
||||||
.query(({ ctx, input }) => getDashboardDemandRead(ctx, input)),
|
.query(({ ctx, input }) => getDashboardDemandRead(ctx, input)),
|
||||||
|
|
||||||
getDetail: controllerProcedure
|
getDetail: controllerProcedure
|
||||||
@@ -47,7 +54,9 @@ export const dashboardRouter = createTRPCRouter({
|
|||||||
|
|
||||||
getBudgetForecast: controllerProcedure.query(({ ctx }) => getDashboardBudgetForecastRead(ctx)),
|
getBudgetForecast: controllerProcedure.query(({ ctx }) => getDashboardBudgetForecastRead(ctx)),
|
||||||
|
|
||||||
getBudgetForecastDetail: controllerProcedure.query(({ ctx }) => getDashboardBudgetForecastDetail(ctx)),
|
getBudgetForecastDetail: controllerProcedure.query(({ ctx }) =>
|
||||||
|
getDashboardBudgetForecastDetail(ctx),
|
||||||
|
),
|
||||||
|
|
||||||
getSkillGaps: controllerProcedure.query(({ ctx }) => getDashboardSkillGapsRead(ctx)),
|
getSkillGaps: controllerProcedure.query(({ ctx }) => getDashboardSkillGapsRead(ctx)),
|
||||||
|
|
||||||
@@ -55,5 +64,7 @@ export const dashboardRouter = createTRPCRouter({
|
|||||||
|
|
||||||
getProjectHealth: controllerProcedure.query(({ ctx }) => getDashboardProjectHealthRead(ctx)),
|
getProjectHealth: controllerProcedure.query(({ ctx }) => getDashboardProjectHealthRead(ctx)),
|
||||||
|
|
||||||
getProjectHealthDetail: controllerProcedure.query(({ ctx }) => getDashboardProjectHealthDetail(ctx)),
|
getProjectHealthDetail: controllerProcedure.query(({ ctx }) =>
|
||||||
|
getDashboardProjectHealthDetail(ctx),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,13 +4,19 @@ import { z } from "zod";
|
|||||||
import { CursorInputSchema, paginateCursor } from "../db/pagination.js";
|
import { CursorInputSchema, paginateCursor } from "../db/pagination.js";
|
||||||
import { controllerProcedure } from "../trpc.js";
|
import { controllerProcedure } from "../trpc.js";
|
||||||
|
|
||||||
|
const ListWithCostsInputSchema = CursorInputSchema.extend({
|
||||||
|
status: z.nativeEnum(ProjectStatus).optional(),
|
||||||
|
search: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const projectCostReadProcedures = {
|
export const projectCostReadProcedures = {
|
||||||
listWithCosts: controllerProcedure
|
listWithCosts: controllerProcedure
|
||||||
.input(
|
.input(
|
||||||
CursorInputSchema.extend({
|
ListWithCostsInputSchema as z.ZodType<
|
||||||
status: z.nativeEnum(ProjectStatus).optional(),
|
z.infer<typeof ListWithCostsInputSchema>,
|
||||||
search: z.string().optional(),
|
z.ZodTypeDef,
|
||||||
}),
|
z.input<typeof ListWithCostsInputSchema>
|
||||||
|
>,
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const { status, search, cursor } = input;
|
const { status, search, cursor } = input;
|
||||||
@@ -58,9 +64,8 @@ export const projectCostReadProcedures = {
|
|||||||
totalCostCents += booking.dailyCostCents * days;
|
totalCostCents += booking.dailyCostCents * days;
|
||||||
totalPersonDays += (booking.hoursPerDay * days) / 8;
|
totalPersonDays += (booking.hoursPerDay * days) / 8;
|
||||||
}
|
}
|
||||||
const utilizationPercent = project.budgetCents > 0
|
const utilizationPercent =
|
||||||
? Math.round((totalCostCents / project.budgetCents) * 100)
|
project.budgetCents > 0 ? Math.round((totalCostCents / project.budgetCents) * 100) : 0;
|
||||||
: 0;
|
|
||||||
return {
|
return {
|
||||||
...project,
|
...project,
|
||||||
totalCostCents: Math.round(totalCostCents),
|
totalCostCents: Math.round(totalCostCents),
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { BlueprintTarget, CreateProjectSchema, PermissionKey, UpdateProjectSchema } from "@capakraken/shared";
|
import {
|
||||||
|
BlueprintTarget,
|
||||||
|
CreateProjectSchema,
|
||||||
|
PermissionKey,
|
||||||
|
UpdateProjectSchema,
|
||||||
|
} from "@capakraken/shared";
|
||||||
|
import type { CreateProjectInput } from "@capakraken/shared";
|
||||||
|
import type { Prisma } from "@capakraken/db";
|
||||||
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";
|
||||||
@@ -21,10 +28,12 @@ function buildProjectCreateData(
|
|||||||
status: input.status,
|
status: input.status,
|
||||||
responsiblePerson: input.responsiblePerson,
|
responsiblePerson: input.responsiblePerson,
|
||||||
...(input.color !== undefined ? { color: input.color } : {}),
|
...(input.color !== undefined ? { color: input.color } : {}),
|
||||||
staffingReqs: input.staffingReqs as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
staffingReqs: input.staffingReqs as unknown as Prisma.InputJsonValue,
|
||||||
dynamicFields: input.dynamicFields as unknown as import("@capakraken/db").Prisma.InputJsonValue,
|
dynamicFields: input.dynamicFields as unknown as Prisma.InputJsonValue,
|
||||||
blueprintId: input.blueprintId,
|
blueprintId: input.blueprintId,
|
||||||
...(input.utilizationCategoryId !== undefined ? { utilizationCategoryId: input.utilizationCategoryId || null } : {}),
|
...(input.utilizationCategoryId !== undefined
|
||||||
|
? { utilizationCategoryId: input.utilizationCategoryId || null }
|
||||||
|
: {}),
|
||||||
...(input.clientId !== undefined ? { clientId: input.clientId || null } : {}),
|
...(input.clientId !== undefined ? { clientId: input.clientId || null } : {}),
|
||||||
} as unknown as Parameters<TRPCContext["db"]["project"]["create"]>[0]["data"];
|
} as unknown as Parameters<TRPCContext["db"]["project"]["create"]>[0]["data"];
|
||||||
}
|
}
|
||||||
@@ -41,24 +50,32 @@ function buildProjectUpdateData(
|
|||||||
...(input.startDate !== undefined ? { startDate: input.startDate } : {}),
|
...(input.startDate !== undefined ? { startDate: input.startDate } : {}),
|
||||||
...(input.endDate !== undefined ? { endDate: input.endDate } : {}),
|
...(input.endDate !== undefined ? { endDate: input.endDate } : {}),
|
||||||
...(input.status !== undefined ? { status: input.status } : {}),
|
...(input.status !== undefined ? { status: input.status } : {}),
|
||||||
...(input.responsiblePerson !== undefined ? { responsiblePerson: input.responsiblePerson } : {}),
|
...(input.responsiblePerson !== undefined
|
||||||
|
? { responsiblePerson: input.responsiblePerson }
|
||||||
|
: {}),
|
||||||
...(input.color !== undefined ? { color: input.color } : {}),
|
...(input.color !== undefined ? { color: input.color } : {}),
|
||||||
...(input.staffingReqs !== undefined ? { staffingReqs: input.staffingReqs as unknown as import("@capakraken/db").Prisma.InputJsonValue } : {}),
|
...(input.staffingReqs !== undefined
|
||||||
...(input.dynamicFields !== undefined ? { dynamicFields: input.dynamicFields as unknown as import("@capakraken/db").Prisma.InputJsonValue } : {}),
|
? { staffingReqs: input.staffingReqs as unknown as Prisma.InputJsonValue }
|
||||||
|
: {}),
|
||||||
|
...(input.dynamicFields !== undefined
|
||||||
|
? { dynamicFields: input.dynamicFields as unknown as Prisma.InputJsonValue }
|
||||||
|
: {}),
|
||||||
...(input.blueprintId !== undefined ? { blueprintId: input.blueprintId } : {}),
|
...(input.blueprintId !== undefined ? { blueprintId: input.blueprintId } : {}),
|
||||||
...(input.utilizationCategoryId !== undefined ? { utilizationCategoryId: input.utilizationCategoryId || null } : {}),
|
...(input.utilizationCategoryId !== undefined
|
||||||
|
? { utilizationCategoryId: input.utilizationCategoryId || null }
|
||||||
|
: {}),
|
||||||
...(input.clientId !== undefined ? { clientId: input.clientId || null } : {}),
|
...(input.clientId !== undefined ? { clientId: input.clientId || null } : {}),
|
||||||
...(input.shoringThreshold !== undefined ? { shoringThreshold: input.shoringThreshold } : {}),
|
...(input.shoringThreshold !== undefined ? { shoringThreshold: input.shoringThreshold } : {}),
|
||||||
...(input.onshoreCountryCode !== undefined ? { onshoreCountryCode: input.onshoreCountryCode } : {}),
|
...(input.onshoreCountryCode !== undefined
|
||||||
|
? { onshoreCountryCode: input.onshoreCountryCode }
|
||||||
|
: {}),
|
||||||
} as unknown as Parameters<TRPCContext["db"]["project"]["update"]>[0]["data"];
|
} as unknown as Parameters<TRPCContext["db"]["project"]["update"]>[0]["data"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createProjectMutationProcedures(
|
export function createProjectMutationProcedures(backgroundEffects: ProjectBackgroundEffects) {
|
||||||
backgroundEffects: ProjectBackgroundEffects,
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
create: managerProcedure
|
create: managerProcedure
|
||||||
.input(CreateProjectSchema)
|
.input(CreateProjectSchema as z.ZodType<CreateProjectInput>)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
|
requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
|
||||||
|
|
||||||
@@ -119,7 +136,9 @@ export function createProjectMutationProcedures(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const nextBlueprintId = input.data.blueprintId ?? existing.blueprintId ?? undefined;
|
const nextBlueprintId = input.data.blueprintId ?? existing.blueprintId ?? undefined;
|
||||||
const nextDynamicFields = (input.data.dynamicFields ?? existing.dynamicFields ?? {}) as Record<string, unknown>;
|
const nextDynamicFields = (input.data.dynamicFields ??
|
||||||
|
existing.dynamicFields ??
|
||||||
|
{}) as Record<string, unknown>;
|
||||||
|
|
||||||
await assertBlueprintDynamicFields({
|
await assertBlueprintDynamicFields({
|
||||||
db: ctx.db,
|
db: ctx.db,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { z } from "zod";
|
||||||
import { projectCostReadProcedures } from "./project-cost-read.js";
|
import { projectCostReadProcedures } from "./project-cost-read.js";
|
||||||
import { projectCoverProcedures } from "./project-cover.js";
|
import { projectCoverProcedures } from "./project-cover.js";
|
||||||
import { projectIdentifierReadProcedures } from "./project-identifier-read.js";
|
import { projectIdentifierReadProcedures } from "./project-identifier-read.js";
|
||||||
@@ -21,13 +22,20 @@ export const projectRouter = createTRPCRouter({
|
|||||||
...projectCoverProcedures,
|
...projectCoverProcedures,
|
||||||
...projectIdentifierReadProcedures,
|
...projectIdentifierReadProcedures,
|
||||||
...createProjectLifecycleProcedures({
|
...createProjectLifecycleProcedures({
|
||||||
invalidateDashboardCacheInBackground: projectBackgroundEffects.invalidateDashboardCacheInBackground,
|
invalidateDashboardCacheInBackground:
|
||||||
|
projectBackgroundEffects.invalidateDashboardCacheInBackground,
|
||||||
dispatchProjectWebhookInBackground: projectBackgroundEffects.dispatchProjectWebhookInBackground,
|
dispatchProjectWebhookInBackground: projectBackgroundEffects.dispatchProjectWebhookInBackground,
|
||||||
}),
|
}),
|
||||||
...createProjectMutationProcedures(projectBackgroundEffects),
|
...createProjectMutationProcedures(projectBackgroundEffects),
|
||||||
|
|
||||||
list: controllerProcedure
|
list: controllerProcedure
|
||||||
.input(ProjectListInputSchema)
|
.input(
|
||||||
|
ProjectListInputSchema as z.ZodType<
|
||||||
|
z.infer<typeof ProjectListInputSchema>,
|
||||||
|
z.ZodTypeDef,
|
||||||
|
z.input<typeof ProjectListInputSchema>
|
||||||
|
>,
|
||||||
|
)
|
||||||
.query(({ ctx, input }) => listProjects(ctx, input)),
|
.query(({ ctx, input }) => listProjects(ctx, input)),
|
||||||
|
|
||||||
getById: controllerProcedure
|
getById: controllerProcedure
|
||||||
@@ -37,5 +45,4 @@ export const projectRouter = createTRPCRouter({
|
|||||||
getShoringRatio: controllerProcedure
|
getShoringRatio: controllerProcedure
|
||||||
.input(ProjectShoringRatioInputSchema)
|
.input(ProjectShoringRatioInputSchema)
|
||||||
.query(({ ctx, input }) => getProjectShoringRatioData(ctx, input)),
|
.query(({ ctx, input }) => getProjectShoringRatioData(ctx, input)),
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
|
import type { z } from "zod";
|
||||||
import { protectedProcedure, resourceOverviewProcedure } from "../trpc.js";
|
import { protectedProcedure, resourceOverviewProcedure } from "../trpc.js";
|
||||||
import {
|
import { ResourceDirectoryQuerySchema, ResourceListQuerySchema } from "./resource-read-shared.js";
|
||||||
ResourceDirectoryQuerySchema,
|
|
||||||
ResourceListQuerySchema,
|
|
||||||
} from "./resource-read-shared.js";
|
|
||||||
import {
|
import {
|
||||||
ResolveResponsiblePersonNameInputSchema,
|
ResolveResponsiblePersonNameInputSchema,
|
||||||
ResourceChargeabilitySummaryInputSchema,
|
ResourceChargeabilitySummaryInputSchema,
|
||||||
@@ -38,7 +36,13 @@ export const resourceSummaryReadProcedures = {
|
|||||||
.query(({ ctx, input }) => listResourceDirectory(ctx, input)),
|
.query(({ ctx, input }) => listResourceDirectory(ctx, input)),
|
||||||
|
|
||||||
listStaff: resourceOverviewProcedure
|
listStaff: resourceOverviewProcedure
|
||||||
.input(ResourceListQuerySchema)
|
.input(
|
||||||
|
ResourceListQuerySchema as z.ZodType<
|
||||||
|
z.infer<typeof ResourceListQuerySchema>,
|
||||||
|
z.ZodTypeDef,
|
||||||
|
z.input<typeof ResourceListQuerySchema>
|
||||||
|
>,
|
||||||
|
)
|
||||||
.query(({ ctx, input }) => listStaffResourceEntries(ctx, input)),
|
.query(({ ctx, input }) => listStaffResourceEntries(ctx, input)),
|
||||||
|
|
||||||
chapters: protectedProcedure.query(({ ctx }) => listResourceChapters(ctx)),
|
chapters: protectedProcedure.query(({ ctx }) => listResourceChapters(ctx)),
|
||||||
|
|||||||
@@ -18,17 +18,23 @@ import {
|
|||||||
toIsoDate,
|
toIsoDate,
|
||||||
} from "./staffing-shared.js";
|
} from "./staffing-shared.js";
|
||||||
|
|
||||||
export const staffingCapacityReadProcedures = {
|
const SearchCapacityInputSchema = z.object({
|
||||||
searchCapacity: planningReadProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
startDate: z.coerce.date(),
|
startDate: z.coerce.date(),
|
||||||
endDate: z.coerce.date(),
|
endDate: z.coerce.date(),
|
||||||
minHoursPerDay: z.number().optional().default(4),
|
minHoursPerDay: z.number().optional().default(4),
|
||||||
roleName: z.string().optional(),
|
roleName: z.string().optional(),
|
||||||
chapter: z.string().optional(),
|
chapter: z.string().optional(),
|
||||||
limit: z.number().int().min(1).max(100).optional().default(20),
|
limit: z.number().int().min(1).max(100).optional().default(20),
|
||||||
}),
|
});
|
||||||
|
|
||||||
|
export const staffingCapacityReadProcedures = {
|
||||||
|
searchCapacity: planningReadProcedure
|
||||||
|
.input(
|
||||||
|
SearchCapacityInputSchema as z.ZodType<
|
||||||
|
z.infer<typeof SearchCapacityInputSchema>,
|
||||||
|
z.ZodTypeDef,
|
||||||
|
z.input<typeof SearchCapacityInputSchema>
|
||||||
|
>,
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const where: Record<string, unknown> = { isActive: true };
|
const where: Record<string, unknown> = { isActive: true };
|
||||||
@@ -107,7 +113,8 @@ export const staffingCapacityReadProcedures = {
|
|||||||
});
|
});
|
||||||
const bookedHours = (bookingsByResourceId.get(resource.id) ?? []).reduce(
|
const bookedHours = (bookingsByResourceId.get(resource.id) ?? []).reduce(
|
||||||
(sum, booking) =>
|
(sum, booking) =>
|
||||||
sum + calculateEffectiveBookedHours({
|
sum +
|
||||||
|
calculateEffectiveBookedHours({
|
||||||
availability,
|
availability,
|
||||||
startDate: booking.startDate,
|
startDate: booking.startDate,
|
||||||
endDate: booking.endDate,
|
endDate: booking.endDate,
|
||||||
@@ -179,7 +186,8 @@ export const staffingCapacityReadProcedures = {
|
|||||||
const availability = resource.availability as unknown as WeekdayAvailability;
|
const availability = resource.availability as unknown as WeekdayAvailability;
|
||||||
const contexts = await loadResourceDailyAvailabilityContexts(
|
const contexts = await loadResourceDailyAvailabilityContexts(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
[{
|
[
|
||||||
|
{
|
||||||
id: resource.id,
|
id: resource.id,
|
||||||
availability,
|
availability,
|
||||||
countryId: resource.countryId,
|
countryId: resource.countryId,
|
||||||
@@ -187,7 +195,8 @@ export const staffingCapacityReadProcedures = {
|
|||||||
federalState: resource.federalState,
|
federalState: resource.federalState,
|
||||||
metroCityId: resource.metroCityId,
|
metroCityId: resource.metroCityId,
|
||||||
metroCityName: resource.metroCity?.name,
|
metroCityName: resource.metroCity?.name,
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
input.startDate,
|
input.startDate,
|
||||||
input.endDate,
|
input.endDate,
|
||||||
);
|
);
|
||||||
@@ -231,9 +240,8 @@ export const staffingCapacityReadProcedures = {
|
|||||||
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentChargeability = totalAvailableHours > 0
|
const currentChargeability =
|
||||||
? (totalChargeableHours / totalAvailableHours) * 100
|
totalAvailableHours > 0 ? (totalChargeableHours / totalAvailableHours) * 100 : 0;
|
||||||
: 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
resourceId: resource.id,
|
resourceId: resource.id,
|
||||||
@@ -291,7 +299,8 @@ export const staffingCapacityReadProcedures = {
|
|||||||
const availability = resource.availability as unknown as WeekdayAvailability;
|
const availability = resource.availability as unknown as WeekdayAvailability;
|
||||||
const contexts = await loadResourceDailyAvailabilityContexts(
|
const contexts = await loadResourceDailyAvailabilityContexts(
|
||||||
ctx.db,
|
ctx.db,
|
||||||
[{
|
[
|
||||||
|
{
|
||||||
id: resource.id,
|
id: resource.id,
|
||||||
availability,
|
availability,
|
||||||
countryId: resource.countryId,
|
countryId: resource.countryId,
|
||||||
@@ -299,7 +308,8 @@ export const staffingCapacityReadProcedures = {
|
|||||||
federalState: resource.federalState,
|
federalState: resource.federalState,
|
||||||
metroCityId: resource.metroCityId,
|
metroCityId: resource.metroCityId,
|
||||||
metroCityName: resource.metroCity?.name,
|
metroCityName: resource.metroCity?.name,
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
input.startDate,
|
input.startDate,
|
||||||
input.endDate,
|
input.endDate,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { rankResources } from "@capakraken/staffing";
|
import { rankResources } from "@capakraken/staffing";
|
||||||
import { listAssignmentBookings } from "@capakraken/application";
|
import { listAssignmentBookings } from "@capakraken/application";
|
||||||
import { PermissionKey, toIsoDateOrNull, type WeekdayAvailability } from "@capakraken/shared";
|
import { PermissionKey, toIsoDateOrNull } from "@capakraken/shared";
|
||||||
|
import type { SkillEntry, WeekdayAvailability } from "@capakraken/shared";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { findUniqueOrThrow } from "../db/helpers.js";
|
import { findUniqueOrThrow } from "../db/helpers.js";
|
||||||
import { loadResourceDailyAvailabilityContexts } from "../lib/resource-capacity.js";
|
import { loadResourceDailyAvailabilityContexts } from "../lib/resource-capacity.js";
|
||||||
@@ -30,10 +31,8 @@ type StaffingSuggestionInput = {
|
|||||||
minProficiency?: number | undefined;
|
minProficiency?: number | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StaffingSuggestionsDbClient =
|
type StaffingSuggestionsDbClient = Parameters<typeof listAssignmentBookings>[0] &
|
||||||
Parameters<typeof listAssignmentBookings>[0]
|
Parameters<typeof loadResourceDailyAvailabilityContexts>[0] & {
|
||||||
& Parameters<typeof loadResourceDailyAvailabilityContexts>[0]
|
|
||||||
& {
|
|
||||||
resource: {
|
resource: {
|
||||||
findMany: (args: Record<string, unknown>) => Promise<unknown[]>;
|
findMany: (args: Record<string, unknown>) => Promise<unknown[]>;
|
||||||
};
|
};
|
||||||
@@ -77,7 +76,7 @@ async function queryStaffingSuggestions(
|
|||||||
mainSkillsOnly,
|
mainSkillsOnly,
|
||||||
minProficiency,
|
minProficiency,
|
||||||
} = input;
|
} = input;
|
||||||
const resources = await db.resource.findMany({
|
const resources = (await db.resource.findMany({
|
||||||
where: {
|
where: {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
...(chapter ? { chapter } : {}),
|
...(chapter ? { chapter } : {}),
|
||||||
@@ -100,7 +99,7 @@ async function queryStaffingSuggestions(
|
|||||||
metroCity: { select: { name: true } },
|
metroCity: { select: { name: true } },
|
||||||
areaRole: { select: { name: true } },
|
areaRole: { select: { name: true } },
|
||||||
},
|
},
|
||||||
}) as StaffingResourceRecord[];
|
})) as StaffingResourceRecord[];
|
||||||
const bookings = await listAssignmentBookings(db, {
|
const bookings = await listAssignmentBookings(db, {
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
@@ -133,7 +132,9 @@ async function queryStaffingSuggestions(
|
|||||||
const availability = resource.availability as unknown as WeekdayAvailability;
|
const availability = resource.availability as unknown as WeekdayAvailability;
|
||||||
const context = contexts.get(resource.id);
|
const context = contexts.get(resource.id);
|
||||||
const resourceBookings = bookingsByResourceId.get(resource.id) ?? [];
|
const resourceBookings = bookingsByResourceId.get(resource.id) ?? [];
|
||||||
const activeBookings = resourceBookings.filter((booking) => ACTIVE_STATUSES.has(booking.status));
|
const activeBookings = resourceBookings.filter((booking) =>
|
||||||
|
ACTIVE_STATUSES.has(booking.status),
|
||||||
|
);
|
||||||
const capacity = buildResourceCapacitySummary({
|
const capacity = buildResourceCapacitySummary({
|
||||||
availability,
|
availability,
|
||||||
periodStart: startDate,
|
periodStart: startDate,
|
||||||
@@ -192,13 +193,17 @@ async function queryStaffingSuggestions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allocatedHours = capacity.bookedHours;
|
const allocatedHours = capacity.bookedHours;
|
||||||
const remainingHours = capacity.remainingHours;
|
|
||||||
const remainingHoursPerDay = capacity.remainingHoursPerDay;
|
const remainingHoursPerDay = capacity.remainingHoursPerDay;
|
||||||
const utilizationPercent =
|
const utilizationPercent =
|
||||||
capacity.availableHours > 0
|
capacity.availableHours > 0
|
||||||
? Math.min(100, (allocatedHours / capacity.availableHours) * 100)
|
? Math.min(100, (allocatedHours / capacity.availableHours) * 100)
|
||||||
: 0;
|
: 0;
|
||||||
type SkillRow = { skill: string; category?: string; proficiency: number; isMainSkill?: boolean };
|
type SkillRow = {
|
||||||
|
skill: string;
|
||||||
|
category?: string;
|
||||||
|
proficiency: number;
|
||||||
|
isMainSkill?: boolean;
|
||||||
|
};
|
||||||
let skills = resource.skills as unknown as SkillRow[];
|
let skills = resource.skills as unknown as SkillRow[];
|
||||||
if (mainSkillsOnly) {
|
if (mainSkillsOnly) {
|
||||||
skills = skills.filter((skill) => skill.isMainSkill);
|
skills = skills.filter((skill) => skill.isMainSkill);
|
||||||
@@ -217,7 +222,7 @@ async function queryStaffingSuggestions(
|
|||||||
fte: resource.fte,
|
fte: resource.fte,
|
||||||
chapter: resource.chapter,
|
chapter: resource.chapter,
|
||||||
role: resource.areaRole?.name ?? null,
|
role: resource.areaRole?.name ?? null,
|
||||||
skills: skills as unknown as import("@capakraken/shared").SkillEntry[],
|
skills: skills as unknown as SkillEntry[],
|
||||||
lcrCents: resource.lcrCents,
|
lcrCents: resource.lcrCents,
|
||||||
chargeabilityTarget: resource.chargeabilityTarget,
|
chargeabilityTarget: resource.chargeabilityTarget,
|
||||||
currentUtilizationPercent: utilizationPercent,
|
currentUtilizationPercent: utilizationPercent,
|
||||||
@@ -267,17 +272,31 @@ async function queryStaffingSuggestions(
|
|||||||
budgetLcrCentsPerHour,
|
budgetLcrCentsPerHour,
|
||||||
} as unknown as Parameters<typeof rankResources>[0]);
|
} as unknown as Parameters<typeof rankResources>[0]);
|
||||||
const baseRankIndex = new Map(ranked.map((suggestion, index) => [suggestion.resourceId, index]));
|
const baseRankIndex = new Map(ranked.map((suggestion, index) => [suggestion.resourceId, index]));
|
||||||
return [...ranked].sort((left, right) => {
|
return [...ranked]
|
||||||
|
.sort((left, right) => {
|
||||||
if (Math.abs(left.score - right.score) <= 2) {
|
if (Math.abs(left.score - right.score) <= 2) {
|
||||||
const leftValue = enrichedResources.find((resource) => resource.id === left.resourceId)?.valueScore ?? 0;
|
const leftValue =
|
||||||
const rightValue = enrichedResources.find((resource) => resource.id === right.resourceId)?.valueScore ?? 0;
|
enrichedResources.find((resource) => resource.id === left.resourceId)?.valueScore ?? 0;
|
||||||
|
const rightValue =
|
||||||
|
enrichedResources.find((resource) => resource.id === right.resourceId)?.valueScore ?? 0;
|
||||||
return rightValue - leftValue;
|
return rightValue - leftValue;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}).map((suggestion, index) => {
|
})
|
||||||
|
.map((suggestion, index) => {
|
||||||
const resource = enrichedResources.find((entry) => entry.id === suggestion.resourceId);
|
const resource = enrichedResources.find((entry) => entry.id === suggestion.resourceId);
|
||||||
const fallbackBreakdown = "breakdown" in suggestion
|
const fallbackBreakdown =
|
||||||
? (suggestion as { breakdown?: { skillScore: number; availabilityScore: number; costScore: number; utilizationScore: number } }).breakdown
|
"breakdown" in suggestion
|
||||||
|
? (
|
||||||
|
suggestion as {
|
||||||
|
breakdown?: {
|
||||||
|
skillScore: number;
|
||||||
|
availabilityScore: number;
|
||||||
|
costScore: number;
|
||||||
|
utilizationScore: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
).breakdown
|
||||||
: undefined;
|
: undefined;
|
||||||
const scoreBreakdown = suggestion.scoreBreakdown ?? {
|
const scoreBreakdown = suggestion.scoreBreakdown ?? {
|
||||||
skillScore: fallbackBreakdown?.skillScore ?? 0,
|
skillScore: fallbackBreakdown?.skillScore ?? 0,
|
||||||
@@ -297,15 +316,26 @@ async function queryStaffingSuggestions(
|
|||||||
chapter: resource?.chapter ?? null,
|
chapter: resource?.chapter ?? null,
|
||||||
role: resource?.role ?? null,
|
role: resource?.role ?? null,
|
||||||
scoreBreakdown,
|
scoreBreakdown,
|
||||||
matchedSkills: suggestion.matchedSkills ?? requiredSkills.filter((skill) =>
|
matchedSkills:
|
||||||
resource?.skills.some((entry) => entry.skill.toLowerCase() === skill.trim().toLowerCase()),
|
suggestion.matchedSkills ??
|
||||||
|
requiredSkills.filter((skill) =>
|
||||||
|
resource?.skills.some(
|
||||||
|
(entry) => entry.skill.toLowerCase() === skill.trim().toLowerCase(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
missingSkills:
|
||||||
|
suggestion.missingSkills ??
|
||||||
|
requiredSkills.filter(
|
||||||
|
(skill) =>
|
||||||
|
!resource?.skills.some(
|
||||||
|
(entry) => entry.skill.toLowerCase() === skill.trim().toLowerCase(),
|
||||||
),
|
),
|
||||||
missingSkills: suggestion.missingSkills ?? requiredSkills.filter((skill) =>
|
|
||||||
!resource?.skills.some((entry) => entry.skill.toLowerCase() === skill.trim().toLowerCase()),
|
|
||||||
),
|
),
|
||||||
availabilityConflicts: suggestion.availabilityConflicts ?? resource?.conflictDays ?? [],
|
availabilityConflicts: suggestion.availabilityConflicts ?? resource?.conflictDays ?? [],
|
||||||
estimatedDailyCostCents: suggestion.estimatedDailyCostCents ?? ((resource?.lcrCents ?? 0) * 8),
|
estimatedDailyCostCents:
|
||||||
currentUtilization: suggestion.currentUtilization ?? round1(resource?.currentUtilizationPercent ?? 0),
|
suggestion.estimatedDailyCostCents ?? (resource?.lcrCents ?? 0) * 8,
|
||||||
|
currentUtilization:
|
||||||
|
suggestion.currentUtilization ?? round1(resource?.currentUtilizationPercent ?? 0),
|
||||||
valueScore: resource?.valueScore ?? 0,
|
valueScore: resource?.valueScore ?? 0,
|
||||||
location: resource?.transparency.location ?? {
|
location: resource?.transparency.location ?? {
|
||||||
countryCode: null,
|
countryCode: null,
|
||||||
@@ -345,9 +375,17 @@ async function queryStaffingSuggestions(
|
|||||||
model: "Composite ranking across skill fit, availability, cost, and utilization.",
|
model: "Composite ranking across skill fit, availability, cost, and utilization.",
|
||||||
components: [
|
components: [
|
||||||
{ key: "skillScore", label: "Skills", score: scoreBreakdown.skillScore },
|
{ key: "skillScore", label: "Skills", score: scoreBreakdown.skillScore },
|
||||||
{ key: "availabilityScore", label: "Availability", score: scoreBreakdown.availabilityScore },
|
{
|
||||||
|
key: "availabilityScore",
|
||||||
|
label: "Availability",
|
||||||
|
score: scoreBreakdown.availabilityScore,
|
||||||
|
},
|
||||||
{ key: "costScore", label: "Cost", score: scoreBreakdown.costScore },
|
{ key: "costScore", label: "Cost", score: scoreBreakdown.costScore },
|
||||||
{ key: "utilizationScore", label: "Utilization", score: scoreBreakdown.utilizationScore },
|
{
|
||||||
|
key: "utilizationScore",
|
||||||
|
label: "Utilization",
|
||||||
|
score: scoreBreakdown.utilizationScore,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
remainingHoursPerDay: resource?.transparency.capacity.remainingHoursPerDay ?? 0,
|
remainingHoursPerDay: resource?.transparency.capacity.remainingHoursPerDay ?? 0,
|
||||||
@@ -358,6 +396,14 @@ async function queryStaffingSuggestions(
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const GetProjectStaffingSuggestionsInputSchema = z.object({
|
||||||
|
projectId: z.string().min(1),
|
||||||
|
roleName: z.string().optional(),
|
||||||
|
startDate: z.coerce.date().optional(),
|
||||||
|
endDate: z.coerce.date().optional(),
|
||||||
|
limit: z.number().int().min(1).max(50).optional().default(5),
|
||||||
|
});
|
||||||
|
|
||||||
export const staffingSuggestionsReadProcedures = {
|
export const staffingSuggestionsReadProcedures = {
|
||||||
getSuggestions: planningReadProcedure
|
getSuggestions: planningReadProcedure
|
||||||
.input(
|
.input(
|
||||||
@@ -380,17 +426,16 @@ export const staffingSuggestionsReadProcedures = {
|
|||||||
}),
|
}),
|
||||||
getProjectStaffingSuggestions: planningReadProcedure
|
getProjectStaffingSuggestions: planningReadProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
GetProjectStaffingSuggestionsInputSchema as z.ZodType<
|
||||||
projectId: z.string().min(1),
|
z.infer<typeof GetProjectStaffingSuggestionsInputSchema>,
|
||||||
roleName: z.string().optional(),
|
z.ZodTypeDef,
|
||||||
startDate: z.coerce.date().optional(),
|
z.input<typeof GetProjectStaffingSuggestionsInputSchema>
|
||||||
endDate: z.coerce.date().optional(),
|
>,
|
||||||
limit: z.number().int().min(1).max(50).optional().default(5),
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.VIEW_COSTS);
|
requirePermission(ctx, PermissionKey.VIEW_COSTS);
|
||||||
const project = await findUniqueOrThrow(ctx.db.project.findUnique({
|
const project = await findUniqueOrThrow(
|
||||||
|
ctx.db.project.findUnique({
|
||||||
where: { id: input.projectId },
|
where: { id: input.projectId },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -399,16 +444,21 @@ export const staffingSuggestionsReadProcedures = {
|
|||||||
startDate: true,
|
startDate: true,
|
||||||
endDate: true,
|
endDate: true,
|
||||||
},
|
},
|
||||||
}), "Project");
|
}),
|
||||||
|
"Project",
|
||||||
|
);
|
||||||
const startDate = input.startDate ?? project.startDate ?? new Date();
|
const startDate = input.startDate ?? project.startDate ?? new Date();
|
||||||
const endDate = input.endDate ?? project.endDate ?? new Date();
|
const endDate = input.endDate ?? project.endDate ?? new Date();
|
||||||
const normalizedRoleFilter = input.roleName?.trim().toLowerCase();
|
const normalizedRoleFilter = input.roleName?.trim().toLowerCase();
|
||||||
const suggestions = await queryStaffingSuggestions(ctx.db as unknown as StaffingSuggestionsDbClient, {
|
const suggestions = await queryStaffingSuggestions(
|
||||||
|
ctx.db as unknown as StaffingSuggestionsDbClient,
|
||||||
|
{
|
||||||
requiredSkills: [],
|
requiredSkills: [],
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
hoursPerDay: 8,
|
hoursPerDay: 8,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
project: `${project.name} (${project.shortCode})`,
|
project: `${project.name} (${project.shortCode})`,
|
||||||
period: `${toIsoDateOrNull(startDate)} to ${toIsoDateOrNull(endDate)}`,
|
period: `${toIsoDateOrNull(startDate)} to ${toIsoDateOrNull(endDate)}`,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { z } from "zod";
|
||||||
import { adminProcedure, createTRPCRouter } from "../trpc.js";
|
import { adminProcedure, createTRPCRouter } from "../trpc.js";
|
||||||
import {
|
import {
|
||||||
listSystemRoleConfigs,
|
listSystemRoleConfigs,
|
||||||
@@ -11,6 +12,10 @@ export const systemRoleConfigRouter = createTRPCRouter({
|
|||||||
|
|
||||||
/** Update a role's default permissions, label, description, and color */
|
/** Update a role's default permissions, label, description, and color */
|
||||||
update: adminProcedure
|
update: adminProcedure
|
||||||
.input(systemRoleConfigUpdateInputSchema)
|
.input(
|
||||||
|
systemRoleConfigUpdateInputSchema as z.ZodType<
|
||||||
|
z.infer<typeof systemRoleConfigUpdateInputSchema>
|
||||||
|
>,
|
||||||
|
)
|
||||||
.mutation(({ ctx, input }) => updateSystemRoleConfig(ctx, input)),
|
.mutation(({ ctx, input }) => updateSystemRoleConfig(ctx, input)),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { PermissionKey, ShiftProjectSchema } from "@capakraken/shared";
|
import { PermissionKey, ShiftProjectSchema } from "@capakraken/shared";
|
||||||
|
import type { ShiftProjectInput } from "@capakraken/shared";
|
||||||
|
import type { z } from "zod";
|
||||||
import { managerProcedure, requirePermission } from "../trpc.js";
|
import { managerProcedure, requirePermission } from "../trpc.js";
|
||||||
import { timelineAllocationMutationProcedures } from "./timeline-allocation-mutations.js";
|
import { timelineAllocationMutationProcedures } from "./timeline-allocation-mutations.js";
|
||||||
import { applyTimelineProjectShiftMutation } from "./timeline-shift-router-support.js";
|
import { applyTimelineProjectShiftMutation } from "./timeline-shift-router-support.js";
|
||||||
@@ -7,7 +9,7 @@ export const timelineMutationProcedures = {
|
|||||||
...timelineAllocationMutationProcedures,
|
...timelineAllocationMutationProcedures,
|
||||||
|
|
||||||
applyShift: managerProcedure
|
applyShift: managerProcedure
|
||||||
.input(ShiftProjectSchema)
|
.input(ShiftProjectSchema as z.ZodType<ShiftProjectInput>)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { adminProcedure, createTRPCRouter, managerProcedure, protectedProcedure, publicProcedure } from "../trpc.js";
|
import type { z } from "zod";
|
||||||
|
import {
|
||||||
|
adminProcedure,
|
||||||
|
createTRPCRouter,
|
||||||
|
managerProcedure,
|
||||||
|
protectedProcedure,
|
||||||
|
publicProcedure,
|
||||||
|
} from "../trpc.js";
|
||||||
import {
|
import {
|
||||||
autoLinkUsersByEmail,
|
autoLinkUsersByEmail,
|
||||||
countActiveUsers,
|
countActiveUsers,
|
||||||
@@ -92,7 +99,9 @@ export const userRouter = createTRPCRouter({
|
|||||||
.mutation(({ ctx, input }) => toggleFavoriteProject(ctx, input)),
|
.mutation(({ ctx, input }) => toggleFavoriteProject(ctx, input)),
|
||||||
|
|
||||||
setPermissions: adminProcedure
|
setPermissions: adminProcedure
|
||||||
.input(SetUserPermissionsInputSchema)
|
.input(
|
||||||
|
SetUserPermissionsInputSchema as z.ZodType<z.infer<typeof SetUserPermissionsInputSchema>>,
|
||||||
|
)
|
||||||
.mutation(({ ctx, input }) => setUserPermissions(ctx, input)),
|
.mutation(({ ctx, input }) => setUserPermissions(ctx, input)),
|
||||||
|
|
||||||
resetPermissions: adminProcedure
|
resetPermissions: adminProcedure
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
||||||
|
import type { z } from "zod";
|
||||||
import {
|
import {
|
||||||
createVacationRequest,
|
createVacationRequest,
|
||||||
CreateVacationRequestSchema,
|
CreateVacationRequestSchema,
|
||||||
|
type CreateVacationRequestInput,
|
||||||
} from "./vacation-create-support.js";
|
} from "./vacation-create-support.js";
|
||||||
import { vacationManagementProcedures } from "./vacation-management-procedures.js";
|
import { vacationManagementProcedures } from "./vacation-management-procedures.js";
|
||||||
import { vacationReadProcedures } from "./vacation-read.js";
|
import { vacationReadProcedures } from "./vacation-read.js";
|
||||||
@@ -11,6 +13,6 @@ export const vacationRouter = createTRPCRouter({
|
|||||||
...vacationManagementProcedures,
|
...vacationManagementProcedures,
|
||||||
|
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(CreateVacationRequestSchema)
|
.input(CreateVacationRequestSchema as z.ZodType<CreateVacationRequestInput>)
|
||||||
.mutation(({ ctx, input }) => createVacationRequest(ctx, input)),
|
.mutation(({ ctx, input }) => createVacationRequest(ctx, input)),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user